Repository: fujiapple852/trippy Branch: master Commit: e12e49159e93 Files: 257 Total size: 1.6 MB Directory structure: gitextract_swvmlbvf/ ├── .config/ │ ├── spellcheck.toml │ └── trippy.dic ├── .devcontainer/ │ └── devcontainer.json ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── AGENTS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── RELEASES.md ├── crates/ │ ├── README.md │ ├── trippy/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── lib.rs │ │ └── main.rs │ ├── trippy-core/ │ │ ├── Cargo.toml │ │ ├── src/ │ │ │ ├── builder.rs │ │ │ ├── config.rs │ │ │ ├── constants.rs │ │ │ ├── error.rs │ │ │ ├── flows.rs │ │ │ ├── lib.rs │ │ │ ├── net/ │ │ │ │ ├── channel.rs │ │ │ │ ├── common.rs │ │ │ │ ├── extension.rs │ │ │ │ ├── ipv4.rs │ │ │ │ ├── ipv6.rs │ │ │ │ ├── platform/ │ │ │ │ │ ├── byte_order.rs │ │ │ │ │ ├── unix.rs │ │ │ │ │ └── windows.rs │ │ │ │ ├── platform.rs │ │ │ │ ├── socket.rs │ │ │ │ └── source.rs │ │ │ ├── net.rs │ │ │ ├── probe.rs │ │ │ ├── state.rs │ │ │ ├── strategy.rs │ │ │ ├── tracer.rs │ │ │ └── types.rs │ │ └── tests/ │ │ ├── resources/ │ │ │ ├── simulation/ │ │ │ │ ├── ipv4_icmp.toml │ │ │ │ ├── ipv4_icmp_gaps.toml │ │ │ │ ├── ipv4_icmp_min.toml │ │ │ │ ├── ipv4_icmp_ooo.toml │ │ │ │ ├── ipv4_icmp_pattern.toml │ │ │ │ ├── ipv4_icmp_quick.toml │ │ │ │ ├── ipv4_icmp_tos.toml │ │ │ │ ├── ipv4_icmp_wrap.toml │ │ │ │ ├── ipv4_tcp_fixed_dest.toml │ │ │ │ ├── ipv4_udp_classic_fixed_dest.toml │ │ │ │ ├── ipv4_udp_classic_fixed_src.toml │ │ │ │ ├── ipv4_udp_classic_privileged_tos.toml │ │ │ │ ├── ipv4_udp_classic_unprivileged.toml │ │ │ │ ├── ipv4_udp_classic_unprivileged_tos.toml │ │ │ │ ├── ipv4_udp_dublin_fixed_both.toml │ │ │ │ ├── ipv4_udp_paris_fixed_both.toml │ │ │ │ ├── ipv6_icmp.toml │ │ │ │ ├── ipv6_icmp_min.toml │ │ │ │ ├── ipv6_icmp_pattern.toml │ │ │ │ ├── ipv6_tcp_fixed_dest.toml │ │ │ │ ├── ipv6_udp_classic_fixed_dest.toml │ │ │ │ ├── ipv6_udp_classic_fixed_src.toml │ │ │ │ ├── ipv6_udp_classic_unprivileged.toml │ │ │ │ ├── ipv6_udp_classic_unprivileged_tos.toml │ │ │ │ ├── ipv6_udp_dublin_fixed_both.toml │ │ │ │ └── ipv6_udp_paris_fixed_both.toml │ │ │ └── state/ │ │ │ ├── all_status.toml │ │ │ ├── floss_bloss.toml │ │ │ ├── full_completed.toml │ │ │ ├── full_mixed.toml │ │ │ ├── minimal.toml │ │ │ ├── nat.toml │ │ │ ├── no_latency.toml │ │ │ ├── non_default_minimum_ttl.toml │ │ │ └── tos.toml │ │ └── sim/ │ │ ├── main.rs │ │ ├── network/ │ │ │ ├── ipv4.rs │ │ │ └── ipv6.rs │ │ ├── network.rs │ │ ├── simulation.rs │ │ ├── tests.rs │ │ ├── tracer.rs │ │ └── tun_device.rs │ ├── trippy-dns/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── config.rs │ │ ├── lazy_resolver.rs │ │ ├── lib.rs │ │ └── resolver.rs │ ├── trippy-packet/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── buffer.rs │ │ ├── checksum.rs │ │ ├── error.rs │ │ ├── icmp_extension.rs │ │ ├── icmpv4.rs │ │ ├── icmpv6.rs │ │ ├── ip.rs │ │ ├── ipv4.rs │ │ ├── ipv6.rs │ │ ├── lib.rs │ │ ├── tcp.rs │ │ └── udp.rs │ ├── trippy-privilege/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ └── trippy-tui/ │ ├── Cargo.toml │ ├── build.rs │ ├── locales.toml │ ├── src/ │ │ ├── app.rs │ │ ├── config/ │ │ │ ├── binding.rs │ │ │ ├── cmd.rs │ │ │ ├── columns.rs │ │ │ ├── constants.rs │ │ │ ├── file.rs │ │ │ └── theme.rs │ │ ├── config.rs │ │ ├── frontend/ │ │ │ ├── binding.rs │ │ │ ├── columns.rs │ │ │ ├── config.rs │ │ │ ├── render/ │ │ │ │ ├── app.rs │ │ │ │ ├── bar.rs │ │ │ │ ├── body.rs │ │ │ │ ├── bsod.rs │ │ │ │ ├── chart.rs │ │ │ │ ├── flows.rs │ │ │ │ ├── footer.rs │ │ │ │ ├── header.rs │ │ │ │ ├── help.rs │ │ │ │ ├── histogram.rs │ │ │ │ ├── history.rs │ │ │ │ ├── settings.rs │ │ │ │ ├── splash.rs │ │ │ │ ├── table.rs │ │ │ │ ├── tabs.rs │ │ │ │ ├── util.rs │ │ │ │ └── world.rs │ │ │ ├── render.rs │ │ │ ├── theme.rs │ │ │ └── tui_app.rs │ │ ├── frontend.rs │ │ ├── geoip.rs │ │ ├── lib.rs │ │ ├── locale.rs │ │ ├── print.rs │ │ ├── report/ │ │ │ ├── csv.rs │ │ │ ├── dot.rs │ │ │ ├── flows.rs │ │ │ ├── json.rs │ │ │ ├── silent.rs │ │ │ ├── stream.rs │ │ │ ├── table.rs │ │ │ └── types.rs │ │ ├── report.rs │ │ └── util.rs │ └── tests/ │ └── resources/ │ └── snapshots/ │ ├── trippy_tui__config__tests__compare_snapshot@trip.snap │ ├── trippy_tui__config__tests__compare_snapshot@trip_--help.snap │ ├── trippy_tui__config__tests__compare_snapshot@trip_-h.snap │ ├── trippy_tui__print__tests__output@generate_bash_shell_completions.snap │ ├── trippy_tui__print__tests__output@generate_elvish_shell_completions.snap │ ├── trippy_tui__print__tests__output@generate_fish_shell_completions.snap │ ├── trippy_tui__print__tests__output@generate_man_page.snap │ ├── trippy_tui__print__tests__output@generate_powershell_shell_completions.snap │ ├── trippy_tui__print__tests__output@generate_zsh_shell_completions.snap │ ├── trippy_tui__print__tests__output@tui_binding_commands_match.snap │ └── trippy_tui__print__tests__output@tui_theme_items_match.snap ├── deny.toml ├── docs/ │ ├── .gitignore │ ├── README.md │ ├── astro.config.mjs │ ├── package.json │ ├── public/ │ │ └── CNAME │ ├── src/ │ │ ├── content/ │ │ │ ├── config.ts │ │ │ ├── docs/ │ │ │ │ ├── 0.12.2/ │ │ │ │ │ ├── development/ │ │ │ │ │ │ └── crates.md │ │ │ │ │ ├── guides/ │ │ │ │ │ │ ├── faq.md │ │ │ │ │ │ ├── privileges.md │ │ │ │ │ │ ├── recommendation.md │ │ │ │ │ │ ├── usage.md │ │ │ │ │ │ └── windows_firewall.md │ │ │ │ │ ├── index.mdx │ │ │ │ │ ├── reference/ │ │ │ │ │ │ ├── bindings.md │ │ │ │ │ │ ├── cli.md │ │ │ │ │ │ ├── column.md │ │ │ │ │ │ ├── configuration.md │ │ │ │ │ │ ├── locale.md │ │ │ │ │ │ ├── theme.md │ │ │ │ │ │ └── version.md │ │ │ │ │ └── start/ │ │ │ │ │ ├── features.md │ │ │ │ │ ├── getting-started.mdx │ │ │ │ │ └── installation.md │ │ │ │ ├── 0.13.0/ │ │ │ │ │ ├── development/ │ │ │ │ │ │ └── crates.md │ │ │ │ │ ├── guides/ │ │ │ │ │ │ ├── faq.md │ │ │ │ │ │ ├── privileges.md │ │ │ │ │ │ ├── recommendation.md │ │ │ │ │ │ ├── usage.md │ │ │ │ │ │ └── windows_firewall.md │ │ │ │ │ ├── index.mdx │ │ │ │ │ ├── reference/ │ │ │ │ │ │ ├── bindings.md │ │ │ │ │ │ ├── cli.md │ │ │ │ │ │ ├── column.md │ │ │ │ │ │ ├── configuration.md │ │ │ │ │ │ ├── locale.md │ │ │ │ │ │ ├── overview.mdx │ │ │ │ │ │ ├── theme.md │ │ │ │ │ │ └── version.md │ │ │ │ │ └── start/ │ │ │ │ │ ├── features.md │ │ │ │ │ ├── getting-started.mdx │ │ │ │ │ └── installation.md │ │ │ │ ├── development/ │ │ │ │ │ └── crates.md │ │ │ │ ├── guides/ │ │ │ │ │ ├── docker.md │ │ │ │ │ ├── faq.md │ │ │ │ │ ├── privileges.md │ │ │ │ │ ├── recommendation.md │ │ │ │ │ ├── usage.md │ │ │ │ │ └── windows_firewall.md │ │ │ │ ├── index.mdx │ │ │ │ ├── reference/ │ │ │ │ │ ├── bindings.md │ │ │ │ │ ├── cli.md │ │ │ │ │ ├── column.md │ │ │ │ │ ├── configuration.md │ │ │ │ │ ├── locale.md │ │ │ │ │ ├── overview.mdx │ │ │ │ │ ├── theme.md │ │ │ │ │ └── version.md │ │ │ │ └── start/ │ │ │ │ ├── features.md │ │ │ │ ├── getting-started.mdx │ │ │ │ └── installation.md │ │ │ └── versions/ │ │ │ ├── 0.12.2.json │ │ │ └── 0.13.0.json │ │ ├── env.d.ts │ │ └── styles/ │ │ └── custom.css │ └── tsconfig.json ├── dprint.json ├── examples/ │ ├── README.md │ ├── hello-world/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ └── toy-traceroute/ │ ├── Cargo.toml │ └── src/ │ └── main.rs ├── snap/ │ └── snapcraft.yaml ├── trippy-config-sample.toml └── ubuntu-ppa/ ├── Dockerfile ├── README.debian ├── README.md ├── cargo.config ├── changelog ├── control ├── copyright ├── release.sh ├── rules ├── source/ │ ├── format │ └── include-binaries ├── trippy.docs └── trippy.install ================================================ FILE CONTENTS ================================================ ================================================ FILE: .config/spellcheck.toml ================================================ dev_comments = true skip_readme = false [Hunspell] lang = "en_US" search_dirs = ["."] extra_dictionaries = ["trippy.dic"] skip_os_lookups = false use_builtin = true ================================================ FILE: .config/trippy.dic ================================================ 100 % ' + 100ms 10ms 1s 300s 5s = > ASN BSD4 CSV Cloudflare DF DSCP ECMP ECN Endianness FreeBSD GeoIp Geolocation Graphviz IANA ICMPv4 ICMPv6 IPinfo IPs IPv4 IPv6 MaxMind NAT'ed Num ROFF RTT TBD TODO TOS TXT Trippy Tui XDG accessor addr addrs asn boolean calc checksum checksums cidr cloneable config connectionless datagram dec deserialization dest dns dublin endianness enqueue enqueued enqueuing frontend geolocation getsockname holsravbwdt hostname hostnames icmp impl ip ipv6 jitter json localhost lookups macOS mmdb mpls multipath newtype paris rfc1889 rfc2460 rfc2474 rfc2475 rfc2476 rfc3168 rfc3246 rfc3550 rfc4443 rfc4884 rfc5865 rfc8622 src stddev struct submodule syscall tcp timestamp toml traceroute trippy ttl tui tuple u8 udp uninitialised unix unselected ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "image": "mcr.microsoft.com/devcontainers/universal:2", "features": { "ghcr.io/devcontainers/features/rust:1": {} } } ================================================ FILE: .github/FUNDING.yml ================================================ github: fujiapple852 buy_me_a_coffee: fujiapple852 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a bug report title: '' labels: triage assignees: '' --- **Describe the bug** **To Reproduce** **Expected behavior** **Screenshots** **Environment Info** - OS: - Trippy version: - Installation method: - Terminal / Console: **Additional context** ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: triage assignees: '' --- **Describe the feature you'd like** **Describe alternatives you've considered** **Additional context** ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" ignore: - dependency-name: "clap*" update-types: - "version-update:semver-patch" - dependency-name: "serde*" update-types: - "version-update:semver-patch" - dependency-name: "anyhow" update-types: - "version-update:semver-patch" - dependency-name: "thiserror" update-types: - "version-update:semver-patch" allow: - dependency-type: "direct" open-pull-requests-limit: 10 rebase-strategy: "disabled" ================================================ FILE: .github/workflows/ci.yml ================================================ on: pull_request: branches: [ master ] schedule: - cron: '00 18 * * *' name: CI jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: build: - linux-stable - linux-musl-stable - linux-beta - linux-nightly - macos-stable - macos-stable-arm64 - windows-stable include: - build: linux-stable os: ubuntu-22.04 target: x86_64-unknown-linux-gnu rust: stable - build: linux-musl-stable os: ubuntu-22.04 target: x86_64-unknown-linux-musl rust: stable - build: linux-beta os: ubuntu-22.04 target: x86_64-unknown-linux-gnu rust: beta - build: linux-nightly os: ubuntu-22.04 target: x86_64-unknown-linux-gnu rust: nightly - build: macos-stable os: macos-15-intel target: x86_64-apple-darwin rust: stable - build: macos-stable-arm64 os: macos-latest target: aarch64-apple-darwin rust: stable - build: windows-stable os: windows-2022 target: x86_64-pc-windows-msvc rust: stable steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - run: cargo test --target ${{ matrix.target }} build-cross: runs-on: ${{ matrix.os }} strategy: matrix: build: [ netbsd, freebsd ] include: - build: netbsd os: ubuntu-22.04 target: x86_64-unknown-netbsd rust: stable - build: freebsd os: ubuntu-22.04 target: x86_64-unknown-freebsd rust: stable steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - name: Use Cross shell: bash run: | cargo install cross --git https://github.com/cross-rs/cross - name: Show command used for Cargo run: | echo "cargo command is: ${{ env.CARGO }}" echo "target flag is: ${{ env.TARGET_FLAGS }}" - name: cross build run: cross build --target ${{ matrix.target }} --verbose sim-test: runs-on: ${{ matrix.os }} strategy: matrix: build: - linux-stable - macos-stable - windows-stable include: - build: linux-stable os: ubuntu-22.04 target: x86_64-unknown-linux-gnu rust: stable - build: macos-stable os: macos-15-intel target: x86_64-apple-darwin rust: stable - build: windows-stable os: windows-2022 target: x86_64-pc-windows-msvc rust: stable steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} - uses: Swatinem/rust-cache@v2 - name: Copy wintun.dll to current dir if: startsWith(matrix.build, 'windows') shell: bash # The simulation tests run from the crates/trippy-core directory and so `wintun.dll` needs to be copied there run: | cp "crates/trippy-core/tests/resources/wintun.dll" "./crates/trippy-core/" - name: Allow ICMPv4 and ICMPv6 in Windows defender firewall if: startsWith(matrix.build, 'windows') shell: pwsh run: | New-NetFirewallRule -DisplayName "ICMPv4 Trippy Allow" -Name ICMPv4_TRIPPY_ALLOW -Protocol ICMPv4 -Action Allow New-NetFirewallRule -DisplayName "ICMPv6 Trippy Allow" -Name ICMPv6_TRIPPY_ALLOW -Protocol ICMPv6 -Action Allow - name: Build (without root) run: cargo build --target ${{ matrix.target }} --features sim-tests --test sim - name: Run simulation test on ${{ matrix.build }} if: ${{ ! startsWith(matrix.build, 'windows') }} run: sudo -E env "PATH=$PATH" cargo test --target ${{ matrix.target }} --features sim-tests --test sim -- --exact --nocapture - name: Run simulation test on ${{ matrix.build }} if: startsWith(matrix.build, 'windows') run: cargo test --target ${{ matrix.target }} --features sim-tests --test sim -- --exact --nocapture fmt: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable components: rustfmt - uses: Swatinem/rust-cache@v2 - run: cargo fmt --all -- --check clippy: runs-on: ${{ matrix.os }} strategy: matrix: build: - linux-stable - macos-stable - windows-stable include: - build: linux-stable os: ubuntu-22.04 target: x86_64-unknown-linux-gnu rust: stable - build: macos-stable os: macos-15-intel target: x86_64-apple-darwin rust: stable - build: windows-stable os: windows-2022 target: x86_64-pc-windows-msvc rust: stable steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} components: clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy --workspace --all-features --target ${{ matrix.target }} --tests -- -Dwarnings build-docker: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Build Docker image run: docker build -t trippy-docker-image . cargo-deny: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v2 with: rust-version: "1.87.0" log-level: warn command: check arguments: --all-features command-arguments: "--hide-inclusion-graph" cargo-msrv: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: install cargo-msrv run: cargo install --git https://github.com/foresterre/cargo-msrv.git cargo-msrv - name: check msrv for trippy run: cargo msrv verify --output-format json --manifest-path crates/trippy/Cargo.toml -- cargo check - name: check msrv for trippy-tui run: cargo msrv verify --output-format json --manifest-path crates/trippy-tui/Cargo.toml -- cargo check - name: check msrv for trippy-core run: cargo msrv verify --output-format json --manifest-path crates/trippy-core/Cargo.toml -- cargo check - name: check msrv for trippy-packet run: cargo msrv verify --output-format json --manifest-path crates/trippy-packet/Cargo.toml -- cargo check - name: check msrv for trippy-dns run: cargo msrv verify --output-format json --manifest-path crates/trippy-dns/Cargo.toml -- cargo check - name: check msrv for trippy-privilege run: cargo msrv verify --output-format json --manifest-path crates/trippy-privilege/Cargo.toml -- cargo check style: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: dprint/check@v2.2 conventional-commits: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Conventional Commits Lint uses: webiny/action-conventional-commits@v1.3.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} allowed-commit-types: "feat,fix,chore,docs,style,refactor,test,build,ci,revert" spelling: runs-on: ubuntu-22.04 steps: - name: Install cargo-spellcheck uses: taiki-e/install-action@v2 with: tool: cargo-spellcheck - uses: actions/checkout@v4 - name: Run cargo-spellcheck run: cargo spellcheck --code 1 ================================================ FILE: .github/workflows/deploy.yml ================================================ on: push: branches: [ master ] permissions: contents: read pages: write id-token: write jobs: build: runs-on: ubuntu-latest steps: - name: Checkout your repository using git uses: actions/checkout@v4 - name: Install, build, and upload your site uses: withastro/action@v3 with: path: docs deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: push: tags: - "[0-9]+.[0-9]+.[0-9]+" jobs: create-release: name: create-release runs-on: ubuntu-latest outputs: upload_url: ${{ steps.release.outputs.upload_url }} trip_version: ${{ env.TRIP_VERSION }} steps: - name: Get the release version from the tag shell: bash if: env.TRIP_VERSION == '' run: | echo "TRIP_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV echo "version is: ${{ env.TRIP_VERSION }}" - name: Create GitHub release id: release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ env.TRIP_VERSION }} release_name: Trippy ${{ env.TRIP_VERSION }} body: See [CHANGELOG.md](https://github.com/fujiapple852/trippy/blob/master/CHANGELOG.md) for details. prerelease: false build-release: name: build-release needs: ['create-release'] runs-on: ${{ matrix.os }} env: CARGO: cargo TARGET_FLAGS: "" TARGET_DIR: ./target CROSS_NO_WARNINGS: 0 RUST_BACKTRACE: 1 strategy: matrix: build: [ x86_64-linux-gnu, x86_64-linux-musl, aarch64-linux-gnu, aarch64-linux-musl, armv7-linux-gnueabihf, armv7-linux-musleabihf, armv7-linux-musleabi, x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc, x86_64-pc-windows-gnu, aarch64-pc-windows-msvc, x86_64-netbsd, x86_64-freebsd ] include: # Linux (x86_64 & aarch64) - build: x86_64-linux-gnu os: ubuntu-22.04 target: x86_64-unknown-linux-gnu - build: x86_64-linux-musl os: ubuntu-22.04 target: x86_64-unknown-linux-musl - build: aarch64-linux-gnu os: ubuntu-22.04 target: aarch64-unknown-linux-gnu - build: aarch64-linux-musl os: ubuntu-22.04 target: aarch64-unknown-linux-musl # Linux (armv7) - build: armv7-linux-gnueabihf os: ubuntu-22.04 target: armv7-unknown-linux-gnueabihf - build: armv7-linux-musleabihf os: ubuntu-22.04 target: armv7-unknown-linux-musleabihf - build: armv7-linux-musleabi os: ubuntu-22.04 target: armv7-unknown-linux-musleabi # macOS (x86_64 & aarch64) - build: x86_64-apple-darwin os: macos-15-intel target: x86_64-apple-darwin - build: aarch64-apple-darwin os: macos-latest target: aarch64-apple-darwin # Windows (x86_64 & aarch64) - build: x86_64-pc-windows-msvc os: windows-2022 target: x86_64-pc-windows-msvc - build: x86_64-pc-windows-gnu os: ubuntu-22.04 target: x86_64-pc-windows-gnu - build: aarch64-pc-windows-msvc os: windows-2022 target: aarch64-pc-windows-msvc # BSD (x86_64) - build: x86_64-netbsd os: ubuntu-22.04 target: x86_64-unknown-netbsd - build: x86_64-freebsd os: ubuntu-22.04 target: x86_64-unknown-freebsd steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: stable profile: minimal override: true target: ${{ matrix.target }} - name: Use Cross shell: bash run: | cargo install cross --git https://github.com/cross-rs/cross echo "CARGO=cross" >> $GITHUB_ENV echo "TARGET_FLAGS=--target ${{ matrix.target }}" >> $GITHUB_ENV echo "TARGET_DIR=./target/${{ matrix.target }}" >> $GITHUB_ENV - name: Show command used for Cargo run: | echo "cargo command is: ${{ env.CARGO }}" echo "target flag is: ${{ env.TARGET_FLAGS }}" echo "target dir is: ${{ env.TARGET_DIR }}" - name: Build release binary run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} - name: Build archive shell: bash run: | staging="trippy-${{ needs.create-release.outputs.trip_version }}-${{ matrix.target }}" mkdir -p "$staging" if [ "${{ matrix.os }}" = "windows-2022" ] || [ "${{ matrix.build }}" = "x86_64-pc-windows-gnu" ]; then cp "target/${{ matrix.target }}/release/trip.exe" "$staging/" 7z a -tzip "$staging.zip" "$staging" echo "ASSET=$staging.zip" >> $GITHUB_ENV else cp "target/${{ matrix.target }}/release/trip" "$staging/" tar czf "$staging.tar.gz" "$staging" echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV fi - name: Build RPM package shell: bash if: startsWith(matrix.build, 'x86_64-linux-gnu') run: | cargo install cargo-generate-rpm cargo generate-rpm -p crates/trippy --target ${{ matrix.target }} -o target/${{ matrix.target }}/generate-rpm/trippy-${{ needs.create-release.outputs.trip_version }}-x86_64.rpm - name: Upload RPM package if: startsWith(matrix.build, 'x86_64-linux-gnu') uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: target/${{ matrix.target }}/generate-rpm/trippy-${{ needs.create-release.outputs.trip_version }}-x86_64.rpm asset_name: trippy-${{ needs.create-release.outputs.trip_version }}-x86_64.rpm asset_content_type: application/x-rpm - name: Create Debian package shell: bash if: startsWith(matrix.build, 'x86_64-linux-gnu') || startsWith(matrix.build, 'x86_64-linux-musl') run: | cargo install cargo-deb cargo deb -p trippy --target ${{ matrix.target }} --deb-version ${{ needs.create-release.outputs.trip_version }} case ${{ matrix.target }} in aarch64-*-linux-*) DPKG_ARCH=arm64 ;; arm-*-linux-*hf) DPKG_ARCH=armhf ;; i686-*-linux-*) DPKG_ARCH=i686 ;; x86_64-*-linux-*) DPKG_ARCH=amd64 ;; *) DPKG_ARCH=notset ;; esac; echo "DPKG_ARCH=${DPKG_ARCH}" >> $GITHUB_ENV - name: Upload Deb Release Asset if: startsWith(matrix.build, 'x86_64-linux-gnu') || startsWith(matrix.build, 'x86_64-linux-musl') uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create-release.outputs.upload_url }} asset_content_type: application/vnd.debian.binary-package asset_path: target/${{ matrix.target }}/debian/trippy_${{ needs.create-release.outputs.trip_version }}_${{ env.DPKG_ARCH }}.deb asset_name: trippy_${{ matrix.target }}_${{ needs.create-release.outputs.trip_version }}_${{ env.DPKG_ARCH }}.deb - name: Upload release archive uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: ${{ env.ASSET }} asset_name: ${{ env.ASSET }} asset_content_type: application/octet-stream ================================================ FILE: .gitignore ================================================ /target .idea .DS_Store .vscode/launch.json *.snap.new ================================================ FILE: AGENTS.md ================================================ # Trippy Agent Guidelines This repository follows the guidance below when making changes. ## Development commands - Check the code with `cargo check --workspace --all-features --tests`. - Test the code with `cargo test`. Do not pass `--all-features`. - Format Rust code with `cargo fmt --all`. - Format non-Rust code with `dprint fmt` (install with `cargo install --locked dprint`). - Lint with `cargo clippy --workspace --all-features --tests -- -Dwarnings`. - If CLI arguments, man pages or shell completions change, update snapshots: `cargo test && cargo insta review`. - If the `Dockerfile` changes, build it locally using `docker build . -t trippy:dev`. ## Commit messages - Use the Conventional Commits format: `[optional scope]: ` where `` is one of `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `build`, `ci`, or `revert`. - For code changes set the scope to one of `core`, `dns`, `packet`, `privilege` or `tui`. - Use backquotes for file names and code items in the description. - For documentation fixes use `docs: fix `. - Prefer small, focused commits. For larger changes, use multiple commits with clear messages. ## Recommendations - Run test, format and clippy before submitting a pull request and ensure all CI checks pass. - Keep documentation and examples in sync with code changes. - Use feature branches for separate tasks. - Open issues and pull requests through GitHub for discussion and review. - Always rebase your branch before when editing an open pull request to keep the history clean. ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - Added translations for locale `zh-TW` ([#1630](https://github.com/fujiapple852/trippy/pull/1630)) ### Changed - [BREAKING CHANGE] Change default `address-family` to be `system` ([#1475](https://github.com/fujiapple852/trippy/issues/1475)) - Increase MSRV to 1.85 ([#1700](https://github.com/fujiapple852/trippy/issues/1700)) ### Fixed - Default the `system` `address-family` to `ipv4-then-ipv6` for non-`system` resolvers ([#1635](https://github.com/fujiapple852/trippy/issues/1635)) - Locale parsing fails for valid BCP 47 language tags ([#1631](https://github.com/fujiapple852/trippy/pull/1631)) ## [0.13.0] - 2025-05-05 ### Added - Added DSCP and ECN columns ([#1539](https://github.com/fujiapple852/trippy/issues/1539)) - Added support for setting IPv6 traffic class from `--tos` ([#202](https://github.com/fujiapple852/trippy/issues/202)) - Added ability to read config from `$XDG_CONFIG_HOME/trippy` directory ([#1528](https://github.com/fujiapple852/trippy/issues/1528)) - Added `--tui-timezone` flag to set a custom timezone ([#1513](https://github.com/fujiapple852/trippy/issues/1513)) - Added support for `--addr-family system` to defer address family selection to the OS resolver ([#1469](https://github.com/fujiapple852/trippy/issues/1469)) - Added tracing start and end timestamps to the `json` report ([#1510](https://github.com/fujiapple852/trippy/issues/1510)) - Added the Trippy logo! ([#100](https://github.com/fujiapple852/trippy/issues/100)) ### Changed - Remove address family downgrade for `dublin` strategy ([#1476](https://github.com/fujiapple852/trippy/issues/1476)) - Reduce verbosity of tracing for library users ([#1482](https://github.com/fujiapple852/trippy/issues/1482)) - Increase MSRV to 1.78 ([#1576](https://github.com/fujiapple852/trippy/issues/1576)) ### Fixed - Tracer panic for large icmp packets ([#1561](https://github.com/fujiapple852/trippy/issues/1561)) - Memory corruption on Windows ([#1527](https://github.com/fujiapple852/trippy/issues/1527)) - Socket being closed twice on Windows ([#1443](https://github.com/fujiapple852/trippy/issues/1443)) - Potential crash on Windows for adapters without unicast addresses ([#1547](https://github.com/fujiapple852/trippy/issues/1547)) - Potential use-after-free when discovering source address on Windows ([#1558](https://github.com/fujiapple852/trippy/issues/1558)) - The `--tos` (`-Q`) flag is ignored for `IPv4/udp` tracing ([#1540](https://github.com/fujiapple852/trippy/issues/1540)) - Items missing from settings dialog ([#1541](https://github.com/fujiapple852/trippy/issues/1541)) ## [0.12.2] - 2025-01-03 ### Fixed - Tracer panic when `--first-ttl` is greater than 1 ([#1460](https://github.com/fujiapple852/trippy/issues/1460)) - IP `--addr-family` not respected for `--dns-resolve-method resolv` ([#1461](https://github.com/fujiapple852/trippy/issues/1461)) - Incorrect cli help text for `--addr-family` ([#1456](https://github.com/fujiapple852/trippy/issues/1456)) ## [0.12.1] - 2024-12-21 ### Changed - Replace use of `yaml` with `toml` dependency ([#1416](https://github.com/fujiapple852/trippy/issues/1416)) ### Fixed - Locale data not copied into docker image ([#1431](https://github.com/fujiapple852/trippy/issues/1431)) ## [0.12.0] - 2024-12-04 ### Added - Highlight lost probes in sample history ([#1247](https://github.com/fujiapple852/trippy/issues/1247)) - Added `quit-preserve-screen` (default: `shift+q`) key binding to quit Tui without clearing the screen ([#1382](https://github.com/fujiapple852/trippy/issues/1382)) - Added forward add backward loss heuristics ([#860](https://github.com/fujiapple852/trippy/issues/860)) - Added `--tui-locale` flag to support i18n ([#1319](https://github.com/fujiapple852/trippy/issues/1319)) - Added translations for locales `en`, `fr`, `tr`, `zh`, `pt`, `sv`, `it`, `ru`, `es` & `de` ([#506](https://github.com/fujiapple852/trippy/issues/506)) - Added `--print-locales` flag to print all available locales ([#1357](https://github.com/fujiapple852/trippy/issues/1357)) - Added Debian package ([#1312](https://github.com/fujiapple852/trippy/issues/1312)) - Added Ubuntu `noble` PPA package ([#1308](https://github.com/fujiapple852/trippy/issues/1308)) ### Changed - Added information bar to Tui ([#1349](https://github.com/fujiapple852/trippy/issues/1349)) - [BREAKING CHANGE] Remove `Timestamp` from all `DnsEntry` variants ([#1296](https://github.com/fujiapple852/trippy/issues/1296)) - [BREAKING CHANGE] Replace `toggle-privacy` key binding with `expand-privacy` and `contract-privacy` ([#1347](https://github.com/fujiapple852/trippy/issues/1347)) - [BREAKING CHANGE] Hide source address when `--tui-privacy-max-ttl` is set ([#1365](https://github.com/fujiapple852/trippy/issues/1365)) - Only show hostnames if different from IPs ([#1363](https://github.com/fujiapple852/trippy/issues/1363)) - Lookup GeoIp with current locale ([#1336](https://github.com/fujiapple852/trippy/issues/1336)) - Enable Link-Time Optimization (LTO) for release builds ([#1341](https://github.com/fujiapple852/trippy/issues/1341)) ### Fixed - Reverse dns enqueued multiple times when dns-ttl expires ([#1290](https://github.com/fujiapple852/trippy/issues/1290)) - Fixed panic for icmp extensions with malformed length ([#1287](https://github.com/fujiapple852/trippy/issues/1287)) - Cursor not moved to the bottom on exit when using `--tui-preserve-screen` ([#1375](https://github.com/fujiapple852/trippy/issues/1375)) - Config item `tui-address-mode` does not accept `ip` ([#1327](https://github.com/fujiapple852/trippy/issues/1327)) - Icmp extension mode not shown in Tui settings ([#1289](https://github.com/fujiapple852/trippy/issues/1289)) - Sample history and frequency charts ignore sub-millisecond samples ([#1398](https://github.com/fujiapple852/trippy/issues/1398)) ## [0.11.0] - 2024-08-11 ### Added - Added NAT detection for `IPv4/udp/dublin` ([#1104](https://github.com/fujiapple852/trippy/issues/1104)) - Added public API ([#1192](https://github.com/fujiapple852/trippy/issues/1192)) - Added support for NAT detection (`N`) column ([#1219](https://github.com/fujiapple852/trippy/issues/1219)) - Added support for last icmp packet type (`T`) column ([#1105](https://github.com/fujiapple852/trippy/issues/1105)) - Added support for last icmp packet code (`C`) column ([#1109](https://github.com/fujiapple852/trippy/issues/1109)) - Added support for the probe failure count (`f`) column ([#1258](https://github.com/fujiapple852/trippy/issues/1258)) - Added settings dialog tab hotkeys ([#1217](https://github.com/fujiapple852/trippy/issues/1217)) - Added `--dns-ttl` flag to allow refreshing the reverse DNS results ([#1233](https://github.com/fujiapple852/trippy/issues/1233)) - Added `--generate-man` flag for generating [ROFF](https://en.wikipedia.org/wiki/Roff_(software)) man page ([#85](https://github.com/fujiapple852/trippy/issues/85)) - Added Ubuntu PPA package ([#859](https://github.com/fujiapple852/trippy/issues/859)) - Added Chocolatey package ([#572](https://github.com/fujiapple852/trippy/issues/572)) ### Changed - [BREAKING CHANGE] Changed initial sequence to be `33434` ([#1203](https://github.com/fujiapple852/trippy/issues/1203)) - [BREAKING CHANGE] Renamed `tui-max-[samples|flows]` as `max-[samples|flows]` ([#1187](https://github.com/fujiapple852/trippy/issues/1187)) - Separated library and binary crates ([#1141](https://github.com/fujiapple852/trippy/issues/1141)) - Record `icmp` packet code ([#734](https://github.com/fujiapple852/trippy/issues/734)) - Transient error handling for `IPv4` on macOS, Linux & Windows ([#1255](https://github.com/fujiapple852/trippy/issues/1255)) - Improved error messages ([#1150](https://github.com/fujiapple852/trippy/issues/1150)) - Revamp the help dialog ([#1260](https://github.com/fujiapple852/trippy/issues/1260)) ### Fixed - Fixed `DestinationUnreachable` incorrectly assumed to come from target host ([#1225](https://github.com/fujiapple852/trippy/issues/1225)) - Fixed incorrect target hop calculation ([#1226](https://github.com/fujiapple852/trippy/issues/1226)) - Do not conflate `AddressInUse` and `AddrNotAvailable` errors ([#1246](https://github.com/fujiapple852/trippy/issues/1246)) ## [0.10.0] - 2024-03-31 ### Added - Added support for calculating and displaying jitter ([#39](https://github.com/fujiapple852/trippy/issues/39)) - Added support for customizing columns ([#757](https://github.com/fujiapple852/trippy/issues/757)) - Added support for reordering and toggling column visibility in Tui ([#1026](https://github.com/fujiapple852/trippy/issues/1026)) - Added support for [dublin](https://github.com/insomniacslk/dublin-traceroute) ECMP routing for `IPv6/udp` ([#272](https://github.com/fujiapple852/trippy/issues/272)) - Added support for [IPinfo](https://ipinfo.io) flavoured `mmdb` files ([#862](https://github.com/fujiapple852/trippy/issues/862)) - Added support for `IPv4->IPv6` and `IPv6->IPv4` DNS fallback modes ([#864](https://github.com/fujiapple852/trippy/issues/864)) - Added [TUN](https://en.wikipedia.org/wiki/TUN/TAP) based simulation tests ([#908](https://github.com/fujiapple852/trippy/issues/908)) - Added support for last src port (`S`) and last dest port (`P`) custom columns ([#974](https://github.com/fujiapple852/trippy/issues/974)) - Added support for last sequence (`Q`) custom column ([#976](https://github.com/fujiapple852/trippy/issues/976)) - Added support for more named theme colors ([#1011](https://github.com/fujiapple852/trippy/issues/1011)) ### Changed - Ensure `paris` and `dublin` ECMP strategy are only used with supported protocols ([#848](https://github.com/fujiapple852/trippy/issues/848)) - Restrict flows to `paris` and `dublin` ECMP strategies ([#1007](https://github.com/fujiapple852/trippy/issues/1007)) - Improved Tui table column layout logic ([#925](https://github.com/fujiapple852/trippy/issues/925)) - Use exclusive reference `&mut` for all Socket operations ([#843](https://github.com/fujiapple852/trippy/issues/843)) - Reduced maximum sequence per round from 1024 to 512 ([#1067](https://github.com/fujiapple852/trippy/issues/1067)) ### Fixed - Fixed off-by-one bug in max-rounds calculation ([#906](https://github.com/fujiapple852/trippy/issues/906)) - Fixed panic with `expand-hosts-max` Tui command ([#892](https://github.com/fujiapple852/trippy/issues/892)) - Fixed failure to parse generated config file on Windows ([#958](https://github.com/fujiapple852/trippy/issues/958)) - Fixed tracer panic for `icmp` TimeExceeded "Fragment reassembly time exceeded" packets ([#979](https://github.com/fujiapple852/trippy/issues/979)) - Fixed tracer not discarding unrelated `icmp` packets for `udp` and `tcp` protocols ([#982](https://github.com/fujiapple852/trippy/issues/982)) - Fixed incorrect minimum packet size for `IPv6` ([#985](https://github.com/fujiapple852/trippy/issues/985)) - Fixed permission denied error reading configuration file from snap installation ([#1058](https://github.com/fujiapple852/trippy/issues/1058)) ## [0.9.0] - 2023-11-30 ### Added - Added support for tracing flows ([#776](https://github.com/fujiapple852/trippy/issues/776)) - Added support for `icmp` extensions ([#33](https://github.com/fujiapple852/trippy/issues/33)) - Added support for `MPLS` label stack class `icmp` extension objects ([#753](https://github.com/fujiapple852/trippy/issues/753)) - Added support for [paris](https://github.com/libparistraceroute/libparistraceroute) ECMP routing for `IPv6/udp` ([#749](https://github.com/fujiapple852/trippy/issues/749)) - Added `--unprivileged` (`-u`) flag to allow tracing without elevated privileges (macOS only) ([#101](https://github.com/fujiapple852/trippy/issues/101)) - Added `--tui-privacy-max-ttl` flag to hide host and IP details for low ttl hops ([#766](https://github.com/fujiapple852/trippy/issues/766)) - Added `toggle-privacy` (default: `p`) key binding to show or hide private hops ([#823](https://github.com/fujiapple852/trippy/issues/823)) - Added `toggle-flows` (default: `f`) key binding to show or hide tracing flows ([#777](https://github.com/fujiapple852/trippy/issues/777)) - Added `--dns-resolve-all` (`-y`) flag to allow tracing to all IPs resolved from DNS lookup entry ([#743](https://github.com/fujiapple852/trippy/issues/743)) - Added `dot` report mode (`-m dot`) to output hop graph in Graphviz `DOT` format ([#582](https://github.com/fujiapple852/trippy/issues/582)) - Added `flows` report mode (`-m flows`) to output a list of all unique tracing flows ([#770](https://github.com/fujiapple852/trippy/issues/770)) - Added `--icmp-extensions` (`-e`) flag for parsing `IPv4`/`IPv6` `icmp` extensions ([#751](https://github.com/fujiapple852/trippy/issues/751)) - Added `--tui-icmp-extension-mode` flag to control how `icmp` extensions are rendered ([#752](https://github.com/fujiapple852/trippy/issues/752)) - Added `--print-config-template` flag to output a template config file ([#792](https://github.com/fujiapple852/trippy/issues/792)) - Added `--icmp` flag as a shortcut for `--protocol icmp` ([#649](https://github.com/fujiapple852/trippy/issues/649)) - Added `toggle-help-alt` (default: `?`) key binding to show or hide help ([#694](https://github.com/fujiapple852/trippy/issues/694)) - Added panic handing to Tui ([#784](https://github.com/fujiapple852/trippy/issues/784)) - Added official Windows `scoop` package ([#462](https://github.com/fujiapple852/trippy/issues/462)) - Added official Windows `winget` package ([#460](https://github.com/fujiapple852/trippy/issues/460)) - Release `musl` Debian `deb` binary asset ([#568](https://github.com/fujiapple852/trippy/issues/568)) - Release `armv7` Linux binary assets ([#712](https://github.com/fujiapple852/trippy/issues/712)) - Release `aarch64-apple-darwin` (aka macOS Apple Silicon) binary assets ([#801](https://github.com/fujiapple852/trippy/issues/801)) - Added additional Rust Tier 1 and Tier 2 binary assets ([#811](https://github.com/fujiapple852/trippy/issues/811)) ### Changed - [BREAKING CHANGE] `icmp` extension object data added to `json` and `stream` reports ([#806](https://github.com/fujiapple852/trippy/issues/806)) - [BREAKING CHANGE] IPs field added to `csv` and all tabular reports ([#597](https://github.com/fujiapple852/trippy/issues/597)) - [BREAKING CHANGE] Command line flags `--dns-lookup-as-info` and `--tui-preserve-screen` no longer require a boolean argument ([#708](https://github.com/fujiapple852/trippy/issues/708)) - [BREAKING CHANGE] Default key binding for `ToggleFreeze` changed from `f` to `ctrl+f` ([#785](https://github.com/fujiapple852/trippy/issues/785)) - Always render AS lines in hop details mode ([#825](https://github.com/fujiapple852/trippy/issues/825)) - Expose DNS resolver module as part of `trippy` library ([#754](https://github.com/fujiapple852/trippy/issues/754)) - Replaced unmaintained `tui-rs` crate with `ratatui` crate ([#569](https://github.com/fujiapple852/trippy/issues/569)) ### Fixed - Reverse DNS lookup not working in reports ([#509](https://github.com/fujiapple852/trippy/issues/509)) - Crash on NetBSD during window resizing ([#276](https://github.com/fujiapple852/trippy/issues/276)) - Protocol mismatch causes tracer panic ([#745](https://github.com/fujiapple852/trippy/issues/745)) - Incorrect row height in Tui hop detail navigation view for hops with no responses ([#765](https://github.com/fujiapple852/trippy/issues/765)) - Unnecessary socket creation in certain tracing modes ([#647](https://github.com/fujiapple852/trippy/issues/647)) - Incorrect byte order in `IPv4` packet length calculation ([#686](https://github.com/fujiapple852/trippy/issues/686)) ## [0.8.0] - 2023-05-15 ### Added - Added `--tui-as-mode` flag to control how AS information is rendered ([#483](https://github.com/fujiapple852/trippy/issues/483)) - Added support for configuration files and added a `-c` (`--config-file`) flag ([#412](https://github.com/fujiapple852/trippy/issues/412)) - Added `--generate` flag for generating shell completions ([#86](https://github.com/fujiapple852/trippy/issues/86)) - Added support for showing and navigating host detail ([#70](https://github.com/fujiapple852/trippy/issues/70)) - Added `--geoip-mmdb-file` and `--tui-geoip-mode` flags for looking up and displaying GeoIp information from `mmdb` files ([#503](https://github.com/fujiapple852/trippy/issues/503)) - Added settings dialog and simplified Tui header display ([#521](https://github.com/fujiapple852/trippy/issues/521)) - Added interactive GeoIp map display ([#505](https://github.com/fujiapple852/trippy/issues/505)) - Added support for the [paris](https://github.com/libparistraceroute/libparistraceroute) ECMP traceroute strategy for `IPv4/udp` ([#542](https://github.com/fujiapple852/trippy/issues/542)) - Added `silent` reporting mode to run tracing without producing any output ([#555](https://github.com/fujiapple852/trippy/issues/555)) - Added `-v` (`--verbose`), `--log-format`, `--log-filter` & `--log-span-events` flags to support generating debug trace logging output ([#552](https://github.com/fujiapple852/trippy/issues/552)) ### Changed - Show AS information for IP addresses without PTR record ([#479](https://github.com/fujiapple852/trippy/issues/479)) - Re-enabled musl release builds ([#456](https://github.com/fujiapple852/trippy/issues/456)) - [BREAKING CHANGE] Renamed short config flag for `report-cycles` from `-c` to `-C` ([#491](https://github.com/fujiapple852/trippy/issues/491)) - Ensure administrator privileges on Windows ([#451](https://github.com/fujiapple852/trippy/issues/451)) - Add context information to socket errors ([#153](https://github.com/fujiapple852/trippy/issues/153)) ### Fixed - Do not require passing targets for certain command line flags ([#500](https://github.com/fujiapple852/trippy/issues/500)) - Key press registering two events on Windows ([#513](https://github.com/fujiapple852/trippy/issues/513)) - Command line parameter names in error messages should be in `kebab-case` ([#516](https://github.com/fujiapple852/trippy/issues/516)) ## [0.7.0] - 2023-03-25 ### Added - Added support for Windows (`icmp`, `udp` & `tcp` for `IPv4` &`IPv6`) ([#98](https://github.com/fujiapple852/trippy/issues/98)) - Added support for custom Tui key bindings ([#448](https://github.com/fujiapple852/trippy/issues/448)) - Added support for custom Tui color themes ([#411](https://github.com/fujiapple852/trippy/issues/411)) - Added RPM packaging ([#95](https://github.com/fujiapple852/trippy/issues/95)) - Added DEB packaging ([#94](https://github.com/fujiapple852/trippy/issues/94)) ### Fixed - Variable Equal Cost Multi-path Routing (ECMP) causing truncated trace ([#269](https://github.com/fujiapple852/trippy/issues/269)) - Tracing using the `tcp` may ignore some incoming `icmp` responses ([#407](https://github.com/fujiapple852/trippy/issues/407)) - Tracer panics with large `--initial-sequence` and delayed TCP probe response ([#435](https://github.com/fujiapple852/trippy/issues/435)) - Trippy Docker fails to start ([#277](https://github.com/fujiapple852/trippy/issues/277)) ## [0.6.0] - 2022-08-19 ### Added - Added support for tracing using `IPv6` for `tcp` ([#191](https://github.com/fujiapple852/trippy/issues/191)) - Added `-R` (`--multipath-strategy`) flag to allow setting the [Equal Cost Multi-path Routing](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) strategy and added support for the [dublin](https://github.com/insomniacslk/dublin-traceroute) traceroute strategies for `IPv4/udp` ([#158](https://github.com/fujiapple852/trippy/issues/158)) - Added zoom-able chart showing round trip times for all hops in a trace ([#209](https://github.com/fujiapple852/trippy/issues/209)) - Added `--udp` and `--tcp` flags as shortcuts to `-p udp` and `-p tcp` respectively ([#205](https://github.com/fujiapple852/trippy/issues/205)) ### Changed - Gray out hops which did not update in the current round ([#216](https://github.com/fujiapple852/trippy/issues/216)) ## [0.5.0] - 2022-06-02 ### Added - Added support for tracing using `IPv6` for `icmp` and `udp` ([#35](https://github.com/fujiapple852/trippy/issues/35)) - Added BSOD error reporting to Tui ([#179](https://github.com/fujiapple852/trippy/issues/179)) - Added Ctrl-C keyboard command to quit the Tui ([#91](https://github.com/fujiapple852/trippy/issues/91)) ### Changed - Rewrite of network code to use RAW sockets ([#195](https://github.com/fujiapple852/trippy/issues/195), [#192](https://github.com/fujiapple852/trippy/issues/192)) ### Fixed - Setting `-c` (`--report-cycles`) to 1 returns no traces ([#189](https://github.com/fujiapple852/trippy/issues/189)) - Tracer failures not being shown for reports ([#183](https://github.com/fujiapple852/trippy/issues/183)) ## [0.4.0] - 2022-05-18 ### Added - Added `-P` (`--target-port`) flag to allow specifying the target port ([1](https://github.com/fujiapple852/trippy/commit/5773fe5e5323543612be6bd4606db5aa8347d71e), [2](https://github.com/fujiapple852/trippy/commit/9f03047dd231b10b13911fcc7af60afbb8b21473)) - Added ability to trace with either a fixed source or a fixed destination port for both `udp` and `tcp` tracing ([#43](https://github.com/fujiapple852/trippy/issues/43)) - Display source and destination ports in Tui ([#156](https://github.com/fujiapple852/trippy/issues/156)) - Added the `-A` (`--source-address`) flag to allow specifying the source address ([#162](https://github.com/fujiapple852/trippy/issues/162)) - Added the `-I` (`--interface`) flag to allow specifying the source interface ([#142](https://github.com/fujiapple852/trippy/issues/42)) - Added the `-Q` (`--tos`) flag to allow specifying the `TOS` (`DSCP`+`ECN`) `IPv4` header value ([#38](https://github.com/fujiapple852/trippy/issues/38)) ### Changed - Changed `tcp` tracing to use a standard (non-raw) socket to be able to detect the target ([#134](https://github.com/fujiapple852/trippy/issues/134)) - Changed `udp` tracing to use a standard (non-raw) socket ([#155](https://github.com/fujiapple852/trippy/issues/155)) - Renamed the `--tui-max-addresses-per-hop` flag as `tui-max-addrs` ([#165](https://github.com/fujiapple852/trippy/issues/165)) - Reorder the cli flags in the help output ([#163](https://github.com/fujiapple852/trippy/issues/163)) - Change short alias for flag `max_round_duration` from `-I` to `-T` ([1](https://github.com/fujiapple852/trippy/commit/15978b0909139bb2b38baa4c6f6ca969c818fc75)) - Added short cli flags for `source-port` (`-S`), `first-ttl` (`-f`) and `tui-max-addrs` ( `-M`) ([1](https://github.com/fujiapple852/trippy/commit/6a6a490174582c8500972b89407ba8d694c4c6fa)) ### Fixed - Checksums for `udp` packets were not being set (obsoleted by [#155](https://github.com/fujiapple852/trippy/issues/155)) ([#159](https://github.com/fujiapple852/trippy/issues/159)) - `TimeExceeded` responses _from_ the target address were not being handled ([1](https://github.com/fujiapple852/trippy/commit/3afa41326a33287a3ad9c17713dd7426ca86b481)) - The largest time-to-live for a given round was being calculated incorrectly in some cases ([1](https://github.com/fujiapple852/trippy/commit/688a8d00d84a816449cfee48b2d6f6dd90946511)) ## [0.3.1] - 2022-05-09 ### Fixed - Local IPv4 discovery fails on some platforms ([#133](https://github.com/fujiapple852/trippy/issues/133), [#142](https://github.com/fujiapple852/trippy/issues/142)) - DNS resolution not filtering for `IPv4` addresses ([#148](https://github.com/fujiapple852/trippy/issues/148)) - Note: see [#35](https://github.com/fujiapple852/trippy/issues/35) for the status of `IPv6` support ## [0.3.0] - 2022-05-08 ### Added - Added ability for `icmp` tracing to multiple targets simultaneously in Tui ([#72](https://github.com/fujiapple852/trippy/issues/72)) - Added ability to enable and disable the `AS` lookup from the Tui ([#126](https://github.com/fujiapple852/trippy/issues/126)) - Added ability to switch between hop address display modes (ip, hostname or both) in thr Tui ([#124](https://github.com/fujiapple852/trippy/issues/124)) - Added ability to expand and collapse the number of hosts displays per hop in the Tui ([#124](https://github.com/fujiapple852/trippy/issues/124)) - Added the `-s` (`--tui-max-samples`) flag to specify the number of samples to keep for analysis and display ([#110](https://github.com/fujiapple852/trippy/issues/110)) - Added ability to flush the DNS cache from the Tui ([#71](https://github.com/fujiapple852/trippy/issues/371)) ### Changed - Simplified `Tracer` by removing circular buffer ([#106](https://github.com/fujiapple852/trippy/issues/106)) - Added round end reason indicator to `Tracer` ([#88](https://github.com/fujiapple852/trippy/issues/88)) - Show better error message for failed DNS resolution ([#119](https://github.com/fujiapple852/trippy/issues/119)) ### Fixed - Tracing with `udp` protocol not showing the target hop due to incorrect handling of `DestinationUnreachable` responses ([#131](https://github.com/fujiapple852/trippy/issues/131)) - Tui failing on shutdown on Windows due to `DisableMouseCapture` being invoked without a prior `EnableMouseCapture` call ([#116](https://github.com/fujiapple852/trippy/issues/116)) - Build failing on Windows due to incorrect conditional compilation configuration ([#113](https://github.com/fujiapple852/trippy/issues/113)) - Tracing not publishing all `Probe` in a round when the round ends without finding the target ([#103](https://github.com/fujiapple852/trippy/issues/103)) - Tracing with `tcp` protocol not working as the checksum was not set ([#79](https://github.com/fujiapple852/trippy/issues/79)) - Do not show FQDN for reverse DNS queries from non-system resolvers ([#120](https://github.com/fujiapple852/trippy/issues/120)) ## [0.2.0] - 2022-04-29 ### Added - Added the `-r` (`--dns-resolve-method`) flag to specify using either the OS DNS resolver (default), a 3rd party resolver (Google `8.8.8.8` and Cloudflare `1.1.1.1`) or DNS resolver configuration from the `/etc/resolv.conf` file - Added the `-z` (`--dns-lookup-as-info`) flag to display the ASN for each discovered host. This is not yet supported for the default `system` resolver, see [#66](https://github.com/fujiapple852/trippy/issues/66). - Added the `--dns-timeout` flag to allow setting a timeout on all DNS queries - Added additional parameter validation for `first-ttl`, `max-ttl` & `initial-sequence` ### Changed - All DNS queries are now non-blocking to prevent the Tui from freezing during slow DNS query - Renamed `min-sequence` flag as `initial-sequence` ### Fixed - Fixed the behaviour when the sequence number wraps around at `2^16 - 1` ## [0.1.0] - 2022-04-27 ### Added - Initial WIP release of `trippy` [Unreleased]: https://github.com/fujiapple852/trippy/compare/0.13.0...master [0.13.0]: https://github.com/fujiapple852/trippy/compare/0.12.2...0.13.0 [0.12.2]: https://github.com/fujiapple852/trippy/compare/0.12.1...0.12.2 [0.12.1]: https://github.com/fujiapple852/trippy/compare/0.12.0...0.12.1 [0.12.0]: https://github.com/fujiapple852/trippy/compare/0.11.0...0.12.0 [0.11.0]: https://github.com/fujiapple852/trippy/compare/0.10.0...0.11.0 [0.10.0]: https://github.com/fujiapple852/trippy/compare/0.9.0...0.10.0 [0.9.0]: https://github.com/fujiapple852/trippy/compare/0.8.0...0.9.0 [0.8.0]: https://github.com/fujiapple852/trippy/compare/0.7.0...0.8.0 [0.7.0]: https://github.com/fujiapple852/trippy/compare/0.6.0...0.7.0 [0.6.0]: https://github.com/fujiapple852/trippy/compare/0.5.0...0.6.0 [0.5.0]: https://github.com/fujiapple852/trippy/compare/0.4.0...0.5.0 [0.4.0]: https://github.com/fujiapple852/trippy/compare/0.3.1...0.4.0 [0.3.1]: https://github.com/fujiapple852/trippy/compare/0.3.0...0.3.1 [0.3.0]: https://github.com/fujiapple852/trippy/compare/0.2.0...0.3.0 [0.2.0]: https://github.com/fujiapple852/trippy/compare/0.1.0...0.2.0 [0.1.0]: https://github.com/fujiapple852/trippy/compare/0.0.0...0.1.0 ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Trippy Contributions to Trippy are most welcome, whether you wish to report a bug, request a feature, or contribute code. Raise issues and feature requests in the GitHub [issue tracker](https://github.com/fujiapple852/trippy/issues) and raise all changes as GitHub [pull requests](https://github.com/fujiapple852/trippy/pulls). ## Development This section describes how to set up a development environment and the development process for Trippy. ### Development tools The following tools are needed for local development. Note that most of the following are checked during CI, so it is recommended to run these checks locally before submitting a pull request. #### Rust Trippy is written in [`Rust`](https://www.rust-lang.org/tools/install) and requires the Rust toolchain to build and run. As well as default components such as `cargo`, you will need `rustfmt` and `clippy` for code formatting and linting. > [!NOTE] > Trippy uses the `stable` toolchain. To install `rustfmt` and `clippy`: ```shell rustup component add rustfmt clippy ``` To format the Rust code: ```shell cargo fmt --all ``` > [!NOTE] > Trippy uses default settings for code formatting. To lint the Rust code: ```shell cargo clippy --workspace --all-features --tests -- -Dwarnings ``` > [!NOTE] > Clippy configuration is defined at the workspace level in the root `Cargo.toml` file. #### Cargo `deny` If you add or update dependencies, you must run Cargo [`deny`](https://github.com/EmbarkStudios/cargo-deny) to ensure that the licenses of the dependencies are acceptable. ```shell cargo deny check --hide-inclusion-graph ``` The allowed licenses are defined in the `deny.toml` file. #### Cargo `insta` If you make changes that impact the command line interface arguments, manual pages or shell completions, you must update the testing snapshots using Cargo [`insta`](https://insta.rs). After making your changes, run `cargo test` to generate the new snapshots followed by `cargo insta` to review and update the snapshots. ```shell cargo test && cargo insta review ``` #### Cargo `spelling` If you make changes to code documentation, you must run Cargo [`spellcheck`](https://github.com/drahnr/cargo-spellcheck) to ensure they are free from misspellings and typos. To check the spelling: ```shell cargo spellcheck check ``` The configuration for `spellcheck` is defined in the `.config/spellcheck.toml` file and the custom dictionary is defined in the `.config/trippy.dic` file. #### Cargo `msrv` If you add or update dependencies, you should use the Cargo [msrv](https://github.com/foresterre/cargo-msrv) tool to check the Minimum Supported Rust Version (MSRV) to ensure that the new dependencies are compatible with the current MSRV. To check the MSRV of the `trippy` crate: ```shell cargo msrv verify --manifest-path crates/trippy/Cargo.toml-- cargo check ``` #### `dprint` The [`dprint`](https://dprint.dev/) tool is needed to ensure consistent formatting of the non-Rust portions of the codebase and docs. To format the non-Rust code: ```shell dprint fmt ``` The configuration for `dprint` is defined in the `dprint.json` file. #### Docker If you make changes to the `Dockerfile`, you should build the Docker image locally to ensure it builds correctly. ```shell docker build . -t trippy:dev ``` > [!NOTE] > If you add new files that are required at build time then you must update the `Dockerfile` to include them explicitly. ### Development process This section describes the development process for Trippy. #### Conventional commits All commit messages should follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format. The commit message should be structured as follows: ```text [optional scope]: ``` Where `type` is one of the following: - `feat`: A new feature - `fix`: A bug fix - `chore`: Build process, dependency and version updates - `docs`: Documentation only changes - `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) - `refactor`: A code change that neither fixes a bug nor adds a feature - `test`: Adding missing tests or correcting existing tests - `build`: Changes that affect the build system or external dependencies - `ci`: Changes to our CI configuration files and scripts - `revert`: Reverts a previous commit The `scope` is optional and, if given, should be the name of the crate being modified, currently one of `core`, `packet`, `dns`, `privilege`, `tui` or `trippy`. > [!NOTE] > Small do-one-things commits are preferred over large do-many-things commits. This makes changes easier to review and > revert if necessary. For example, if you are adding a new feature and fixing a bug, it is better to create two > separate commits. ## Releases Instructions for releasing a new `0.xx.0` version of Trippy. Many distribution packages are managed by external maintainers, however the following are managed by the Trippy maintainers: - GitHub Releases - Crates.io - Docker - Snapcraft - WinGet - Ubuntu PPA ### Prerequisites - Check the MSRV (Minimum Supported Rust Version) in the `trippy` crate `Cargo.toml` is still correct. > [!NOTE] > The MSRV should typically be the version from around 1 year before the current date to maximise compatibility. - Update all dependencies to the latest SemVer compatible versions > [!NOTE] > Some distributions may not support the latest versions of all dependencies, so be conservative with updates. - Record and add an `assets/0.xx.0/demo.gif` for the new version - Update the `README.md` with details of the features in the new version - Update the `CHANGELOG.md` for the new version - Update the `RELEASES.md` for the new version - Update the version to `0.xx.0` in `Cargo.toml`, `snap/snapcraft.yaml` & `ubuntu-ppa/release.sh` ### Testing Trippy is tested extensively in CI on Linux, Windows and macOS for every pull request. However, it is recommended to test the release binaries on all platforms before release. ### GitHub Releases - Tag the release with the version number `0.xx.0` and push the tag to GitHub: ```shell git tag 0.xx.0 git push origin tag 0.xx.0 ``` This will trigger the GitHub Actions workflow to build the release binaries and publish them to the GitHub release page. - Edit GitHub release page and copy the relevant sections from `RELEASES.md` and `CHANGELOG.md`. Refer to previous releases for the format. ### Crates.io - Publish all crates to crates.io (in order): ```shell cargo publish -p trippy-dns cargo publish -p trippy-packet cargo publish -p trippy-privilege cargo publish -p trippy-core cargo publish -p trippy-tui cargo publish -p trippy ``` ### Docker From the repository root directory: ```shell docker build . -t fujiapple/trippy:0.xx.0 -t fujiapple/trippy:latest docker push fujiapple/trippy:0.xx.0 docker push fujiapple/trippy:latest ``` ### Snapcraft - Promote the first `0.xx.0` build to the `latest/stable` channel from the Snapcraft [releases](https://snapcraft.io/trippy/releases) page ### WinGet - Download the latest release Windows `zip` from the [GitHub releases page](https://github.com/fujiapple852/trippy/releases/latest) - Determine the SHA256 checksum of the release: ```shell shasum -a 256 trippy-0.xx.0-x86_64-pc-windows-msvc.zip ``` - Update the `winget` [fork](https://github.com/fujiapple852/winget-pkgs) to the latest upstream - Checkout the fork and create a branch called `fujiapple852-trippy-0.xx.0` - Go to the Trippy directory ```shell cd winget-pkgs/manifests/f/FujiApple/Trippy ``` - Copy the previous `0.yy.0` directory to a new directory for the new `0.xx.0` version - Update the `PackageVersion`, `ReleaseDate` and update all paths to the new version - Update the `InstallerSha256` with the checksum from the previous step - Update the release notes from [CHANGELOG.md](https://github.com/fujiapple852/trippy/blob/master/CHANGELOG.md) - Commit the changes with message: ```text update fujiapple852/trippy to 0.xx.0 ``` - Push the branch to the fork and create a pull request against the upstream `winget-pkgs` repository ### Ubuntu PPA See the Ubuntu PPA [README.md](https://github.com/fujiapple852/trippy/blob/master/ubuntu-ppa/README.md) ## Help wanted There are several the issues tagged as [help wanted](https://github.com/fujiapple852/trippy/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) in the GitHub issue tracker for which I would be especially grateful for assistance. ## License This project is distributed under the terms of the Apache License (Version 2.0). Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in time by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions. ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "2" members = [ "crates/trippy", "crates/trippy-tui", "crates/trippy-core", "crates/trippy-packet", "crates/trippy-privilege", "crates/trippy-dns", "examples/*", ] [workspace.package] version = "0.14.0-dev" authors = ["FujiApple "] documentation = "https://github.com/fujiapple852/trippy" homepage = "https://github.com/fujiapple852/trippy" repository = "https://github.com/fujiapple852/trippy" readme = "README.md" license = "Apache-2.0" edition = "2024" rust-version = "1.85" keywords = ["cli", "tui", "traceroute", "ping", "icmp"] categories = ["command-line-utilities", "network-programming"] [workspace.dependencies] trippy-tui = { version = "0.14.0-dev", path = "crates/trippy-tui" } trippy-core = { version = "0.14.0-dev", path = "crates/trippy-core" } trippy-privilege = { version = "0.14.0-dev", path = "crates/trippy-privilege" } trippy-dns = { version = "0.14.0-dev", path = "crates/trippy-dns" } trippy-packet = { version = "0.14.0-dev", path = "crates/trippy-packet" } anyhow = "1.0.91" arrayvec = { version = "0.7.6", default-features = false } bitflags = "2.11.0" caps = "0.5.6" chrono = { version = "0.4.44", default-features = false } chrono-tz = "0.10.4" clap = { version = "4.5.60", default-features = false } clap-cargo = "0.15.2" clap_complete = "4.6.0" clap_mangen = "0.2.20" comfy-table = { version = "7.1.4", default-features = false } crossbeam = "0.8.4" crossterm = { version = "0.28.1", default-features = false } csv = "1.4.0" derive_more = { version = "2.1.1", default-features = false } dns-lookup = "3.0.1" encoding_rs_io = "0.1.7" etcetera = "0.10.0" futures-concurrency = "7.6.3" hex-literal = "1.1.0" hickory-resolver = "0.24.4" humantime = "2.3.0" indexmap = { version = "2.13.0", default-features = false } insta = "1.46.3" itertools = "0.14.0" maxminddb = "0.27.3" mockall = "0.14.0" nix = { version = "0.31.2", default-features = false } parking_lot = "0.12.5" paste = "1.0.15" petgraph = "0.8.3" pretty_assertions = "1.4.1" rand = "0.10.0" ratatui = "0.29.0" serde = { version = "1.0.201", default-features = false } serde_json = { version = "1.0.117", default-features = false } serde_with = { version = "3.17.0", default-features = false, features = ["macros"] } socket2 = "0.6.3" strum = { version = "0.28.0", default-features = false } sys-locale = "0.3.2" test-case = "3.3.1" thiserror = "2.0.3" tokio = "1.50.0" tokio-util = "0.7.18" toml = { version = "1.0.7", default-features = false, features = ["serde"] } tracing = "0.1.44" tracing-chrome = "0.7.2" tracing-subscriber = { version = "0.3.23", default-features = false } tun-rs = "2.8.2" unic-langid = "0.9.6" unicode-width = "0.2.0" widestring = "1.2.1" windows-sys = "0.52.0" [workspace.lints.rust] unsafe_code = "deny" rust_2018_idioms = { level = "warn", priority = -1 } [workspace.lints.clippy] all = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 } module_name_repetitions = "allow" option_if_let_else = "allow" cast_possible_truncation = "allow" missing_errors_doc = "allow" cast_precision_loss = "allow" bool_assert_comparison = "allow" missing_const_for_fn = "allow" struct_field_names = "allow" cognitive_complexity = "allow" [profile.release] lto = true ================================================ FILE: Dockerfile ================================================ FROM rust:1.85 AS build-env RUN rustup target add x86_64-unknown-linux-musl WORKDIR /app COPY Cargo.toml /app COPY Cargo.lock /app RUN mkdir -p /app/crates/trippy/src RUN mkdir -p /app/crates/trippy-tui/src RUN mkdir -p /app/crates/trippy-core/src RUN mkdir -p /app/crates/trippy-dns/src RUN mkdir -p /app/crates/trippy-packet/src RUN mkdir -p /app/crates/trippy-privilege/src COPY crates/trippy/Cargo.toml /app/crates/trippy/Cargo.toml COPY crates/trippy-tui/Cargo.toml /app/crates/trippy-tui/Cargo.toml COPY crates/trippy-core/Cargo.toml /app/crates/trippy-core/Cargo.toml COPY crates/trippy-dns/Cargo.toml /app/crates/trippy-dns/Cargo.toml COPY crates/trippy-packet/Cargo.toml /app/crates/trippy-packet/Cargo.toml COPY crates/trippy-privilege/Cargo.toml /app/crates/trippy-privilege/Cargo.toml COPY examples/ /app/examples/ # dummy build to cache dependencies RUN echo "fn main() {}" > /app/crates/trippy/src/main.rs RUN touch /app/crates/trippy-tui/src/lib.rs RUN touch /app/crates/trippy-core/src/lib.rs RUN touch /app/crates/trippy-dns/src/lib.rs RUN touch /app/crates/trippy-packet/src/lib.rs RUN touch /app/crates/trippy-privilege/src/lib.rs RUN cargo build --release --target=x86_64-unknown-linux-musl --package trippy # copy the actual application code and build COPY crates/trippy/src /app/crates/trippy/src COPY crates/trippy-tui/src /app/crates/trippy-tui/src COPY crates/trippy-core/src /app/crates/trippy-core/src COPY crates/trippy-dns/src /app/crates/trippy-dns/src COPY crates/trippy-packet/src /app/crates/trippy-packet/src COPY crates/trippy-privilege/src /app/crates/trippy-privilege/src COPY crates/trippy-tui/build.rs /app/crates/trippy-tui COPY crates/trippy-tui/locales.toml /app/crates/trippy-tui COPY trippy-config-sample.toml /app COPY trippy-config-sample.toml /app/crates/trippy-tui COPY README.md /app COPY README.md /app/crates/trippy RUN cargo clean --release --target=x86_64-unknown-linux-musl -p trippy-tui -p trippy-core -p trippy-dns -p trippy-packet -p trippy-privilege RUN cargo build --release --target=x86_64-unknown-linux-musl FROM alpine RUN apk update && apk add ncurses COPY --from=build-env /app/target/x86_64-unknown-linux-musl/release/trip / ENTRYPOINT ["./trip"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: README.md ================================================





Trippy combines the functionality of traceroute and ping and is designed to assist with the analysis of networking issues.

trippy ## Quick Start See the [getting started](https://trippy.rs/start/getting-started) guide. ### Install Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most package managers, precompiled binaries, or source. For example, to install Trippy from `cargo`: ```shell cargo install trippy --locked ```
All package managers ### Cargo [![Crates.io](https://img.shields.io/crates/v/trippy)](https://crates.io/crates/trippy/0.13.0) ```shell cargo install trippy --locked ``` ### APT (Debian) [![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy) ```shell apt install trippy ``` > ⓘ Note: > > Only available for Debian 13 (`trixie`) and later. ### PPA (Ubuntu) [![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.13.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages) ```shell add-apt-repository ppa:fujiapple/trippy apt update && apt install trippy ``` > ⓘ Note: > > Only available for Ubuntu 24.04 (`Noble`) and 22.04 (`Jammy`). ### Snap (Linux) [![trippy](https://snapcraft.io/trippy/badge.svg)](https://snapcraft.io/trippy) ```shell snap install trippy ``` ### Homebrew (macOS) [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/trippy.svg)](https://formulae.brew.sh/formula/trippy) ```shell brew install trippy ``` ### WinGet (Windows) [![winget package](https://img.shields.io/badge/WinGet-0.13.0-brightgreen)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/f/FujiApple/Trippy/0.13.0) ```shell winget install trippy ``` ### Scoop (Windows) [![Scoop package](https://img.shields.io/scoop/v/trippy?style=flat&labelColor=5c5c5c&color=%234dc71f)](https://github.com/ScoopInstaller/Main/blob/master/bucket/trippy.json) ```shell scoop install trippy ``` ### Chocolatey (Windows) [![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy) ```shell choco install trippy ``` ### NetBSD [![pkgsrc current package](https://repology.org/badge/version-for-repo/pkgsrc_current/trippy.svg)](https://pkgsrc.se/net/trippy) ```shell pkgin install trippy ``` ### FreeBSD [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/trippy.svg)](https://www.freshports.org/net/trippy/) ```shell pkg install trippy ``` ### OpenBSD [![OpenBSD port](https://repology.org/badge/version-for-repo/openbsd/trippy.svg)](https://openports.pl/path/net/trippy) ```shell pkg_add trippy ``` ### Arch Linux [![Arch package](https://repology.org/badge/version-for-repo/arch/trippy.svg)](https://archlinux.org/packages/extra/x86_64/trippy) ```shell pacman -S trippy ``` ### Gentoo Linux [![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/trippy.svg)](https://packages.gentoo.org/packages/net-analyzer/trippy) ```shell emerge -av net-analyzer/trippy ``` ### Void Linux [![Void Linux x86_64 package](https://repology.org/badge/version-for-repo/void_x86_64/trippy.svg)](https://github.com/void-linux/void-packages/tree/master/srcpkgs/trippy) ```shell xbps-install -S trippy ``` ### ALT Sisyphus [![ALT Sisyphus package](https://repology.org/badge/version-for-repo/altsisyphus/trippy.svg)](https://packages.altlinux.org/en/sisyphus/srpms/trippy/) ```shell apt-get install trippy ``` ### Chimera Linux [![Chimera Linux package](https://repology.org/badge/version-for-repo/chimera/trippy.svg)](https://github.com/chimera-linux/cports/tree/master/user/trippy) ```shell apk add trippy ``` ### Nix [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/trippy.svg)](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/tr/trippy/package.nix) ```shell nix-env -iA trippy ``` ### Docker [![Docker Image Version (latest by date)](https://img.shields.io/docker/v/fujiapple/trippy)](https://hub.docker.com/r/fujiapple/trippy/) ```shell docker run -it fujiapple/trippy ``` ### All Repositories [![Packaging status](https://repology.org/badge/vertical-allrepos/trippy.svg)](https://repology.org/project/trippy/versions)
See the [installation](https://trippy.rs/start/installation) guide for details of how to install Trippy on your system. ### Run To run a basic trace to `example.com` with default settings, use the following command: ```shell sudo trip example.com ``` See the [usage examples](https://trippy.rs/guides/usage) and [CLI reference](https://trippy.rs/reference/cli) for details of how to use Trippy. To use Trippy without elevated privileges, see the [privileges](https://trippy.rs/guides/privileges) guide. ## Documentation Full documentation is available at [trippy.rs](https://trippy.rs).
documentation links ## Getting Started See the [Getting Started](https://trippy.rs/start/getting-started/) guide. ## Features See the [Features](https://trippy.rs/start/features/) list. ## Distributions See the [Distributions](https://trippy.rs/start/installation/) list. ## Privileges See the [Privileges](https://trippy.rs/guides/privileges/) guide. ## Usage Examples See the [Usage Examples](https://trippy.rs/guides/usage/). ## Command Reference See the [Command Reference](https://trippy.rs/reference/cli/). ## Theme Reference See the [Theme Reference](https://trippy.rs/reference/theme/). ## Column Reference See the [Column Reference](https://trippy.rs/reference/column/). ## Configuration Reference See the [Configuration Reference](https://trippy.rs/reference/configuration/). ## Locale Reference See the [Locale Reference](https://trippy.rs/reference/locale/). ## Versions See the [Version Reference](https://trippy.rs/reference/version/). ## Frequently Asked Questions ### Why does Trippy show "Awaiting data..."? See the [Awaiting Data](https://trippy.rs/guides/faq/) guide. ### How do I allow incoming ICMP traffic in the Windows Defender firewall? See the [Windows Defender Firewall](https://trippy.rs/guides/windows_firewall/) guide. ### What are the recommended settings for Trippy? See the [Recommended Tracing Settings](https://trippy.rs/guides/recommendation/) guide.
## Acknowledgements Trippy is made possible by [ratatui](https://github.com/ratatui-org/ratatui) ( formerly [tui-rs](https://github.com/fdehau/tui-rs)), [crossterm](https://github.com/crossterm-rs/crossterm) as well as [several](https://github.com/fujiapple852/trippy/blob/master/Cargo.toml) foundational Rust libraries. Trippy draws heavily from [mtr](https://github.com/traviscross/mtr) and also incorporates ideas from both [libparistraceroute](https://github.com/libparistraceroute/libparistraceroute) & [Dublin Traceroute](https://github.com/insomniacslk/dublin-traceroute). The Trippy networking code is inspired by [pnet](https://github.com/libpnet/libpnet) and some elements of that codebase are incorporated in Trippy. The [AS][autonomous_system] data is retrieved from the [IP to ASN Mapping Service](https://team-cymru.com/community-services/ip-asn-mapping/#dns) provided by [Team Cymru](https://team-cymru.com). The [trippy.cli.rs](https://trippy.cli.rs) CNAME hosting is provided by [cli.rs](https://cli.rs). The Trippy chat room is sponsored by [Zulip](https://zulip.com). Trippy logo designed by [Harun Ocaksiz Design](https://www.instagram.com/harunocaksiz). ## License This project is distributed under the terms of the Apache License (Version 2.0). Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in time by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions. See [LICENSE](LICENSE) for details. Copyright 2022 [Trippy Contributors](https://github.com/fujiapple852/trippy/graphs/contributors) [autonomous_system]: https://en.wikipedia.org/wiki/Autonomous_system_(Internet) ================================================ FILE: RELEASES.md ================================================ # Release Notes Release notes for Trippy 0.6.0 onwards. See also the [CHANGELOG](CHANGELOG.md). # 0.13.0 ## Highlights The 0.13.0 release of Trippy includes several enhancements related to Type of Service (`ToS`) and adds new `Dscp` and `Ecn` columns. It also includes improvements to the TUI such as allowing a custom timezone to be set and adding the ability to read the configuration file from the XDG app config directory. The `json` report has been enriched with start and end timestamps. This release includes a number of bug fixes. For Windows users in particular, this release includes several important stability improvements. The release also introduces a new `system` address family option, which will become the new default in the next major release. Finally, Trippy now has a dedicated website and a logo! ### Type of Service (DSCP/ECN) Improvements Trippy allows setting the Type of Service (`ToS`) for IPv4 via the `--tos` (`-Q`) command-line argument (or via the configuration file). The `ToS` value is the second byte of the `IPv4` header and encodes the Differentiated Services Code Point (`DSCP`) and Explicit Congestion Notification (`ECN`) fields. Setting the `ToS` on outgoing probe packets can influence the Quality of Service (`QoS`) used by the network devices along the path. Probe responses received from the hops along the path include the `ToS` values in the Original Datagram (the `IPv4`/ `IPv6` header of the probe packet nested inside the `ICMP` error). Examining the `ToS` value from the Original Datagram can provide useful insight into the `QoS` treatment of the probe packets by network devices along the path. This release of Trippy adds two new columns to display the `DSCP` & `ECN` values, which are derived from the `ToS` value from the Original Datagram for each hop. The new columns are: - `Dscp` (`K`): The Differentiated Services Code Point (`DSCP`) of the Original Datagram for a hop - `Ecn` (`M`): The Explicit Congestion Notification (`ECN`) of the Original Datagram for a hop The `Dscp` and `Ecn` columns are decoded from the `ToS` field of the Original Datagram. If no `ToS` value is present, then the columns will show `na`. Note that these columns show the most recent `ToS` value received from the hop and may therefore change between rounds. Well-known `DSCP` values are displayed as follows: - Default Forwarding (`DF`) aka Best Effort aka Class Selector 0 (`CS0`) - Assured Forwarding (`AFn`) - Class Selector (`CSn`) - High Priority Expedited Forwarding (`EF`) - Voice Admit (`VA`) - Lower Effort (`LE`) Unknown `DSCP` values are displayed as a hexadecimal value. The `ECN` value is displayed as follows: - Not ECN-Capable Transport (`NotECT`) - ECN Capable Transport(1) (`ECT1`) - ECN Capable Transport(0) (`ECT0`) - Congestion Experienced (`CE`) These columns are hidden by default but can be enabled as needed. For more details, see the [Column Reference](https://trippy.rs/reference/column). The following example sets the `ToS` to be `224`, which is a `DSCP` value of `CS7` (0x38) and an `ECN` value of `NotECT` (0x0), and enables the new columns: ```shell trip example.com --tos 224 --tui-custom-columns holsravbwdtKM ``` The following screenshot shows the example trace: The `ToS` field of the Original Datagram has also been added to the `json` output format as a decimal value. See [#1539](https://github.com/fujiapple852/trippy/issues/1539) for details. This release also adds support for setting the `IPv6` Traffic Class (which encodes the `DSCP` & `ECN` values in the same way as `IPv4`) via the same `--tos` (`-Q`) command-line argument (or via the configuration file). Note that setting the `IPv6` Traffic Class is not currently supported on Windows. See [#202](https://github.com/fujiapple852/trippy/issues/202) for details. Finally, a bug which caused the `--tos` (`-Q`) command-line argument to be ignored for `IPv4/UDP` tracing has been fixed in this release. See [#1540](https://github.com/fujiapple852/trippy/issues/1540) for details. ### Custom TUI Timezone Trippy shows the wall-clock time in the header of the TUI. Currently, this is set to show the local timezone of the system running Trippy. This can be problematic for users who are running Trippy in a container or on a remote system that uses a different timezone. This release adds the ability to set a custom timezone for the TUI using the `--tui-timezone` command-line argument (or via the configuration file). The timezone can be set to any valid IANA timezone identifier, such as `UTC`, `America/New_York`, or `Europe/London`. The following example sets the timezone to `UTC`: ```shell trip example.com --tui-timezone UTC ``` This can be made permanent by setting the `tui-timezone` value in the `tui` section of the configuration file: ```toml [tui] tui-timezone = "UTC" ``` See [#1513](https://github.com/fujiapple852/trippy/issues/1513) for details. ### XDG App Config Directory Trippy will now attempt to locate a `trippy.toml` or `.trippy.toml` config file in the XDG app config directory (i.e. `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy`) in addition to existing locations. This allows users to store their Trippy configuration files in a dedicated directory for Trippy, separate from other applications. The full list of locations Trippy will check for a `trippy.toml` or `.trippy.toml` config file is as follows: - the current directory - the user home directory - the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config` - the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy` - the Windows data directory (Windows only): `%APPDATA%` See [#1528](https://github.com/fujiapple852/trippy/issues/1528) for details. ### System Address Family Trippy supports tracing for both `IPv4` and `IPv6` address families. If the tracing target is supplied as a hostname, Trippy will attempt to resolve the hostname to a single `IPv4` or `IPv6` address. If the hostname resolves to both, Trippy will use the address family (`--addr-family`) configuration to determine which address family to use. The possible values for `--addr-family` are: - `ipv4` - Lookup IPv4 only - `ipv6` - Lookup IPv6 only - `ipv6-then-ipv4` - Lookup IPv6 with a fallback to IPv4 - `ipv4-then-ipv6` - Lookup IPv4 with a fallback to IPv6 [default] The current default value for `--addr-family` is `ipv4-then-ipv6`, which means that if the hostname resolves to both `IPv4` and `IPv6` addresses, Trippy will prefer the `IPv4` address family. Some users find the default behavior undesirable, as it can lead to unexpected results when the hostname resolves to a different address family than the one used by other applications on the system. This release adds a new value for `--addr-family` called `system`. This value defers the choice of address family to the first address returned by OS resolver. This means that if the hostname resolves to both `IPv4` and `IPv6` addresses, the OS resolver will determine which address family to use based on the OS configuration. Note that if the `--addr-family` value is set to `system` and the `--dns-resolve-method` is set to any value _other_ than `system` (i.e. `resolv`, `cloudflare` or `google`), then the address family lookup will effectively default to `ipv6-then-ipv4`. > **Important**: The default value for `--addr-family` will change to become `system` in the next major release of > Trippy (0.14.0). This will be a breaking change for users who rely on the current default value of `ipv4-then-ipv6`. See [#1469](https://github.com/fujiapple852/trippy/issues/1469) for details. ### Remove Address Family "downgrade" for Dublin Strategy Currently, the address families `ipv4-then-ipv6` and `ipv6-then-ipv4` are silently _downgraded_ to `ipv4` when the `dublin` ECMP strategy is used. This behaviour was previously necessary, as Trippy did not support the `dublin` ECMP strategy for `IPv6`. However, Trippy has supported the `dublin` ECMP strategy for `IPv6` since version 0.10.0. As a result, this release removes the address family _downgrade_ for the `dublin` ECMP strategy. See [#1476](https://github.com/fujiapple852/trippy/issues/1476) for details. ### Windows Stability Improvements This release includes several stability improvements for Windows. It fixes several known or potential issues that could cause crashes or memory corruption. It is recommended that all Windows users upgrade to this release. See the following issues for details: - Memory corruption on Windows ([#1527](https://github.com/fujiapple852/trippy/issues/1527)) - Socket being closed twice on Windows ([#1443](https://github.com/fujiapple852/trippy/issues/1443)) - Potential crash on Windows for adapters without unicast addresses ([#1547](https://github.com/fujiapple852/trippy/issues/1547)) - Potential use-after-free when discovering source address on Windows ([#1558](https://github.com/fujiapple852/trippy/issues/1558)) ### Start and End Timestamps in JSON report The Trippy `json` report mode has been enhanced to show the start and end timestamps for the trace. These timestamps are shown in UTC using RFC 3339 format. The following example runs a trace to `example.com` for a single round and outputs the results in `json` format: ```shell trip example.com -m json -C 1 ``` The `info` section of the output now includes the `start_timestamp` and `end_timestamp` fields: ```json { "info": { "target": { "ip": "23.192.228.80", "hostname": "example.com" }, "start_timestamp": "2025-05-04T09:50:10.383221Z", "end_timestamp": "2025-05-04T09:50:11.392039Z" } } ``` See [#1510](https://github.com/fujiapple852/trippy/issues/1510) for details. ### Reduce Tracing Verbosity The `trippy-core` crate logging is overly verbose. This release reduces all `#[instrument]` annotations from the default `info` level to the `trace` level. It also removes some tracing annotations and, in some cases, adds new ones. There are now no `info` level logs and only a handful of `debug` level logs: - Log the channel config when a channel is created (typically just once) - Log the strategy config when the tracer is started (typically just once) - Log each probe sent and received during a round (typically a handful per round) For application users there is no change; however the default logging level (`--log-filter trippy=debug`) used when `-v` is passed will now show substantially fewer logs. Users can set `--log-filter trippy=trace` to see a logging level similar to the previous default. See [#1482](https://github.com/fujiapple852/trippy/issues/1482) for details. ### Bug Fixes This release fixes a bug where ICMP packets larger than 256 bytes could cause a tracer panic. See [#1561](https://github.com/fujiapple852/trippy/issues/1561) for details. It also adds a handful of missing configuration options to the settings dialog. See [#1541](https://github.com/fujiapple852/trippy/issues/1541) for details. ### Trippy Website & Logo Trippy now has a dedicated website: https://trippy.rs The website is now the primary source of documentation. My thanks to @orhun for building the https://binsider.dev website which I ~~took inspiration from~~ shamelessly copied for Trippy. Along with the new website, Trippy (finally!) has a logo:
With thanks to [Harun Ocaksiz Design](https://www.instagram.com/harunocaksiz). See [#100](https://github.com/fujiapple852/trippy/issues/100) for details. ### New Distribution Packages Trippy has been added to the Void Linux package repository (with thanks to @icp1994!): [![Void Linux x86_64 package](https://repology.org/badge/version-for-repo/void_x86_64/trippy.svg)](https://github.com/void-linux/void-packages/tree/master/srcpkgs/trippy) ```shell xbps-install -S trippy ``` Trippy was also added to ALT Sisyphus package repository (with thanks to [Aleksandr Voyt](https://packages.altlinux.org/en/sisyphus/maintainers/sobue)!) [![ALT Sisyphus package](https://repology.org/badge/version-for-repo/altsisyphus/trippy.svg)](https://packages.altlinux.org/en/sisyphus/srpms/trippy/) ```shell apt-get install trippy ``` Finally, Trippy has been added to the Chimera Linux package repository (with thanks to @ttyyls!): [![Chimera Linux package](https://repology.org/badge/version-for-repo/chimera/trippy.svg)](https://github.com/chimera-linux/cports/tree/master/user/trippy) ```shell apk add trippy ``` ### Thanks My thanks to all Trippy contributors, package maintainers, translators and community members. Feel free to drop by the Trippy Zulip room for a chat: [![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://trippy.zulipchat.com/) Happy Tracing! # 0.12.2 ## Highlights This maintenance release of Trippy fixes a bug introduced in 0.12.0 which causes a tracer panic if `--first-ttl` is set to be greater than one. The release also addresses a longstanding bug which causes `--dns-resolve-method resolv` to ignore any value provided for `--addr-family` and therefore always use the default value of `ipv4`. Finally the help text for `--addr-family` has been corrected. See the main [0.12.0](https://github.com/fujiapple852/trippy/releases/tag/0.12.0) release note. # 0.12.1 ## Highlights This maintenance release of Trippy fixes a bug which prevented translations from working in Docker and also divests all internal use of `yaml` dependencies which were problematic to maintain on some platforms (thanks to @nc7s). See the main [0.12.0](https://github.com/fujiapple852/trippy/releases/tag/0.12.0) release note. # 0.12.0 ## Highlights The latest release of Trippy brings both cosmetic and functional improvements to the TUI, new columns, new distribution packages, and a number of bug fixes. The TUI has been updated to include a new _information bar_ at the bottom of the screen which allows for the header to be shortened and simplified. The sample history chart has been enhanced to highlight missing probes and the presentation of source and target addresses has also been simplified. As well as these cosmetic changes, the TUI has gained support for internationalization (i18n) and the ability to adjust the hop privacy setting dynamically. This release introduces three new columns, which provide novel heuristics for measuring _forward loss_ and _backward loss_, that are designed to assist users in interpreting the status of the trace. Finally, this update includes new distribution packages for Debian and Ubuntu and addresses a number of bugs. ### TUI Information Bar The TUI now includes an _information bar_ at the bottom of the screen, replacing the previous `Config` line in the header. This change shortens the header by one line, optimizing space usage while keeping the overall vertical space of the TUI unchanged. The main TUI screen now appears as shown below (120x40 terminal size): The left-hand side of the information bar displays a selection of static configuration items (in order): - The address family and tracing protocol, e.g., `IPv4/ICMP` - The privilege level, either `privileged` or `unprivileged` - The locale, e.g., English (`en`), French (`fr`), etc. The right-hand side of the information bar displays a selection of adjustable configuration items (in order): - A toggle controlling whether `ASN` information is displayed (`□ ASN` for disabled, `■ ASN` for enabled) - A toggle controlling whether hop detail mode is enabled (`□ detail` for disabled, `■ detail` for enabled) - A toggle controlling whether hostnames, IP addresses, or both are displayed (`host`, `ip`, or `both`) - The maximum `ttl` value for hop privacy, shown as `-` (privacy disabled) or a number (0, 1, 2, etc.) - The maximum number of hosts displayed per hop, shown as `-` (automatic) or a number (1, 2, etc.) In the above screenshot, the information bar indicates the trace is using `IPv4/ICMP`, is running in `privileged` mode, the locale is English (`en`), `ASN` information is displayed, hop detail mode is disabled, hostnames are displayed, the hop privacy maximum `ttl` is 2, and the maximum number of hosts per hop is set to automatic. > **Note**: The information bar displays only a small number of important settings. All other settings can be viewed in > the settings dialog, which can be opened by pressing `s` (default key binding). The theme colors of the information bar can be customized using the `info-bar-bg-color` and `info-bar-text-color` theme items. Refer to the [Theme Reference](https://github.com/fujiapple852/trippy#theme-reference) for more details. Thanks to @c-git for their valuable feedback in refining the design of the information bar. See [#1349](https://github.com/fujiapple852/trippy/issues/1349) for details. ### Sample History Missing Probes Trippy displays a history of samples for each hop as a chart at the bottom of the TUI display. Each vertical line in the chart corresponds to one sample, representing the value of the `Last` column. Previously, if a probe was lost, the sample for that round would be shown as a blank vertical line. Starting with this release, Trippy now highlights lost probes using a full vertical line in red (default theme color), making them easier to identify. The theme color for regular samples can be configured using the existing `samples-chart-color` configuration option. Additionally, the theme color for lost probes can now be customized using the new `samples-chart-lost-color` configuration option. For more details, see the [Theme Reference](https://github.com/fujiapple852/trippy#theme-reference). See [#1247](https://github.com/fujiapple852/trippy/issues/1247) for further details. ### Source and Target Address Display Improvements This release simplifies the display of the source and target addresses in the `Target` line in the header of the TUI. The `Target` line has been updated such that, for both the source and destination addresses, the hostname is only shown if it differs from the IP address. For the destination address: - If the user supplies a target hostname, it is resolved to an IP address, and both the IP address and the _provided_ hostname are shown. - If the user supplies an IP address, a reverse DNS hostname lookup is attempted. If successful, both the IP address and the _first resolved_ hostname are shown; otherwise, only the IP address is displayed. For the source address: - A reverse DNS hostname lookup is attempted. If successful, both the IP address and the _first resolved_ hostname are shown; otherwise, only the IP address is displayed. For example, when the user supplies an IP address as the tracing target, the `Target` line in the header is now shown as follows: ``` Target: 192.168.1.21 -> 93.184.215.14 (example.com) ``` See [#1363](https://github.com/fujiapple852/trippy/issues/1363) for details. ### Adjustable Hop Privacy Mode Settings Trippy includes a privacy feature designed to hide sensitive information, such as IP addresses and GeoIP data, for all hops up to a configurable maximum `ttl` via the `tui-privacy-max-ttl` configuration option. Previously, the privacy feature could only be toggled on or off within the TUI using the `toggle-privacy` command and only if `tui-privacy-max-ttl` was configured _before_ Trippy was started. In this release, the `toggle-privacy` command has been deprecated and replaced by two new TUI commands, `expand-privacy` (bound to the `p` key by default) and `contract-privacy` (bound to the `o` key by default). The `expand-privacy` command increases the `tui-privacy-max-ttl` value up to the maximum number of hops in the current trace and the `contract-privacy` command decreases the `tui-privacy-max-ttl` value to the minimum value, which disables privacy mode. See [#1347](https://github.com/fujiapple852/trippy/issues/1347) for more details. This release also repurposes the meaning of `tui-privacy-max-ttl` when set to `0`. Previously, a value of `0` indicated that no hops should be hidden. Starting from this release, a value of `0` will indicate that the source of the trace, as shown in the `Target` line of the header, should be hidden. Values of `1` or greater retain their existing behavior but will now also hide the source of the trace in addition to the specified number of hops. As a result of this change, the default value for `tui-privacy-max-ttl` has been updated: - If not explicitly set (via a command-line argument or the configuration file), nothing will be hidden by default. - If explicitly set to `0` (the previous default), the source of the trace will be hidden. See [#1365](https://github.com/fujiapple852/trippy/issues/1365) for details. ### Preserve Screen on Exit Trippy previously supported the `--tui-preserve-screen` command-line flag, which could be used to prevent the terminal screen from being cleared when Trippy exits. This feature is useful for users who wish to review trace results after exiting the application. However, the flag had to be set before starting Trippy and could not be toggled during a trace. This release introduces the `quit-preserve-screen` TUI command (bound to the `shift+q` key by default). This command allows users to quit Trippy without clearing the terminal screen, regardless of whether the `--tui-preserve-screen` flag is set. See [#1382](https://github.com/fujiapple852/trippy/issues/1382) for details. ### TUI Internationalization (i18n) The Trippy TUI has been translated into multiple languages. This includes all text displayed in the TUI across all screens and dialogs, as well as GeoIP location data shown on the world map. The TUI will automatically detect the system locale and use the corresponding translations if available. The locale can be overridden using the `--tui-locale` configuration option. Locales can be specified for a language or a combination of language and region. For example a general locale can be created for English (`en`) and specific regional locales can be created, such as United Kingdom English (`en-UK`) and United States English (`en-US`). If the user's chosen full locale (`language-region`) is not available, Trippy will fall back to using the locale for the language only, if it exists. For example if the user sets the locale to `en-AU`, which is not currently defined in Trippy, it will fall back to the `en` locale, which is defined. If the user's chosen locale does not exist at all, Trippy will fall back to English (`en`). Locales are generally added for the language only unless there is a specific need for region-based translations. Some caveats to be aware of: - The configuration file, command-line options, and most error messages are not translated. - Many common abbreviated technical terms, such as `IPv4` and `ASN`, are not translated. The following example sets the TUI locale to be Chinese (`zh`): ```shell trip example.com --tui-locale zh ``` This can be made permanent by setting the `tui-locale` value in the `tui` section of the configuration file: ```toml [tui] tui-locale = "zh" ``` The following screenshot shows the TUI with the locale set to Chinese (`zh`): The list of available locales can be printed using the `--print-locales` flag: ```shell trip --print-locales ``` As of this release, the following locales are available: - Chinese (`zh`) - English (`en`) - French (`fr`) - German (`de`) - Italian (`it`) - Portuguese (`pt`) - Russian (`ru`) - Spanish (`es`) - Swedish (`sv`) - Turkish (`tr`) See [#1319](https://github.com/fujiapple852/trippy/issues/1319), [#1357](https://github.com/fujiapple852/trippy/issues/1357), [#1336](https://github.com/fujiapple852/trippy/issues/1336) and the [Locale Reference](https://github.com/fujiapple852/trippy#locale-reference) for more details. Corrections to existing translations or the addition of new translations are always welcome. See the [tracking issue](https://github.com/fujiapple852/trippy/issues/506) for the status of each translation and details on how to contribute. Adding these translations has been a significant effort and I would like to express a huge _thank you_ (谢谢! Merci! Danke! Grazie! Obrigado! Спасибо! Gracias! Tack! Teşekkürler!) to @0323pin, @arda-guler, @histrio, @josueBarretogit, @one, @orhun, @peshay, @ricott1, @sxyazi, @ulissesf, and @zarkdav for all of their time and effort adding and reviewing translations for this release. ### Forward and Backward Packet Loss Heuristics In line with most classic traceroute tools, Trippy displays the number of probes sent (`Snd`), received (`Recv`), and a loss percentage (`Loss%`) for each hop. However, many routers are configured to rate-limit or even drop ICMP traffic. This can lead to false positives for packet loss, particularly for intermediate hops, as the lack of a response from such hops does not typically indicate genuine packet loss. This is a common source of confusion for users interpreting trace results. Trippy already provides a color-coded status column (`Sts`), that considers both packet loss percentage and whether the hop is the target of the trace, to try and assist users in interpreting the status of each hop. While this feature is helpful, it does not make it clear _why_ a hop has a particular status nor help users interpret the overall status of the trace. To further assist users, this release of Trippy introduces a pair of novel heuristics to measure _forward loss_ and _backward loss_. Informally, _forward loss_ indicates whether the loss of a probe is the _cause_ of subsequent losses and _backward loss_ indicates whether the loss of a probe is the _result_ of a prior loss on the path. More precisely: - _forward loss_ for probe `P` in round `R` occurs when probe `P` is lost in round `R` and _all_ subsequent probes within round `R` are also lost. - _backward loss_ for probe `P` in round `R` occurs when probe `P` is lost in round `R` and _any_ prior probe within round `R` has _forward loss_. These heuristics are encoded in three new columns: - `Floss` (`F`): The number of probes with _forward loss_ - `Bloss` (`B`): The number of probes with _backward loss_ - `Floss%` (`D`): The percentage of probes with _forward loss_ These columns are hidden by default but can be enabled as needed. For more details, see the [Column Reference](https://github.com/fujiapple852/trippy#column-reference). The following screenshot shows an example trace with the new columns enabled: In the following (contrived) example, after initially discovering the target (`10.0.0.105`) during the first round, genuine packet loss occurs in _all_ subsequent rounds at the third hop. This means that no probes on the common path are able to get beyond the third hop. ``` ╭Hops───────────────────────────────────────────────────────────────╮ │# Host Loss% Snd Recv Floss Bloss Floss% │ │1 10.0.0.101 0.0% 96 96 0 0 0.0% │ │2 10.0.0.102 0.0% 96 96 0 0 0.0% │ │3 No response 100.0% 96 0 95 0 98.9% │ │4 No response 100.0% 96 0 0 95 0.0% │ │5 10.0.0.105 99.0% 96 1 0 95 0.0% │ ``` From this we can determine that the loss at the third hop is classified as _forward loss_ because all subsequent probes (4th and 5th) in the same round are also lost. We can also conclude that the 4th and 5th hops have _backward loss_ starting from round two, as in those rounds a prior hop (the third hop) has _forward loss_. Note the difference between the traditional `Loss%` column and the new `Floss%` column. The `Loss%` column indicates packet loss at several hops (3rd, 4th, and 5th). In contrast, the `Floss%` column helps us determine that the true packet loss most likely occurs at the 3rd hop. It is important to stress that this technique is a _heuristic_, and both _false positives_ and _false negatives_ are possible. Some specific caveats to be aware of include: - Every probe sent in every round is an _independent trial_, meaning there is no guarantee that all probes within a given round will follow the same path (or "flow"). The concept of "forward loss" and "backward loss" assumes that all probes followed a single path. This assumption is typically met (but not guaranteed) when using tracing strategies such as ICMP, UDP/Dublin, or UDP/Paris. - Any given host on the path may drop packets for only a subset of probes sent within a round, either due to rate limiting or genuine intermittent packet loss. This could result in a false positive for "forward loss" at a given hop if all subsequent hops in the round exhibit packet loss that is not genuine. For example, in the scenario above, the hop with `ttl=3` could be incorrectly deemed to have "forward loss" if observed loss from hops `ttl=4` and `ttl=5` is not genuine (e.g., caused by rate-limiting). - A false positive for "backward loss" could occur at a hop experiencing genuine packet loss if a previous hop on the path has "forward loss" that is not genuine. In the scenario above, if the hop with `ttl=4` has genuine packet loss, it will still be marked with "backward loss" due to the "forward loss" at `ttl=3`. Despite these caveats, the addition of _forward loss_ and _backward loss_ heuristics aims to help users more accurately interpret trace outputs. However, these heuristics should be considered experimental and may be subject to change in future releases. See [#860](https://github.com/fujiapple852/trippy/issues/860) for details. ### Bug Fixes The previous release of Trippy introduced a bug ([#1290](https://github.com/fujiapple852/trippy/issues/1290)) that caused reverse DNS lookups to be enqueued multiple times when the `dns-ttl` expired, potentially leading to the hostname being displayed as `Timeout: xxx` for a brief period. A long-standing bug ([#1398](https://github.com/fujiapple852/trippy/issues/1398)) which caused the TUI sample history and frequency charts to ignore sub-millisecond samples has been fixed. This release fixes a bug ([#1287](https://github.com/fujiapple852/trippy/issues/1287)) that caused the tracer to panic when parsing certain ICMP extensions with malformed lengths. It also resolves an issue ([#1289](https://github.com/fujiapple852/trippy/issues/1289)) where the ICMP extensions mode was not being displayed in the TUI settings dialog. A bug ([#1375](https://github.com/fujiapple852/trippy/issues/1375)) that caused the cursor to not move to the bottom of the screen when exiting while preserving the screen has also been fixed. Finally, this release fixes a bug ([#1327](https://github.com/fujiapple852/trippy/issues/1327)) that caused Trippy to incorrectly reject the value `ip` for the `tui-address-mode` configuration option (thanks to @c-git). ### New Distribution Packages Trippy is now available in Debian 13 (`trixie`) and later (with thanks to @nc7s!). [![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy) ```shell apt install trippy ``` See ([#1312](https://github.com/fujiapple852/trippy/issues/1312)) for details. The official Trippy PPA for Ubuntu is now also available for the `noble` distribution. [![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.12.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages) ```shell sudo add-apt-repository ppa:fujiapple/trippy sudo apt update && apt install trippy ``` See ([#1308](https://github.com/fujiapple852/trippy/issues/1308)) for details. You can find the full list of [distributions](https://github.com/fujiapple852/trippy/tree/master#distributions) in the documentation. ### Thanks My thanks to all Trippy contributors, package maintainers, translators and community members. Feel free to drop by the Trippy Zulip room for a chat: [![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://trippy.zulipchat.com/) Happy Tracing! # 0.11.0 ## Highlights This release of Trippy adds NAT detection for IPv4/UDP/Dublin tracing, a new public API, reverse DNS lookup cache time-to-live, transient error handling for IPv4, a new ROFF manual page generator, several new columns, improved error messages and a revamped help dialog with settings tab hotkeys. There are two breaking changes, a new initial sequence number is used which impacts the default behavior of UDP tracing and two configuration fields have been renamed and moved. Finally, there are a handful of bug fixes and two new distribution packages, Chocolatey for Windows and an official PPA for Ubuntu and Debian based distributions. ### NAT Detection for IPv4/UDP/Dublin When tracing with the Dublin tracing strategy for IPv4/UDP, Trippy can now detect the presence of NAT (Network Address Translation) devices on the path. [RFC 3022 section 4.3](https://datatracker.ietf.org/doc/html/rfc3022#section-4.3) requires that "NAT to be completely transparent to the host" however in practice some fully compliant NAT devices leave behind a telltale sign that Trippy can use. Trippy will indicate if a NAT device has been detected by adding `[NAT]` at the end of the hostname. There is also a new (hidden by default) column, `Nat`, which can be enabled to show the NAT status per hop. NAT devices are detected by observing a difference in the _expected_ and _actual_ checksum of the UDP packet that is returned as the part of the Original Datagram in the ICMP Time Exceeded message. If they differ then it indicates that a NAT device has modified the packet. This happens because the NAT device must recalculate the UDP checksum after modifying the packet (i.e. translating the source port) and so the checksum in the UDP packet that is nested in the ICMP error may not, depending on the device, match the original checksum. To help illustrate the technique, consider sending the following IPv4/UDP packet (note the UDP `Checksum B` here): ``` +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ |Version| IHL |Type of Service| Total Length | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Identification |Flags| Fragment Offset | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Time to Live | Protocol | Checksum A | │ IPv4 Header +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Source Address | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Destination Address | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Source Port | Destination Port | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ UDP Header | Length | Checksum B | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ ``` Trippy expect to receive an IPv4/ICMP `TimeExceeded` (or other) error which contains the Original Datagram (OD) IPv4/UDP packet that was sent above with `Checksum B'` in the Original Datagram (OD) IPv4/UDP packet: ``` +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ |Version| IHL |Type of Service| Total Length | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Identification |Flags| Fragment Offset | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Time to Live | Protocol | Checksum C | │ IPv4 Header +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Source Address | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Destination Address | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ | Type | Code | Checksum D | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ IPv4 Payload (ICMP TE Header) | Unused | │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ |Version| IHL |Type of Service| Total Length | │ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ | Identification |Flags| Fragment Offset | │ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ | Time to Live | Protocol | Checksum A' | │ │ ICMP TE Payload (OD IPv4 Header) +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ | Source Address | │ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ | Destination Address | │ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ │ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ │ | Source Port | Destination Port | │ │ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ │ OD IPv4 Payload (UDP header) | Length | Checksum B' | │ │ │ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ │ ``` If `Checksum B'` in the UDP packet nested in the ICMP error does not match `Checksum B` in the UDP packet that was sent then Trippy can infer that a NAT device is present. This technique allows for the detection of NAT at the first hop. To detect multiple NAT devices along the path, Trippy must also check for _changes_ in the observed checksum between consecutive hops, as changes to the UDP checksum will " carry forward" to subsequent hops. This requires taking care to account for hops that do not respond. This is only possible when using the Dublin tracing strategy, as it does not modify the UDP header per probe; therefore, the checksums are expected to remain constant, allowing changes in the checksum between hops to be detected. Note that this method cannot detect all types of NAT devices and so should be used in conjunction with other methods where possible. See the [issue](https://github.com/fujiapple852/trippy/issues/1104) for more details. ### Public API Trippy has been designed primarily as a standalone _tool_, however it is built on top of a number of useful libraries, such as the core tracer, DNS resolver and more. These libraries have always existed but were tightly integrated into the tool and were not designed for use by third party crates. This release introduces the Trippy public API which can be used to build custom tools on top of the Trippy libraries. The full set of libraries exposed is: | Crate | Description | | ---------------------------------------------------- | ---------------------------------------------------- | | [trippy](https://docs.rs/trippy) | Common entrypoint crate | | [trippy-core](https://docs.rs/trippy-core) | The core Trippy tracing functionality | | [trippy-packet](https://docs.rs/trippy-packet) | Packet wire formats and packet parsing functionality | | [trippy-dns](https://docs.rs/trippy-dns) | Perform forward and reverse lazy DNS resolution | | [trippy-privilege](https://docs.rs/trippy-privilege) | Discover platform privileges | | [trippy-tui](https://docs.rs/trippy-tui) | The Trippy terminal user interface | To use the Trippy public API you should add the common entrypoint `trippy` crate to your `Cargo.toml` file and then enable the desired features. Note that the `trippy` crate includes `tui` as a default feature and so you should disable default features when using it as a library. Alternatively, it is also possible to add the crates individually. For example, to use the core Trippy tracing functionality you would add the `trippy` crate, disable default features and enable the `core` feature: ```toml [dependencies] trippy = { version = "0.11.0", default-features = false, features = ["core"] } ``` The `hello-world` example below demonstrates how to use the Trippy public API to perform a simple trace and print the results of each round: ```rust use std::str::FromStr; use trippy::core::Builder; fn main() -> anyhow::Result<()> { let addr = std::net::IpAddr::from_str("1.1.1.1")?; Builder::new(addr) .build()? .run_with(|round| println!("{:?}", round))?; Ok(()) } ``` Whilst Trippy adheres to [Semantic Versioning](https://semver.org/), the public API is not yet considered stable and may change in future releases. See [crates](crates/README.md) and the usage [examples](examples/README.md) for more information. ### New Initial Sequence For UDP tracing, by default, Trippy uses a fixed source port and a variable destination port which is set from the sequence number, starting from an initial sequence of 33000 and incremented for each probe, eventually wrapping around. By [convention](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers), many devices on the internet allow UDP probes to ports in the range 33434..=33534 and will return a `DestinationUnreachable` ICMP error, which can be used to confirm that the target has been reached. Since Trippy does not use destination ports in this range for UDP probes by default, the target host will typically not respond with an ICMP error, and so Trippy cannot know that the target was reached, and must therefore show the hop as unknown. Another issue with this default setup is that the sequence number will eventually enter the range 33434..=33534 at which point the target will _begin_ to respond with the `DestinationUnreachable` ICMP error. However, there is no guarantee that the probe sent for sequence 33434 (i.e., the first one for which the target host will be able to respond) will be for the minimum time-to-live (ttl) required to reach the target. This leads to confusing output, which is hard for users to interpret. See [issue](https://github.com/fujiapple852/trippy/issues/1203) for more details. These issues can be avoided today, either by changing the initial sequence number to be in the range 33434..=33534 by setting the `--initial-sequence` flag or by using a fixed destination port (and therefore a variable source port) in the same range by setting the `--target-port` flag. In the following example, the initial sequence number is set to 33434: ```shell trip example.com --udp --initial-sequence 33434 ``` This can be made permanent by setting the `initial-sequence` value in the `strategy` section of the configuration file: ```toml [strategy] initial-sequence = 33434 ``` In the following example, the destination port is set to 33434: ```shell trip example.com --udp --target-port 33434 ``` This can be made permanent by setting the `target-port` value in the `strategy` section of the configuration file: ```toml [strategy] target-port = 33434 ``` As the default behavior in Trippy leads to these confusing issues, this release modifies the default sequence number to be 33434. This is a **breaking change** and will impact users who rely on the old default initial sequence number. This change introduces a new problem, albeit a lesser one: UDP traces will now begin with a destination port of 33434 and so `DestinationUnreachable` ICMP errors will typically be returned by the target immediately. However, eventually the sequence number will move _beyond_ the range 33434..=33534 and so the target host will _stop_ responding with `DestinationUnreachable` ICMP errors. This leads to the appearance that the target has started dropping packets. While this is technically correct, this is not desirable behavior as the target has not really disappeared. It is therefore recommended to _always_ fix the `target-port` to be in the range 33434..=33534 for UDP tracing and allow the source port to vary instead. This may become the default behavior for UDP tracing in a future release; that would represent a significant difference in default behavior compared to most traditional Unix traceroute tools, which vary the destination port by default. ### Reverse DNS Lookup Cache Time-to-live Trippy performs a reverse DNS lookup for each host encountered during the trace and the resulting hostnames are cached indefinitely. This can lead to stale hostnames being displayed in the TUI if they change after the trace has begun. Note that the DNS cache can be flushed manually by pressing `ctrl+k` (default key binding) in the TUI. Starting from this release, the reverse DNS cache can be configured to expire after a certain time to live. By default this is set to be 5 minutes (300 seconds) and can be configured using the `--dns-ttl` flag or the `dns-ttl` configuration option. The following example sets the DNS cache time-to-live to 30 seconds: ```shell trip example.com --dns-ttl 30s ``` This can be made permanent by setting the `dns-ttl` value in the `dns` section of the configuration file: ```toml [dns] dns-ttl = "30s" ``` ### Transient Error Handling for IPv4 Trippy records the number of probes sent and the number of probes received for each hop and uses this information to calculate packet loss. Any probe that is _successfully_ sent for which no response is received is considered lost. Currently, if a probe cannot be sent for any reason, then Trippy will crash and show a BSOD. This is not typically an issue, as such failures imply a local issue with the host network configuration rather than an issue with the target or any intermediate hops. However, it is possible that a probe may fail to send for a transient reason, such as a temporary local host issue, and so it would be useful to be able to handle such errors gracefully. A common example would be running Trippy on a host and during the trace disabling the network interface. Starting from this release, Trippy will continue the trace even if a probe fails to send and will instead show a warning to the user in the TUI about the number of probe failures. A new column (hidden by default), `Fail`, has also been added to the TUI to show the number of probes that failed to send for each hop. This has been implemented for macOS, Linux and Windows for IPv4 only. Support for IPv6 and other platforms will be added in future releases. See the [tracking issue](https://github.com/fujiapple852/trippy/issues/1238) for more details. ### Generate ROFF Man Page Trippy can now generate manual pages in ROFF format. This can be useful for users who wish to install Trippy on systems which do not have a package manager or for users who wish to install Trippy from source. It can also be used by package maintainers to generate manual pages for their distribution. The following command generates a ROFF manual page for Trippy: ```shell trip --generate-man > /path/to/man/pages/trip.1 ``` ### New Columns This release introduced several new columns, all of which are hidden by default. These are: - `Type`: The ICMP packet type for the last probe for the hop - `Code`: The ICMP packet code for the last probe for the hop - `Nat`: The NAT detection status for the hop - `Fail`: The number of probes which failed to send for the hop The following shows the `Type` and `Code` columns: See the [Column Reference](https://github.com/fujiapple852/trippy#column-reference) for a full list of all available columns. ### Settings Dialog Tab Hotkeys The settings dialog can be accessed by pressing `s` (default key binding) and users can navigate between the tabs using the left and right arrow keys (default key bindings). This release introduces hotkeys to allow users to jump directly to a specific tab by pressing `1`-`7` (default key bindings). See the [Key Bindings Reference](https://github.com/fujiapple852/trippy#key-bindings-reference) for details. ### Help Dialog Revamped The existing Trippy help dialog shows a hardcoded list of key bindings which may not reflect the actual key bindings the user has configured. Trippy shows the correct key bindings in the settings dialog which can be accessed by pressing `s` (default key binding) and navigating to the Bindings tab. Therefore, the key bindings in the help dialog are both potentially incorrect and redundant. This release revamps the help dialog and includes instructions on how to access the key bindings from the settings dialog as well as some other useful information. ### Improved Error Messages Error reporting has been improved for parameters such as `--min-round-duration` (`-i`). Previously, if an invalid duration was provided, the following error would be reported: ```shell $ trip example.com -i 0.05 Error: invalid character at 1 ``` Starting from this release, such error will instead be shown as: ```shell $ trip example.com -i 0.05 error: invalid value '0.05' for '--min-round-duration ': expected time unit (i.e. 100ms, 2s, 1000us) For more information, try '--help'. ``` This covers all "duration" parameters, namely: - `min_round_duration` - `max_round_duration` - `grace_duration` - `read_timeout` - `dns_timeout` - `tui_refresh_rate` ### Renamed Configuration The following configuration fields have been renamed and moved from the `[tui]` to the `[strategy]` section in the configuration file: - `tui-max-samples` -> `max-samples` - `tui-max-flows` -> `max-flows` This is a **breaking change**. Attempting to use the legacy field names will result in an error pointing to the new name. The following example shows the error reported if the old names are used from the command line: ```shell error: unexpected argument '--tui-max-samples' found tip: a similar argument exists: '--max-samples' ``` The following examples shows the error reported if the ld names are used from the configuration file: ```shell Error: tui-max-samples in [tui] section is deprecated, use max-samples in [strategy] section instead ``` ### Bug Fixes This release fixes a bug where `DestinationUnreachable` ICMP errors were assumed to have been sent by the target host, whereas they may also be sent by an intermediate hop. Another fix addresses an issue where the TUI would calculate the maximum number of hops to display based on the maximum observed across all rounds rather than for the latest round. Finally, a minor bug was fixed where `AddressInUse` and `AddrNotAvailable` errors were being conflated. ### New Distribution Packages Trippy has been added to the Chocolatey community repository (with thanks to @Aurocosh!): [![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy) ```shell choco install trippy ``` Trippy also has an official PPA for Ubuntu and Debian based distributions (with thanks to @zarkdav!): [![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.11.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages) ```shell sudo add-apt-repository ppa:fujiapple/trippy sudo apt update && apt install trippy ``` You can find the full list of [distributions](https://github.com/fujiapple852/trippy/tree/master#distributions) in the documentation. ### Thanks My thanks to all Trippy contributors, package maintainers and community members. Feel free to drop by the new Trippy Zulip room for a chat: [![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://trippy.zulipchat.com/) Happy Tracing! # 0.10.0 ## Highlights The first release of 2024 is packed with new features, such as customizable columns, jitter calculations, Dublin tracing strategy for IPv6/UDP, support for IPinfo GeoIp files, enhanced DNS resolution with IPv6/IPv4 fallback and CSS named colors for the TUI as well as a number of bug fixes. Since the last release there has also been a significant improvement in automated testing, notably the introduction of TUN based simulation testing for IPv4. ### Customize Columns #### Customize Columns in TUI It is now possible to customize which columns are shown in the TUI and to adjust the order in which they are displayed. This customization can be made from within the TUI or via configuration. To customize the columns from the TUI you must open the settings dialog (`s` key) and navigating to the new `Columns` tab (left and right arrow keys). From this tab you can select the desired column (up and down arrow keys) and toggle the column visibility on and off (`c` key) or move it left (`,` key) or right (`.` key) in the list of columns. columns You can supply the full list of columns, in the desired order, using the new `--tui-custom-columns` command line argument. The following example specifies the standard list of columns in the default order: ```shell trip example.com --tui-custom-columns holsravbwdt ``` Alternatively, to make the changes permanent you may add the `tui-custom-columns` entry to the `tui` section of the Trippy configuration file: ```toml [tui] tui-custom-columns = "holsravbwdt" ``` Note that the value of `tui-custom-columns` can be seen in the corresponding field of the `Tui` tab of the settings dialog and will reflect any changes made to the column order and visibility via the Tui. This can be useful as you may copy this value and use it in the configuration file directly. tui-custom-columns #### New Columns This release also introduced several new columns, all of which are hidden by default. These are: - Last source port: The source port for last probe for the hop - Last destination port: The destination port for last probe for the hop - Last sequence number: The sequence number for the last probe for the hop - Jitter columns: see the "Calculate and Display Jitter" section below See the [Column Reference](https://github.com/fujiapple852/trippy#column-reference) for a full list of all available columns. #### Column Layout Improvement The column layout algorithm used in the hop table has been improved to allow the maximum possible space for the `Host` column. The width of the `Host` column is now calculated dynamically based on the terminal width and the set of columns currently configured. ### Calculate and Display Jitter Trippy can now calculate and display a variety of measurements related to _jitter_ for each hop. Jitter is a measurement of the difference in round trip time between consecutive probes. Specifically, the following new calculated values are available in Trippy `0.10.0`: - Jitter: The round-trip-time (RTT) difference between consecutive rounds for the hop - Average Jitter: The average jitter of all probes for the hop - Maximum Jitter: The maximum jitter of all probes for the hop - Inter-arrival Jitter: The smoothed jitter value of all probes for the hop These values are always calculated and are included in the `json` report. These may also be displayed as columns in the TUI, however they are not shown by default. To enabled these columns in the TUI, please see the [Column Reference](https://github.com/fujiapple852/trippy#column-reference). jitter ### Dublin Tracing Strategy for IPv6/UDP The addition of support for the [dublin](https://github.com/insomniacslk/dublin-traceroute) tracing strategy for IPv6/UDP marks the completion of a multi-release journey to provide support for both Dublin and [paris](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum) tracing strategies for both IPv4/UDP and IPv6/UDP. As a reminder, unlike classic traceroute and MTR, these alternative tracing strategies do not encode the probe sequence number in either the src or dest port of the UDP packet, but instead use other protocol and address family specific techniques. Specifically, the Dublin tracing strategy for IPv6/UDP varies the length of the UDP payload for this purpose. By doing so, these strategies are able to keep the src and dest ports fixed which makes it much more likely (though not guaranteed) that each round of tracing will follow the same path through the network (note that this is not true for the return path). The following command runs an IPv6/UDP trace using the Dublin tracing strategy with fixed src and dest ports: ```shell trip example.com --udp -6 -R dublin -S 5000 -P 3500 ``` Note that, for both Paris and Dublin tracing strategies, if you fix either the src or dest ports (but _not_ both) then Trippy will vary the unfixed port _per round_ rather than _per hop_. This has the effect that all probes _within_ a round will likely follow the same network path but probes _between_ round will follow different paths. This can be useful in conjunction with flows (`f` key) to visualize the various paths packet flow through the network. See this [issue](https://github.com/fujiapple852/trippy/issues/1007) for more details. ipv6_dublin With UDP support for the Paris and Dublin tracing strategies now complete, what remains is adding support for these for the TCP protocol. Refer to the [ECMP tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details. ### IPinfo GeoIp Provider Trippy currently supports the ability to lookup and display GeoIp information from MMDB files, but prior to `0.10.0` only the [MaxMind](https://www.maxmind.com) "GeoLite2 City" (and lite) MMDB files were supported. This release introduces support for the "IP to Country + ASN Database" and "IP to Geolocation Extended Database" MMDB files from [IPinfo](https://ipinfo.io). The "IP to Country + ASN Database" MMDB file provided by IPinfo can be used as follows: ```shell trip example.com --geoip-mmdb-file /path/to/country_asn.mmdb --tui-geoip-mode short ``` These settings can be made permanent by setting the following values in the `tui` section of the configuration file: ```toml [tui] geoip-mmdb-file = "/path/to/country_asn.mmdb" tui-geoip-mode = "short" ``` ### Enhanced DNS Resolution with IPv4/IPv6 Fallback When provided with a DNS name such as `example.com` Trippy tries to resolve it to an IPv4 or an IPv6 address and fails if no such IP exists for the configured `addr-family` mode, which must be either IPv4 or IPv6. Starting from version `0.10.0`, Trippy can be configured to support `ipv4-then-ipv6` and `ipv6-then-ipv4` modes for `addr-family`. In the new `ipv4-then-ipv6` mode Trippy will first attempt to resolve the given hostname to an IPv4 address and, if no such address exists, it will attempt to resolve to an IPv6 address and only fail if neither are available (and the opposite for the new `ipv6-then-ipv4` mode). The `addr-family` mode may also be set to be `ipv4` or `ipv6` for IPv4 only and IPv6 only respectively. To set the `addr-family` to be IPv6 with fallback to IPv4 you can set the `--addr-family` command line parameter: ```shell trip example.com --addr-family ipv6-then-ipv4 ``` To make the change permanent you can set the `addr-family` value in the `strategy` section of the configuration file: ```toml [strategy] addr-family = "ipv6-then-ipv4" ``` Note that Trippy supports both the `addr-family` entry in the configuration file and also the `--ipv4` (`-4`) and `--ipv6` (`-6`) command line flags, all of which are optional. The command line flags (which are mutually exclusive) take precedence over the config file entry and if neither are provided there it defaults to `ipv4-then-ipv6`. ### Extended Colors in TUI Trippy allows the theme to be customized and supports the named [ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors): Black, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, DarkGray, LightRed, LightGreen, LightYellow, LightBlue, LightMagenta, LightCyan, White The `0.10.0` release adds support for CSS [named colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color) ( e.g. SkyBlue). Note that these are only supported on some platforms and terminals and may not render correctly elsewhere. See the [Theme Reference](https://github.com/fujiapple852/trippy#theme-reference) ### Simulation Testing Manually testing all Trippy features in all modes and on all supported platforms is an increasingly time consuming and error prone activity. Since the last release a significant effort has been made to increase the testing coverage, including unit and integration testing. In particular, the introduction of simulation testing allows for full end-to-end testing of all modes and features on Linux, macOS and Windows without the need to mock or stub any behaviour _within_ Trippy. This is achieved by creating a [TUN](https://en.wikipedia.org/wiki/TUN/TAP) device to simulate the behavior of network nodes, responding to various pre-configured scenarios like packet loss and out-of-order arrivals. Whilst not a change that directly benefits end users, this new testing approach should reduce the effort needed to test each release of Trippy and help improve the overall reliability of the tool. Note that the simulation testing is currently only supported for IPv4. See the [Integration Testing](https://github.com/fujiapple852/trippy/issues/759) tracking issue for more details. ### Thanks My thanks to all Trippy contributors, package maintainers and community members. Feel free to drop by the Trippy Matrix room for a chat: [![#trippy-dev:matrix.org](https://img.shields.io/badge/matrix/trippy-dev:matrix.org-blue)](https://matrix.to/#/#trippy-dev:matrix.org) Happy Tracing! # 0.9.0 ## Highlights Trippy `0.9.0` introduces many new features, including tracing flows and ICMP extensions, the expansion of support for the Paris tracing strategy to encompass IPv6/UDP, an unprivileged execution mode for macOS, a hop privacy mode and many more. Additionally, this release includes several important bug fixes along with a range of new distribution packages. ### Tracing Flows #### Flow ID A tracing flow represents the sequence of hosts traversed from the source to the target. Trippy is now able to identify individual flows within a trace and assign each a unique flow id. Trippy calculate a flow id for each round of tracing, based on the sequence of hosts which responded during that round, taking care to account for rounds in which only a subset of hosts responded. Tracing statistics, such as packet loss % and average RTT are recorded on a per-flow basis as well as being aggregated across all flow. Tracing flows adds to the existing capabilities provided by Trippy to assist with [ECMP](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) (Equal-Cost Multi-Path Routing) when tracing with UDP and TCP protocols. Some of these capabilities, such as the [paris](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum) and [dublin](https://github.com/insomniacslk/dublin-traceroute) tracing strategies, are designed to _restrict_ tracing to a single flow, whilst others, such as the hop detail navigation mode (introduce in the last release) and tracing flows, are designed to help _visualize_ tracing data in the presence of multiple flows. See the `0.8.0` [release note](https://github.com/fujiapple852/trippy/releases/tag/0.8.0) for other such capabilities. #### Tracing Flows in the TUI The TUI has been enhanced with a new mode to help visualise flows. This can be toggled on and off with the `toggle-flows` command (bound to the `f` key by default). When toggled on, this mode display flow information as a chart in a new panel above the hops table. Flows can be selected by using the left and right arrow keys (default key bindings). Flows are sorted by the number of rounds in which a given flow id was observed, with the most frequent flow ids shown on the left. When entering this mode flow id 1 is selected automatically. The selected flow acts as a filter for the other parts of the TUI, including the hops table, chart and maps views which only show data relevant to that specific flow. flows When toggled off, Trippy behaves as it did in previous versions where aggregated statistics (across all flows) are shown. Note that per-flow data is always recorded, the toggle only influences how the data is displayed. The number of flows visible in the TUI is limited and can be controlled by the `tui-max-flows` configuration items which can be set via the command line or via the configuration file. By default up to 64 flows are shown. The flows panel, as with all other parts of the TUI, can also be themed, see the [theme reference](https://github.com/fujiapple852/trippy#theme-reference) for details. #### Flow Reports As well as visualising flows in the TUI, Trippy `0.9.0` introduces two new reports which make use of the tracing flow data. The new `flows` report mode records and print all flows observed during tracing. The following command will run a TCP trace for 10 round and report all of the flows observed: ```shell trip example.com --tcp -m flows -C 10 ``` Sample output (truncated) showing three unique flows: ```text flow 1: 192.168.1.1, 10.193.232.245, 218.102.40.38, 10.195.41.9, 172.217.27.14 flow 2: 192.168.1.1, 10.193.232.245, 218.102.40.22, 10.195.41.17, 172.217.27.14 flow 3: 192.168.1.1, 10.193.232.245, 218.102.40.38, 10.195.41.1, 172.217.27.14 ``` Another new report, `dot`, outputs a [GraphViz](https://graphviz.org/) [`DOT`](https://graphviz.org/doc/info/lang.html) format chart of all hosts observed during tracing. The following command will run a TCP trace for 10 round and output a graph of flows in `DOT` format: ```shell trip example.com --tcp -m dot -C 10 ``` If you have a tool such as `dot` (Graphviz) installed you can use this to rendered the output in various formats, such as PNG: ```shell trip example.com --tcp -m dot -C 10 | dot -Tpng > path.png ``` Sample output: dot ### ICMP Extensions #### Parsing Extensions Trippy `0.9.0` adds the ability to parse and display ICMP Multi-Part Messages (aka extensions). It supports both compliant and non-compliant ICMP extensions as defined in [section 5 of rfc4884](https://www.rfc-editor.org/rfc/rfc4884#section-5). Trippy is able to parse and render any generic Extension Object but is also able to parse some well known Object Classes, notably the MPLS class. Support for [additional classes](https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xml#icmp-parameters-ext-classes) will be added to future versions of Trippy, see the ICMP Extensions [tracking issue](https://github.com/fujiapple852/trippy/issues/33). Parsing of ICMP extensions can be enabled by setting the `--icmp-extensions` (`-e`) command line flag or by adding the `icmp-extensions` entry in the `strategy` section of the configuration file: ```toml [strategy] icmp-extensions = true ``` #### ICMP Extensions in the TUI The TUI has been enhanced to display ICMP extensions in both the normal and hop detail navigation modes. In normal mode, ICMP extensions are not shown by default but can be enabled by setting the `--tui-icmp-extension-mode` command line flag or by adding the `tui-icmp-extension-mode` entry in the `tui` section of the configuration file: ```toml [tui] tui-icmp-extension-mode = "full" ``` This can be set to `off` (do not show ICMP extension data), `mpls` (shows a list of MPLS label(s) per hop), `full` ( shows all details of all extensions, such as `ttl`, `exp` and `bos` for MPLS) or `all` (the same as `full` but also shows `class`, `subtype` and `bytes` for unknown extension objects). The following screenshot shows ICMP extensions in normal mode with `tui-icmp-extension-mode` set to be `mpls`: extensions In hop detail mode, the full details of all ICMP extension objects are always shown if parsing of ICMP extensions is enabled. The following screenshot shows ICMP extensions in hop detail mode: extensions_detail #### ICMP Extensions in Reports ICMP extension information is also included the `json` and `stream` report modes. Sample output for a single hop from the `json` report: ```json { "ttl": 14, "hosts": [ { "ip": "129.250.3.125", "hostname": "ae-4.r25.sttlwa01.us.bb.gin.ntt.net" } ], "extensions": [ { "mpls": { "members": [ { "label": 91106, "exp": 0, "bos": 1, "ttl": 1 } ] } } ], "loss_pct": "0.00", "sent": 1, "last": "178.16", "recv": 1, "avg": "178.16", "best": "178.16", "worst": "178.16", "stddev": "0.00" } ``` ### Paris Tracing Strategy for IPv6/UDP The work to support the remaining [paris](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum) and [dublin](https://github.com/insomniacslk/dublin-traceroute) tracing modes continues in this release with the addition of support for the Paris tracing strategy for IPv6/UDP. As a reminder, unlike classic traceroute and MTR, these alternative tracing strategies do not encode the probe sequence number in either the src or dest port of the UDP or TCP packet, but instead use other protocol and address family specific techniques. Specifically, the Paris tracing strategy for IPv6/UDP utilizes the UDP checksum for this purposes and manipulates the UDP payload to ensure packets remind valid. By doing so, these strategies are able to keep the src and dest ports fixed which makes it much more likely (though not guaranteed) that each round of tracing will follow the same path through the network (note that this is _not_ true for the return path). The following command runs a IPv6/UDP trace using the `paris` tracing strategy with fixed src and dest ports: ```shell trip example.com --udp -6 -R paris -S 5000 -P 3500 ``` Refer to the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details of the work remaining to support all ECMP strategies for both UDP and TCP for IPv4 and IPv6. ### Unprivileged Mode Trippy normally requires elevated privileges due to the use of raw sockets. Enabling the required privileges for a given platform can be achieved in several ways as in described the [privileges](https://github.com/fujiapple852/trippy#privileges) section of the documentation. This release of Trippy adds the ability to run _without_ elevated privileged on a subset of platforms, but with some limitations which are described below. The unprivileged mode can be enabled by adding the `--unprivileged` (`-u`) command line flag or by adding the `unprivileged` entry in the `trippy` section of the configuration file: ```toml [trippy] unprivileged = true ``` The following command runs a trace in unprivileged mode: ```shell trip example.com -u ``` Unprivileged mode is currently only supported on macOS. Linux support is possible and may be added in the future. Unprivileged mode is not supported on NetBSD, OpenBSD, FreeBSD or Windows as these platforms do not support the `IPPROTO_ICMP` socket type. Unprivileged mode does not support the `paris` or `dublin` tracing strategies as these require raw sockets in order to manipulate the UDP and IP header respectively. See [#101](https://github.com/fujiapple852/trippy/issues/101) for further information. ### Resolve All DNS Trippy can be provided with either an IP address or a hostname as the target for tracing. Trippy will resolve hostnames to IP addresses via DNS lookup (using the configured DNS resolver, see the existing `--dns-resolve-method` flag) and pick an arbitrary IP address from those returned. Trippy also has the ability to trace to several targets simultaneously (for the ICMP protocol only) and can be provided with a list of IP addresses and hostnames. Trippy `0.9.0` combined these features and introduces a convenience flag `--dns-resolve-all` which resolves a given hostname to all IP addresses and will begin to trace to all of them simultaneously. dns_resolve_all ### Hop Privacy At times it is desirable to share tracing information with others to help with diagnostics of a network problem. These traces can contain sensitive information, such as IP addresses, hostnames and GeoIp details of the internet facing hops. Users often wish to avoid exposing this data and are forced to redact the tracing output or screenshots. Trippy `0.9.0` adds a new privacy feature, which hides all sensitive information for a configurable number of hops in the hops table, chart and GeoIP world map. The following screenshot shows the world map view with the sensitive information of some hops hidden: privacy The following command will hide all sensitive information for the first 3 hops (ttl 1, 2 & 3) in the TUI: ```shell trip example.com --tui-privacy-max-ttl 3 ``` This can also be made the default behaviour by setting the value in the Trippy configuration file: ```toml [tui] tui-privacy-max-ttl = 3 ``` From within the TUI the privacy mode can be toggled on and off using the `toggle-privacy` TUI command (bound to the `p` key by default). Note the toggle is only available if `tui-privacy-max-ttl` is configured to be non-zero. Privacy mode is entered automatically on startup to avoid any accidental exposure of sensitive data, such as when sharing a screen. ### Print Config Template The `0.8.0` release of Trippy introduced a [configuration file](https://github.com/fujiapple852/trippy#configuration-reference) and provided a sample configuration file you could download. This release adds a command which generates a configuration template appropriate for the specific version of Trippy. The following command generates a `trippy.toml` configuration file with all possible configuration options specified and set to their default values: ```shell trip --print-config-template > trippy.toml ``` ### Alternative Help Key Binding Can't decide whether you want to use `h` or `?` to display help information? Well fear not, Trippy now supports an `toggle-help-alt` TUI command (bound to the `?` key by default) in additional to the existing `toggle-help` TUI command (bound to the `h` key by default). ### Improvements to Reports This release fixes a bug that prevented reverse DNS lookup from working in all reporting modes. The list of IPs associated with a given hop have also been added to the `csv` and all tabular reports. ICMP extension data has also been included in several reports. Note that these are breaking change as the output of the reports has changed. ### New Binary Asset Downloads The list of operating systems, CPU architectures and environments which have pre-build binary assets available for download has been greatly expanded for the `0.9.0` release. This includes assets for Linux, macOS, Windows, NetBSD and FreeBSD. Assets are available for `x86_64`, `aarch64` and `arm7` and includes builds for various environments such as `gnu` and `musl` where appropriate. There are also pre-build `RPM` and `deb` downloads available. See the [Binary Asset Download](https://github.com/fujiapple852/trippy#binary-asset-download) section for a full list. Note that Trippy `0.9.0` has only been [tested](https://github.com/fujiapple852/trippy/issues/836) on a small subset of these platforms. ### New Distribution Packages Since the last release Trippy has been added as an official WinGet package (kudos to @mdanish-kh and @BrandonWanHuanSheng!) and can be installed as follows: ```shell winget install trippy ``` Trippy has also been added to the scoop `Main` bucket (thanks to @StarsbySea!) and can be installed as follows: ```shell scoop install trippy ``` You can find the full list of [distributions](https://github.com/fujiapple852/trippy/tree/master#distributions) in the documentation. ### Thanks My thanks to all Trippy contributors, package maintainers and community members. Feel free to drop by the Trippy Matrix room for a chat: [![#trippy-dev:matrix.org](https://img.shields.io/badge/matrix/trippy-dev:matrix.org-blue)](https://matrix.to/#/#trippy-dev:matrix.org) Happy Tracing! ## New Contributors - @c-git made their first contribution in https://github.com/fujiapple852/trippy/pull/632 - @trkelly23 made their first contribution in https://github.com/fujiapple852/trippy/pull/788 # 0.8.0 ## Highlights The `0.8.0` release of Trippy brings several new features, UX enhancements, and quality of life improvements, as well as various small fixes and other minor improvements. #### Hop Detail Navigation Trippy offers various mechanisms to visualize [ECMP](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) ( Equal-Cost Multi-Path Routing) when tracing with UDP and TCP protocols. Features include displaying all hosts for a given hop in a scrollable table, limiting the number of hosts shown per hop (showing the % of traffic for each host), and greying out hops that are not part of a specific tracing round. Despite these helpful features, visualizing a complete trace can be challenging when there are numerous hosts for some hops, which is common in environments where ECMP is heavily utilized. This release enhances ECMP visualization support by introducing a hop detail navigation mode, which can be toggled on and off by pressing `d` (default key binding). This mode displays multiline information for the selected hop only, including IP, hostname, AS, and GeoIP details about a single host for the hop. Users can navigate forward and backward between hosts in a given hop by pressing `,` and `.` (default key bindings), respectively. In addition to visualizing ECMP, Trippy also supports alternative tracing strategies to assist with ECMP routing, which are described below. #### Paris Tracing Strategy Trippy already supports both classic and [dublin](https://github.com/insomniacslk/dublin-traceroute) tracing strategies, and this release adds support for the [paris](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum) tracing strategy for the UDP protocol. Unlike classic traceroute and MTR, these alternative tracing strategies do not encode the probe sequence number in either the src or dest port of the UDP or TCP packet, but instead use other protocol and address family specific techniques. This means that every probe in a trace can share common values for the src & dest hosts and ports which, when combined with the protocol, is typically what is used to making traffic route decisions in ECMP routing. This means that these alternative tracing strategies significantly increase the likelihood that the same path is followed for each probe in a trace (but not the return path!) in the presence of ECMP routing. The following command runs a UDP trace using the new `paris` tracing strategy with fixed src and dest ports (the src and dest hosts and the protocol are always fixed) and will therefore likely follow a common path for each probe in the trace: ```shell trip www.example.com --udp -R paris -S 5000 -P 3500 ``` Future Trippy versions will build upon these strategies and further improve the ability to control and visualize ECMP routing, refer to the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for further details. #### GeoIp Information & Interactive Map Trippy now supports the ability to look up and display GeoIP information from a user-provided MaxMind [GeoLite2 City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data). This information is displayed per host in the hop table (for both normal and new detail navigation modes) and can be shown in various formats. For example, short form like "San Jose, CA, US" or long form like "San Jose, California, United States, North America," or latitude, longitude, and accuracy radius like "37.3512, -121.8846 (~20km)". The following command enables GeoIP lookup from the provided `GeoLite2-City.mmdb` file and will show long form locations in the hop table: ```shell trip example.com --geoip-mmdb-file GeoLite2-City.mmdb --tui-geoip-mode long ``` Additionally, Trippy features a new interactive map screen that can be toggled on and off by pressing `m` (default key binding). This screen displays a world map and plots the location of all hosts for all hops in the current trace, as well as highlighting the location of the selected hop. #### Autonomous System Display Enhancements Trippy has long offered the ability to look up and display AS information. This release makes this feature more flexible by allowing different AS details to be shown in the hops table, including AS number, AS name, prefix CIDR, and registry details. The following command enables AS lookup and will display the prefix CIDR for each host in the TUI: ```shell trip example.com -z true -r resolv --tui-as-mode prefix ``` This release also fixes a limitation in earlier versions of Trippy that prevented the lookup of AS information for IP addresses without a corresponding `PTR` DNS record. #### UI Cleanup & Configuration Dialog The number of configurable parameters in Trippy has grown significantly, surpassing the number that can be comfortably displayed in the TUI header section. Previous Trippy versions displayed an arbitrarily chosen subset of these parameters, many of which have limited value for users and consume valuable screen space. This release introduces a new interactive settings dialog that can be toggled on and off with `s` (default key binding) to display all configured parameters. The TUI header has also been cleaned up to show only the most relevant information, specifically the protocol and address family, the AS info toggle, the hop details toggle, and the max-hosts setting. #### Configuration File The previous Trippy release introduced the ability to customize the TUI color theme and key bindings, both of which could be specified by command-line arguments. While functional, this method is inconvenient when configuring a large number of colors or keys. This release adds support for a Trippy configuration file, allowing for persistent storage of color themes, key bindings, and all other configuration items supported by Trippy. For a sample configuration file showing all possible configurable items that are available, see the [configuration reference](https://github.com/fujiapple852/trippy#configuration-reference) for details. #### Shell Completions This release enables the generation of shell completions for various shells, including bash, zsh, PowerShell, and fish, using the new `--generate` command-line flag. The following command will generate and store shell completions for the fish shell: ```shell trip --generate fish > ~/.config/fish/completions/trip.fish ``` #### Improved Error Reporting & Debug Logging This release adds a number of command-line flags to enable debug logging, enhancing the ability to diagnose failures. For example, the following command can be used to run tracing with no output, except for debug output in a format suitable to be displayed with `chrome://tracing` or similar tools: ```shell trip www.example.com -m silent -v --log-format chrome ``` Socket errors have also been augmented with contextual information, such as the socket address for a bind failure, to help with the diagnosis of issues. #### New Distribution Packages Trippy is now also available as a Nix package (@figsoda), a FreeBSD port (@ehaupt) and a Windows Scoop package. This release also re-enables support for a `musl` binary which was disabled in `0.7.0` due to a bug in a critical library used by Trippy. See [distributions](https://github.com/fujiapple852/trippy#distributions) for the full list of available packages. My thanks, as ever, to all Trippy contributors! ## New Contributors - @utkarshgupta137 made their first contribution in https://github.com/fujiapple852/trippy/pull/537 # 0.7.0 ## Highlights The major highlight of the 0.7.0 release of Trippy is the addition of full support for Windows, for all tracing modes and protocols! 🎉. This has been many months in the making and is thanks to the hard work and perseverance of @zarkdav. This release also sees the introduction of custom Tui themes and key bindings, `deb` and `rpm` package releases, as well as several important bug fixes. My thanks to all the contributors! # 0.6.0 ## Highlights The first official release of Trippy! ================================================ FILE: crates/README.md ================================================ ## Crates The following is a list of the crates defined by Trippy and their purposes: ### `trippy` A binary crate for the Trippy application and a library crate. This is the crate you would use if you wish to install and run Trippy as a standalone tool. ```shell cargo install --locked trippy ``` It can also be used as library for crates that wish to use the Trippy tracing functionality. > [!NOTE] > The `trippy` crate has `tui` as a default feature and so you should disable default features when using it as a > library. ```shell cargo add trippy --no-default-features --features core,dns ``` ### `trippy-core` A library crate providing the core Trippy tracing functionality. This crate is used by the Trippy application and is the crate you would use if you wish to provide the Trippy tracing functionality in your own application. ```shell cargo add trippy-core ``` ### `trippy-packet` A library crate which provides packet wire formats and packet parsing functionality. This crate is used by the Trippy application and is the crate you would use if you wish to provide packet parsing functionality in your own application. ```shell cargo add trippy-packet ``` ### `trippy-dns` A library crate for performing forward and reverse lazy DNS resolution. This crate is designed to be used by the Trippy application but may also be useful for other applications that need to perform forward and reverse lazy DNS resolution. ```shell cargo add trippy-dns ``` ### `trippy-privilege` A library crate for discovering platform privileges. This crate is designed to be used by the Trippy application but may also be useful for other applications. ```shell cargo add trippy-privilege ``` ### `trippy-tui` A library crate for the Trippy terminal user interface. ```shell cargo add trippy-tui ``` ================================================ FILE: crates/trippy/Cargo.toml ================================================ [package] name = "trippy" description = "A network diagnostic tool" version.workspace = true authors.workspace = true documentation.workspace = true homepage.workspace = true repository.workspace = true readme = "README.md" license.workspace = true edition.workspace = true rust-version.workspace = true keywords.workspace = true categories.workspace = true [[bin]] name = "trip" path = "src/main.rs" required-features = ["tui"] [features] default = ["tui"] tui = ["trippy-tui", "anyhow"] core = ["trippy-core"] privilege = ["trippy-privilege"] dns = ["trippy-dns"] packet = ["trippy-packet"] [dependencies] trippy-tui = { workspace = true, optional = true } trippy-core = { workspace = true, optional = true } trippy-privilege = { workspace = true, optional = true } trippy-dns = { workspace = true, optional = true } trippy-packet = { workspace = true, optional = true } anyhow = { workspace = true, optional = true } [lints] workspace = true [package.metadata.generate-rpm] assets = [ { source = "target/release/trip", dest = "/usr/bin/trip", mode = "755" }, ] ================================================ FILE: crates/trippy/src/lib.rs ================================================ #![allow( rustdoc::broken_intra_doc_links, rustdoc::bare_urls, clippy::doc_markdown, clippy::doc_lazy_continuation )] #![doc = include_str!("../README.md")] // Re-export the user facing libraries, so they may be used from trippy crate directly. #[cfg(feature = "core")] /// A network tracer. pub mod core { pub use trippy_core::*; } #[cfg(feature = "dns")] /// A lazy DNS resolver. pub mod dns { pub use trippy_dns::*; } #[cfg(feature = "privilege")] /// Discover platform privileges. pub mod privilege { pub use trippy_privilege::*; } #[cfg(feature = "packet")] /// Network packets. pub mod packet { pub use trippy_packet::*; } ================================================ FILE: crates/trippy/src/main.rs ================================================ fn main() -> anyhow::Result<()> { trippy_tui::trippy() } ================================================ FILE: crates/trippy-core/Cargo.toml ================================================ [package] name = "trippy-core" description = "A network tracing library" version.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true readme.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true keywords.workspace = true categories.workspace = true [dependencies] trippy-packet.workspace = true trippy-privilege.workspace = true arrayvec.workspace = true bitflags.workspace = true derive_more = { workspace = true, default-features = false, features = ["mul", "add", "add_assign"] } indexmap = { workspace = true, default-features = false, features = ["std"] } itertools.workspace = true parking_lot.workspace = true socket2 = { workspace = true, features = ["all"] } thiserror.workspace = true tracing.workspace = true [target.'cfg(unix)'.dependencies] nix = { workspace = true, default-features = false, features = ["user", "poll", "net"] } [target.'cfg(windows)'.dependencies] paste.workspace = true widestring.workspace = true windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_Networking_WinSock", "Win32_System_IO", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_System_IO", "Win32_System_Threading", "Win32_Security"] } [dev-dependencies] anyhow.workspace = true futures-concurrency.workspace = true hex-literal.workspace = true mockall.workspace = true rand.workspace = true serde = { workspace = true, default-features = false, features = ["derive", "std"] } test-case.workspace = true tokio-util.workspace = true tokio = { workspace = true, features = ["full"] } toml = { workspace = true, default-features = false, features = ["parse"] } tracing-subscriber = { workspace = true, default-features = false, features = ["env-filter", "fmt"] } [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dev-dependencies] tun-rs = { workspace = true, features = ["async"] } [features] # Enable simulation integration tests sim-tests = [] [lints] workspace = true ================================================ FILE: crates/trippy-core/src/builder.rs ================================================ use crate::config::{ChannelConfig, StateConfig, StrategyConfig}; use crate::constants::MAX_INITIAL_SEQUENCE; use crate::error::Result; use crate::{ Error, IcmpExtensionParseMode, MAX_TTL, MaxInflight, MaxRounds, MultipathStrategy, PacketSize, PayloadPattern, PortDirection, PrivilegeMode, Protocol, Sequence, TimeToLive, TraceId, Tracer, TypeOfService, }; use std::net::IpAddr; use std::num::NonZeroUsize; use std::time::Duration; /// Build a tracer. /// /// This is a convenience builder to simplify the creation of execution of a /// tracer. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use trippy_core::{Builder, MultipathStrategy, Port, PortDirection, PrivilegeMode, Protocol}; /// /// let addr = std::net::IpAddr::from([1, 2, 3, 4]); /// let tracer = Builder::new(addr) /// .privilege_mode(PrivilegeMode::Unprivileged) /// .protocol(Protocol::Udp) /// .multipath_strategy(MultipathStrategy::Dublin) /// .port_direction(PortDirection::FixedBoth(Port(33434), Port(3500))) /// .build()?; /// # Ok(()) /// # } /// ``` /// /// # See Also /// /// - [`Tracer`] - A traceroute implementation. #[derive(Debug)] pub struct Builder { interface: Option, source_addr: Option, target_addr: IpAddr, privilege_mode: PrivilegeMode, protocol: Protocol, packet_size: PacketSize, payload_pattern: PayloadPattern, tos: TypeOfService, icmp_extension_parse_mode: IcmpExtensionParseMode, read_timeout: Duration, tcp_connect_timeout: Duration, trace_identifier: TraceId, max_rounds: Option, first_ttl: TimeToLive, max_ttl: TimeToLive, grace_duration: Duration, max_inflight: MaxInflight, initial_sequence: Sequence, multipath_strategy: MultipathStrategy, port_direction: PortDirection, min_round_duration: Duration, max_round_duration: Duration, max_samples: usize, max_flows: usize, drop_privileges: bool, } impl Default for Builder { fn default() -> Self { Self { interface: None, source_addr: None, target_addr: ChannelConfig::default().target_addr, privilege_mode: ChannelConfig::default().privilege_mode, protocol: ChannelConfig::default().protocol, packet_size: ChannelConfig::default().packet_size, payload_pattern: ChannelConfig::default().payload_pattern, tos: ChannelConfig::default().tos, icmp_extension_parse_mode: ChannelConfig::default().icmp_extension_parse_mode, read_timeout: ChannelConfig::default().read_timeout, tcp_connect_timeout: ChannelConfig::default().tcp_connect_timeout, trace_identifier: StrategyConfig::default().trace_identifier, max_rounds: StrategyConfig::default().max_rounds, first_ttl: StrategyConfig::default().first_ttl, max_ttl: StrategyConfig::default().max_ttl, grace_duration: StrategyConfig::default().grace_duration, max_inflight: StrategyConfig::default().max_inflight, initial_sequence: StrategyConfig::default().initial_sequence, multipath_strategy: StrategyConfig::default().multipath_strategy, port_direction: StrategyConfig::default().port_direction, min_round_duration: StrategyConfig::default().min_round_duration, max_round_duration: StrategyConfig::default().max_round_duration, max_samples: StateConfig::default().max_samples, max_flows: StateConfig::default().max_flows, drop_privileges: false, } } } impl Builder { /// Build a tracer builder for a given target. /// /// # Examples /// /// Basic usage: /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use trippy_core::Builder; /// /// let addr = std::net::IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn new(target_addr: IpAddr) -> Self { Self { target_addr, ..Default::default() } } /// Set the source address. /// /// If not set then the source address will be discovered based on the /// target address and the interface. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let source_addr = IpAddr::from([192, 168, 1, 1]); /// let tracer = Builder::new(addr).source_addr(Some(source_addr)).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn source_addr(self, source_addr: Option) -> Self { Self { source_addr, ..self } } /// Set the source interface. /// /// If the source interface is provided it will be used to look up the IPv4 /// or IPv6 source address. /// /// If not provided the source address will be determined by OS based on /// the target IPv4 or IPv6 address. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).interface(Some("eth0")).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn interface>(self, interface: Option) -> Self { Self { interface: interface.map(Into::into), ..self } } /// Set the protocol. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::{Builder, Protocol}; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).protocol(Protocol::Udp).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn protocol(self, protocol: Protocol) -> Self { Self { protocol, ..self } } /// Set the trace identifier. /// /// If not set then 0 will be used as the trace identifier. /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).trace_identifier(12345).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn trace_identifier(self, trace_id: u16) -> Self { Self { trace_identifier: TraceId(trace_id), ..self } } /// Set the privilege mode. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::{Builder, PrivilegeMode}; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr) /// .privilege_mode(PrivilegeMode::Unprivileged) /// .build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn privilege_mode(self, privilege_mode: PrivilegeMode) -> Self { Self { privilege_mode, ..self } } /// Set the multipath strategy. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::{Builder, MultipathStrategy}; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr) /// .multipath_strategy(MultipathStrategy::Paris) /// .build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn multipath_strategy(self, multipath_strategy: MultipathStrategy) -> Self { Self { multipath_strategy, ..self } } /// Set the packet size. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).packet_size(128).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn packet_size(self, packet_size: u16) -> Self { Self { packet_size: PacketSize(packet_size), ..self } } /// Set the payload pattern. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).payload_pattern(0xff).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn payload_pattern(self, payload_pattern: u8) -> Self { Self { payload_pattern: PayloadPattern(payload_pattern), ..self } } /// Set the type of service. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).tos(0x1a).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn tos(self, tos: u8) -> Self { Self { tos: TypeOfService(tos), ..self } } /// Set the ICMP extensions mode. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::{Builder, IcmpExtensionParseMode}; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr) /// .icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled) /// .build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn icmp_extension_parse_mode( self, icmp_extension_parse_mode: IcmpExtensionParseMode, ) -> Self { Self { icmp_extension_parse_mode, ..self } } /// Set the read timeout. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use std::time::Duration; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr) /// .read_timeout(Duration::from_millis(50)) /// .build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn read_timeout(self, read_timeout: Duration) -> Self { Self { read_timeout, ..self } } /// Set the TCP connect timeout. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use std::time::Duration; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr) /// .tcp_connect_timeout(Duration::from_millis(100)) /// .build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn tcp_connect_timeout(self, tcp_connect_timeout: Duration) -> Self { Self { tcp_connect_timeout, ..self } } /// Set the maximum number of rounds. /// /// If set to `None` then the tracer will run indefinitely, otherwise it /// will stop after the given number of rounds. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).max_rounds(Some(10)).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn max_rounds(self, max_rounds: Option) -> Self { Self { max_rounds: max_rounds .and_then(|max_rounds| NonZeroUsize::new(max_rounds).map(MaxRounds)), ..self } } /// Set the first ttl. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).first_ttl(2).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn first_ttl(self, first_ttl: u8) -> Self { Self { first_ttl: TimeToLive(first_ttl), ..self } } /// Set the maximum ttl. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).max_ttl(16).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn max_ttl(self, max_ttl: u8) -> Self { Self { max_ttl: TimeToLive(max_ttl), ..self } } /// Set the grace duration. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use std::time::Duration; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr) /// .grace_duration(Duration::from_millis(100)) /// .build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn grace_duration(self, grace_duration: Duration) -> Self { Self { grace_duration, ..self } } /// Set the max number of probes in flight at any given time. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).max_inflight(22).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn max_inflight(self, max_inflight: u8) -> Self { Self { max_inflight: MaxInflight(max_inflight), ..self } } /// Set the initial sequence number. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).initial_sequence(35000).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn initial_sequence(self, initial_sequence: u16) -> Self { Self { initial_sequence: Sequence(initial_sequence), ..self } } /// Set the port direction. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::{Builder, Port, PortDirection}; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr) /// .port_direction(PortDirection::FixedDest(Port(8080))) /// .build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn port_direction(self, port_direction: PortDirection) -> Self { Self { port_direction, ..self } } /// Set the minimum round duration. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use std::time::Duration; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr) /// .min_round_duration(Duration::from_millis(500)) /// .build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn min_round_duration(self, min_round_duration: Duration) -> Self { Self { min_round_duration, ..self } } /// Set the maximum round duration. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use std::time::Duration; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr) /// .max_round_duration(Duration::from_millis(1500)) /// .build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn max_round_duration(self, max_round_duration: Duration) -> Self { Self { max_round_duration, ..self } } /// Set the maximum number of samples to record. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).max_samples(256).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn max_samples(self, max_samples: usize) -> Self { Self { max_samples, ..self } } /// Set the maximum number of flows to record. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).max_flows(64).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn max_flows(self, max_flows: usize) -> Self { Self { max_flows, ..self } } /// Drop privileges after connection is established. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).drop_privileges(true).build()?; /// # Ok(()) /// # } /// ``` #[must_use] pub fn drop_privileges(self, drop_privileges: bool) -> Self { Self { drop_privileges, ..self } } /// Build the `Tracer`. /// /// # Examples /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// use std::net::IpAddr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from([1, 1, 1, 1]); /// let tracer = Builder::new(addr).build()?; /// # Ok(()) /// # } /// ``` /// /// # Errors /// /// This function will return `Error::BadConfig` if the configuration is invalid. pub fn build(self) -> Result { match (self.protocol, self.port_direction) { (Protocol::Udp, PortDirection::None) => { return Err(Error::BadConfig( "port_direction may not be None for udp protocol".to_string(), )); } (Protocol::Tcp, PortDirection::None) => { return Err(Error::BadConfig( "port_direction may not be None for tcp protocol".to_string(), )); } _ => (), } if self.first_ttl.0 > MAX_TTL { return Err(Error::BadConfig(format!( "first_ttl {} > {MAX_TTL}", self.first_ttl.0 ))); } if self.max_ttl.0 > MAX_TTL { return Err(Error::BadConfig(format!( "max_ttl {} > {MAX_TTL}", self.max_ttl.0 ))); } if self.initial_sequence.0 > MAX_INITIAL_SEQUENCE { return Err(Error::BadConfig(format!( "initial_sequence {} > {MAX_INITIAL_SEQUENCE}", self.initial_sequence.0 ))); } Ok(Tracer::new( self.interface, self.source_addr, self.target_addr, self.privilege_mode, self.protocol, self.packet_size, self.payload_pattern, self.tos, self.icmp_extension_parse_mode, self.read_timeout, self.tcp_connect_timeout, self.trace_identifier, self.max_rounds, self.first_ttl, self.max_ttl, self.grace_duration, self.max_inflight, self.initial_sequence, self.multipath_strategy, self.port_direction, self.min_round_duration, self.max_round_duration, self.max_samples, self.max_flows, self.drop_privileges, )) } } #[cfg(test)] mod tests { use super::*; use crate::{Port, config}; use config::defaults; use std::net::Ipv4Addr; use std::num::NonZeroUsize; const SOURCE_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::UNSPECIFIED); const TARGET_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2)); #[test] fn test_builder_minimal() { let tracer = Builder::new(TARGET_ADDR).build().unwrap(); assert_eq!(TARGET_ADDR, tracer.target_addr()); assert_eq!(None, tracer.source_addr()); assert_eq!(None, tracer.interface()); assert_eq!(defaults::DEFAULT_MAX_SAMPLES, tracer.max_samples()); assert_eq!(defaults::DEFAULT_MAX_FLOWS, tracer.max_flows()); assert_eq!(defaults::DEFAULT_STRATEGY_PROTOCOL, tracer.protocol()); assert_eq!(TraceId::default(), tracer.trace_identifier()); assert_eq!(defaults::DEFAULT_PRIVILEGE_MODE, tracer.privilege_mode()); assert_eq!( defaults::DEFAULT_STRATEGY_MULTIPATH, tracer.multipath_strategy() ); assert_eq!( defaults::DEFAULT_STRATEGY_PACKET_SIZE, tracer.packet_size().0 ); assert_eq!( defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN, tracer.payload_pattern().0 ); assert_eq!(defaults::DEFAULT_STRATEGY_TOS, tracer.tos().0); assert_eq!( defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE, tracer.icmp_extension_parse_mode() ); assert_eq!( defaults::DEFAULT_STRATEGY_READ_TIMEOUT, tracer.read_timeout() ); assert_eq!( defaults::DEFAULT_STRATEGY_TCP_CONNECT_TIMEOUT, tracer.tcp_connect_timeout() ); assert_eq!(None, tracer.max_rounds()); assert_eq!(defaults::DEFAULT_STRATEGY_FIRST_TTL, tracer.first_ttl().0); assert_eq!(defaults::DEFAULT_STRATEGY_MAX_TTL, tracer.max_ttl().0); assert_eq!( defaults::DEFAULT_STRATEGY_GRACE_DURATION, tracer.grace_duration() ); assert_eq!( defaults::DEFAULT_STRATEGY_MAX_INFLIGHT, tracer.max_inflight().0 ); assert_eq!( defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE, tracer.initial_sequence().0 ); assert_eq!(PortDirection::None, tracer.port_direction()); assert_eq!( defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION, tracer.min_round_duration() ); assert_eq!( defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION, tracer.max_round_duration() ); } #[test] fn test_builder_full() { let tracer = Builder::new(TARGET_ADDR) .source_addr(Some(SOURCE_ADDR)) .interface(Some("eth0")) .max_samples(10) .max_flows(20) .protocol(Protocol::Udp) .trace_identifier(101) .privilege_mode(PrivilegeMode::Unprivileged) .multipath_strategy(MultipathStrategy::Paris) .packet_size(128) .payload_pattern(0xff) .tos(0x1a) .icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled) .read_timeout(Duration::from_millis(50)) .tcp_connect_timeout(Duration::from_millis(100)) .max_rounds(Some(10)) .first_ttl(2) .max_ttl(16) .grace_duration(Duration::from_millis(100)) .max_inflight(22) .initial_sequence(35000) .port_direction(PortDirection::FixedSrc(Port(8080))) .min_round_duration(Duration::from_millis(500)) .max_round_duration(Duration::from_millis(1500)) .build() .unwrap(); assert_eq!(TARGET_ADDR, tracer.target_addr()); // note that `source_addr` is not set until the tracer is run assert_eq!(None, tracer.source_addr()); assert_eq!(Some("eth0"), tracer.interface()); assert_eq!(10, tracer.max_samples()); assert_eq!(20, tracer.max_flows()); assert_eq!(Protocol::Udp, tracer.protocol()); assert_eq!(TraceId(101), tracer.trace_identifier()); assert_eq!(PrivilegeMode::Unprivileged, tracer.privilege_mode()); assert_eq!(MultipathStrategy::Paris, tracer.multipath_strategy()); assert_eq!(PacketSize(128), tracer.packet_size()); assert_eq!(PayloadPattern(0xff), tracer.payload_pattern()); assert_eq!(TypeOfService(0x1a), tracer.tos()); assert_eq!( IcmpExtensionParseMode::Enabled, tracer.icmp_extension_parse_mode() ); assert_eq!(Duration::from_millis(50), tracer.read_timeout()); assert_eq!(Duration::from_millis(100), tracer.tcp_connect_timeout()); assert_eq!( Some(MaxRounds(NonZeroUsize::new(10).unwrap())), tracer.max_rounds() ); assert_eq!(TimeToLive(2), tracer.first_ttl()); assert_eq!(TimeToLive(16), tracer.max_ttl()); assert_eq!(Duration::from_millis(100), tracer.grace_duration()); assert_eq!(MaxInflight(22), tracer.max_inflight()); assert_eq!(Sequence(35000), tracer.initial_sequence()); assert_eq!(PortDirection::FixedSrc(Port(8080)), tracer.port_direction()); assert_eq!(Duration::from_millis(500), tracer.min_round_duration()); assert_eq!(Duration::from_millis(1500), tracer.max_round_duration()); } #[test] fn test_zero_max_rounds() { let tracer = Builder::new(IpAddr::from([1, 2, 3, 4])) .max_rounds(Some(0)) .build() .unwrap(); assert_eq!(None, tracer.max_rounds()); } #[test] fn test_invalid_initial_sequence() { let err = Builder::new(IpAddr::from([1, 2, 3, 4])) .initial_sequence(u16::MAX) .build() .unwrap_err(); assert!(matches!(err, Error::BadConfig(s) if s == "initial_sequence 65535 > 64511")); } } ================================================ FILE: crates/trippy-core/src/config.rs ================================================ use crate::types::Port; use crate::{ MaxInflight, MaxRounds, PacketSize, PayloadPattern, Sequence, TimeToLive, TraceId, TypeOfService, }; use std::fmt::{Display, Formatter}; use std::net::{IpAddr, Ipv4Addr}; use std::time::Duration; /// Default values for configuration. pub mod defaults { use crate::config::IcmpExtensionParseMode; use crate::{MultipathStrategy, PrivilegeMode, Protocol}; use std::time::Duration; /// The default value for `unprivileged`. pub const DEFAULT_PRIVILEGE_MODE: PrivilegeMode = PrivilegeMode::Privileged; /// The default value for `protocol`. pub const DEFAULT_STRATEGY_PROTOCOL: Protocol = Protocol::Icmp; /// The default value for `multipath-strategy`. pub const DEFAULT_STRATEGY_MULTIPATH: MultipathStrategy = MultipathStrategy::Classic; /// The default value for `icmp-extensions`. pub const DEFAULT_ICMP_EXTENSION_PARSE_MODE: IcmpExtensionParseMode = IcmpExtensionParseMode::Disabled; /// The default value for `max-inflight`. pub const DEFAULT_STRATEGY_MAX_INFLIGHT: u8 = 24; /// The default value for `first-ttl`. pub const DEFAULT_STRATEGY_FIRST_TTL: u8 = 1; /// The default value for `max-ttl`. pub const DEFAULT_STRATEGY_MAX_TTL: u8 = 64; /// The default value for `packet-size`. pub const DEFAULT_STRATEGY_PACKET_SIZE: u16 = 84; /// The default value for `payload-pattern`. pub const DEFAULT_STRATEGY_PAYLOAD_PATTERN: u8 = 0; /// The default value for `min-round-duration`. pub const DEFAULT_STRATEGY_MIN_ROUND_DURATION: Duration = Duration::from_millis(1000); /// The default value for `max-round-duration`. pub const DEFAULT_STRATEGY_MAX_ROUND_DURATION: Duration = Duration::from_millis(1000); /// The default value for `initial-sequence`. pub const DEFAULT_STRATEGY_INITIAL_SEQUENCE: u16 = 33434; /// The default value for `tos`. pub const DEFAULT_STRATEGY_TOS: u8 = 0; /// The default value for `read-timeout`. pub const DEFAULT_STRATEGY_READ_TIMEOUT: Duration = Duration::from_millis(10); /// The default value for `grace-duration`. pub const DEFAULT_STRATEGY_GRACE_DURATION: Duration = Duration::from_millis(100); /// The default TCP connect timeout. pub const DEFAULT_STRATEGY_TCP_CONNECT_TIMEOUT: Duration = Duration::from_millis(1000); /// The default value for `max-samples`. pub const DEFAULT_MAX_SAMPLES: usize = 256; /// The default value for `max-flows`. pub const DEFAULT_MAX_FLOWS: usize = 64; } /// The privilege mode. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum PrivilegeMode { /// Privileged mode. Privileged, /// Unprivileged mode. Unprivileged, } impl PrivilegeMode { #[must_use] pub const fn is_unprivileged(self) -> bool { match self { Self::Privileged => false, Self::Unprivileged => true, } } } impl Display for PrivilegeMode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Privileged => write!(f, "privileged"), Self::Unprivileged => write!(f, "unprivileged"), } } } /// The ICMP extension parsing mode. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum IcmpExtensionParseMode { /// Do not parse ICMP extensions. Disabled, /// Parse ICMP extensions. Enabled, } impl IcmpExtensionParseMode { #[must_use] pub const fn is_enabled(self) -> bool { match self { Self::Disabled => false, Self::Enabled => true, } } } impl Display for IcmpExtensionParseMode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Disabled => write!(f, "disabled"), Self::Enabled => write!(f, "enabled"), } } } /// The tracing protocol. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum Protocol { /// Internet Control Message Protocol Icmp, /// User Datagram Protocol Udp, /// Transmission Control Protocol Tcp, } impl Display for Protocol { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Icmp => write!(f, "icmp"), Self::Udp => write!(f, "udp"), Self::Tcp => write!(f, "tcp"), } } } /// The [Equal-cost Multi-Path](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) routing strategy. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum MultipathStrategy { /// The src or dest port is used to store the sequence number. /// /// This does _not_ allow fixing both the src and dest port and so `PortDirection::Both` and /// `SequenceField::Port` are mutually exclusive. Classic, /// The UDP `checksum` field is used to store the sequence number. /// /// a.k.a. [`paris`](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum) traceroute approach. /// /// This requires that the UDP payload contains a well-chosen value to ensure the UDP checksum /// remains valid for the packet and therefore this cannot be used along with a custom /// payload pattern. Paris, /// The IP `identifier` field is used to store the sequence number. /// /// a.k.a. [`dublin`](https://github.com/insomniacslk/dublin-traceroute) traceroute approach. /// /// The allow either the src or dest or both ports to be fixed. /// /// If either of the src or dest port may vary (i.e. `PortDirection::FixedSrc` or /// `PortDirection::FixedDest`) then the port number is set to be the `initial_sequence` /// plus the round number to ensure that there is a fixed `flowid` (protocol, src ip/port, /// dest ip/port) for all packets in a given tracing round. Each round may /// therefore discover different paths. /// /// If both src and dest ports are fixed (i.e. `PortDirection::FixedBoth`) then every packet in /// every round will share the same `flowid` and thus only a single path will be /// discovered. Dublin, } impl Display for MultipathStrategy { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Classic => write!(f, "classic"), Self::Paris => write!(f, "paris"), Self::Dublin => write!(f, "dublin"), } } } /// Whether to fix the src, dest or both ports for a trace. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum PortDirection { /// Trace without any source or destination port (i.e. for ICMP tracing). None, /// Trace from a fixed source port to a variable destination port (i.e. 5000 -> *). /// /// This is the default direction for UDP tracing. FixedSrc(Port), /// Trace from a variable source port to a fixed destination port (i.e. * -> 80). /// /// This is the default direction for TCP tracing. FixedDest(Port), /// Trace from a fixed source port to a fixed destination port (i.e. 5000 -> 80). /// /// When both ports are fixed another element of the IP header is required to vary per probe /// such that probes can be identified. Typically, this is only used for UDP, whereby the /// checksum is manipulated by adjusting the payload and therefore used as the identifier. /// /// Note that this case is not currently implemented. FixedBoth(Port, Port), } impl PortDirection { #[must_use] pub const fn new_fixed_src(src: u16) -> Self { Self::FixedSrc(Port(src)) } #[must_use] pub const fn new_fixed_dest(dest: u16) -> Self { Self::FixedDest(Port(dest)) } #[must_use] pub const fn new_fixed_both(src: u16, dest: u16) -> Self { Self::FixedBoth(Port(src), Port(dest)) } #[must_use] pub const fn src(&self) -> Option { match *self { Self::FixedSrc(src) | Self::FixedBoth(src, _) => Some(src), _ => None, } } #[must_use] pub const fn dest(&self) -> Option { match *self { Self::FixedDest(dest) | Self::FixedBoth(_, dest) => Some(dest), _ => None, } } } /// Tracer state configuration. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct StateConfig { /// The maximum number of samples to record per hop. /// /// Once the maximum number of samples has been reached the oldest sample /// is discarded (FIFO). pub max_samples: usize, /// The maximum number of flows to record. /// /// Once the maximum number of flows has been reached no new flows will be /// created, existing flows are updated and are never removed. pub max_flows: usize, } impl Default for StateConfig { fn default() -> Self { Self { max_samples: defaults::DEFAULT_MAX_SAMPLES, max_flows: defaults::DEFAULT_MAX_FLOWS, } } } /// Tracer network channel configuration. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct ChannelConfig { pub privilege_mode: PrivilegeMode, pub protocol: Protocol, pub source_addr: IpAddr, pub target_addr: IpAddr, pub packet_size: PacketSize, pub payload_pattern: PayloadPattern, pub initial_sequence: Sequence, pub tos: TypeOfService, pub icmp_extension_parse_mode: IcmpExtensionParseMode, pub read_timeout: Duration, pub tcp_connect_timeout: Duration, } impl Default for ChannelConfig { fn default() -> Self { Self { privilege_mode: defaults::DEFAULT_PRIVILEGE_MODE, protocol: defaults::DEFAULT_STRATEGY_PROTOCOL, source_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), packet_size: PacketSize(defaults::DEFAULT_STRATEGY_PACKET_SIZE), payload_pattern: PayloadPattern(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN), initial_sequence: Sequence(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE), tos: TypeOfService(defaults::DEFAULT_STRATEGY_TOS), icmp_extension_parse_mode: defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE, read_timeout: defaults::DEFAULT_STRATEGY_READ_TIMEOUT, tcp_connect_timeout: defaults::DEFAULT_STRATEGY_TCP_CONNECT_TIMEOUT, } } } /// Tracing strategy configuration. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct StrategyConfig { pub target_addr: IpAddr, pub protocol: Protocol, pub trace_identifier: TraceId, pub max_rounds: Option, pub first_ttl: TimeToLive, pub max_ttl: TimeToLive, pub grace_duration: Duration, pub max_inflight: MaxInflight, pub initial_sequence: Sequence, pub multipath_strategy: MultipathStrategy, pub port_direction: PortDirection, pub min_round_duration: Duration, pub max_round_duration: Duration, } impl Default for StrategyConfig { fn default() -> Self { Self { target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), protocol: defaults::DEFAULT_STRATEGY_PROTOCOL, trace_identifier: TraceId::default(), max_rounds: None, first_ttl: TimeToLive(defaults::DEFAULT_STRATEGY_FIRST_TTL), max_ttl: TimeToLive(defaults::DEFAULT_STRATEGY_MAX_TTL), grace_duration: defaults::DEFAULT_STRATEGY_GRACE_DURATION, max_inflight: MaxInflight(defaults::DEFAULT_STRATEGY_MAX_INFLIGHT), initial_sequence: Sequence(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE), multipath_strategy: defaults::DEFAULT_STRATEGY_MULTIPATH, port_direction: PortDirection::None, min_round_duration: defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION, max_round_duration: defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION, } } } ================================================ FILE: crates/trippy-core/src/constants.rs ================================================ /// The maximum time-to-live value allowed. /// /// The IP `ttl` is an u8 (0..255) but since a `ttl` of zero isn't useful we only allow 254 distinct /// hops (1..255). pub const MAX_TTL: u8 = 254; /// The maximum number of sequence numbers allowed per round. /// /// This is set to be far larger than the `MAX_TTL` to allow for the re-issue of probes (with the /// next sequence number, but the same ttl) which can occur for some protocols such as TCP when it /// cannot bind to a given port. pub const MAX_SEQUENCE_PER_ROUND: u16 = 512; /// The maximum _starting_ sequence number allowed. /// /// This ensures that there are sufficient sequence numbers available for at least _two_ rounds. We /// require two rounds to ensure that delayed probe responses from the immediate prior round can be /// detected and excluded. pub const MAX_INITIAL_SEQUENCE: u16 = u16::MAX - (MAX_SEQUENCE_PER_ROUND * 2); ================================================ FILE: crates/trippy-core/src/error.rs ================================================ use std::fmt::{Display, Formatter}; use std::io; use std::net::{IpAddr, SocketAddr}; use thiserror::Error; /// A tracer error result. pub type Result = std::result::Result; /// A tracer error. #[derive(Error, Debug)] pub enum Error { #[error("invalid packet size: {0}")] InvalidPacketSize(usize), #[error("invalid packet: {0}")] PacketError(#[from] trippy_packet::error::Error), #[error("unknown interface: {0}")] UnknownInterface(String), #[error("invalid config: {0}")] BadConfig(String), #[error("IO error: {0}")] IoError(#[from] IoError), #[error("Probe failed to send: {0}")] ProbeFailed(IoError), #[error("insufficient buffer capacity")] InsufficientCapacity, #[error("address {0} in use")] AddressInUse(SocketAddr), #[error("source IP address {0} could not be bound")] InvalidSourceAddr(IpAddr), #[error("missing address from socket call")] MissingAddr, #[error("connect callback error: {0}")] PrivilegeError(#[from] trippy_privilege::Error), #[error("tracer error: {0}")] Other(String), } /// Custom IO error result. pub type IoResult = std::result::Result; /// Custom IO error. #[derive(Error, Debug)] pub enum IoError { #[error("Bind error for {1}: {0}")] Bind(io::Error, SocketAddr), #[error("Connect error for {1}: {0}")] Connect(io::Error, SocketAddr), #[error("Sendto error for {1}: {0}")] SendTo(io::Error, SocketAddr), #[error("Failed to {0}: {1}")] Other(io::Error, IoOperation), } impl IoError { /// Get the custom error kind. pub fn kind(&self) -> ErrorKind { match self { Self::Bind(e, _) | Self::Connect(e, _) | Self::SendTo(e, _) | Self::Other(e, _) => { ErrorKind::from(e) } } } } /// Custom error kind. /// /// This includes additional error kinds that are not part of the standard [`io::ErrorKind`]. #[derive(Debug, Eq, PartialEq)] pub enum ErrorKind { InProgress, HostUnreachable, NetUnreachable, Std(io::ErrorKind), } /// Io operation. #[derive(Debug)] pub enum IoOperation { NewSocket, SetNonBlocking, Select, RecvFrom, Read, Shutdown, LocalAddr, PeerAddr, TakeError, SetTos, SetTclassV6, SetTtl, SetReusePort, SetHeaderIncluded, SetUnicastHopsV6, WSACreateEvent, WSARecvFrom, WSAEventSelect, WSAResetEvent, WSAGetOverlappedResult, WaitForSingleObject, SetTcpFailConnectOnIcmpError, TcpIcmpErrorInfo, ConvertSocketAddress, SioRoutingInterfaceQuery, Startup, } impl Display for IoOperation { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::NewSocket => write!(f, "create new socket"), Self::SetNonBlocking => write!(f, "set non-blocking"), Self::Select => write!(f, "select"), Self::RecvFrom => write!(f, "recv from"), Self::Read => write!(f, "read"), Self::Shutdown => write!(f, "shutdown"), Self::LocalAddr => write!(f, "local addr"), Self::PeerAddr => write!(f, "peer addr"), Self::TakeError => write!(f, "take error"), Self::SetTos => write!(f, "set TOS"), Self::SetTclassV6 => write!(f, "set TCLASS v6"), Self::SetTtl => write!(f, "set TTL"), Self::SetReusePort => write!(f, "set reuse port"), Self::SetHeaderIncluded => write!(f, "set header included"), Self::SetUnicastHopsV6 => write!(f, "set unicast hops v6"), Self::WSACreateEvent => write!(f, "WSA create event"), Self::WSARecvFrom => write!(f, "WSA recv from"), Self::WSAEventSelect => write!(f, "WSA event select"), Self::WSAResetEvent => write!(f, "WSA reset event"), Self::WSAGetOverlappedResult => write!(f, "WSA get overlapped result"), Self::WaitForSingleObject => write!(f, "wait for single object"), Self::SetTcpFailConnectOnIcmpError => write!(f, "set TCP failed connect on ICMP error"), Self::TcpIcmpErrorInfo => write!(f, "get TCP ICMP error info"), Self::ConvertSocketAddress => write!(f, "convert socket address"), Self::SioRoutingInterfaceQuery => write!(f, "SIO routing interface query"), Self::Startup => write!(f, "startup"), } } } ================================================ FILE: crates/trippy-core/src/flows.rs ================================================ use derive_more::{Add, AddAssign, Sub, SubAssign}; use itertools::{EitherOrBoth, Itertools}; use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use tracing::instrument; /// Identifies a tracing `Flow`. #[derive( Debug, Clone, Copy, Default, Ord, PartialOrd, Eq, PartialEq, Hash, Add, AddAssign, Sub, SubAssign, )] pub struct FlowId(pub u64); impl Display for FlowId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// A register of tracing `Flows`. #[derive(Debug, Clone, Default)] pub struct FlowRegistry { /// The id to assign to the next flow registered. next_flow_id: FlowId, /// The registry of flows observed. flows: Vec<(Flow, FlowId)>, } impl FlowRegistry { /// Create a new `FlowRegistry`. pub const fn new() -> Self { Self { flows: Vec::new(), next_flow_id: FlowId(1), } } /// Register a `Flow` with the `FlowRegistry`. /// /// If the flow matches a flow that has previously been observed by the registry then /// the id of that flow is return. Otherwise, a new flow id is created and /// returned and the corresponding flow is stored in the registry. /// /// If the flow matches but also contains additional data not previously /// observed for that flow then the existing flow will be updated to /// merge the data. In this case the existing flow id will be reused. /// /// If a flow matches more than one existing flow then only the first /// matching flow will be updated. #[instrument(skip(self), level = "trace")] pub fn register(&mut self, flow: Flow) -> FlowId { for (entry, id) in &mut self.flows { let status = entry.check(&flow); match status { CheckStatus::Match => { return *id; } CheckStatus::NoMatch => {} CheckStatus::MatchMerge => { entry.merge(&flow); return *id; } } } let flow_id = self.next_flow_id; self.flows.push((flow, flow_id)); self.next_flow_id.0 += 1; flow_id } /// All recorded flows. pub fn flows(&self) -> &[(Flow, FlowId)] { &self.flows } } /// Represents a single tracing path over a number of (possibly unknown) hops. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Flow { pub entries: Vec, } impl Flow { /// Create a new Flow from a slice of hops. /// /// Note that each entry is implicitly associated with a `ttl`. For /// example `hops[0]` would have a `ttl` of 1, `hops[1]` would have a /// `ttl` of 2 and so on. pub fn from_hops(hops: impl IntoIterator>) -> Self { let entries = hops .into_iter() .map(|addr| { if let Some(addr) = addr { FlowEntry::Known(addr) } else { FlowEntry::Unknown } }) .collect(); Self { entries } } /// Check if a given `Flow` matches this `Flow`. /// /// Two flows are said to match _unless_ they contain different IP /// addresses for the _same_ position (i.e. the same `ttl`). /// /// This is true even for flows of differing lengths. /// /// In the even of a match, if the flow being checked contains /// `FlowEntry::Known` entries which are `FlowEntry::Unknown` in the /// current flow then `CheckStatus::MatchMerge` is returned to indicate /// the two flows should be merged. /// /// This will also be the case if the flow being checked matches and is /// longer than the existing flow. #[instrument(skip(self), level = "trace")] pub fn check(&self, flow: &Self) -> CheckStatus { let mut additions = 0; for (old, new) in self.entries.iter().zip(&flow.entries) { match (old, new) { (FlowEntry::Known(fst), FlowEntry::Known(snd)) if fst != snd => { return CheckStatus::NoMatch; } (FlowEntry::Unknown, FlowEntry::Known(_)) => additions += 1, _ => {} } } if flow.entries.len() > self.entries.len() || additions > 0 { CheckStatus::MatchMerge } else { CheckStatus::Match } } /// Marge the entries from the given `Flow` into our `Flow`. #[instrument(skip(self), level = "trace")] fn merge(&mut self, flow: &Self) { self.entries = self .entries .iter() .zip_longest(flow.entries.iter()) .map(|eob| match eob { EitherOrBoth::Both(left, right) => match (left, right) { (FlowEntry::Unknown, FlowEntry::Known(_)) => *right, _ => *left, }, EitherOrBoth::Left(left) => *left, EitherOrBoth::Right(right) => *right, }) .collect::>(); } } impl Display for Flow { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.entries.iter().format(", ")) } } /// The result of a `Flow` comparison check. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum CheckStatus { /// The flows match. Match, /// The flows do not match. NoMatch, /// The flows match but should be merged. MatchMerge, } /// An entry in a `Flow`. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub enum FlowEntry { /// An unknown flow entry. Unknown, /// A known flow entry with an `IpAddr`. Known(IpAddr), } impl Display for FlowEntry { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Unknown => f.write_str("*"), Self::Known(addr) => { write!(f, "{addr}") } } } } #[cfg(test)] mod tests { use super::*; use std::net::Ipv4Addr; use std::str::FromStr; #[test] fn test_single_flow() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1")]); let flow_id = registry.register(flow1); assert_eq!(FlowId(1), flow_id); assert_eq!( &[(Flow::from_hops([addr("1.1.1.1")]), FlowId(1))], registry.flows() ); } #[test] fn test_two_different_flows() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1")]); let flow1_id = registry.register(flow1.clone()); let flow2 = Flow::from_hops([addr("2.2.2.2")]); let flow2_id = registry.register(flow2.clone()); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(2), flow2_id); assert_eq!(&[(flow1, flow1_id), (flow2, flow2_id)], registry.flows()); } #[test] fn test_two_same_flows() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1")]); let flow1_id = registry.register(flow1.clone()); let flow2 = Flow::from_hops([addr("1.1.1.1")]); let flow2_id = registry.register(flow2); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(1), flow2_id); assert_eq!(&[(flow1, flow1_id)], registry.flows()); } #[test] fn test_two_same_one_different_flows() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1")]); let flow1_id = registry.register(flow1.clone()); let flow2 = Flow::from_hops([addr("2.2.2.2")]); let flow2_id = registry.register(flow2.clone()); let flow3 = Flow::from_hops([addr("1.1.1.1")]); let flow3_id = registry.register(flow3); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(2), flow2_id); assert_eq!(FlowId(1), flow3_id); assert_eq!(&[(flow1, flow1_id), (flow2, flow2_id)], registry.flows()); } #[test] fn test_merge_flow1() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1")]); let flow1_id = registry.register(flow1); let flow2 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2")]); let flow2_id = registry.register(flow2); let flow3 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2")]); let flow3_id = registry.register(flow3); let flow4 = Flow::from_hops([addr("1.1.1.1"), addr("3.3.3.3")]); let flow4_id = registry.register(flow4); let flow5 = Flow::from_hops([addr("1.1.1.1")]); let flow5_id = registry.register(flow5); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(1), flow2_id); assert_eq!(FlowId(1), flow3_id); assert_eq!(FlowId(2), flow4_id); assert_eq!(FlowId(1), flow5_id); } #[test] fn test_merge_flow2() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2"), addr("3.3.3.3")]); let flow1_id = registry.register(flow1); let flow2 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2")]); let flow2_id = registry.register(flow2); let flow3 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2")]); let flow3_id = registry.register(flow3); let flow4 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2"), addr("3.3.3.3")]); let flow4_id = registry.register(flow4); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(1), flow2_id); assert_eq!(FlowId(1), flow3_id); assert_eq!(FlowId(1), flow4_id); } #[test] fn test_merge_flow3() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1"), None, addr("3.3.3.3")]); let flow1_id = registry.register(flow1); // doesn't match so new flow let flow2 = Flow::from_hops([addr("2.2.2.2")]); let flow2_id = registry.register(flow2); // matches and replaces flow 0 let flow3 = Flow::from_hops([ None, addr("2.2.2.2"), None, addr("4.4.4.4"), addr("5.5.5.5"), ]); let flow3_id = registry.register(flow3); // still matches flow 1 let flow4 = Flow::from_hops([addr("2.2.2.2")]); let flow4_id = registry.register(flow4); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(2), flow2_id); assert_eq!(FlowId(1), flow3_id); assert_eq!(FlowId(2), flow4_id); } #[test] fn test_subset() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2")]); let flow1_id = registry.register(flow1); let flow2 = Flow::from_hops([addr("1.1.1.1")]); let flow2_id = registry.register(flow2); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(1), flow2_id); } #[test] fn test_subset_any() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2")]); let flow1_id = registry.register(flow1); let flow2 = Flow::from_hops([addr("1.1.1.1"), None]); let flow2_id = registry.register(flow2); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(1), flow2_id); } #[test] fn test_superset() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1")]); let flow1_id = registry.register(flow1); let flow2 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2")]); let flow2_id = registry.register(flow2); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(1), flow2_id); } #[test] fn test_superset_any() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([addr("1.1.1.1"), None]); let flow1_id = registry.register(flow1); let flow2 = Flow::from_hops([addr("1.1.1.1"), addr("2.2.2.2")]); let flow2_id = registry.register(flow2); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(1), flow2_id); } #[test] fn test_start_any_then_same_flows() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([None, addr("1.1.1.1")]); let flow1_id = registry.register(flow1); let flow2 = Flow::from_hops([None, addr("1.1.1.1")]); let flow2_id = registry.register(flow2); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(1), flow2_id); } #[test] fn test_start_any_then_diff_flows() { let mut registry = FlowRegistry::new(); let flow1 = Flow::from_hops([None, addr("1.1.1.1")]); let flow1_id = registry.register(flow1); let flow2 = Flow::from_hops([None, addr("2.2.2.2")]); let flow2_id = registry.register(flow2); assert_eq!(FlowId(1), flow1_id); assert_eq!(FlowId(2), flow2_id); } #[expect(clippy::unnecessary_wraps)] fn addr(addr: &str) -> Option { Some(IpAddr::V4(Ipv4Addr::from_str(addr).unwrap())) } } ================================================ FILE: crates/trippy-core/src/lib.rs ================================================ //! Trippy - A network tracing library. //! //! This crate provides the core network tracing facility used by the //! standalone [Trippy](https://trippy.rs) application. //! //! Note: the public API is not stable and is highly likely to change //! in the future. //! //! # Example //! //! The following example builds and runs a tracer with default configuration //! and prints out the tracing data for each round: //! //! ```no_run //! # fn main() -> anyhow::Result<()> { //! # use std::net::IpAddr; //! # use std::str::FromStr; //! use trippy_core::Builder; //! //! let addr = IpAddr::from_str("1.1.1.1")?; //! Builder::new(addr) //! .build()? //! .run_with(|round| println!("{:?}", round))?; //! # Ok(()) //! # } //! ``` //! //! The following example traces using the UDP protocol with the Dublin ECMP //! strategy with fixed src and dest ports. It also operates in unprivileged //! mode (only supported on some platforms): //! //! ```no_run //! # fn main() -> anyhow::Result<()> { //! # use std::net::IpAddr; //! # use std::str::FromStr; //! use trippy_core::{Builder, MultipathStrategy, Port, PortDirection, PrivilegeMode, Protocol}; //! //! let addr = IpAddr::from_str("1.1.1.1")?; //! Builder::new(addr) //! .privilege_mode(PrivilegeMode::Unprivileged) //! .protocol(Protocol::Udp) //! .multipath_strategy(MultipathStrategy::Dublin) //! .port_direction(PortDirection::FixedBoth(Port(33434), Port(3500))) //! .build()? //! .run_with(|round| println!("{:?}", round))?; //! # Ok(()) //! # } //! ``` //! //! # See Also //! //! - [`Builder`] - Build a [`Tracer`]. //! - [`Tracer::run`] - Run the tracer on the current thread. //! - [`Tracer::run_with`] - Run the tracer with a custom round handler. //! - [`Tracer::spawn`] - Run the tracer on a new thread. //! - [`Tracer::spawn_with`] - Run the tracer on a new thread with a custom round handler. mod builder; mod config; mod constants; mod error; mod flows; mod net; mod probe; mod state; mod strategy; mod tracer; mod types; use net::channel::Channel; use net::source::SourceAddr; pub use builder::Builder; pub use config::{ IcmpExtensionParseMode, MultipathStrategy, PortDirection, PrivilegeMode, Protocol, defaults, }; pub use constants::MAX_TTL; pub use error::Error; pub use flows::{FlowEntry, FlowId}; pub use probe::{ Extension, Extensions, IcmpPacketType, MplsLabelStack, MplsLabelStackMember, Probe, ProbeComplete, ProbeStatus, UnknownExtension, }; pub use state::{Hop, NatStatus, State}; pub use strategy::{CompletionReason, Round, Strategy}; pub use tracer::Tracer; pub use types::{ Dscp, Ecn, Flags, MaxInflight, MaxRounds, PacketSize, PayloadPattern, Port, RoundId, Sequence, TimeToLive, TraceId, TypeOfService, }; ================================================ FILE: crates/trippy-core/src/net/channel.rs ================================================ use crate::config::ChannelConfig; use crate::error::{Error, Result}; use crate::net::socket::Socket; use crate::net::{Network, ipv4::Ipv4, ipv6::Ipv6, platform}; use crate::probe::{Probe, Response}; use crate::{Port, PrivilegeMode, Protocol}; use arrayvec::ArrayVec; use std::net::IpAddr; use std::time::{Duration, SystemTime}; use tracing::instrument; /// The maximum size of the IP packet we allow. pub const MAX_PACKET_SIZE: usize = 1024; /// The maximum number of TCP probes we allow. const MAX_TCP_PROBES: usize = 256; /// A channel for sending and receiving `Probe` packets. pub struct Channel { protocol: Protocol, read_timeout: Duration, tcp_connect_timeout: Duration, send_socket: Option, recv_socket: S, tcp_probes: ArrayVec, MAX_TCP_PROBES>, family_config: FamilyConfig, } /// The IP family configuration for the channel. enum FamilyConfig { V4(Ipv4), V6(Ipv6), } impl Channel { /// Create an `IcmpChannel`. /// /// This operation requires the `CAP_NET_RAW` capability on Linux. #[instrument(skip_all, level = "trace")] pub fn connect(config: &ChannelConfig) -> Result { tracing::debug!(?config); if usize::from(config.packet_size.0) > MAX_PACKET_SIZE { return Err(Error::InvalidPacketSize(usize::from(config.packet_size.0))); } let raw = config.privilege_mode == PrivilegeMode::Privileged; platform::startup()?; let ipv4_length_order = platform::Ipv4ByteOrder::for_address(config.source_addr)?; let send_socket = match config.protocol { Protocol::Icmp => Some(make_icmp_send_socket(config.source_addr, raw)?), Protocol::Udp => Some(make_udp_send_socket(config.source_addr, raw)?), Protocol::Tcp => None, }; let recv_socket = make_recv_socket(config.source_addr, raw)?; let family_config = match (config.source_addr, config.target_addr) { (IpAddr::V4(src_addr), IpAddr::V4(dest_addr)) => FamilyConfig::V4(Ipv4 { src_addr, dest_addr, byte_order: ipv4_length_order, packet_size: config.packet_size, payload_pattern: config.payload_pattern, privilege_mode: config.privilege_mode, tos: config.tos, protocol: config.protocol, icmp_extension_mode: config.icmp_extension_parse_mode, }), (IpAddr::V6(src_addr), IpAddr::V6(dest_addr)) => FamilyConfig::V6(Ipv6 { src_addr, dest_addr, packet_size: config.packet_size, payload_pattern: config.payload_pattern, privilege_mode: config.privilege_mode, tos: config.tos, protocol: config.protocol, icmp_extension_mode: config.icmp_extension_parse_mode, initial_sequence: config.initial_sequence, }), _ => unreachable!(), }; Ok(Self { protocol: config.protocol, read_timeout: config.read_timeout, tcp_connect_timeout: config.tcp_connect_timeout, send_socket, recv_socket, tcp_probes: ArrayVec::new(), family_config, }) } } impl Network for Channel { #[instrument(skip(self), level = "trace")] fn send_probe(&mut self, probe: Probe) -> Result<()> { tracing::debug!(?probe); match self.protocol { Protocol::Icmp => self.dispatch_icmp_probe(&probe), Protocol::Udp => self.dispatch_udp_probe(&probe), Protocol::Tcp => self.dispatch_tcp_probe(&probe), } } #[instrument(skip_all, level = "trace")] fn recv_probe(&mut self) -> Result> { let prob_response = match self.protocol { Protocol::Icmp | Protocol::Udp => self.recv_icmp_probe(), Protocol::Tcp => match self.recv_tcp_sockets()? { None => self.recv_icmp_probe(), resp => Ok(resp), }, }?; if let Some(resp) = &prob_response { tracing::debug!(?resp); } Ok(prob_response) } } impl Channel { /// Dispatch a ICMP probe. #[instrument(skip_all, level = "trace")] fn dispatch_icmp_probe(&mut self, probe: &Probe) -> Result<()> { match (&self.family_config, self.send_socket.as_mut()) { (FamilyConfig::V4(ipv4), Some(socket)) => ipv4.dispatch_icmp_probe(socket, probe), (FamilyConfig::V6(ipv6), Some(socket)) => ipv6.dispatch_icmp_probe(socket, probe), _ => unreachable!(), } } /// Dispatch a UDP probe. #[instrument(skip_all, level = "trace")] fn dispatch_udp_probe(&mut self, probe: &Probe) -> Result<()> { match (&self.family_config, self.send_socket.as_mut()) { (FamilyConfig::V4(ipv4), Some(socket)) => ipv4.dispatch_udp_probe(socket, probe), (FamilyConfig::V6(ipv6), Some(socket)) => ipv6.dispatch_udp_probe(socket, probe), _ => unreachable!(), } } /// Dispatch a TCP probe. #[instrument(skip_all, level = "trace")] fn dispatch_tcp_probe(&mut self, probe: &Probe) -> Result<()> { let socket = match &self.family_config { FamilyConfig::V4(ipv4) => ipv4.dispatch_tcp_probe(probe), FamilyConfig::V6(ipv6) => ipv6.dispatch_tcp_probe(probe), }?; self.tcp_probes.push(TcpProbe::new( socket, probe.src_port, probe.dest_port, SystemTime::now(), )); Ok(()) } /// Generate a `ProbeResponse` for the next available ICMP packet, if any #[instrument(skip(self), level = "trace")] fn recv_icmp_probe(&mut self) -> Result> { if self.recv_socket.is_readable(self.read_timeout)? { match &self.family_config { FamilyConfig::V4(ipv4) => ipv4.recv_icmp_probe(&mut self.recv_socket), FamilyConfig::V6(ipv6) => ipv6.recv_icmp_probe(&mut self.recv_socket), } } else { Ok(None) } } /// Generate synthetic `ProbeResponse` if a TCP socket is connected or if the connection was /// refused. /// /// Any TCP socket which has not connected or failed after a timeout will be removed. #[instrument(skip(self), level = "trace")] fn recv_tcp_sockets(&mut self) -> Result> { self.tcp_probes .retain(|probe| probe.start.elapsed().unwrap_or_default() < self.tcp_connect_timeout); let found_index = self .tcp_probes .iter_mut() .enumerate() .find_map(|(index, probe)| { if probe.socket.is_writable().unwrap_or_default() { Some(index) } else { None } }); if let Some(i) = found_index { let mut probe = self.tcp_probes.remove(i); match &self.family_config { FamilyConfig::V4(ipv4) => { ipv4.recv_tcp_socket(&mut probe.socket, probe.src_port, probe.dest_port) } FamilyConfig::V6(ipv6) => { ipv6.recv_tcp_socket(&mut probe.socket, probe.src_port, probe.dest_port) } } } else { Ok(None) } } } /// An entry in the TCP probes array. struct TcpProbe { socket: S, src_port: Port, dest_port: Port, start: SystemTime, } impl TcpProbe { pub const fn new(socket: S, src_port: Port, dest_port: Port, start: SystemTime) -> Self { Self { socket, src_port, dest_port, start, } } } /// Make a socket for sending raw `ICMP` packets. #[instrument(level = "trace")] fn make_icmp_send_socket(addr: IpAddr, raw: bool) -> Result { Ok(match addr { IpAddr::V4(_) => S::new_icmp_send_socket_ipv4(raw), IpAddr::V6(_) => S::new_icmp_send_socket_ipv6(raw), }?) } /// Make a socket for sending `UDP` packets. #[instrument(level = "trace")] fn make_udp_send_socket(addr: IpAddr, raw: bool) -> Result { Ok(match addr { IpAddr::V4(_) => S::new_udp_send_socket_ipv4(raw), IpAddr::V6(_) => S::new_udp_send_socket_ipv6(raw), }?) } /// Make a socket for receiving raw `ICMP` packets. #[instrument(level = "trace")] fn make_recv_socket(addr: IpAddr, raw: bool) -> Result { Ok(match addr { IpAddr::V4(ipv4addr) => S::new_recv_socket_ipv4(ipv4addr, raw), IpAddr::V6(ipv6addr) => S::new_recv_socket_ipv6(ipv6addr, raw), }?) } ================================================ FILE: crates/trippy-core/src/net/common.rs ================================================ use crate::error::ErrorKind; use crate::error::{Error, Result}; use std::net::SocketAddr; /// Utility methods to map errors. pub struct ErrorMapper; impl ErrorMapper { /// Convert [`ErrorKind::InProgress`] to [`Ok`]. pub fn in_progress(err: Error) -> Result<()> { match err { Error::IoError(io_err) => match io_err.kind() { ErrorKind::InProgress => Ok(()), _ => Err(Error::IoError(io_err)), }, err => Err(err), } } /// Convert [`io::ErrorKind::AddrInUse`] to [`Error::AddressInUse`]. #[must_use] pub fn addr_in_use(err: Error, addr: SocketAddr) -> Error { match err { Error::IoError(io_err) => match io_err.kind() { ErrorKind::Std(std::io::ErrorKind::AddrInUse) => Error::AddressInUse(addr), _ => Error::IoError(io_err), }, err => err, } } /// Convert a given [`ErrorKind`] to [`Error::ProbeFailed`]. #[expect(clippy::needless_pass_by_value)] pub fn probe_failed(err: Error, kind: ErrorKind) -> Error { match err { Error::IoError(io_err) if io_err.kind() == kind => Error::ProbeFailed(io_err), _ => err, } } } #[cfg(test)] mod tests { use super::*; use crate::error::IoError; use std::io; use std::net::{Ipv4Addr, SocketAddrV4}; const ADDR: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)); #[test] fn test_in_progress() { let io_err = io::Error::from(ErrorKind::InProgress); let err = Error::IoError(IoError::Bind(io_err, ADDR)); assert!(ErrorMapper::in_progress(err).is_ok()); } #[test] fn test_not_in_progress() { let io_err = io::Error::from(ErrorKind::Std(io::ErrorKind::Other)); let err = Error::IoError(IoError::Bind(io_err, ADDR)); assert!(ErrorMapper::in_progress(err).is_err()); } #[test] fn test_addr_in_use() { let io_err = io::Error::from(ErrorKind::Std(io::ErrorKind::AddrInUse)); let err = Error::IoError(IoError::Bind(io_err, ADDR)); let addr_in_use_err = ErrorMapper::addr_in_use(err, ADDR); assert!(matches!(addr_in_use_err, Error::AddressInUse(ADDR))); } #[test] fn test_not_addr_in_use() { let io_err = io::Error::from(ErrorKind::Std(io::ErrorKind::Other)); let err = Error::IoError(IoError::Bind(io_err, ADDR)); let addr_in_use_err = ErrorMapper::addr_in_use(err, ADDR); assert!(matches!(addr_in_use_err, Error::IoError(_))); } #[test] fn test_probe_failed() { let io_err = io::Error::from(ErrorKind::HostUnreachable); let err = Error::IoError(IoError::Bind(io_err, ADDR)); let probe_err = ErrorMapper::probe_failed(err, ErrorKind::HostUnreachable); assert!(matches!(probe_err, Error::ProbeFailed(_))); } } ================================================ FILE: crates/trippy-core/src/net/extension.rs ================================================ use crate::error::Error; use crate::probe::{Extension, Extensions, MplsLabelStack, MplsLabelStackMember, UnknownExtension}; use trippy_packet::icmp_extension::extension_header::ExtensionHeaderPacket; use trippy_packet::icmp_extension::extension_object::{ClassNum, ExtensionObjectPacket}; use trippy_packet::icmp_extension::extension_structure::ExtensionsPacket; use trippy_packet::icmp_extension::mpls_label_stack::MplsLabelStackPacket; use trippy_packet::icmp_extension::mpls_label_stack_member::MplsLabelStackMemberPacket; /// The supported ICMP extension version number. const ICMP_EXTENSION_VERSION: u8 = 2; impl TryFrom<&[u8]> for Extensions { type Error = Error; fn try_from(value: &[u8]) -> Result { Self::try_from(ExtensionsPacket::new_view(value)?) } } impl TryFrom> for Extensions { type Error = Error; fn try_from(value: ExtensionsPacket<'_>) -> Result { let header = ExtensionHeaderPacket::new_view(value.header())?; if header.get_version() != ICMP_EXTENSION_VERSION { return Ok(Self::default()); } let extensions = value .objects() .flat_map(ExtensionObjectPacket::new_view) .map(|obj| match obj.get_class_num() { ClassNum::MultiProtocolLabelSwitchingLabelStack => { MplsLabelStackPacket::new_view(obj.payload()) .map(|mpls| Extension::Mpls(MplsLabelStack::from(mpls))) } _ => Ok(Extension::Unknown(UnknownExtension::from(obj))), }) .collect::>()?; Ok(Self { extensions }) } } impl From> for MplsLabelStack { fn from(value: MplsLabelStackPacket<'_>) -> Self { Self { members: value .members() .flat_map(MplsLabelStackMemberPacket::new_view) .map(MplsLabelStackMember::from) .collect(), } } } impl From> for MplsLabelStackMember { fn from(value: MplsLabelStackMemberPacket<'_>) -> Self { Self { label: value.get_label(), exp: value.get_exp(), bos: value.get_bos(), ttl: value.get_ttl(), } } } impl From> for UnknownExtension { fn from(value: ExtensionObjectPacket<'_>) -> Self { Self { class_num: value.get_class_num().id(), class_subtype: value.get_class_subtype().0, bytes: value.payload().to_owned(), } } } #[cfg(test)] mod tests { use super::*; /// Convert a single MPLS extension which contains two labels. #[test] fn test_convert_mpls_extensions() { let buf = hex_literal::hex!("20 00 96 53 00 0c 01 01 06 9f 18 01 00 00 29 ff"); let exts = Extensions::try_from(buf.as_slice()).unwrap(); assert_eq!(1, exts.extensions.len()); match &exts.extensions[0] { Extension::Mpls(mpls) => { assert_eq!(2, mpls.members.len()); assert_eq!(27121, mpls.members[0].label); assert_eq!(1, mpls.members[0].ttl); assert_eq!(4, mpls.members[0].exp); assert_eq!(0, mpls.members[0].bos); assert_eq!(2, mpls.members[1].label); assert_eq!(255, mpls.members[1].ttl); assert_eq!(4, mpls.members[1].exp); assert_eq!(1, mpls.members[1].bos); } Extension::Unknown(_) => panic!("expected Extension::Mpls"), } } /// Convert a single unknown extension. #[test] fn test_convert_unknown_extensions() { let buf = hex_literal::hex!("20 00 96 53 00 0c 99 01 06 9f 18 01 00 00 29 ff"); let exts = Extensions::try_from(buf.as_slice()).unwrap(); assert_eq!(1, exts.extensions.len()); match &exts.extensions[0] { Extension::Unknown(unknown) => { assert_eq!(0x99, unknown.class_num); assert_eq!(0x01, unknown.class_subtype); assert_eq!( hex_literal::hex!("06 9f 18 01 00 00 29 ff"), unknown.bytes.as_slice() ); } Extension::Mpls(_) => panic!("expected Extension::Unknown"), } } /// Convert an extension with an unknown header version. #[test] fn test_convert_unknown_version() { let buf = hex_literal::hex!("30 00 96 53 00 0c 99 01 06 9f 18 01 00 00 29 ff"); let exts = Extensions::try_from(buf.as_slice()).unwrap(); assert_eq!(0, exts.extensions.len()); } } ================================================ FILE: crates/trippy-core/src/net/ipv4.rs ================================================ use crate::config::IcmpExtensionParseMode; use crate::error::{Error, ErrorKind, Result}; use crate::net::channel::MAX_PACKET_SIZE; use crate::net::common::ErrorMapper; use crate::net::platform; use crate::net::socket::{Socket, SocketError}; use crate::probe::{ Extensions, IcmpPacketCode, IcmpProtocolResponse, Probe, ProtocolResponse, Response, ResponseData, TcpProtocolResponse, UdpProtocolResponse, }; use crate::types::{PacketSize, PayloadPattern, Sequence, TraceId, TypeOfService}; use crate::{Flags, Port, PrivilegeMode, Protocol}; use std::io; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::SystemTime; use tracing::instrument; use trippy_packet::IpProtocol; use trippy_packet::checksum::{icmp_ipv4_checksum, udp_ipv4_checksum}; use trippy_packet::icmpv4::destination_unreachable::DestinationUnreachablePacket; use trippy_packet::icmpv4::echo_reply::EchoReplyPacket; use trippy_packet::icmpv4::echo_request::EchoRequestPacket; use trippy_packet::icmpv4::time_exceeded::TimeExceededPacket; use trippy_packet::icmpv4::{IcmpCode, IcmpPacket, IcmpTimeExceededCode, IcmpType}; use trippy_packet::ipv4::Ipv4Packet; use trippy_packet::tcp::TcpPacket; use trippy_packet::udp::UdpPacket; /// The maximum size of UDP packet we allow. const MAX_UDP_PACKET_BUF: usize = MAX_PACKET_SIZE - Ipv4Packet::minimum_packet_size(); /// The maximum size of UDP payload we allow. const MAX_UDP_PAYLOAD_BUF: usize = MAX_UDP_PACKET_BUF - UdpPacket::minimum_packet_size(); /// The maximum size of ICMP packet we allow. const MAX_ICMP_PACKET_BUF: usize = MAX_PACKET_SIZE - Ipv4Packet::minimum_packet_size(); /// The maximum size of ICMP payload we allow. const MAX_ICMP_PAYLOAD_BUF: usize = MAX_ICMP_PACKET_BUF - IcmpPacket::minimum_packet_size(); /// The minimum size of ICMP packets we allow. const MIN_PACKET_SIZE_ICMP: usize = Ipv4Packet::minimum_packet_size() + IcmpPacket::minimum_packet_size(); /// The minimum size of UDP packets we allow. const MIN_PACKET_SIZE_UDP: usize = Ipv4Packet::minimum_packet_size() + UdpPacket::minimum_packet_size(); /// The value for the IPv4 `flags_and_fragment_offset` field to set the `Don't fragment` bit. /// /// 0100 0000 0000 0000 const DONT_FRAGMENT: u16 = 0x4000; /// IPv4 configuration. #[derive(Debug)] pub struct Ipv4 { pub src_addr: Ipv4Addr, pub dest_addr: Ipv4Addr, pub byte_order: platform::Ipv4ByteOrder, pub packet_size: PacketSize, pub payload_pattern: PayloadPattern, pub privilege_mode: PrivilegeMode, pub tos: TypeOfService, pub protocol: Protocol, pub icmp_extension_mode: IcmpExtensionParseMode, } impl Default for Ipv4 { fn default() -> Self { Self { src_addr: Ipv4Addr::UNSPECIFIED, dest_addr: Ipv4Addr::UNSPECIFIED, byte_order: platform::Ipv4ByteOrder::Network, packet_size: PacketSize(0), payload_pattern: PayloadPattern(0), privilege_mode: PrivilegeMode::Privileged, tos: TypeOfService(0), protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, } } } impl Ipv4 { /// Dispatch an ICMP probe. #[instrument(skip(self, icmp_send_socket), level = "trace")] pub fn dispatch_icmp_probe( &self, icmp_send_socket: &mut S, probe: &Probe, ) -> Result<()> { let mut ipv4_buf = [0_u8; MAX_PACKET_SIZE]; let mut icmp_buf = [0_u8; MAX_ICMP_PACKET_BUF]; let packet_size = usize::from(self.packet_size.0); if !(MIN_PACKET_SIZE_ICMP..=MAX_PACKET_SIZE).contains(&packet_size) { return Err(Error::InvalidPacketSize(packet_size)); } let echo_request = self.make_echo_request_icmp_packet( &mut icmp_buf, probe.identifier, probe.sequence, icmp_payload_size(packet_size), )?; let ipv4 = self.make_ipv4_packet( &mut ipv4_buf, IpProtocol::Icmp, probe.ttl.0, 0, echo_request.packet(), )?; let remote_addr = SocketAddr::new(IpAddr::V4(self.dest_addr), 0); icmp_send_socket .send_to(ipv4.packet(), remote_addr) .map_err(Error::IoError) .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::HostUnreachable)) .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::NetUnreachable)) .map_err(|err| ErrorMapper::probe_failed(err, INVALID_INPUT_KIND))?; Ok(()) } /// Dispatch a UDP probe. #[instrument(skip(self, raw_send_socket), level = "trace")] pub fn dispatch_udp_probe( &self, raw_send_socket: &mut S, probe: &Probe, ) -> Result<()> { let packet_size = usize::from(self.packet_size.0); if !(MIN_PACKET_SIZE_UDP..=MAX_PACKET_SIZE).contains(&packet_size) { return Err(Error::InvalidPacketSize(packet_size)); } let payload_size = udp_payload_size(packet_size); let payload = &[self.payload_pattern.0; MAX_UDP_PAYLOAD_BUF][0..payload_size]; match self.privilege_mode { PrivilegeMode::Privileged => { self.dispatch_udp_probe_raw(raw_send_socket, probe, payload) } PrivilegeMode::Unprivileged => self.dispatch_udp_probe_non_raw::(probe, payload), } } /// Dispatch a UDP probe using a raw socket with `IP_HDRINCL` set. /// /// As `IP_HDRINCL` is set we must supply the IP and UDP headers which allows us to set custom /// values for certain fields such as the checksum as required by the Paris tracing strategy. #[instrument(skip(self, raw_send_socket), level = "trace")] fn dispatch_udp_probe_raw( &self, raw_send_socket: &mut S, probe: &Probe, payload: &[u8], ) -> Result<()> { let mut ipv4_buf = [0_u8; MAX_PACKET_SIZE]; let mut udp_buf = [0_u8; MAX_UDP_PACKET_BUF]; let payload_paris = probe.sequence.0.to_be_bytes(); let payload = if probe.flags.contains(Flags::PARIS_CHECKSUM) { payload_paris.as_slice() } else { payload }; let mut udp = self.make_udp_packet(&mut udp_buf, probe.src_port.0, probe.dest_port.0, payload)?; if probe.flags.contains(Flags::PARIS_CHECKSUM) { let checksum = udp.get_checksum().to_be_bytes(); let payload = u16::from_be_bytes(core::array::from_fn(|i| udp.payload()[i])); udp.set_checksum(payload); udp.set_payload(&checksum); } let ipv4 = self.make_ipv4_packet( &mut ipv4_buf, IpProtocol::Udp, probe.ttl.0, probe.identifier.0, udp.packet(), )?; let remote_addr = SocketAddr::new(IpAddr::V4(self.dest_addr), probe.dest_port.0); raw_send_socket .send_to(ipv4.packet(), remote_addr) .map_err(Error::IoError) .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::HostUnreachable)) .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::NetUnreachable))?; Ok(()) } /// Dispatch a UDP probe using a new UDP datagram socket. #[instrument(skip(self), level = "trace")] fn dispatch_udp_probe_non_raw(&self, probe: &Probe, payload: &[u8]) -> Result<()> { let local_addr = SocketAddr::new(IpAddr::V4(self.src_addr), probe.src_port.0); let remote_addr = SocketAddr::new(IpAddr::V4(self.dest_addr), probe.dest_port.0); let mut socket = S::new_udp_send_socket_ipv4(false)?; socket .bind(local_addr) .map_err(Error::IoError) .or_else(ErrorMapper::in_progress) .map_err(|err| ErrorMapper::addr_in_use(err, local_addr)) .map_err(|err| ErrorMapper::probe_failed(err, ADDR_NOT_AVAILABLE_KIND))?; socket.set_ttl(u32::from(probe.ttl.0))?; socket.set_tos(u32::from(self.tos.0))?; socket.send_to(payload, remote_addr)?; Ok(()) } /// Dispatch a TCP probe. #[instrument(skip(self), level = "trace")] pub fn dispatch_tcp_probe(&self, probe: &Probe) -> Result { let mut socket = S::new_stream_socket_ipv4()?; let local_addr = SocketAddr::new(IpAddr::V4(self.src_addr), probe.src_port.0); socket .bind(local_addr) .map_err(Error::IoError) .or_else(ErrorMapper::in_progress) .map_err(|err| ErrorMapper::addr_in_use(err, local_addr)) .map_err(|err| ErrorMapper::probe_failed(err, ADDR_NOT_AVAILABLE_KIND))?; socket.set_ttl(u32::from(probe.ttl.0))?; socket.set_tos(u32::from(self.tos.0))?; let remote_addr = SocketAddr::new(IpAddr::V4(self.dest_addr), probe.dest_port.0); socket .connect(remote_addr) .map_err(Error::IoError) .or_else(ErrorMapper::in_progress) .map_err(|err| ErrorMapper::addr_in_use(err, remote_addr)) .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::NetUnreachable))?; Ok(socket) } /// Receive an ICMP probe response. #[instrument(skip(self, recv_socket), level = "trace")] pub fn recv_icmp_probe(&self, recv_socket: &mut S) -> Result> { let mut buf = [0_u8; MAX_PACKET_SIZE]; match recv_socket.read(&mut buf) { Ok(bytes_read) => { let ipv4 = Ipv4Packet::new_view(&buf[..bytes_read])?; Ok(self.extract_probe_resp(&ipv4)?) } Err(err) => match err.kind() { ErrorKind::Std(io::ErrorKind::WouldBlock) => Ok(None), _ => Err(Error::IoError(err)), }, } } /// Receive a TCP probe response. #[instrument(skip(self, tcp_socket), level = "trace")] pub fn recv_tcp_socket( &self, tcp_socket: &mut S, src_port: Port, dest_port: Port, ) -> Result> { let proto_resp = ProtocolResponse::Tcp(TcpProtocolResponse::new( IpAddr::V4(self.dest_addr), src_port.0, dest_port.0, None, )); match tcp_socket.take_error()? { None => { let addr = tcp_socket.peer_addr()?.ok_or(Error::MissingAddr)?.ip(); tcp_socket.shutdown()?; return Ok(Some(Response::TcpReply(ResponseData::new( SystemTime::now(), addr, proto_resp, )))); } Some(err) => match err { SocketError::ConnectionRefused => { return Ok(Some(Response::TcpRefused(ResponseData::new( SystemTime::now(), IpAddr::V4(self.dest_addr), proto_resp, )))); } SocketError::HostUnreachable => { let error_addr = tcp_socket.icmp_error_info()?; return Ok(Some(Response::TimeExceeded( ResponseData::new(SystemTime::now(), error_addr, proto_resp), IcmpPacketCode(1), None, ))); } SocketError::Other(_) => {} }, } Ok(None) } #[instrument(skip(self), level = "trace")] fn extract_probe_resp(&self, ipv4: &Ipv4Packet<'_>) -> Result> { let recv = SystemTime::now(); let src = IpAddr::V4(ipv4.get_source()); let icmp_v4 = IcmpPacket::new_view(ipv4.payload())?; let icmp_type = icmp_v4.get_icmp_type(); let icmp_code = icmp_v4.get_icmp_code(); Ok(match icmp_type { IcmpType::TimeExceeded => { if IcmpTimeExceededCode::from(icmp_code) == IcmpTimeExceededCode::TtlExpired { let packet = TimeExceededPacket::new_view(icmp_v4.packet())?; let (nested_ipv4, extension) = match self.icmp_extension_mode { IcmpExtensionParseMode::Enabled => { let ipv4 = Ipv4Packet::new_view(packet.payload())?; let ext = packet.extension().map(Extensions::try_from).transpose()?; (ipv4, ext) } IcmpExtensionParseMode::Disabled => { let ipv4 = Ipv4Packet::new_view(packet.payload_raw())?; (ipv4, None) } }; self.extract_probe_proto_resp(&nested_ipv4)? .map(|proto_resp| { Response::TimeExceeded( ResponseData::new(recv, src, proto_resp), IcmpPacketCode(icmp_code.0), extension, ) }) } else { None } } IcmpType::DestinationUnreachable => { let packet = DestinationUnreachablePacket::new_view(icmp_v4.packet())?; let nested_ipv4 = Ipv4Packet::new_view(packet.payload())?; let extension = match self.icmp_extension_mode { IcmpExtensionParseMode::Enabled => { packet.extension().map(Extensions::try_from).transpose()? } IcmpExtensionParseMode::Disabled => None, }; self.extract_probe_proto_resp(&nested_ipv4)? .map(|proto_resp| { Response::DestinationUnreachable( ResponseData::new(recv, src, proto_resp), IcmpPacketCode(icmp_code.0), extension, ) }) } IcmpType::EchoReply => match self.protocol { Protocol::Icmp => { let packet = EchoReplyPacket::new_view(icmp_v4.packet())?; let id = packet.get_identifier(); let seq = packet.get_sequence(); let proto_resp = ProtocolResponse::Icmp(IcmpProtocolResponse::new(id, seq, None)); Some(Response::EchoReply( ResponseData::new(recv, src, proto_resp), IcmpPacketCode(icmp_code.0), )) } Protocol::Udp | Protocol::Tcp => None, }, _ => None, }) } #[instrument(skip(self), level = "trace")] fn extract_probe_proto_resp(&self, ipv4: &Ipv4Packet<'_>) -> Result> { Ok(match (self.protocol, ipv4.get_protocol()) { (Protocol::Icmp, IpProtocol::Icmp) => { let echo_request = extract_echo_request(ipv4)?; let identifier = echo_request.get_identifier(); let sequence = echo_request.get_sequence(); Some(ProtocolResponse::Icmp(IcmpProtocolResponse::new( identifier, sequence, Some(TypeOfService(ipv4.get_tos())), ))) } (Protocol::Udp, IpProtocol::Udp) => { let (src_port, dest_port, actual_checksum, identifier, payload_length) = extract_udp_packet(ipv4)?; let expected_checksum = self.calc_udp_checksum(Port(src_port), Port(dest_port), payload_length)?; Some(ProtocolResponse::Udp(UdpProtocolResponse::new( identifier, IpAddr::V4(ipv4.get_destination()), src_port, dest_port, Some(TypeOfService(ipv4.get_tos())), expected_checksum, actual_checksum, payload_length, false, ))) } (Protocol::Tcp, IpProtocol::Tcp) => { let (src_port, dest_port) = extract_tcp_packet(ipv4)?; Some(ProtocolResponse::Tcp(TcpProtocolResponse::new( IpAddr::V4(ipv4.get_destination()), src_port, dest_port, Some(TypeOfService(ipv4.get_tos())), ))) } _ => None, }) } /// Create an ICMP `EchoRequest` packet. fn make_echo_request_icmp_packet<'a>( &self, icmp_buf: &'a mut [u8], identifier: TraceId, sequence: Sequence, payload_size: usize, ) -> Result> { let payload_buf = [self.payload_pattern.0; MAX_ICMP_PAYLOAD_BUF]; let packet_size = IcmpPacket::minimum_packet_size() + payload_size; let mut icmp = EchoRequestPacket::new(&mut icmp_buf[..packet_size])?; icmp.set_icmp_type(IcmpType::EchoRequest); icmp.set_icmp_code(IcmpCode(0)); icmp.set_identifier(identifier.0); icmp.set_payload(&payload_buf[..payload_size]); icmp.set_sequence(sequence.0); icmp.set_checksum(icmp_ipv4_checksum(icmp.packet())); Ok(icmp) } /// Create a `UdpPacket` fn make_udp_packet<'a>( &self, udp_buf: &'a mut [u8], src_port: u16, dest_port: u16, payload: &'_ [u8], ) -> Result> { let udp_packet_size = UdpPacket::minimum_packet_size() + payload.len(); let mut udp = UdpPacket::new(&mut udp_buf[..udp_packet_size])?; udp.set_source(src_port); udp.set_destination(dest_port); udp.set_length(udp_packet_size as u16); udp.set_payload(payload); udp.set_checksum(udp_ipv4_checksum( udp.packet(), self.src_addr, self.dest_addr, )); Ok(udp) } /// Create an `Ipv4Packet`. fn make_ipv4_packet<'a>( &self, ipv4_buf: &'a mut [u8], protocol: IpProtocol, ttl: u8, identification: u16, payload: &[u8], ) -> Result> { let ipv4_total_length = (Ipv4Packet::minimum_packet_size() + payload.len()) as u16; let ipv4_total_length_header = self.byte_order.adjust_length(ipv4_total_length); let ipv4_flags_and_fragment_offset_header = self.byte_order.adjust_length(DONT_FRAGMENT); let mut ipv4 = Ipv4Packet::new(&mut ipv4_buf[..ipv4_total_length as usize])?; ipv4.set_version(4); ipv4.set_header_length(5); ipv4.set_total_length(ipv4_total_length_header); ipv4.set_ttl(ttl); ipv4.set_protocol(protocol); ipv4.set_source(self.src_addr); ipv4.set_destination(self.dest_addr); ipv4.set_tos(self.tos.0); ipv4.set_payload(payload); ipv4.set_identification(identification); ipv4.set_flags_and_fragment_offset(ipv4_flags_and_fragment_offset_header); Ok(ipv4) } /// Calculate the expected checksum for a UDP packet. /// /// Note that this calculation takes place for incoming UDP packet before /// packet validation and so this may not be a packet sent by us and so we /// cannot assume the payload size is within the bounds of `MAX_UDP_PAYLOAD_BUF`. pub fn calc_udp_checksum( &self, src_port: Port, dest_port: Port, payload_size: u16, ) -> Result { let mut udp_buf = [0_u8; MAX_UDP_PACKET_BUF]; let size = usize::from(payload_size).min(MAX_UDP_PAYLOAD_BUF); let payload = &[self.payload_pattern.0; MAX_UDP_PAYLOAD_BUF][0..size]; let udp = self.make_udp_packet(&mut udp_buf, src_port.0, dest_port.0, payload)?; Ok(udp.get_checksum()) } } const ADDR_NOT_AVAILABLE_KIND: ErrorKind = ErrorKind::Std(io::ErrorKind::AddrNotAvailable); const INVALID_INPUT_KIND: ErrorKind = ErrorKind::Std(io::ErrorKind::InvalidInput); const fn icmp_payload_size(packet_size: usize) -> usize { let ip_header_size = Ipv4Packet::minimum_packet_size(); let icmp_header_size = IcmpPacket::minimum_packet_size(); packet_size - icmp_header_size - ip_header_size } const fn udp_payload_size(packet_size: usize) -> usize { let ip_header_size = Ipv4Packet::minimum_packet_size(); let udp_header_size = UdpPacket::minimum_packet_size(); packet_size - udp_header_size - ip_header_size } #[instrument(level = "trace")] fn extract_echo_request<'a>(ipv4: &'a Ipv4Packet<'a>) -> Result> { Ok(EchoRequestPacket::new_view(ipv4.payload())?) } /// Get the src and dest ports from the original `UdpPacket` packet embedded in the payload. #[instrument(level = "trace")] fn extract_udp_packet(ipv4: &Ipv4Packet<'_>) -> Result<(u16, u16, u16, u16, u16)> { let nested = UdpPacket::new_view(ipv4.payload())?; Ok(( nested.get_source(), nested.get_destination(), nested.get_checksum(), ipv4.get_identification(), nested.get_length() - UdpPacket::minimum_packet_size() as u16, )) } /// Get the src and dest ports from the original `TcpPacket` packet embedded in the payload. /// /// Unlike the embedded `ICMP` and `UDP` packets, which have a minimum header size of 8 bytes, the /// `TCP` packet header is a minimum of 20 bytes. /// /// The `ICMP` packets we are extracting these from, such as `TimeExceeded`, only guarantee that 8 /// bytes of the original packet (plus the IP header) be returned, and so we may not have a complete /// TCP packet. /// /// We therefore have to detect this situation and ensure we provide buffer a large enough for a /// complete TCP packet header. #[instrument(level = "trace")] fn extract_tcp_packet(ipv4: &Ipv4Packet<'_>) -> Result<(u16, u16)> { let nested_tcp = ipv4.payload(); if nested_tcp.len() < TcpPacket::minimum_packet_size() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; buf[..nested_tcp.len()].copy_from_slice(nested_tcp); let tcp_packet = TcpPacket::new_view(&buf)?; Ok((tcp_packet.get_source(), tcp_packet.get_destination())) } else { let tcp_packet = TcpPacket::new_view(nested_tcp)?; Ok((tcp_packet.get_source(), tcp_packet.get_destination())) } } #[cfg(test)] mod tests { use super::*; use crate::error::IoResult; use crate::mocket_read; use crate::net::socket::MockSocket; use crate::{Flags, Port, RoundId, TimeToLive}; use mockall::predicate; use std::str::FromStr; use std::sync::Mutex; static MTX: Mutex<()> = Mutex::new(()); // Test dispatching a IPv4/ICMP probe. #[test] fn test_dispatch_icmp_probe_no_payload() -> anyhow::Result<()> { let probe = make_icmp_probe(); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let packet_size = PacketSize(28); let payload_pattern = PayloadPattern(0x00); let byte_order = platform::Ipv4ByteOrder::Network; let expected_send_to_buf = hex_literal::hex!( " 45 00 00 1c 00 00 40 00 0a 01 00 00 01 02 03 04 05 06 07 08 08 00 70 93 04 d2 82 9a " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 0); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, ..Default::default() }; ipv4.dispatch_icmp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_icmp_probe_with_payload() -> anyhow::Result<()> { let probe = make_icmp_probe(); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let packet_size = PacketSize(48); let payload_pattern = PayloadPattern(0xff); let byte_order = platform::Ipv4ByteOrder::Network; let expected_send_to_buf = hex_literal::hex!( " 45 00 00 30 00 00 40 00 0a 01 00 00 01 02 03 04 05 06 07 08 08 00 70 93 04 d2 82 9a ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 0); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, ..Default::default() }; ipv4.dispatch_icmp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_icmp_probe_invalid_packet_size_low() -> anyhow::Result<()> { let probe = make_icmp_probe(); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let packet_size = PacketSize(27); let payload_pattern = PayloadPattern(0x00); let byte_order = platform::Ipv4ByteOrder::Network; let mut mocket = MockSocket::new(); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, ..Default::default() }; let err = ipv4.dispatch_icmp_probe(&mut mocket, &probe).unwrap_err(); assert!(matches!(err, Error::InvalidPacketSize(_))); Ok(()) } #[test] fn test_dispatch_icmp_probe_invalid_packet_size_high() -> anyhow::Result<()> { let probe = make_icmp_probe(); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let packet_size = PacketSize(1025); let payload_pattern = PayloadPattern(0x00); let byte_order = platform::Ipv4ByteOrder::Network; let mut mocket = MockSocket::new(); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, ..Default::default() }; let err = ipv4.dispatch_icmp_probe(&mut mocket, &probe).unwrap_err(); assert!(matches!(err, Error::InvalidPacketSize(_))); Ok(()) } #[test] fn test_dispatch_icmp_probe_with_tos() -> anyhow::Result<()> { let probe = make_icmp_probe(); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let packet_size = PacketSize(28); let payload_pattern = PayloadPattern(0x00); let tos = TypeOfService(0xE0); let byte_order = platform::Ipv4ByteOrder::Network; let expected_send_to_buf = hex_literal::hex!( " 45 e0 00 1c 00 00 40 00 0a 01 00 00 01 02 03 04 05 06 07 08 08 00 70 93 04 d2 82 9a " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 0); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, tos, ..Default::default() }; ipv4.dispatch_icmp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_classic_privileged_no_payload() -> anyhow::Result<()> { let probe = make_udp_probe(123, 456); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(28); let payload_pattern = PayloadPattern(0x00); let byte_order = platform::Ipv4ByteOrder::Network; let expected_send_to_buf = hex_literal::hex!( " 45 00 00 1c 04 d2 40 00 0a 11 00 00 01 02 03 04 05 06 07 08 00 7b 01 c8 00 08 ed 87 " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, ..Default::default() }; ipv4.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_classic_privileged_with_payload() -> anyhow::Result<()> { let probe = make_udp_probe(123, 456); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(38); let payload_pattern = PayloadPattern(0xaa); let byte_order = platform::Ipv4ByteOrder::Network; let expected_send_to_buf = hex_literal::hex!( " 45 00 00 26 04 d2 40 00 0a 11 00 00 01 02 03 04 05 06 07 08 00 7b 01 c8 00 12 98 1e aa aa aa aa aa aa aa aa aa aa " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, ..Default::default() }; ipv4.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_paris_privileged() -> anyhow::Result<()> { let probe = Probe { flags: Flags::PARIS_CHECKSUM, ..make_udp_probe(123, 456) }; let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Privileged; // packet size and payload pattern are ignored for paris mode as a // fixed two byte payload is used to hold the sequence let packet_size = PacketSize(300); let payload_pattern = PayloadPattern(0xaa); let byte_order = platform::Ipv4ByteOrder::Network; let expected_send_to_buf = hex_literal::hex!( " 45 00 00 1e 04 d2 40 00 0a 11 00 00 01 02 03 04 05 06 07 08 00 7b 01 c8 00 0a 82 9a 6a e9 " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, ..Default::default() }; ipv4.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_dublin_privileged() -> anyhow::Result<()> { let probe = Probe { // note: this is always set for UDP/Dublin but is a no-op for IPv4 flags: Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, identifier: TraceId(33434), ..make_udp_probe(123, 456) }; let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(28); let payload_pattern = PayloadPattern(0xaa); let byte_order = platform::Ipv4ByteOrder::Network; let expected_send_to_buf = hex_literal::hex!( " 45 00 00 1c 82 9a 40 00 0a 11 00 00 01 02 03 04 05 06 07 08 00 7b 01 c8 00 08 ed 87 " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, ..Default::default() }; ipv4.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_classic_unprivileged_no_payload() -> anyhow::Result<()> { let _m = MTX.lock(); let probe = make_udp_probe(123, 456); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Unprivileged; let packet_size = PacketSize(28); let payload_pattern = PayloadPattern(0x00); let byte_order = platform::Ipv4ByteOrder::Network; let expected_send_to_buf = hex_literal::hex!(""); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let expected_bind_addr = SocketAddr::new(IpAddr::V4(src_addr), 123); let expected_set_ttl = 10; let expected_set_tos = 0; let mut mocket = MockSocket::new(); let ctx = MockSocket::new_udp_send_socket_ipv4_context(); ctx.expect().with(predicate::eq(false)).returning(move |_| { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|_| Ok(())); mocket .expect_set_ttl() .with(predicate::eq(expected_set_ttl)) .times(1) .returning(|_| Ok(())); mocket .expect_set_tos() .with(predicate::eq(expected_set_tos)) .times(1) .returning(|_| Ok(())); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); Ok(mocket) }); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, ..Default::default() }; ipv4.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_classic_unprivileged_with_payload() -> anyhow::Result<()> { let _m = MTX.lock(); let probe = make_udp_probe(123, 456); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Unprivileged; let packet_size = PacketSize(36); let payload_pattern = PayloadPattern(0x1f); let byte_order = platform::Ipv4ByteOrder::Network; let expected_send_to_buf = hex_literal::hex!("1f 1f 1f 1f 1f 1f 1f 1f"); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let expected_bind_addr = SocketAddr::new(IpAddr::V4(src_addr), 123); let expected_set_ttl = 10; let expected_set_tos = 0; let mut mocket = MockSocket::new(); let ctx = MockSocket::new_udp_send_socket_ipv4_context(); ctx.expect().with(predicate::eq(false)).returning(move |_| { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|_| Ok(())); mocket .expect_set_ttl() .with(predicate::eq(expected_set_ttl)) .times(1) .returning(|_| Ok(())); mocket .expect_set_tos() .with(predicate::eq(expected_set_tos)) .times(1) .returning(|_| Ok(())); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); Ok(mocket) }); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, ..Default::default() }; ipv4.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_classic_privileged_with_tos() -> anyhow::Result<()> { let probe = make_udp_probe(123, 456); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(28); let payload_pattern = PayloadPattern(0x00); let byte_order = platform::Ipv4ByteOrder::Network; let tos = TypeOfService(0xE0); let expected_send_to_buf = hex_literal::hex!( " 45 e0 00 1c 04 d2 40 00 0a 11 00 00 01 02 03 04 05 06 07 08 00 7b 01 c8 00 08 ed 87 " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, tos, ..Default::default() }; ipv4.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_classic_unprivileged_with_tos() -> anyhow::Result<()> { let _m = MTX.lock(); let probe = make_udp_probe(123, 456); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Unprivileged; let packet_size = PacketSize(28); let payload_pattern = PayloadPattern(0x00); let byte_order = platform::Ipv4ByteOrder::Network; let tos = TypeOfService(224); let expected_send_to_buf = hex_literal::hex!(""); let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let expected_bind_addr = SocketAddr::new(IpAddr::V4(src_addr), 123); let expected_set_ttl = 10; let expected_set_tos = u32::from(tos.0); let mut mocket = MockSocket::new(); let ctx = MockSocket::new_udp_send_socket_ipv4_context(); ctx.expect().with(predicate::eq(false)).returning(move |_| { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|_| Ok(())); mocket .expect_set_ttl() .with(predicate::eq(expected_set_ttl)) .times(1) .returning(|_| Ok(())); mocket .expect_set_tos() .with(predicate::eq(expected_set_tos)) .times(1) .returning(|_| Ok(())); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); Ok(mocket) }); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, tos, ..Default::default() }; ipv4.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_invalid_packet_size_low() -> anyhow::Result<()> { let probe = make_udp_probe(123, 456); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(27); let payload_pattern = PayloadPattern(0x00); let byte_order = platform::Ipv4ByteOrder::Network; let mut mocket = MockSocket::new(); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, ..Default::default() }; let err = ipv4.dispatch_udp_probe(&mut mocket, &probe).unwrap_err(); assert!(matches!(err, Error::InvalidPacketSize(_))); Ok(()) } #[test] fn test_dispatch_udp_probe_invalid_packet_size_high() -> anyhow::Result<()> { let probe = make_udp_probe(123, 456); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(1025); let payload_pattern = PayloadPattern(0x00); let byte_order = platform::Ipv4ByteOrder::Network; let mut mocket = MockSocket::new(); let ipv4 = Ipv4 { src_addr, dest_addr, byte_order, packet_size, payload_pattern, privilege_mode, ..Default::default() }; let err = ipv4.dispatch_udp_probe(&mut mocket, &probe).unwrap_err(); assert!(matches!(err, Error::InvalidPacketSize(_))); Ok(()) } #[test] fn test_dispatch_tcp_probe() -> anyhow::Result<()> { let _m = MTX.lock(); let probe = make_udp_probe(123, 456); let src_addr = Ipv4Addr::from_str("1.2.3.4")?; let dest_addr = Ipv4Addr::from_str("5.6.7.8")?; let tos = TypeOfService(224); let expected_bind_addr = SocketAddr::new(IpAddr::V4(src_addr), 123); let expected_set_ttl = 10; let expected_set_tos = u32::from(tos.0); let expected_connect_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let ctx = MockSocket::new_stream_socket_ipv4_context(); ctx.expect().returning(move || { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|_| Ok(())); mocket .expect_set_ttl() .with(predicate::eq(expected_set_ttl)) .times(1) .returning(|_| Ok(())); mocket .expect_set_tos() .with(predicate::eq(expected_set_tos)) .times(1) .returning(|_| Ok(())); mocket .expect_connect() .with(predicate::eq(expected_connect_addr)) .times(1) .returning(|_| Ok(())); Ok(mocket) }); let ipv4 = Ipv4 { src_addr, dest_addr, tos, ..Default::default() }; ipv4.dispatch_tcp_probe::(&probe)?; Ok(()) } #[test] fn test_recv_icmp_probe_echo_reply() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 20 00 54 00 00 00 00 3b 01 50 02 8e fb de ce c0 a8 01 15 00 00 09 0f 75 d7 81 19 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(1) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::EchoReply( ResponseData { addr, proto_resp: ProtocolResponse::Icmp(IcmpProtocolResponse { identifier, sequence, tos, }), .. }, icmp_code, ) = resp else { panic!("expected EchoReply") }; assert_eq!(IpAddr::V4(Ipv4Addr::from_str("142.251.222.206")?), addr); assert_eq!(30167, identifier); assert_eq!(33049, sequence); assert_eq!(None, tos); assert_eq!(IcmpPacketCode(0), icmp_code); Ok(()) } #[test] fn test_recv_icmp_probe_time_exceeded_icmp_no_extensions() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 20 00 70 07 d7 00 00 3b 01 e9 5d 8e fa 3d 81 c0 a8 01 15 0b 00 f4 ff 00 00 00 00 45 60 00 54 65 b0 40 00 01 01 e4 11 c0 a8 01 15 8e fb de ce 08 00 01 11 75 d7 81 17 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(1) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Icmp(IcmpProtocolResponse { identifier, sequence, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(IpAddr::V4(Ipv4Addr::from_str("142.250.61.129")?), addr); assert_eq!(30167, identifier); assert_eq!(33047, sequence); assert_eq!(Some(TypeOfService(96)), tos); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_destination_unreachable_icmp_no_extensions() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 20 00 38 00 00 40 00 70 01 33 ea 14 00 00 fe c0 a8 01 15 03 01 fc fe 00 00 00 00 45 00 00 54 00 00 40 00 80 01 23 ee c0 a8 01 15 14 00 00 fe 08 00 fb d9 7b 01 81 24 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(1) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::DestinationUnreachable( ResponseData { addr, proto_resp: ProtocolResponse::Icmp(IcmpProtocolResponse { identifier, sequence, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected DestinationUnreachable") }; assert_eq!(IpAddr::V4(Ipv4Addr::from_str("20.0.0.254")?), addr); assert_eq!(31489, identifier); assert_eq!(33060, sequence); assert_eq!(Some(TypeOfService(0)), tos); assert_eq!(IcmpPacketCode(1), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_time_exceeded_udp_no_extensions() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 c0 00 70 0e c8 00 00 40 01 e7 9e c0 a8 01 01 c0 a8 01 15 0b 00 12 98 00 00 00 00 45 00 00 54 90 69 00 00 01 11 0b ea c0 a8 01 15 8e fa cc 8e 7c 55 81 06 00 40 e4 cb 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(1) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Udp, src_addr: Ipv4Addr::from_str("192.168.1.21")?, dest_addr: Ipv4Addr::from_str("142.250.204.142")?, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Udp(UdpProtocolResponse { identifier, dest_addr, src_port, dest_port, tos, expected_udp_checksum, actual_udp_checksum, payload_len, has_magic, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(IpAddr::V4(Ipv4Addr::from_str("192.168.1.1")?), addr); assert_eq!(36969, identifier); assert_eq!( IpAddr::V4(Ipv4Addr::from_str("142.250.204.142")?), dest_addr ); assert_eq!(31829, src_port); assert_eq!(33030, dest_port); assert_eq!(Some(TypeOfService(0)), tos); assert_eq!(58571, expected_udp_checksum); assert_eq!(58571, actual_udp_checksum); assert_eq!(56, payload_len); assert!(!has_magic); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_destination_unreachable_udp_no_extensions() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 20 00 70 bc f6 00 00 39 01 f0 a7 09 09 09 09 c0 a8 01 15 03 0a d1 16 00 00 00 00 45 20 00 54 a2 09 00 00 01 11 43 a1 c0 a8 01 15 09 09 09 09 80 0b 80 f2 00 40 2a a1 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(1) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Udp, src_addr: Ipv4Addr::from_str("192.168.1.21")?, dest_addr: Ipv4Addr::from_str("9.9.9.9")?, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::DestinationUnreachable( ResponseData { addr, proto_resp: ProtocolResponse::Udp(UdpProtocolResponse { identifier, dest_addr, src_port, dest_port, tos, expected_udp_checksum, actual_udp_checksum, payload_len, has_magic, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected DestinationUnreachable") }; assert_eq!(IpAddr::V4(Ipv4Addr::from_str("9.9.9.9")?), addr); assert_eq!(41481, identifier); assert_eq!(IpAddr::V4(Ipv4Addr::from_str("9.9.9.9")?), dest_addr); assert_eq!(32779, src_port); assert_eq!(33010, dest_port); assert_eq!(Some(TypeOfService(32)), tos); assert_eq!(10913, expected_udp_checksum); assert_eq!(10913, actual_udp_checksum); assert_eq!(56, payload_len); assert!(!has_magic); assert_eq!(IcmpPacketCode(10), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_time_exceeded_tcp_no_extensions() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 20 00 5c a6 9d 00 00 3b 01 54 e5 d1 55 f0 eb c0 a8 01 15 0b 00 12 79 00 00 00 00 45 80 00 40 00 00 40 00 01 06 5b f2 c0 a8 01 15 8e fa cc 8e 80 fd 00 50 61 f2 4d 4a 00 00 00 00 b0 02 ff ff 14 05 00 00 02 04 05 b4 01 03 03 06 01 01 08 0a 55 59 7f cd 00 00 00 00 04 02 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(1) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(IpAddr::V4(Ipv4Addr::from_str("209.85.240.235")?), addr); assert_eq!( IpAddr::V4(Ipv4Addr::from_str("142.250.204.142")?), dest_addr ); assert_eq!(33021, src_port); assert_eq!(80, dest_port); assert_eq!(Some(TypeOfService(128)), tos); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_destination_unreachable_tcp_no_extensions() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 20 00 5c d6 e0 00 00 39 01 d6 d1 09 09 09 09 c0 a8 01 15 03 0a d0 f7 00 00 00 00 45 20 00 40 00 00 00 00 01 06 e5 c9 c0 a8 01 15 09 09 09 09 80 f2 27 1b 5e b1 fa c7 00 00 00 00 b0 02 ff ff a4 53 00 00 02 04 05 b4 01 03 03 06 01 01 08 0a 1d 02 a0 50 00 00 00 00 04 02 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(1) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::DestinationUnreachable( ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected DestinationUnreachable") }; assert_eq!(IpAddr::V4(Ipv4Addr::from_str("9.9.9.9")?), addr); assert_eq!(IpAddr::V4(Ipv4Addr::from_str("9.9.9.9")?), dest_addr); assert_eq!(33010, src_port); assert_eq!(10011, dest_port); assert_eq!(Some(TypeOfService(32)), tos); assert_eq!(IcmpPacketCode(10), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_wrong_icmp_original_datagram_type_ignored() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 20 00 70 07 d7 00 00 3b 01 e9 5d 8e fa 3d 81 c0 a8 01 15 0b 00 f4 ff 00 00 00 00 45 60 00 54 65 b0 40 00 01 01 e4 11 c0 a8 01 15 8e fb de ce 08 00 01 11 75 d7 81 17 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(3) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_some()); let ipv4 = Ipv4 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); let ipv4 = Ipv4 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); Ok(()) } #[test] fn test_recv_icmp_probe_wrong_udp_original_datagram_type_ignored() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 c0 00 70 0e c8 00 00 40 01 e7 9e c0 a8 01 01 c0 a8 01 15 0b 00 12 98 00 00 00 00 45 00 00 54 90 69 00 00 01 11 0b ea c0 a8 01 15 8e fa cc 8e 7c 55 81 06 00 40 e4 cb 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(3) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_some()); let ipv4 = Ipv4 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); let ipv4 = Ipv4 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); Ok(()) } #[test] fn test_recv_icmp_probe_wrong_tcp_original_datagram_type_ignored() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 20 00 5c a6 9d 00 00 3b 01 54 e5 d1 55 f0 eb c0 a8 01 15 0b 00 12 79 00 00 00 00 45 80 00 40 00 00 40 00 01 06 5b f2 c0 a8 01 15 8e fa cc 8e 80 fd 00 50 61 f2 4d 4a 00 00 00 00 b0 02 ff ff 14 05 00 00 02 04 05 b4 01 03 03 06 01 01 08 0a 55 59 7f cd 00 00 00 00 04 02 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(3) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_some()); let ipv4 = Ipv4 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); let ipv4 = Ipv4 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); Ok(()) } #[test] fn test_recv_tcp_socket_tcp_reply() -> anyhow::Result<()> { let dest_addr = Ipv4Addr::from_str("1.2.3.4")?; let expected_peer_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456); let mut mocket = MockSocket::new(); mocket.expect_take_error().times(1).returning(|| Ok(None)); mocket .expect_peer_addr() .times(1) .returning(move || Ok(Some(expected_peer_addr))); mocket.expect_shutdown().times(1).returning(|| Ok(())); let ipv4 = Ipv4 { dest_addr, ..Default::default() }; let resp = ipv4 .recv_tcp_socket(&mut mocket, Port(33434), Port(456))? .unwrap(); let Response::TcpReply(ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }) = resp else { panic!("expected TcpReply") }; assert_eq!(dest_addr, addr); assert_eq!(33434, src_port); assert_eq!(456, dest_port); assert_eq!(None, tos); Ok(()) } #[test] fn test_recv_tcp_socket_tcp_refused() -> anyhow::Result<()> { let dest_addr = Ipv4Addr::from_str("1.2.3.4")?; let mut mocket = MockSocket::new(); mocket .expect_take_error() .times(1) .returning(|| Ok(Some(SocketError::ConnectionRefused))); let ipv4 = Ipv4 { dest_addr, ..Default::default() }; let resp = ipv4 .recv_tcp_socket(&mut mocket, Port(33434), Port(80))? .unwrap(); let Response::TcpRefused(ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }) = resp else { panic!("expected TcpRefused") }; assert_eq!(dest_addr, addr); assert_eq!(33434, src_port); assert_eq!(80, dest_port); assert_eq!(None, tos); Ok(()) } #[test] fn test_recv_tcp_socket_tcp_host_unreachable() -> anyhow::Result<()> { let dest_addr = Ipv4Addr::from_str("1.2.3.4")?; let mut mocket = MockSocket::new(); mocket .expect_take_error() .times(1) .returning(|| Ok(Some(SocketError::HostUnreachable))); mocket .expect_icmp_error_info() .times(1) .returning(move || Ok(IpAddr::V4(dest_addr))); let ipv4 = Ipv4 { dest_addr, ..Default::default() }; let resp = ipv4 .recv_tcp_socket(&mut mocket, Port(33434), Port(80))? .unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(dest_addr, addr); assert_eq!(33434, src_port); assert_eq!(80, dest_port); assert_eq!(None, tos); assert_eq!(IcmpPacketCode(1), icmp_code); assert_eq!(None, extensions); Ok(()) } // This IPv4/ICMP `TimeExceeded` packet has code 1 ("Fragment reassembly // time exceeded") and must be ignored. // // Note this is not real packet and so the length and checksum are not // accurate. #[test] fn test_icmp_time_exceeded_fragment_reassembly_ignored() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 20 2c 02 e4 5c 00 00 72 01 2e 04 67 4b 0b 34 c0 a8 01 15 0b 01 1c 38 00 00 00 00 45 00 8c 05 85 4e 20 00 30 11 ab d6 c0 a8 01 15 67 4b 0b 34 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(1) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); Ok(()) } // This IPv4/ICMP `TimeExceeded` packet has an UDP Original Datagram // with a bogus length (claimed 2040 vs actual 56). // // This is a test to ensure that the UDP checksum validation is working for // packets which are larger than the maximum payload size. This can occur // as unrelated ICMP packets are delivered to our socket and the filtering // occurs later on in the strategy module. // // The packet is not ignored and the UDP Original Datagram is parsed but // notice the expected UDP checksum does not match the actual checksum as // the calculation relies on the claimed payload length, which we restrict // to the maximum packet size we can send. #[test] fn test_recv_icmp_probe_udp_wrong_payload_size() -> anyhow::Result<()> { let expected_read_buf = hex_literal::hex!( " 45 c0 00 70 0e c8 00 00 40 01 e7 9e c0 a8 01 01 c0 a8 01 15 0b 00 12 98 00 00 00 00 45 00 00 54 90 69 00 00 01 11 0b ea c0 a8 01 15 8e fa cc 8e 7c 55 81 06 08 00 e4 cb 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let mut mocket = MockSocket::new(); mocket .expect_read() .times(1) .returning(mocket_read!(expected_read_buf)); let ipv4 = Ipv4 { protocol: Protocol::Udp, src_addr: Ipv4Addr::from_str("192.168.1.21")?, dest_addr: Ipv4Addr::from_str("9.9.9.9")?, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Udp(UdpProtocolResponse { identifier, dest_addr, src_port, dest_port, tos, expected_udp_checksum, actual_udp_checksum, payload_len, has_magic, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(IpAddr::V4(Ipv4Addr::from_str("192.168.1.1")?), addr); assert_eq!(36969, identifier); assert_eq!( IpAddr::V4(Ipv4Addr::from_str("142.250.204.142")?), dest_addr ); assert_eq!(31829, src_port); assert_eq!(33030, dest_port); assert_eq!(Some(TypeOfService(0)), tos); assert_eq!(9963, expected_udp_checksum); assert_eq!(58571, actual_udp_checksum); assert_eq!(2040, payload_len); assert!(!has_magic); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } fn make_icmp_probe() -> Probe { Probe::new( Sequence(33434), TraceId(1234), Port(0), Port(0), TimeToLive(10), RoundId(0), SystemTime::now(), Flags::empty(), ) } fn make_udp_probe(src_port: u16, dest_port: u16) -> Probe { Probe::new( Sequence(33434), TraceId(1234), Port(src_port), Port(dest_port), TimeToLive(10), RoundId(0), SystemTime::now(), Flags::empty(), ) } } ================================================ FILE: crates/trippy-core/src/net/ipv6.rs ================================================ use crate::config::IcmpExtensionParseMode; use crate::error::{Error, ErrorKind, Result}; use crate::net::channel::MAX_PACKET_SIZE; use crate::net::common::ErrorMapper; use crate::net::socket::{Socket, SocketError}; use crate::probe::{ Extensions, IcmpPacketCode, IcmpProtocolResponse, Probe, ProtocolResponse, Response, ResponseData, TcpProtocolResponse, UdpProtocolResponse, }; use crate::types::{PacketSize, PayloadPattern, Sequence, TraceId}; use crate::{Flags, Port, PrivilegeMode, Protocol, TypeOfService}; use std::io; use std::net::{IpAddr, Ipv6Addr, SocketAddr}; use std::time::SystemTime; use tracing::instrument; use trippy_packet::IpProtocol; use trippy_packet::checksum::{icmp_ipv6_checksum, udp_ipv6_checksum}; use trippy_packet::icmpv6::destination_unreachable::DestinationUnreachablePacket; use trippy_packet::icmpv6::echo_reply::EchoReplyPacket; use trippy_packet::icmpv6::echo_request::EchoRequestPacket; use trippy_packet::icmpv6::time_exceeded::TimeExceededPacket; use trippy_packet::icmpv6::{IcmpCode, IcmpPacket, IcmpTimeExceededCode, IcmpType}; use trippy_packet::ipv6::Ipv6Packet; use trippy_packet::tcp::TcpPacket; use trippy_packet::udp::UdpPacket; /// The maximum size of UDP packet we allow. const MAX_UDP_PACKET_BUF: usize = MAX_PACKET_SIZE - Ipv6Packet::minimum_packet_size(); /// The maximum size of UDP payload we allow. const MAX_UDP_PAYLOAD_BUF: usize = MAX_UDP_PACKET_BUF - UdpPacket::minimum_packet_size(); /// The maximum size of UDP packet we allow. const MAX_ICMP_PACKET_BUF: usize = MAX_PACKET_SIZE - Ipv6Packet::minimum_packet_size(); /// The maximum size of ICMP payload we allow. const MAX_ICMP_PAYLOAD_BUF: usize = MAX_ICMP_PACKET_BUF - IcmpPacket::minimum_packet_size(); /// The minimum size of ICMP packets we allow. const MIN_PACKET_SIZE_ICMP: usize = Ipv6Packet::minimum_packet_size() + IcmpPacket::minimum_packet_size(); /// The minimum size of UDP packets we allow. const MIN_PACKET_SIZE_UDP: usize = Ipv6Packet::minimum_packet_size() + UdpPacket::minimum_packet_size(); /// Magic prefix for IPv6/UDP/Dublin payloads. const MAGIC: &[u8] = b"trippy"; /// IPv6 configuration. #[derive(Debug)] pub struct Ipv6 { pub src_addr: Ipv6Addr, pub dest_addr: Ipv6Addr, pub packet_size: PacketSize, pub payload_pattern: PayloadPattern, pub privilege_mode: PrivilegeMode, pub tos: TypeOfService, pub protocol: Protocol, pub icmp_extension_mode: IcmpExtensionParseMode, pub initial_sequence: Sequence, } impl Default for Ipv6 { fn default() -> Self { Self { src_addr: Ipv6Addr::UNSPECIFIED, dest_addr: Ipv6Addr::UNSPECIFIED, packet_size: PacketSize(0), payload_pattern: PayloadPattern(0), privilege_mode: PrivilegeMode::Privileged, tos: TypeOfService(0), protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, initial_sequence: Sequence(0), } } } impl Ipv6 { /// Dispatch an ICMP probe. #[instrument(skip(self, icmp_send_socket), level = "trace")] pub fn dispatch_icmp_probe( &self, icmp_send_socket: &mut S, probe: &Probe, ) -> Result<()> { let mut icmp_buf = [0_u8; MAX_ICMP_PACKET_BUF]; let packet_size = usize::from(self.packet_size.0); if !(MIN_PACKET_SIZE_ICMP..=MAX_PACKET_SIZE).contains(&packet_size) { return Err(Error::InvalidPacketSize(packet_size)); } let echo_request = self.make_echo_request_icmp_packet( &mut icmp_buf, probe.identifier, probe.sequence, icmp_payload_size(packet_size), )?; icmp_send_socket.set_unicast_hops_v6(probe.ttl.0)?; icmp_send_socket.set_tclass_v6(u32::from(self.tos.0))?; let remote_addr = SocketAddr::new(IpAddr::V6(self.dest_addr), 0); icmp_send_socket.send_to(echo_request.packet(), remote_addr)?; Ok(()) } /// Dispatch a UDP probe. #[instrument(skip(self, raw_send_socket), level = "trace")] pub fn dispatch_udp_probe( &self, raw_send_socket: &mut S, probe: &Probe, ) -> Result<()> { let packet_size = usize::from(self.packet_size.0); if !(MIN_PACKET_SIZE_UDP..=MAX_PACKET_SIZE).contains(&packet_size) { return Err(Error::InvalidPacketSize(packet_size)); } let payload_size = udp_payload_size(packet_size); let payload = &[self.payload_pattern.0; MAX_UDP_PAYLOAD_BUF][0..payload_size]; match self.privilege_mode { PrivilegeMode::Privileged => { self.dispatch_udp_probe_raw(raw_send_socket, probe, payload) } PrivilegeMode::Unprivileged => self.dispatch_udp_probe_non_raw::(probe, payload), } } #[instrument(skip(self, udp_send_socket), level = "trace")] fn dispatch_udp_probe_raw( &self, udp_send_socket: &mut S, probe: &Probe, payload: &[u8], ) -> Result<()> { let mut udp_buf = [0_u8; MAX_UDP_PACKET_BUF]; let mut dublin_payload = [self.payload_pattern.0; MAX_UDP_PAYLOAD_BUF]; let payload_paris = probe.sequence.0.to_be_bytes(); let payload = if probe.flags.contains(Flags::PARIS_CHECKSUM) { payload_paris.as_slice() } else if probe.flags.contains(Flags::DUBLIN_IPV6_PAYLOAD_LENGTH) { let payload_len = probe.sequence.0 - self.initial_sequence.0; dublin_payload[..MAGIC.len()].copy_from_slice(MAGIC); &dublin_payload[..usize::from(payload_len) + MAGIC.len()] } else { payload }; let mut udp = self.make_udp_packet(&mut udp_buf, probe.src_port.0, probe.dest_port.0, payload)?; if probe.flags.contains(Flags::PARIS_CHECKSUM) { let checksum = udp.get_checksum().to_be_bytes(); let payload = u16::from_be_bytes(core::array::from_fn(|i| udp.payload()[i])); udp.set_checksum(payload); udp.set_payload(&checksum); } udp_send_socket.set_unicast_hops_v6(probe.ttl.0)?; udp_send_socket.set_tclass_v6(u32::from(self.tos.0))?; // Note that we set the port to be 0 in the remote `SocketAddr` as the target port is // encoded in the `UDP` packet. If we (redundantly) set the target port here then // the `send_to` will fail with `EINVAL`. let remote_addr = SocketAddr::new(IpAddr::V6(self.dest_addr), 0); udp_send_socket.send_to(udp.packet(), remote_addr)?; Ok(()) } #[instrument(skip(self), level = "trace")] fn dispatch_udp_probe_non_raw(&self, probe: &Probe, payload: &[u8]) -> Result<()> { let local_addr = SocketAddr::new(IpAddr::V6(self.src_addr), probe.src_port.0); let remote_addr = SocketAddr::new(IpAddr::V6(self.dest_addr), probe.dest_port.0); let mut socket = S::new_udp_send_socket_ipv6(false)?; socket .bind(local_addr) .map_err(Error::IoError) .or_else(ErrorMapper::in_progress) .map_err(|err| ErrorMapper::addr_in_use(err, local_addr))?; socket.set_unicast_hops_v6(probe.ttl.0)?; socket.set_tclass_v6(u32::from(self.tos.0))?; socket.send_to(payload, remote_addr)?; Ok(()) } /// Dispatch a TCP probe. #[instrument(skip(self), level = "trace")] pub fn dispatch_tcp_probe(&self, probe: &Probe) -> Result { let mut socket = S::new_stream_socket_ipv6()?; let local_addr = SocketAddr::new(IpAddr::V6(self.src_addr), probe.src_port.0); socket .bind(local_addr) .map_err(Error::IoError) .or_else(ErrorMapper::in_progress) .map_err(|err| ErrorMapper::addr_in_use(err, local_addr))?; socket.set_unicast_hops_v6(probe.ttl.0)?; socket.set_tclass_v6(u32::from(self.tos.0))?; let remote_addr = SocketAddr::new(IpAddr::V6(self.dest_addr), probe.dest_port.0); socket .connect(remote_addr) .map_err(Error::IoError) .or_else(ErrorMapper::in_progress) .map_err(|err| ErrorMapper::addr_in_use(err, remote_addr))?; Ok(socket) } /// Receive an ICMP probe. #[instrument(skip(self, recv_socket), level = "trace")] pub fn recv_icmp_probe(&self, recv_socket: &mut S) -> Result> { let mut buf = [0_u8; MAX_PACKET_SIZE]; match recv_socket.recv_from(&mut buf) { Ok((bytes_read, addr)) => { let icmp_v6 = IcmpPacket::new_view(&buf[..bytes_read])?; let src_addr = match addr.as_ref().ok_or(Error::MissingAddr)? { SocketAddr::V6(addr) => addr.ip(), SocketAddr::V4(_) => panic!(), }; Ok(self.extract_probe_resp(&icmp_v6, *src_addr)?) } Err(err) => match err.kind() { ErrorKind::Std(io::ErrorKind::WouldBlock) => Ok(None), _ => Err(Error::IoError(err)), }, } } /// Receive a TCP probe. #[instrument(skip(self, tcp_socket), level = "trace")] pub fn recv_tcp_socket( &self, tcp_socket: &mut S, src_port: Port, dest_port: Port, ) -> Result> { let proto_resp = ProtocolResponse::Tcp(TcpProtocolResponse::new( IpAddr::V6(self.dest_addr), src_port.0, dest_port.0, None, )); match tcp_socket.take_error()? { None => { let addr = tcp_socket.peer_addr()?.ok_or(Error::MissingAddr)?.ip(); tcp_socket.shutdown()?; return Ok(Some(Response::TcpReply(ResponseData::new( SystemTime::now(), addr, proto_resp, )))); } Some(err) => match err { SocketError::ConnectionRefused => { return Ok(Some(Response::TcpRefused(ResponseData::new( SystemTime::now(), IpAddr::V6(self.dest_addr), proto_resp, )))); } SocketError::HostUnreachable => { let error_addr = tcp_socket.icmp_error_info()?; return Ok(Some(Response::TimeExceeded( ResponseData::new(SystemTime::now(), error_addr, proto_resp), IcmpPacketCode(1), None, ))); } SocketError::Other(_) => {} }, } Ok(None) } fn extract_probe_resp( &self, icmp_v6: &IcmpPacket<'_>, src: Ipv6Addr, ) -> Result> { let recv = SystemTime::now(); let ip = IpAddr::V6(src); let icmp_type = icmp_v6.get_icmp_type(); let icmp_code = icmp_v6.get_icmp_code(); Ok(match icmp_type { IcmpType::TimeExceeded => { if IcmpTimeExceededCode::from(icmp_code) == IcmpTimeExceededCode::TtlExpired { let packet = TimeExceededPacket::new_view(icmp_v6.packet())?; let (nested_ipv6, extension) = match self.icmp_extension_mode { IcmpExtensionParseMode::Enabled => { let ipv6 = Ipv6Packet::new_view(packet.payload())?; let ext = packet.extension().map(Extensions::try_from).transpose()?; (ipv6, ext) } IcmpExtensionParseMode::Disabled => { let ipv6 = Ipv6Packet::new_view(packet.payload_raw())?; (ipv6, None) } }; self.extract_probe_proto_resp(&nested_ipv6)? .map(|proto_resp| { Response::TimeExceeded( ResponseData::new(recv, ip, proto_resp), IcmpPacketCode(icmp_code.0), extension, ) }) } else { None } } IcmpType::DestinationUnreachable => { let packet = DestinationUnreachablePacket::new_view(icmp_v6.packet())?; let nested_ipv6 = Ipv6Packet::new_view(packet.payload())?; let extension = match self.icmp_extension_mode { IcmpExtensionParseMode::Enabled => { packet.extension().map(Extensions::try_from).transpose()? } IcmpExtensionParseMode::Disabled => None, }; self.extract_probe_proto_resp(&nested_ipv6)? .map(|proto_resp| { Response::DestinationUnreachable( ResponseData::new(recv, ip, proto_resp), IcmpPacketCode(icmp_code.0), extension, ) }) } IcmpType::EchoReply => match self.protocol { Protocol::Icmp => { let packet = EchoReplyPacket::new_view(icmp_v6.packet())?; let id = packet.get_identifier(); let seq = packet.get_sequence(); let proto_resp = ProtocolResponse::Icmp(IcmpProtocolResponse::new(id, seq, None)); Some(Response::EchoReply( ResponseData::new(recv, ip, proto_resp), IcmpPacketCode(icmp_code.0), )) } Protocol::Udp | Protocol::Tcp => None, }, _ => None, }) } fn extract_probe_proto_resp(&self, ipv6: &Ipv6Packet<'_>) -> Result> { Ok(match (self.protocol, ipv6.get_next_header()) { (Protocol::Icmp, IpProtocol::IcmpV6) => { let (identifier, sequence) = extract_echo_request(ipv6)?; Some(ProtocolResponse::Icmp(IcmpProtocolResponse::new( identifier, sequence, Some(TypeOfService(ipv6.get_traffic_class())), ))) } (Protocol::Udp, IpProtocol::Udp) => { let (src_port, dest_port, actual_checksum, udp_payload_len) = extract_udp_packet(ipv6)?; let has_magic = udp_payload_has_magic_prefix(ipv6)?; let payload_len = if has_magic { udp_payload_len - MAGIC.len() as u16 } else { udp_payload_len }; Some(ProtocolResponse::Udp(UdpProtocolResponse::new( 0, IpAddr::V6(ipv6.get_destination_address()), src_port, dest_port, Some(TypeOfService(ipv6.get_traffic_class())), actual_checksum, actual_checksum, payload_len, has_magic, ))) } (Protocol::Tcp, IpProtocol::Tcp) => { let (src_port, dest_port) = extract_tcp_packet(ipv6)?; Some(ProtocolResponse::Tcp(TcpProtocolResponse::new( IpAddr::V6(ipv6.get_destination_address()), src_port, dest_port, Some(TypeOfService(ipv6.get_traffic_class())), ))) } _ => None, }) } /// Create a `UdpPacket` fn make_udp_packet<'a>( &self, udp_buf: &'a mut [u8], src_port: u16, dest_port: u16, payload: &'_ [u8], ) -> Result> { let udp_packet_size = UdpPacket::minimum_packet_size() + payload.len(); let mut udp = UdpPacket::new(&mut udp_buf[..udp_packet_size])?; udp.set_source(src_port); udp.set_destination(dest_port); udp.set_length(udp_packet_size as u16); udp.set_payload(payload); udp.set_checksum(udp_ipv6_checksum( udp.packet(), self.src_addr, self.dest_addr, )); Ok(udp) } /// Create an ICMP `EchoRequest` packet. fn make_echo_request_icmp_packet<'a>( &self, icmp_buf: &'a mut [u8], identifier: TraceId, sequence: Sequence, payload_size: usize, ) -> Result> { let payload_buf = [self.payload_pattern.0; MAX_ICMP_PAYLOAD_BUF]; let packet_size = IcmpPacket::minimum_packet_size() + payload_size; let mut icmp = EchoRequestPacket::new(&mut icmp_buf[..packet_size])?; icmp.set_icmp_type(IcmpType::EchoRequest); icmp.set_icmp_code(IcmpCode(0)); icmp.set_identifier(identifier.0); icmp.set_payload(&payload_buf[..payload_size]); icmp.set_sequence(sequence.0); icmp.set_checksum(icmp_ipv6_checksum( icmp.packet(), self.src_addr, self.dest_addr, )); Ok(icmp) } } const fn icmp_payload_size(packet_size: usize) -> usize { let ip_header_size = Ipv6Packet::minimum_packet_size(); let icmp_header_size = IcmpPacket::minimum_packet_size(); packet_size - icmp_header_size - ip_header_size } const fn udp_payload_size(packet_size: usize) -> usize { let ip_header_size = Ipv6Packet::minimum_packet_size(); let udp_header_size = UdpPacket::minimum_packet_size(); packet_size - udp_header_size - ip_header_size } fn extract_echo_request(ipv6: &Ipv6Packet<'_>) -> Result<(u16, u16)> { let echo_request_packet = EchoRequestPacket::new_view(ipv6.payload())?; Ok(( echo_request_packet.get_identifier(), echo_request_packet.get_sequence(), )) } fn extract_udp_packet(ipv6: &Ipv6Packet<'_>) -> Result<(u16, u16, u16, u16)> { let udp_packet = UdpPacket::new_view(ipv6.payload())?; Ok(( udp_packet.get_source(), udp_packet.get_destination(), udp_packet.get_checksum(), udp_packet.get_length() - UdpPacket::minimum_packet_size() as u16, )) } /// From [rfc4443] (section 2.4, point c): /// /// "Every `ICMPv6` error message (type < 128) MUST include as much of /// the IPv6 offending (invoking) packet (the packet that caused the /// error) as possible without making the error message packet exceed /// the minimum IPv6 MTU" /// /// From [rfc2460] (section 5): /// /// "IPv6 requires that every link in the internet have an MTU of 1280 /// octets or greater. On any link that cannot convey a 1280-octet /// packet in one piece, link-specific fragmentation and reassembly must /// be provided at a layer below IPv6." /// /// The maximum packet size we allow is 1024, and so we can safely assume that the originating IPv6 /// packet being extracted will be at least as large as the minimum IPv6 packet size. /// /// [rfc4443]: https://datatracker.ietf.org/doc/html/rfc4443#section-2.4 /// [rfc2460]: https://datatracker.ietf.org/doc/html/rfc2460#section-5 fn extract_tcp_packet(ipv6: &Ipv6Packet<'_>) -> Result<(u16, u16)> { let tcp_packet = TcpPacket::new_view(ipv6.payload())?; Ok((tcp_packet.get_source(), tcp_packet.get_destination())) } fn udp_payload_has_magic_prefix(ipv6: &Ipv6Packet<'_>) -> Result { let udp_packet = UdpPacket::new_view(ipv6.payload())?; Ok(udp_packet.payload().starts_with(MAGIC)) } #[cfg(test)] mod tests { use super::*; use crate::error::IoResult; use crate::mocket_recv_from; use crate::net::socket::MockSocket; use crate::{Flags, Port, RoundId, TimeToLive}; use mockall::predicate; use std::str::FromStr; use std::sync::Mutex; static MTX: Mutex<()> = Mutex::new(()); // Test dispatching an IPv6/ICMP probe. #[test] fn test_dispatch_icmp_probe_no_payload() -> anyhow::Result<()> { let probe = make_icmp_probe(); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let packet_size = PacketSize(48); let payload_pattern = PayloadPattern(0x00); let expected_send_to_buf = hex_literal::hex!("80 00 75 a2 04 d2 82 9a"); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); mocket .expect_set_unicast_hops_v6() .times(1) .with(predicate::eq(10)) .returning(|_| Ok(())); mocket .expect_set_tclass_v6() .times(1) .with(predicate::eq(0)) .returning(|_| Ok(())); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, ..Default::default() }; ipv6.dispatch_icmp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_icmp_probe_with_payload() -> anyhow::Result<()> { let probe = make_icmp_probe(); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let packet_size = PacketSize(68); let payload_pattern = PayloadPattern(0xff); let expected_send_to_buf = hex_literal::hex!( " 80 00 75 8e 04 d2 82 9a ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); mocket .expect_set_unicast_hops_v6() .times(1) .with(predicate::eq(10)) .returning(|_| Ok(())); mocket .expect_set_tclass_v6() .times(1) .with(predicate::eq(0)) .returning(|_| Ok(())); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, ..Default::default() }; ipv6.dispatch_icmp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_icmp_probe_invalid_packet_size_low() -> anyhow::Result<()> { let probe = make_icmp_probe(); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let packet_size = PacketSize(47); let payload_pattern = PayloadPattern(0x00); let mut mocket = MockSocket::new(); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, ..Default::default() }; let err = ipv6.dispatch_icmp_probe(&mut mocket, &probe).unwrap_err(); assert!(matches!(err, Error::InvalidPacketSize(_))); Ok(()) } #[test] fn test_dispatch_icmp_probe_invalid_packet_size_high() -> anyhow::Result<()> { let probe = make_icmp_probe(); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let packet_size = PacketSize(1025); let payload_pattern = PayloadPattern(0x00); let mut mocket = MockSocket::new(); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, ..Default::default() }; let err = ipv6.dispatch_icmp_probe(&mut mocket, &probe).unwrap_err(); assert!(matches!(err, Error::InvalidPacketSize(_))); Ok(()) } #[test] fn test_dispatch_udp_probe_classic_privileged_no_payload() -> anyhow::Result<()> { let probe = make_udp_probe(123, 456); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(48); let payload_pattern = PayloadPattern(0x00); let initial_sequence = Sequence(33434); let expected_send_to_buf = hex_literal::hex!("00 7b 01 c8 00 08 7a ed"); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); mocket .expect_set_unicast_hops_v6() .times(1) .with(predicate::eq(10)) .returning(|_| Ok(())); mocket .expect_set_tclass_v6() .times(1) .with(predicate::eq(0)) .returning(|_| Ok(())); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, privilege_mode, initial_sequence, ..Default::default() }; ipv6.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_classic_privileged_with_payload() -> anyhow::Result<()> { let probe = make_udp_probe(123, 456); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(56); let payload_pattern = PayloadPattern(0xaa); let initial_sequence = Sequence(33434); let expected_send_to_buf = hex_literal::hex!( " 00 7b 01 c8 00 10 d0 32 aa aa aa aa aa aa aa aa " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); mocket .expect_set_unicast_hops_v6() .times(1) .with(predicate::eq(10)) .returning(|_| Ok(())); mocket .expect_set_tclass_v6() .times(1) .with(predicate::eq(0)) .returning(|_| Ok(())); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, privilege_mode, initial_sequence, ..Default::default() }; ipv6.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_paris_privileged() -> anyhow::Result<()> { let probe = Probe { flags: Flags::PARIS_CHECKSUM, ..make_udp_probe(123, 456) }; let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let privilege_mode = PrivilegeMode::Privileged; // packet size and payload pattern are ignored for paris mode as a // fixed two byte payload is used to hold the sequence let packet_size = PacketSize(300); let payload_pattern = PayloadPattern(0xaa); let initial_sequence = Sequence(33434); let expected_send_to_buf = hex_literal::hex!( " 00 7b 01 c8 00 0a 82 9a f8 4e " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); mocket .expect_set_unicast_hops_v6() .times(1) .with(predicate::eq(10)) .returning(|_| Ok(())); mocket .expect_set_tclass_v6() .times(1) .with(predicate::eq(0)) .returning(|_| Ok(())); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, privilege_mode, initial_sequence, ..Default::default() }; ipv6.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } // Here we send probe 33007 (the 8th probe when starting from 33434) and // so the payload will be 13 octets in length (7 + 6 for the magic prefix // "trippy"). #[test] fn test_dispatch_udp_probe_dublin_privileged() -> anyhow::Result<()> { let probe = Probe { flags: Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, sequence: Sequence(33441), identifier: TraceId(33441), ..make_udp_probe(123, 456) }; let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let privilege_mode = PrivilegeMode::Privileged; // packet size and payload pattern are ignored for ipv6/udp/dublin mode. let packet_size = PacketSize(300); let payload_pattern = PayloadPattern(0xaa); let initial_sequence = Sequence(33434); let expected_send_to_buf = hex_literal::hex!( " 00 7b 01 c8 00 15 82 76 74 72 69 70 70 79 aa aa aa aa aa aa aa " ); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0); let mut mocket = MockSocket::new(); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); mocket .expect_set_unicast_hops_v6() .times(1) .with(predicate::eq(10)) .returning(|_| Ok(())); mocket .expect_set_tclass_v6() .times(1) .with(predicate::eq(0)) .returning(|_| Ok(())); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, privilege_mode, initial_sequence, ..Default::default() }; ipv6.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_classic_unprivileged_no_payload() -> anyhow::Result<()> { let _m = MTX.lock(); let probe = make_udp_probe(123, 456); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let privilege_mode = PrivilegeMode::Unprivileged; let packet_size = PacketSize(48); let payload_pattern = PayloadPattern(0x00); let initial_sequence = Sequence(33434); let expected_send_to_buf = hex_literal::hex!(""); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456); let expected_bind_addr = SocketAddr::new(IpAddr::V6(src_addr), 123); let expected_set_unicast_hops_v6 = 10; let mut mocket = MockSocket::new(); let ctx = MockSocket::new_udp_send_socket_ipv6_context(); ctx.expect().with(predicate::eq(false)).returning(move |_| { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|_| Ok(())); mocket .expect_set_unicast_hops_v6() .times(1) .with(predicate::eq(expected_set_unicast_hops_v6)) .returning(|_| Ok(())); mocket .expect_set_tclass_v6() .times(1) .with(predicate::eq(0)) .returning(|_| Ok(())); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); Ok(mocket) }); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, privilege_mode, initial_sequence, ..Default::default() }; ipv6.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_classic_unprivileged_with_payload() -> anyhow::Result<()> { let _m = MTX.lock(); let probe = make_udp_probe(123, 456); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let privilege_mode = PrivilegeMode::Unprivileged; let packet_size = PacketSize(56); let payload_pattern = PayloadPattern(0x1f); let initial_sequence = Sequence(33434); let expected_send_to_buf = hex_literal::hex!("1f 1f 1f 1f 1f 1f 1f 1f"); let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456); let expected_bind_addr = SocketAddr::new(IpAddr::V6(src_addr), 123); let expected_set_unicast_hops_v6 = 10; let mut mocket = MockSocket::new(); let ctx = MockSocket::new_udp_send_socket_ipv6_context(); ctx.expect().with(predicate::eq(false)).returning(move |_| { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|_| Ok(())); mocket .expect_set_unicast_hops_v6() .times(1) .with(predicate::eq(expected_set_unicast_hops_v6)) .returning(|_| Ok(())); mocket .expect_set_tclass_v6() .times(1) .with(predicate::eq(0)) .returning(|_| Ok(())); mocket .expect_send_to() .with( predicate::eq(expected_send_to_buf), predicate::eq(expected_send_to_addr), ) .times(1) .returning(|_, _| Ok(())); Ok(mocket) }); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, privilege_mode, initial_sequence, ..Default::default() }; ipv6.dispatch_udp_probe(&mut mocket, &probe)?; Ok(()) } #[test] fn test_dispatch_udp_probe_invalid_packet_size_low() -> anyhow::Result<()> { let probe = make_udp_probe(123, 456); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(47); let payload_pattern = PayloadPattern(0x00); let initial_sequence = Sequence(33434); let mut mocket = MockSocket::new(); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, privilege_mode, initial_sequence, ..Default::default() }; let err = ipv6.dispatch_udp_probe(&mut mocket, &probe).unwrap_err(); assert!(matches!(err, Error::InvalidPacketSize(_))); Ok(()) } #[test] fn test_dispatch_udp_probe_invalid_packet_size_high() -> anyhow::Result<()> { let probe = make_udp_probe(123, 456); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let privilege_mode = PrivilegeMode::Privileged; let packet_size = PacketSize(1025); let payload_pattern = PayloadPattern(0x00); let initial_sequence = Sequence(33434); let mut mocket = MockSocket::new(); let ipv6 = Ipv6 { src_addr, dest_addr, packet_size, payload_pattern, privilege_mode, initial_sequence, ..Default::default() }; let err = ipv6.dispatch_udp_probe(&mut mocket, &probe).unwrap_err(); assert!(matches!(err, Error::InvalidPacketSize(_))); Ok(()) } #[test] fn test_dispatch_tcp_probe() -> anyhow::Result<()> { let _m = MTX.lock(); let probe = make_udp_probe(123, 456); let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a")?; let dest_addr = Ipv6Addr::from_str("2a00:1450:4009:815::200e")?; let expected_bind_addr = SocketAddr::new(IpAddr::V6(src_addr), 123); let expected_set_unicast_hops_v6 = 10; let expected_connect_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456); let ctx = MockSocket::new_stream_socket_ipv6_context(); ctx.expect().returning(move || { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|_| Ok(())); mocket .expect_set_unicast_hops_v6() .times(1) .with(predicate::eq(expected_set_unicast_hops_v6)) .returning(|_| Ok(())); mocket .expect_set_tclass_v6() .times(1) .with(predicate::eq(0)) .returning(|_| Ok(())); mocket .expect_connect() .with(predicate::eq(expected_connect_addr)) .times(1) .returning(|_| Ok(())); Ok(mocket) }); let ipv6 = Ipv6 { src_addr, dest_addr, ..Default::default() }; ipv6.dispatch_tcp_probe::(&probe)?; Ok(()) } #[test] fn test_recv_icmp_probe_echo_reply() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 81 00 52 c0 55 b9 81 26 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(1) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::EchoReply( ResponseData { addr, proto_resp: ProtocolResponse::Icmp(IcmpProtocolResponse { identifier, sequence, tos, }), .. }, icmp_code, ) = resp else { panic!("expected EchoReply") }; assert_eq!(recv_from_addr, addr); assert_eq!(21945, identifier); assert_eq!(33062, sequence); assert_eq!(None, tos); assert_eq!(IcmpPacketCode(0), icmp_code); Ok(()) } #[test] fn test_recv_icmp_probe_time_exceeded_icmp_no_extensions() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 03 00 4e c5 00 00 00 00 60 0f 08 00 00 2c 3a 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 04 4e 42 00 00 00 00 00 00 00 00 00 00 00 81 80 00 53 c6 55 b9 81 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(1) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Icmp(IcmpProtocolResponse { identifier, sequence, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(recv_from_addr, addr); assert_eq!(21945, identifier); assert_eq!(33056, sequence); assert_eq!(Some(TypeOfService(0)), tos); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_destination_unreachable_icmp_no_extensions() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 01 00 ad ba 00 00 00 00 60 06 08 00 00 2c 3a 02 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 14 04 68 00 40 03 0c 02 00 00 00 00 00 00 00 69 80 00 02 62 57 a5 80 ed 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(1) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::DestinationUnreachable( ResponseData { addr, proto_resp: ProtocolResponse::Icmp(IcmpProtocolResponse { identifier, sequence, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected DestinationUnreachable") }; assert_eq!(recv_from_addr, addr); assert_eq!(22437, identifier); assert_eq!(33005, sequence); assert_eq!(Some(TypeOfService(0)), tos); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_time_exceeded_udp_no_extensions() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 03 00 7b a7 00 00 00 00 60 04 04 00 00 2c 11 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 04 4e 42 00 00 00 00 00 00 00 00 00 00 00 81 58 a6 81 05 00 2c d0 f1 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(1) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Udp(UdpProtocolResponse { identifier, dest_addr, src_port, dest_port, tos, expected_udp_checksum, actual_udp_checksum, payload_len, has_magic, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(recv_from_addr, addr); assert_eq!(0, identifier); assert_eq!(IpAddr::V6(Ipv6Addr::from_str("2a04:4e42::81")?), dest_addr); assert_eq!(22694, src_port); assert_eq!(33029, dest_port); assert_eq!(Some(TypeOfService(0)), tos); assert_eq!(53489, expected_udp_checksum); assert_eq!(53489, actual_udp_checksum); assert_eq!(36, payload_len); assert!(!has_magic); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_destination_unreachable_udp_no_extensions() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 01 00 a5 f5 00 00 00 00 60 03 08 00 00 2c 11 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 00 14 50 40 09 08 1f 00 00 00 00 00 00 20 0e 67 6d 81 5e 00 2c 94 12 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(1) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::DestinationUnreachable( ResponseData { addr, proto_resp: ProtocolResponse::Udp(UdpProtocolResponse { identifier, dest_addr, src_port, dest_port, tos, expected_udp_checksum, actual_udp_checksum, payload_len, has_magic, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected DestinationUnreachable") }; assert_eq!(recv_from_addr, addr); assert_eq!(0, identifier); assert_eq!( IpAddr::V6(Ipv6Addr::from_str("2a00:1450:4009:81f::200e")?), dest_addr ); assert_eq!(26477, src_port); assert_eq!(33118, dest_port); assert_eq!(Some(TypeOfService(0)), tos); assert_eq!(37906, expected_udp_checksum); assert_eq!(37906, actual_udp_checksum); assert_eq!(36, payload_len); assert!(!has_magic); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } // Here we receive a `TimeExceeded` in UDP/Dublin mode and so extract the // sequence from the length of the UDP payload, after subtracting the // length of the magic prefix "trippy" (11 - 6 == 5). // // Note we do not know if we are in UDP/Dublin mode when decoding the // packet and so the decision to reject the probe response is left to // the `tracer::Tracer::validate(..)` function. #[test] fn test_recv_icmp_probe_time_exceeded_udp_dublin_with_magic() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 03 00 23 6f 00 00 00 00 60 0e 0e 00 00 13 11 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 00 14 50 40 09 08 20 00 00 00 00 00 00 20 0e 80 e8 13 88 00 13 9a 42 74 72 69 70 70 79 00 00 00 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(1) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Udp(UdpProtocolResponse { identifier, dest_addr, src_port, dest_port, tos, expected_udp_checksum, actual_udp_checksum, payload_len, has_magic, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(recv_from_addr, addr); assert_eq!(0, identifier); assert_eq!( IpAddr::V6(Ipv6Addr::from_str("2a00:1450:4009:820::200e")?), dest_addr ); assert_eq!(33000, src_port); assert_eq!(5000, dest_port); assert_eq!(Some(TypeOfService(0)), tos); assert_eq!(39490, expected_udp_checksum); assert_eq!(39490, actual_udp_checksum); assert_eq!(5, payload_len); assert!(has_magic); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_time_exceeded_tcp_no_extensions() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 03 00 f0 2d 00 00 00 00 68 0b 09 00 00 2c 06 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 00 14 50 40 09 08 15 00 00 00 00 00 00 20 0e 81 0e 00 50 aa c4 08 e6 00 00 00 00 b0 c2 ff ff 6d b4 00 00 02 04 04 c4 01 03 03 06 01 01 08 0a cc f7 44 c9 00 00 00 00 04 02 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(1) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(recv_from_addr, addr); assert_eq!( IpAddr::V6(Ipv6Addr::from_str("2a00:1450:4009:815::200e")?), dest_addr ); assert_eq!(33038, src_port); assert_eq!(80, dest_port); assert_eq!(Some(TypeOfService(128)), tos); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_destination_unreachable_tcp_no_extensions() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 01 00 b1 e9 00 00 00 00 60 04 07 00 00 2c 06 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 00 14 50 40 09 08 21 00 00 00 00 00 00 20 0e 81 24 00 7b 35 d2 32 c6 00 00 00 00 b0 c2 ff ff 71 b2 00 00 02 04 04 c4 01 03 03 06 01 01 08 0a fa 0b 5e 7c 00 00 00 00 04 02 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(1) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Disabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap(); let Response::DestinationUnreachable( ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected DestinationUnreachable") }; assert_eq!(recv_from_addr, addr); assert_eq!( IpAddr::V6(Ipv6Addr::from_str("2a00:1450:4009:821::200e")?), dest_addr ); assert_eq!(33060, src_port); assert_eq!(123, dest_port); assert_eq!(Some(TypeOfService(0)), tos); assert_eq!(IcmpPacketCode(0), icmp_code); assert_eq!(None, extensions); Ok(()) } #[test] fn test_recv_icmp_probe_wrong_icmp_original_datagram_type_ignored() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 03 00 4e c5 00 00 00 00 60 0f 08 00 00 2c 3a 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 04 4e 42 00 00 00 00 00 00 00 00 00 00 00 81 80 00 53 c6 55 b9 81 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(3) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_some()); let ipv6 = Ipv6 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); let ipv6 = Ipv6 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); Ok(()) } #[test] fn test_recv_icmp_probe_wrong_udp_original_datagram_type_ignored() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 03 00 7b a7 00 00 00 00 60 04 04 00 00 2c 11 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 04 4e 42 00 00 00 00 00 00 00 00 00 00 00 81 58 a6 81 05 00 2c d0 f1 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(3) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_some()); let ipv6 = Ipv6 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); let ipv6 = Ipv6 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); Ok(()) } #[test] fn test_recv_icmp_probe_wrong_tcp_original_datagram_type_ignored() -> anyhow::Result<()> { let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?); let expected_recv_from_buf = hex_literal::hex!( " 03 00 f0 2d 00 00 00 00 68 0b 09 00 00 2c 06 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 00 14 50 40 09 08 15 00 00 00 00 00 00 20 0e 81 0e 00 50 aa c4 08 e6 00 00 00 00 b0 c2 ff ff 6d b4 00 00 02 04 04 c4 01 03 03 06 01 01 08 0a cc f7 44 c9 00 00 00 00 04 02 00 00 " ); let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(3) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Tcp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_some()); let ipv6 = Ipv6 { protocol: Protocol::Icmp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); let ipv6 = Ipv6 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); Ok(()) } #[test] fn test_recv_tcp_socket_tcp_reply() -> anyhow::Result<()> { let dest_addr = Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?; let expected_peer_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456); let mut mocket = MockSocket::new(); mocket.expect_take_error().times(1).returning(|| Ok(None)); mocket .expect_peer_addr() .times(1) .returning(move || Ok(Some(expected_peer_addr))); mocket.expect_shutdown().times(1).returning(|| Ok(())); let ipv6 = Ipv6 { dest_addr, ..Default::default() }; let resp = ipv6 .recv_tcp_socket(&mut mocket, Port(33434), Port(456))? .unwrap(); let Response::TcpReply(ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }) = resp else { panic!("expected TcpReply") }; assert_eq!(dest_addr, addr); assert_eq!(33434, src_port); assert_eq!(456, dest_port); assert_eq!(None, tos); Ok(()) } #[test] fn test_recv_tcp_socket_tcp_refused() -> anyhow::Result<()> { let dest_addr = Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?; let mut mocket = MockSocket::new(); mocket .expect_take_error() .times(1) .returning(|| Ok(Some(SocketError::ConnectionRefused))); let ipv6 = Ipv6 { dest_addr, ..Default::default() }; let resp = ipv6 .recv_tcp_socket(&mut mocket, Port(33434), Port(80))? .unwrap(); let Response::TcpRefused(ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }) = resp else { panic!("expected TcpRefused") }; assert_eq!(dest_addr, addr); assert_eq!(33434, src_port); assert_eq!(80, dest_port); assert_eq!(None, tos); Ok(()) } #[test] fn test_recv_tcp_socket_tcp_host_unreachable() -> anyhow::Result<()> { let dest_addr = Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?; let mut mocket = MockSocket::new(); mocket .expect_take_error() .times(1) .returning(|| Ok(Some(SocketError::HostUnreachable))); mocket .expect_icmp_error_info() .times(1) .returning(move || Ok(IpAddr::V6(dest_addr))); let ipv6 = Ipv6 { dest_addr, ..Default::default() }; let resp = ipv6 .recv_tcp_socket(&mut mocket, Port(33434), Port(80))? .unwrap(); let Response::TimeExceeded( ResponseData { addr, proto_resp: ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, tos, }), .. }, icmp_code, extensions, ) = resp else { panic!("expected TimeExceeded") }; assert_eq!(dest_addr, addr); assert_eq!(33434, src_port); assert_eq!(80, dest_port); assert_eq!(None, tos); assert_eq!(IcmpPacketCode(1), icmp_code); assert_eq!(None, extensions); Ok(()) } // This ICMPv6 packet has code 1 ("Fragment reassembly time exceeded") // and must be ignored. // // Note this is not real packet and so the length and checksum are not // accurate. #[test] fn test_icmp_time_exceeded_fragment_reassembly_ignored() -> anyhow::Result<()> { let expected_recv_from_buf = hex_literal::hex!( " 03 01 da 90 00 00 00 00 60 0f 02 00 00 2c 11 01 fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a 2a 00 14 50 40 09 08 15 00 00 00 00 00 00 20 0e 95 ce 81 24 00 2c 65 f5 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let expected_recv_from_addr = SocketAddr::new( IpAddr::V6(Ipv6Addr::from_str("2604:a880:ffff:6:1::41c")?), 0, ); let mut mocket = MockSocket::new(); mocket .expect_recv_from() .times(1) .returning(mocket_recv_from!( expected_recv_from_buf, expected_recv_from_addr )); let ipv6 = Ipv6 { protocol: Protocol::Udp, icmp_extension_mode: IcmpExtensionParseMode::Enabled, ..Default::default() }; let resp = ipv6.recv_icmp_probe(&mut mocket)?; assert!(resp.is_none()); Ok(()) } fn make_icmp_probe() -> Probe { Probe::new( Sequence(33434), TraceId(1234), Port(0), Port(0), TimeToLive(10), RoundId(0), SystemTime::now(), Flags::empty(), ) } fn make_udp_probe(src_port: u16, dest_port: u16) -> Probe { Probe::new( Sequence(33434), TraceId(1234), Port(src_port), Port(dest_port), TimeToLive(10), RoundId(0), SystemTime::now(), Flags::empty(), ) } } ================================================ FILE: crates/trippy-core/src/net/platform/byte_order.rs ================================================ use crate::error::Result; use crate::net::platform::{Platform, PlatformImpl}; use std::net::IpAddr; /// The byte order to encode the `total_length`, `flags` and `fragment_offset` fields of the IPv4 /// header. /// /// To quote directly from the `mtr` source code (from `check_length_order` in `probe_unix.c`): /// /// "Nearly all fields in the IP header should be encoded in network byte /// order prior to passing to `send()`. However, the required byte order of /// the length field of the IP header is inconsistent between operating /// systems and operating system versions. FreeBSD 11 requires the length /// field in network byte order, but some older versions of FreeBSD /// require host byte order. OS X requires the length field in host /// byte order. Linux will accept either byte order." #[derive(Debug, Copy, Clone)] pub enum Ipv4ByteOrder { #[cfg(all(unix, not(target_os = "linux"), not(target_os = "windows")))] Host, Network, } impl Ipv4ByteOrder { /// Discover the required byte ordering for the IPv4 header fields `total_length`, `flags` and /// `fragment_offset`. /// /// This is achieved by creating a raw socket and attempting to send an `IPv4` packet to /// localhost with the `total_length` set in either host byte order or network byte order. /// The OS will return an `InvalidInput` error if the buffer provided is smaller than the /// `total_length` indicated, which will be the case when the byte order is set incorrectly. /// /// This is a little confusing as `Ipv4Packet::set_total_length` method will _always_ convert /// from host byte order to network byte order (which will be a no-op on big-endian system) /// and so to test the host byte order case we must try both the normal and the swapped byte /// order. /// /// For example, for a packet of length 4660 bytes (dec): /// /// For a little-endian architecture: /// /// Try Host (LE) Wire (BE) Order (if succeeds) /// normal 34 12 12 34 `Ipv4ByteOrder::Network` /// swapped 12 34 34 12 `Ipv4ByteOrder::Host` /// /// For a big-endian architecture: /// /// Try Host (BE) Wire (BE) Order (if succeeds) /// normal 12 34 12 34 `Ipv4ByteOrder::Host` /// swapped 34 12 34 12 `Ipv4ByteOrder::Network` pub fn for_address(addr: IpAddr) -> Result { PlatformImpl::byte_order_for_address(addr) } /// Adjust the IPv4 `total_length` header. #[must_use] pub const fn adjust_length(self, ipv4_total_length: u16) -> u16 { match self { #[cfg(all(unix, not(target_os = "linux"), not(target_os = "windows")))] Self::Host => ipv4_total_length.swap_bytes(), Self::Network => ipv4_total_length, } } } ================================================ FILE: crates/trippy-core/src/net/platform/unix.rs ================================================ use crate::error::Result; use crate::net::platform::{Ipv4ByteOrder, Platform}; use std::net::IpAddr; pub struct PlatformImpl; impl Platform for PlatformImpl { fn byte_order_for_address(addr: IpAddr) -> Result { address::for_address(addr) } fn lookup_interface_addr(addr: IpAddr, name: &str) -> Result { address::lookup_interface_addr(addr, name) } fn discover_local_addr(target_addr: IpAddr, port: u16) -> Result { address::discover_local_addr(target_addr, port) } } mod address { use crate::error::{Error, Result}; use crate::net::SocketImpl; use crate::net::platform::Ipv4ByteOrder; use crate::net::socket::Socket; use nix::sys::socket::{AddressFamily, SockaddrLike}; use std::net::{IpAddr, SocketAddr}; use tracing::instrument; #[cfg(not(target_os = "linux"))] use std::net::Ipv4Addr; /// The size of the test packet to use for discovering the `total_length` byte order. #[cfg(not(target_os = "linux"))] const TEST_PACKET_LENGTH: u16 = 256; /// Discover the required byte ordering for the IPv4 header fields `total_length`, `flags` and /// `fragment_offset`. /// /// Linux accepts either network byte order or host byte order for the `total_length` field, and /// so we skip the check and return network byte order unconditionally. #[cfg(target_os = "linux")] #[expect(clippy::unnecessary_wraps)] pub const fn for_address(_src_addr: IpAddr) -> Result { Ok(Ipv4ByteOrder::Network) } #[cfg(not(target_os = "linux"))] #[instrument(ret, level = "trace")] pub fn for_address(addr: IpAddr) -> Result { let addr = match addr { IpAddr::V4(addr) => addr, IpAddr::V6(_) => return Ok(Ipv4ByteOrder::Network), }; match test_send_local_ip4_packet(addr, TEST_PACKET_LENGTH) { Ok(()) => Ok(Ipv4ByteOrder::Network), Err(Error::IoError(io)) if io.kind() == crate::error::ErrorKind::Std(std::io::ErrorKind::InvalidInput) => { match test_send_local_ip4_packet(addr, TEST_PACKET_LENGTH.swap_bytes()) { Ok(()) => Ok(Ipv4ByteOrder::Host), Err(err) => Err(err), } } Err(err) => Err(err), } } /// Attempt to send an `ICMP` packet to a local address. /// /// The packet is actually of length `256` bytes, but we set the `total_length` based on the /// input provided to test if the OS rejects the attempt during the call to `send_to`. /// /// Note that this implementation will try to create an `IPPROTO_ICMP` socket and if that fails /// it will fall back to creating an `IPPROTO_RAW` socket. #[cfg(not(target_os = "linux"))] #[instrument(ret, level = "trace")] fn test_send_local_ip4_packet(src_addr: Ipv4Addr, total_length: u16) -> Result<()> { use socket2::Protocol; let mut icmp_buf = [0_u8; trippy_packet::icmpv4::IcmpPacket::minimum_packet_size()]; let mut icmp = trippy_packet::icmpv4::echo_request::EchoRequestPacket::new(&mut icmp_buf)?; icmp.set_icmp_type(trippy_packet::icmpv4::IcmpType::EchoRequest); icmp.set_icmp_code(trippy_packet::icmpv4::IcmpCode(0)); icmp.set_identifier(0); icmp.set_sequence(0); icmp.set_checksum(trippy_packet::checksum::icmp_ipv4_checksum(icmp.packet())); let mut ipv4_buf = [0_u8; TEST_PACKET_LENGTH as usize]; let mut ipv4 = trippy_packet::ipv4::Ipv4Packet::new(&mut ipv4_buf)?; ipv4.set_version(4); ipv4.set_header_length(5); ipv4.set_protocol(trippy_packet::IpProtocol::Icmp); ipv4.set_ttl(255); ipv4.set_source(src_addr); ipv4.set_destination(Ipv4Addr::LOCALHOST); ipv4.set_total_length(total_length); ipv4.set_payload(icmp.packet()); let mut probe_socket = SocketImpl::new_dgram_ipv4(Protocol::ICMPV4) .or_else(|_| SocketImpl::new_raw_ipv4(Protocol::from(nix::libc::IPPROTO_RAW)))?; probe_socket.set_header_included(true)?; let remote_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); probe_socket.send_to(ipv4.packet(), remote_addr)?; Ok(()) } pub fn lookup_interface_addr(addr: IpAddr, name: &str) -> Result { match addr { IpAddr::V4(_) => lookup_interface_addr_ipv4(name), IpAddr::V6(_) => lookup_interface_addr_ipv6(name), } } #[instrument(ret, level = "trace")] fn lookup_interface_addr_ipv4(name: &str) -> Result { nix::ifaddrs::getifaddrs() .map_err(|_| Error::UnknownInterface(name.to_string()))? .find_map(|ia| { ia.address.and_then(|addr| match addr.family() { Some(AddressFamily::Inet) if ia.interface_name == name => addr .as_sockaddr_in() .map(|sock_addr| IpAddr::V4(sock_addr.ip())), _ => None, }) }) .ok_or_else(|| Error::UnknownInterface(name.to_string())) } #[instrument(ret, level = "trace")] fn lookup_interface_addr_ipv6(name: &str) -> Result { nix::ifaddrs::getifaddrs() .map_err(|_| Error::UnknownInterface(name.to_string()))? .find_map(|ia| { ia.address.and_then(|addr| match addr.family() { Some(AddressFamily::Inet6) if ia.interface_name == name => addr .as_sockaddr_in6() .map(|sock_addr| IpAddr::V6(sock_addr.ip())), _ => None, }) }) .ok_or_else(|| Error::UnknownInterface(name.to_string())) } // Note that no packets are transmitted by this method. #[instrument(ret, level = "trace")] pub fn discover_local_addr(target_addr: IpAddr, port: u16) -> Result { let mut socket = match target_addr { IpAddr::V4(_) => SocketImpl::new_udp_dgram_socket_ipv4(), IpAddr::V6(_) => SocketImpl::new_udp_dgram_socket_ipv6(), }?; socket.connect(SocketAddr::new(target_addr, port))?; Ok(socket.local_addr()?.ok_or(Error::MissingAddr)?.ip()) } } mod socket { use crate::error::{ErrorKind, IoError, IoOperation}; use crate::error::{IoResult, Result}; use crate::net::socket::{Socket, SocketError}; use itertools::Itertools; use nix::{ Error, sys::select::FdSet, sys::time::{TimeVal, TimeValLike}, }; use socket2::{Domain, Protocol, SockAddr, Type}; use std::io; use std::io::Read; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::net::{Shutdown, SocketAddr}; use std::os::fd::AsFd; use std::time::Duration; use tracing::instrument; #[instrument(level = "trace")] pub fn startup() -> Result<()> { Ok(()) } /// A network socket. pub struct SocketImpl { inner: socket2::Socket, } impl SocketImpl { fn new(domain: Domain, ty: Type, protocol: Protocol) -> IoResult { Ok(Self { inner: socket2::Socket::new(domain, ty, Some(protocol)) .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?, }) } pub(super) fn new_raw_ipv4(protocol: Protocol) -> IoResult { Ok(Self { inner: socket2::Socket::new(Domain::IPV4, Type::RAW, Some(protocol)) .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?, }) } fn new_raw_ipv6(protocol: Protocol) -> IoResult { Ok(Self { inner: socket2::Socket::new(Domain::IPV6, Type::RAW, Some(protocol)) .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?, }) } pub(super) fn new_dgram_ipv4(protocol: Protocol) -> IoResult { Ok(Self { inner: socket2::Socket::new(Domain::IPV4, Type::DGRAM, Some(protocol)) .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?, }) } fn new_dgram_ipv6(protocol: Protocol) -> IoResult { Ok(Self { inner: socket2::Socket::new(Domain::IPV6, Type::DGRAM, Some(protocol)) .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?, }) } fn set_nonblocking(&self, nonblocking: bool) -> IoResult<()> { self.inner .set_nonblocking(nonblocking) .map_err(|err| IoError::Other(err, IoOperation::SetNonBlocking)) } pub(super) fn local_addr(&self) -> IoResult> { Ok(self .inner .local_addr() .map_err(|err| IoError::Other(err, IoOperation::LocalAddr))? .as_socket()) } } impl Socket for SocketImpl { #[instrument(level = "trace")] fn new_icmp_send_socket_ipv4(raw: bool) -> IoResult { if raw { let mut socket = Self::new_raw_ipv4(Protocol::from(nix::libc::IPPROTO_RAW))?; socket.set_nonblocking(true)?; socket.set_header_included(true)?; Ok(socket) } else { let mut socket = Self::new(Domain::IPV4, Type::DGRAM, Protocol::ICMPV4)?; socket.set_nonblocking(true)?; socket.set_header_included(true)?; Ok(socket) } } #[instrument(level = "trace")] fn new_icmp_send_socket_ipv6(raw: bool) -> IoResult { if raw { let socket = Self::new_raw_ipv6(Protocol::ICMPV6)?; socket.set_nonblocking(true)?; Ok(socket) } else { let socket = Self::new_dgram_ipv6(Protocol::ICMPV6)?; socket.set_nonblocking(true)?; Ok(socket) } } #[instrument(level = "trace")] fn new_udp_send_socket_ipv4(raw: bool) -> IoResult { if raw { let mut socket = Self::new_raw_ipv4(Protocol::from(nix::libc::IPPROTO_RAW))?; socket.set_nonblocking(true)?; socket.set_header_included(true)?; Ok(socket) } else { let socket = Self::new_dgram_ipv4(Protocol::UDP)?; socket.set_nonblocking(true)?; Ok(socket) } } #[instrument(level = "trace")] fn new_udp_send_socket_ipv6(raw: bool) -> IoResult { if raw { let socket = Self::new_raw_ipv6(Protocol::UDP)?; socket.set_nonblocking(true)?; Ok(socket) } else { let socket = Self::new_dgram_ipv6(Protocol::UDP)?; socket.set_nonblocking(true)?; Ok(socket) } } #[instrument(level = "trace")] fn new_recv_socket_ipv4(_: Ipv4Addr, raw: bool) -> IoResult { if raw { let mut socket = Self::new_raw_ipv4(Protocol::ICMPV4)?; socket.set_nonblocking(true)?; socket.set_header_included(true)?; Ok(socket) } else { let socket = Self::new(Domain::IPV4, Type::DGRAM, Protocol::ICMPV4)?; socket.set_nonblocking(true)?; Ok(socket) } } #[instrument(level = "trace")] fn new_recv_socket_ipv6(_: Ipv6Addr, raw: bool) -> IoResult { if raw { let socket = Self::new_raw_ipv6(Protocol::ICMPV6)?; socket.set_nonblocking(true)?; Ok(socket) } else { let socket = Self::new_dgram_ipv6(Protocol::ICMPV6)?; socket.set_nonblocking(true)?; Ok(socket) } } #[instrument(level = "trace")] fn new_stream_socket_ipv4() -> IoResult { let mut socket = Self::new(Domain::IPV4, Type::STREAM, Protocol::TCP)?; socket.set_nonblocking(true)?; socket.set_reuse_port(true)?; Ok(socket) } #[instrument(level = "trace")] fn new_stream_socket_ipv6() -> IoResult { let mut socket = Self::new(Domain::IPV6, Type::STREAM, Protocol::TCP)?; socket.set_nonblocking(true)?; socket.set_reuse_port(true)?; Ok(socket) } #[instrument(level = "trace")] fn new_udp_dgram_socket_ipv4() -> IoResult { Self::new_dgram_ipv4(Protocol::UDP) } #[instrument(level = "trace")] fn new_udp_dgram_socket_ipv6() -> IoResult { Self::new_dgram_ipv6(Protocol::UDP) } #[instrument(skip(self), level = "trace")] fn bind(&mut self, address: SocketAddr) -> IoResult<()> { self.inner .bind(&SockAddr::from(address)) .map_err(|err| IoError::Bind(err, address)) } #[instrument(skip(self), level = "trace")] fn set_tos(&mut self, tos: u32) -> IoResult<()> { self.inner .set_tos_v4(tos) .map_err(|err| IoError::Other(err, IoOperation::SetTos)) } #[instrument(skip(self), level = "trace")] fn set_tclass_v6(&mut self, tclass: u32) -> IoResult<()> { self.inner .set_tclass_v6(tclass) .map_err(|err| IoError::Other(err, IoOperation::SetTclassV6)) } #[instrument(skip(self), level = "trace")] fn set_ttl(&mut self, ttl: u32) -> IoResult<()> { self.inner .set_ttl_v4(ttl) .map_err(|err| IoError::Other(err, IoOperation::SetTtl)) } #[instrument(skip(self), level = "trace")] fn set_reuse_port(&mut self, reuse: bool) -> IoResult<()> { self.inner .set_reuse_port(reuse) .map_err(|err| IoError::Other(err, IoOperation::SetReusePort)) } #[instrument(skip(self), level = "trace")] fn set_header_included(&mut self, included: bool) -> IoResult<()> { self.inner .set_header_included_v4(included) .map_err(|err| IoError::Other(err, IoOperation::SetHeaderIncluded)) } #[instrument(skip(self), level = "trace")] fn set_unicast_hops_v6(&mut self, hops: u8) -> IoResult<()> { self.inner .set_unicast_hops_v6(u32::from(hops)) .map_err(|err| IoError::Other(err, IoOperation::SetUnicastHopsV6)) } #[instrument(skip(self), level = "trace")] fn connect(&mut self, address: SocketAddr) -> IoResult<()> { tracing::trace!(?address); self.inner .connect(&SockAddr::from(address)) .map_err(|err| IoError::Connect(err, address)) } #[instrument(skip(self, buf), level = "trace")] fn send_to(&mut self, buf: &[u8], addr: SocketAddr) -> IoResult<()> { tracing::trace!(buf = format!("{:02x?}", buf.iter().format(" ")), ?addr); self.inner .send_to(buf, &SockAddr::from(addr)) .map_err(|err| IoError::SendTo(err, addr))?; Ok(()) } #[instrument(skip(self), level = "trace")] fn is_readable(&mut self, timeout: Duration) -> IoResult { let mut read = FdSet::new(); read.insert(self.inner.as_fd()); let readable = nix::sys::select::select( None, Some(&mut read), None, None, Some(&mut TimeVal::milliseconds(timeout.as_millis() as i64)), ); match readable { Ok(readable) => Ok(readable == 1), Err(Error::EINTR) => Ok(false), Err(err) => Err(IoError::Other( std::io::Error::from(err), IoOperation::Select, )), } } #[instrument(skip(self), level = "trace")] fn is_writable(&mut self) -> IoResult { let mut write = FdSet::new(); write.insert(self.inner.as_fd()); let writable = nix::sys::select::select( None, None, Some(&mut write), None, Some(&mut TimeVal::zero()), ); match writable { Ok(writable) => Ok(writable == 1), Err(Error::EINTR) => Ok(false), Err(err) => Err(IoError::Other( std::io::Error::from(err), IoOperation::Select, )), } } #[instrument(skip(self, buf), level = "trace")] fn recv_from(&mut self, buf: &mut [u8]) -> IoResult<(usize, Option)> { let (bytes_read, addr) = self .inner .recv_from_into_buf(buf) .map_err(|err| IoError::Other(err, IoOperation::RecvFrom))?; tracing::trace!( buf = format!("{:02x?}", buf[..bytes_read].iter().format(" ")), bytes_read, ?addr ); Ok((bytes_read, addr)) } #[instrument(skip(self, buf), level = "trace")] fn read(&mut self, buf: &mut [u8]) -> IoResult { let bytes_read = self .inner .read(buf) .map_err(|err| IoError::Other(err, IoOperation::Read))?; tracing::trace!( buf = format!("{:02x?}", buf[..bytes_read].iter().format(" ")), bytes_read ); Ok(bytes_read) } #[instrument(skip(self), level = "trace")] fn shutdown(&mut self) -> IoResult<()> { self.inner .shutdown(Shutdown::Both) .map_err(|err| IoError::Other(err, IoOperation::Shutdown)) } #[instrument(skip(self), level = "trace")] fn peer_addr(&mut self) -> IoResult> { let addr = self .inner .peer_addr() .map_err(|err| IoError::Other(err, IoOperation::PeerAddr))? .as_socket(); tracing::trace!(?addr); Ok(addr) } #[instrument(skip(self), ret, level = "trace")] fn take_error(&mut self) -> IoResult> { self.inner .take_error() .map(|err| { err.map(|e| match e.raw_os_error() { Some(errno) if Error::from_raw(errno) == Error::ECONNREFUSED => { SocketError::ConnectionRefused } _ => SocketError::Other(e), }) }) .map_err(|err| IoError::Other(err, IoOperation::TakeError)) } #[instrument(skip(self), ret, level = "trace")] fn icmp_error_info(&mut self) -> IoResult { Ok(IpAddr::V4(Ipv4Addr::UNSPECIFIED)) } } impl From<&io::Error> for ErrorKind { fn from(value: &io::Error) -> Self { if value.raw_os_error() == io::Error::from(Error::EINPROGRESS).raw_os_error() { Self::InProgress } else if value.raw_os_error() == io::Error::from(Error::EHOSTUNREACH).raw_os_error() { Self::HostUnreachable } else if value.raw_os_error() == io::Error::from(Error::ENETUNREACH).raw_os_error() { Self::NetUnreachable } else { Self::Std(value.kind()) } } } // only used for unit tests impl From for io::Error { fn from(value: ErrorKind) -> Self { match value { ErrorKind::InProgress => Self::from(Error::EINPROGRESS), ErrorKind::HostUnreachable => Self::from(Error::EHOSTUNREACH), ErrorKind::NetUnreachable => Self::from(Error::ENETUNREACH), ErrorKind::Std(kind) => Self::from(kind), } } } impl Read for SocketImpl { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.inner.read(buf) } } /// An extension trait to allow `recv_from` method which writes to a `&mut [u8]`. /// /// This is required for `socket2::Socket` which [does not currently provide] this method. /// /// [does not currently provide]: https://github.com/rust-lang/socket2/issues/223 trait RecvFrom { fn recv_from_into_buf(&self, buf: &mut [u8]) -> io::Result<(usize, Option)>; } impl RecvFrom for socket2::Socket { // Safety: the `recv` implementation promises not to write uninitialised // bytes to the `buf`fer, so this casting is safe. #![allow(unsafe_code)] fn recv_from_into_buf(&self, buf: &mut [u8]) -> io::Result<(usize, Option)> { let buf = unsafe { &mut *(std::ptr::from_mut::<[u8]>(buf) as *mut [std::mem::MaybeUninit]) }; self.recv_from(buf) .map(|(size, addr)| (size, addr.as_socket())) } } } pub use socket::{SocketImpl, startup}; ================================================ FILE: crates/trippy-core/src/net/platform/windows.rs ================================================ use super::byte_order::Ipv4ByteOrder; use crate::error::{Error, ErrorKind, IoError, IoOperation, IoResult, Result}; use crate::net::channel::MAX_PACKET_SIZE; use crate::net::platform::Platform; use crate::net::platform::windows::adapter::Adapters; use crate::net::socket::{Socket, SocketError}; use itertools::Itertools; use socket2::{Domain, Protocol, SockAddr, Type}; use std::ffi::c_void; use std::io::{Error as StdIoError, ErrorKind as StdErrorKind, Result as StdIoResult}; use std::mem::{size_of, zeroed}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use std::os::windows::prelude::AsRawSocket; use std::ptr::{addr_of, addr_of_mut, null_mut}; use std::time::Duration; use tracing::instrument; use windows_sys::Win32::Foundation::{WAIT_FAILED, WAIT_TIMEOUT}; use windows_sys::Win32::Networking::WinSock::{ AF_INET, AF_INET6, FD_CONNECT, FD_WRITE, ICMP_ERROR_INFO, IN_ADDR, IN_ADDR_0, IN6_ADDR, IN6_ADDR_0, IPPROTO_RAW, IPPROTO_TCP, SIO_ROUTING_INTERFACE_QUERY, SO_ERROR, SO_PORT_SCALABILITY, SO_REUSE_UNICASTPORT, SOCKADDR_IN, SOCKADDR_IN6, SOCKADDR_IN6_0, SOCKADDR_STORAGE, SOCKET_ERROR, SOL_SOCKET, TCP_FAIL_CONNECT_ON_ICMP_ERROR, TCP_ICMP_ERROR_INFO, WSA_IO_INCOMPLETE, WSA_IO_PENDING, WSABUF, WSADATA, WSAEADDRNOTAVAIL, WSAECONNREFUSED, WSAEHOSTUNREACH, WSAEINPROGRESS, WSAENETUNREACH, WSAENOBUFS, }; use windows_sys::Win32::System::IO::OVERLAPPED; /// Execute a `Win32::Networking::WinSock` syscall. /// /// The result of the syscall will be passed to the supplied boolean closure to determine if it /// represents an error and if so returns the last OS error, otherwise the result of the syscall is /// returned. macro_rules! syscall { ($fn: ident ( $($arg: expr),* $(,)* ), $err_fn: expr) => {{ #[expect(unsafe_code)] let res = unsafe { windows_sys::Win32::Networking::WinSock::$fn($($arg, )*) }; if $err_fn(res) { Err(StdIoError::last_os_error()) } else { Ok(res) } }}; } /// Execute a `Win32::NetworkManagement::IpHelper` syscall. /// /// The raw result of the syscall is returned. macro_rules! syscall_ip_helper { ($fn: ident ( $($arg: expr),* $(,)* )) => {{ #[expect(unsafe_code)] unsafe { windows_sys::Win32::NetworkManagement::IpHelper::$fn($($arg, )*) } }}; } /// Execute a `Win32::System::Threading` syscall. /// /// The raw result of the syscall is returned. macro_rules! syscall_threading { ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{ #[expect(unsafe_code)] unsafe { windows_sys::Win32::System::Threading::$fn($($arg, )*) } }}; } pub struct PlatformImpl; impl Platform for PlatformImpl { fn byte_order_for_address(_addr: IpAddr) -> Result { Ok(Ipv4ByteOrder::Network) } fn lookup_interface_addr(addr: IpAddr, name: &str) -> Result { match addr { IpAddr::V4(_) => lookup_interface_addr(&Adapters::ipv4()?, name), IpAddr::V6(_) => lookup_interface_addr(&Adapters::ipv6()?, name), } } fn discover_local_addr(target_addr: IpAddr, _port: u16) -> Result { routing_interface_query(target_addr) } } #[instrument(level = "trace")] pub fn startup() -> Result<()> { SocketImpl::startup().map_err(Error::IoError) } /// `WinSock` version 2.2 const WINSOCK_VERSION: u16 = 0x202; /// A network socket. pub struct SocketImpl { inner: socket2::Socket, ol: Box, buf: Box<[u8]>, from: Box, from_len: i32, bytes_read: u32, } #[expect(clippy::cast_possible_wrap)] impl SocketImpl { fn startup() -> IoResult<()> { let mut wsa_data = Self::new_wsa_data(); syscall!(WSAStartup(WINSOCK_VERSION, addr_of_mut!(wsa_data)), |res| { res != 0 }) .map_err(|err| IoError::Other(err, IoOperation::Startup)) .map(|_| ()) } fn new(domain: Domain, ty: Type, protocol: Option) -> IoResult { let inner = socket2::Socket::new(domain, ty, protocol) .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?; let from = Box::new(Self::new_sockaddr_storage()); let from_len = std::mem::size_of::() as i32; let ol = Box::new(Self::new_overlapped()); let buf = Box::new([0; MAX_PACKET_SIZE]); Ok(Self { inner, ol, buf, from, from_len, bytes_read: 0, }) } #[instrument(skip(self), level = "trace")] fn create_event(&mut self) -> IoResult<()> { self.ol.hEvent = syscall!(WSACreateEvent(), |res| { res == 0 || res == -1 }) .map_err(|err| IoError::Other(err, IoOperation::WSACreateEvent))?; Ok(()) } #[instrument(skip(self), level = "trace")] fn wait_for_event(&self, timeout: Duration) -> IoResult { let millis = timeout.as_millis() as u32; let rc = syscall_threading!(WaitForSingleObject(self.ol.hEvent, millis)); if rc == WAIT_TIMEOUT { return Ok(false); } else if rc == WAIT_FAILED { return Err(IoError::Other( StdIoError::last_os_error(), IoOperation::WaitForSingleObject, )); } Ok(true) } #[instrument(skip(self), level = "trace")] fn reset_event(&self) -> IoResult<()> { syscall!(WSAResetEvent(self.ol.hEvent), |res| { res == 0 }) .map_err(|err| IoError::Other(err, IoOperation::WSAResetEvent)) .map(|_| ()) } #[instrument(skip(self, optval), level = "trace")] fn getsockopt(&self, level: i32, optname: i32, mut optval: T) -> StdIoResult { let mut optlen = size_of::() as i32; syscall!( getsockopt( self.inner.as_raw_socket() as _, level, optname, addr_of_mut!(optval).cast(), &raw mut optlen, ), |res| res == SOCKET_ERROR )?; Ok(optval) } #[instrument(skip(self), level = "trace")] fn setsockopt_u32(&self, level: i32, optname: i32, optval: u32) -> StdIoResult<()> { let bytes = optval.to_ne_bytes(); let optval = addr_of!(bytes).cast(); syscall!( setsockopt( self.inner.as_raw_socket() as _, level, optname, optval, size_of::() as i32, ), |res| res == SOCKET_ERROR ) .map(|_| ()) } #[instrument(skip(self), level = "trace")] fn setsockopt_bool(&self, level: i32, optname: i32, optval: bool) -> StdIoResult<()> { self.setsockopt_u32(level, optname, u32::from(optval)) } #[instrument(skip(self), level = "trace")] fn set_fail_connect_on_icmp_error(&self, enabled: bool) -> IoResult<()> { self.setsockopt_bool(IPPROTO_TCP, TCP_FAIL_CONNECT_ON_ICMP_ERROR as _, enabled) .map_err(|err| IoError::Other(err, IoOperation::SetTcpFailConnectOnIcmpError)) } #[instrument(skip(self), level = "trace")] fn set_non_blocking(&self, is_non_blocking: bool) -> IoResult<()> { self.inner .set_nonblocking(is_non_blocking) .map_err(|err| IoError::Other(err, IoOperation::SetNonBlocking)) } // TODO handle case where `WSARecvFrom` succeeded immediately. #[instrument(skip(self), level = "trace")] fn post_recv_from(&mut self) -> IoResult<()> { fn is_err(res: i32) -> bool { res == SOCKET_ERROR && StdIoError::last_os_error().raw_os_error() != Some(WSA_IO_PENDING) } let wbuf = WSABUF { len: MAX_PACKET_SIZE as u32, buf: self.buf.as_mut_ptr(), }; syscall!( WSARecvFrom( self.inner.as_raw_socket() as usize, addr_of!(wbuf), 1, null_mut(), &mut 0, addr_of_mut!(*self.from).cast(), addr_of_mut!(self.from_len), addr_of_mut!(*self.ol), None, ), is_err ) .map_err(|err| IoError::Other(err, IoOperation::WSARecvFrom))?; Ok(()) } #[instrument(skip(self), level = "trace")] fn get_overlapped_result(&mut self) -> IoResult<()> { let mut bytes_read = 0; let mut flags = 0; let ol = *self.ol; syscall!( WSAGetOverlappedResult( self.inner.as_raw_socket() as _, addr_of!(ol), &raw mut bytes_read, 0, &raw mut flags, ), |res| { res == 0 } ) .map_err(|err| IoError::Other(err, IoOperation::WSAGetOverlappedResult))?; self.bytes_read = bytes_read; Ok(()) } #[expect(unsafe_code)] const fn new_wsa_data() -> WSADATA { // Safety: an all-zero value is valid for `WSADATA`. unsafe { zeroed::() } } #[expect(unsafe_code)] const fn new_sockaddr_storage() -> SOCKADDR_STORAGE { // Safety: an all-zero value is valid for `SOCKADDR_STORAGE`. unsafe { zeroed::() } } #[expect(unsafe_code)] const fn new_overlapped() -> OVERLAPPED { // Safety: an all-zero value is valid for `OVERLAPPED.` unsafe { zeroed::() } } #[expect(unsafe_code)] const fn new_icmp_error_info() -> ICMP_ERROR_INFO { // Safety: an all-zero value is valid for `ICMP_ERROR_INFO`. unsafe { zeroed::() } } } impl Drop for SocketImpl { fn drop(&mut self) { if self.ol.hEvent != -1 && self.ol.hEvent != 0 { syscall!(WSACloseEvent(self.ol.hEvent), |res| { res == 0 }).unwrap_or_default(); } } } #[expect(clippy::cast_possible_wrap)] impl Socket for SocketImpl { #[instrument(level = "trace")] fn new_icmp_send_socket_ipv4(raw: bool) -> IoResult { if raw { let mut sock = Self::new(Domain::IPV4, Type::RAW, Some(Protocol::from(IPPROTO_RAW)))?; sock.set_non_blocking(true)?; sock.set_header_included(true)?; Ok(sock) } else { unimplemented!("non-raw socket is not supported on Windows") } } #[instrument(level = "trace")] fn new_icmp_send_socket_ipv6(raw: bool) -> IoResult { if raw { let sock = Self::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6))?; sock.set_non_blocking(true)?; Ok(sock) } else { unimplemented!("non-raw socket is not supported on Windows") } } #[instrument(level = "trace")] fn new_udp_send_socket_ipv4(raw: bool) -> IoResult { if raw { let mut sock = Self::new(Domain::IPV4, Type::RAW, Some(Protocol::from(IPPROTO_RAW)))?; sock.set_non_blocking(true)?; sock.set_header_included(true)?; Ok(sock) } else { unimplemented!("non-raw socket is not supported on Windows") } } #[instrument(level = "trace")] fn new_udp_send_socket_ipv6(raw: bool) -> IoResult { if raw { let sock = Self::new(Domain::IPV6, Type::RAW, Some(Protocol::UDP))?; sock.set_non_blocking(true)?; Ok(sock) } else { unimplemented!("non-raw socket is not supported on Windows") } } #[instrument(level = "trace")] fn new_recv_socket_ipv4(src_addr: Ipv4Addr, raw: bool) -> IoResult { if raw { let mut sock = Self::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?; sock.bind(SocketAddr::new(IpAddr::V4(src_addr), 0))?; sock.post_recv_from()?; sock.set_non_blocking(true)?; sock.set_header_included(true)?; Ok(sock) } else { unimplemented!("non-raw socket is not supported on Windows") } } #[instrument(level = "trace")] fn new_recv_socket_ipv6(src_addr: Ipv6Addr, raw: bool) -> IoResult { if raw { let mut sock = Self::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6))?; sock.bind(SocketAddr::new(IpAddr::V6(src_addr), 0))?; sock.post_recv_from()?; sock.set_non_blocking(true)?; Ok(sock) } else { unimplemented!("non-raw socket is not supported on Windows") } } #[instrument(level = "trace")] fn new_stream_socket_ipv4() -> IoResult { let mut sock = Self::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?; sock.set_non_blocking(true)?; sock.set_reuse_port(true)?; Ok(sock) } #[instrument(level = "trace")] fn new_stream_socket_ipv6() -> IoResult { let mut sock = Self::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))?; sock.set_non_blocking(true)?; sock.set_reuse_port(true)?; Ok(sock) } #[instrument(level = "trace")] fn new_udp_dgram_socket_ipv4() -> IoResult { Self::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP)) } #[instrument(level = "trace")] fn new_udp_dgram_socket_ipv6() -> IoResult { Self::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP)) } #[instrument(skip(self), level = "trace")] fn bind(&mut self, addr: SocketAddr) -> IoResult<()> { self.inner .bind(&SockAddr::from(addr)) .map_err(|e| { if e.kind() == StdErrorKind::PermissionDenied { StdIoError::from_raw_os_error(WSAEADDRNOTAVAIL) } else { e } }) .map_err(|err| IoError::Bind(err, addr))?; self.create_event()?; Ok(()) } #[instrument(skip(self), level = "trace")] fn set_tos(&mut self, tos: u32) -> IoResult<()> { self.inner .set_tos_v4(tos) .map_err(|err| IoError::Other(err, IoOperation::SetTos)) } fn set_tclass_v6(&mut self, tclass: u32) -> IoResult<()> { if tclass > 0 { unimplemented!("setting tclass_v6 is not supported on Windows") } Ok(()) } #[instrument(skip(self), level = "trace")] fn set_ttl(&mut self, ttl: u32) -> IoResult<()> { self.inner .set_ttl_v4(ttl) .map_err(|err| IoError::Other(err, IoOperation::SetTtl)) } #[instrument(skip(self), level = "trace")] fn set_reuse_port(&mut self, is_reuse_port: bool) -> IoResult<()> { self.setsockopt_bool(SOL_SOCKET as _, SO_REUSE_UNICASTPORT as _, is_reuse_port) .or_else(|_| { self.setsockopt_bool(SOL_SOCKET as _, SO_PORT_SCALABILITY as _, is_reuse_port) }) .map_err(|err| IoError::Other(err, IoOperation::SetReusePort)) } #[instrument(skip(self), level = "trace")] fn set_header_included(&mut self, is_header_included: bool) -> IoResult<()> { self.inner .set_header_included_v4(is_header_included) .map_err(|err| IoError::Other(err, IoOperation::SetHeaderIncluded)) } #[instrument(skip(self), level = "trace")] fn set_unicast_hops_v6(&mut self, max_hops: u8) -> IoResult<()> { self.inner .set_unicast_hops_v6(max_hops.into()) .map_err(|err| IoError::Other(err, IoOperation::SetUnicastHopsV6)) } #[instrument(skip(self), level = "trace")] fn connect(&mut self, addr: SocketAddr) -> IoResult<()> { self.set_fail_connect_on_icmp_error(true)?; syscall!( WSAEventSelect( self.inner.as_raw_socket() as _, self.ol.hEvent, (FD_CONNECT | FD_WRITE) as _ ), |res| res == SOCKET_ERROR ) .map_err(|err| IoError::Other(err, IoOperation::WSAEventSelect))?; let res = self.inner.connect(&SockAddr::from(addr)); match res { Ok(()) => Ok(()), Err(ref e) if e.kind() == StdErrorKind::WouldBlock => Ok(()), Err(err) => Err(IoError::Connect(err, addr)), } } #[instrument(skip(self, buf), level = "trace")] fn send_to(&mut self, buf: &[u8], addr: SocketAddr) -> IoResult<()> { tracing::trace!(buf = format!("{:02x?}", buf.iter().format(" ")), ?addr); self.inner .send_to(buf, &SockAddr::from(addr)) .map_err(|err| IoError::SendTo(err, addr))?; Ok(()) } #[instrument(skip(self), level = "trace")] fn is_readable(&mut self, timeout: Duration) -> IoResult { if !self.wait_for_event(timeout)? { return Ok(false); } while let Err(err) = self.get_overlapped_result() { if err.kind() != ErrorKind::Std(StdIoError::from_raw_os_error(WSA_IO_INCOMPLETE).kind()) { return Err(err); } } self.reset_event()?; Ok(true) } #[instrument(skip(self), level = "trace")] fn is_writable(&mut self) -> IoResult { if !self.wait_for_event(Duration::ZERO)? { return Ok(false); } while let Err(err) = self.get_overlapped_result() { if err.kind() != ErrorKind::Std(StdIoError::from_raw_os_error(WSA_IO_INCOMPLETE).kind()) { return Err(err); } } self.reset_event()?; Ok(true) } #[instrument(skip(self, buf), level = "trace")] fn recv_from(&mut self, buf: &mut [u8]) -> IoResult<(usize, Option)> { let addr = sockaddrptr_to_ipaddr(addr_of_mut!(*self.from)) .map_err(|err| IoError::Other(err, IoOperation::RecvFrom))?; let len = self.read(buf)?; tracing::trace!( buf = format!("{:02x?}", buf[..len].iter().format(" ")), len, ?addr ); Ok((len, Some(SocketAddr::new(addr, 0)))) } #[instrument(skip(self, buf), ret, level = "trace")] fn read(&mut self, buf: &mut [u8]) -> IoResult { let bytes_read = std::cmp::min(self.bytes_read as usize, buf.len()); buf[..bytes_read].copy_from_slice(&self.buf[..bytes_read]); tracing::trace!(buf = format!("{:02x?}", buf[..bytes_read].iter().format(" "))); self.post_recv_from()?; Ok(bytes_read) } #[instrument(skip(self), level = "trace")] fn shutdown(&mut self) -> IoResult<()> { self.inner .shutdown(std::net::Shutdown::Both) .map_err(|err| IoError::Other(err, IoOperation::Shutdown)) } #[instrument(skip(self), ret, level = "trace")] fn peer_addr(&mut self) -> IoResult> { Ok(self .inner .peer_addr() .map_err(|err| IoError::Other(err, IoOperation::PeerAddr))? .as_socket()) } #[instrument(skip(self), ret, level = "trace")] fn take_error(&mut self) -> IoResult> { match self.getsockopt(SOL_SOCKET as _, SO_ERROR as _, 0) { Ok(0) => Ok(None), Ok(errno) if errno == WSAEHOSTUNREACH => Ok(Some(SocketError::HostUnreachable)), Ok(errno) if errno == WSAECONNREFUSED => Ok(Some(SocketError::ConnectionRefused)), Ok(errno) => Ok(Some(SocketError::Other(StdIoError::from_raw_os_error( errno, )))), Err(e) => Err(e), } .map_err(|err| IoError::Other(err, IoOperation::TakeError)) } #[instrument(skip(self), ret, level = "trace")] #[expect(unsafe_code)] fn icmp_error_info(&mut self) -> IoResult { let icmp_error_info = self .getsockopt::( IPPROTO_TCP as _, TCP_ICMP_ERROR_INFO as _, Self::new_icmp_error_info(), ) .map_err(|err| IoError::Other(err, IoOperation::TcpIcmpErrorInfo))?; let src_addr = icmp_error_info.srcaddress; match unsafe { src_addr.si_family } { AF_INET => Ok(IpAddr::V4(Ipv4Addr::from(unsafe { src_addr.Ipv4.sin_addr.S_un.S_addr.to_ne_bytes() }))), AF_INET6 => Ok(IpAddr::V6(Ipv6Addr::from(unsafe { src_addr.Ipv6.sin6_addr.u.Byte }))), _ => Err(IoError::Other( StdIoError::from(StdErrorKind::AddrNotAvailable), IoOperation::TcpIcmpErrorInfo, )), } } } // Note that we handle `WSAENOBUFS`, which can occurs when calling `send_to()` // for ICMP and UDP. We return it as `NetUnreachable` to piggyback on the // existing error handling. impl From<&StdIoError> for ErrorKind { fn from(value: &StdIoError) -> Self { if let Some(raw) = value.raw_os_error() { if raw == WSAEINPROGRESS { Self::InProgress } else if raw == WSAEHOSTUNREACH { Self::HostUnreachable } else if raw == WSAENETUNREACH || raw == WSAENOBUFS { Self::NetUnreachable } else { Self::Std(value.kind()) } } else { Self::Std(value.kind()) } } } // only used for unit tests impl From for StdIoError { fn from(value: ErrorKind) -> Self { match value { ErrorKind::InProgress => Self::from_raw_os_error(WSAEINPROGRESS), ErrorKind::HostUnreachable => Self::from_raw_os_error(WSAEHOSTUNREACH), ErrorKind::NetUnreachable => Self::from_raw_os_error(WSAENETUNREACH), ErrorKind::Std(kind) => Self::from(kind), } } } /// NOTE under Windows, we cannot use a bind connect/getsockname as "If the socket /// is using a connectionless protocol, the address may not be available until I/O /// occurs on the socket." We use `SIO_ROUTING_INTERFACE_QUERY` instead. #[expect(clippy::cast_sign_loss)] #[instrument(level = "trace")] fn routing_interface_query(target: IpAddr) -> Result { let mut src_buf = [0; 1024]; let src: *mut c_void = src_buf.as_mut_ptr().cast(); let mut bytes = 0; let socket = match target { IpAddr::V4(_) => SocketImpl::new_udp_dgram_socket_ipv4(), IpAddr::V6(_) => SocketImpl::new_udp_dgram_socket_ipv6(), }?; let (dest, destlen) = socketaddr_to_sockaddr(SocketAddr::new(target, 0)); syscall!( WSAIoctl( socket.inner.as_raw_socket() as _, SIO_ROUTING_INTERFACE_QUERY, addr_of!(dest).cast(), destlen as u32, src, 1024, addr_of_mut!(bytes), null_mut(), None, ), |res| res == SOCKET_ERROR ) .map_err(|err| IoError::Other(err, IoOperation::SioRoutingInterfaceQuery))?; // Note that the `WSAIoctl` call potentially returns multiple results (see // ), // TBD We choose the first one arbitrarily. let sockaddr = src.cast::(); sockaddrptr_to_ipaddr(sockaddr) .map_err(|err| Error::IoError(IoError::Other(err, IoOperation::ConvertSocketAddress))) } #[expect(unsafe_code)] fn sockaddrptr_to_ipaddr(sockaddr: *mut SOCKADDR_STORAGE) -> StdIoResult { // Safety: TODO match sockaddr_to_socketaddr(unsafe { sockaddr.as_ref().unwrap() }) { Err(e) => Err(e), Ok(socketaddr) => match socketaddr { SocketAddr::V4(socketaddrv4) => Ok(IpAddr::V4(*socketaddrv4.ip())), SocketAddr::V6(socketaddrv6) => Ok(IpAddr::V6(*socketaddrv6.ip())), }, } } #[expect(unsafe_code)] fn sockaddr_to_socketaddr(sockaddr: &SOCKADDR_STORAGE) -> StdIoResult { let ptr = std::ptr::from_ref(sockaddr); let af = sockaddr.ss_family; if af == AF_INET { let sockaddr_in_ptr = ptr.cast::(); // Safety: TODO let sockaddr_in = unsafe { *sockaddr_in_ptr }; let ipv4addr = u32::from_be(unsafe { sockaddr_in.sin_addr.S_un.S_addr }); let port = sockaddr_in.sin_port; Ok(SocketAddr::V4(SocketAddrV4::new( Ipv4Addr::from(ipv4addr), port, ))) } else if af == AF_INET6 { let sockaddr_in6_ptr = ptr.cast::(); // Safety: TODO let sockaddr_in6 = unsafe { *sockaddr_in6_ptr }; // TODO: check endianness // Safety: TODO let ipv6addr = unsafe { sockaddr_in6.sin6_addr.u.Byte }; let port = sockaddr_in6.sin6_port; // Safety: TODO let scope_id = unsafe { sockaddr_in6.Anonymous.sin6_scope_id }; Ok(SocketAddr::V6(SocketAddrV6::new( Ipv6Addr::from(ipv6addr), port, sockaddr_in6.sin6_flowinfo, scope_id, ))) } else { Err(StdIoError::new( StdErrorKind::Unsupported, format!("Unsupported address family: {af:?}"), )) } } #[expect(unsafe_code)] #[expect(clippy::cast_possible_wrap)] #[must_use] fn socketaddr_to_sockaddr(socketaddr: SocketAddr) -> (SOCKADDR_STORAGE, i32) { #[repr(C)] union SockAddr { storage: SOCKADDR_STORAGE, in4: SOCKADDR_IN, in6: SOCKADDR_IN6, } let sockaddr = match socketaddr { SocketAddr::V4(socketaddrv4) => SockAddr { in4: SOCKADDR_IN { sin_family: AF_INET, sin_port: socketaddrv4.port().to_be(), sin_addr: IN_ADDR { S_un: IN_ADDR_0 { S_addr: u32::from(*socketaddrv4.ip()).to_be(), }, }, sin_zero: [0; 8], }, }, SocketAddr::V6(socketaddrv6) => SockAddr { in6: SOCKADDR_IN6 { sin6_family: AF_INET6, sin6_port: socketaddrv6.port().to_be(), sin6_flowinfo: socketaddrv6.flowinfo(), sin6_addr: IN6_ADDR { u: IN6_ADDR_0 { Byte: socketaddrv6.ip().octets(), }, }, Anonymous: SOCKADDR_IN6_0 { sin6_scope_id: socketaddrv6.scope_id(), }, }, }, }; (unsafe { sockaddr.storage }, size_of::() as i32) } #[instrument(skip(adapters), ret, level = "trace")] fn lookup_interface_addr(adapters: &Adapters, name: &str) -> Result { adapters .iter() .find_map(|addr| { if addr.name.eq_ignore_ascii_case(name) { addr.addr } else { None } }) .ok_or_else(|| Error::UnknownInterface(name.to_string())) } mod adapter { use crate::error::{Error, Result}; use crate::net::platform::windows::sockaddrptr_to_ipaddr; use std::io::Error as StdIoError; use std::marker::PhantomData; use std::net::IpAddr; use std::ptr::null_mut; use widestring::WideCString; use windows_sys::Win32::Foundation::{ERROR_BUFFER_OVERFLOW, NO_ERROR}; use windows_sys::Win32::NetworkManagement::IpHelper; use windows_sys::Win32::NetworkManagement::IpHelper::{ GET_ADAPTERS_ADDRESSES_FLAGS, IP_ADAPTER_ADDRESSES_LH, }; use windows_sys::Win32::Networking::WinSock::{ADDRESS_FAMILY, AF_INET, AF_INET6}; /// Retrieve adapter address information. pub struct Adapters { buf: Vec, } impl Adapters { /// Retrieve IPv4 adapter details. pub fn ipv4() -> Result { Self::retrieve_addresses(AF_INET) } /// Retrieve IPv6 adapter details. pub fn ipv6() -> Result { Self::retrieve_addresses(AF_INET6) } /// Return an iterator of `AdapterAddress` in this `Adapters`. pub fn iter(&self) -> AdaptersIter<'_> { AdaptersIter::new(self) } // The maximum number of attempts to retrieve addresses. const MAX_ATTEMPTS: usize = 3; // The size of the buffer to use for the first retrieval attempt. const INITIAL_BUFFER_SIZE: u32 = 15000; // The flags to use when performing the adapter addresses retrieval. const ADDRESS_FLAGS: GET_ADAPTERS_ADDRESSES_FLAGS = IpHelper::GAA_FLAG_SKIP_ANYCAST | IpHelper::GAA_FLAG_SKIP_MULTICAST | IpHelper::GAA_FLAG_SKIP_DNS_SERVER; fn retrieve_addresses(family: ADDRESS_FAMILY) -> Result { let mut buf_len = Self::INITIAL_BUFFER_SIZE; let mut buf: Vec; for _ in 0..Self::MAX_ATTEMPTS { buf = vec![0_u8; buf_len as usize]; let res = syscall_ip_helper!(GetAdaptersAddresses( u32::from(family), Self::ADDRESS_FLAGS, null_mut(), buf.as_mut_ptr().cast(), &raw mut buf_len, )); if res == ERROR_BUFFER_OVERFLOW { continue; } if res != NO_ERROR { return Err(Error::UnknownInterface(format!( "GetAdaptersAddresses returned error: {}", StdIoError::from_raw_os_error(res.try_into().unwrap()) ))); } return Ok(Self { buf }); } Err(Error::UnknownInterface(format!( "GetAdaptersAddresses did not succeed after {} attempts", Self::MAX_ATTEMPTS ))) } } /// A named adapter address. #[derive(Debug)] pub struct AdapterAddress { /// The adapter friendly name. pub name: String, /// The first adapter uni-cast `IpAddr`, if any. pub addr: Option, } /// An iterator for `Adapters` which yields `AdapterAddress` pub struct AdaptersIter<'a> { next: *const IP_ADAPTER_ADDRESSES_LH, _data: PhantomData<&'a ()>, } impl<'a> AdaptersIter<'a> { /// Create an iterator for an `Adapters`. pub fn new(data: &'a Adapters) -> Self { let next = data.buf.as_ptr().cast(); Self { next, // tie the lifetime of this iterator to the lifetime of the `Adapters` _data: PhantomData, } } } impl Iterator for AdaptersIter<'_> { type Item = AdapterAddress; fn next(&mut self) -> Option { if self.next.is_null() { None } else { // Safety: `next` is not null and points to a valid `IP_ADAPTER_ADDRESSES_LH` #[expect(unsafe_code)] unsafe { let friendly_name = WideCString::from_ptr_str((*self.next).FriendlyName) .to_string() .ok()?; let addr = { let first_unicast = (*self.next).FirstUnicastAddress; if first_unicast.is_null() { None } else { let socket_address = (*first_unicast).Address; let sockaddr = socket_address.lpSockaddr; Some(sockaddrptr_to_ipaddr(sockaddr.cast()).ok()?) } }; self.next = (*self.next).Next; Some(AdapterAddress { name: friendly_name, addr, }) } } } } } ================================================ FILE: crates/trippy-core/src/net/platform.rs ================================================ pub mod byte_order; pub use byte_order::Ipv4ByteOrder; use std::net::IpAddr; #[cfg(unix)] mod unix; use crate::error::Result; #[cfg(unix)] pub use unix::*; #[cfg(windows)] mod windows; #[cfg(windows)] pub use self::windows::*; /// Platform specific operations. #[cfg_attr(test, mockall::automock)] pub trait Platform { /// Determine the required byte ordering for IPv4 header fields. fn byte_order_for_address(addr: IpAddr) -> Result; /// Lookup an `IpAddr` for an interface. /// /// If the interface has more than one address then an arbitrary address /// is selected and returned. fn lookup_interface_addr(addr: IpAddr, name: &str) -> Result; /// Discover a local `IpAddr` which can route to the target address. fn discover_local_addr(target_addr: IpAddr, port: u16) -> Result; } ================================================ FILE: crates/trippy-core/src/net/socket.rs ================================================ use crate::error::IoResult as Result; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::time::Duration; #[cfg_attr(test, mockall::automock)] pub trait Socket where Self: Sized, { /// Create an IPv4 socket for sending ICMP probes. fn new_icmp_send_socket_ipv4(raw: bool) -> Result; /// Create an IPv6 socket for sending ICMP probes. fn new_icmp_send_socket_ipv6(raw: bool) -> Result; /// Create an IPv4 socket for sending UDP probes. fn new_udp_send_socket_ipv4(raw: bool) -> Result; /// Create an IPv6 socket for sending UDP probes. fn new_udp_send_socket_ipv6(raw: bool) -> Result; /// Create an IPv4 socket for receiving UDP probe responses. fn new_recv_socket_ipv4(addr: Ipv4Addr, raw: bool) -> Result; /// Create an IPv6 socket for receiving UDP probe responses. fn new_recv_socket_ipv6(addr: Ipv6Addr, raw: bool) -> Result; /// Create a IPv4/TCP socket for sending TCP probes. fn new_stream_socket_ipv4() -> Result; /// Create a IPv6/TCP socket for sending TCP probes. fn new_stream_socket_ipv6() -> Result; /// Create (non-raw) IPv4/UDP socket for local address validation. fn new_udp_dgram_socket_ipv4() -> Result; /// Create (non-raw) IPv6/UDP socket for local address validation. fn new_udp_dgram_socket_ipv6() -> Result; fn bind(&mut self, address: SocketAddr) -> Result<()>; fn set_tos(&mut self, tos: u32) -> Result<()>; fn set_tclass_v6(&mut self, tclass: u32) -> Result<()>; fn set_ttl(&mut self, ttl: u32) -> Result<()>; fn set_reuse_port(&mut self, reuse: bool) -> Result<()>; fn set_header_included(&mut self, included: bool) -> Result<()>; fn set_unicast_hops_v6(&mut self, hops: u8) -> Result<()>; fn connect(&mut self, address: SocketAddr) -> Result<()>; fn send_to(&mut self, buf: &[u8], addr: SocketAddr) -> Result<()>; /// Returns true if the socket becomes readable before the timeout, false otherwise. fn is_readable(&mut self, timeout: Duration) -> Result; /// Returns true if the socket is currently writable, false otherwise. fn is_writable(&mut self) -> Result; fn recv_from(&mut self, buf: &mut [u8]) -> Result<(usize, Option)>; fn read(&mut self, buf: &mut [u8]) -> Result; fn shutdown(&mut self) -> Result<()>; fn peer_addr(&mut self) -> Result>; fn take_error(&mut self) -> Result>; fn icmp_error_info(&mut self) -> Result; } /// A socket error returned by `Socket::take_error`. #[derive(Debug)] pub enum SocketError { ConnectionRefused, #[allow(dead_code)] HostUnreachable, Other(#[expect(dead_code)] std::io::Error), } #[cfg(test)] pub mod tests { #[macro_export] macro_rules! mocket_read { ($packet: expr) => { move |buf: &mut [u8]| -> IoResult { buf[..$packet.len()].copy_from_slice(&$packet); Ok(buf.len()) } }; } #[macro_export] macro_rules! mocket_recv_from { ($packet: expr, $addr: expr) => { move |buf: &mut [u8]| -> IoResult<(usize, Option)> { buf[..$packet.len()].copy_from_slice(&$packet); Ok((buf.len(), Some($addr))) } }; } } ================================================ FILE: crates/trippy-core/src/net/source.rs ================================================ use crate::PortDirection; use crate::error::Error::InvalidSourceAddr; use crate::error::Result; use crate::net::platform::Platform; use crate::net::socket::Socket; use crate::types::Port; use std::net::{IpAddr, SocketAddr}; /// The port used for local address discovery if not dest port is available. const DISCOVERY_PORT: Port = Port(80); /// Discover or validate a source address. pub struct SourceAddr; impl SourceAddr { /// Discover the source `IpAddr`. pub fn discover( target_addr: IpAddr, port_direction: PortDirection, interface: Option<&str>, ) -> Result { let port = port_direction.dest().unwrap_or(DISCOVERY_PORT).0; match interface.as_ref() { Some(interface) => P::lookup_interface_addr(target_addr, interface), None => P::discover_local_addr(target_addr, port), } } /// Validate that we can bind to the source `IpAddr`. pub fn validate(source_addr: IpAddr) -> Result { let mut socket = match source_addr { IpAddr::V4(_) => S::new_udp_dgram_socket_ipv4(), IpAddr::V6(_) => S::new_udp_dgram_socket_ipv6(), }?; let sock_addr = SocketAddr::new(source_addr, 0); match socket.bind(sock_addr) { Ok(()) => Ok(source_addr), Err(_) => Err(InvalidSourceAddr(sock_addr.ip())), } } } #[cfg(test)] mod tests { use super::*; use crate::error::IoError; use crate::net::platform::MockPlatform; use crate::net::socket::MockSocket; use mockall::predicate; use std::str::FromStr; use std::sync::Mutex; static MTX: Mutex<()> = Mutex::new(()); #[test] fn test_discover_local_addr_default_port() { let _m = MTX.lock(); let direction = PortDirection::None; let interface = None; let expected_target = IpAddr::from_str("1.2.3.4").unwrap(); let expected_port = DISCOVERY_PORT.0; let expected_src = IpAddr::from_str("192.168.0.1").unwrap(); let ctx = MockPlatform::discover_local_addr_context(); ctx.expect() .with(predicate::eq(expected_target), predicate::eq(expected_port)) .times(1) .returning(move |_, _| Ok(expected_src)); let src_addr = SourceAddr::discover::(expected_target, direction, interface) .unwrap(); assert_eq!(expected_src, src_addr); } #[test] fn test_discover_local_addr_fixed_dest_port() { let _m = MTX.lock(); let direction = PortDirection::FixedDest(Port(99)); let interface = None; let expected_target = IpAddr::from_str("1.2.3.4").unwrap(); let expected_port = 99; let expected_src = IpAddr::from_str("192.168.0.1").unwrap(); let ctx = MockPlatform::discover_local_addr_context(); ctx.expect() .with(predicate::eq(expected_target), predicate::eq(expected_port)) .times(1) .returning(move |_, _| Ok(expected_src)); let src_addr = SourceAddr::discover::(expected_target, direction, interface) .unwrap(); assert_eq!(expected_src, src_addr); } #[test] fn test_discover_local_addr_fixed_both_port() { let _m = MTX.lock(); let direction = PortDirection::FixedBoth(Port(1), Port(99)); let interface = None; let expected_target = IpAddr::from_str("1.2.3.4").unwrap(); let expected_port = 99; let expected_src = IpAddr::from_str("192.168.0.1").unwrap(); let ctx = MockPlatform::discover_local_addr_context(); ctx.expect() .with(predicate::eq(expected_target), predicate::eq(expected_port)) .times(1) .returning(move |_, _| Ok(expected_src)); let src_addr = SourceAddr::discover::(expected_target, direction, interface) .unwrap(); assert_eq!(expected_src, src_addr); } #[test] fn test_discover_lookup_interface() { let _m = MTX.lock(); let direction = PortDirection::None; let interface = Some("en0"); let expected_target = IpAddr::from_str("1.2.3.4").unwrap(); let expected_src = IpAddr::from_str("192.168.0.1").unwrap(); let expected_interface = "en0"; let ctx = MockPlatform::lookup_interface_addr_context(); ctx.expect() .with( predicate::eq(expected_target), predicate::eq(expected_interface), ) .times(1) .returning(move |_, _| Ok(expected_src)); let src_addr = SourceAddr::discover::(expected_target, direction, interface) .unwrap(); assert_eq!(expected_src, src_addr); } #[test] fn test_validate_ipv4() { let _m = MTX.lock(); let addr = IpAddr::from_str("192.168.0.1").unwrap(); let expected_bind_addr = SocketAddr::new(addr, 0); let ctx = MockSocket::new_udp_dgram_socket_ipv4_context(); ctx.expect().times(1).returning(move || { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|_| Ok(())); Ok(mocket) }); let src_addr = SourceAddr::validate::(addr).unwrap(); assert_eq!(addr, src_addr); } #[test] fn test_validate_ipv6() { let _m = MTX.lock(); let addr = IpAddr::from_str("2a00:1450:4009:815::200e").unwrap(); let expected_bind_addr = SocketAddr::new(addr, 0); let ctx = MockSocket::new_udp_dgram_socket_ipv6_context(); ctx.expect().times(1).returning(move || { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|_| Ok(())); Ok(mocket) }); let src_addr = SourceAddr::validate::(addr).unwrap(); assert_eq!(addr, src_addr); } #[test] fn test_validate_invalid() { let _m = MTX.lock(); let addr = IpAddr::from_str("1.2.3.4").unwrap(); let expected_bind_addr = SocketAddr::new(addr, 0); let ctx = MockSocket::new_udp_dgram_socket_ipv4_context(); ctx.expect().times(1).returning(move || { let mut mocket = MockSocket::new(); mocket .expect_bind() .with(predicate::eq(expected_bind_addr)) .times(1) .returning(|addr| Err(IoError::Bind(std::io::Error::last_os_error(), addr))); Ok(mocket) }); let err = SourceAddr::validate::(addr).unwrap_err(); assert!(matches!(err, InvalidSourceAddr(_))); } } ================================================ FILE: crates/trippy-core/src/net.rs ================================================ use crate::error::Result; use crate::probe::{Probe, Response}; /// Common types and helper functions. mod common; /// IPv4 implementation. mod ipv4; /// IPv6 implementation. mod ipv6; /// ICMP extensions. mod extension; /// Platform specific network code. mod platform; /// A network socket. mod socket; /// A channel for sending and receiving probes. pub mod channel; /// Determine the source address. pub mod source; /// The platform specific socket type. pub use platform::{PlatformImpl, SocketImpl}; /// An abstraction over a network interface for tracing. #[cfg_attr(test, mockall::automock)] pub trait Network { /// Send a `Probe`. fn send_probe(&mut self, probe: Probe) -> Result<()>; /// Receive the next Icmp packet and return a `ProbeResponse`. /// /// Returns `None` if the read times out or the packet read is not one of the types expected. fn recv_probe(&mut self) -> Result>; } ================================================ FILE: crates/trippy-core/src/probe.rs ================================================ use crate::TypeOfService; use crate::types::{Checksum, Flags, Port, RoundId, Sequence, TimeToLive, TraceId}; use std::net::IpAddr; use std::time::SystemTime; /// A network tracing probe. /// /// A `Probe` is a packet sent across the network to trace the path to a target host. /// It contains information such as sequence number, trace identifier, ports, and TTL. /// /// A probe is always in one of the following states: /// /// - `NotSent` - The probe has not been sent. /// - `Skipped` - The probe was skipped. /// - `Awaited` - The probe has been sent and is awaiting a response. /// - `Complete` - The probe has been sent and a response has been received. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum ProbeStatus { /// The probe has not been sent. #[default] NotSent, /// The probe was skipped. /// /// A probe may be skipped if, for TCP, it could not be bound to a local /// port. When a probe is skipped, it will be marked as `Skipped` and a /// new probe will be sent with the same TTL next available sequence number. Skipped, /// The probe has failed. /// /// A probe is considered failed when an error occurs while sending or /// receiving. Failed(ProbeFailed), /// The probe has been sent and is awaiting a response. /// /// If no response is received within the timeout, the probe will remain /// in this state indefinitely. Awaited(Probe), /// The probe has been sent and a response has been received. Complete(ProbeComplete), } /// An incomplete network tracing probe. /// /// A `Probe` is a packet sent across the network to trace the path to a target host. /// It contains information such as sequence number, trace identifier, ports, and TTL. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Probe { /// The sequence of the probe. pub sequence: Sequence, /// The trace identifier. pub identifier: TraceId, /// The source port (UDP/TCP only). pub src_port: Port, /// The destination port (UDP/TCP only). pub dest_port: Port, /// The TTL of the probe. pub ttl: TimeToLive, /// Which round the probe belongs to. pub round: RoundId, /// Timestamp when the probe was sent. pub sent: SystemTime, /// Probe flags. pub flags: Flags, } impl Probe { /// Create a new probe. #[must_use] #[expect(clippy::too_many_arguments)] pub(crate) const fn new( sequence: Sequence, identifier: TraceId, src_port: Port, dest_port: Port, ttl: TimeToLive, round: RoundId, sent: SystemTime, flags: Flags, ) -> Self { Self { sequence, identifier, src_port, dest_port, ttl, round, sent, flags, } } /// A response has been received and the probe is now complete. #[expect(clippy::too_many_arguments)] #[must_use] pub(crate) const fn complete( self, host: IpAddr, received: SystemTime, icmp_packet_type: IcmpPacketType, tos: Option, expected_udp_checksum: Option, actual_udp_checksum: Option, extensions: Option, ) -> ProbeComplete { ProbeComplete { sequence: self.sequence, identifier: self.identifier, src_port: self.src_port, dest_port: self.dest_port, ttl: self.ttl, round: self.round, sent: self.sent, host, received, icmp_packet_type, tos, expected_udp_checksum, actual_udp_checksum, extensions, } } /// The probe has failed to send. #[must_use] pub(crate) const fn failed(self) -> ProbeFailed { ProbeFailed { sequence: self.sequence, identifier: self.identifier, src_port: self.src_port, dest_port: self.dest_port, ttl: self.ttl, round: self.round, sent: self.sent, } } } /// A complete network tracing probe. /// /// A probe is considered complete when one of the following responses has been /// received: /// /// - `TimeExceeded` - an ICMP packet indicating the TTL has expired. /// - `EchoReply` - an ICMP packet indicating the probe has reached the target. /// - `DestinationUnreachable` - an ICMP packet indicating the probe could not reach the target. /// - `NotApplicable` - a non-ICMP response (i.e. for some `UDP` & `TCP` probes). #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProbeComplete { /// The sequence of the probe. pub sequence: Sequence, /// The trace identifier. pub identifier: TraceId, /// The source port (UDP/TCP only) pub src_port: Port, /// The destination port (UDP/TCP only) pub dest_port: Port, /// The TTL of the probe. pub ttl: TimeToLive, /// Which round the probe belongs to. pub round: RoundId, /// Timestamp when the probe was sent. pub sent: SystemTime, /// The host which responded to the probe. pub host: IpAddr, /// Timestamp when the response to the probe was received. pub received: SystemTime, /// The type of ICMP response packet received for the probe. pub icmp_packet_type: IcmpPacketType, /// The type of service (DSCP/ECN) of the original datagram. pub tos: Option, /// The expected UDP checksum of the original datagram. pub expected_udp_checksum: Option, /// The actual UDP checksum of the original datagram. pub actual_udp_checksum: Option, /// The ICMP response extensions. pub extensions: Option, } /// A failed network tracing probe. /// /// A probe is considered failed when an error occurs while sending or /// receiving. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ProbeFailed { /// The sequence of the probe. pub sequence: Sequence, /// The trace identifier. pub identifier: TraceId, /// The source port (UDP/TCP only) pub src_port: Port, /// The destination port (UDP/TCP only) pub dest_port: Port, /// The TTL of the probe. pub ttl: TimeToLive, /// Which round the probe belongs to. pub round: RoundId, /// Timestamp when the probe was sent. pub sent: SystemTime, } /// The type of ICMP packet received. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IcmpPacketType { /// `TimeExceeded` packet. TimeExceeded(IcmpPacketCode), /// `EchoReply` packet. EchoReply(IcmpPacketCode), /// Unreachable packet. Unreachable(IcmpPacketCode), /// Non-ICMP response (i.e. for some `UDP` & `TCP` probes). NotApplicable, } /// The code of `TimeExceeded`, `EchoReply` and `Unreachable` ICMP packets. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IcmpPacketCode(pub u8); /// The response to a probe. #[derive(Debug, Clone)] pub enum Response { TimeExceeded(ResponseData, IcmpPacketCode, Option), DestinationUnreachable(ResponseData, IcmpPacketCode, Option), EchoReply(ResponseData, IcmpPacketCode), TcpReply(ResponseData), TcpRefused(ResponseData), } impl Response { /// The data in the probe response. pub const fn data(&self) -> &ResponseData { match self { Self::TimeExceeded(data, _, _) | Self::DestinationUnreachable(data, _, _) | Self::EchoReply(data, _) | Self::TcpReply(data) | Self::TcpRefused(data) => data, } } } /// The ICMP extensions for a probe response. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Extensions { pub extensions: Vec, } /// A probe response extension. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Extension { Unknown(UnknownExtension), Mpls(MplsLabelStack), } impl Default for Extension { fn default() -> Self { Self::Unknown(UnknownExtension::default()) } } /// The members of a MPLS probe response extension. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct MplsLabelStack { pub members: Vec, } /// A member of a MPLS probe response extension. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct MplsLabelStackMember { pub label: u32, pub exp: u8, pub bos: u8, pub ttl: u8, } /// An unknown ICMP extension. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct UnknownExtension { pub class_num: u8, pub class_subtype: u8, pub bytes: Vec, } /// The data in the probe response. #[derive(Debug, Clone)] pub struct ResponseData { /// Timestamp of the probe response. pub recv: SystemTime, /// The `IpAddr` that responded to the probe. pub addr: IpAddr, /// Protocol specific response information. pub proto_resp: ProtocolResponse, } impl ResponseData { pub const fn new(recv: SystemTime, addr: IpAddr, proto_resp: ProtocolResponse) -> Self { Self { recv, addr, proto_resp, } } } /// Protocol specific response information. /// /// This includes protocol specific information that is used to: /// /// - determine the sequence number for matching the incoming probe response /// against the outgoing probe. /// - validate the probe response against the expected values and discard /// invalid responses. /// - record information from the probe Original Datagram such as the type of /// service (DSCP/ECN) and the expected UDP checksum. #[derive(Debug, Clone)] pub enum ProtocolResponse { Icmp(IcmpProtocolResponse), Udp(UdpProtocolResponse), Tcp(TcpProtocolResponse), } /// The data in the response to an ICMP probe. #[derive(Debug, Clone)] pub struct IcmpProtocolResponse { /// The ICMP identifier. pub identifier: u16, /// The ICMP sequence number. pub sequence: u16, /// The type of service (DSCP/ECN) of the original datagram. pub tos: Option, } impl IcmpProtocolResponse { pub const fn new(identifier: u16, sequence: u16, tos: Option) -> Self { Self { identifier, sequence, tos, } } } /// The data in the response to a UDP probe. #[derive(Debug, Clone)] pub struct UdpProtocolResponse { /// The IPv4 identifier. /// /// This will be the sequence number for IPv4/Dublin. pub identifier: u16, /// The destination IP address. /// /// This is used to validate the probe response matches the expected values. pub dest_addr: IpAddr, /// The source port. /// /// This is used to validate the probe response matches the expected values. pub src_port: u16, /// The destination port. /// /// This is used to validate the probe response matches the expected values. pub dest_port: u16, /// The type of service (DSCP/ECN) of the original datagram. pub tos: Option, /// The expected UDP checksum. /// /// This is calculated based on the data from the probe response and should /// match the checksum that in the probe that was sent. pub expected_udp_checksum: u16, /// The actual UDP checksum. /// /// This will contain the sequence number for IPv4 and IPv6 Paris. pub actual_udp_checksum: u16, /// The length of the UDP payload. /// /// This payload length will be the sequence number (offset from the /// initial sequence number) for IPv6 Dublin. Note that this length /// does not include the length of the MAGIC payload prefix. pub payload_len: u16, /// Whether the response had the MAGIC payload prefix. /// /// This will be true for IPv6 Dublin for probe responses which /// originated from the tracer and is used to validate the probe response. pub has_magic: bool, } impl UdpProtocolResponse { #[expect(clippy::too_many_arguments)] pub const fn new( identifier: u16, dest_addr: IpAddr, src_port: u16, dest_port: u16, tos: Option, expected_udp_checksum: u16, actual_udp_checksum: u16, payload_len: u16, has_magic: bool, ) -> Self { Self { identifier, dest_addr, src_port, dest_port, tos, expected_udp_checksum, actual_udp_checksum, payload_len, has_magic, } } } /// The data in the response to an TCP probe. #[derive(Debug, Clone)] pub struct TcpProtocolResponse { /// The destination IP address. /// /// This is used to validate the probe response matches the expected values. pub dest_addr: IpAddr, /// The source port. /// /// This is used to validate the probe response matches the expected values. pub src_port: u16, /// The destination port. /// /// This is used to validate the probe response matches the expected values. pub dest_port: u16, /// The type of service (DSCP/ECN) of the original datagram. pub tos: Option, } impl TcpProtocolResponse { pub const fn new( dest_addr: IpAddr, src_port: u16, dest_port: u16, tos: Option, ) -> Self { Self { dest_addr, src_port, dest_port, tos, } } } #[cfg(test)] impl ProbeStatus { #[must_use] pub fn try_into_awaited(self) -> Option { if let Self::Awaited(awaited) = self { Some(awaited) } else { None } } #[must_use] pub fn try_into_complete(self) -> Option { if let Self::Complete(complete) = self { Some(complete) } else { None } } } ================================================ FILE: crates/trippy-core/src/state.rs ================================================ use crate::config::StateConfig; use crate::constants::MAX_TTL; use crate::flows::{Flow, FlowId, FlowRegistry}; use crate::{ Dscp, Ecn, Extensions, IcmpPacketType, ProbeStatus, Round, RoundId, TimeToLive, TypeOfService, }; use indexmap::IndexMap; use std::collections::HashMap; use std::iter::once; use std::net::IpAddr; use std::time::Duration; use tracing::instrument; /// The state of a trace. #[derive(Debug, Clone, Default)] pub struct State { /// The configuration for the state. state_config: StateConfig, /// The flow id for the current round. round_flow_id: FlowId, /// Tracing state per registered flow id. state: HashMap, /// Flow registry. registry: FlowRegistry, /// Tracing error message. error: Option, } impl State { /// Create a new `State`. #[must_use] pub fn new(state_config: StateConfig) -> Self { Self { state: once(( Self::default_flow_id(), FlowState::new(state_config.max_samples), )) .collect::>(), round_flow_id: Self::default_flow_id(), state_config, registry: FlowRegistry::new(), error: None, } } /// Return the id of the default flow. #[must_use] pub const fn default_flow_id() -> FlowId { FlowId(0) } /// Information about each hop for the combined default flow. #[must_use] pub fn hops(&self) -> &[Hop] { self.state[&Self::default_flow_id()].hops() } /// Information about each hop for a given flow. #[must_use] pub fn hops_for_flow(&self, flow_id: FlowId) -> &[Hop] { self.state[&flow_id].hops() } /// Is a given `Hop` the target hop for a given flow? /// /// A `Hop` is considered to be the target if it has the highest `ttl` value observed. /// /// Note that if the target host does not respond to probes then the highest `ttl` observed /// will be one greater than the `ttl` of the last host which did respond. #[must_use] pub fn is_target(&self, hop: &Hop, flow_id: FlowId) -> bool { self.state[&flow_id].is_target(hop) } /// Is a given `Hop` in the current round for a given flow? #[must_use] pub fn is_in_round(&self, hop: &Hop, flow_id: FlowId) -> bool { self.state[&flow_id].is_in_round(hop) } /// Return the target `Hop` for a given flow. #[must_use] pub fn target_hop(&self, flow_id: FlowId) -> &Hop { self.state[&flow_id].target_hop() } /// The current round of tracing for a given flow. #[must_use] pub fn round(&self, flow_id: FlowId) -> Option { self.state[&flow_id].round() } /// The total rounds of tracing for a given flow. #[must_use] pub fn round_count(&self, flow_id: FlowId) -> usize { self.state[&flow_id].round_count() } /// The `FlowId` for the current round. #[must_use] pub const fn round_flow_id(&self) -> FlowId { self.round_flow_id } /// The registry of flows in the trace. #[must_use] pub fn flows(&self) -> &[(Flow, FlowId)] { self.registry.flows() } /// The error message for the trace, if any. #[must_use] pub fn error(&self) -> Option<&str> { self.error.as_deref() } pub fn set_error(&mut self, error: Option) { self.error = error; } /// The maximum number of samples to record per hop. #[must_use] pub const fn max_samples(&self) -> usize { self.state_config.max_samples } /// The maximum number of flows to record. #[must_use] pub const fn max_flows(&self) -> usize { self.state_config.max_flows } /// Update the tracing state from a `TracerRound`. #[instrument(skip(self, round), level = "trace")] pub fn update_from_round(&mut self, round: &Round<'_>) { let flow = Flow::from_hops( round .probes .iter() .filter_map(|probe| match probe { ProbeStatus::Awaited(_) => Some(None), ProbeStatus::Complete(completed) => Some(Some(completed.host)), _ => None, }) .take(usize::from(round.largest_ttl.0)), ); self.update_trace_flow(Self::default_flow_id(), round); if self.registry.flows().len() < self.state_config.max_flows { let flow_id = self.registry.register(flow); self.round_flow_id = flow_id; self.update_trace_flow(flow_id, round); } } #[instrument(skip(self, round), level = "trace")] fn update_trace_flow(&mut self, flow_id: FlowId, round: &Round<'_>) { let flow_trace = self .state .entry(flow_id) .or_insert_with(|| FlowState::new(self.state_config.max_samples)); flow_trace.update_from_round(round); } } /// Information about a single `Hop` within a `Trace`. #[derive(Debug, Clone)] pub struct Hop { /// The ttl of this hop. ttl: u8, /// The addrs of this hop and associated counts. addrs: IndexMap, /// The total probes sent for this hop. total_sent: usize, /// The total probes received for this hop. total_recv: usize, /// The total probes that failed for this hop. total_failed: usize, /// The total forward loss for this hop. total_forward_lost: usize, /// The total backward loss for this hop. total_backward_lost: usize, /// The total round trip time for this hop across all rounds. total_time: Duration, /// The round trip time for this hop in the current round. last: Option, /// The best round trip time for this hop across all rounds. best: Option, /// The worst round trip time for this hop across all rounds. worst: Option, /// The current jitter i.e. round-trip difference with the last round-trip. jitter: Option, /// The average jitter time for all probes at this hop. javg: f64, /// The worst round-trip jitter time for all probes at this hop. jmax: Option, /// The smoothed jitter value for all probes at this hop. jinta: f64, /// The source port for last probe for this hop. last_src_port: u16, /// The destination port for last probe for this hop. last_dest_port: u16, /// The sequence number for the last probe for this hop. last_sequence: u16, /// The icmp packet type for the last probe for this hop. last_icmp_packet_type: Option, /// The NAT detection status for the last probe for this hop. last_nat_status: NatStatus, /// The history of round trip times across the last N rounds. samples: Vec, /// The type of service (DSCP/ECN) for this hop. tos: Option, /// The ICMP extensions for this hop. extensions: Option, mean: f64, m2: f64, } impl Hop { /// The time-to-live of this hop. #[must_use] pub const fn ttl(&self) -> u8 { self.ttl } /// The set of addresses that have responded for this time-to-live. pub fn addrs(&self) -> impl Iterator { self.addrs.keys() } pub fn addrs_with_counts(&self) -> impl Iterator { self.addrs.iter() } /// The number of unique address observed for this time-to-live. #[must_use] pub fn addr_count(&self) -> usize { self.addrs.len() } /// The total number of probes sent. #[must_use] pub const fn total_sent(&self) -> usize { self.total_sent } /// The total number of probes responses received. #[must_use] pub const fn total_recv(&self) -> usize { self.total_recv } /// The total number of probes with forward loss. #[must_use] pub const fn total_forward_loss(&self) -> usize { self.total_forward_lost } /// The total number of probes with backward loss. #[must_use] pub const fn total_backward_loss(&self) -> usize { self.total_backward_lost } /// The total number of probes that failed. #[must_use] pub const fn total_failed(&self) -> usize { self.total_failed } /// The % of packets that are lost. #[must_use] pub fn loss_pct(&self) -> f64 { if self.total_sent > 0 { let lost = self.total_sent - self.total_recv; lost as f64 / self.total_sent as f64 * 100_f64 } else { 0_f64 } } /// The % of packets that are lost forward. #[must_use] pub fn forward_loss_pct(&self) -> f64 { if self.total_sent > 0 { let lost = self.total_forward_lost; lost as f64 / self.total_sent as f64 * 100_f64 } else { 0_f64 } } /// The % of packets that are lost backward. #[must_use] pub fn backward_loss_pct(&self) -> f64 { if self.total_sent > 0 { let lost = self.total_backward_lost; lost as f64 / self.total_sent as f64 * 100_f64 } else { 0_f64 } } /// The duration of the last probe. #[must_use] pub fn last_ms(&self) -> Option { self.last.map(|last| last.as_secs_f64() * 1000_f64) } /// The duration of the best probe observed. #[must_use] pub fn best_ms(&self) -> Option { self.best.map(|last| last.as_secs_f64() * 1000_f64) } /// The duration of the worst probe observed. #[must_use] pub fn worst_ms(&self) -> Option { self.worst.map(|last| last.as_secs_f64() * 1000_f64) } /// The average duration of all probes. #[must_use] pub fn avg_ms(&self) -> f64 { if self.total_recv() > 0 { (self.total_time.as_secs_f64() * 1000_f64) / self.total_recv as f64 } else { 0_f64 } } /// The standard deviation of all probes. #[must_use] pub fn stddev_ms(&self) -> f64 { if self.total_recv > 1 { (self.m2 / (self.total_recv - 1) as f64).sqrt() } else { 0_f64 } } /// The duration of the jitter probe observed. #[must_use] pub fn jitter_ms(&self) -> Option { self.jitter.map(|j| j.as_secs_f64() * 1000_f64) } /// The duration of the worst probe observed. #[must_use] pub fn jmax_ms(&self) -> Option { self.jmax.map(|x| x.as_secs_f64() * 1000_f64) } /// The jitter average duration of all probes. #[must_use] pub const fn javg_ms(&self) -> f64 { self.javg } /// The jitter interval of all probes. #[must_use] pub const fn jinta(&self) -> f64 { self.jinta } /// The source port for last probe for this hop. #[must_use] pub const fn last_src_port(&self) -> u16 { self.last_src_port } /// The destination port for last probe for this hop. #[must_use] pub const fn last_dest_port(&self) -> u16 { self.last_dest_port } /// The sequence number for the last probe for this hop. #[must_use] pub const fn last_sequence(&self) -> u16 { self.last_sequence } /// The icmp packet type for the last probe for this hop. #[must_use] pub const fn last_icmp_packet_type(&self) -> Option { self.last_icmp_packet_type } /// The NAT detection status for the last probe for this hop. #[must_use] pub const fn last_nat_status(&self) -> NatStatus { self.last_nat_status } /// The type of service (DSCP/ECN) for this hop. #[must_use] pub fn tos(&self) -> Option { self.tos } /// The `DSCP` for this hop. #[must_use] pub fn dscp(&self) -> Option { self.tos.map(|tos| tos.dscp()) } /// The `ECN` for this hop. #[must_use] pub fn ecn(&self) -> Option { self.tos.map(|tos| tos.ecn()) } /// The last N samples. #[must_use] pub fn samples(&self) -> &[Duration] { &self.samples } #[must_use] pub const fn extensions(&self) -> Option<&Extensions> { self.extensions.as_ref() } } impl Default for Hop { fn default() -> Self { Self { ttl: 0, addrs: IndexMap::default(), total_sent: 0, total_recv: 0, total_forward_lost: 0, total_backward_lost: 0, total_failed: 0, total_time: Duration::default(), last: None, best: None, worst: None, jitter: None, javg: 0f64, jmax: None, jinta: 0f64, last_src_port: 0_u16, last_dest_port: 0_u16, last_sequence: 0_u16, last_icmp_packet_type: None, mean: 0f64, m2: 0f64, samples: Vec::default(), tos: None, extensions: None, last_nat_status: NatStatus::NotApplicable, } } } /// The state of a NAT detection for a `Hop`. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum NatStatus { /// NAT detection was not applicable. NotApplicable, /// NAT was not detected at this hop. NotDetected, /// NAT was detected at this hop. Detected, } /// Data for a single trace flow. #[derive(Debug, Clone)] struct FlowState { /// The maximum number of samples to record. max_samples: usize, /// The lowest ttl observed across all rounds. lowest_ttl: u8, /// The highest ttl observed across all rounds. highest_ttl: u8, /// The highest ttl observed for the latest round. highest_ttl_for_round: u8, /// The latest round received. round: Option, /// The total number of rounds received. round_count: usize, /// The hops in this trace. hops: Vec, } impl FlowState { fn new(max_samples: usize) -> Self { Self { max_samples, lowest_ttl: 0, highest_ttl: 0, highest_ttl_for_round: 0, round: None, round_count: 0, hops: (0..MAX_TTL).map(|_| Hop::default()).collect(), } } fn hops(&self) -> &[Hop] { if self.lowest_ttl == 0 || self.highest_ttl == 0 { &[] } else { let start = (self.lowest_ttl as usize) - 1; let end = self.highest_ttl as usize; &self.hops[start..end] } } const fn is_target(&self, hop: &Hop) -> bool { self.highest_ttl_for_round == hop.ttl } const fn is_in_round(&self, hop: &Hop) -> bool { hop.ttl <= self.highest_ttl_for_round } fn target_hop(&self) -> &Hop { if self.highest_ttl_for_round > 0 { &self.hops[usize::from(self.highest_ttl_for_round) - 1] } else { &self.hops[0] } } const fn round(&self) -> Option { self.round } const fn round_count(&self) -> usize { self.round_count } fn update_from_round(&mut self, round: &Round<'_>) { state_updater::StateUpdater::new(self, round).apply(); } fn update_round(&mut self, round: RoundId) { self.round = match self.round { None => Some(round.0), Some(r) => Some(r.max(round.0)), } } fn update_lowest_ttl(&mut self, ttl: TimeToLive) { if self.lowest_ttl == 0 { self.lowest_ttl = ttl.0; } else { self.lowest_ttl = self.lowest_ttl.min(ttl.0); } } } mod state_updater { use crate::state::FlowState; use crate::types::Checksum; use crate::{NatStatus, ProbeStatus, Round, TimeToLive}; use std::time::Duration; use tracing::instrument; /// Update the state of a `FlowState` from a `Round`. pub(super) struct StateUpdater<'a> { /// The state to update. state: &'a mut FlowState, /// The `Round` being processed. round: &'a Round<'a>, /// The checksum of the previous hop, if any. prev_hop_checksum: Option, /// Whether any previous hop in the round had forward loss. forward_loss: bool, } impl<'a> StateUpdater<'a> { pub(super) fn new(state: &'a mut FlowState, round: &'a Round<'_>) -> Self { Self { state, round, prev_hop_checksum: None, forward_loss: false, } } #[instrument(skip(self), level = "trace")] pub(super) fn apply(&mut self) { self.state.round_count += 1; self.state.highest_ttl = std::cmp::max(self.state.highest_ttl, self.round.largest_ttl.0); self.state.highest_ttl_for_round = self.round.largest_ttl.0; for probe in self.round.probes { self.update_for_probe(probe); } } #[instrument(skip(self), level = "trace")] fn update_for_probe(&mut self, probe: &ProbeStatus) { let state = &mut *self.state; match probe { ProbeStatus::Complete(complete) => { state.update_lowest_ttl(complete.ttl); state.update_round(complete.round); let index = usize::from(complete.ttl.0) - 1; let hop = &mut state.hops[index]; hop.ttl = complete.ttl.0; hop.total_sent += 1; hop.total_recv += 1; let dur = complete .received .duration_since(complete.sent) .unwrap_or_default(); let dur_ms = dur.as_secs_f64() * 1000_f64; hop.total_time += dur; // Before last is set use it to calc jitter let last_ms = hop.last_ms().unwrap_or_default(); let jitter_ms = (dur_ms - last_ms).abs(); let jitter_dur = Duration::from_secs_f64(jitter_ms / 1000_f64); hop.jitter = hop.last.and(Some(jitter_dur)); hop.javg += (jitter_ms - hop.javg) / hop.total_recv as f64; // algorithm is from rfc1889, A.8 or rfc3550 hop.jinta += jitter_ms.max(0.5) - ((hop.jinta + 8.0) / 16.0); hop.jmax = hop .jmax .map_or(Some(jitter_dur), |d| Some(d.max(jitter_dur))); hop.last = Some(dur); hop.samples.insert(0, dur); hop.best = hop.best.map_or(Some(dur), |d| Some(d.min(dur))); hop.worst = hop.worst.map_or(Some(dur), |d| Some(d.max(dur))); hop.mean += (dur_ms - hop.mean) / hop.total_recv as f64; hop.m2 += (dur_ms - hop.mean) * (dur_ms - hop.mean); if hop.samples.len() > state.max_samples { hop.samples.pop(); } let host = complete.host; *hop.addrs.entry(host).or_default() += 1; hop.extensions.clone_from(&complete.extensions); hop.last_src_port = complete.src_port.0; hop.last_dest_port = complete.dest_port.0; hop.last_sequence = complete.sequence.0; hop.last_icmp_packet_type = Some(complete.icmp_packet_type); hop.tos = complete.tos; if let (Some(expected), Some(actual)) = (complete.expected_udp_checksum, complete.actual_udp_checksum) { let (nat_status, checksum) = nat_status(expected, actual, self.prev_hop_checksum); hop.last_nat_status = nat_status; self.prev_hop_checksum = Some(checksum); } } ProbeStatus::Awaited(awaited) => { state.update_lowest_ttl(awaited.ttl); state.update_round(awaited.round); let index = usize::from(awaited.ttl.0) - 1; let hop = &mut state.hops[index]; hop.total_sent += 1; hop.ttl = awaited.ttl.0; hop.samples.insert(0, Duration::default()); if hop.samples.len() > state.max_samples { hop.samples.pop(); } hop.last_src_port = awaited.src_port.0; hop.last_dest_port = awaited.dest_port.0; hop.last_sequence = awaited.sequence.0; if self.forward_loss { hop.total_backward_lost += 1; } else if is_forward_loss(self.round.probes, awaited.ttl) { hop.total_forward_lost += 1; self.forward_loss = true; } } ProbeStatus::Failed(failed) => { state.update_lowest_ttl(failed.ttl); state.update_round(failed.round); let index = usize::from(failed.ttl.0) - 1; let hop = &mut state.hops[index]; hop.total_sent += 1; hop.total_failed += 1; hop.ttl = failed.ttl.0; hop.samples.insert(0, Duration::default()); if hop.samples.len() > state.max_samples { hop.samples.pop(); } hop.last_src_port = failed.src_port.0; hop.last_dest_port = failed.dest_port.0; hop.last_sequence = failed.sequence.0; } ProbeStatus::NotSent | ProbeStatus::Skipped => {} } } } /// Determine if forward loss has occurred at a given time-to-live. /// /// This is determined by checking if all probes after the awaited probe are all also awaited. fn is_forward_loss(probes: &[ProbeStatus], awaited_ttl: TimeToLive) -> bool { // Skip all probes that have a ttl less than or equal to the awaited ttl. What remains // are the probes we are interested in. let mut remaining = probes .iter() .skip_while(|p| match p { ProbeStatus::Awaited(a) => a.ttl <= awaited_ttl, ProbeStatus::Complete(c) => c.ttl <= awaited_ttl, ProbeStatus::Failed(f) => f.ttl <= awaited_ttl, ProbeStatus::NotSent | ProbeStatus::Skipped => true, }) .peekable(); let is_empty = remaining.peek().is_none(); let all_awaited = remaining.all(|p| matches!(p, ProbeStatus::Awaited(_) | ProbeStatus::Skipped)); // If there is at least one probe remaining and all are awaited then we have forward loss. !is_empty && all_awaited } /// Determine the NAT detection status. /// /// Returns a tuple of the NAT detection status and the checksum to use for the next hop. const fn nat_status( expected: Checksum, actual: Checksum, prev_hop_checksum: Option, ) -> (NatStatus, u16) { if let Some(prev_hop_checksum) = prev_hop_checksum { // If the actual checksum matches the checksum of the previous probe // then we can assume NAT has not occurred. Note that it is perfectly // valid for the expected checksum to differ from the actual checksum // in this case as the NAT'ed checksum "carries forward" throughout the // remainder of the hops on the path. if prev_hop_checksum == actual.0 { (NatStatus::NotDetected, prev_hop_checksum) } else { (NatStatus::Detected, actual.0) } } else { // If we have no prior checksum (i.e. this is the first probe that // responded) and the expected and actual checksums do not match then // we can assume NAT has occurred. if expected.0 == actual.0 { (NatStatus::NotDetected, actual.0) } else { (NatStatus::Detected, actual.0) } } } #[cfg(test)] mod tests { use super::*; use crate::probe::ProbeFailed; use crate::{ Flags, IcmpPacketType, Port, Probe, ProbeComplete, RoundId, Sequence, TimeToLive, TraceId, }; use std::net::{IpAddr, Ipv4Addr}; use std::time::SystemTime; use test_case::test_case; #[test_case(false, &[], 1; "no forward loss no probes ttl 1")] #[test_case(true, &[('a', 1), ('a', 2)], 1; "forward loss AA ttl 1")] #[test_case(false, &[('a', 1), ('c', 2)], 1; "no forward loss AC ttl 1")] #[test_case(false, &[('a', 1), ('f', 2)], 1; "no forward loss AF ttl 1")] #[test_case(false, &[('a', 1), ('n', 2)], 1; "no forward loss AN ttl 1")] #[test_case(false, &[('a', 1), ('c', 2), ('a', 3), ('a', 4)], 1; "no forward loss ACAA ttl 1")] #[test_case(true, &[('a', 1), ('c', 2), ('a', 3), ('a', 4)], 3; "forward loss ACAA ttl 3")] #[test_case(false, &[('a', 1), ('c', 2), ('a', 3), ('a', 4)], 4; "no forward loss ACAA ttl 4")] #[test_case(false, &[('a', 1), ('f', 2), ('n', 3), ('a', 4)], 4; "no forward loss AFAN ttl 1")] #[test_case(true, &[('a', 4), ('a', 5)], 4; "forward loss AA non-default minimum ttl 4")] #[test_case(false, &[('a', 4), ('c', 5)], 4; "no forward loss AC non-default minimum ttl 4")] #[test_case(false, &[('a', 4), ('c', 5), ('a', 6), ('a', 7)], 4; "no forward loss ACAA non-default minimum ttl 4")] #[test_case(true, &[('a', 4), ('c', 5), ('a', 6), ('a', 7)], 6; "forward loss ACAA non-default minimum ttl 6")] fn test_is_forward_loss(expected: bool, probes: &[(char, u8)], awaited_ttl: u8) { assert!(awaited_ttl > 0); let probes = probes .iter() .map(|(typ, ttl)| { assert!(matches!(typ, 'n' | 's' | 'f' | 'a' | 'c')); if *ttl == awaited_ttl { assert!(matches!(typ, 'a')); } match typ { 'n' => ProbeStatus::NotSent, 's' => ProbeStatus::Skipped, 'f' => ProbeStatus::Failed(ProbeFailed { sequence: Sequence::default(), identifier: TraceId::default(), src_port: Port::default(), dest_port: Port::default(), ttl: TimeToLive(*ttl), round: RoundId::default(), sent: SystemTime::now(), }), 'a' => ProbeStatus::Awaited(Probe { sequence: Sequence::default(), identifier: TraceId::default(), src_port: Port::default(), dest_port: Port::default(), ttl: TimeToLive(*ttl), round: RoundId(0), sent: SystemTime::now(), flags: Flags::empty(), }), 'c' => ProbeStatus::Complete(ProbeComplete { sequence: Sequence::default(), identifier: TraceId::default(), src_port: Port::default(), dest_port: Port::default(), ttl: TimeToLive(*ttl), round: RoundId::default(), sent: SystemTime::now(), host: IpAddr::V4(Ipv4Addr::UNSPECIFIED), received: SystemTime::now(), icmp_packet_type: IcmpPacketType::NotApplicable, tos: None, expected_udp_checksum: None, actual_udp_checksum: None, extensions: None, }), _ => unreachable!(), } }) .collect::>(); assert_eq!(is_forward_loss(&probes, TimeToLive(awaited_ttl)), expected); } #[test_case(123, 123, None => (NatStatus::NotDetected, 123); "first hop matching checksum")] #[test_case(123, 321, None => (NatStatus::Detected, 321); "first hop non-matching checksum")] #[test_case(123, 123, Some(123) => (NatStatus::NotDetected, 123); "non-first hop matching checksum match previous")] #[test_case(999, 999, Some(321) => (NatStatus::Detected, 999); "non-first hop matching checksum not match previous")] #[test_case(777, 888, Some(321) => (NatStatus::Detected, 888); "non-first hop non-matching checksum not match previous")] const fn test_nat(expected: u16, actual: u16, prev: Option) -> (NatStatus, u16) { nat_status(Checksum(expected), Checksum(actual), prev) } } } #[cfg(test)] mod tests { use super::*; use crate::types::Checksum; use crate::{ CompletionReason, Flags, IcmpPacketType, Port, Probe, ProbeComplete, ProbeStatus, Sequence, TimeToLive, TraceId, TypeOfService, }; use anyhow::anyhow; use serde::Deserialize; use std::collections::HashSet; use std::fmt::Debug; use std::ops::Add; use std::str::FromStr; use std::time::SystemTime; use test_case::test_case; /// A test scenario. #[derive(Deserialize, Debug)] #[serde(deny_unknown_fields)] struct Scenario { /// the biggest ttl expected in this scenario largest_ttl: u8, /// The rounds of probe tracing data in this scenario. rounds: Vec, /// The expected outcome from running this scenario. expected: Expected, } /// A single round of tracing probe data. #[derive(Deserialize, Debug)] #[serde(deny_unknown_fields)] struct RoundData { /// The probes in this round. probes: Vec, } /// A single probe from a single round. #[derive(Deserialize, Debug)] #[serde(deny_unknown_fields)] #[serde(try_from = "String")] struct ProbeData(ProbeStatus); impl TryFrom for ProbeData { type Error = anyhow::Error; fn try_from(value: String) -> Result { // format: `{ttl} {status} {duration} {host} {sequence} {src_port} {dest_port} {checksum} {tos}` let values = value.split_ascii_whitespace().collect::>(); if values.len() == 10 { let ttl = TimeToLive(u8::from_str(values[0])?); let state = values[1].to_ascii_lowercase(); let sequence = Sequence(u16::from_str(values[4])?); let src_port = Port(u16::from_str(values[5])?); let dest_port = Port(u16::from_str(values[6])?); let round = RoundId(0); // note we inject this later, see `ProbeRound` let sent = SystemTime::now(); let flags = Flags::empty(); let state = match state.as_str() { "n" => Ok(ProbeStatus::NotSent), "s" => Ok(ProbeStatus::Skipped), "a" => Ok(ProbeStatus::Awaited(Probe::new( sequence, TraceId(0), src_port, dest_port, ttl, round, sent, flags, ))), "c" => { let host = IpAddr::from_str(values[3])?; let duration = Duration::from_millis(u64::from_str(values[2])?); let received = sent.add(duration); let expected_udp_checksum = Some(Checksum(u16::from_str(values[7])?)); let actual_udp_checksum = Some(Checksum(u16::from_str(values[8])?)); let icmp_packet_type = IcmpPacketType::NotApplicable; let tos = Some(TypeOfService(u8::from_str(values[9])?)); Ok(ProbeStatus::Complete( Probe::new( sequence, TraceId(0), src_port, dest_port, ttl, round, sent, flags, ) .complete( host, received, icmp_packet_type, tos, expected_udp_checksum, actual_udp_checksum, None, ), )) } _ => Err(anyhow!("unknown probe state")), }?; Ok(Self(state)) } else { Err(anyhow!("failed to parse {value}")) } } } /// A helper struct so we may inject the round into the probes. struct ProbeRound(ProbeData, RoundId); impl From for ProbeStatus { fn from(value: ProbeRound) -> Self { let probe_data = value.0; let round = value.1; match probe_data.0 { Self::NotSent => Self::NotSent, Self::Skipped => Self::Skipped, Self::Awaited(awaited) => Self::Awaited(Probe { round, ..awaited }), Self::Complete(completed) => Self::Complete(ProbeComplete { round, ..completed }), Self::Failed(failed) => Self::Failed(failed), } } } /// The expected outcome. #[derive(Deserialize, Debug, Clone)] #[serde(deny_unknown_fields)] struct Expected { /// The expected outcome per hop. hops: Vec, } /// The expected outcome for a single hop. #[derive(Deserialize, Debug, Clone)] #[serde(deny_unknown_fields)] struct HopData { ttl: Option, total_sent: Option, total_recv: Option, total_forward_loss: Option, total_backward_loss: Option, loss_pct: Option, last_ms: Option, best_ms: Option, worst_ms: Option, avg_ms: Option, jitter: Option, javg: Option, jmax: Option, jinta: Option, addrs: Option>, samples: Option>, last_src: Option, last_dest: Option, last_sequence: Option, last_nat_status: Option, tos: Option, } /// A wrapper struct over `NatStatus` to allow deserialization. #[derive(Deserialize, Debug, Clone)] #[serde(try_from = "String")] struct NatStatusWrapper(NatStatus); impl TryFrom for NatStatusWrapper { type Error = anyhow::Error; fn try_from(value: String) -> Result { match value.to_ascii_lowercase().as_str() { "none" => Ok(Self(NatStatus::NotApplicable)), "nat" => Ok(Self(NatStatus::Detected)), "no_nat" => Ok(Self(NatStatus::NotDetected)), _ => Err(anyhow!("unknown nat status")), } } } macro_rules! file { ($path:expr) => {{ let data = include_str!(concat!("../tests/resources/state/", $path)); toml::from_str(data).unwrap() }}; } #[test_case(file!("full_mixed.toml"))] #[test_case(file!("full_completed.toml"))] #[test_case(file!("all_status.toml"))] #[test_case(file!("no_latency.toml"))] #[test_case(file!("nat.toml"))] #[test_case(file!("minimal.toml"))] #[test_case(file!("floss_bloss.toml"))] #[test_case(file!("non_default_minimum_ttl.toml"))] #[test_case(file!("tos.toml"))] fn test_scenario(scenario: Scenario) { let mut trace = State::new(StateConfig { max_flows: 1, ..StateConfig::default() }); for (i, round) in scenario.rounds.into_iter().enumerate() { let probes = round .probes .into_iter() .map(|p| ProbeRound(p, RoundId(i))) .map(Into::into) .collect::>(); let largest_ttl = TimeToLive(scenario.largest_ttl); let tracer_round = Round::new(&probes, largest_ttl, CompletionReason::TargetFound); trace.update_from_round(&tracer_round); } let actual_hops = trace.hops(); let expected_hops = scenario.expected.hops; for (actual, expected) in actual_hops.iter().zip(expected_hops) { assert_eq_opt(Some(&actual.ttl()), expected.ttl.as_ref()); assert_eq_opt( Some(actual.addrs().collect::>()), expected .addrs .as_ref() .map(|addrs| addrs.keys().collect::>()), ); assert_eq_opt( Some(actual.addr_count()), expected.addrs.as_ref().map(HashMap::len), ); assert_eq_opt(Some(&actual.total_sent()), expected.total_sent.as_ref()); assert_eq_opt(Some(&actual.total_recv()), expected.total_recv.as_ref()); assert_eq_opt_f64(Some(&actual.loss_pct()), expected.loss_pct.as_ref()); assert_eq_opt( Some(&actual.total_forward_loss()), expected.total_forward_loss.as_ref(), ); assert_eq_opt( Some(&actual.total_backward_loss()), expected.total_backward_loss.as_ref(), ); assert_eq_opt_f64(actual.last_ms().as_ref(), expected.last_ms.as_ref()); assert_eq_opt_f64(actual.best_ms().as_ref(), expected.best_ms.as_ref()); assert_eq_opt_f64(actual.worst_ms().as_ref(), expected.worst_ms.as_ref()); assert_eq_opt_f64(Some(&actual.avg_ms()), expected.avg_ms.as_ref()); assert_eq_opt_f64(actual.jitter_ms().as_ref(), expected.jitter.as_ref()); assert_eq_opt_f64(Some(&actual.javg_ms()), expected.javg.as_ref()); assert_eq_opt_f64(actual.jmax_ms().as_ref(), expected.jmax.as_ref()); assert_eq_opt_f64(Some(&actual.jinta()), expected.jinta.as_ref()); assert_eq_opt(Some(&actual.last_src_port()), expected.last_src.as_ref()); assert_eq_opt(Some(&actual.last_dest_port()), expected.last_dest.as_ref()); assert_eq_opt( Some(&actual.last_sequence()), expected.last_sequence.as_ref(), ); assert_eq_opt( Some(&actual.last_nat_status()), expected.last_nat_status.as_ref().map(|nat| &nat.0), ); assert_eq_vec_f64( Some( &actual .samples() .iter() .map(|s| s.as_secs_f64() * 1000_f64) .collect(), ), expected.samples.as_ref(), ); assert_eq_opt(actual.tos().map(|tos| tos.0), expected.tos); } } #[expect(clippy::needless_pass_by_value)] fn assert_eq_opt(actual: Option, expected: Option) { assert_eq_inner(actual.as_ref(), expected.as_ref(), |a, e| a == e); } fn assert_eq_opt_f64(actual: Option<&f64>, expected: Option<&f64>) { assert_eq_inner(actual, expected, |a, e| (e - a).abs() < f64::EPSILON); } fn assert_eq_vec_f64(actual: Option<&Vec>, expected: Option<&Vec>) { assert_eq_inner(actual, expected, |a, e| { if a.len() != e.len() { return false; } a.iter() .zip(e.iter()) .all(|(a, e)| (e - a).abs() < f64::EPSILON) }); } fn assert_eq_inner( actual: Option<&T>, expected: Option<&T>, eq: impl Fn(&T, &T) -> bool, ) { match (actual, expected) { (Some(actual), Some(expected)) if eq(actual, expected) => {} (Some(actual), Some(expected)) => { panic!("expected {expected:?} did not match actual {actual:?}") } (None, Some(_)) => panic!("expected {expected:?} but no actual"), (_, None) => {} } } } ================================================ FILE: crates/trippy-core/src/strategy.rs ================================================ use self::state::TracerState; use crate::config::StrategyConfig; use crate::error::{Error, Result}; use crate::net::Network; use crate::probe::{ IcmpProtocolResponse, ProbeStatus, ProtocolResponse, Response, ResponseData, TcpProtocolResponse, UdpProtocolResponse, }; use crate::types::{Checksum, Sequence, TimeToLive, TraceId}; use crate::{ Extensions, IcmpPacketType, MultipathStrategy, PortDirection, Probe, Protocol, TypeOfService, }; use std::net::IpAddr; use std::time::{Duration, SystemTime}; use tracing::instrument; /// The output from a round of tracing. #[derive(Debug, Clone)] pub struct Round<'a> { /// The state of all `ProbeStatus` that were sent in the round. pub probes: &'a [ProbeStatus], /// The largest time-to-live (ttl) for which we received a reply in the round. pub largest_ttl: TimeToLive, /// Indicates what triggered the completion of the tracing round. pub reason: CompletionReason, } impl<'a> Round<'a> { #[must_use] pub const fn new( probes: &'a [ProbeStatus], largest_ttl: TimeToLive, reason: CompletionReason, ) -> Self { Self { probes, largest_ttl, reason, } } } /// Indicates what triggered the completion of the tracing round. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum CompletionReason { /// The round ended because the target was found. TargetFound, /// The round ended because the time exceeded the configured maximum round time. RoundTimeLimitExceeded, } /// Trace a path to a target. #[derive(Debug, Clone)] pub struct Strategy { config: StrategyConfig, publish: F, } impl)> Strategy { #[instrument(skip_all, level = "trace")] pub fn new(config: &StrategyConfig, publish: F) -> Self { tracing::debug!(?config); Self { config: *config, publish, } } /// Run a continuous trace and publish results. #[instrument(skip(self, network), level = "trace")] pub fn run(self, mut network: N) -> Result<()> { let mut state = TracerState::new(self.config); while !state.finished(self.config.max_rounds) { self.send_request(&mut network, &mut state)?; self.recv_response(&mut network, &mut state)?; self.update_round(&mut state); } Ok(()) } /// Send the next probe if required. /// /// Send a `ProbeStatus` for the next time-to-live (ttl) if all the following are true: /// /// 1 - the target host has not been found /// 2 - the next ttl is not greater than the maximum allowed ttl /// 3 - if the target ttl of the target is known: /// - the next ttl is not greater than the ttl of the target host observed from the prior /// round /// otherwise: /// - the number of unknown-in-flight probes is lower than the maximum allowed fn send_request(&self, network: &mut N, st: &mut TracerState) -> Result<()> { let can_send_ttl = if let Some(target_ttl) = st.target_ttl() { st.ttl() <= target_ttl } else { st.ttl() - st.max_received_ttl().unwrap_or_default() < TimeToLive(self.config.max_inflight.0) }; if !st.target_found() && st.ttl() <= self.config.max_ttl && can_send_ttl { let sent = SystemTime::now(); match self.config.protocol { Protocol::Icmp | Protocol::Udp => { let probe = st.next_probe(sent); Self::do_send(network, st, probe)?; } Protocol::Tcp => { let mut probe = if st.round_has_capacity() { st.next_probe(sent) } else { return Err(Error::InsufficientCapacity); }; while let Err(err) = Self::do_send(network, st, probe) { match err { Error::AddressInUse(_) => { if st.round_has_capacity() { probe = st.reissue_probe(SystemTime::now()); } else { return Err(Error::InsufficientCapacity); } } other => return Err(other), } } } } } Ok(()) } /// Send the probe and handle errors. /// /// Some errors are transient and should not be considered fatal. In these cases we mark the /// probe as failed and continue. #[instrument(skip(network, st), level = "trace")] fn do_send(network: &mut N, st: &mut TracerState, probe: Probe) -> Result<()> { match network.send_probe(probe) { Ok(()) => Ok(()), Err(Error::ProbeFailed(_)) => { st.fail_probe(); Ok(()) } Err(err) => Err(err), } } /// Read and process the next incoming `ICMP` packet. /// /// We allow multiple probes to be in-flight at any time, and we cannot guarantee that responses /// will be received in-order. We therefore maintain a buffer which holds details of each /// `ProbeStatus` which is indexed by the offset of the sequence number from the sequence number /// at the beginning of the round. The sequence number is set in the outgoing `ICMP` /// `EchoRequest` (or `UDP` / `TCP`) packet and returned in both the `TimeExceeded` and /// `EchoReply` responses. /// /// Each incoming `ICMP` packet contains the original `ICMP` `EchoRequest` packet from which we /// can read the `identifier` that we set which we can now validate to ensure we only /// process responses which correspond to packets sent from this process. For The `UDP` and /// `TCP` protocols, only packets destined for our src port will be delivered to us by the /// OS and so no other `identifier` is needed, and so we allow the special case value of 0. /// /// When we process an `EchoReply` from the target host we extract the time-to-live from the /// corresponding original `EchoRequest`. Note that this may not be the greatest /// time-to-live that was sent in the round as the algorithm will send `EchoRequest` with /// larger time-to-live values before the `EchoReply` is received. fn recv_response(&self, network: &mut N, st: &mut TracerState) -> Result<()> { let next = network.recv_probe()?; if let Some(resp) = next { if self.validate(resp.data()) { let resp = StrategyResponse::from((resp, &self.config)); if self.check_trace_id(resp.trace_id) && st.in_round(resp.sequence) { st.complete_probe(resp); } } } Ok(()) } /// Check if the round is complete and publish the results. /// /// A round is considered to be complete when: /// /// 1 - the round has exceeded the minimum round duration AND /// 2 - the duration since the last packet was received exceeds the grace period AND /// 3 - either: /// A - the target has been found OR /// B - the target has not been found and the round has exceeded the maximum round duration fn update_round(&self, st: &mut TracerState) { let now = SystemTime::now(); let round_duration = now.duration_since(st.round_start()).unwrap_or_default(); let round_min = round_duration > self.config.min_round_duration; let grace_exceeded = exceeds(st.received_time(), now, self.config.grace_duration); let round_max = round_duration > self.config.max_round_duration; let target_found = st.target_found(); if round_min && grace_exceeded && target_found || round_max { self.publish_trace(st); st.advance_round(self.config.first_ttl); } } /// Publish details of all `ProbeStatus` in the completed round. /// /// If the round completed without receiving an `EchoReply` from the target host then we also /// publish the next `ProbeStatus` which is assumed to represent the TTL of the target host. #[instrument(skip(self, state), level = "trace")] fn publish_trace(&self, state: &TracerState) { let max_received_ttl = if let Some(target_ttl) = state.target_ttl() { target_ttl } else { state .max_received_ttl() .map_or(TimeToLive(0), |max_received_ttl| { let max_sent_ttl = state.ttl() - TimeToLive(1); max_sent_ttl.min(max_received_ttl + TimeToLive(1)) }) }; let probes = state.probes(); let largest_ttl = max_received_ttl; let reason = if state.target_found() { CompletionReason::TargetFound } else { CompletionReason::RoundTimeLimitExceeded }; (self.publish)(&Round::new(probes, largest_ttl, reason)); } /// Check if the `TraceId` matches the expected value for this tracer. /// /// A special value of `0` is accepted for `udp` and `tcp` which do not have an identifier. #[instrument(skip(self), level = "trace")] fn check_trace_id(&self, trace_id: TraceId) -> bool { self.config.trace_identifier == trace_id || trace_id == TraceId(0) } /// Validate the probe response data. /// /// Carries out specific check for UDP/TCP probe responses. This is /// required as the network layer may receive incoming ICMP /// `DestinationUnreachable` (and other types) packets with a UDP/TCP /// original datagram which does not correspond to a probe sent by the /// tracer and must therefore be ignored. /// /// For UDP and TCP probe responses, check that the src/dest ports and /// dest address match the expected values. /// /// For ICMP probe responses no additional checks are required. #[instrument(skip(self), level = "trace")] fn validate(&self, resp: &ResponseData) -> bool { const fn validate_ports( port_direction: PortDirection, src_port: u16, dest_port: u16, ) -> bool { match port_direction { PortDirection::FixedSrc(src) if src.0 == src_port => true, PortDirection::FixedDest(dest) if dest.0 == dest_port => true, PortDirection::FixedBoth(src, dest) if src.0 == src_port && dest.0 == dest_port => { true } _ => false, } } match resp.proto_resp { ProtocolResponse::Icmp(_) => true, ProtocolResponse::Udp(UdpProtocolResponse { dest_addr, src_port, dest_port, has_magic, .. }) => { let check_ports = validate_ports(self.config.port_direction, src_port, dest_port); let check_dest_addr = self.config.target_addr == dest_addr; let check_magic = match (self.config.multipath_strategy, self.config.target_addr) { (MultipathStrategy::Dublin, IpAddr::V6(_)) => has_magic, _ => true, }; check_dest_addr && check_ports && check_magic } ProtocolResponse::Tcp(TcpProtocolResponse { dest_addr, src_port, dest_port, .. }) => { let check_ports = validate_ports(self.config.port_direction, src_port, dest_port); let check_dest_addr = self.config.target_addr == dest_addr; check_dest_addr && check_ports } } } } /// Derived response based on strategy config. #[derive(Debug)] struct StrategyResponse { icmp_packet_type: IcmpPacketType, trace_id: TraceId, sequence: Sequence, tos: Option, expected_udp_checksum: Option, actual_udp_checksum: Option, received: SystemTime, addr: IpAddr, is_target: bool, exts: Option, } impl From<(Response, &StrategyConfig)> for StrategyResponse { fn from((resp, config): (Response, &StrategyConfig)) -> Self { match resp { Response::TimeExceeded(data, code, exts) => { let proto_resp = ProtocolStrategyResponse::from((data.proto_resp, config)); let is_target = data.addr == config.target_addr; Self { icmp_packet_type: IcmpPacketType::TimeExceeded(code), trace_id: proto_resp.trace_id, sequence: proto_resp.sequence, tos: proto_resp.tos, expected_udp_checksum: proto_resp.expected_udp_checksum, actual_udp_checksum: proto_resp.actual_udp_checksum, received: data.recv, addr: data.addr, is_target, exts, } } Response::DestinationUnreachable(data, code, exts) => { let proto_resp = ProtocolStrategyResponse::from((data.proto_resp, config)); let is_target = data.addr == config.target_addr; Self { icmp_packet_type: IcmpPacketType::Unreachable(code), trace_id: proto_resp.trace_id, sequence: proto_resp.sequence, tos: proto_resp.tos, expected_udp_checksum: proto_resp.expected_udp_checksum, actual_udp_checksum: proto_resp.actual_udp_checksum, received: data.recv, addr: data.addr, is_target, exts, } } Response::EchoReply(data, code) => { let proto_resp = ProtocolStrategyResponse::from((data.proto_resp, config)); Self { icmp_packet_type: IcmpPacketType::EchoReply(code), trace_id: proto_resp.trace_id, sequence: proto_resp.sequence, tos: proto_resp.tos, expected_udp_checksum: proto_resp.expected_udp_checksum, actual_udp_checksum: proto_resp.actual_udp_checksum, received: data.recv, addr: data.addr, is_target: true, exts: None, } } Response::TcpReply(data) | Response::TcpRefused(data) => { let proto_resp = ProtocolStrategyResponse::from((data.proto_resp, config)); Self { icmp_packet_type: IcmpPacketType::NotApplicable, trace_id: proto_resp.trace_id, sequence: proto_resp.sequence, tos: proto_resp.tos, expected_udp_checksum: proto_resp.expected_udp_checksum, actual_udp_checksum: proto_resp.actual_udp_checksum, received: data.recv, addr: data.addr, is_target: true, exts: None, } } } } } /// Derived response sequence based on strategy config. #[derive(Debug)] struct ProtocolStrategyResponse { trace_id: TraceId, sequence: Sequence, tos: Option, expected_udp_checksum: Option, actual_udp_checksum: Option, } impl From<(ProtocolResponse, &StrategyConfig)> for ProtocolStrategyResponse { fn from((proto_resp, config): (ProtocolResponse, &StrategyConfig)) -> Self { match proto_resp { ProtocolResponse::Icmp(IcmpProtocolResponse { identifier, sequence, tos, }) => Self { trace_id: TraceId(identifier), sequence: Sequence(sequence), tos, expected_udp_checksum: None, actual_udp_checksum: None, }, ProtocolResponse::Udp(UdpProtocolResponse { identifier, src_port, dest_port, tos, expected_udp_checksum, actual_udp_checksum, payload_len, .. }) => { let sequence = match ( config.multipath_strategy, config.port_direction, config.target_addr, ) { (MultipathStrategy::Classic, PortDirection::FixedDest(_), _) => src_port, (MultipathStrategy::Classic, _, _) => dest_port, (MultipathStrategy::Paris, _, _) => actual_udp_checksum, (MultipathStrategy::Dublin, _, IpAddr::V4(_)) => identifier, (MultipathStrategy::Dublin, _, IpAddr::V6(_)) => { config.initial_sequence.0 + payload_len } }; let (expected_udp_checksum, actual_udp_checksum) = match (config.multipath_strategy, config.target_addr) { (MultipathStrategy::Dublin, IpAddr::V4(_)) => ( Some(Checksum(expected_udp_checksum)), Some(Checksum(actual_udp_checksum)), ), _ => (None, None), }; Self { trace_id: TraceId(0), sequence: Sequence(sequence), tos, expected_udp_checksum, actual_udp_checksum, } } ProtocolResponse::Tcp(TcpProtocolResponse { src_port, dest_port, tos, .. }) => { let sequence = match config.port_direction { PortDirection::FixedSrc(_) => dest_port, _ => src_port, }; Self { trace_id: TraceId(0), sequence: Sequence(sequence), tos, expected_udp_checksum: None, actual_udp_checksum: None, } } } } } #[cfg(test)] mod tests { use super::*; use crate::net::MockNetwork; use crate::probe::IcmpPacketCode; use crate::{MaxRounds, Port}; use std::net::Ipv4Addr; use std::num::NonZeroUsize; #[test] fn test_time_exceeded_target_response() { let config = StrategyConfig { target_addr: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), ..Default::default() }; let now = SystemTime::now(); let resp_data = Response::TimeExceeded(response_data(now), IcmpPacketCode(1), None); let resp = StrategyResponse::from((resp_data, &config)); assert_eq!( resp.icmp_packet_type, IcmpPacketType::TimeExceeded(IcmpPacketCode(1)) ); assert_eq!(resp.trace_id, TraceId(0)); assert_eq!(resp.sequence, Sequence(33434)); assert_eq!(resp.received, now); assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))); assert_eq!(resp.is_target, true); assert!(resp.exts.is_none()); } #[test] fn test_time_exceeded_not_target_response() { let config = StrategyConfig { target_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), ..Default::default() }; let now = SystemTime::now(); let resp_data = Response::TimeExceeded(response_data(now), IcmpPacketCode(1), None); let resp = StrategyResponse::from((resp_data, &config)); assert_eq!( resp.icmp_packet_type, IcmpPacketType::TimeExceeded(IcmpPacketCode(1)) ); assert_eq!(resp.trace_id, TraceId(0)); assert_eq!(resp.sequence, Sequence(33434)); assert_eq!(resp.received, now); assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))); assert_eq!(resp.is_target, false); assert!(resp.exts.is_none()); } #[test] fn test_destination_unreachable_target_response() { let config = StrategyConfig { target_addr: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), ..Default::default() }; let now = SystemTime::now(); let resp_data = Response::DestinationUnreachable(response_data(now), IcmpPacketCode(10), None); let resp = StrategyResponse::from((resp_data, &config)); assert_eq!( resp.icmp_packet_type, IcmpPacketType::Unreachable(IcmpPacketCode(10)) ); assert_eq!(resp.trace_id, TraceId(0)); assert_eq!(resp.sequence, Sequence(33434)); assert_eq!(resp.received, now); assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))); assert_eq!(resp.is_target, true); assert!(resp.exts.is_none()); } #[test] fn test_destination_unreachable_not_target_response() { let config = StrategyConfig::default(); let now = SystemTime::now(); let resp_data = Response::DestinationUnreachable(response_data(now), IcmpPacketCode(10), None); let resp = StrategyResponse::from((resp_data, &config)); assert_eq!( resp.icmp_packet_type, IcmpPacketType::Unreachable(IcmpPacketCode(10)) ); assert_eq!(resp.trace_id, TraceId(0)); assert_eq!(resp.sequence, Sequence(33434)); assert_eq!(resp.received, now); assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))); assert_eq!(resp.is_target, false); assert!(resp.exts.is_none()); } #[test] fn test_echo_reply_response() { let config = StrategyConfig::default(); let now = SystemTime::now(); let resp_data = Response::EchoReply(response_data(now), IcmpPacketCode(99)); let resp = StrategyResponse::from((resp_data, &config)); assert_eq!( resp.icmp_packet_type, IcmpPacketType::EchoReply(IcmpPacketCode(99)) ); assert_eq!(resp.trace_id, TraceId(0)); assert_eq!(resp.sequence, Sequence(33434)); assert_eq!(resp.received, now); assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))); assert_eq!(resp.is_target, true); assert!(resp.exts.is_none()); } #[test] fn test_tcp_reply_response() { let config = StrategyConfig::default(); let now = SystemTime::now(); let resp_data = Response::TcpReply(response_data(now)); let resp = StrategyResponse::from((resp_data, &config)); assert_eq!(resp.icmp_packet_type, IcmpPacketType::NotApplicable); assert_eq!(resp.trace_id, TraceId(0)); assert_eq!(resp.sequence, Sequence(33434)); assert_eq!(resp.received, now); assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))); assert_eq!(resp.is_target, true); assert!(resp.exts.is_none()); } #[test] fn test_tcp_refused_response() { let config = StrategyConfig::default(); let now = SystemTime::now(); let resp_data = Response::TcpRefused(response_data(now)); let resp = StrategyResponse::from((resp_data, &config)); assert_eq!(resp.icmp_packet_type, IcmpPacketType::NotApplicable); assert_eq!(resp.trace_id, TraceId(0)); assert_eq!(resp.sequence, Sequence(33434)); assert_eq!(resp.received, now); assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))); assert_eq!(resp.is_target, true); assert!(resp.exts.is_none()); } #[test] fn test_icmp_response() { let config = StrategyConfig::default(); let proto_resp = ProtocolResponse::Icmp(IcmpProtocolResponse { identifier: 1234, sequence: 33434, tos: Some(TypeOfService(0)), }); let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config)); assert_eq!(strategy_resp.trace_id, TraceId(1234)); assert_eq!(strategy_resp.sequence, Sequence(33434)); } #[test] fn test_udp_classic_fixed_src_response() { let config = StrategyConfig { protocol: Protocol::Udp, port_direction: PortDirection::FixedSrc(Port(5000)), ..Default::default() }; let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse { identifier: 0, dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), src_port: 5000, dest_port: 33434, tos: Some(TypeOfService(0)), expected_udp_checksum: 0, actual_udp_checksum: 0, payload_len: 0, has_magic: false, }); let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config)); assert_eq!(strategy_resp.trace_id, TraceId(0)); assert_eq!(strategy_resp.sequence, Sequence(33434)); } #[test] fn test_udp_classic_fixed_dest_response() { let config = StrategyConfig { protocol: Protocol::Udp, port_direction: PortDirection::FixedDest(Port(5000)), ..Default::default() }; let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse { identifier: 0, dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), src_port: 33434, dest_port: 5000, tos: Some(TypeOfService(0)), expected_udp_checksum: 0, actual_udp_checksum: 0, payload_len: 0, has_magic: false, }); let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config)); assert_eq!(strategy_resp.trace_id, TraceId(0)); assert_eq!(strategy_resp.sequence, Sequence(33434)); } #[test] fn test_udp_paris_response() { let config = StrategyConfig { protocol: Protocol::Udp, multipath_strategy: MultipathStrategy::Paris, port_direction: PortDirection::FixedSrc(Port(5000)), ..Default::default() }; let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse { identifier: 33434, dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), src_port: 5000, dest_port: 35000, tos: Some(TypeOfService(0)), expected_udp_checksum: 33434, actual_udp_checksum: 33434, payload_len: 0, has_magic: false, }); let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config)); assert_eq!(strategy_resp.trace_id, TraceId(0)); assert_eq!(strategy_resp.sequence, Sequence(33434)); } #[test] fn test_udp_dublin_ipv4_response() { let config = StrategyConfig { protocol: Protocol::Udp, multipath_strategy: MultipathStrategy::Dublin, port_direction: PortDirection::FixedSrc(Port(5000)), ..Default::default() }; let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse { identifier: 33434, dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), src_port: 5000, dest_port: 35000, tos: Some(TypeOfService(0)), expected_udp_checksum: 0, actual_udp_checksum: 0, payload_len: 0, has_magic: false, }); let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config)); assert_eq!(strategy_resp.trace_id, TraceId(0)); assert_eq!(strategy_resp.sequence, Sequence(33434)); } #[test] fn test_udp_dublin_ipv6_response() { let config = StrategyConfig { protocol: Protocol::Udp, target_addr: IpAddr::V6("::1".parse().unwrap()), multipath_strategy: MultipathStrategy::Dublin, port_direction: PortDirection::FixedSrc(Port(5000)), ..Default::default() }; let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse { identifier: 0, dest_addr: IpAddr::V6("::1".parse().unwrap()), src_port: 5000, dest_port: 35000, tos: Some(TypeOfService(0)), expected_udp_checksum: 0, actual_udp_checksum: 0, payload_len: 55, has_magic: true, }); let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config)); assert_eq!(strategy_resp.trace_id, TraceId(0)); assert_eq!(strategy_resp.sequence, Sequence(33489)); } #[test] fn test_tcp_fixed_dest_response() { let config = StrategyConfig { protocol: Protocol::Tcp, port_direction: PortDirection::FixedDest(Port(80)), ..Default::default() }; let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse { identifier: 0, dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), src_port: 33434, dest_port: 80, tos: Some(TypeOfService(0)), expected_udp_checksum: 0, actual_udp_checksum: 0, payload_len: 0, has_magic: false, }); let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config)); assert_eq!(strategy_resp.trace_id, TraceId(0)); assert_eq!(strategy_resp.sequence, Sequence(33434)); } #[test] fn test_tcp_fixed_src_response() { let config = StrategyConfig { protocol: Protocol::Tcp, port_direction: PortDirection::FixedSrc(Port(5000)), ..Default::default() }; let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse { identifier: 0, dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), src_port: 5000, dest_port: 33434, tos: Some(TypeOfService(0)), expected_udp_checksum: 0, actual_udp_checksum: 0, payload_len: 0, has_magic: false, }); let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config)); assert_eq!(strategy_resp.trace_id, TraceId(0)); assert_eq!(strategy_resp.sequence, Sequence(33434)); } // The network can return both `DestinationUnreachable` and `TcpRefused` // for the same sequence number. This can occur for the target hop for // TCP protocol as the network layer check for ICMP responses such as // `DestinationUnreachable` and also synthesizes a `TcpRefused` response. // // This test simulates sending 1 TCP probe (seq=33434) and receiving two // responses for that probe, a `DestinationUnreachable` followed by a // `TcpRefused`. #[test] fn test_tcp_dest_unreachable_and_refused() -> anyhow::Result<()> { let sequence = 33434; let target_addr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)); let mut network = MockNetwork::new(); let mut seq = mockall::Sequence::new(); network.expect_send_probe().times(1).returning(|_| Ok(())); network .expect_recv_probe() .times(1) .in_sequence(&mut seq) .returning(move || { Ok(Some(Response::DestinationUnreachable( ResponseData::new( SystemTime::now(), target_addr, ProtocolResponse::Tcp(TcpProtocolResponse::new( target_addr, sequence, 80, None, )), ), IcmpPacketCode(1), None, ))) }); network .expect_recv_probe() .times(1) .in_sequence(&mut seq) .returning(move || { Ok(Some(Response::TcpRefused(ResponseData::new( SystemTime::now(), target_addr, ProtocolResponse::Tcp(TcpProtocolResponse::new( target_addr, sequence, 80, None, )), )))) }); let config = StrategyConfig { target_addr, max_rounds: Some(MaxRounds(NonZeroUsize::MIN)), initial_sequence: Sequence(sequence), port_direction: PortDirection::FixedDest(Port(80)), protocol: Protocol::Tcp, ..Default::default() }; let tracer = Strategy::new(&config, |_| {}); let mut state = TracerState::new(config); tracer.send_request(&mut network, &mut state)?; tracer.recv_response(&mut network, &mut state)?; tracer.recv_response(&mut network, &mut state)?; Ok(()) } const fn response_data(now: SystemTime) -> ResponseData { ResponseData::new( now, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), ProtocolResponse::Icmp(IcmpProtocolResponse { identifier: 0, sequence: 33434, tos: Some(TypeOfService(0)), }), ) } } /// Mutable state needed for the tracing algorithm. /// /// This is contained within a submodule to ensure that mutations are only performed via methods on /// the `TracerState` struct. mod state { use crate::constants::MAX_SEQUENCE_PER_ROUND; use crate::probe::{Probe, ProbeStatus}; use crate::strategy::{StrategyConfig, StrategyResponse}; use crate::types::{MaxRounds, Port, RoundId, Sequence, TimeToLive, TraceId}; use crate::{Flags, MultipathStrategy, PortDirection, Protocol}; use std::array::from_fn; use std::net::IpAddr; use std::time::SystemTime; use tracing::instrument; /// The maximum number of `ProbeStatus` entries in the buffer. /// /// This is larger than maximum number of time-to-live (TTL) we can support to allow for skipped /// sequences. const BUFFER_SIZE: u16 = MAX_SEQUENCE_PER_ROUND; /// The maximum sequence number. /// /// The sequence number is only ever wrapped between rounds, and so we need to ensure that there /// are enough sequence numbers for a complete round. /// /// A sequence number can be skipped if, for example, the port for that sequence number cannot /// be bound as it is already in use. /// /// To ensure each `ProbeStatus` is in the correct place in the buffer (i.e. the index into the /// buffer is always `Probe.sequence - round_sequence`), when we skip a sequence we leave /// the skipped `ProbeStatus` in-place and use the next slot for the next sequence. /// /// We cap the number of sequences that can potentially be skipped in a round to ensure that /// sequence number does not even need to wrap around during a round. /// /// We only ever send `ttl` in the range 1..255, and so we may use all buffer capacity, except /// the minimum needed to send up to a max `ttl` of 255 (a `ttl` of 0 is never sent). const MAX_SEQUENCE: Sequence = Sequence(u16::MAX - BUFFER_SIZE); /// Mutable state needed for the tracing algorithm. #[derive(Debug)] pub struct TracerState { /// Tracer configuration. config: StrategyConfig, /// The state of all `ProbeStatus` requests and responses. buffer: [ProbeStatus; BUFFER_SIZE as usize], /// An increasing sequence number for every `EchoRequest`. sequence: Sequence, /// The starting sequence number of the current round. round_sequence: Sequence, /// The time-to-live for the _next_ `EchoRequest` packet to be sent. ttl: TimeToLive, /// The current round. round: RoundId, /// The timestamp of when the current round started. round_start: SystemTime, /// Did we receive an `EchoReply` from the target host in this round? target_found: bool, /// The maximum time-to-live echo response packet we have received. max_received_ttl: Option, /// The observed time-to-live of the `EchoReply` from the target host. /// /// Note that this is _not_ reset each round and that it can also _change_ over time, /// including going _down_ as responses can be received out-of-order. target_ttl: Option, /// The timestamp of the echo response packet. received_time: Option, } impl TracerState { pub fn new(config: StrategyConfig) -> Self { Self { config, buffer: from_fn(|_| ProbeStatus::default()), sequence: config.initial_sequence, round_sequence: config.initial_sequence, ttl: config.first_ttl, round: RoundId(0), round_start: SystemTime::now(), target_found: false, max_received_ttl: None, target_ttl: None, received_time: None, } } /// Get a slice of `ProbeStatus` for the current round. pub fn probes(&self) -> &[ProbeStatus] { let round_size = self.sequence - self.round_sequence; &self.buffer[..round_size.0 as usize] } /// Get the `ProbeStatus` for `sequence` pub fn probe_at(&self, sequence: Sequence) -> ProbeStatus { self.buffer[usize::from(sequence - self.round_sequence)].clone() } pub const fn ttl(&self) -> TimeToLive { self.ttl } pub const fn round_start(&self) -> SystemTime { self.round_start } pub const fn target_found(&self) -> bool { self.target_found } pub const fn max_received_ttl(&self) -> Option { self.max_received_ttl } pub const fn target_ttl(&self) -> Option { self.target_ttl } pub const fn received_time(&self) -> Option { self.received_time } /// Is `sequence` in the current round? pub fn in_round(&self, sequence: Sequence) -> bool { sequence >= self.round_sequence && sequence.0 - self.round_sequence.0 < BUFFER_SIZE } /// Do we have capacity in the current round for another sequence? pub fn round_has_capacity(&self) -> bool { let round_size = self.sequence - self.round_sequence; round_size.0 < BUFFER_SIZE } /// Are all rounds complete? pub const fn finished(&self, max_rounds: Option) -> bool { match max_rounds { None => false, Some(max_rounds) => self.round.0 > max_rounds.0.get() - 1, } } /// Create and return the next `Probe` at the current `sequence` and `ttl`. /// /// We post-increment `ttl` here and so in practice we only allow `ttl` values in the range /// `1..254` to allow us to use a `u8`. #[instrument(skip(self), level = "trace")] pub fn next_probe(&mut self, sent: SystemTime) -> Probe { let (src_port, dest_port, identifier, flags) = self.probe_data(); let probe = Probe::new( self.sequence, identifier, src_port, dest_port, self.ttl, self.round, sent, flags, ); let probe_index = usize::from(self.sequence - self.round_sequence); self.buffer[probe_index] = ProbeStatus::Awaited(probe.clone()); debug_assert!(self.ttl < TimeToLive(u8::MAX)); self.ttl += TimeToLive(1); debug_assert!(self.sequence < Sequence(u16::MAX)); self.sequence += Sequence(1); probe } /// Re-issue the `Probe` with the next sequence number. /// /// This will mark the `ProbeStatus` at the previous `sequence` as skipped and re-create it /// with the previous `ttl` and the current `sequence`. /// /// For example, if the sequence is `4` and the `ttl` is `5` prior to calling this method /// then afterward: /// - The `ProbeStatus` at sequence `3` will be set to `Skipped` state /// - A new `ProbeStatus` will be created at sequence `4` with a `ttl` of `5` #[instrument(skip(self), level = "trace")] pub fn reissue_probe(&mut self, sent: SystemTime) -> Probe { let probe_index = usize::from(self.sequence - self.round_sequence); self.buffer[probe_index - 1] = ProbeStatus::Skipped; let (src_port, dest_port, identifier, flags) = self.probe_data(); let probe = Probe::new( self.sequence, identifier, src_port, dest_port, self.ttl - TimeToLive(1), self.round, sent, flags, ); self.buffer[probe_index] = ProbeStatus::Awaited(probe.clone()); debug_assert!(self.sequence < Sequence(u16::MAX)); self.sequence += Sequence(1); probe } /// Mark the `ProbeStatus` at the current `sequence` as failed. #[instrument(skip(self), level = "trace")] pub fn fail_probe(&mut self) { let probe_index = usize::from(self.sequence - self.round_sequence); let probe = self.buffer[probe_index - 1].clone(); match probe { ProbeStatus::Awaited(awaited) => { self.buffer[probe_index - 1] = ProbeStatus::Failed(awaited.failed()); } _ => unreachable!("expected ProbeStatus::Awaited"), } } /// Determine the `src_port`, `dest_port` and `identifier` for the current probe. /// /// This will differ depending on the `TracerProtocol`, `MultipathStrategy` & /// `PortDirection`. fn probe_data(&self) -> (Port, Port, TraceId, Flags) { match self.config.protocol { Protocol::Icmp => self.probe_icmp_data(), Protocol::Udp => self.probe_udp_data(), Protocol::Tcp => self.probe_tcp_data(), } } /// Determine the `src_port`, `dest_port` and `identifier` for the current ICMP probe. const fn probe_icmp_data(&self) -> (Port, Port, TraceId, Flags) { ( Port(0), Port(0), self.config.trace_identifier, Flags::empty(), ) } /// Determine the `src_port`, `dest_port` and `identifier` for the current UDP probe. fn probe_udp_data(&self) -> (Port, Port, TraceId, Flags) { match self.config.multipath_strategy { MultipathStrategy::Classic => match self.config.port_direction { PortDirection::FixedSrc(src_port) => ( Port(src_port.0), Port(self.sequence.0), TraceId(0), Flags::empty(), ), PortDirection::FixedDest(dest_port) => ( Port(self.sequence.0), Port(dest_port.0), TraceId(0), Flags::empty(), ), PortDirection::FixedBoth(_, _) | PortDirection::None => { unimplemented!() } }, MultipathStrategy::Paris => { let round_port = ((self.config.initial_sequence.0 as usize + self.round.0) % usize::from(u16::MAX)) as u16; match self.config.port_direction { PortDirection::FixedSrc(src_port) => ( Port(src_port.0), Port(round_port), TraceId(0), Flags::PARIS_CHECKSUM, ), PortDirection::FixedDest(dest_port) => ( Port(round_port), Port(dest_port.0), TraceId(0), Flags::PARIS_CHECKSUM, ), PortDirection::FixedBoth(src_port, dest_port) => ( Port(src_port.0), Port(dest_port.0), TraceId(0), Flags::PARIS_CHECKSUM, ), PortDirection::None => unimplemented!(), } } MultipathStrategy::Dublin => { let round_port = ((self.config.initial_sequence.0 as usize + self.round.0) % usize::from(u16::MAX)) as u16; match self.config.port_direction { PortDirection::FixedSrc(src_port) => ( Port(src_port.0), Port(round_port), TraceId(self.sequence.0), Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, ), PortDirection::FixedDest(dest_port) => ( Port(round_port), Port(dest_port.0), TraceId(self.sequence.0), Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, ), PortDirection::FixedBoth(src_port, dest_port) => ( Port(src_port.0), Port(dest_port.0), TraceId(self.sequence.0), Flags::DUBLIN_IPV6_PAYLOAD_LENGTH, ), PortDirection::None => unimplemented!(), } } } } /// Determine the `src_port`, `dest_port` and `identifier` for the current TCP probe. fn probe_tcp_data(&self) -> (Port, Port, TraceId, Flags) { let (src_port, dest_port) = match self.config.port_direction { PortDirection::FixedSrc(src_port) => (src_port.0, self.sequence.0), PortDirection::FixedDest(dest_port) => (self.sequence.0, dest_port.0), PortDirection::FixedBoth(_, _) | PortDirection::None => unimplemented!(), }; (Port(src_port), Port(dest_port), TraceId(0), Flags::empty()) } /// Update the state of a `ProbeStatus` and the trace. /// /// We want to update: /// /// - the `target_ttl` to be the time-to-live of the `ProbeStatus` request from the target /// - the `max_received_ttl` we have observed this round /// - the latest packet `received_time` in this round /// - whether the target has been found in this round /// /// The ICMP replies may arrive out-of-order, and so we must be careful here to avoid /// overwriting the state with stale values. We may also receive multiple replies /// from the target host with differing time-to-live values and so must ensure we /// use the time-to-live with the lowest sequence number. #[instrument(skip(self), level = "trace")] pub fn complete_probe(&mut self, resp: StrategyResponse) { // Retrieve and update the `ProbeStatus` at `sequence`. let probe = self.probe_at(resp.sequence); let awaited = match probe { ProbeStatus::Awaited(awaited) => awaited, // there is a valid scenario for TCP where a probe is already // `Complete`, see `test_tcp_dest_unreachable_and_refused`. ProbeStatus::Complete(_) => { return; } _ => { debug_assert!( false, "completed probe was not in Awaited state (probe={probe:#?})" ); return; } }; let completed = awaited.complete( resp.addr, resp.received, resp.icmp_packet_type, resp.tos, resp.expected_udp_checksum, resp.actual_udp_checksum, resp.exts, ); let ttl = completed.ttl; self.buffer[usize::from(resp.sequence - self.round_sequence)] = ProbeStatus::Complete(completed); // If this `ProbeStatus` found the target then we set the `target_ttl` if not already // set, being careful to account for `Probes` being received out-of-order. // // If this `ProbeStatus` did not find the target but has a ttl that is greater or equal // to the target ttl (if known) then we reset the target ttl to None. This // is to support Equal Cost Multi-path Routing (ECMP) cases where the number // of hops to the target will vary over the lifetime of the trace. self.target_ttl = if resp.is_target { match self.target_ttl { None => Some(ttl), Some(target_ttl) if ttl < target_ttl => Some(ttl), Some(target_ttl) => Some(target_ttl), } } else { match self.target_ttl { Some(target_ttl) if ttl >= target_ttl => None, Some(target_ttl) => Some(target_ttl), None => None, } }; self.max_received_ttl = match self.max_received_ttl { None => Some(ttl), Some(max_received_ttl) => Some(max_received_ttl.max(ttl)), }; self.received_time = Some(resp.received); self.target_found |= resp.is_target; } /// Advance to the next round. /// /// If, during the round which just completed, we went above the max sequence number then we /// reset it here. We do this here to avoid having to deal with the sequence number /// wrapping during a round, which is more problematic. #[instrument(skip(self), level = "trace")] pub fn advance_round(&mut self, first_ttl: TimeToLive) { if self.sequence >= self.max_sequence() { self.sequence = self.config.initial_sequence; } self.target_found = false; self.round_sequence = self.sequence; self.received_time = None; self.round_start = SystemTime::now(); self.max_received_ttl = None; self.round += RoundId(1); self.ttl = first_ttl; } /// The maximum sequence number allowed. /// /// The Dublin multipath strategy for IPv6/udp encodes the sequence /// number as the payload length and consequently the maximum sequence /// number must be no larger than the maximum IPv6/udp payload size. /// /// It is also required that the range of possible sequence numbers is /// _at least_ `BUFFER_SIZE` to ensure delayed responses from a prior /// round are not incorrectly associated with later rounds (see /// `in_round` function). fn max_sequence(&self) -> Sequence { match (self.config.multipath_strategy, self.config.target_addr) { (MultipathStrategy::Dublin, IpAddr::V6(_)) => { self.config.initial_sequence + Sequence(BUFFER_SIZE) } _ => MAX_SEQUENCE, } } } #[cfg(test)] mod tests { use super::*; use crate::TypeOfService; use crate::probe::{IcmpPacketCode, IcmpPacketType}; use crate::types::MaxInflight; use rand::RngExt; use std::net::{IpAddr, Ipv4Addr}; use std::time::Duration; #[expect(clippy::too_many_lines, clippy::bool_assert_comparison)] #[test] fn test_state() { let mut state = TracerState::new(cfg(Sequence(33434))); // Validate the initial `TracerState` assert_eq!(state.round, RoundId(0)); assert_eq!(state.sequence, Sequence(33434)); assert_eq!(state.round_sequence, Sequence(33434)); assert_eq!(state.ttl, TimeToLive(1)); assert_eq!(state.max_received_ttl, None); assert_eq!(state.received_time, None); assert_eq!(state.target_ttl, None); assert_eq!(state.target_found, false); // The initial state of the probe before sending let prob_init = state.probe_at(Sequence(33434)); assert_eq!(ProbeStatus::NotSent, prob_init); // Prepare probe 1 (round 0, sequence 33434, ttl 1) for sending let sent_1 = SystemTime::now(); let probe_1 = state.next_probe(sent_1); assert_eq!(probe_1.sequence, Sequence(33434)); assert_eq!(probe_1.ttl, TimeToLive(1)); assert_eq!(probe_1.round, RoundId(0)); assert_eq!(probe_1.sent, sent_1); // Update the state of the probe 1 after receiving a `TimeExceeded` let received_1 = SystemTime::now(); let host = IpAddr::V4(Ipv4Addr::LOCALHOST); state.complete_probe(StrategyResponse { icmp_packet_type: IcmpPacketType::TimeExceeded(IcmpPacketCode(1)), trace_id: TraceId(0), sequence: Sequence(33434), tos: Some(TypeOfService(0)), expected_udp_checksum: None, actual_udp_checksum: None, received: received_1, addr: host, is_target: false, exts: None, }); // Validate the state of the probe 1 after the update let probe_1_fetch = state.probe_at(Sequence(33434)).try_into_complete().unwrap(); assert_eq!(probe_1_fetch.sequence, Sequence(33434)); assert_eq!(probe_1_fetch.ttl, TimeToLive(1)); assert_eq!(probe_1_fetch.round, RoundId(0)); assert_eq!(probe_1_fetch.received, received_1); assert_eq!(probe_1_fetch.host, host); assert_eq!(probe_1_fetch.sent, sent_1); assert_eq!( probe_1_fetch.icmp_packet_type, IcmpPacketType::TimeExceeded(IcmpPacketCode(1)) ); // Validate the `TracerState` after the update assert_eq!(state.round, RoundId(0)); assert_eq!(state.sequence, Sequence(33435)); assert_eq!(state.round_sequence, Sequence(33434)); assert_eq!(state.ttl, TimeToLive(2)); assert_eq!(state.max_received_ttl, Some(TimeToLive(1))); assert_eq!(state.received_time, Some(received_1)); assert_eq!(state.target_ttl, None); assert_eq!(state.target_found, false); // Validate the probes() iterator returns only a single probe { let mut probe_iter = state.probes().iter(); let probe_next1 = probe_iter.next().unwrap(); assert_eq!(ProbeStatus::Complete(probe_1_fetch), probe_next1.clone()); assert_eq!(None, probe_iter.next()); } // Advance to the next round state.advance_round(TimeToLive(1)); // Validate the `TracerState` after the round update assert_eq!(state.round, RoundId(1)); assert_eq!(state.sequence, Sequence(33435)); assert_eq!(state.round_sequence, Sequence(33435)); assert_eq!(state.ttl, TimeToLive(1)); assert_eq!(state.max_received_ttl, None); assert_eq!(state.received_time, None); assert_eq!(state.target_ttl, None); assert_eq!(state.target_found, false); // Prepare probe 2 (round 1, sequence 33001, ttl 1) for sending let sent_2 = SystemTime::now(); let probe_2 = state.next_probe(sent_2); assert_eq!(probe_2.sequence, Sequence(33435)); assert_eq!(probe_2.ttl, TimeToLive(1)); assert_eq!(probe_2.round, RoundId(1)); assert_eq!(probe_2.sent, sent_2); // Prepare probe 3 (round 1, sequence 33002, ttl 2) for sending let sent_3 = SystemTime::now(); let probe_3 = state.next_probe(sent_3); assert_eq!(probe_3.sequence, Sequence(33436)); assert_eq!(probe_3.ttl, TimeToLive(2)); assert_eq!(probe_3.round, RoundId(1)); assert_eq!(probe_3.sent, sent_3); // Update the state of probe 2 after receiving a `TimeExceeded` let received_2 = SystemTime::now(); let host = IpAddr::V4(Ipv4Addr::LOCALHOST); state.complete_probe(StrategyResponse { icmp_packet_type: IcmpPacketType::TimeExceeded(IcmpPacketCode(1)), trace_id: TraceId(0), sequence: Sequence(33435), tos: Some(TypeOfService(0)), expected_udp_checksum: None, actual_udp_checksum: None, received: received_2, addr: host, is_target: false, exts: None, }); let probe_2_recv = state.probe_at(Sequence(33435)); // Validate the `TracerState` after the update to probe 2 assert_eq!(state.round, RoundId(1)); assert_eq!(state.sequence, Sequence(33437)); assert_eq!(state.round_sequence, Sequence(33435)); assert_eq!(state.ttl, TimeToLive(3)); assert_eq!(state.max_received_ttl, Some(TimeToLive(1))); assert_eq!(state.received_time, Some(received_2)); assert_eq!(state.target_ttl, None); assert_eq!(state.target_found, false); // Validate the probes() iterator returns the two probes in the states we expect { let mut probe_iter = state.probes().iter(); let probe_next1 = probe_iter.next().unwrap(); assert_eq!(&probe_2_recv, probe_next1); let probe_next2 = probe_iter.next().unwrap(); assert_eq!(ProbeStatus::Awaited(probe_3), probe_next2.clone()); } // Update the state of probe 3 after receiving a `EchoReply` let received_3 = SystemTime::now(); let host = IpAddr::V4(Ipv4Addr::LOCALHOST); state.complete_probe(StrategyResponse { icmp_packet_type: IcmpPacketType::EchoReply(IcmpPacketCode(0)), trace_id: TraceId(0), sequence: Sequence(33436), tos: Some(TypeOfService(0)), expected_udp_checksum: None, actual_udp_checksum: None, received: received_3, addr: host, is_target: true, exts: None, }); let probe_3_recv = state.probe_at(Sequence(33436)); // Validate the `TracerState` after the update to probe 3 assert_eq!(state.round, RoundId(1)); assert_eq!(state.sequence, Sequence(33437)); assert_eq!(state.round_sequence, Sequence(33435)); assert_eq!(state.ttl, TimeToLive(3)); assert_eq!(state.max_received_ttl, Some(TimeToLive(2))); assert_eq!(state.received_time, Some(received_3)); assert_eq!(state.target_ttl, Some(TimeToLive(2))); assert_eq!(state.target_found, true); // Validate the probes() iterator returns the two probes in the states we expect { let mut probe_iter = state.probes().iter(); let probe_next1 = probe_iter.next().unwrap(); assert_eq!(&probe_2_recv, probe_next1); let probe_next2 = probe_iter.next().unwrap(); assert_eq!(&probe_3_recv, probe_next2); } } #[test] fn test_sequence_wrap1() { // Start from `MAX_SEQUENCE` - 1 which is (65279 - 1) == 65278 let initial_sequence = Sequence(65278); let mut state = TracerState::new(cfg(initial_sequence)); assert_eq!(state.round, RoundId(0)); assert_eq!(state.sequence, initial_sequence); assert_eq!(state.round_sequence, initial_sequence); // Create a probe at seq 65278 assert_eq!( state.next_probe(SystemTime::now()).sequence, Sequence(65278) ); assert_eq!(state.sequence, Sequence(65279)); // Validate the probes() { let mut iter = state.probes().iter(); assert_eq!( iter.next() .unwrap() .clone() .try_into_awaited() .unwrap() .sequence, Sequence(65278) ); iter.take(BUFFER_SIZE as usize - 1) .for_each(|p| assert!(matches!(p, ProbeStatus::NotSent))); } // Advance the round, which will wrap the sequence back to `initial_sequence` state.advance_round(TimeToLive(1)); assert_eq!(state.round, RoundId(1)); assert_eq!(state.sequence, initial_sequence); assert_eq!(state.round_sequence, initial_sequence); // Create a probe at seq 65278 assert_eq!( state.next_probe(SystemTime::now()).sequence, Sequence(65278) ); assert_eq!(state.sequence, Sequence(65279)); // Validate the probes() again { let mut iter = state.probes().iter(); assert_eq!( iter.next() .unwrap() .clone() .try_into_awaited() .unwrap() .sequence, Sequence(65278) ); iter.take(BUFFER_SIZE as usize - 1) .for_each(|p| assert!(matches!(p, ProbeStatus::NotSent))); } } #[test] fn test_sequence_wrap2() { let total_rounds = 2000; let max_probe_per_round = 254; let mut state = TracerState::new(cfg(Sequence(33434))); for _ in 0..total_rounds { for _ in 0..max_probe_per_round { let _probe = state.next_probe(SystemTime::now()); } state.advance_round(TimeToLive(1)); } assert_eq!(state.round, RoundId(2000)); assert_eq!(state.round_sequence, Sequence(33434)); assert_eq!(state.sequence, Sequence(33434)); } #[test] fn test_sequence_wrap3() { let total_rounds = 2000; let max_probe_per_round = 20; let mut state = TracerState::new(cfg(Sequence(33434))); let mut rng = rand::rng(); for _ in 0..total_rounds { for _ in 0..rng.random_range(0..max_probe_per_round) { state.next_probe(SystemTime::now()); } state.advance_round(TimeToLive(1)); } } #[test] fn test_sequence_wrap_with_skip() { let total_rounds = 2000; let max_probe_per_round = 254; let mut state = TracerState::new(cfg(Sequence(33434))); for _ in 0..total_rounds { for _ in 0..max_probe_per_round { _ = state.next_probe(SystemTime::now()); _ = state.reissue_probe(SystemTime::now()); } state.advance_round(TimeToLive(1)); } assert_eq!(state.round, RoundId(2000)); assert_eq!(state.round_sequence, Sequence(57310)); assert_eq!(state.sequence, Sequence(57310)); } #[test] fn test_in_round() { let state = TracerState::new(cfg(Sequence(33434))); assert!(state.in_round(Sequence(33434))); assert!(state.in_round(Sequence(33945))); assert!(!state.in_round(Sequence(33946))); } #[test] #[should_panic(expected = "assertion failed: !state.in_round(Sequence(64491))")] fn test_in_delayed_probe_not_in_round() { let mut state = TracerState::new(cfg(Sequence(64000))); for _ in 0..55 { _ = state.next_probe(SystemTime::now()); } state.advance_round(TimeToLive(1)); assert!(!state.in_round(Sequence(64491))); } fn cfg(initial_sequence: Sequence) -> StrategyConfig { StrategyConfig { target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED), protocol: Protocol::Icmp, trace_identifier: TraceId::default(), max_rounds: None, first_ttl: TimeToLive(1), max_ttl: TimeToLive(24), grace_duration: Duration::default(), max_inflight: MaxInflight::default(), initial_sequence, multipath_strategy: MultipathStrategy::Classic, port_direction: PortDirection::None, min_round_duration: Duration::default(), max_round_duration: Duration::default(), } } } } /// Returns true if the duration between start and end is grater than a duration, false otherwise. fn exceeds(start: Option, end: SystemTime, dur: Duration) -> bool { start.is_some_and(|start| end.duration_since(start).unwrap_or_default() > dur) } ================================================ FILE: crates/trippy-core/src/tracer.rs ================================================ use crate::error::Result; use crate::{ Error, IcmpExtensionParseMode, MaxInflight, MaxRounds, MultipathStrategy, PacketSize, PayloadPattern, PortDirection, PrivilegeMode, Protocol, Round, Sequence, State, TimeToLive, TraceId, TypeOfService, }; use std::fmt::Debug; use std::net::IpAddr; use std::sync::Arc; use std::thread; use std::thread::JoinHandle; use std::time::Duration; /// A traceroute implementation. /// /// See the [`crate`] documentation for more information. /// /// Note that this is type cheaply cloneable. #[derive(Debug, Clone)] pub struct Tracer { inner: Arc, } impl Tracer { /// Create a `Tracer`. /// /// Use the [`crate::Builder`] type to create a [`Tracer`]. #[expect(clippy::too_many_arguments)] #[must_use] pub(crate) fn new( interface: Option, source_addr: Option, target_addr: IpAddr, privilege_mode: PrivilegeMode, protocol: Protocol, packet_size: PacketSize, payload_pattern: PayloadPattern, tos: TypeOfService, icmp_extension_parse_mode: IcmpExtensionParseMode, read_timeout: Duration, tcp_connect_timeout: Duration, trace_identifier: TraceId, max_rounds: Option, first_ttl: TimeToLive, max_ttl: TimeToLive, grace_duration: Duration, max_inflight: MaxInflight, initial_sequence: Sequence, multipath_strategy: MultipathStrategy, port_direction: PortDirection, min_round_duration: Duration, max_round_duration: Duration, max_samples: usize, max_flows: usize, drop_privileges: bool, ) -> Self { Self { inner: Arc::new(inner::TracerInner::new( interface, source_addr, target_addr, privilege_mode, protocol, packet_size, payload_pattern, tos, icmp_extension_parse_mode, read_timeout, tcp_connect_timeout, trace_identifier, max_rounds, first_ttl, max_ttl, grace_duration, max_inflight, initial_sequence, multipath_strategy, port_direction, min_round_duration, max_round_duration, max_samples, max_flows, drop_privileges, )), } } /// Run the [`Tracer`]. /// /// This method will block until either the trace completes all rounds (if /// [`crate::Builder::max_rounds`] has been called to set to a non-zero /// value) or until the trace fails. /// /// At the completion of the trace, the state of the tracer can be /// retrieved using the [`Tracer::snapshot`] method. /// /// If you want to run the tracer indefinitely (by not setting /// [`crate::Builder::max_rounds`]), you can either clone and run the /// tracer on a separate thread by using the [`Tracer::spawn`] method or /// by use the [`Tracer::run_with`] method in the current thread to gather /// pee round state manually. /// /// # Example /// /// The following will run the tracer for a fixed number (3) of rounds and /// then retrieve the final state snapshot: /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// # use std::net::IpAddr; /// # use std::str::FromStr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from_str("1.1.1.1")?; /// let tracer = Builder::new(addr).max_rounds(Some(3)).build()?; /// tracer.run()?; /// let _state = tracer.snapshot(); /// # Ok(()) /// # } /// ``` /// /// # See Also /// /// - [`Tracer::run_with`] - Run the tracer with a custom round handler. /// - [`Tracer::spawn`] - Spawn the tracer on a new thread without a custom round handler. pub fn run(&self) -> Result<()> { self.inner.run() } /// Run the [`Tracer`] with a custom round handler. /// /// This method will block until either the trace completes all rounds (if /// [`crate::Builder::max_rounds`] has been called to set to a non-zero /// value) or until the trace fails. /// /// At the completion of the trace, the state of the tracer can be /// retrieved using the [`Tracer::snapshot`] method. /// /// This method will additionally call the provided function for each round /// that is completed. This can be useful if you want to gather round state /// manually if the tracer is run indefinitely (by not setting /// [`crate::Builder::max_rounds`]) /// /// # Example /// /// The following will run the tracer indefinitely and print the data from /// each round of tracing: /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// # use std::net::IpAddr; /// # use std::str::FromStr; /// use trippy_core::Builder; /// /// let addr = IpAddr::from_str("1.1.1.1")?; /// let tracer = Builder::new(addr).build()?; /// tracer.run_with(|round| println!("{:?}", round))?; /// # Ok(()) /// # } /// ``` /// /// # See Also /// /// - [`Tracer::run`] - Run the tracer without a custom round handler. pub fn run_with)>(&self, func: F) -> Result<()> { self.inner.run_with(func) } /// Spawn the tracer on a new thread. /// /// This method will spawn a new thread to run the tracer and immediately /// return the [`Tracer`] and a handle to the thread, so it may be joined /// with [`JoinHandle::join`]. /// /// If you want to run the tracer indefinitely (by not setting /// [`crate::Builder::max_rounds`]) you can use this method to spawn the /// tracer on a new thread and return the [`Tracer`] such that a /// [`Tracer::snapshot`] of the state can be taken at any time. /// /// # Example /// /// The following will spawn a tracer on a new thread and take a snapshot /// of the state every 5 seconds: /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// # use std::net::IpAddr; /// # use std::str::FromStr; /// # use std::thread; /// # use std::time::Duration; /// use trippy_core::Builder; /// /// let addr = IpAddr::from_str("1.1.1.1")?; /// let (tracer, _) = Builder::new(addr).build()?.spawn()?; /// loop { /// thread::sleep(Duration::from_secs(5)); /// // get the latest state. /// let _state = tracer.snapshot(); /// } /// # Ok(()) /// # } /// ``` /// /// # See Also /// /// - [`Tracer::run`] - Run the tracer on the current thread. pub fn spawn(self) -> Result<(Self, JoinHandle>)> { let tracer = self.clone(); let handle = thread::Builder::new() .name(format!("tracer-{}", self.trace_identifier().0)) .spawn(move || tracer.run()) .map_err(|err| Error::Other(err.to_string()))?; Ok((self, handle)) } /// Spawn the tracer with a custom round handler on a new thread. /// /// This method will spawn a new thread to run the tracer with a custom /// round handler and immediately return the [`Tracer`] and a handle to the /// thread, so it may be joined with [`JoinHandle::join`]. /// /// # Example /// /// The following will spawn a tracer on a new thread with a custom round /// handler to print the data from each round of tracing and also take a /// snapshot of the state every 5 seconds until the tracer completes all /// rounds: /// /// ```no_run /// # fn main() -> anyhow::Result<()> { /// # use std::net::IpAddr; /// # use std::str::FromStr; /// # use std::thread; /// # use std::time::Duration; /// use trippy_core::Builder; /// /// let addr = IpAddr::from_str("1.1.1.1")?; /// let (tracer, handle) = Builder::new(addr) /// .max_rounds(Some(3)) /// .build()? /// .spawn_with(|round| println!("{:?}", round))?; /// for i in 0..3 { /// thread::sleep(Duration::from_secs(5)); /// // get the latest state. /// let _state = tracer.snapshot(); /// } /// handle.join().unwrap()?; /// # Ok(()) /// # } /// ``` /// /// # See Also /// /// - [`Tracer::spawn`] - Spawn the tracer on a new thread without a custom round handler. pub fn spawn_with) + Send + 'static>( self, func: F, ) -> Result<(Self, JoinHandle>)> { let tracer = self.clone(); let handle = thread::Builder::new() .name(format!("tracer-{}", self.trace_identifier().0)) .spawn(move || tracer.run_with(func)) .map_err(|err| Error::Other(err.to_string()))?; Ok((self, handle)) } /// Take a snapshot of the tracer state. #[must_use] pub fn snapshot(&self) -> State { self.inner.snapshot() } /// Clear the tracer state. pub fn clear(&self) { self.inner.clear(); } /// The maximum number of flows to record. #[must_use] pub fn max_flows(&self) -> usize { self.inner.max_flows() } /// The maximum number of samples to record. #[must_use] pub fn max_samples(&self) -> usize { self.inner.max_samples() } /// The privilege mode of the tracer. #[must_use] pub fn privilege_mode(&self) -> PrivilegeMode { self.inner.privilege_mode() } /// The protocol of the tracer. #[must_use] pub fn protocol(&self) -> Protocol { self.inner.protocol() } /// The interface to use for the tracer. #[must_use] pub fn interface(&self) -> Option<&str> { self.inner.interface() } /// The source address of the tracer. #[must_use] pub fn source_addr(&self) -> Option { self.inner.source_addr() } /// The target address of the tracer. #[must_use] pub fn target_addr(&self) -> IpAddr { self.inner.target_addr() } /// The packet size of the tracer. #[must_use] pub fn packet_size(&self) -> PacketSize { self.inner.packet_size() } /// The payload pattern of the tracer. #[must_use] pub fn payload_pattern(&self) -> PayloadPattern { self.inner.payload_pattern() } /// The initial sequence number of the tracer. #[must_use] pub fn initial_sequence(&self) -> Sequence { self.inner.initial_sequence() } /// The type of service of the tracer. #[must_use] pub fn tos(&self) -> TypeOfService { self.inner.tos() } /// The ICMP extension parse mode of the tracer. #[must_use] pub fn icmp_extension_parse_mode(&self) -> IcmpExtensionParseMode { self.inner.icmp_extension_parse_mode() } /// The read timeout of the tracer. #[must_use] pub fn read_timeout(&self) -> Duration { self.inner.read_timeout() } /// The TCP connect timeout of the tracer. #[must_use] pub fn tcp_connect_timeout(&self) -> Duration { self.inner.tcp_connect_timeout() } /// The trace identifier of the tracer. #[must_use] pub fn trace_identifier(&self) -> TraceId { self.inner.trace_identifier() } /// The maximum number of rounds of the tracer. #[must_use] pub fn max_rounds(&self) -> Option { self.inner.max_rounds() } /// The first time-to-live value of the tracer. #[must_use] pub fn first_ttl(&self) -> TimeToLive { self.inner.first_ttl() } /// The maximum time-to-live value of the tracer. #[must_use] pub fn max_ttl(&self) -> TimeToLive { self.inner.max_ttl() } /// The grace duration of the tracer. #[must_use] pub fn grace_duration(&self) -> Duration { self.inner.grace_duration() } /// The maximum number of in-flight probes of the tracer. #[must_use] pub fn max_inflight(&self) -> MaxInflight { self.inner.max_inflight() } /// The multipath strategy of the tracer. #[must_use] pub fn multipath_strategy(&self) -> MultipathStrategy { self.inner.multipath_strategy() } /// The port direction of the tracer. #[must_use] pub fn port_direction(&self) -> PortDirection { self.inner.port_direction() } /// The minimum round duration of the tracer. #[must_use] pub fn min_round_duration(&self) -> Duration { self.inner.min_round_duration() } /// The maximum round duration of the tracer. #[must_use] pub fn max_round_duration(&self) -> Duration { self.inner.max_round_duration() } } mod inner { use crate::config::{ChannelConfig, StateConfig, StrategyConfig}; use crate::error::Result; use crate::net::{PlatformImpl, SocketImpl}; use crate::{ Channel, Error, IcmpExtensionParseMode, MaxInflight, MaxRounds, MultipathStrategy, PacketSize, PayloadPattern, PortDirection, PrivilegeMode, Protocol, Round, Sequence, SourceAddr, State, Strategy, TimeToLive, TraceId, TypeOfService, }; use parking_lot::RwLock; use std::fmt::Debug; use std::net::IpAddr; use std::sync::OnceLock; use std::time::Duration; use tracing::instrument; use trippy_privilege::Privilege; #[derive(Debug)] pub(super) struct TracerInner { source_addr: Option, interface: Option, target_addr: IpAddr, privilege_mode: PrivilegeMode, protocol: Protocol, packet_size: PacketSize, payload_pattern: PayloadPattern, tos: TypeOfService, icmp_extension_parse_mode: IcmpExtensionParseMode, read_timeout: Duration, tcp_connect_timeout: Duration, trace_identifier: TraceId, max_rounds: Option, first_ttl: TimeToLive, max_ttl: TimeToLive, grace_duration: Duration, max_inflight: MaxInflight, initial_sequence: Sequence, multipath_strategy: MultipathStrategy, port_direction: PortDirection, min_round_duration: Duration, max_round_duration: Duration, max_samples: usize, max_flows: usize, drop_privileges: bool, state: RwLock, src: OnceLock, } impl TracerInner { #[expect(clippy::too_many_arguments)] pub(super) fn new( interface: Option, source_addr: Option, target_addr: IpAddr, privilege_mode: PrivilegeMode, protocol: Protocol, packet_size: PacketSize, payload_pattern: PayloadPattern, tos: TypeOfService, icmp_extension_parse_mode: IcmpExtensionParseMode, read_timeout: Duration, tcp_connect_timeout: Duration, trace_identifier: TraceId, max_rounds: Option, first_ttl: TimeToLive, max_ttl: TimeToLive, grace_duration: Duration, max_inflight: MaxInflight, initial_sequence: Sequence, multipath_strategy: MultipathStrategy, port_direction: PortDirection, min_round_duration: Duration, max_round_duration: Duration, max_samples: usize, max_flows: usize, drop_privileges: bool, ) -> Self { Self { source_addr, interface, target_addr, privilege_mode, protocol, packet_size, payload_pattern, tos, icmp_extension_parse_mode, read_timeout, tcp_connect_timeout, trace_identifier, max_rounds, first_ttl, max_ttl, grace_duration, max_inflight, initial_sequence, multipath_strategy, port_direction, min_round_duration, max_round_duration, max_samples, max_flows, drop_privileges, state: RwLock::new(State::new(Self::make_state_config(max_flows, max_samples))), src: OnceLock::new(), } } #[instrument(skip_all, level = "trace")] pub(super) fn run(&self) -> Result<()> { self.run_internal(|_| ()) .map_err(|err| self.handle_error(err)) } #[instrument(skip_all, level = "trace")] pub(super) fn run_with)>(&self, func: F) -> Result<()> { self.run_internal(func) .map_err(|err| self.handle_error(err)) } pub(super) fn snapshot(&self) -> State { self.state.read().clone() } pub(super) fn clear(&self) { *self.state.write() = State::new(Self::make_state_config(self.max_flows, self.max_samples)); } pub(super) const fn max_flows(&self) -> usize { self.max_flows } pub(super) const fn max_samples(&self) -> usize { self.max_samples } pub(super) const fn privilege_mode(&self) -> PrivilegeMode { self.privilege_mode } pub(super) const fn protocol(&self) -> Protocol { self.protocol } pub(super) fn interface(&self) -> Option<&str> { self.interface.as_deref() } pub(super) fn source_addr(&self) -> Option { self.src.get().copied() } pub(super) const fn target_addr(&self) -> IpAddr { self.target_addr } pub(super) const fn packet_size(&self) -> PacketSize { self.packet_size } pub(super) const fn payload_pattern(&self) -> PayloadPattern { self.payload_pattern } pub(super) const fn initial_sequence(&self) -> Sequence { self.initial_sequence } pub(super) const fn tos(&self) -> TypeOfService { self.tos } pub(super) const fn icmp_extension_parse_mode(&self) -> IcmpExtensionParseMode { self.icmp_extension_parse_mode } pub(super) const fn read_timeout(&self) -> Duration { self.read_timeout } pub(super) const fn tcp_connect_timeout(&self) -> Duration { self.tcp_connect_timeout } pub(super) const fn trace_identifier(&self) -> TraceId { self.trace_identifier } pub(super) const fn max_rounds(&self) -> Option { self.max_rounds } pub(super) const fn first_ttl(&self) -> TimeToLive { self.first_ttl } pub(super) const fn max_ttl(&self) -> TimeToLive { self.max_ttl } pub(super) const fn grace_duration(&self) -> Duration { self.grace_duration } pub(super) const fn max_inflight(&self) -> MaxInflight { self.max_inflight } pub(super) const fn multipath_strategy(&self) -> MultipathStrategy { self.multipath_strategy } pub(super) const fn port_direction(&self) -> PortDirection { self.port_direction } pub(super) const fn min_round_duration(&self) -> Duration { self.min_round_duration } pub(super) const fn max_round_duration(&self) -> Duration { self.max_round_duration } #[instrument(skip_all, level = "trace")] fn run_internal)>(&self, func: F) -> Result<()> { // if we are given a source address, validate it otherwise // discover it based on the target address and interface. let source_addr = match self.source_addr { None => SourceAddr::discover::( self.target_addr, self.port_direction, self.interface.as_deref(), )?, Some(addr) => SourceAddr::validate::(addr)?, }; self.src .set(source_addr) .map_err(|_| Error::Other(String::from("failed to set source_addr")))?; let channel_config = self.make_channel_config(source_addr); let channel = Channel::::connect(&channel_config)?; if self.drop_privileges { Privilege::drop_privileges()?; } let strategy_config = self.make_strategy_config(); let strategy = Strategy::new(&strategy_config, |round| { self.handler(round); func(round); }); strategy.run(channel)?; Ok(()) } fn handler(&self, round: &Round<'_>) { self.state.write().update_from_round(round); } fn handle_error(&self, err: Error) -> Error { self.state.write().set_error(Some(err.to_string())); err } const fn make_state_config(max_flows: usize, max_samples: usize) -> StateConfig { StateConfig { max_samples, max_flows, } } const fn make_channel_config(&self, source_addr: IpAddr) -> ChannelConfig { ChannelConfig { privilege_mode: self.privilege_mode, protocol: self.protocol, source_addr, target_addr: self.target_addr, packet_size: self.packet_size, payload_pattern: self.payload_pattern, initial_sequence: self.initial_sequence, tos: self.tos, icmp_extension_parse_mode: self.icmp_extension_parse_mode, read_timeout: self.read_timeout, tcp_connect_timeout: self.tcp_connect_timeout, } } const fn make_strategy_config(&self) -> StrategyConfig { StrategyConfig { target_addr: self.target_addr, protocol: self.protocol, trace_identifier: self.trace_identifier, max_rounds: self.max_rounds, first_ttl: self.first_ttl, max_ttl: self.max_ttl, grace_duration: self.grace_duration, max_inflight: self.max_inflight, initial_sequence: self.initial_sequence, multipath_strategy: self.multipath_strategy, port_direction: self.port_direction, min_round_duration: self.min_round_duration, max_round_duration: self.max_round_duration, } } } } ================================================ FILE: crates/trippy-core/src/types.rs ================================================ use bitflags::bitflags; use derive_more::{Add, AddAssign, Rem, Sub}; use std::num::NonZeroUsize; /// `Round` newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd, AddAssign)] pub struct RoundId(pub usize); /// `MaxRound` newtype. #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] pub struct MaxRounds(pub NonZeroUsize); /// `TimeToLive` (ttl) newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd, Add, Sub, AddAssign)] pub struct TimeToLive(pub u8); /// `Sequence` number newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd, Add, Sub, AddAssign, Rem)] pub struct Sequence(pub u16); /// `TraceId` newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)] pub struct TraceId(pub u16); /// `MaxInflight` newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)] pub struct MaxInflight(pub u8); /// `PacketSize` newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)] pub struct PacketSize(pub u16); /// `PayloadPattern` newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)] pub struct PayloadPattern(pub u8); /// `TypeOfService` (aka `DSCP` & `ECN`) newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)] pub struct TypeOfService(pub u8); /// Port newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)] pub struct Port(pub u16); /// Checksum newtype. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)] pub struct Checksum(pub u16); bitflags! { /// Probe flags. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Flags: u32 { /// Swap the checksum and payload (UDP only). const PARIS_CHECKSUM = 1; /// Encode the sequence number as the payload length (IPv6/UDP only) const DUBLIN_IPV6_PAYLOAD_LENGTH = 2; } } impl From for usize { fn from(sequence: Sequence) -> Self { sequence.0 as Self } } /// Explicit Congestion Notification (`ECN`). /// /// This is used in the `ECN` field of the `IP` header. /// /// - See [rfc3246](https://datatracker.ietf.org/doc/html/rfc3246) for more details. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Ecn { /// Not ECN-Capable Transport (00, 0 dec). NotECT, /// ECN Capable Transport(1) (01, 1 dec). ECT1, /// ECN Capable Transport(0) (10, 2 dec). ECT0, /// Congestion Experienced (11, 3 dec). CE, } /// Differentiated Services Code Point (`DSCP`). /// /// This is used in the `DSCP` field of the `IP` header. /// /// - See [rfc2474](https://datatracker.ietf.org/doc/html/rfc2474) for more details on `AFnn`. /// - See [rfc2475](https://datatracker.ietf.org/doc/html/rfc2475) and /// [rfc2476](https://datatracker.ietf.org/doc/html/rfc2476) for more details on `CSn`. /// - See [rfc3168](https://datatracker.ietf.org/doc/html/rfc3168) for more details on `CE`. /// - See [rfc5865](https://datatracker.ietf.org/doc/html/rfc5865) for more details on `VA`. /// - See [rfc8622](https://datatracker.ietf.org/doc/html/rfc8622) for more details on `LE`. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Dscp { /// Default Forwarding (000 000, 0 dec). /// /// aka Best Effort (`BE`) aka Class Selector 0 (`CS0`). /// /// See rfc2474 and 2475. DF, /// Assured Forwarding 11 (001 010, 10 dec). AF11, /// Assured Forwarding 12 (001 100, 12 dec). AF12, /// Assured Forwarding 13 (001 110, 14 dec). AF13, /// Assured Forwarding 21 (010 010, 18 dec). AF21, /// Assured Forwarding 22 (010 100, 20 dec). AF22, /// Assured Forwarding 23 (010 110, 22 dec). AF23, /// Assured Forwarding 31 (011 010, 26 dec). AF31, /// Assured Forwarding 32 (011 100, 28 dec). AF32, /// Assured Forwarding 33 (011 110, 30 dec). AF33, /// Assured Forwarding 41 (100 010, 34 dec). AF41, /// Assured Forwarding 42 (100 100, 36 dec). AF42, /// Assured Forwarding 43 (100 110, 38 dec). AF43, /// Class Selector 1 (001 000, 8 dec). CS1, /// Class Selector 2 (010 000, 16 dec). CS2, /// Class Selector 3 (011 000, 24 dec). CS3, /// Class Selector 4 (100 000, 32 dec). CS4, /// Class Selector 5 (101 000, 40 dec). CS5, /// Class Selector 6 (110 000, 48 dec). CS6, /// Class Selector 7 (111 000, 56 dec). CS7, /// High Priority Expedited Forwarding (101 110, 46 dec). EF, /// Voice Admit (101 100, 44 dec). VA, /// Lower Effort (000 001, 1 dec). LE, /// Other DSCP value (not defined in the standard). Other(u8), } impl TypeOfService { #[must_use] pub fn dscp(&self) -> Dscp { self.split().0 } #[must_use] pub fn ecn(&self) -> Ecn { self.split().1 } fn split(self) -> (Dscp, Ecn) { let dscp = match (self.0 & 0xfc) >> 2 { 0 => Dscp::DF, 10 => Dscp::AF11, 12 => Dscp::AF12, 14 => Dscp::AF13, 18 => Dscp::AF21, 20 => Dscp::AF22, 22 => Dscp::AF23, 26 => Dscp::AF31, 28 => Dscp::AF32, 30 => Dscp::AF33, 34 => Dscp::AF41, 36 => Dscp::AF42, 38 => Dscp::AF43, 8 => Dscp::CS1, 16 => Dscp::CS2, 24 => Dscp::CS3, 32 => Dscp::CS4, 40 => Dscp::CS5, 48 => Dscp::CS6, 56 => Dscp::CS7, 46 => Dscp::EF, 44 => Dscp::VA, 1 => Dscp::LE, n => Dscp::Other(n), }; let ecn = match self.0 & 0x3 { 0 => Ecn::NotECT, 1 => Ecn::ECT1, 2 => Ecn::ECT0, 3 => Ecn::CE, _ => unreachable!(), }; (dscp, ecn) } } #[cfg(test)] mod tests { use super::*; use crate::TypeOfService; use test_case::test_case; #[test_case(TypeOfService(0x0), Dscp::DF, Ecn::NotECT; "BE, Not-ECT")] #[test_case(TypeOfService(0xe0), Dscp::CS7, Ecn::NotECT; "CS7, Not-ECT")] #[test_case(TypeOfService(0xa), Dscp::Other(2), Ecn::ECT0; "Other, ECT0")] #[test_case(TypeOfService(0x8b), Dscp::AF41, Ecn::CE; "AF41, CE")] fn test_dscp_ecn(tos: TypeOfService, dscp: Dscp, ecn: Ecn) { assert_eq!(tos.dscp(), dscp); assert_eq!(tos.ecn(), ecn); } } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_icmp.toml ================================================ name = "IPv4/ICMP" target = "10.0.0.107" protocol = "Icmp" icmp_identifier = 1 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 30 } [[hops]] ttl = 4 resp = { tag = "SingleHost", addr = "10.0.0.104", rtt_ms = 40 } [[hops]] ttl = 5 resp = { tag = "SingleHost", addr = "10.0.0.105", rtt_ms = 50 } [[hops]] ttl = 6 resp = { tag = "SingleHost", addr = "10.0.0.106", rtt_ms = 60 } [[hops]] ttl = 7 resp = { tag = "SingleHost", addr = "10.0.0.107", rtt_ms = 70 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_icmp_gaps.toml ================================================ name = "IPv4/ICMP with 9 hops, 2 of which do not respond" target = "10.0.0.109" protocol = "Icmp" icmp_identifier = 3 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 20 } [[hops]] ttl = 2 resp.tag = "NoResponse" [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 20 } [[hops]] ttl = 4 resp = { tag = "SingleHost", addr = "10.0.0.104", rtt_ms = 20 } [[hops]] ttl = 5 resp = { tag = "SingleHost", addr = "10.0.0.105", rtt_ms = 20 } [[hops]] ttl = 6 resp = { tag = "SingleHost", addr = "10.0.0.106", rtt_ms = 20 } [[hops]] ttl = 7 resp = { tag = "SingleHost", addr = "10.0.0.107", rtt_ms = 20 } [[hops]] ttl = 8 resp.tag = "NoResponse" [[hops]] ttl = 9 resp = { tag = "SingleHost", addr = "10.0.0.109", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_icmp_min.toml ================================================ name = "IPv4/ICMP with a minimum packet size" target = "10.0.0.103" protocol = "Icmp" icmp_identifier = 5 packet_size = 28 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 30 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_icmp_ooo.toml ================================================ name = "IPv4/ICMP with out of order responses" target = "10.0.0.105" protocol = "Icmp" icmp_identifier = 4 grace_duration = 300 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 20 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 15 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 10 } [[hops]] ttl = 4 resp = { tag = "SingleHost", addr = "10.0.0.104", rtt_ms = 5 } [[hops]] ttl = 5 resp = { tag = "SingleHost", addr = "10.0.0.105", rtt_ms = 0 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_icmp_pattern.toml ================================================ name = "IPv4/ICMP with an alternative payload pattern (0xFF)" target = "10.0.0.103" protocol = "Icmp" icmp_identifier = 6 payload_pattern = 255 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 30 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_icmp_quick.toml ================================================ name = "IPv4/ICMP with min/max round duration 250ms and grace 50ms" target = "10.0.0.110" protocol = "Icmp" icmp_identifier = 7 min_round_duration = 250 max_round_duration = 250 grace_duration = 50 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 1 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 2 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 3 } [[hops]] ttl = 4 resp = { tag = "SingleHost", addr = "10.0.0.104", rtt_ms = 4 } [[hops]] ttl = 5 resp = { tag = "SingleHost", addr = "10.0.0.105", rtt_ms = 5 } [[hops]] ttl = 6 resp = { tag = "SingleHost", addr = "10.0.0.106", rtt_ms = 6 } [[hops]] ttl = 7 resp = { tag = "SingleHost", addr = "10.0.0.107", rtt_ms = 7 } [[hops]] ttl = 8 resp = { tag = "SingleHost", addr = "10.0.0.108", rtt_ms = 8 } [[hops]] ttl = 9 resp = { tag = "SingleHost", addr = "10.0.0.109", rtt_ms = 9 } [[hops]] ttl = 10 resp = { tag = "SingleHost", addr = "10.0.0.110", rtt_ms = 10 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_icmp_tos.toml ================================================ name = "IPv4/ICMP with a TOS" target = "10.0.0.103" protocol = "Icmp" icmp_identifier = 9 tos = 224 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 30 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_icmp_wrap.toml ================================================ name = "IPv4/ICMP wrap sequence" target = "10.0.0.130" rounds = 20 protocol = "Icmp" icmp_identifier = 8 initial_sequence = 64511 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 0 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 0 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 0 } [[hops]] ttl = 4 resp = { tag = "SingleHost", addr = "10.0.0.104", rtt_ms = 0 } [[hops]] ttl = 5 resp = { tag = "SingleHost", addr = "10.0.0.105", rtt_ms = 0 } [[hops]] ttl = 6 resp = { tag = "SingleHost", addr = "10.0.0.106", rtt_ms = 0 } [[hops]] ttl = 7 resp = { tag = "SingleHost", addr = "10.0.0.107", rtt_ms = 0 } [[hops]] ttl = 8 resp = { tag = "SingleHost", addr = "10.0.0.108", rtt_ms = 0 } [[hops]] ttl = 9 resp = { tag = "SingleHost", addr = "10.0.0.109", rtt_ms = 0 } [[hops]] ttl = 10 resp = { tag = "SingleHost", addr = "10.0.0.110", rtt_ms = 0 } [[hops]] ttl = 11 resp = { tag = "SingleHost", addr = "10.0.0.111", rtt_ms = 0 } [[hops]] ttl = 12 resp = { tag = "SingleHost", addr = "10.0.0.112", rtt_ms = 0 } [[hops]] ttl = 13 resp = { tag = "SingleHost", addr = "10.0.0.113", rtt_ms = 0 } [[hops]] ttl = 14 resp = { tag = "SingleHost", addr = "10.0.0.114", rtt_ms = 0 } [[hops]] ttl = 15 resp = { tag = "SingleHost", addr = "10.0.0.115", rtt_ms = 0 } [[hops]] ttl = 16 resp = { tag = "SingleHost", addr = "10.0.0.116", rtt_ms = 0 } [[hops]] ttl = 17 resp = { tag = "SingleHost", addr = "10.0.0.117", rtt_ms = 0 } [[hops]] ttl = 18 resp = { tag = "SingleHost", addr = "10.0.0.118", rtt_ms = 0 } [[hops]] ttl = 19 resp = { tag = "SingleHost", addr = "10.0.0.119", rtt_ms = 0 } [[hops]] ttl = 20 resp = { tag = "SingleHost", addr = "10.0.0.120", rtt_ms = 0 } [[hops]] ttl = 21 resp = { tag = "SingleHost", addr = "10.0.0.121", rtt_ms = 0 } [[hops]] ttl = 22 resp = { tag = "SingleHost", addr = "10.0.0.122", rtt_ms = 0 } [[hops]] ttl = 23 resp = { tag = "SingleHost", addr = "10.0.0.123", rtt_ms = 0 } [[hops]] ttl = 24 resp = { tag = "SingleHost", addr = "10.0.0.124", rtt_ms = 0 } [[hops]] ttl = 25 resp = { tag = "SingleHost", addr = "10.0.0.125", rtt_ms = 0 } [[hops]] ttl = 26 resp = { tag = "SingleHost", addr = "10.0.0.126", rtt_ms = 0 } [[hops]] ttl = 27 resp = { tag = "SingleHost", addr = "10.0.0.127", rtt_ms = 0 } [[hops]] ttl = 28 resp = { tag = "SingleHost", addr = "10.0.0.128", rtt_ms = 0 } [[hops]] ttl = 29 resp = { tag = "SingleHost", addr = "10.0.0.129", rtt_ms = 0 } [[hops]] ttl = 30 resp = { tag = "SingleHost", addr = "10.0.0.130", rtt_ms = 0 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_tcp_fixed_dest.toml ================================================ name = "IPv4/TCP with a fixed dest port" target = "10.0.0.103" protocol = "Tcp" port_direction = { tag = "FixedDest", value = 80 } tos = 224 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_fixed_dest.toml ================================================ name = "IPv4/UDP classic with a fixed dest port" target = "10.0.0.103" protocol = "Udp" port_direction = { tag = "FixedDest", value = 33434 } multipath_strategy = "Classic" [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_fixed_src.toml ================================================ name = "IPv4/UDP classic with a fixed src port" target = "10.0.0.103" protocol = "Udp" port_direction = { tag = "FixedSrc", value = 5000 } multipath_strategy = "Classic" [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_privileged_tos.toml ================================================ name = "IPv4/UDP classic privileged with a TOS" target = "10.0.0.103" protocol = "Udp" port_direction = { tag = "FixedDest", value = 33434 } multipath_strategy = "Classic" tos = 224 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_unprivileged.toml ================================================ name = "IPv4/UDP classic unprivileged" privilege_mode = "Unprivileged" target = "10.0.0.103" protocol = "Udp" port_direction = { tag = "FixedDest", value = 33434 } multipath_strategy = "Classic" [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_unprivileged_tos.toml ================================================ name = "IPv4/UDP classic unprivileged with a TOS" privilege_mode = "Unprivileged" target = "10.0.0.103" protocol = "Udp" port_direction = { tag = "FixedDest", value = 33434 } multipath_strategy = "Classic" tos = 224 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_udp_dublin_fixed_both.toml ================================================ name = "IPv4/UDP Dublin with a fixed src and dest port" target = "10.0.0.103" protocol = "Udp" multipath_strategy = "Dublin" port_direction = { tag = "FixedBoth", value = { src = 5000, dest = 33434 } } [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv4_udp_paris_fixed_both.toml ================================================ name = "IPv4/UDP Paris with a fixed src and dest port" target = "10.0.0.103" protocol = "Udp" multipath_strategy = "Paris" port_direction = { tag = "FixedBoth", value = { src = 5000, dest = 33434 } } [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "10.0.0.101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "10.0.0.102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "10.0.0.103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_icmp.toml ================================================ name = "IPv6/ICMP" target = "fd00:10::107" protocol = "Icmp" icmp_identifier = 1 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 30 } [[hops]] ttl = 4 resp = { tag = "SingleHost", addr = "fd00:10::104", rtt_ms = 40 } [[hops]] ttl = 5 resp = { tag = "SingleHost", addr = "fd00:10::105", rtt_ms = 50 } [[hops]] ttl = 6 resp = { tag = "SingleHost", addr = "fd00:10::106", rtt_ms = 60 } [[hops]] ttl = 7 resp = { tag = "SingleHost", addr = "fd00:10::107", rtt_ms = 70 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_icmp_min.toml ================================================ name = "IPv6/ICMP with a minimum packet size" target = "fd00:10::103" protocol = "Icmp" icmp_identifier = 5 packet_size = 48 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 30 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_icmp_pattern.toml ================================================ name = "IPv6/ICMP with an alternative payload pattern (0xFF)" target = "fd00:10::103" protocol = "Icmp" icmp_identifier = 6 payload_pattern = 255 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 30 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_tcp_fixed_dest.toml ================================================ name = "IPv6/TCP with a fixed dest port" target = "fd00:10::103" protocol = "Tcp" port_direction = { tag = "FixedDest", value = 80 } [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_udp_classic_fixed_dest.toml ================================================ name = "IPv6/UDP classic with a fixed dest port" target = "fd00:10::103" protocol = "Udp" port_direction = { tag = "FixedDest", value = 33434 } multipath_strategy = "Classic" [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_udp_classic_fixed_src.toml ================================================ name = "IPv6/UDP classic with a fixed src port" target = "fd00:10::103" protocol = "Udp" port_direction = { tag = "FixedSrc", value = 5000 } multipath_strategy = "Classic" [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_udp_classic_unprivileged.toml ================================================ name = "IPv6/UDP classic unprivileged" privilege_mode = "Unprivileged" target = "fd00:10::103" protocol = "Udp" port_direction = { tag = "FixedDest", value = 33434 } multipath_strategy = "Classic" [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_udp_classic_unprivileged_tos.toml ================================================ name = "IPv6/UDP classic unprivileged with a TOS" privilege_mode = "Unprivileged" target = "fd00:10::103" protocol = "Udp" port_direction = { tag = "FixedDest", value = 33434 } multipath_strategy = "Classic" tos = 224 [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_udp_dublin_fixed_both.toml ================================================ name = "IPv6/UDP Dublin with a fixed src and dest port" target = "fd00:10::103" protocol = "Udp" multipath_strategy = "Dublin" port_direction = { tag = "FixedBoth", value = { src = 5000, dest = 33434 } } [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/simulation/ipv6_udp_paris_fixed_both.toml ================================================ name = "IPv6/UDP Paris with a fixed src and dest port" target = "fd00:10::103" protocol = "Udp" multipath_strategy = "Paris" port_direction = { tag = "FixedBoth", value = { src = 5000, dest = 33434 } } [[hops]] ttl = 1 resp = { tag = "SingleHost", addr = "fd00:10::101", rtt_ms = 10 } [[hops]] ttl = 2 resp = { tag = "SingleHost", addr = "fd00:10::102", rtt_ms = 20 } [[hops]] ttl = 3 resp = { tag = "SingleHost", addr = "fd00:10::103", rtt_ms = 20 } ================================================ FILE: crates/trippy-core/tests/resources/state/all_status.toml ================================================ largest_ttl = 1 [[rounds]] probes = [ "1 A 300 10.1.0.2 0 12340 80 0 0 0", ] [[rounds]] probes = [ "1 C 700 10.1.0.2 0 12340 80 0 0 0", ] [[rounds]] probes = [ "1 N 300 10.1.0.2 0 12340 80 0 0 0", ] [[rounds]] probes = [ "1 S 300 10.1.0.2 0 12340 80 0 0 0", ] [[expected.hops]] ttl = 1 total_sent = 2 total_recv = 1 loss_pct = 50 best_ms = 700 worst_ms = 700 avg_ms = 700 samples = [700.0, 0.0] last_ms = 700 last_sequence = 0 last_src = 12340 last_dest = 80 last_nat_status = "no_nat" tos = 0 addrs = { "10.1.0.2" = 1 } ================================================ FILE: crates/trippy-core/tests/resources/state/floss_bloss.toml ================================================ largest_ttl = 3 [[rounds]] probes = [ "1 C 333 10.1.0.1 0 12340 80 0 0 0", "2 C 777 10.1.0.2 1 12340 80 0 0 0", "3 C 778 10.1.0.3 2 12340 80 0 0 0", ] [[rounds]] probes = [ "1 C 333 10.1.0.1 3 12340 80 0 0 0", "2 A 777 10.1.0.2 4 12340 80 0 0 0", "3 A 778 10.1.0.3 5 12340 80 0 0 0", ] [[rounds]] probes = [ "1 C 333 10.1.0.1 6 12340 80 0 0 0", "2 C 777 10.1.0.2 7 12340 80 0 0 0", "3 C 778 10.1.0.3 8 12340 80 0 0 0", ] [[expected.hops]] ttl = 1 total_sent = 3 total_recv = 3 total_forward_loss = 0 total_backward_loss = 0 [[expected.hops]] ttl = 2 total_sent = 3 total_recv = 2 total_forward_loss = 1 total_backward_loss = 0 [[expected.hops]] ttl = 3 total_sent = 3 total_recv = 2 total_forward_loss = 0 total_backward_loss = 1 ================================================ FILE: crates/trippy-core/tests/resources/state/full_completed.toml ================================================ largest_ttl = 3 [[rounds]] probes = [ "1 C 333 10.1.0.1 0 12340 80 0 0 0", "2 C 777 10.1.0.2 1 12340 80 0 0 0", "3 C 778 10.1.0.3 2 12340 80 0 0 0", ] [[rounds]] probes = [ "1 C 123 10.1.0.1 3 12340 80 0 0 0", "2 C 788 10.1.0.2 4 12340 80 0 0 0", "3 C 789 10.1.0.3 5 12340 80 0 0 0", ] [[rounds]] probes = [ "1 C 123 10.1.0.1 6 12340 80 0 0 0", "2 C 780 10.1.0.2 7 12340 80 0 0 0", "3 C 781 10.1.0.3 8 12340 80 0 0 0", ] [[expected.hops]] ttl = 1 total_sent = 3 total_recv = 3 loss_pct = 0 best_ms = 123 worst_ms = 333 avg_ms = 193 jitter = 0 javg = 181.0 jinta = 488.642578125 jmax = 333 last_nat_status = "no_nat" samples = [123, 123, 333] last_ms = 123 last_sequence = 6 last_src = 12340 last_dest = 80 tos = 0 addrs = { "10.1.0.1" = 3 } [[expected.hops]] ttl = 2 total_sent = 3 total_recv = 3 loss_pct = 0 best_ms = 777 worst_ms = 788 avg_ms = 781.6666666666665 jitter = 8 javg = 265.33333333333337 jinta = 699.814453125 jmax = 777.0 last_nat_status = "no_nat" samples = [780, 788, 777] last_ms = 780 last_sequence = 7 last_src = 12340 last_dest = 80 tos = 0 addrs = { "10.1.0.2" = 3 } [[expected.hops]] ttl = 3 total_sent = 3 total_recv = 3 loss_pct = 0 best_ms = 778 worst_ms = 789 avg_ms = 782.6666666666666 jitter = 8 javg = 265.66666666666663 jinta = 700.693359375 jmax = 778.0 last_nat_status = "no_nat" samples = [781, 789, 778] last_ms = 781 last_sequence = 8 last_src = 12340 last_dest = 80 tos = 0 addrs = { "10.1.0.3" = 3 } ================================================ FILE: crates/trippy-core/tests/resources/state/full_mixed.toml ================================================ largest_ttl = 3 [[rounds]] probes = [ "1 C 10 10.0.0.1 0 12345 80 0 0 0", "2 A 12 10.0.0.2 1 12345 80 0 0 0", "3 C 11 10.0.0.3 2 12345 80 0 0 0", ] [[rounds]] probes = [ "1 C 101 10.0.0.1 3 12345 80 0 0 0", "2 A 121 10.0.0.2 4 12345 80 0 0 0", "3 C 111 10.0.0.4 5 12345 80 0 0 0", ] [[expected.hops]] ttl = 1 total_sent = 2 total_recv = 2 loss_pct = 0 best_ms = 10 worst_ms = 101 avg_ms = 55.5 jitter = 91 javg = 50.5 jinta = 99.40625 jmax = 91 last_nat_status = "no_nat" samples = [101, 10] last_ms = 101 last_src = 12345 last_dest = 80 last_sequence = 3 tos = 0 addrs = { "10.0.0.1" = 2 } [[expected.hops]] ttl = 2 total_sent = 2 total_recv = 0 loss_pct = 100 avg_ms = 0 javg = 0 jinta = 0 last_nat_status = "none" samples = [0, 0] last_src = 12345 last_dest = 80 last_sequence = 4 [[expected.hops]] ttl = 3 total_sent = 2 total_recv = 2 loss_pct = 0 best_ms = 11 worst_ms = 111 avg_ms = 61 jitter = 100 javg = 55.5 jinta = 109.34375 jmax = 100 last_nat_status = "no_nat" samples = [111, 11] last_ms = 111 last_src = 12345 last_dest = 80 last_sequence = 5 tos = 0 addrs = { "10.0.0.3" = 1, "10.0.0.4" = 1 } ================================================ FILE: crates/trippy-core/tests/resources/state/minimal.toml ================================================ largest_ttl = 3 [[rounds]] probes = [ "1 C 333 10.1.0.1 0 12340 80 0 0 0", "2 C 777 10.1.0.2 1 12340 80 0 0 0", "3 C 778 10.1.0.3 2 12340 80 0 0 0", ] [[expected.hops]] ttl = 1 [[expected.hops]] ttl = 2 [[expected.hops]] ttl = 3 ================================================ FILE: crates/trippy-core/tests/resources/state/nat.toml ================================================ largest_ttl = 3 [[rounds]] probes = [ "1 C 333 10.1.0.1 0 12340 80 43012 43012 0", "2 C 777 10.1.0.2 1 12340 80 20544 20544 0", "3 C 778 10.1.0.3 2 12340 80 20544 20544 0", ] [[rounds]] probes = [ "1 C 123 10.1.0.1 3 12340 80 43012 43012 0", "2 C 788 10.1.0.2 4 12340 80 20544 20544 0", "3 C 789 10.1.0.3 5 12340 80 20544 20544 0", ] [[rounds]] probes = [ "1 C 123 10.1.0.1 6 12340 80 43012 43012 0", "2 C 780 10.1.0.2 7 12340 80 20544 20544 0", "3 C 781 10.1.0.3 8 12340 80 20544 20544 0", ] [[expected.hops]] ttl = 1 last_nat_status = "no_nat" [[expected.hops]] ttl = 2 last_nat_status = "nat" [[expected.hops]] ttl = 3 last_nat_status = "no_nat" ================================================ FILE: crates/trippy-core/tests/resources/state/no_latency.toml ================================================ largest_ttl = 1 [[rounds]] probes = ["1 C 0 127.0.0.1 0 80 80 0 0 0"] [[rounds]] probes = ["1 C 0 127.0.0.1 0 80 80 0 0 0"] [[rounds]] probes = ["1 C 0 127.0.0.1 0 80 80 0 0 0"] [[rounds]] probes = ["1 C 0 127.0.0.1 0 80 80 0 0 0"] [[expected.hops]] ttl = 1 loss_pct = 0 last_ms = 0.0 best_ms = 0 worst_ms = 0 avg_ms = 0 samples = [0, 0, 0, 0] jitter = 0 javg = 0.0 jmax = 0.0 jinta = 0.0 ================================================ FILE: crates/trippy-core/tests/resources/state/non_default_minimum_ttl.toml ================================================ largest_ttl = 5 [[rounds]] probes = [ "4 A 333 10.1.0.1 0 12340 80 0 0 0", "5 A 777 10.1.0.2 1 12340 80 0 0 0", ] [[expected.hops]] ttl = 4 total_sent = 1 total_recv = 0 total_forward_loss = 1 total_backward_loss = 0 [[expected.hops]] ttl = 5 total_sent = 1 total_recv = 0 total_forward_loss = 0 total_backward_loss = 1 ================================================ FILE: crates/trippy-core/tests/resources/state/tos.toml ================================================ largest_ttl = 3 [[rounds]] probes = [ "1 C 333 10.1.0.1 0 12340 80 0 0 224", "2 A 777 10.1.0.2 1 12340 80 0 0 0", ] [[expected.hops]] ttl = 1 tos = 224 [[expected.hops]] ttl = 2 ================================================ FILE: crates/trippy-core/tests/sim/main.rs ================================================ #![cfg(all( feature = "sim-tests", any(target_os = "linux", target_os = "macos", target_os = "windows") ))] mod network; mod simulation; mod tests; mod tracer; mod tun_device; ================================================ FILE: crates/trippy-core/tests/sim/network/ipv4.rs ================================================ use crate::simulation::{Protocol, Response, Simulation, SingleHost}; use std::net::{IpAddr, Ipv4Addr}; use tracing::{debug, info}; use trippy_packet::IpProtocol; use trippy_packet::checksum::{icmp_ipv4_checksum, ipv4_header_checksum, tcp_ipv4_checksum}; use trippy_packet::icmpv4::destination_unreachable::DestinationUnreachablePacket; use trippy_packet::icmpv4::echo_reply::EchoReplyPacket; use trippy_packet::icmpv4::echo_request::EchoRequestPacket; use trippy_packet::icmpv4::time_exceeded::TimeExceededPacket; use trippy_packet::icmpv4::{IcmpCode, IcmpType}; use trippy_packet::ipv4::Ipv4Packet; use trippy_packet::tcp::TcpPacket; use trippy_packet::udp::UdpPacket; #[expect(clippy::too_many_lines)] pub fn process(sim: &Simulation, packet_buf: &[u8]) -> anyhow::Result)>> { let ipv4 = Ipv4Packet::new_view(packet_buf)?; debug!("read: {:?}", ipv4); let orig_datagram_length = usize::from(ipv4.get_header_length() * 4) + 8; match (ipv4.get_protocol(), sim.protocol) { (IpProtocol::Icmp, Protocol::Icmp) => { let echo_request = EchoRequestPacket::new_view(ipv4.payload())?; if echo_request.get_identifier() != sim.icmp_identifier { debug!( "skipping EchoRequest with unexpected id (exp={} act={}))", echo_request.get_identifier(), sim.icmp_identifier ); return Ok(None); } debug!("payload in: {:?}", echo_request); info!( "received EchoRequest with ttl={} id={} seq={}", ipv4.get_ttl(), echo_request.get_identifier(), echo_request.get_sequence() ); } (IpProtocol::Udp, Protocol::Udp) => { let udp = UdpPacket::new_view(ipv4.payload())?; debug!("payload in: {:?}", udp); info!( "received UdpPacket with ttl={} src={} dest={}", ipv4.get_ttl(), udp.get_source(), udp.get_destination() ); } (IpProtocol::Tcp, Protocol::Tcp) => { let tcp = TcpPacket::new_view(ipv4.payload())?; debug!("payload in: {:?}", tcp); info!( "received TcpPacket with ttl={} src={} dest={}", ipv4.get_ttl(), tcp.get_source(), tcp.get_destination() ); } _ => { return Ok(None); } } // if the ttl is greater than the largest ttl in our sim we will reply as the last node in // the sim let index = std::cmp::min(usize::from(ipv4.get_ttl()) - 1, sim.hops.len() - 1); let (reply_addr, reply_delay_ms) = match sim.hops[index].resp { Response::NoResponse => { return Ok(None); } Response::SingleHost(SingleHost { addr: IpAddr::V4(addr), rtt_ms, }) => (addr, rtt_ms), Response::SingleHost(SingleHost { addr, .. }) => anyhow::bail!( "invalid simulation hop {}: expected IPv4 responder, got {}", index + 1, addr ), }; // decide what response to send let (protocol, payload) = if IpAddr::V4(reply_addr) == sim.target { match sim.protocol { Protocol::Icmp => { info!( "sending ICMP EchoReply from {} to {} for ttl {} after {}ms delay", reply_addr, ipv4.get_source(), ipv4.get_ttl(), reply_delay_ms, ); let echo_request = EchoRequestPacket::new_view(ipv4.payload())?; let mut packet_buf = vec![0_u8; EchoReplyPacket::minimum_packet_size()]; let packet = make_echo_reply( &mut packet_buf, sim.icmp_identifier, echo_request.get_sequence(), )?; debug!("payload out: {:?}", packet); (IpProtocol::Icmp, packet_buf) } Protocol::Udp => { info!( "sending ICMP DestinationUnreachable from {} to {} for ttl {} after {}ms delay", reply_addr, ipv4.get_source(), ipv4.get_ttl(), reply_delay_ms, ); let length = DestinationUnreachablePacket::minimum_packet_size() + orig_datagram_length; let mut packet_buf = vec![0_u8; length]; let packet = make_destination_unreachable( &mut packet_buf, &ipv4.packet()[..orig_datagram_length], )?; debug!("payload out: {:?}", packet); (IpProtocol::Icmp, packet_buf) } Protocol::Tcp => { info!( "sending TCP syn+ack from {} to {} for ttl {} after {}ms delay", reply_addr, ipv4.get_source(), ipv4.get_ttl(), reply_delay_ms, ); let tcp_in = TcpPacket::new_view(ipv4.payload())?; let mut packet_buf = vec![0_u8; TcpPacket::minimum_packet_size()]; let packet = make_tcp_syn_ack(&mut packet_buf, &ipv4, &tcp_in)?; debug!("payload out: {:?}", packet); (IpProtocol::Tcp, packet_buf) } } } else { info!( "sending ICMP TimeExceeded from {} to {} for ttl {} after {}ms delay", reply_addr, ipv4.get_source(), ipv4.get_ttl(), reply_delay_ms, ); let length = TimeExceededPacket::minimum_packet_size() + orig_datagram_length; let mut packet_buf = vec![0_u8; length]; let packet = make_time_exceeded(&mut packet_buf, &ipv4.packet()[..orig_datagram_length])?; debug!("payload out: {:?}", packet); (IpProtocol::Icmp, packet_buf) }; let ipv4_length = Ipv4Packet::minimum_packet_size() + payload.len(); let mut ipv4_buf = vec![0_u8; ipv4_length]; make_ip( &mut ipv4_buf, reply_addr, ipv4.get_source(), protocol, &payload, )?; Ok(Some((reply_delay_ms, ipv4_buf))) } fn make_time_exceeded<'a>( buf: &'a mut [u8], payload: &[u8], ) -> anyhow::Result> { let mut packet = TimeExceededPacket::new(buf)?; packet.set_icmp_type(IcmpType::TimeExceeded); packet.set_icmp_code(IcmpCode(0)); packet.set_payload(payload); packet.set_checksum(icmp_ipv4_checksum(packet.packet())); Ok(packet) } fn make_echo_reply( buf: &mut [u8], icmp_identifier: u16, sequence: u16, ) -> anyhow::Result> { let mut packet = EchoReplyPacket::new(buf)?; packet.set_icmp_type(IcmpType::EchoReply); packet.set_icmp_code(IcmpCode(0)); packet.set_identifier(icmp_identifier); packet.set_sequence(sequence); packet.set_checksum(icmp_ipv4_checksum(packet.packet())); Ok(packet) } fn make_destination_unreachable<'a>( buf: &'a mut [u8], payload: &[u8], ) -> anyhow::Result> { let mut packet = DestinationUnreachablePacket::new(buf)?; packet.set_icmp_type(IcmpType::DestinationUnreachable); packet.set_icmp_code(IcmpCode(3)); packet.set_payload(payload); packet.set_checksum(icmp_ipv4_checksum(packet.packet())); Ok(packet) } fn make_tcp_syn_ack<'a>( buf: &'a mut [u8], ipv4: &Ipv4Packet<'_>, tcp_in: &TcpPacket<'_>, ) -> anyhow::Result> { let mut packet = TcpPacket::new(buf)?; packet.set_data_offset(5); packet.set_source(tcp_in.get_destination()); packet.set_destination(tcp_in.get_source()); packet.set_sequence(0); packet.set_acknowledgement(tcp_in.get_sequence() + 1); packet.set_flags(0b0001_0010); packet.set_window_size(0xFFFF); packet.set_checksum(tcp_ipv4_checksum( packet.packet(), ipv4.get_destination(), ipv4.get_source(), )); Ok(packet) } fn make_ip<'a>( buf: &'a mut [u8], source: Ipv4Addr, destination: Ipv4Addr, protocol: IpProtocol, payload: &[u8], ) -> anyhow::Result> { let ipv4_total_length = buf.len(); let mut packet = Ipv4Packet::new(buf)?; packet.set_version(4); packet.set_header_length(5); packet.set_protocol(protocol); packet.set_ttl(64); packet.set_source(source); packet.set_destination(destination); packet.set_total_length(u16::try_from(ipv4_total_length)?); packet.set_checksum(ipv4_header_checksum( &packet.packet()[..Ipv4Packet::minimum_packet_size()], )); packet.set_payload(payload); Ok(packet) } ================================================ FILE: crates/trippy-core/tests/sim/network/ipv6.rs ================================================ use crate::simulation::{Protocol, Response, Simulation, SingleHost}; use std::net::{IpAddr, Ipv6Addr}; use tracing::{debug, info}; use trippy_packet::IpProtocol; use trippy_packet::checksum::{icmp_ipv6_checksum, tcp_ipv6_checksum}; use trippy_packet::icmpv6::destination_unreachable::DestinationUnreachablePacket; use trippy_packet::icmpv6::echo_reply::EchoReplyPacket; use trippy_packet::icmpv6::echo_request::EchoRequestPacket; use trippy_packet::icmpv6::time_exceeded::TimeExceededPacket; use trippy_packet::icmpv6::{IcmpCode, IcmpType}; use trippy_packet::ipv6::Ipv6Packet; use trippy_packet::tcp::TcpPacket; use trippy_packet::udp::UdpPacket; #[expect(clippy::too_many_lines)] pub fn process(sim: &Simulation, packet_buf: &[u8]) -> anyhow::Result)>> { let ipv6 = Ipv6Packet::new_view(packet_buf)?; debug!("read: {:?}", ipv6); let orig_datagram_length = Ipv6Packet::minimum_packet_size() + ipv6.payload().len(); match (ipv6.get_next_header(), sim.protocol) { (IpProtocol::IcmpV6, Protocol::Icmp) => { let echo_request = EchoRequestPacket::new_view(ipv6.payload())?; if echo_request.get_identifier() != sim.icmp_identifier { debug!( "skipping EchoRequest with unexpected id (exp={} act={}))", echo_request.get_identifier(), sim.icmp_identifier ); return Ok(None); } debug!("payload in: {:?}", echo_request); info!( "received EchoRequest with hop_limit={} id={} seq={}", ipv6.get_hop_limit(), echo_request.get_identifier(), echo_request.get_sequence() ); } (IpProtocol::Udp, Protocol::Udp) => { let udp = UdpPacket::new_view(ipv6.payload())?; debug!("payload in: {:?}", udp); info!( "received UdpPacket with hop_limit={} src={} dest={}", ipv6.get_hop_limit(), udp.get_source(), udp.get_destination() ); } (IpProtocol::Tcp, Protocol::Tcp) => { let tcp = TcpPacket::new_view(ipv6.payload())?; debug!("payload in: {:?}", tcp); info!( "received TcpPacket with hop_limit={} src={} dest={}", ipv6.get_hop_limit(), tcp.get_source(), tcp.get_destination() ); } _ => { return Ok(None); } } // if the hop limit is greater than the largest ttl in our sim we will reply as the last node in // the sim let index = std::cmp::min(usize::from(ipv6.get_hop_limit()) - 1, sim.hops.len() - 1); let (reply_addr, reply_delay_ms) = match sim.hops[index].resp { Response::NoResponse => { return Ok(None); } Response::SingleHost(SingleHost { addr: IpAddr::V6(addr), rtt_ms, }) => (addr, rtt_ms), Response::SingleHost(SingleHost { addr, .. }) => anyhow::bail!( "invalid simulation hop {}: expected IPv6 responder, got {}", index + 1, addr ), }; // decide what response to send let (next_header, payload) = if IpAddr::V6(reply_addr) == sim.target { match sim.protocol { Protocol::Icmp => { info!( "sending ICMPv6 EchoReply from {} to {} for hop_limit {} after {}ms delay", reply_addr, ipv6.get_source_address(), ipv6.get_hop_limit(), reply_delay_ms, ); let echo_request = EchoRequestPacket::new_view(ipv6.payload())?; let mut packet_buf = vec![0_u8; EchoReplyPacket::minimum_packet_size()]; let packet = make_echo_reply( &mut packet_buf, reply_addr, ipv6.get_source_address(), sim.icmp_identifier, echo_request.get_sequence(), )?; debug!("payload out: {:?}", packet); (IpProtocol::IcmpV6, packet_buf) } Protocol::Udp => { info!( "sending ICMPv6 DestinationUnreachable from {} to {} for hop_limit {} after {}ms delay", reply_addr, ipv6.get_source_address(), ipv6.get_hop_limit(), reply_delay_ms, ); let length = DestinationUnreachablePacket::minimum_packet_size() + orig_datagram_length; let mut packet_buf = vec![0_u8; length]; let packet = make_destination_unreachable( &mut packet_buf, reply_addr, ipv6.get_source_address(), &ipv6.packet()[..orig_datagram_length], )?; debug!("payload out: {:?}", packet); (IpProtocol::IcmpV6, packet_buf) } Protocol::Tcp => { info!( "sending TCP syn+ack from {} to {} for hop_limit {} after {}ms delay", reply_addr, ipv6.get_source_address(), ipv6.get_hop_limit(), reply_delay_ms, ); let tcp_in = TcpPacket::new_view(ipv6.payload())?; let mut packet_buf = vec![0_u8; TcpPacket::minimum_packet_size()]; let packet = make_tcp_syn_ack(&mut packet_buf, &ipv6, &tcp_in)?; debug!("payload out: {:?}", packet); (IpProtocol::Tcp, packet_buf) } } } else { info!( "sending ICMPv6 TimeExceeded from {} to {} for hop_limit {} after {}ms delay", reply_addr, ipv6.get_source_address(), ipv6.get_hop_limit(), reply_delay_ms, ); let length = TimeExceededPacket::minimum_packet_size() + orig_datagram_length; let mut packet_buf = vec![0_u8; length]; let packet = make_time_exceeded( &mut packet_buf, reply_addr, ipv6.get_source_address(), &ipv6.packet()[..orig_datagram_length], )?; debug!("payload out: {:?}", packet); (IpProtocol::IcmpV6, packet_buf) }; let ipv6_length = Ipv6Packet::minimum_packet_size() + payload.len(); let mut ipv6_buf = vec![0_u8; ipv6_length]; make_ip( &mut ipv6_buf, reply_addr, ipv6.get_source_address(), next_header, &payload, )?; Ok(Some((reply_delay_ms, ipv6_buf))) } fn make_time_exceeded<'a>( buf: &'a mut [u8], source: Ipv6Addr, destination: Ipv6Addr, payload: &[u8], ) -> anyhow::Result> { let mut packet = TimeExceededPacket::new(buf)?; packet.set_icmp_type(IcmpType::TimeExceeded); packet.set_icmp_code(IcmpCode(0)); packet.set_payload(payload); packet.set_checksum(icmp_ipv6_checksum(packet.packet(), source, destination)); Ok(packet) } fn make_echo_reply( buf: &mut [u8], source: Ipv6Addr, destination: Ipv6Addr, icmp_identifier: u16, sequence: u16, ) -> anyhow::Result> { let mut packet = EchoReplyPacket::new(buf)?; packet.set_icmp_type(IcmpType::EchoReply); packet.set_icmp_code(IcmpCode(0)); packet.set_identifier(icmp_identifier); packet.set_sequence(sequence); packet.set_checksum(icmp_ipv6_checksum(packet.packet(), source, destination)); Ok(packet) } fn make_destination_unreachable<'a>( buf: &'a mut [u8], source: Ipv6Addr, destination: Ipv6Addr, payload: &[u8], ) -> anyhow::Result> { let mut packet = DestinationUnreachablePacket::new(buf)?; packet.set_icmp_type(IcmpType::DestinationUnreachable); packet.set_icmp_code(IcmpCode(4)); packet.set_payload(payload); packet.set_checksum(icmp_ipv6_checksum(packet.packet(), source, destination)); Ok(packet) } fn make_tcp_syn_ack<'a>( buf: &'a mut [u8], ipv6: &Ipv6Packet<'_>, tcp_in: &TcpPacket<'_>, ) -> anyhow::Result> { let mut packet = TcpPacket::new(buf)?; packet.set_data_offset(5); packet.set_source(tcp_in.get_destination()); packet.set_destination(tcp_in.get_source()); packet.set_sequence(0); packet.set_acknowledgement(tcp_in.get_sequence() + 1); packet.set_flags(0b0001_0010); packet.set_window_size(0xFFFF); packet.set_checksum(tcp_ipv6_checksum( packet.packet(), ipv6.get_destination_address(), ipv6.get_source_address(), )); Ok(packet) } fn make_ip<'a>( buf: &'a mut [u8], source: Ipv6Addr, destination: Ipv6Addr, next_header: IpProtocol, payload: &[u8], ) -> anyhow::Result> { let mut packet = Ipv6Packet::new(buf)?; packet.set_version(6); packet.set_traffic_class(0); packet.set_flow_label(0); packet.set_payload_length(u16::try_from(payload.len())?); packet.set_next_header(next_header); packet.set_hop_limit(64); packet.set_source_address(source); packet.set_destination_address(destination); packet.set_payload(payload); Ok(packet) } ================================================ FILE: crates/trippy-core/tests/sim/network.rs ================================================ mod ipv4; mod ipv6; use crate::simulation::Simulation; use crate::tun_device::TunDevice; use futures_concurrency::future::Race; use std::io; use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tracing::debug; use trippy_packet::ip::{IpPacket, IpVersion}; use trippy_packet::ipv4::Ipv4Packet; use trippy_packet::ipv6::Ipv6Packet; const READ_TIMEOUT: Duration = Duration::from_millis(10); pub async fn run( tun: Arc>, sim: Arc, token: CancellationToken, ) -> anyhow::Result<()> { let mut handles: Vec> = vec![]; let expected_version = match sim.target { IpAddr::V4(_) => IpVersion::Ipv4, IpAddr::V6(_) => IpVersion::Ipv6, }; loop { let mut buf = [0_u8; 4096]; let Some(bytes_read) = { let tun = tun.clone(); ( async { token.cancelled().await; Ok::, io::Error>(None) }, async { read_with_timeout(&mut buf, tun.clone()).await.map(Some) }, ) .race() .await }? else { for h in handles { h.abort(); } return Ok(()); }; if bytes_read == 0 { continue; } let ip = IpPacket::new_view(&buf[..bytes_read]).expect("valid IP packet"); match ip.get_version() { IpVersion::Ipv4 => { if expected_version != IpVersion::Ipv4 { debug!( "skipping IPv4 packet while expecting {:?} packets", expected_version ); continue; } if let Some((reply_delay_ms, packet_buf)) = ipv4::process(sim.as_ref(), ip.packet())? { handles.push(tokio::spawn(write_packet( tun.clone(), reply_delay_ms, packet_buf, IpVersion::Ipv4, ))); } } IpVersion::Ipv6 => { if expected_version != IpVersion::Ipv6 { debug!( "skipping IPv6 packet while expecting {:?} packets", expected_version ); continue; } if let Some((reply_delay_ms, packet_buf)) = ipv6::process(sim.as_ref(), ip.packet())? { handles.push(tokio::spawn(write_packet( tun.clone(), reply_delay_ms, packet_buf, IpVersion::Ipv6, ))); } } IpVersion::Other(version) => { debug!("skipping unknown IP version packet: {}", version); } } } } /// Read from the tun device with a timeout. /// /// Note that the tun device is only locked for the timeout period async fn read_with_timeout(buf: &mut [u8], tun: Arc>) -> io::Result { tokio::time::timeout(READ_TIMEOUT, tun.lock().await.read(buf)) .await .unwrap_or(Ok(0)) } async fn write_packet( tun: Arc>, reply_delay_ms: u16, packet_buf: Vec, version: IpVersion, ) { tokio::time::sleep(Duration::from_millis(u64::from(reply_delay_ms))).await; match version { IpVersion::Ipv4 => { let packet = Ipv4Packet::new_view(&packet_buf).expect("valid ipv4 packet"); debug!("write: {:?}", packet); tun.lock().await.write(packet.packet()).await.expect("send"); } IpVersion::Ipv6 => { let packet = Ipv6Packet::new_view(&packet_buf).expect("valid ipv6 packet"); debug!("write: {:?}", packet); tun.lock().await.write(packet.packet()).await.expect("send"); } IpVersion::Other(version) => { panic!("unexpected packet version: {version}"); } } } ================================================ FILE: crates/trippy-core/tests/sim/simulation.rs ================================================ use serde::Deserialize; use std::net::IpAddr; use trippy_core::Port; /// A simulated trace. #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct Simulation { pub name: String, pub rounds: Option, #[serde(default)] pub privilege_mode: PrivilegeMode, pub target: IpAddr, pub protocol: Protocol, #[serde(default)] pub port_direction: PortDirection, #[serde(default)] pub multipath_strategy: MultipathStrategy, #[serde(default)] pub icmp_identifier: u16, pub initial_sequence: Option, pub packet_size: Option, pub payload_pattern: Option, pub tos: Option, pub min_round_duration: Option, pub max_round_duration: Option, pub grace_duration: Option, pub hops: Vec, } impl Simulation { pub fn latest_ttl(&self) -> u8 { if self.hops.is_empty() { 0 } else { self.hops[self.hops.len() - 1].ttl } } } /// A simulated hop. #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct Hop { /// The simulated time-to-live (TTL). pub ttl: u8, /// The simulated probe response. pub resp: Response, } /// A simulated probe response. #[derive(Debug, Deserialize)] #[serde(tag = "tag")] pub enum Response { /// Simulate a hop which does not response to probes. NoResponse, /// Simulate a hop which responds to probes from a single host. SingleHost(SingleHost), } /// A simulated probe response with a single addr and fixed ttl. #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct SingleHost { /// The simulated host responding to the probe. pub addr: IpAddr, /// The simulated round trim time (RTT) in ms. pub rtt_ms: u16, } #[derive(Copy, Clone, Debug, Default, Deserialize)] pub enum PrivilegeMode { #[default] Privileged, Unprivileged, } impl From for trippy_core::PrivilegeMode { fn from(value: PrivilegeMode) -> Self { match value { PrivilegeMode::Privileged => Self::Privileged, PrivilegeMode::Unprivileged => Self::Unprivileged, } } } #[derive(Copy, Clone, Debug, Deserialize)] pub enum Protocol { Icmp, Udp, Tcp, } impl From for trippy_core::Protocol { fn from(value: Protocol) -> Self { match value { Protocol::Icmp => Self::Icmp, Protocol::Udp => Self::Udp, Protocol::Tcp => Self::Tcp, } } } #[derive(Copy, Clone, Debug, Default, Deserialize)] #[serde(tag = "tag", content = "value")] pub enum PortDirection { #[default] None, FixedSrc(u16), FixedDest(u16), FixedBoth(FixedBoth), } #[derive(Copy, Clone, Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] pub struct FixedBoth { pub src: u16, pub dest: u16, } impl From for trippy_core::PortDirection { fn from(value: PortDirection) -> Self { match value { PortDirection::None => Self::None, PortDirection::FixedSrc(src) => Self::FixedSrc(Port(src)), PortDirection::FixedDest(dest) => Self::FixedDest(Port(dest)), PortDirection::FixedBoth(FixedBoth { src, dest }) => { Self::FixedBoth(Port(src), Port(dest)) } } } } #[derive(Copy, Clone, Debug, Default, Deserialize)] pub enum MultipathStrategy { #[default] Classic, Paris, Dublin, } impl From for trippy_core::MultipathStrategy { fn from(value: MultipathStrategy) -> Self { match value { MultipathStrategy::Classic => Self::Classic, MultipathStrategy::Paris => Self::Paris, MultipathStrategy::Dublin => Self::Dublin, } } } ================================================ FILE: crates/trippy-core/tests/sim/tests.rs ================================================ use crate::simulation::Simulation; use crate::tun_device::tun; use crate::{network, tracer}; use std::sync::{Arc, Mutex, OnceLock}; use test_case::test_case; use tokio::runtime::Runtime; use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; use tracing_subscriber::fmt::format::FmtSpan; /// The maximum number of attempts for each test. const MAX_ATTEMPTS: usize = 5; static RUNTIME: OnceLock>> = OnceLock::new(); pub fn runtime() -> &'static Arc> { RUNTIME.get_or_init(|| { tracing_subscriber::fmt() .with_span_events(FmtSpan::NONE) .with_env_filter("trippy=off,sim=debug") .init(); let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(); Arc::new(Mutex::new(runtime)) }) } macro_rules! sim { ($path:expr) => {{ let data = include_str!(concat!("../resources/simulation/", $path)); toml::from_str(data)? }}; } #[test_case(sim!("ipv4_icmp.toml"))] #[test_case(sim!("ipv6_icmp.toml"))] #[test_case(sim!("ipv4_icmp_gaps.toml"))] #[test_case(sim!("ipv4_icmp_ooo.toml"))] #[test_case(sim!("ipv4_icmp_min.toml"))] #[test_case(sim!("ipv6_icmp_min.toml"))] #[test_case(sim!("ipv4_icmp_pattern.toml"))] #[test_case(sim!("ipv6_icmp_pattern.toml"))] #[test_case(sim!("ipv4_icmp_quick.toml"))] #[test_case(sim!("ipv4_icmp_wrap.toml"))] #[test_case(sim!("ipv4_icmp_tos.toml"))] #[test_case(sim!("ipv4_udp_classic_fixed_src.toml"))] #[test_case(sim!("ipv6_udp_classic_fixed_src.toml"))] #[test_case(sim!("ipv4_udp_classic_fixed_dest.toml"))] #[test_case(sim!("ipv6_udp_classic_fixed_dest.toml"))] #[test_case(sim!("ipv4_udp_paris_fixed_both.toml"))] #[test_case(sim!("ipv6_udp_paris_fixed_both.toml"))] #[test_case(sim!("ipv4_udp_dublin_fixed_both.toml"))] #[test_case(sim!("ipv6_udp_dublin_fixed_both.toml"))] #[test_case(sim!("ipv4_udp_classic_privileged_tos.toml"))] #[test_case(sim!("ipv4_tcp_fixed_dest.toml"))] #[test_case(sim!("ipv6_tcp_fixed_dest.toml"))] fn test_simulation(simulation: Simulation) -> anyhow::Result<()> { run_simulation_with_retry(simulation) } // unprivileged mode is only supported on macOS #[cfg(target_os = "macos")] #[test_case(sim!("ipv4_udp_classic_unprivileged.toml"))] #[test_case(sim!("ipv4_udp_classic_unprivileged_tos.toml"))] #[test_case(sim!("ipv6_udp_classic_unprivileged.toml"))] #[test_case(sim!("ipv6_udp_classic_unprivileged_tos.toml"))] fn test_simulation_macos(simulation: Simulation) -> anyhow::Result<()> { run_simulation_with_retry(simulation) } fn run_simulation_with_retry(simulation: Simulation) -> anyhow::Result<()> { let runtime = runtime().lock().unwrap(); let simulation = Arc::new(simulation); let name = simulation.name.clone(); if !trippy_privilege::Privilege::discover()?.has_privileges() { // Skip if the current test as the user cannot create a tun device. warn!("skipping test {}: insufficient privileges", name); return Ok(()); } for attempt in 1..=MAX_ATTEMPTS { info!("start simulating {} [attempt #{}]", name, attempt); if let Err(err) = runtime.block_on(run_simulation(simulation.clone())) { error!("failed simulating {} {} [attempt #{}]", name, err, attempt); } else { info!("end simulating {} [attempt #{}]", name, attempt); return Ok(()); } } anyhow::bail!("failed simulating {name} after {MAX_ATTEMPTS} attempts") } async fn run_simulation(sim: Arc) -> anyhow::Result<()> { let tun = tun(); let token = CancellationToken::new(); let handle = tokio::spawn(network::run(tun.clone(), sim.clone(), token.clone())); tokio::task::spawn_blocking(move || tracer::Tracer::new(sim, token).trace()).await??; handle.await? } ================================================ FILE: crates/trippy-core/tests/sim/tracer.rs ================================================ use crate::simulation::{Response, Simulation, SingleHost}; use crate::tun_device::{TUN_NETWORK_ADDR_V4, TUN_NETWORK_ADDR_V6}; use std::cell::RefCell; use std::net::IpAddr; use std::sync::Arc; use std::thread; use std::time::Duration; use tokio_util::sync::CancellationToken; use tracing::info; use trippy_core::{ Builder, CompletionReason, MultipathStrategy, PortDirection, PrivilegeMode, ProbeStatus, Protocol, Round, TimeToLive, defaults, }; // The length of time to wait after the completion of the tracing before // cancelling the network simulator. This is needed to ensure that all // in-flight packets for the current test are sent or received prior to // ending the round so that they are not incorrectly used in a subsequent // test. const CLEANUP_DELAY: Duration = Duration::from_millis(1000); macro_rules! assert_eq_result { ($res:ident, $exp1:expr, $exp2:expr) => {{ fn ensure_match(fst: T, snd: T) -> anyhow::Result<()> { anyhow::ensure!(fst == snd); Ok(()) } if let err @ Err(_) = ensure_match($exp1, $exp2) { *$res.borrow_mut() = err; return; } }}; } macro_rules! error_result { ($res:ident, $err:expr) => {{ *$res.borrow_mut() = Err($err); return; }}; } pub struct Tracer { sim: Arc, token: CancellationToken, } impl Tracer { pub const fn new(sim: Arc, token: CancellationToken) -> Self { Self { sim, token } } pub fn trace(&self) -> anyhow::Result<()> { let result = RefCell::new(Ok(())); let source_addr = Some(match self.sim.target { IpAddr::V4(_) => IpAddr::V4(TUN_NETWORK_ADDR_V4), IpAddr::V6(_) => IpAddr::V6(TUN_NETWORK_ADDR_V6), }); let tracer = Builder::new(self.sim.target) .source_addr(source_addr) .privilege_mode(PrivilegeMode::from(self.sim.privilege_mode)) .trace_identifier(self.sim.icmp_identifier) .initial_sequence( self.sim .initial_sequence .unwrap_or(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE), ) .protocol(Protocol::from(self.sim.protocol)) .port_direction(PortDirection::from(self.sim.port_direction)) .multipath_strategy(MultipathStrategy::from(self.sim.multipath_strategy)) .packet_size( self.sim .packet_size .unwrap_or(defaults::DEFAULT_STRATEGY_PACKET_SIZE), ) .payload_pattern( self.sim .payload_pattern .unwrap_or(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN), ) .tos(self.sim.tos.unwrap_or(defaults::DEFAULT_STRATEGY_TOS)) .min_round_duration(self.sim.min_round_duration.map_or( defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION, Duration::from_millis, )) .max_round_duration(self.sim.max_round_duration.map_or( defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION, Duration::from_millis, )) .grace_duration(self.sim.grace_duration.map_or( defaults::DEFAULT_STRATEGY_GRACE_DURATION, Duration::from_millis, )) .max_rounds(self.sim.rounds.or(Some(1))) .build()?; let tracer_res = tracer .run_with(|round| self.validate_round(round, &result)) .map_err(anyhow::Error::from); thread::sleep(CLEANUP_DELAY); self.token.cancel(); // ensure both the tracer and the validator were successful. tracer_res.and(result.replace(Ok(()))) } fn validate_round(&self, round: &Round<'_>, result: &RefCell>) { assert_eq_result!(result, round.reason, CompletionReason::TargetFound); assert_eq_result!(result, TimeToLive(self.sim.latest_ttl()), round.largest_ttl); for hop in round .probes .iter() .filter(|p| matches!(p, ProbeStatus::Awaited(_) | ProbeStatus::Complete(_))) .take(round.largest_ttl.0 as usize) { match hop { ProbeStatus::Complete(complete) => { info!( "{} {} {}", complete.round.0, complete.ttl.0, complete.host.to_string(), ); let hop_index = usize::from(complete.ttl.0 - 1); let sim_hop = &self.sim.hops[hop_index]; if matches!(sim_hop.resp, Response::NoResponse) { error_result!(result, anyhow::anyhow!("expected Response::SingleHost")); } let expected_host = match sim_hop.resp { Response::NoResponse => None, Response::SingleHost(SingleHost { addr, .. }) => Some(addr), }; assert_eq_result!(result, expected_host, Some(complete.host)); let expected_ttl = TimeToLive(self.sim.hops[hop_index].ttl); assert_eq_result!(result, expected_ttl, complete.ttl); } ProbeStatus::Awaited(awaited) => { info!("{} {} * * *", awaited.round.0, awaited.ttl.0); let hop_index = usize::from(awaited.ttl.0 - 1); let sim_hop = &self.sim.hops[hop_index]; if let Response::SingleHost(_) = sim_hop.resp { error_result!(result, anyhow::anyhow!("expected Response::NoResponse")); } let expected_host = match sim_hop.resp { Response::NoResponse => None, Response::SingleHost(SingleHost { addr, .. }) => Some(addr), }; assert_eq_result!(result, expected_host, None); let expected_ttl = TimeToLive(self.sim.hops[hop_index].ttl); assert_eq_result!(result, expected_ttl, awaited.ttl); } _ => {} } } } } ================================================ FILE: crates/trippy-core/tests/sim/tun_device.rs ================================================ use std::net::{Ipv4Addr, Ipv6Addr}; use std::sync::{Arc, OnceLock}; use tokio::sync::Mutex; static TUN: OnceLock>> = OnceLock::new(); /// Get a reference to the singleton `tun` device, initializing as necessary. pub fn tun() -> &'static Arc> { TUN.get_or_init(|| { let tun = TunDevice::start().expect("tun"); Arc::new(Mutex::new(tun)) }) } /// IPv4 address and CIDR prefix configured on the `tun` device. /// /// For example, if this is set to `10.0.0.1` with a prefix length of 24 then /// the `tun` device will be assigned the IP `10.0.0.1` and packets sent to /// the network range `10.0.0.0/24` will typically be routed via the `tun` /// device and therefore have the source address `10.0.0.1`. pub const TUN_NETWORK_ADDR_V4: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1); const TUN_NETWORK_PREFIX_V4: u8 = 24; /// IPv6 address and CIDR prefix configured on the `tun` device. pub const TUN_NETWORK_ADDR_V6: Ipv6Addr = Ipv6Addr::new(0xfd00, 0x0010, 0, 0, 0, 0, 0, 1); const TUN_NETWORK_PREFIX_V6: u8 = 64; /// A `tun` device. pub struct TunDevice { dev: tun_rs::AsyncDevice, } impl TunDevice { pub fn start() -> anyhow::Result { let dev = tun_rs::DeviceBuilder::new() .ipv4(TUN_NETWORK_ADDR_V4, TUN_NETWORK_PREFIX_V4, None) .ipv6(TUN_NETWORK_ADDR_V6, TUN_NETWORK_PREFIX_V6) .build_async()?; #[cfg(target_os = "windows")] std::thread::sleep(std::time::Duration::from_millis(10000)); Ok(Self { dev }) } pub async fn read(&self, buf: &mut [u8]) -> std::io::Result { let bytes_read = self.dev.recv(buf).await?; Ok(bytes_read) } pub async fn write(&self, buf: &[u8]) -> std::io::Result { self.dev.send(buf).await } } ================================================ FILE: crates/trippy-dns/Cargo.toml ================================================ [package] name = "trippy-dns" description = "A lazy DNS resolver for Trippy" version.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true readme.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true keywords.workspace = true categories.workspace = true [dependencies] crossbeam.workspace = true dns-lookup.workspace = true hickory-resolver.workspace = true itertools.workspace = true parking_lot.workspace = true thiserror.workspace = true [dev-dependencies] anyhow.workspace = true [lints] workspace = true ================================================ FILE: crates/trippy-dns/src/config.rs ================================================ use crate::{IpAddrFamily, ResolveMethod}; use std::time::Duration; /// A builder for DNS `Config`. /// /// # Example /// /// Build a DNS `Config` for the `Ipv4Only` address family. /// /// ```no_run /// use trippy_dns::{Builder, IpAddrFamily}; /// /// let config = Builder::new().addr_family(IpAddrFamily::Ipv4Only).build(); pub struct Builder { resolve_method: ResolveMethod, addr_family: IpAddrFamily, timeout: Duration, ttl: Duration, } impl Builder { /// Create a new `Builder`. #[must_use] pub fn new() -> Self { Self { resolve_method: Config::default().resolve_method, addr_family: Config::default().addr_family, timeout: Config::default().timeout, ttl: Config::default().ttl, } } /// Set the method to use for DNS resolution. #[must_use] pub const fn resolve_method(self, resolve_method: ResolveMethod) -> Self { Self { resolve_method, ..self } } /// Set the address family. #[must_use] pub const fn addr_family(self, addr_family: IpAddrFamily) -> Self { Self { addr_family, ..self } } /// Set the timeout for DNS resolution. #[must_use] pub const fn timeout(self, timeout: Duration) -> Self { Self { timeout, ..self } } /// Set the time-to-live (TTL) for DNS cache entries. #[must_use] pub const fn ttl(self, ttl: Duration) -> Self { Self { ttl, ..self } } /// Build the DNS `Config`. #[must_use] pub const fn build(self) -> Config { Config { resolve_method: self.resolve_method, addr_family: self.addr_family, timeout: self.timeout, ttl: self.ttl, } } } impl Default for Builder { fn default() -> Self { Self::new() } } /// Configuration for the `DnsResolver`. #[derive(Debug, Copy, Clone)] pub struct Config { /// The method to use for DNS resolution. pub resolve_method: ResolveMethod, /// The IP address resolution family. pub addr_family: IpAddrFamily, /// The timeout for DNS resolution. pub timeout: Duration, /// The time-to-live (TTL) for DNS cache entries. pub ttl: Duration, } impl Config { /// Create a `Config`. #[must_use] pub const fn new( resolve_method: ResolveMethod, addr_family: IpAddrFamily, timeout: Duration, ttl: Duration, ) -> Self { Self { resolve_method, addr_family, timeout, ttl, } } } impl Default for Config { fn default() -> Self { Self { resolve_method: ResolveMethod::System, addr_family: IpAddrFamily::Ipv4thenIpv6, timeout: Duration::from_millis(5000), ttl: Duration::from_secs(300), } } } ================================================ FILE: crates/trippy-dns/src/lazy_resolver.rs ================================================ use crate::config::Config; use crate::resolver::{DnsEntry, ResolvedIpAddrs, Resolver, Result}; use std::fmt::{Display, Formatter}; use std::net::IpAddr; use std::rc::Rc; /// How DNS queries will be resolved. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ResolveMethod { /// Resolve using the OS resolver. System, /// Resolve using the `/etc/resolv.conf` DNS configuration. Resolv, /// Resolve using the Google `8.8.8.8` DNS service. Google, /// Resolve using the Cloudflare `1.1.1.1` DNS service. Cloudflare, } /// How to resolve IP addresses. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum IpAddrFamily { /// Lookup IPv4 only. Ipv4Only, /// Lookup IPv6 only. Ipv6Only, /// Lookup IPv6 with a fallback to IPv4. Ipv6thenIpv4, /// Lookup IPv4 with a fallback to IPv6. Ipv4thenIpv6, /// Use the first IP address returned by the OS resolver when using `ResolveMethod::System`, /// otherwise lookup IPv4 with a fallback to IPv6. System, } impl Display for IpAddrFamily { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Ipv4Only => write!(f, "Ipv4Only"), Self::Ipv6Only => write!(f, "Ipv6Only"), Self::Ipv6thenIpv4 => write!(f, "Ipv6thenIpv4"), Self::Ipv4thenIpv6 => write!(f, "Ipv4thenIpv6"), Self::System => write!(f, "System"), } } } /// A cheaply cloneable, non-blocking, caching, forward and reverse DNS resolver. #[derive(Clone)] pub struct DnsResolver { inner: Rc, } impl DnsResolver { /// Create and start a new `DnsResolver`. pub fn start(config: Config) -> std::io::Result { Ok(Self { inner: Rc::new(inner::DnsResolver::start(config)?), }) } /// Get the `Config`. #[must_use] pub fn config(&self) -> &Config { self.inner.config() } /// Flush the cache of responses. pub fn flush(&self) { self.inner.flush(); } } impl Resolver for DnsResolver { fn lookup(&self, hostname: impl AsRef) -> Result { self.inner.lookup(hostname.as_ref()) } fn reverse_lookup(&self, addr: impl Into) -> DnsEntry { self.inner.reverse_lookup(addr.into(), false, false) } fn reverse_lookup_with_asinfo(&self, addr: impl Into) -> DnsEntry { self.inner.reverse_lookup(addr.into(), true, false) } fn lazy_reverse_lookup(&self, addr: impl Into) -> DnsEntry { self.inner.reverse_lookup(addr.into(), false, true) } fn lazy_reverse_lookup_with_asinfo(&self, addr: impl Into) -> DnsEntry { self.inner.reverse_lookup(addr.into(), true, true) } } /// Private impl of resolver. mod inner { use super::{Config, IpAddrFamily, ResolveMethod}; use crate::resolver::{AsInfo, DnsEntry, Error, Resolved, ResolvedIpAddrs, Result, Unresolved}; use crossbeam::channel::{Receiver, Sender, bounded}; use hickory_resolver::config::{LookupIpStrategy, ResolverConfig, ResolverOpts}; use hickory_resolver::error::{ResolveError, ResolveErrorKind}; use hickory_resolver::proto::error::ProtoError; use hickory_resolver::proto::rr::RecordType; use hickory_resolver::system_conf::read_system_conf; use hickory_resolver::{Name, Resolver}; use itertools::{Either, Itertools}; use parking_lot::RwLock; use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::str::FromStr; use std::sync::Arc; use std::thread; use std::time::{Duration, SystemTime}; /// The maximum number of in-flight reverse DNS resolutions that may be const RESOLVER_MAX_QUEUE_SIZE: usize = 100; /// The duration wait to enqueue a `DnsEntry::Pending` to the resolver before returning /// `DnsEntry::Timeout`. const RESOLVER_QUEUE_TIMEOUT: Duration = Duration::from_millis(10); /// Alias for a cache of reverse DNS lookup entries. type Cache = Arc>>; /// A cache entry for a reverse DNS lookup. #[derive(Debug, Clone)] struct CacheEntry { /// The DNS entry to cache. entry: DnsEntry, /// The timestamp of the entry. timestamp: SystemTime, } impl CacheEntry { const fn new(entry: DnsEntry, timestamp: SystemTime) -> Self { Self { entry, timestamp } } fn set_timestamp(&mut self, timestamp: SystemTime) { self.timestamp = timestamp; } } #[derive(Clone)] enum DnsProvider { TrustDns(Arc), DnsLookup, } #[derive(Debug, Clone)] struct DnsResolveRequest { addr: IpAddr, with_asinfo: bool, } /// Resolver implementation. pub(super) struct DnsResolver { config: Config, provider: DnsProvider, tx: Sender, addr_cache: Cache, } impl DnsResolver { pub(super) fn start(config: Config) -> std::io::Result { let (tx, rx) = bounded(RESOLVER_MAX_QUEUE_SIZE); let addr_cache = Arc::new(RwLock::new(HashMap::new())); let provider = if matches!(config.resolve_method, ResolveMethod::System) { DnsProvider::DnsLookup } else { let mut options = ResolverOpts::default(); #[expect(clippy::match_same_arms)] let ip_strategy = match config.addr_family { IpAddrFamily::Ipv4Only => LookupIpStrategy::Ipv4Only, IpAddrFamily::Ipv6Only => LookupIpStrategy::Ipv6Only, IpAddrFamily::Ipv6thenIpv4 => LookupIpStrategy::Ipv6thenIpv4, IpAddrFamily::Ipv4thenIpv6 => LookupIpStrategy::Ipv4thenIpv6, // see issue #1469 IpAddrFamily::System => LookupIpStrategy::Ipv4thenIpv6, }; options.timeout = config.timeout; options.ip_strategy = ip_strategy; let res = match config.resolve_method { ResolveMethod::Resolv => { let (resolver_cfg, mut options) = read_system_conf()?; options.timeout = config.timeout; options.ip_strategy = ip_strategy; Resolver::new(resolver_cfg, options) } ResolveMethod::Google => Resolver::new(ResolverConfig::google(), options), ResolveMethod::Cloudflare => { Resolver::new(ResolverConfig::cloudflare(), options) } ResolveMethod::System => unreachable!(), }?; let resolver = Arc::new(res); DnsProvider::TrustDns(resolver) }; // spawn a thread to process the resolve queue { let cache = addr_cache.clone(); let provider = provider.clone(); thread::spawn(move || resolver_queue_processor(rx, &provider, &cache)); } Ok(Self { config, provider, tx, addr_cache, }) } pub(super) const fn config(&self) -> &Config { &self.config } pub(super) fn lookup(&self, hostname: &str) -> Result { fn partition>(all: I) -> (Vec, Vec) { all.partition_map(|ip| match ip { IpAddr::V4(_) => Either::Left(ip), IpAddr::V6(_) => Either::Right(ip), }) } match &self.provider { DnsProvider::TrustDns(resolver) => Ok(resolver .lookup_ip(hostname) .map_err(|err| Error::LookupFailed(Box::new(err)))? .iter() .collect::>()), DnsProvider::DnsLookup => { let all = dns_lookup::lookup_host(hostname) .map_err(|err| Error::LookupFailed(Box::new(err)))?; Ok(match self.config.addr_family { IpAddrFamily::Ipv4Only => { let (ipv4, _): (Vec<_>, Vec<_>) = partition(all); if ipv4.is_empty() { vec![] } else { ipv4 } } IpAddrFamily::Ipv6Only => { let (_, ipv6): (Vec<_>, Vec<_>) = partition(all); if ipv6.is_empty() { vec![] } else { ipv6 } } IpAddrFamily::Ipv6thenIpv4 => { let (ipv4, ipv6): (Vec<_>, Vec<_>) = partition(all); if ipv6.is_empty() { ipv4 } else { ipv6 } } IpAddrFamily::Ipv4thenIpv6 => { let (ipv4, ipv6): (Vec<_>, Vec<_>) = partition(all); if ipv4.is_empty() { ipv6 } else { ipv4 } } IpAddrFamily::System => all.collect(), }) } } .map(ResolvedIpAddrs) } pub(super) fn reverse_lookup( &self, addr: IpAddr, with_asinfo: bool, lazy: bool, ) -> DnsEntry { if lazy { self.lazy_reverse_lookup(addr, with_asinfo).entry } else { reverse_lookup(&self.provider, addr, with_asinfo).entry } } fn lazy_reverse_lookup(&self, addr: IpAddr, with_asinfo: bool) -> CacheEntry { let mut enqueue = false; let now = SystemTime::now(); // Check if we have already attempted to resolve this `IpAddr` and return the current // `DnsEntry` if so, otherwise add it in a state of `DnsEntry::Pending`. let mut dns_entry = self .addr_cache .write() .entry(addr) .or_insert_with(|| { enqueue = true; CacheEntry::new(DnsEntry::Pending(addr), now) }) .clone(); // If the entry exists but is stale then enqueue it again. The existing entry will // be returned until it is refreshed but with an updated timestamp to prevent it from // being enqueued multiple times. match &dns_entry.entry { DnsEntry::Resolved(_) | DnsEntry::NotFound(_) | DnsEntry::Failed(_) => { if now.duration_since(dns_entry.timestamp).unwrap_or_default() > self.config.ttl { self.addr_cache .write() .get_mut(&addr) .expect("addr must be in cache") .set_timestamp(now); enqueue = true; } } _ => {} } // If the entry exists but has timed out, then set it as `DnsEntry::Pending` and enqueue // it again. if let DnsEntry::Timeout(addr) = dns_entry.entry { *self .addr_cache .write() .get_mut(&addr) .expect("addr must be in cache") = CacheEntry::new(DnsEntry::Pending(addr), now); dns_entry = CacheEntry::new(DnsEntry::Pending(addr), now); enqueue = true; } // If this is a newly added `DnsEntry` then send it to the channel to be resolved in the // background. We do this after the above to ensure we aren't holding the // lock on the cache, which is used by the resolver and so would deadlock. if enqueue { if self .tx .send_timeout( DnsResolveRequest { addr, with_asinfo }, RESOLVER_QUEUE_TIMEOUT, ) .is_ok() { dns_entry } else { *self .addr_cache .write() .get_mut(&addr) .expect("addr must be in cache") = CacheEntry::new(DnsEntry::Timeout(addr), now); CacheEntry::new(DnsEntry::Timeout(addr), now) } } else { dns_entry } } pub fn flush(&self) { self.addr_cache.write().clear(); } } /// Process each `IpAddr` from the resolver queue and perform the reverse DNS lookup. /// /// For each `IpAddr`, perform the reverse DNS lookup and update the cache with the result /// (`Resolved`, `NotFound`, `Timeout` or `Failed`) for that addr. fn resolver_queue_processor( rx: Receiver, provider: &DnsProvider, cache: &Cache, ) { for DnsResolveRequest { addr, with_asinfo } in rx { let dns_entry = reverse_lookup(provider, addr, with_asinfo); cache.write().insert(addr, dns_entry); } } fn reverse_lookup(provider: &DnsProvider, addr: IpAddr, with_asinfo: bool) -> CacheEntry { let now = SystemTime::now(); match &provider { DnsProvider::DnsLookup => { // we can't distinguish between a failed lookup or a genuine error, and so we just // assume all failures are `DnsEntry::NotFound`. match dns_lookup::lookup_addr(&addr) { Ok(dns) => { CacheEntry::new(DnsEntry::Resolved(Resolved::Normal(addr, vec![dns])), now) } Err(_) => CacheEntry::new(DnsEntry::NotFound(Unresolved::Normal(addr)), now), } } DnsProvider::TrustDns(resolver) => match resolver.reverse_lookup(addr) { Ok(name) => { let hostnames = name .into_iter() .map(|mut s| { s.0.set_fqdn(false); s }) .map(|s| s.to_string()) .collect(); if with_asinfo { let as_info = lookup_asinfo(resolver, addr).unwrap_or_default(); CacheEntry::new( DnsEntry::Resolved(Resolved::WithAsInfo(addr, hostnames, as_info)), now, ) } else { CacheEntry::new(DnsEntry::Resolved(Resolved::Normal(addr, hostnames)), now) } } Err(err) => match err.kind() { ResolveErrorKind::NoRecordsFound { .. } => { if with_asinfo { let as_info = lookup_asinfo(resolver, addr).unwrap_or_default(); CacheEntry::new( DnsEntry::NotFound(Unresolved::WithAsInfo(addr, as_info)), now, ) } else { CacheEntry::new(DnsEntry::NotFound(Unresolved::Normal(addr)), now) } } ResolveErrorKind::Timeout => CacheEntry::new(DnsEntry::Timeout(addr), now), _ => CacheEntry::new(DnsEntry::Failed(addr), now), }, }, } } /// Lookup up `AsInfo` for an `IpAddr` address. fn lookup_asinfo(resolver: &Arc, addr: IpAddr) -> Result { let origin_query_txt = match addr { IpAddr::V4(addr) => query_asn_ipv4(resolver, addr)?, IpAddr::V6(addr) => query_asn_ipv6(resolver, addr)?, }; let asinfo = parse_origin_query_txt(&origin_query_txt)?; let asn_query_txt = query_asn_name(resolver, &asinfo.asn)?; let as_name = parse_asn_query_txt(&asn_query_txt)?; Ok(AsInfo { asn: asinfo.asn, prefix: asinfo.prefix, cc: asinfo.cc, registry: asinfo.registry, allocated: asinfo.allocated, name: as_name, }) } /// Perform the `origin` query. fn query_asn_ipv4(resolver: &Arc, addr: Ipv4Addr) -> Result { let query = format!( "{}.origin.asn.cymru.com.", addr.octets().iter().rev().join(".") ); let name = Name::from_str(query.as_str()).map_err(proto_error)?; let response = resolver .lookup(name, RecordType::TXT) .map_err(resolve_error)?; let data = response .iter() .next() .ok_or_else(|| Error::QueryAsnOriginFailed)?; let bytes = data.as_txt().ok_or_else(|| Error::QueryAsnOriginFailed)?; Ok(bytes.to_string()) } /// Perform the `origin` query. fn query_asn_ipv6(resolver: &Arc, addr: Ipv6Addr) -> Result { let query = format!( "{:x}.origin6.asn.cymru.com.", addr.octets() .iter() .rev() .flat_map(|o| [o & 0x0F, (o & 0xF0) >> 4]) .format(".") ); let name = Name::from_str(query.as_str()).map_err(proto_error)?; let response = resolver .lookup(name, RecordType::TXT) .map_err(resolve_error)?; let data = response .iter() .next() .ok_or_else(|| Error::QueryAsnOriginFailed)?; let bytes = data.as_txt().ok_or_else(|| Error::QueryAsnOriginFailed)?; Ok(bytes.to_string()) } /// Perform the `asn` query. fn query_asn_name(resolver: &Arc, asn: &str) -> Result { let query = format!("AS{asn}.asn.cymru.com."); let name = Name::from_str(query.as_str()).map_err(proto_error)?; let response = resolver .lookup(name, RecordType::TXT) .map_err(resolve_error)?; let data = response .iter() .next() .ok_or_else(|| Error::QueryAsnFailed)?; let bytes = data.as_txt().ok_or_else(|| Error::QueryAsnFailed)?; Ok(bytes.to_string()) } /// The `origin` DNS query returns a TXT record in the formal: /// `asn | prefix | cc | registry | allocated` /// /// For example: /// `12301 | 81.0.100.0/22 | HU | ripencc | 2001-12-06` /// /// From this we extract all fields. fn parse_origin_query_txt(origin_query_txt: &str) -> Result { if origin_query_txt.chars().filter(|c| *c == '|').count() != 4 { return Err(Error::ParseOriginQueryFailed(String::from( origin_query_txt, ))); } let mut split = origin_query_txt.split('|'); let asn = split.next().unwrap_or_default().trim().to_string(); let prefix = split.next().unwrap_or_default().trim().to_string(); let cc = split.next().unwrap_or_default().trim().to_string(); let registry = split.next().unwrap_or_default().trim().to_string(); let allocated = split.next().unwrap_or_default().trim().to_string(); Ok(AsInfo { asn, prefix, cc, registry, allocated, name: String::default(), }) } /// The `asn` DNS query returns a TXT record in the formal: /// `asn | cc | registry | allocated | name` /// /// For example: /// `12301 | HU | ripencc | 1999-02-25 | INVITECH, HU` /// /// From this we extract the 4th field (name, `INVITECH, HU` in this example) fn parse_asn_query_txt(asn_query_txt: &str) -> Result { if asn_query_txt.chars().filter(|c| *c == '|').count() != 4 { return Err(Error::ParseAsnQueryFailed(String::from(asn_query_txt))); } let mut split = asn_query_txt.split('|'); Ok(split.nth(4).unwrap_or_default().trim().to_string()) } /// Convert a `ResolveError` to an `Error::LookupFailed`. fn resolve_error(err: ResolveError) -> Error { Error::LookupFailed(Box::new(err)) } /// Convert a `ProtoError` to an `Error::LookupFailed`. fn proto_error(err: ProtoError) -> Error { Error::LookupFailed(Box::new(err)) } } ================================================ FILE: crates/trippy-dns/src/lib.rs ================================================ //! This crate provides a cheaply cloneable, non-blocking, caching, forward //! and reverse DNS resolver which support the ability to lookup Autonomous //! System (AS) information. //! //! Only a single reverse DNS lookup is performed (lazily) regardless of how //! often the lookup is performed unless: //! - the previous lookup failed with `DnsEntry::Timeout(_)` //! - the previous lookup is older than the configured time-to-live (TTL) //! //! # Example //! //! The following example perform a reverse DNS lookup and loop until it is //! resolved or fails. The lookup uses the Cloudflare 1.1.1.1 public DNS //! service. //! //! ```no_run //! # fn main() -> anyhow::Result<()> { //! # use std::net::IpAddr; //! # use std::str::FromStr; //! # use std::thread::sleep; //! # use std::time::Duration; //! use trippy_dns::{ //! Config, DnsEntry, DnsResolver, IpAddrFamily, ResolveMethod, Resolved, Resolver, Unresolved, //! }; //! //! let config = Config::new( //! ResolveMethod::Cloudflare, //! IpAddrFamily::Ipv4Only, //! Duration::from_secs(5), //! Duration::from_secs(300), //! ); //! let resolver = DnsResolver::start(config)?; //! let addr = IpAddr::from_str("1.1.1.1")?; //! loop { //! let entry = resolver.lazy_reverse_lookup_with_asinfo(addr); //! match entry { //! DnsEntry::Pending(ip) => { //! println!("lookup of {ip} is pending, sleeping for 1 sec"); //! sleep(Duration::from_secs(1)); //! } //! DnsEntry::Resolved(Resolved::Normal(ip, addrs)) => { //! println!("lookup of {ip} resolved to {addrs:?}"); //! return Ok(()); //! } //! DnsEntry::Resolved(Resolved::WithAsInfo(ip, addrs, as_info)) => { //! println!("lookup of {ip} resolved to {addrs:?} with AS information {as_info:?}"); //! return Ok(()); //! } //! DnsEntry::NotFound(Unresolved::Normal(ip)) => { //! println!("lookup of {ip} did not match any records"); //! return Ok(()); //! } //! DnsEntry::NotFound(Unresolved::WithAsInfo(ip, as_info)) => { //! println!( //! "lookup of {ip} did not match any records with AS information {as_info:?}" //! ); //! return Ok(()); //! } //! DnsEntry::Timeout(ip) => { //! println!("lookup of {ip} timed out"); //! return Ok(()); //! } //! DnsEntry::Failed(ip) => { //! println!("lookup of {ip} failed"); //! return Ok(()); //! } //! } //! } //! # Ok(()) //! # } //! ``` #![forbid(unsafe_code)] mod config; mod lazy_resolver; mod resolver; pub use config::{Builder, Config}; pub use lazy_resolver::{DnsResolver, IpAddrFamily, ResolveMethod}; pub use resolver::{AsInfo, DnsEntry, Error, Resolved, Resolver, Result, Unresolved}; ================================================ FILE: crates/trippy-dns/src/resolver.rs ================================================ use std::fmt::{Display, Formatter}; use std::net::IpAddr; use thiserror::Error; /// A DNS resolver. pub trait Resolver { /// Perform a blocking DNS hostname lookup and return the resolved IPv4 or IPv6 addresses. fn lookup(&self, hostname: impl AsRef) -> Result; /// Perform a blocking reverse DNS lookup of `IpAddr` and return a `DnsEntry`. /// /// As this method is blocking it will never return a `DnsEntry::Pending`. #[must_use] fn reverse_lookup(&self, addr: impl Into) -> DnsEntry; /// Perform a blocking reverse DNS lookup of `IpAddr` and return a `DnsEntry` with `AS` /// information. /// /// See [`Resolver::reverse_lookup`] #[must_use] fn reverse_lookup_with_asinfo(&self, addr: impl Into) -> DnsEntry; /// Perform a lazy reverse DNS lookup of `IpAddr` and return a `DnsEntry`. /// /// If the `IpAddr` has already been resolved then `DnsEntry::Resolved` is returned immediately. /// /// Otherwise, the `IpAddr` is enqueued to be resolved in the background and a /// `DnsEntry::Pending` is returned. /// /// If the entry exists but is `DnsEntry::Timeout` then it is changed to be `DnsEntry::Pending` /// and enqueued. /// /// If enqueuing times out then the entry is changed to be `DnsEntry::Timeout` and returned. #[must_use] fn lazy_reverse_lookup(&self, addr: impl Into) -> DnsEntry; /// Perform a lazy reverse DNS lookup of `IpAddr` and return a `DnsEntry` with `AS` information. /// /// See [`Resolver::lazy_reverse_lookup`] #[must_use] fn lazy_reverse_lookup_with_asinfo(&self, addr: impl Into) -> DnsEntry; } /// A DNS resolver error result. pub type Result = std::result::Result; /// A DNS resolver error. #[derive(Error, Debug)] pub enum Error { #[error("DNS lookup failed")] LookupFailed(Box), #[error("ASN origin query failed")] QueryAsnOriginFailed, #[error("ASN query failed")] QueryAsnFailed, #[error("origin query txt parse failed: {0}")] ParseOriginQueryFailed(String), #[error("asn query txt parse failed: {0}")] ParseAsnQueryFailed(String), } /// The output of a successful DNS lookup. #[derive(Debug, Clone)] pub struct ResolvedIpAddrs(pub(super) Vec); impl ResolvedIpAddrs { pub fn iter(&self) -> impl Iterator { self.0.iter() } } impl IntoIterator for ResolvedIpAddrs { type Item = IpAddr; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } /// The state of reverse DNS resolution. #[derive(Debug, Clone)] pub enum DnsEntry { /// The reverse DNS resolution of `IpAddr` is pending. Pending(IpAddr), /// The reverse DNS resolution of `IpAddr` has resolved. Resolved(Resolved), /// The `IpAddr` could not be resolved. NotFound(Unresolved), /// The reverse DNS resolution of `IpAddr` failed. Failed(IpAddr), /// The reverse DNS resolution of `IpAddr` timed out. Timeout(IpAddr), } /// The resolved hostnames of a `DnsEntry`. #[derive(Debug, Clone)] pub struct ResolvedHostnames<'a>(pub(super) std::slice::Iter<'a, String>); impl<'a> Iterator for ResolvedHostnames<'a> { type Item = &'a str; fn next(&mut self) -> Option { self.0.next().map(String::as_str) } } impl DnsEntry { /// The resolved hostnames. #[must_use] pub fn hostnames(&self) -> ResolvedHostnames<'_> { match self { Self::Resolved(Resolved::WithAsInfo(_, hosts, _) | Resolved::Normal(_, hosts)) => { ResolvedHostnames(hosts.iter()) } Self::Pending(_) | Self::Timeout(_) | Self::NotFound(_) | Self::Failed(_) => { ResolvedHostnames([].iter()) } } } } /// Information about a resolved `IpAddr`. #[derive(Debug, Clone)] pub enum Resolved { /// Resolved without `AsInfo`. Normal(IpAddr, Vec), /// Resolved with `AsInfo`. WithAsInfo(IpAddr, Vec, AsInfo), } /// Information about an unresolved `IpAddr`. #[derive(Debug, Clone)] pub enum Unresolved { /// Unresolved without `AsInfo`. Normal(IpAddr), /// Unresolved with `AsInfo`. WithAsInfo(IpAddr, AsInfo), } /// Information about an autonomous System (AS). #[derive(Debug, Clone, Default)] pub struct AsInfo { /// The autonomous system Number. /// /// This is returned without the AS prefix i.e. `12301`. pub asn: String, /// The AS prefix. /// /// Given in CIDR notation i.e. `81.0.100.0/22`. pub prefix: String, /// The country code. /// /// Given as a ISO format i.e. `HU`. pub cc: String, /// AS registry name. /// /// Given as a string i.e. `ripencc`. pub registry: String, /// Allocation date. /// /// Given as an ISO date i.e. `1999-02-25`. pub allocated: String, /// The autonomous system (AS) Name. /// /// Given as a string i.e. `INVITECH, HU`. pub name: String, } impl Display for DnsEntry { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { #[expect(clippy::match_same_arms)] match self { Self::Resolved(Resolved::Normal(_, hosts)) => write!(f, "{}", hosts.join(" ")), Self::Resolved(Resolved::WithAsInfo(_, hosts, asinfo)) => { write!(f, "AS{} {}", asinfo.asn, hosts.join(" ")) } Self::Pending(ip) => write!(f, "{ip}"), Self::Timeout(ip) => write!(f, "Timeout: {ip}"), Self::NotFound(Unresolved::Normal(ip)) => write!(f, "{ip}"), Self::NotFound(Unresolved::WithAsInfo(ip, asinfo)) => { write!(f, "AS{} {}", asinfo.asn, ip) } Self::Failed(ip) => write!(f, "Failed: {ip}"), } } } #[cfg(test)] mod tests { use super::*; use std::net::IpAddr; use std::str::FromStr; #[test] fn test_iterator_returns_each_hostname_once() { let entry = DnsEntry::Resolved(Resolved::Normal( IpAddr::from_str("1.1.1.1").unwrap(), vec!["one".to_string(), "two".to_string(), "three".to_string()], )); let mut iter = entry.hostnames(); assert_eq!(iter.next(), Some("one")); assert_eq!(iter.next(), Some("two")); assert_eq!(iter.next(), Some("three")); assert_eq!(iter.next(), None); } } ================================================ FILE: crates/trippy-packet/Cargo.toml ================================================ [package] name = "trippy-packet" description = "Network packets for Trippy" version.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true readme.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true keywords.workspace = true categories.workspace = true [dependencies] itertools.workspace = true thiserror.workspace = true [dev-dependencies] anyhow.workspace = true hex-literal.workspace = true [lints] workspace = true ================================================ FILE: crates/trippy-packet/src/buffer.rs ================================================ /// A byte buffer that holds a mutable or immutable byte slice. #[derive(Debug)] pub enum Buffer<'a> { Immutable(&'a [u8]), Mutable(&'a mut [u8]), } impl Buffer<'_> { /// access the buffer as an immutable slice of bytes. pub fn as_slice(&self) -> &[u8] { match &self { Buffer::Immutable(packet) => packet, Buffer::Mutable(packet) => packet, } } /// Get N bytes from the packet at a given byte offset. pub fn get_bytes(&self, offset: usize) -> [u8; N] { core::array::from_fn(|i| self.read(offset + i)) } /// Set N bytes in the packet at a given offset. pub fn set_bytes(&mut self, offset: usize, bytes: [u8; N]) { self.as_slice_mut()[offset..offset + N].copy_from_slice(&bytes); } /// Get the value at a given offset. pub fn read(&self, offset: usize) -> u8 { match &self { Buffer::Immutable(packet) => packet[offset], Buffer::Mutable(packet) => packet[offset], } } /// Set the value at a given offset. pub fn write(&mut self, offset: usize) -> &mut u8 { match self { Buffer::Immutable(_) => panic!("write operation called on readonly buffer"), Buffer::Mutable(packet) => &mut packet[offset], } } /// access the buffer as a mutable slice of bytes. pub fn as_slice_mut(&mut self) -> &mut [u8] { match self { Buffer::Immutable(_) => panic!("write operation called on readonly buffer"), Buffer::Mutable(packet) => packet, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_immutable_buffer() { let buf = [0_u8; 5]; let buffer = Buffer::Immutable(&buf); assert_eq!(buf.as_slice(), buffer.as_slice()); assert_eq!(buf, buffer.get_bytes(0)); assert_eq!(0_u8, buffer.read(0)); } #[test] fn test_mutable_buffer() { let mut buf = [0_u8; 5]; let mut buffer = Buffer::Mutable(&mut buf); assert_eq!(&[0_u8; 5], buffer.as_slice()); assert_eq!([0_u8; 5], buffer.get_bytes(0)); assert_eq!(0_u8, buffer.read(0)); buffer.set_bytes(1, [1_u8; 4]); assert_eq!([1_u8; 4], buffer.get_bytes(1)); *buffer.write(0) = 2; assert_eq!(2_u8, buffer.read(0)); buffer.as_slice_mut().copy_from_slice(&[3_u8; 5]); assert_eq!(&[3_u8; 5], buffer.as_slice()); } #[test] fn test_debug() { let buf = [0_u8; 5]; let buffer = Buffer::Immutable(&buf); assert_eq!( String::from("Immutable([0, 0, 0, 0, 0])"), format!("{buffer:?}") ); let mut buf = [0_u8; 5]; let buffer = Buffer::Mutable(&mut buf); assert_eq!( String::from("Mutable([0, 0, 0, 0, 0])"), format!("{buffer:?}") ); } #[test] #[should_panic(expected = "write operation called on readonly buffer")] fn test_immutable_buffer_cannot_write() { let buf = [0_u8; 5]; let mut buffer = Buffer::Immutable(&buf); buffer.set_bytes(0, [1_u8; 5]); } #[test] #[should_panic(expected = "write operation called on readonly buffer")] fn test_immutable_buffer_cannot_mut_slice() { let buf = [0_u8; 5]; let mut buffer = Buffer::Immutable(&buf); buffer.as_slice_mut(); } } ================================================ FILE: crates/trippy-packet/src/checksum.rs ================================================ //! Checksum implementations for ICMP & UDP over IPv4 and IPV6. //! //! This code is derived from [`libpnet`] which is available under the Apache 2.0 license. //! //! [`libpnet`]: https://github.com/libpnet/libpnet use crate::IpProtocol; use std::net::{Ipv4Addr, Ipv6Addr}; /// Calculate the checksum for an `Ipv4` header. #[must_use] pub fn ipv4_header_checksum(data: &[u8]) -> u16 { checksum(data, 5) } /// Calculate the checksum for an `Ipv4` `ICMP` packet. #[must_use] pub fn icmp_ipv4_checksum(data: &[u8]) -> u16 { checksum(data, 1) } /// Calculate the checksum for an `Ipv4` `ICMP` packet. #[must_use] pub fn icmp_ipv6_checksum(data: &[u8], src_addr: Ipv6Addr, dest_addr: Ipv6Addr) -> u16 { ipv6_checksum(data, 1, src_addr, dest_addr, IpProtocol::IcmpV6) } /// Calculate the checksum for an `IPv4` `UDP` packet. #[must_use] pub fn udp_ipv4_checksum(data: &[u8], src_addr: Ipv4Addr, dest_addr: Ipv4Addr) -> u16 { ipv4_checksum(data, 3, src_addr, dest_addr, IpProtocol::Udp) } /// Calculate the checksum for an `IPv4` `TCP` packet. #[must_use] pub fn tcp_ipv4_checksum(data: &[u8], src_addr: Ipv4Addr, dest_addr: Ipv4Addr) -> u16 { ipv4_checksum(data, 8, src_addr, dest_addr, IpProtocol::Tcp) } /// Calculate the checksum for an `IPv6` `UDP` packet. #[must_use] pub fn udp_ipv6_checksum(data: &[u8], src_addr: Ipv6Addr, dest_addr: Ipv6Addr) -> u16 { ipv6_checksum(data, 3, src_addr, dest_addr, IpProtocol::Udp) } /// Calculate the checksum for an `IPv6` `TCP` packet. #[must_use] pub fn tcp_ipv6_checksum(data: &[u8], src_addr: Ipv6Addr, dest_addr: Ipv6Addr) -> u16 { ipv6_checksum(data, 8, src_addr, dest_addr, IpProtocol::Tcp) } fn checksum(data: &[u8], ignore_word: usize) -> u16 { if data.is_empty() { return 0; } let sum = sum_be_words(data, ignore_word); finalize_checksum(sum) } fn ipv4_checksum( data: &[u8], ignore_word: usize, source: Ipv4Addr, destination: Ipv4Addr, next_level_protocol: IpProtocol, ) -> u16 { let mut sum = 0u32; sum += ipv4_word_sum(source); sum += ipv4_word_sum(destination); sum += u32::from(next_level_protocol.id()); sum += data.len() as u32; sum += sum_be_words(data, ignore_word); finalize_checksum(sum) } fn ipv4_word_sum(ip: Ipv4Addr) -> u32 { let octets = ip.octets(); (((u32::from(octets[0])) << 8) | u32::from(octets[1])) + (((u32::from(octets[2])) << 8) | u32::from(octets[3])) } /// Calculate the checksum for a packet built on IPv6. fn ipv6_checksum( data: &[u8], ignore_word: usize, source: Ipv6Addr, destination: Ipv6Addr, next_level_protocol: IpProtocol, ) -> u16 { let mut sum = 0u32; sum += ipv6_word_sum(source); sum += ipv6_word_sum(destination); sum += u32::from(next_level_protocol.id()); sum += data.len() as u32; sum += sum_be_words(data, ignore_word); finalize_checksum(sum) } fn ipv6_word_sum(ip: Ipv6Addr) -> u32 { ip.segments().iter().map(|x| u32::from(*x)).sum() } fn sum_be_words(data: &[u8], ignore_word: usize) -> u32 { if data.is_empty() { return 0; } let len = data.len(); let mut cur_data = data; let mut sum = 0u32; let mut i = 0; while cur_data.len() >= 2 { if i != ignore_word { sum += u32::from(u16::from_be_bytes(cur_data[0..2].try_into().unwrap())); } cur_data = &cur_data[2..]; i += 1; } if i != ignore_word && len & 1 != 0 { sum += u32::from(data[len - 1]) << 8; } sum } const fn finalize_checksum(mut sum: u32) -> u16 { while sum >> 16 != 0 { sum = (sum >> 16) + (sum & 0xFFFF); } !sum as u16 } #[cfg(test)] mod tests { use super::*; use hex_literal::hex; use std::str::FromStr; #[test] fn test_empty_ipv4_checksum() { let src_addr = Ipv4Addr::from_str("192.168.1.201").unwrap(); let dest_addr = Ipv4Addr::from_str("142.250.66.46").unwrap(); assert_eq!(0, ipv4_header_checksum(&[])); assert_eq!(0, icmp_ipv4_checksum(&[])); assert_eq!(27732, udp_ipv4_checksum(&[], src_addr, dest_addr)); assert_eq!(27743, tcp_ipv4_checksum(&[], src_addr, dest_addr)); } #[test] fn test_empty_ipv6_checksum() { let src_addr = Ipv6Addr::from_str("fe80::811:3f6:7601:6c3f").unwrap(); let dest_addr = Ipv6Addr::from_str("fe80::1c8d:7d69:d0b6:8182").unwrap(); assert_eq!(10316, icmp_ipv6_checksum(&[], src_addr, dest_addr)); assert_eq!(10357, udp_ipv6_checksum(&[], src_addr, dest_addr)); } #[test] fn test_odd_length() { assert_eq!(65535, ipv4_header_checksum(&[0x00])); } #[test] fn test_icmp_ipv4_checksum() { let bytes = [ 0x0b, 0x00, 0x88, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x54, 0xb0, 0xde, 0x00, 0x00, 0x01, 0x11, 0x75, 0x21, 0xc0, 0xa8, 0x01, 0xc9, 0x8e, 0xfa, 0x42, 0x2e, 0x62, 0x57, 0x81, 0x95, 0x00, 0x40, 0x87, 0xe7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; assert_eq!(35051, icmp_ipv4_checksum(&bytes)); } #[test] fn test_icmp_ipv6_checksum() { let src_addr = Ipv6Addr::from_str("fe80::811:3f6:7601:6c3f").unwrap(); let dest_addr = Ipv6Addr::from_str("fe80::1c8d:7d69:d0b6:8182").unwrap(); let bytes = [ 0x88, 0x00, 0x73, 0x6a, 0x40, 0x00, 0x00, 0x00, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x11, 0x03, 0xf6, 0x76, 0x01, 0x6c, 0x3f, ]; assert_eq!(29546, icmp_ipv6_checksum(&bytes, src_addr, dest_addr)); } #[test] fn test_udp_ipv4_checksum() { let src_addr = Ipv4Addr::from_str("192.168.1.201").unwrap(); let dest_addr = Ipv4Addr::from_str("142.250.66.46").unwrap(); let bytes = [ 0x62, 0x57, 0x81, 0xa8, 0x00, 0x40, 0x87, 0xd4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; assert_eq!(34772, udp_ipv4_checksum(&bytes, src_addr, dest_addr)); } #[test] fn test_udp_ipv6_checksum() { let src_addr = Ipv6Addr::from_str("2406:da18:599:2d01:fa25:98be:5ab1:87a5").unwrap(); let dest_addr = Ipv6Addr::from_str("2404:6800:4003:c02::8b").unwrap(); let bytes = [ 0x10, 0x13, 0x80, 0xeb, 0x00, 0x2c, 0xf0, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; assert_eq!(61454, udp_ipv6_checksum(&bytes, src_addr, dest_addr)); } #[test] fn test_tcp_ipv6_checksum() { let src_addr = Ipv6Addr::from_str("fd7a:115c:a1e0:ab12:4843:cd96:6263:82a").unwrap(); let dest_addr = Ipv6Addr::from_str("2404:6800:4003:c03::bc").unwrap(); let bytes = hex!("fa 54 14 6c 96 16 44 89 08 f0 39 2b 50 10 08 00 c7 60 00 00"); assert_eq!(0xc760, tcp_ipv6_checksum(&bytes, src_addr, dest_addr)); } #[test] fn test_ipv4_header_checksum() { let bytes = hex!("45 00 0f fc 38 c0 00 00 40 01 2e 3b 0a 00 00 02 0a 00 00 01"); assert_eq!(0x1e3f, ipv4_header_checksum(&bytes)); } #[test] fn test_tcp_ipv4_checksum() { let bytes = hex!("00 50 80 ea 00 00 00 00 95 9d 2e c7 50 12 ff ff 55 cc 00 00"); assert_eq!( 0x55cc, tcp_ipv4_checksum( &bytes, Ipv4Addr::new(10, 0, 0, 103), Ipv4Addr::new(10, 0, 0, 1) ) ); } } ================================================ FILE: crates/trippy-packet/src/error.rs ================================================ use thiserror::Error; /// A packet error result. pub type Result = std::result::Result; /// A packet error. #[derive(Error, Debug, Eq, PartialEq)] pub enum Error { /// Attempting to create a packet with an insufficient buffer size. #[error("insufficient buffer for {0} packet, minimum={1}, provided={2}")] InsufficientPacketBuffer(String, usize, usize), } ================================================ FILE: crates/trippy-packet/src/icmp_extension.rs ================================================ pub mod extension_structure { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::icmp_extension::extension_object::ExtensionObjectPacket; /// Represents an ICMP `ExtensionsPacket` pseudo object. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct ExtensionsPacket<'a> { buf: Buffer<'a>, } impl<'a> ExtensionsPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("ExtensionsPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("ExtensionsPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 4 } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn header(&self) -> &[u8] { &self.buf.as_slice()[..Self::minimum_packet_size()] } /// An iterator of Extension Objects contained within this `ExtensionsPacket`. #[must_use] pub const fn objects(&self) -> ExtensionObjectIter<'_> { ExtensionObjectIter::new(&self.buf) } } pub struct ExtensionObjectIter<'a> { buf: &'a Buffer<'a>, offset: usize, } impl<'a> ExtensionObjectIter<'a> { #[must_use] pub const fn new(buf: &'a Buffer<'_>) -> Self { Self { buf, offset: ExtensionsPacket::minimum_packet_size(), } } } impl<'a> Iterator for ExtensionObjectIter<'a> { type Item = &'a [u8]; fn next(&mut self) -> Option { let buf_slice = self.buf.as_slice(); if self.offset > buf_slice.len() { None } else { let object_bytes = &buf_slice[self.offset..]; if let Ok(object) = ExtensionObjectPacket::new_view(object_bytes) { let length = usize::from(object.get_length()); // If a malformed extension object has a length that is less than the minimum // size or extends beyond the end of available bytes, then we discard it. if length < ExtensionObjectPacket::minimum_packet_size() || length > object_bytes.len() { return None; } self.offset += length; Some(object_bytes) } else { None } } } } #[cfg(test)] mod tests { use super::*; use crate::icmp_extension::extension_header::ExtensionHeaderPacket; use crate::icmp_extension::extension_object::{ ClassNum, ClassSubType, ExtensionObjectPacket, }; #[test] fn test_header() { let buf = [ 0x20, 0x00, 0x99, 0x3a, 0x00, 0x08, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01, ]; let extensions = ExtensionsPacket::new_view(&buf).unwrap(); let header = ExtensionHeaderPacket::new_view(extensions.header()).unwrap(); assert_eq!(2, header.get_version()); assert_eq!(0x993A, header.get_checksum()); } #[test] fn test_object_iterator() { let buf = [ 0x20, 0x00, 0x99, 0x3a, 0x00, 0x08, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01, ]; let extensions = ExtensionsPacket::new_view(&buf).unwrap(); let mut object_iter = extensions.objects(); let object_bytes = object_iter.next().unwrap(); let object = ExtensionObjectPacket::new_view(object_bytes).unwrap(); assert_eq!(8, object.get_length()); assert_eq!( ClassNum::MultiProtocolLabelSwitchingLabelStack, object.get_class_num() ); assert_eq!(ClassSubType(1), object.get_class_subtype()); assert_eq!([0x04, 0xbb, 0x41, 0x01], object.payload()); assert!(object_iter.next().is_none()); } #[test] fn test_object_iterator_zero_length() { let buf = [ 0x20, 0x00, 0x99, 0x3a, 0x00, 0x00, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01, ]; let extensions = ExtensionsPacket::new_view(&buf).unwrap(); let mut object_iter = extensions.objects(); assert!(object_iter.next().is_none()); } #[test] fn test_object_iterator_minimum_length() { let buf = [ 0x20, 0x00, 0x99, 0x3a, 0x00, 0x04, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01, ]; let extensions = ExtensionsPacket::new_view(&buf).unwrap(); let mut object_iter = extensions.objects(); let object_bytes = object_iter.next().unwrap(); let object = ExtensionObjectPacket::new_view(object_bytes).unwrap(); assert_eq!(4, object.get_length()); assert_eq!(0, object.payload().len()); } #[test] fn test_object_iterator_length_to_short() { let buf = [ 0x20, 0x00, 0x99, 0x3a, 0x00, 0x03, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01, ]; let extensions = ExtensionsPacket::new_view(&buf).unwrap(); let mut object_iter = extensions.objects(); assert!(object_iter.next().is_none()); } #[test] fn test_object_iterator_length_to_long() { let buf = [ 0x20, 0x00, 0x99, 0x3a, 0xa7, 0xdd, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01, ]; let extensions = ExtensionsPacket::new_view(&buf).unwrap(); let mut object_iter = extensions.objects(); assert!(object_iter.next().is_none()); } } } pub mod extension_header { use crate::buffer::Buffer; use crate::error::{Error, Result}; use std::fmt::{Debug, Formatter}; const VERSION_OFFSET: usize = 0; const CHECKSUM_OFFSET: usize = 2; /// Represents an ICMP `ExtensionHeaderPacket`. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct ExtensionHeaderPacket<'a> { buf: Buffer<'a>, } impl<'a> ExtensionHeaderPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("ExtensionHeaderPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("ExtensionHeaderPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 4 } #[must_use] pub fn get_version(&self) -> u8 { (self.buf.read(VERSION_OFFSET) & 0xf0) >> 4 } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } pub fn set_version(&mut self, val: u8) { *self.buf.write(VERSION_OFFSET) = (self.buf.read(VERSION_OFFSET) & 0xf) | ((val & 0xf) << 4); } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } } impl Debug for ExtensionHeaderPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("ExtensionHeader") .field("version", &self.get_version()) .field("checksum", &self.get_checksum()) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_version() { let mut buf = [0_u8; ExtensionHeaderPacket::minimum_packet_size()]; let mut extension = ExtensionHeaderPacket::new(&mut buf).unwrap(); extension.set_version(0); assert_eq!(0, extension.get_version()); assert_eq!([0x00], extension.packet()[0..1]); extension.set_version(2); assert_eq!(2, extension.get_version()); assert_eq!([0x20], extension.packet()[0..1]); extension.set_version(15); assert_eq!(15, extension.get_version()); assert_eq!([0xF0], extension.packet()[0..1]); } #[test] fn test_checksum() { let mut buf = [0_u8; ExtensionHeaderPacket::minimum_packet_size()]; let mut extension = ExtensionHeaderPacket::new(&mut buf).unwrap(); extension.set_checksum(0); assert_eq!(0, extension.get_checksum()); assert_eq!([0x00, 0x00], extension.packet()[2..=3]); extension.set_checksum(1999); assert_eq!(1999, extension.get_checksum()); assert_eq!([0x07, 0xCF], extension.packet()[2..=3]); extension.set_checksum(39226); assert_eq!(39226, extension.get_checksum()); assert_eq!([0x99, 0x3A], extension.packet()[2..=3]); extension.set_checksum(u16::MAX); assert_eq!(u16::MAX, extension.get_checksum()); assert_eq!([0xFF, 0xFF], extension.packet()[2..=3]); } #[test] fn test_extension_header_view() { let buf = [ 0x20, 0x00, 0x99, 0x3a, 0x00, 0x08, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01, ]; let extension = ExtensionHeaderPacket::new_view(&buf).unwrap(); assert_eq!(2, extension.get_version()); assert_eq!(0x993A, extension.get_checksum()); } } } pub mod extension_object { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use std::fmt::{Debug, Formatter}; /// The ICMP Extension Object Class Num. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub enum ClassNum { MultiProtocolLabelSwitchingLabelStack, InterfaceInformationObject, InterfaceIdentificationObject, ExtendedInformation, Other(u8), } impl ClassNum { #[must_use] pub const fn id(&self) -> u8 { match self { Self::MultiProtocolLabelSwitchingLabelStack => 1, Self::InterfaceInformationObject => 2, Self::InterfaceIdentificationObject => 3, Self::ExtendedInformation => 4, Self::Other(id) => *id, } } } impl From for ClassNum { fn from(val: u8) -> Self { match val { 1 => Self::MultiProtocolLabelSwitchingLabelStack, 2 => Self::InterfaceInformationObject, 3 => Self::InterfaceIdentificationObject, 4 => Self::ExtendedInformation, id => Self::Other(id), } } } /// The ICMP Extension Object Class Sub-type. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub struct ClassSubType(pub u8); impl From for ClassSubType { fn from(val: u8) -> Self { Self(val) } } const LENGTH_OFFSET: usize = 0; const CLASS_NUM_OFFSET: usize = 2; const CLASS_SUBTYPE_OFFSET: usize = 3; /// Represents an ICMP `ExtensionObjectPacket`. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct ExtensionObjectPacket<'a> { buf: Buffer<'a>, } impl<'a> ExtensionObjectPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("ExtensionObjectPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("ExtensionObjectPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 4 } pub fn set_length(&mut self, val: u16) { self.buf.set_bytes(LENGTH_OFFSET, val.to_be_bytes()); } pub fn set_class_num(&mut self, val: ClassNum) { *self.buf.write(CLASS_NUM_OFFSET) = val.id(); } pub fn set_class_subtype(&mut self, val: ClassSubType) { *self.buf.write(CLASS_SUBTYPE_OFFSET) = val.0; } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()] .copy_from_slice(vals); } #[must_use] pub fn get_length(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(LENGTH_OFFSET)) } #[must_use] pub fn get_class_num(&self) -> ClassNum { ClassNum::from(self.buf.read(CLASS_NUM_OFFSET)) } #[must_use] pub fn get_class_subtype(&self) -> ClassSubType { ClassSubType::from(self.buf.read(CLASS_SUBTYPE_OFFSET)) } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..usize::from(self.get_length())] } } impl Debug for ExtensionObjectPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("ExtensionObject") .field("length", &self.get_length()) .field("class_num", &self.get_class_num()) .field("class_subtype", &self.get_class_subtype()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_length() { let mut buf = [0_u8; ExtensionObjectPacket::minimum_packet_size()]; let mut extension = ExtensionObjectPacket::new(&mut buf).unwrap(); extension.set_length(0); assert_eq!(0, extension.get_length()); assert_eq!([0x00, 0x00], extension.packet()[0..=1]); extension.set_length(8); assert_eq!(8, extension.get_length()); assert_eq!([0x00, 0x08], extension.packet()[0..=1]); extension.set_length(u16::MAX); assert_eq!(u16::MAX, extension.get_length()); assert_eq!([0xFF, 0xFF], extension.packet()[0..=1]); } #[test] fn test_class_num() { let mut buf = [0_u8; ExtensionObjectPacket::minimum_packet_size()]; let mut extension = ExtensionObjectPacket::new(&mut buf).unwrap(); extension.set_class_num(ClassNum::MultiProtocolLabelSwitchingLabelStack); assert_eq!( ClassNum::MultiProtocolLabelSwitchingLabelStack, extension.get_class_num() ); assert_eq!([0x01], extension.packet()[2..3]); extension.set_class_num(ClassNum::InterfaceInformationObject); assert_eq!( ClassNum::InterfaceInformationObject, extension.get_class_num() ); assert_eq!([0x02], extension.packet()[2..3]); extension.set_class_num(ClassNum::InterfaceIdentificationObject); assert_eq!( ClassNum::InterfaceIdentificationObject, extension.get_class_num() ); assert_eq!([0x03], extension.packet()[2..3]); extension.set_class_num(ClassNum::ExtendedInformation); assert_eq!(ClassNum::ExtendedInformation, extension.get_class_num()); assert_eq!([0x04], extension.packet()[2..3]); extension.set_class_num(ClassNum::Other(255)); assert_eq!(ClassNum::Other(255), extension.get_class_num()); assert_eq!([0xFF], extension.packet()[2..3]); } #[test] fn test_class_subtype() { let mut buf = [0_u8; ExtensionObjectPacket::minimum_packet_size()]; let mut extension = ExtensionObjectPacket::new(&mut buf).unwrap(); extension.set_class_subtype(ClassSubType(0)); assert_eq!(ClassSubType(0), extension.get_class_subtype()); assert_eq!([0x00], extension.packet()[3..4]); extension.set_class_subtype(ClassSubType(1)); assert_eq!(ClassSubType(1), extension.get_class_subtype()); assert_eq!([0x01], extension.packet()[3..4]); extension.set_class_subtype(ClassSubType(255)); assert_eq!(ClassSubType(255), extension.get_class_subtype()); assert_eq!([0xff], extension.packet()[3..4]); } #[test] fn test_extension_header_view() { let buf = [0x00, 0x08, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01]; let object = ExtensionObjectPacket::new_view(&buf).unwrap(); assert_eq!(8, object.get_length()); assert_eq!( ClassNum::MultiProtocolLabelSwitchingLabelStack, object.get_class_num() ); assert_eq!(ClassSubType(1), object.get_class_subtype()); assert_eq!([0x04, 0xbb, 0x41, 0x01], object.payload()); } } } pub mod mpls_label_stack { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::icmp_extension::mpls_label_stack_member::MplsLabelStackMemberPacket; /// Represents an ICMP `MplsLabelStackPacket`. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct MplsLabelStackPacket<'a> { buf: Buffer<'a>, } impl<'a> MplsLabelStackPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("MplsLabelStackPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("MplsLabelStackPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 4 } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub const fn members(&self) -> MplsLabelStackIter<'_> { MplsLabelStackIter::new(&self.buf) } } pub struct MplsLabelStackIter<'a> { buf: &'a Buffer<'a>, offset: usize, bos: u8, } impl<'a> MplsLabelStackIter<'a> { #[must_use] pub const fn new(buf: &'a Buffer<'_>) -> Self { Self { buf, offset: 0, bos: 0, } } } impl<'a> Iterator for MplsLabelStackIter<'a> { type Item = &'a [u8]; fn next(&mut self) -> Option { if self.bos > 0 || self.offset >= self.buf.as_slice().len() { None } else { let member_bytes = &self.buf.as_slice()[self.offset..]; if let Ok(member) = MplsLabelStackMemberPacket::new_view(member_bytes) { self.bos = member.get_bos(); self.offset += MplsLabelStackMemberPacket::minimum_packet_size(); Some(member_bytes) } else { None } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_stack_member_iterator() { let buf = [0x04, 0xbb, 0x41, 0x01]; let stack = MplsLabelStackPacket::new_view(&buf).unwrap(); let mut member_iter = stack.members(); let member_bytes = member_iter.next().unwrap(); let member = MplsLabelStackMemberPacket::new_view(member_bytes).unwrap(); assert_eq!(19380, member.get_label()); assert_eq!(0, member.get_exp()); assert_eq!(1, member.get_bos()); assert_eq!(1, member.get_ttl()); assert!(member_iter.next().is_none()); } } } pub mod mpls_label_stack_member { use crate::buffer::Buffer; use crate::error::{Error, Result}; use std::fmt::{Debug, Formatter}; const LABEL_OFFSET: usize = 0; const EXP_OFFSET: usize = 2; const BOS_OFFSET: usize = 2; const TTL_OFFSET: usize = 3; /// Represents an ICMP `MplsLabelStackMemberPacket`. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct MplsLabelStackMemberPacket<'a> { buf: Buffer<'a>, } impl<'a> MplsLabelStackMemberPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("MplsLabelStackMemberPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("MplsLabelStackMemberPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 4 } #[must_use] pub fn get_label(&self) -> u32 { u32::from_be_bytes([ 0x0, self.buf.read(LABEL_OFFSET), self.buf.read(LABEL_OFFSET + 1), self.buf.read(LABEL_OFFSET + 2), ]) >> 4 } #[must_use] pub fn get_exp(&self) -> u8 { (self.buf.read(EXP_OFFSET) & 0x0e) >> 1 } #[must_use] pub fn get_bos(&self) -> u8 { self.buf.read(BOS_OFFSET) & 0x01 } #[must_use] pub fn get_ttl(&self) -> u8 { self.buf.read(TTL_OFFSET) } pub fn set_label(&mut self, val: u32) { let bytes = (val << 4).to_be_bytes(); *self.buf.write(LABEL_OFFSET) = bytes[1]; *self.buf.write(LABEL_OFFSET + 1) = bytes[2]; *self.buf.write(LABEL_OFFSET + 2) = (self.buf.read(LABEL_OFFSET + 2) & 0x0f) | (bytes[3] & 0xf0); } pub fn set_exp(&mut self, exp: u8) { *self.buf.write(EXP_OFFSET) = (self.buf.read(EXP_OFFSET) & 0xf1) | ((exp << 1) & 0x0e); } pub fn set_bos(&mut self, bos: u8) { *self.buf.write(BOS_OFFSET) = (self.buf.read(BOS_OFFSET) & 0xfe) | (bos & 0x01); } pub fn set_ttl(&mut self, ttl: u8) { *self.buf.write(TTL_OFFSET) = ttl; } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } } impl Debug for MplsLabelStackMemberPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("MplsLabelStackMember") .field("label", &self.get_label()) .field("exp", &self.get_exp()) .field("bos", &self.get_bos()) .field("ttl", &self.get_ttl()) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_label() { let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()]; let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap(); mpls_extension.set_label(0); assert_eq!(0, mpls_extension.get_label()); assert_eq!([0x00, 0x00, 0x00], mpls_extension.packet()[0..3]); mpls_extension.set_label(19380); assert_eq!(19380, mpls_extension.get_label()); assert_eq!([0x04, 0xbb, 0x40], mpls_extension.packet()[0..3]); mpls_extension.set_label(1_048_575); assert_eq!(1_048_575, mpls_extension.get_label()); assert_eq!([0xff, 0xff, 0xf0], mpls_extension.packet()[0..3]); } #[test] fn test_exp() { let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()]; let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap(); mpls_extension.set_exp(0); assert_eq!(0, mpls_extension.get_exp()); assert_eq!([0x00], mpls_extension.packet()[2..3]); mpls_extension.set_exp(7); assert_eq!(7, mpls_extension.get_exp()); assert_eq!([0x0e], mpls_extension.packet()[2..3]); } #[test] fn test_bos() { let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()]; let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap(); mpls_extension.set_bos(0); assert_eq!(0, mpls_extension.get_bos()); assert_eq!([0x00], mpls_extension.packet()[2..3]); mpls_extension.set_bos(1); assert_eq!(1, mpls_extension.get_bos()); assert_eq!([0x01], mpls_extension.packet()[2..3]); } #[test] fn test_ttl() { let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()]; let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap(); mpls_extension.set_ttl(0); assert_eq!(0, mpls_extension.get_ttl()); assert_eq!([0x00], mpls_extension.packet()[3..4]); mpls_extension.set_ttl(1); assert_eq!(1, mpls_extension.get_ttl()); assert_eq!([0x01], mpls_extension.packet()[3..4]); mpls_extension.set_ttl(255); assert_eq!(255, mpls_extension.get_ttl()); assert_eq!([0xff], mpls_extension.packet()[3..4]); } #[test] fn test_combined() { let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()]; let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap(); mpls_extension.set_label(19380); mpls_extension.set_exp(0); mpls_extension.set_bos(1); mpls_extension.set_ttl(1); assert_eq!(19380, mpls_extension.get_label()); assert_eq!(0, mpls_extension.get_exp()); assert_eq!(1, mpls_extension.get_bos()); assert_eq!(1, mpls_extension.get_ttl()); assert_eq!([0x04, 0xbb, 0x41, 0x01], mpls_extension.packet()[0..4]); mpls_extension.set_label(1_048_575); mpls_extension.set_exp(7); mpls_extension.set_bos(1); mpls_extension.set_ttl(255); assert_eq!(1_048_575, mpls_extension.get_label()); assert_eq!(7, mpls_extension.get_exp()); assert_eq!(1, mpls_extension.get_bos()); assert_eq!(255, mpls_extension.get_ttl()); assert_eq!([0xff, 0xff, 0xff, 0xff], mpls_extension.packet()[0..4]); } #[test] fn test_view() { let buf = [0x04, 0xbb, 0x41, 0x01]; let object = MplsLabelStackMemberPacket::new_view(&buf).unwrap(); assert_eq!(19380, object.get_label()); assert_eq!(0, object.get_exp()); assert_eq!(1, object.get_bos()); assert_eq!(1, object.get_ttl()); } } } pub mod extension_splitter { use crate::icmp_extension::extension_header::ExtensionHeaderPacket; const MIN_HEADER: usize = ExtensionHeaderPacket::minimum_packet_size(); /// From rfc4884 (section 3) entitled "Summary of Changes to ICMP": /// /// "When the ICMP Extension Structure is appended to an ICMP message /// and that ICMP message contains an "original datagram" field, the /// "original datagram" field MUST contain at least 128 octets." const ICMP_ORIG_DATAGRAM_MIN_LENGTH: usize = 128; /// Separate an ICMP payload from ICMP extensions as defined in rfc4884. /// /// Applies to `TimeExceeded` and `DestinationUnreachable` ICMP messages only. #[must_use] pub fn split(length: usize, icmp_payload: &[u8]) -> (&[u8], Option<&[u8]>) { // If the rfc4884 length field provided is larger than the payload length then // the full payload is returned without any extension. if length > icmp_payload.len() { return (icmp_payload, None); } if icmp_payload.len() > ICMP_ORIG_DATAGRAM_MIN_LENGTH { if length > ICMP_ORIG_DATAGRAM_MIN_LENGTH { // a 'compliant' ICMP extension longer than 128 octets. match icmp_payload.split_at(length) { (payload, extension) if extension.len() >= MIN_HEADER => { (payload, Some(extension)) } _ => (icmp_payload, None), } } else if length > 0 { // a 'compliant' ICMP extension padded to at least 128 octets, // so we trim the original datagram to rfc4884 length. match icmp_payload.split_at(ICMP_ORIG_DATAGRAM_MIN_LENGTH) { (payload, extension) if extension.len() >= MIN_HEADER => { (&payload[..length], Some(extension)) } _ => (icmp_payload, None), } } else { // a 'non-compliant' ICMP extension padded to 128 octets. match icmp_payload.split_at(ICMP_ORIG_DATAGRAM_MIN_LENGTH) { (payload, extension) if extension.len() >= MIN_HEADER => { (payload, Some(extension)) } _ => (icmp_payload, None), } } } else { // no extension present (icmp_payload, None) } } #[cfg(test)] mod tests { use super::*; use crate::icmp_extension::extension_header::ExtensionHeaderPacket; use crate::icmp_extension::extension_object::{ ClassNum, ClassSubType, ExtensionObjectPacket, }; use crate::icmp_extension::extension_structure::ExtensionsPacket; use crate::icmp_extension::mpls_label_stack::MplsLabelStackPacket; use crate::icmp_extension::mpls_label_stack_member::MplsLabelStackMemberPacket; #[test] fn test_split_empty_payload() { let icmp_payload: [u8; 0] = []; let (payload, extension) = split(0, &icmp_payload); assert!(payload.is_empty() && extension.is_none()); } // Test ICMP payload which is 12 bytes and has rfc4884 length of 3 (12 // bytes) so payload is 12 bytes and there is no extension. #[test] fn test_split_payload_with_compliant_empty_extension() { let icmp_payload: [u8; 12] = [0; 12]; let (payload, extension) = split(3 * 4, &icmp_payload); assert_eq!(payload, &[0; 12]); assert_eq!(extension, None); } // Test ICMP payload with a minimal compliant extension. #[test] fn test_split_payload_with_compliant_minimal_extension() { let icmp_payload: [u8; 132] = [0; 132]; let (payload, extension) = split(32 * 4, &icmp_payload); assert_eq!(payload, &[0; 128]); assert_eq!(extension, Some([0; 4].as_slice())); } // Test handling of an ICMP payload which has a rfc4884 length that // is longer than the original datagram. // // For such invalid packets we assume there is no extension. #[test] fn test_split_payload_with_invalid_rfc4884_length() { let icmp_payload: [u8; 128] = [0; 128]; let (payload, extension) = split(33 * 4, &icmp_payload); assert_eq!(payload, &[0; 128]); assert!(extension.is_none()); } // Test handling of an ICMP payload which has a compliant extension // which is not as long as the minimum size for an ICMP extension // header (4 bytes). // // For such invalid packets we assume there is no extension. #[test] fn test_split_payload_with_compliant_invalid_extension() { let icmp_payload: [u8; 129] = [0; 129]; let (payload, extension) = split(32 * 4, &icmp_payload); assert_eq!(payload, &[0; 129]); assert!(extension.is_none()); } mod ipv4 { use super::*; use crate::icmpv4::echo_request::EchoRequestPacket; use crate::icmpv4::time_exceeded::TimeExceededPacket; use crate::icmpv4::{IcmpCode, IcmpType}; use crate::ipv4::Ipv4Packet; use std::net::Ipv4Addr; // This ICMP `TimeExceeded` packet which contains single `MPLS` extension // object with a single member. The packet does not have a `length` // field and is therefore rfc4884 non-complaint. #[test] fn test_split_extension_ipv4_time_exceeded_non_compliant_mpls() { let buf = hex_literal::hex!( " 0b 00 f4 ff 00 00 00 00 45 00 00 54 cc 1c 40 00 01 01 b5 f4 c0 a8 01 15 5d b8 d8 22 08 00 0f e3 65 da 82 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00 99 3a 00 08 01 01 04 bb 41 01 " ); let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::TimeExceeded, time_exceeded_packet.get_icmp_type()); assert_eq!(IcmpCode(0), time_exceeded_packet.get_icmp_code()); assert_eq!(62719, time_exceeded_packet.get_checksum()); assert_eq!(0, time_exceeded_packet.get_length()); assert_eq!(&buf[8..136], time_exceeded_packet.payload()); assert_eq!(Some(&buf[136..]), time_exceeded_packet.extension()); let nested_ipv4 = Ipv4Packet::new_view(time_exceeded_packet.payload()).unwrap(); assert_eq!(Ipv4Addr::from([192, 168, 1, 21]), nested_ipv4.get_source()); assert_eq!( Ipv4Addr::from([93, 184, 216, 34]), nested_ipv4.get_destination() ); assert_eq!(&buf[28..136], nested_ipv4.payload()); let nested_echo = EchoRequestPacket::new_view(nested_ipv4.payload()).unwrap(); assert_eq!(IcmpCode(0), nested_echo.get_icmp_code()); assert_eq!(IcmpType::EchoRequest, nested_echo.get_icmp_type()); assert_eq!(0x0FE3, nested_echo.get_checksum()); assert_eq!(26074, nested_echo.get_identifier()); assert_eq!(33346, nested_echo.get_sequence()); assert_eq!(&buf[36..136], nested_echo.payload()); let extensions = ExtensionsPacket::new_view(time_exceeded_packet.extension().unwrap()).unwrap(); let extension_header = ExtensionHeaderPacket::new_view(extensions.header()).unwrap(); assert_eq!(2, extension_header.get_version()); assert_eq!(0x993A, extension_header.get_checksum()); let object_bytes = extensions.objects().next().unwrap(); let extension_object = ExtensionObjectPacket::new_view(object_bytes).unwrap(); assert_eq!(8, extension_object.get_length()); assert_eq!( ClassNum::MultiProtocolLabelSwitchingLabelStack, extension_object.get_class_num() ); assert_eq!(ClassSubType(1), extension_object.get_class_subtype()); assert_eq!([0x04, 0xbb, 0x41, 0x01], extension_object.payload()); let mpls_stack = MplsLabelStackPacket::new_view(extension_object.payload()).unwrap(); let mpls_stack_member_bytes = mpls_stack.members().next().unwrap(); let mpls_stack_member = MplsLabelStackMemberPacket::new_view(mpls_stack_member_bytes).unwrap(); assert_eq!(19380, mpls_stack_member.get_label()); assert_eq!(0, mpls_stack_member.get_exp()); assert_eq!(1, mpls_stack_member.get_bos()); assert_eq!(1, mpls_stack_member.get_ttl()); } // This ICMP `TimeExceeded` packet does not have any ICMP extensions. // It has a rfc4884 complaint `length` field. #[test] fn test_split_extension_ipv4_time_exceeded_compliant_no_extension() { let buf = hex_literal::hex!( " 0b 00 f4 ee 00 11 00 00 45 00 00 54 a2 ee 40 00 01 01 df 22 c0 a8 01 15 5d b8 d8 22 08 00 0f e1 65 da 82 44 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::TimeExceeded, time_exceeded_packet.get_icmp_type()); assert_eq!(IcmpCode(0), time_exceeded_packet.get_icmp_code()); assert_eq!(62702, time_exceeded_packet.get_checksum()); assert_eq!(17, time_exceeded_packet.get_length()); assert_eq!(&buf[8..76], time_exceeded_packet.payload()); assert_eq!(None, time_exceeded_packet.extension()); let nested_ipv4 = Ipv4Packet::new_view(&buf[8..76]).unwrap(); assert_eq!(Ipv4Addr::from([192, 168, 1, 21]), nested_ipv4.get_source()); assert_eq!( Ipv4Addr::from([93, 184, 216, 34]), nested_ipv4.get_destination() ); assert_eq!(&buf[28..76], nested_ipv4.payload()); let nested_echo = EchoRequestPacket::new_view(nested_ipv4.payload()).unwrap(); assert_eq!(IcmpCode(0), nested_echo.get_icmp_code()); assert_eq!(IcmpType::EchoRequest, nested_echo.get_icmp_type()); assert_eq!(0x0FE1, nested_echo.get_checksum()); assert_eq!(26074, nested_echo.get_identifier()); assert_eq!(33348, nested_echo.get_sequence()); assert_eq!(&buf[36..76], nested_echo.payload()); } // This is a real example that was observed in the wild whilst testing. // // It has a rfc4884 complaint `length` field set to be 17 and so has // an original datagram if length 68 octet (17 * 4 = 68) but is padded // to be 128 octets. // // See `https://github.com/fujiapple852/trippy/issues/804` for further // discussion and analysis of this case. #[test] fn test_split_extension_ipv4_time_exceeded_compliant_extension() { let buf = hex_literal::hex!( " 0b 00 f4 ee 00 11 00 00 45 00 00 54 20 c3 40 00 02 01 b5 7e 64 63 08 2a 5d b8 d8 22 08 00 11 8d 65 83 80 ef 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00 78 56 00 08 01 01 65 9f 01 01 " ); let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap(); assert_eq!(68, time_exceeded_packet.payload().len()); assert_eq!(12, time_exceeded_packet.extension().unwrap().len()); let extensions = ExtensionsPacket::new_view(time_exceeded_packet.extension().unwrap()).unwrap(); let extension_header = ExtensionHeaderPacket::new_view(extensions.header()).unwrap(); assert_eq!(2, extension_header.get_version()); assert_eq!(0x7856, extension_header.get_checksum()); let object_bytes = extensions.objects().next().unwrap(); let extension_object = ExtensionObjectPacket::new_view(object_bytes).unwrap(); assert_eq!(8, extension_object.get_length()); assert_eq!( ClassNum::MultiProtocolLabelSwitchingLabelStack, extension_object.get_class_num() ); assert_eq!(ClassSubType(1), extension_object.get_class_subtype()); assert_eq!([0x65, 0x9f, 0x01, 0x01], extension_object.payload()); let mpls_stack = MplsLabelStackPacket::new_view(extension_object.payload()).unwrap(); let mpls_stack_member_bytes = mpls_stack.members().next().unwrap(); let mpls_stack_member = MplsLabelStackMemberPacket::new_view(mpls_stack_member_bytes).unwrap(); assert_eq!(416_240, mpls_stack_member.get_label()); assert_eq!(0, mpls_stack_member.get_exp()); assert_eq!(1, mpls_stack_member.get_bos()); assert_eq!(1, mpls_stack_member.get_ttl()); } } mod ipv6 { use crate::icmp_extension::extension_header::ExtensionHeaderPacket; use crate::icmp_extension::extension_object::{ ClassNum, ClassSubType, ExtensionObjectPacket, }; use crate::icmp_extension::extension_structure::ExtensionsPacket; use crate::icmp_extension::mpls_label_stack::MplsLabelStackPacket; use crate::icmp_extension::mpls_label_stack_member::MplsLabelStackMemberPacket; use crate::icmpv6::echo_request::EchoRequestPacket; use crate::icmpv6::time_exceeded::TimeExceededPacket; use crate::icmpv6::{IcmpCode, IcmpType}; use crate::ipv6::Ipv6Packet; // Real IPv6 example with a rfc4884 length of 10 (10 * 8 = 80 // octets). // // This example contain an MPLS extension stack which contains // two member (i.e. labels) #[test] fn test_ipv6() { let buf = hex_literal::hex!( " 03 00 be a8 0a 00 00 00 68 04 83 fe 00 2c 3a 01 24 00 61 80 00 00 00 d0 00 00 00 00 12 65 b0 01 24 04 68 00 40 03 0c 1c 00 00 00 00 00 00 00 8a 80 00 b2 e1 2a 60 80 f2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00 96 53 00 0c 01 01 06 9f 18 01 00 00 29 ff " ); let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::TimeExceeded, time_exceeded_packet.get_icmp_type()); assert_eq!(IcmpCode(0), time_exceeded_packet.get_icmp_code()); assert_eq!(48808, time_exceeded_packet.get_checksum()); assert_eq!(10, time_exceeded_packet.get_length()); assert_eq!(&buf[8..88], time_exceeded_packet.payload()); assert_eq!(Some(&buf[136..]), time_exceeded_packet.extension()); assert_eq!(80, time_exceeded_packet.payload().len()); assert_eq!(16, time_exceeded_packet.extension().unwrap().len()); let nested_ipv6 = Ipv6Packet::new_view(time_exceeded_packet.payload()).unwrap(); let nested_echo = EchoRequestPacket::new_view(nested_ipv6.payload()).unwrap(); assert_eq!(IcmpCode(0), nested_echo.get_icmp_code()); assert_eq!(IcmpType::EchoRequest, nested_echo.get_icmp_type()); assert_eq!(0xB2E1, nested_echo.get_checksum()); assert_eq!(10848, nested_echo.get_identifier()); assert_eq!(33010, nested_echo.get_sequence()); let extensions = ExtensionsPacket::new_view(time_exceeded_packet.extension().unwrap()).unwrap(); let extension_header = ExtensionHeaderPacket::new_view(extensions.header()).unwrap(); assert_eq!(2, extension_header.get_version()); assert_eq!(0x9653, extension_header.get_checksum()); let object_bytes = extensions.objects().next().unwrap(); let extension_object = ExtensionObjectPacket::new_view(object_bytes).unwrap(); assert_eq!(12, extension_object.get_length()); assert_eq!( ClassNum::MultiProtocolLabelSwitchingLabelStack, extension_object.get_class_num() ); assert_eq!(ClassSubType(1), extension_object.get_class_subtype()); assert_eq!( [0x06, 0x9f, 0x18, 0x01, 0x00, 0x00, 0x29, 0xff], extension_object.payload() ); let mpls_stack = MplsLabelStackPacket::new_view(extension_object.payload()).unwrap(); let mut mpls_stack_member_iter = mpls_stack.members(); // 1st stack member let mpls_stack_member_bytes = mpls_stack_member_iter.next().unwrap(); let mpls_stack_member = MplsLabelStackMemberPacket::new_view(mpls_stack_member_bytes).unwrap(); assert_eq!(27121, mpls_stack_member.get_label()); assert_eq!(4, mpls_stack_member.get_exp()); assert_eq!(0, mpls_stack_member.get_bos()); assert_eq!(1, mpls_stack_member.get_ttl()); // 2nd stack member let mpls_stack_member_bytes = mpls_stack_member_iter.next().unwrap(); let mpls_stack_member = MplsLabelStackMemberPacket::new_view(mpls_stack_member_bytes).unwrap(); assert_eq!(2, mpls_stack_member.get_label()); assert_eq!(4, mpls_stack_member.get_exp()); assert_eq!(1, mpls_stack_member.get_bos()); assert_eq!(255, mpls_stack_member.get_ttl()); assert!(mpls_stack_member_iter.next().is_none()); } // Real IPv6 example with a rfc4884 length of 16 (16 * 8 = 128 // octets for) but the total payload is only 84 octets and // therefore this is a malformed packet. // // For such packets Trippy assumes there are no extensions. #[test] fn test_ipv6_2() { let buf = hex_literal::hex!( " 03 00 5a b4 10 00 00 00 68 0e 0d 91 00 2c 3a 01 24 00 61 80 00 00 00 d0 00 00 00 00 12 65 b0 01 24 04 68 00 40 03 0c 05 00 00 00 00 00 00 00 71 80 00 a8 e7 34 88 80 f4 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ); let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap(); assert_eq!(84, time_exceeded_packet.payload().len()); assert_eq!(None, time_exceeded_packet.extension()); let nested_ipv6 = Ipv6Packet::new_view(time_exceeded_packet.payload()).unwrap(); let nested_echo = EchoRequestPacket::new_view(nested_ipv6.payload()).unwrap(); assert_eq!(IcmpCode(0), nested_echo.get_icmp_code()); assert_eq!(IcmpType::EchoRequest, nested_echo.get_icmp_type()); assert_eq!(0xA8E7, nested_echo.get_checksum()); assert_eq!(13448, nested_echo.get_identifier()); assert_eq!(33012, nested_echo.get_sequence()); } } } } ================================================ FILE: crates/trippy-packet/src/icmpv4.rs ================================================ use crate::buffer::Buffer; use crate::error::{Error, Result}; use std::fmt::{Debug, Formatter}; /// The type of ICMP packet. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub enum IcmpType { EchoRequest, EchoReply, DestinationUnreachable, TimeExceeded, Other(u8), } impl IcmpType { #[must_use] pub const fn id(&self) -> u8 { match self { Self::EchoRequest => 8, Self::EchoReply => 0, Self::DestinationUnreachable => 3, Self::TimeExceeded => 11, Self::Other(id) => *id, } } } impl From for IcmpType { fn from(val: u8) -> Self { match val { 8 => Self::EchoRequest, 0 => Self::EchoReply, 3 => Self::DestinationUnreachable, 11 => Self::TimeExceeded, id => Self::Other(id), } } } /// The ICMP code. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub struct IcmpCode(pub u8); impl From for IcmpCode { fn from(val: u8) -> Self { Self(val) } } /// The code for `TimeExceeded` ICMP packet type. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub enum IcmpTimeExceededCode { /// TTL expired in transit. TtlExpired, /// Fragment reassembly time exceeded. FragmentReassembly, /// An unknown code. Unknown(u8), } impl From for IcmpTimeExceededCode { fn from(val: IcmpCode) -> Self { match val { IcmpCode(0) => Self::TtlExpired, IcmpCode(1) => Self::FragmentReassembly, IcmpCode(id) => Self::Unknown(id), } } } const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; /// Represents an ICMP packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor methods /// take and return data in host byte order, converting as necessary for the given architecture. pub struct IcmpPacket<'a> { buf: Buffer<'a>, } impl<'a> IcmpPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("IcmpPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("IcmpPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } } impl Debug for IcmpPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("IcmpPacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; IcmpPacket::minimum_packet_size()]; let mut packet = IcmpPacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x08], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x00], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x0B], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; IcmpPacket::minimum_packet_size()]; let mut packet = IcmpPacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; IcmpPacket::minimum_packet_size()]; let mut packet = IcmpPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = IcmpPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = IcmpPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("IcmpPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = IcmpPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = IcmpPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("IcmpPacket"), SIZE, SIZE - 1), err ); } } pub mod echo_request { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use crate::icmpv4::{IcmpCode, IcmpType}; use std::fmt::{Debug, Formatter}; const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; const IDENTIFIER_OFFSET: usize = 4; const SEQUENCE_OFFSET: usize = 6; /// Represents an ICMP `EchoRequest` packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct EchoRequestPacket<'a> { buf: Buffer<'a>, } impl<'a> EchoRequestPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("EchoRequestPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("EchoRequestPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_identifier(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(IDENTIFIER_OFFSET)) } #[must_use] pub fn get_sequence(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET)) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_identifier(&mut self, val: u16) { self.buf.set_bytes(IDENTIFIER_OFFSET, val.to_be_bytes()); } pub fn set_sequence(&mut self, val: u16) { self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes()); } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()] .copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..] } } impl Debug for EchoRequestPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("EchoRequestPacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .field("identifier", &self.get_identifier()) .field("sequence", &self.get_sequence()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x08], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x00], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x0B], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_identifier() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_identifier(0); assert_eq!(0, packet.get_identifier()); assert_eq!([0x00, 0x00], packet.packet()[4..=5]); packet.set_identifier(1999); assert_eq!(1999, packet.get_identifier()); assert_eq!([0x07, 0xCF], packet.packet()[4..=5]); packet.set_identifier(u16::MAX); assert_eq!(u16::MAX, packet.get_identifier()); assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]); } #[test] fn test_sequence() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_sequence(0); assert_eq!(0, packet.get_sequence()); assert_eq!([0x00, 0x00], packet.packet()[6..=7]); packet.set_sequence(1999); assert_eq!(1999, packet.get_sequence()); assert_eq!([0x07, 0xCF], packet.packet()[6..=7]); packet.set_sequence(u16::MAX); assert_eq!(u16::MAX, packet.get_sequence()); assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]); } #[test] fn test_view() { let buf = [0x08, 0x00, 0x16, 0x7c, 0x60, 0x9b, 0x82, 0x9a]; let packet = EchoRequestPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!(5756, packet.get_checksum()); assert_eq!(24731, packet.get_identifier()); assert_eq!(33434, packet.get_sequence()); assert!(packet.payload().is_empty()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = EchoRequestPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = EchoRequestPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("EchoRequestPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = EchoRequestPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = EchoRequestPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("EchoRequestPacket"), SIZE, SIZE - 1), err ); } } } pub mod echo_reply { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use crate::icmpv4::{IcmpCode, IcmpType}; use std::fmt::{Debug, Formatter}; const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; const IDENTIFIER_OFFSET: usize = 4; const SEQUENCE_OFFSET: usize = 6; /// Represents an ICMP `EchoReply` packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct EchoReplyPacket<'a> { buf: Buffer<'a>, } impl<'a> EchoReplyPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("EchoReplyPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("EchoReplyPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_identifier(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(IDENTIFIER_OFFSET)) } #[must_use] pub fn get_sequence(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET)) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_identifier(&mut self, val: u16) { self.buf.set_bytes(IDENTIFIER_OFFSET, val.to_be_bytes()); } pub fn set_sequence(&mut self, val: u16) { self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes()); } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()] .copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..] } } impl Debug for EchoReplyPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("EchoReplyPacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .field("identifier", &self.get_identifier()) .field("sequence", &self.get_sequence()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x08], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x00], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x0B], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_identifier() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_identifier(0); assert_eq!(0, packet.get_identifier()); assert_eq!([0x00, 0x00], packet.packet()[4..=5]); packet.set_identifier(1999); assert_eq!(1999, packet.get_identifier()); assert_eq!([0x07, 0xCF], packet.packet()[4..=5]); packet.set_identifier(u16::MAX); assert_eq!(u16::MAX, packet.get_identifier()); assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]); } #[test] fn test_sequence() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_sequence(0); assert_eq!(0, packet.get_sequence()); assert_eq!([0x00, 0x00], packet.packet()[6..=7]); packet.set_sequence(1999); assert_eq!(1999, packet.get_sequence()); assert_eq!([0x07, 0xCF], packet.packet()[6..=7]); packet.set_sequence(u16::MAX); assert_eq!(u16::MAX, packet.get_sequence()); assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]); } #[test] fn test_view() { let buf = [0x00, 0x00, 0x1e, 0x70, 0x60, 0x9b, 0x80, 0xf4]; let packet = EchoReplyPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!(7792, packet.get_checksum()); assert_eq!(24731, packet.get_identifier()); assert_eq!(33012, packet.get_sequence()); assert!(packet.payload().is_empty()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = EchoReplyPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = EchoReplyPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("EchoReplyPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = EchoReplyPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = EchoReplyPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("EchoReplyPacket"), SIZE, SIZE - 1), err ); } } } pub mod time_exceeded { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use crate::icmp_extension::extension_splitter::split; use crate::icmpv4::{IcmpCode, IcmpType}; use std::fmt::{Debug, Formatter}; const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; const LENGTH_OFFSET: usize = 5; /// Represents an ICMP `TimeExceeded` packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct TimeExceededPacket<'a> { buf: Buffer<'a>, } impl<'a> TimeExceededPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("TimeExceededPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("TimeExceededPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_length(&self) -> u8 { self.buf.read(LENGTH_OFFSET) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_length(&mut self, val: u8) { *self.buf.write(LENGTH_OFFSET) = val; } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()] .copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { let (payload, _) = self.split_payload_extension(); payload } #[must_use] pub fn payload_raw(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..] } #[must_use] pub fn extension(&self) -> Option<&[u8]> { let (_, extension) = self.split_payload_extension(); extension } fn split_payload_extension(&self) -> (&[u8], Option<&[u8]>) { // From rfc4884: // // "For ICMPv4 messages, the length attribute represents 32-bit words let length = usize::from(self.get_length()) * 4; let icmp_payload = &self.buf.as_slice()[Self::minimum_packet_size()..]; split(length, icmp_payload) } } impl Debug for TimeExceededPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("TimeExceededPacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .field("length", &self.get_length()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()]; let mut packet = TimeExceededPacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x08], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x00], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x0B], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()]; let mut packet = TimeExceededPacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()]; let mut packet = TimeExceededPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_length() { let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()]; let mut packet = TimeExceededPacket::new(&mut buf).unwrap(); packet.set_length(0); assert_eq!(0, packet.get_length()); assert_eq!([0x00], packet.packet()[5..6]); packet.set_length(8); assert_eq!(8, packet.get_length()); assert_eq!([0x08], packet.packet()[5..6]); packet.set_length(u8::MAX); assert_eq!(u8::MAX, packet.get_length()); assert_eq!([0xFF], packet.packet()[5..6]); } #[test] fn test_view() { let buf = [0x0b, 0x00, 0xf4, 0xee, 0x00, 0x11, 0x00, 0x00]; let packet = TimeExceededPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!(62702, packet.get_checksum()); assert_eq!(17, packet.get_length()); assert!(packet.payload().is_empty()); } #[test] fn test_view_large() { let mut buf = [0x0_u8; 256]; buf[..8].copy_from_slice(&[0x0b, 0x00, 0xf4, 0xee, 0x00, 0x40, 0x00, 0x00]); let packet = TimeExceededPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!(62702, packet.get_checksum()); assert_eq!(64, packet.get_length()); assert_eq!(&[0x0_u8; 248], packet.payload()); assert_eq!(None, packet.extension()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = TimeExceededPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = TimeExceededPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("TimeExceededPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = TimeExceededPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = TimeExceededPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("TimeExceededPacket"), SIZE, SIZE - 1), err ); } } } pub mod destination_unreachable { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use crate::icmp_extension::extension_splitter::split; use crate::icmpv4::{IcmpCode, IcmpType}; use std::fmt::{Debug, Formatter}; const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; const LENGTH_OFFSET: usize = 5; const NEXT_HOP_MTU_OFFSET: usize = 6; /// Represents an ICMP `DestinationUnreachable` packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct DestinationUnreachablePacket<'a> { buf: Buffer<'a>, } impl<'a> DestinationUnreachablePacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("DestinationUnreachablePacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("DestinationUnreachablePacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_length(&self) -> u8 { self.buf.read(LENGTH_OFFSET) } #[must_use] pub fn get_next_hop_mtu(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(NEXT_HOP_MTU_OFFSET)) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_length(&mut self, val: u8) { *self.buf.write(LENGTH_OFFSET) = val; } pub fn set_next_hop_mtu(&mut self, val: u16) { self.buf.set_bytes(NEXT_HOP_MTU_OFFSET, val.to_be_bytes()); } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()] .copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { let (payload, _) = self.split_payload_extension(); payload } #[must_use] pub fn payload_raw(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..] } #[must_use] pub fn extension(&self) -> Option<&[u8]> { let (_, extension) = self.split_payload_extension(); extension } fn split_payload_extension(&self) -> (&[u8], Option<&[u8]>) { let length = usize::from(self.get_length()) * 4; let icmp_payload = &self.buf.as_slice()[Self::minimum_packet_size()..]; split(length, icmp_payload) } } impl Debug for DestinationUnreachablePacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("DestinationUnreachablePacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .field("length", &self.get_length()) .field("next_hop_mtu", &self.get_next_hop_mtu()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()]; let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x08], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x00], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x0B], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()]; let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()]; let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_length() { let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()]; let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap(); packet.set_length(0); assert_eq!(0, packet.get_length()); assert_eq!([0x00], packet.packet()[5..6]); packet.set_length(8); assert_eq!(8, packet.get_length()); assert_eq!([0x08], packet.packet()[5..6]); packet.set_length(u8::MAX); assert_eq!(u8::MAX, packet.get_length()); assert_eq!([0xFF], packet.packet()[5..6]); } #[test] fn test_view() { let buf = [0x03, 0x03, 0xdf, 0xdc, 0x00, 0x00, 0x00, 0x00]; let packet = DestinationUnreachablePacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!(IcmpCode(3), packet.get_icmp_code()); assert_eq!(57308, packet.get_checksum()); assert_eq!(0, packet.get_length()); assert!(packet.payload().is_empty()); } #[test] fn test_view_large() { let mut buf = [0x0_u8; 256]; buf[..8].copy_from_slice(&[0x03, 0x03, 0xdf, 0xdc, 0x00, 0x40, 0x00, 0x00]); let packet = DestinationUnreachablePacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!(IcmpCode(3), packet.get_icmp_code()); assert_eq!(57308, packet.get_checksum()); assert_eq!(64, packet.get_length()); assert_eq!(&[0x0_u8; 248], packet.payload()); assert_eq!(None, packet.extension()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = DestinationUnreachablePacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = DestinationUnreachablePacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer( String::from("DestinationUnreachablePacket"), SIZE, SIZE - 1 ), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = DestinationUnreachablePacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = DestinationUnreachablePacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer( String::from("DestinationUnreachablePacket"), SIZE, SIZE - 1 ), err ); } } } ================================================ FILE: crates/trippy-packet/src/icmpv6.rs ================================================ use crate::buffer::Buffer; use crate::error::{Error, Result}; use std::fmt::{Debug, Formatter}; /// The type of `ICMPv6` packet. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub enum IcmpType { EchoRequest, EchoReply, DestinationUnreachable, TimeExceeded, Other(u8), } impl IcmpType { #[must_use] pub const fn id(&self) -> u8 { match self { Self::EchoRequest => 128, Self::EchoReply => 129, Self::DestinationUnreachable => 1, Self::TimeExceeded => 3, Self::Other(id) => *id, } } } impl From for IcmpType { fn from(val: u8) -> Self { match val { 128 => Self::EchoRequest, 129 => Self::EchoReply, 1 => Self::DestinationUnreachable, 3 => Self::TimeExceeded, id => Self::Other(id), } } } /// The `ICMPv6` code. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub struct IcmpCode(pub u8); impl From for IcmpCode { fn from(val: u8) -> Self { Self(val) } } /// The code for `TimeExceeded` `ICMPv6` packet type. #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub enum IcmpTimeExceededCode { /// Hop limit exceeded in transit. TtlExpired, /// Fragment reassembly time exceeded. FragmentReassembly, /// An unknown code. Unknown(u8), } impl From for IcmpTimeExceededCode { fn from(val: IcmpCode) -> Self { match val { IcmpCode(0) => Self::TtlExpired, IcmpCode(1) => Self::FragmentReassembly, IcmpCode(id) => Self::Unknown(id), } } } const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; /// Represents an ICMP packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor methods /// take and return data in host byte order, converting as necessary for the given architecture. pub struct IcmpPacket<'a> { buf: Buffer<'a>, } impl<'a> IcmpPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("IcmpPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("IcmpPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } } impl Debug for IcmpPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("IcmpPacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; IcmpPacket::minimum_packet_size()]; let mut packet = IcmpPacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x80], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x81], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x01], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; IcmpPacket::minimum_packet_size()]; let mut packet = IcmpPacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; IcmpPacket::minimum_packet_size()]; let mut packet = IcmpPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = IcmpPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = IcmpPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("IcmpPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = IcmpPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = IcmpPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("IcmpPacket"), SIZE, SIZE - 1), err ); } } pub mod echo_request { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use crate::icmpv6::{IcmpCode, IcmpType}; use std::fmt::{Debug, Formatter}; const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; const IDENTIFIER_OFFSET: usize = 4; const SEQUENCE_OFFSET: usize = 6; /// Represents an `ICMPv6` `EchoRequest` packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct EchoRequestPacket<'a> { buf: Buffer<'a>, } impl<'a> EchoRequestPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("EchoRequestPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("EchoRequestPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_identifier(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(IDENTIFIER_OFFSET)) } #[must_use] pub fn get_sequence(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET)) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_identifier(&mut self, val: u16) { self.buf.set_bytes(IDENTIFIER_OFFSET, val.to_be_bytes()); } pub fn set_sequence(&mut self, val: u16) { self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes()); } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()] .copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..] } } impl Debug for EchoRequestPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("EchoRequestPacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .field("identifier", &self.get_identifier()) .field("sequence", &self.get_sequence()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x80], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x81], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x01], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_identifier() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_identifier(0); assert_eq!(0, packet.get_identifier()); assert_eq!([0x00, 0x00], packet.packet()[4..=5]); packet.set_identifier(1999); assert_eq!(1999, packet.get_identifier()); assert_eq!([0x07, 0xCF], packet.packet()[4..=5]); packet.set_identifier(u16::MAX); assert_eq!(u16::MAX, packet.get_identifier()); assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]); } #[test] fn test_sequence() { let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()]; let mut packet = EchoRequestPacket::new(&mut buf).unwrap(); packet.set_sequence(0); assert_eq!(0, packet.get_sequence()); assert_eq!([0x00, 0x00], packet.packet()[6..=7]); packet.set_sequence(1999); assert_eq!(1999, packet.get_sequence()); assert_eq!([0x07, 0xCF], packet.packet()[6..=7]); packet.set_sequence(u16::MAX); assert_eq!(u16::MAX, packet.get_sequence()); assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]); } #[test] fn test_view() { let buf = [0x80, 0x00, 0x16, 0x7c, 0x60, 0x9b, 0x82, 0x9a]; let packet = EchoRequestPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!(5756, packet.get_checksum()); assert_eq!(24731, packet.get_identifier()); assert_eq!(33434, packet.get_sequence()); assert!(packet.payload().is_empty()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = EchoRequestPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = EchoRequestPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("EchoRequestPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = EchoRequestPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = EchoRequestPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("EchoRequestPacket"), SIZE, SIZE - 1), err ); } } } pub mod echo_reply { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use crate::icmpv6::{IcmpCode, IcmpType}; use std::fmt::{Debug, Formatter}; const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; const IDENTIFIER_OFFSET: usize = 4; const SEQUENCE_OFFSET: usize = 6; /// Represents an ICMP `EchoReply` packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct EchoReplyPacket<'a> { buf: Buffer<'a>, } impl<'a> EchoReplyPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("EchoReplyPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("EchoReplyPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_identifier(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(IDENTIFIER_OFFSET)) } #[must_use] pub fn get_sequence(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET)) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_identifier(&mut self, val: u16) { self.buf.set_bytes(IDENTIFIER_OFFSET, val.to_be_bytes()); } pub fn set_sequence(&mut self, val: u16) { self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes()); } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()] .copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..] } } impl Debug for EchoReplyPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("EchoReplyPacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .field("identifier", &self.get_identifier()) .field("sequence", &self.get_sequence()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x80], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x81], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x01], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_identifier() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_identifier(0); assert_eq!(0, packet.get_identifier()); assert_eq!([0x00, 0x00], packet.packet()[4..=5]); packet.set_identifier(1999); assert_eq!(1999, packet.get_identifier()); assert_eq!([0x07, 0xCF], packet.packet()[4..=5]); packet.set_identifier(u16::MAX); assert_eq!(u16::MAX, packet.get_identifier()); assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]); } #[test] fn test_sequence() { let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()]; let mut packet = EchoReplyPacket::new(&mut buf).unwrap(); packet.set_sequence(0); assert_eq!(0, packet.get_sequence()); assert_eq!([0x00, 0x00], packet.packet()[6..=7]); packet.set_sequence(1999); assert_eq!(1999, packet.get_sequence()); assert_eq!([0x07, 0xCF], packet.packet()[6..=7]); packet.set_sequence(u16::MAX); assert_eq!(u16::MAX, packet.get_sequence()); assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]); } #[test] fn test_view() { let buf = [0x81, 0x00, 0x1e, 0x70, 0x60, 0x9b, 0x80, 0xf4]; let packet = EchoReplyPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!(7792, packet.get_checksum()); assert_eq!(24731, packet.get_identifier()); assert_eq!(33012, packet.get_sequence()); assert!(packet.payload().is_empty()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = EchoReplyPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = EchoReplyPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("EchoReplyPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = EchoReplyPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = EchoReplyPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("EchoReplyPacket"), SIZE, SIZE - 1), err ); } } } pub mod time_exceeded { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use crate::icmp_extension::extension_splitter::split; use crate::icmpv6::{IcmpCode, IcmpType}; use std::fmt::{Debug, Formatter}; const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; const LENGTH_OFFSET: usize = 4; /// Represents an ICMP `TimeExceeded` packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct TimeExceededPacket<'a> { buf: Buffer<'a>, } impl<'a> TimeExceededPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("TimeExceededPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("TimeExceededPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_length(&self) -> u8 { self.buf.read(LENGTH_OFFSET) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_length(&mut self, val: u8) { *self.buf.write(LENGTH_OFFSET) = val; } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()] .copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { let (payload, _) = self.split_payload_extension(); payload } #[must_use] pub fn payload_raw(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..] } #[must_use] pub fn extension(&self) -> Option<&[u8]> { let (_, extension) = self.split_payload_extension(); extension } fn split_payload_extension(&self) -> (&[u8], Option<&[u8]>) { let length = usize::from(self.get_length()) * 8; let icmp_payload = &self.buf.as_slice()[Self::minimum_packet_size()..]; split(length, icmp_payload) } } impl Debug for TimeExceededPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("TimeExceededPacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .field("length", &self.get_length()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()]; let mut packet = TimeExceededPacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x80], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x81], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x01], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()]; let mut packet = TimeExceededPacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()]; let mut packet = TimeExceededPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_length() { let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()]; let mut packet = TimeExceededPacket::new(&mut buf).unwrap(); packet.set_length(0); assert_eq!(0, packet.get_length()); assert_eq!([0x00], packet.packet()[4..5]); packet.set_length(8); assert_eq!(8, packet.get_length()); assert_eq!([0x08], packet.packet()[4..5]); packet.set_length(u8::MAX); assert_eq!(u8::MAX, packet.get_length()); assert_eq!([0xFF], packet.packet()[4..5]); } #[test] fn test_view() { let buf = [0x03, 0x00, 0xf4, 0xee, 0x11, 0x00, 0x00, 0x00]; let packet = TimeExceededPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!(62702, packet.get_checksum()); assert_eq!(17, packet.get_length()); assert!(packet.payload().is_empty()); } #[test] fn test_view_large() { let mut buf = [0x0_u8; 128]; buf[..8].copy_from_slice(&[0x03, 0x00, 0xf4, 0xee, 0x20, 0x00, 0x00, 0x00]); let packet = TimeExceededPacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!(62702, packet.get_checksum()); assert_eq!(32, packet.get_length()); assert_eq!(&[0x0_u8; 120], packet.payload()); assert_eq!(None, packet.extension()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = TimeExceededPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = TimeExceededPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("TimeExceededPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = TimeExceededPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = TimeExceededPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("TimeExceededPacket"), SIZE, SIZE - 1), err ); } } } pub mod destination_unreachable { use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use crate::icmp_extension::extension_splitter::split; use crate::icmpv6::{IcmpCode, IcmpType}; use std::fmt::{Debug, Formatter}; const TYPE_OFFSET: usize = 0; const CODE_OFFSET: usize = 1; const CHECKSUM_OFFSET: usize = 2; const LENGTH_OFFSET: usize = 4; const NEXT_HOP_MTU_OFFSET: usize = 6; /// Represents an ICMP `DestinationUnreachable` packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor /// methods take and return data in host byte order, converting as necessary for the given /// architecture. pub struct DestinationUnreachablePacket<'a> { buf: Buffer<'a>, } impl<'a> DestinationUnreachablePacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("DestinationUnreachablePacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("DestinationUnreachablePacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_icmp_type(&self) -> IcmpType { IcmpType::from(self.buf.read(TYPE_OFFSET)) } #[must_use] pub fn get_icmp_code(&self) -> IcmpCode { IcmpCode::from(self.buf.read(CODE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_length(&self) -> u8 { self.buf.read(LENGTH_OFFSET) } #[must_use] pub fn get_next_hop_mtu(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(NEXT_HOP_MTU_OFFSET)) } pub fn set_icmp_type(&mut self, val: IcmpType) { *self.buf.write(TYPE_OFFSET) = val.id(); } pub fn set_icmp_code(&mut self, val: IcmpCode) { *self.buf.write(CODE_OFFSET) = val.0; } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_length(&mut self, val: u8) { *self.buf.write(LENGTH_OFFSET) = val; } pub fn set_next_hop_mtu(&mut self, val: u16) { self.buf.set_bytes(NEXT_HOP_MTU_OFFSET, val.to_be_bytes()); } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()] .copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { let (payload, _) = self.split_payload_extension(); payload } #[must_use] pub fn payload_raw(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..] } #[must_use] pub fn extension(&self) -> Option<&[u8]> { let (_, extension) = self.split_payload_extension(); extension } fn split_payload_extension(&self) -> (&[u8], Option<&[u8]>) { // From rfc4884: // // "For ICMPv6 messages, the length attribute represents 64-bit words" let length = usize::from(self.get_length()) * 8; let icmp_payload = &self.buf.as_slice()[Self::minimum_packet_size()..]; split(length, icmp_payload) } } impl Debug for DestinationUnreachablePacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("DestinationUnreachablePacket") .field("icmp_type", &self.get_icmp_type()) .field("icmp_code", &self.get_icmp_code()) .field("checksum", &self.get_checksum()) .field("length", &self.get_length()) .field("next_hop_mtu", &self.get_next_hop_mtu()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_icmp_type() { let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()]; let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap(); packet.set_icmp_type(IcmpType::EchoRequest); assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type()); assert_eq!([0x80], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::EchoReply); assert_eq!(IcmpType::EchoReply, packet.get_icmp_type()); assert_eq!([0x81], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::DestinationUnreachable); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!([0x01], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::TimeExceeded); assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type()); assert_eq!([0x03], packet.packet()[0..1]); packet.set_icmp_type(IcmpType::Other(255)); assert_eq!(IcmpType::Other(255), packet.get_icmp_type()); assert_eq!([0xFF], packet.packet()[0..1]); } #[test] fn test_icmp_code() { let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()]; let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap(); packet.set_icmp_code(IcmpCode(0)); assert_eq!(IcmpCode(0), packet.get_icmp_code()); assert_eq!([0x00], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(5)); assert_eq!(IcmpCode(5), packet.get_icmp_code()); assert_eq!([0x05], packet.packet()[1..2]); packet.set_icmp_code(IcmpCode(255)); assert_eq!(IcmpCode(255), packet.get_icmp_code()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_checksum() { let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()]; let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_checksum(1999); assert_eq!(1999, packet.get_checksum()); assert_eq!([0x07, 0xCF], packet.packet()[2..=3]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_length() { let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()]; let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap(); packet.set_length(0); assert_eq!(0, packet.get_length()); assert_eq!([0x00], packet.packet()[4..5]); packet.set_length(8); assert_eq!(8, packet.get_length()); assert_eq!([0x08], packet.packet()[4..5]); packet.set_length(u8::MAX); assert_eq!(u8::MAX, packet.get_length()); assert_eq!([0xFF], packet.packet()[4..5]); } #[test] fn test_view() { let buf = [0x01, 0x03, 0xdf, 0xdc, 0x00, 0x00, 0x00, 0x00]; let packet = DestinationUnreachablePacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!(IcmpCode(3), packet.get_icmp_code()); assert_eq!(57308, packet.get_checksum()); assert_eq!(0, packet.get_length()); assert!(packet.payload().is_empty()); } #[test] fn test_view_large() { let mut buf = [0x0_u8; 128]; buf[..8].copy_from_slice(&[0x01, 0x03, 0xdf, 0xdc, 0x20, 0x00, 0x00, 0x00]); let packet = DestinationUnreachablePacket::new_view(&buf).unwrap(); assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type()); assert_eq!(IcmpCode(3), packet.get_icmp_code()); assert_eq!(57308, packet.get_checksum()); assert_eq!(32, packet.get_length()); assert_eq!(&[0x0_u8; 120], packet.payload()); assert_eq!(None, packet.extension()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = DestinationUnreachablePacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = DestinationUnreachablePacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer( String::from("DestinationUnreachablePacket"), SIZE, SIZE - 1 ), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = DestinationUnreachablePacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = DestinationUnreachablePacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer( String::from("DestinationUnreachablePacket"), SIZE, SIZE - 1 ), err ); } } } ================================================ FILE: crates/trippy-packet/src/ip.rs ================================================ use crate::buffer::Buffer; use crate::error::{Error, Result}; use std::fmt::{Debug, Formatter}; /// The IP packet version. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum IpVersion { Ipv4, Ipv6, Other(u8), } impl IpVersion { #[must_use] pub const fn id(self) -> u8 { match self { Self::Ipv4 => 4, Self::Ipv6 => 6, Self::Other(id) => id, } } #[must_use] pub const fn new(value: u8) -> Self { Self::Other(value) } } impl From for IpVersion { fn from(id: u8) -> Self { match id { 4 => Self::Ipv4, 6 => Self::Ipv6, p => Self::Other(p), } } } const VERSION_OFFSET: usize = 0; /// Represents a generic IP packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor methods /// take and return data in host byte order, converting as necessary for the given architecture. pub struct IpPacket<'a> { buf: Buffer<'a>, } impl<'a> IpPacket<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("IpPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("IpPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 20 } #[must_use] pub fn get_version(&self) -> IpVersion { IpVersion::from((self.buf.read(VERSION_OFFSET) & 0xF0) >> 4) } pub fn set_version(&mut self, val: IpVersion) { *self.buf.write(VERSION_OFFSET) = (self.buf.read(VERSION_OFFSET) & 0x0F) | ((val.id() & 0x0F) << 4); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } } impl Debug for IpPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("IpPacket") .field("version", &self.get_version()) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_version() { let mut buf = [0_u8; IpPacket::minimum_packet_size()]; let mut packet = IpPacket::new(&mut buf).unwrap(); packet.set_version(IpVersion::Ipv4); assert_eq!(IpVersion::Ipv4, packet.get_version()); assert_eq!([0x40], packet.packet()[..1]); packet.set_version(IpVersion::Ipv6); assert_eq!(IpVersion::Ipv6, packet.get_version()); assert_eq!([0x60], packet.packet()[..1]); packet.set_version(IpVersion::Other(15)); assert_eq!(IpVersion::Other(15), packet.get_version()); assert_eq!([0xF0], packet.packet()[..1]); } #[test] fn test_view_ipv4_packet() { let buf = hex_literal::hex!( " 45 00 00 54 a2 71 00 00 15 11 9a ee 7f 00 00 01 de 9a 56 12 " ); let packet = IpPacket::new_view(&buf).unwrap(); assert_eq!(IpVersion::Ipv4, packet.get_version()); } #[test] fn test_view_ipv6_packet() { let buf = hex_literal::hex!( " 60 06 05 00 00 20 06 40 fe 80 00 00 00 00 00 00 1c 8d 7d 69 d0 b6 81 82 fe 80 00 00 00 00 00 00 08 11 03 f6 76 01 6c 3f " ); let packet = IpPacket::new_view(&buf).unwrap(); assert_eq!(IpVersion::Ipv6, packet.get_version()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = IpPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = IpPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("IpPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = IpPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = IpPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("IpPacket"), SIZE, SIZE - 1), err ); } } ================================================ FILE: crates/trippy-packet/src/ipv4.rs ================================================ use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::{IpProtocol, fmt_payload}; use std::fmt::{Debug, Formatter}; use std::net::Ipv4Addr; const VERSION_OFFSET: usize = 0; const IHL_OFFSET: usize = 0; const DSCP_OFFSET: usize = 1; const ECN_OFFSET: usize = 1; const TOTAL_LENGTH_OFFSET: usize = 2; const IDENTIFICATION_OFFSET: usize = 4; const FLAGS_AND_FRAGMENT_OFFSET_OFFSET: usize = 6; const TIME_TO_LIVE_OFFSET: usize = 8; const PROTOCOL_OFFSET: usize = 9; const CHECKSUM_OFFSET: usize = 10; const SOURCE_OFFSET: usize = 12; const DESTINATION_OFFSET: usize = 16; /// Represents an IPv4 Packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor methods /// take and return data in host byte order, converting as necessary for the given architecture. pub struct Ipv4Packet<'a> { buf: Buffer<'a>, } impl<'a> Ipv4Packet<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("Ipv4Packet"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("Ipv4Packet"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 20 } #[must_use] pub fn get_version(&self) -> u8 { (self.buf.read(VERSION_OFFSET) & 0xf0) >> 4 } #[must_use] pub fn get_header_length(&self) -> u8 { self.buf.read(IHL_OFFSET) & 0xf } #[must_use] pub fn get_dscp(&self) -> u8 { (self.buf.read(DSCP_OFFSET) & 0xfc) >> 2 } #[must_use] pub fn get_ecn(&self) -> u8 { self.buf.read(ECN_OFFSET) & 0x3 } #[must_use] pub fn get_tos(&self) -> u8 { (self.get_dscp() << 2) | self.get_ecn() } #[must_use] pub fn get_total_length(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(TOTAL_LENGTH_OFFSET)) } #[must_use] pub fn get_identification(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(IDENTIFICATION_OFFSET)) } #[must_use] pub fn get_flags_and_fragment_offset(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(FLAGS_AND_FRAGMENT_OFFSET_OFFSET)) } #[must_use] pub fn get_ttl(&self) -> u8 { self.buf.read(TIME_TO_LIVE_OFFSET) } #[must_use] pub fn get_protocol(&self) -> IpProtocol { IpProtocol::from(self.buf.read(PROTOCOL_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_source(&self) -> Ipv4Addr { Ipv4Addr::from(self.buf.get_bytes(SOURCE_OFFSET)) } #[must_use] pub fn get_destination(&self) -> Ipv4Addr { Ipv4Addr::from(self.buf.get_bytes(DESTINATION_OFFSET)) } #[must_use] pub fn get_options_raw(&self) -> &[u8] { let current_offset = Self::minimum_packet_size(); let end = std::cmp::min( current_offset + ipv4_options_length(self), self.buf.as_slice().len(), ); &self.buf.as_slice()[current_offset..end] } pub fn set_version(&mut self, val: u8) { *self.buf.write(VERSION_OFFSET) = (self.buf.read(VERSION_OFFSET) & 0xf) | ((val & 0xf) << 4); } pub fn set_header_length(&mut self, val: u8) { *self.buf.write(IHL_OFFSET) = (self.buf.read(IHL_OFFSET) & 0xf0) | (val & 0xf); } pub fn set_dscp(&mut self, val: u8) { *self.buf.write(DSCP_OFFSET) = (self.buf.read(DSCP_OFFSET) & 0x3) | ((val & 0x3f) << 2); } pub fn set_ecn(&mut self, val: u8) { *self.buf.write(ECN_OFFSET) = (self.buf.read(ECN_OFFSET) & 0xfc) | (val & 0x3); } pub fn set_tos(&mut self, val: u8) { self.set_dscp((val & 0xfc) >> 2); self.set_ecn(val & 0x3); } pub fn set_total_length(&mut self, val: u16) { self.buf.set_bytes(TOTAL_LENGTH_OFFSET, val.to_be_bytes()); } pub fn set_identification(&mut self, val: u16) { self.buf.set_bytes(IDENTIFICATION_OFFSET, val.to_be_bytes()); } pub fn set_flags_and_fragment_offset(&mut self, val: u16) { self.buf .set_bytes(FLAGS_AND_FRAGMENT_OFFSET_OFFSET, val.to_be_bytes()); } pub fn set_ttl(&mut self, val: u8) { *self.buf.write(TIME_TO_LIVE_OFFSET) = val; } pub fn set_protocol(&mut self, val: IpProtocol) { *self.buf.write(PROTOCOL_OFFSET) = val.id(); } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_source(&mut self, val: Ipv4Addr) { self.buf.set_bytes(SOURCE_OFFSET, val.octets()); } pub fn set_destination(&mut self, val: Ipv4Addr) { self.buf.set_bytes(DESTINATION_OFFSET, val.octets()); } pub fn get_options_raw_mut(&mut self) -> &mut [u8] { use std::cmp::min; let current_offset = Self::minimum_packet_size(); let end = min( current_offset + ipv4_options_length(self), self.buf.as_slice().len(), ); &mut self.buf.as_slice_mut()[current_offset..end] } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size() + ipv4_options_length(self); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()].copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { let start = Ipv4Packet::minimum_packet_size() + ipv4_options_length(self); &self.buf.as_slice()[start..] } } fn ipv4_options_length(ipv4: &Ipv4Packet<'_>) -> usize { (ipv4.get_header_length() as usize * 4).saturating_sub(Ipv4Packet::minimum_packet_size()) } impl Debug for Ipv4Packet<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Ipv4Packet") .field("version", &self.get_version()) .field("header_length", &self.get_header_length()) .field("dscp", &self.get_dscp()) .field("ecn", &self.get_ecn()) .field("total_length", &self.get_total_length()) .field("identification", &self.get_identification()) .field( "flags_and_fragment_offset", &self.get_flags_and_fragment_offset(), ) .field("ttl", &self.get_ttl()) .field("protocol", &self.get_protocol()) .field("checksum", &self.get_checksum()) .field("source", &self.get_source()) .field("destination", &self.get_destination()) .field("options_raw", &self.get_options_raw()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_version() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_version(4); assert_eq!(4, packet.get_version()); assert_eq!([0x40], packet.packet()[..1]); packet.set_version(15); assert_eq!(15, packet.get_version()); assert_eq!([0xF0], packet.packet()[..1]); } #[test] fn test_header_length() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_header_length(5); assert_eq!(5, packet.get_header_length()); assert_eq!([0x05], packet.packet()[..1]); packet.set_header_length(15); assert_eq!(15, packet.get_header_length()); assert_eq!([0x0F], packet.packet()[..1]); } #[test] fn test_version_and_header_length() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_version(4); packet.set_header_length(5); assert_eq!(4, packet.get_version()); assert_eq!(5, packet.get_header_length()); assert_eq!([0x45], packet.packet()[..1]); packet.set_version(15); packet.set_header_length(15); assert_eq!(15, packet.get_version()); assert_eq!(15, packet.get_header_length()); assert_eq!([0xFF], packet.packet()[..1]); } #[test] fn test_dscp() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_dscp(63); assert_eq!(63, packet.get_dscp()); assert_eq!([0xFC], packet.packet()[1..2]); } #[test] fn test_ecn() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_ecn(3); assert_eq!(3, packet.get_ecn()); assert_eq!([0x03], packet.packet()[1..2]); } #[test] fn test_dscp_and_ecn() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_dscp(63); packet.set_ecn(3); assert_eq!(63, packet.get_dscp()); assert_eq!(3, packet.get_ecn()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_tos() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_tos(224); assert_eq!(224, packet.get_tos()); assert_eq!(56, packet.get_dscp()); assert_eq!(0, packet.get_ecn()); assert_eq!([0xE0], packet.packet()[1..2]); packet.set_tos(255); assert_eq!(255, packet.get_tos()); assert_eq!(63, packet.get_dscp()); assert_eq!(3, packet.get_ecn()); assert_eq!([0xFF], packet.packet()[1..2]); } #[test] fn test_total_length() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_total_length(84); assert_eq!(84, packet.get_total_length()); assert_eq!([0x00, 0x54], packet.packet()[2..=3]); packet.set_total_length(65535); assert_eq!(65535, packet.get_total_length()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_identification() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_identification(32); assert_eq!(32, packet.get_identification()); assert_eq!([0x00, 0x20], packet.packet()[4..=5]); packet.set_identification(u16::MAX); assert_eq!(u16::MAX, packet.get_identification()); assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]); } #[test] fn test_flags() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_flags_and_fragment_offset(0); assert_eq!(0, packet.get_flags_and_fragment_offset()); assert_eq!([0x00, 0x00], packet.packet()[6..=7]); // The Don't Fragment (DF) bit set: packet.set_flags_and_fragment_offset(0x4000); assert_eq!(0x4000, packet.get_flags_and_fragment_offset()); assert_eq!([0x40, 0x00], packet.packet()[6..=7]); } #[test] fn test_time_to_live() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_ttl(16); assert_eq!(16, packet.get_ttl()); assert_eq!([0x10], packet.packet()[8..9]); packet.set_ttl(u8::MAX); assert_eq!(u8::MAX, packet.get_ttl()); assert_eq!([0xFF], packet.packet()[8..9]); } #[test] fn test_protocol() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_protocol(IpProtocol::Icmp); assert_eq!(IpProtocol::Icmp, packet.get_protocol()); assert_eq!([0x01], packet.packet()[9..10]); packet.set_protocol(IpProtocol::IcmpV6); assert_eq!(IpProtocol::IcmpV6, packet.get_protocol()); assert_eq!([0x3A], packet.packet()[9..10]); packet.set_protocol(IpProtocol::Udp); assert_eq!(IpProtocol::Udp, packet.get_protocol()); assert_eq!([0x11], packet.packet()[9..10]); packet.set_protocol(IpProtocol::Tcp); assert_eq!(IpProtocol::Tcp, packet.get_protocol()); assert_eq!([0x06], packet.packet()[9..10]); packet.set_protocol(IpProtocol::Other(123)); assert_eq!(IpProtocol::Other(123), packet.get_protocol()); assert_eq!([0x7B], packet.packet()[9..10]); packet.set_protocol(IpProtocol::Other(255)); assert_eq!(IpProtocol::Other(255), packet.get_protocol()); assert_eq!([0xFF], packet.packet()[9..10]); } #[test] fn test_header_checksum() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[10..=11]); packet.set_checksum(12345); assert_eq!(12345, packet.get_checksum()); assert_eq!([0x30, 0x39], packet.packet()[10..=11]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0x0FF, 0xFF], packet.packet()[10..=11]); } #[test] fn test_source() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_source(Ipv4Addr::LOCALHOST); assert_eq!(Ipv4Addr::LOCALHOST, packet.get_source()); assert_eq!([0x07F, 0x00, 0x00, 0x01], packet.packet()[12..=15]); packet.set_source(Ipv4Addr::UNSPECIFIED); assert_eq!(Ipv4Addr::UNSPECIFIED, packet.get_source()); assert_eq!([0x00, 0x00, 0x00, 0x00], packet.packet()[12..=15]); packet.set_source(Ipv4Addr::BROADCAST); assert_eq!(Ipv4Addr::BROADCAST, packet.get_source()); assert_eq!([0xFF, 0xFF, 0xFF, 0xFF], packet.packet()[12..=15]); packet.set_source(Ipv4Addr::new(0xDE, 0x9A, 0x56, 0x12)); assert_eq!(Ipv4Addr::new(0xDE, 0x9A, 0x56, 0x12), packet.get_source()); assert_eq!([0xDE, 0x9A, 0x56, 0x12], packet.packet()[12..=15]); } #[test] fn test_destination() { let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()]; let mut packet = Ipv4Packet::new(&mut buf).unwrap(); packet.set_destination(Ipv4Addr::LOCALHOST); assert_eq!(Ipv4Addr::LOCALHOST, packet.get_destination()); assert_eq!([0x07F, 0x00, 0x00, 0x01], packet.packet()[16..=19]); packet.set_destination(Ipv4Addr::UNSPECIFIED); assert_eq!(Ipv4Addr::UNSPECIFIED, packet.get_destination()); assert_eq!([0x00, 0x00, 0x00, 0x00], packet.packet()[16..=19]); packet.set_destination(Ipv4Addr::BROADCAST); assert_eq!(Ipv4Addr::BROADCAST, packet.get_destination()); assert_eq!([0xFF, 0xFF, 0xFF, 0xFF], packet.packet()[16..=19]); packet.set_destination(Ipv4Addr::new(0xDE, 0x9A, 0x56, 0x12)); assert_eq!( Ipv4Addr::new(0xDE, 0x9A, 0x56, 0x12), packet.get_destination() ); assert_eq!([0xDE, 0x9A, 0x56, 0x12], packet.packet()[16..=19]); } #[test] fn test_view() { let buf = [ 0x45, 0x00, 0x00, 0x54, 0xa2, 0x71, 0x00, 0x00, 0x15, 0x11, 0x9a, 0xee, 0x7f, 0x00, 0x00, 0x01, 0xde, 0x9a, 0x56, 0x12, ]; let packet = Ipv4Packet::new_view(&buf).unwrap(); assert_eq!(4, packet.get_version()); assert_eq!(5, packet.get_header_length()); assert_eq!(0, packet.get_dscp()); assert_eq!(0, packet.get_ecn()); assert_eq!(84, packet.get_total_length()); assert_eq!(41585, packet.get_identification()); assert_eq!(0, packet.get_flags_and_fragment_offset()); assert_eq!(21, packet.get_ttl()); assert_eq!(IpProtocol::Udp, packet.get_protocol()); assert_eq!(39662, packet.get_checksum()); assert_eq!(Ipv4Addr::LOCALHOST, packet.get_source()); assert_eq!( Ipv4Addr::new(0xde, 0x9a, 0x56, 0x12), packet.get_destination() ); assert!(packet.payload().is_empty()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = Ipv4Packet::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = Ipv4Packet::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("Ipv4Packet"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = Ipv4Packet::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = Ipv4Packet::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("Ipv4Packet"), SIZE, SIZE - 1), err ); } } ================================================ FILE: crates/trippy-packet/src/ipv6.rs ================================================ use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::{IpProtocol, fmt_payload}; use std::fmt::{Debug, Formatter}; use std::net::Ipv6Addr; const VERSION_OFFSET: usize = 0; const TRAFFIC_CLASS_OFFSET: usize = 0; const FLOW_LABEL_OFFSET: usize = 1; const PAYLOAD_LENGTH_OFFSET: usize = 4; const NEXT_HEADER_OFFSET: usize = 6; const HOP_LIMIT_OFFSET: usize = 7; const SOURCE_ADDRESS_OFFSET: usize = 8; const DESTINATION_ADDRESS_OFFSET: usize = 24; /// Represents an IPv6 Packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor methods /// take and return data in host byte order, converting as necessary for the given architecture. pub struct Ipv6Packet<'a> { buf: Buffer<'a>, } impl<'a> Ipv6Packet<'a> { pub fn new(packet: &'a mut [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("Ipv6Packet"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &'a [u8]) -> Result { if packet.len() >= Self::minimum_packet_size() { Ok(Self { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("Ipv6Packet"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 40 } #[must_use] pub fn get_version(&self) -> u8 { (self.buf.read(VERSION_OFFSET) & 0xf0) >> 4 } #[must_use] pub fn get_traffic_class(&self) -> u8 { let b0 = ((self.buf.read(TRAFFIC_CLASS_OFFSET)) & 0xf) << 4; let b1 = ((self.buf.read(TRAFFIC_CLASS_OFFSET + 1)) & 0xf0) >> 4; b0 | b1 } #[must_use] pub fn get_flow_label(&self) -> u32 { let b1 = (self.buf.read(FLOW_LABEL_OFFSET)) & 0xf; let b2 = self.buf.read(FLOW_LABEL_OFFSET + 1); let b3 = self.buf.read(FLOW_LABEL_OFFSET + 2); u32::from_be_bytes([0, b1, b2, b3]) } #[must_use] pub fn get_payload_length(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(PAYLOAD_LENGTH_OFFSET)) } #[must_use] pub fn get_next_header(&self) -> IpProtocol { IpProtocol::from(self.buf.read(NEXT_HEADER_OFFSET)) } #[must_use] pub fn get_hop_limit(&self) -> u8 { self.buf.read(HOP_LIMIT_OFFSET) } #[must_use] pub fn get_source_address(&self) -> Ipv6Addr { Ipv6Addr::from(self.buf.get_bytes(SOURCE_ADDRESS_OFFSET)) } #[must_use] pub fn get_destination_address(&self) -> Ipv6Addr { Ipv6Addr::from(self.buf.get_bytes(DESTINATION_ADDRESS_OFFSET)) } pub fn set_version(&mut self, val: u8) { *self.buf.write(VERSION_OFFSET) = (self.buf.read(VERSION_OFFSET) & 0xf) | ((val & 0xf) << 4); } pub fn set_traffic_class(&mut self, val: u8) { *self.buf.write(TRAFFIC_CLASS_OFFSET) = (self.buf.read(TRAFFIC_CLASS_OFFSET) & 0xf0) | ((val & 0xf0) >> 4); *self.buf.write(TRAFFIC_CLASS_OFFSET + 1) = (self.buf.read(TRAFFIC_CLASS_OFFSET + 1) & 0xf) | ((val & 0xf) << 4); } pub fn set_flow_label(&mut self, val: u32) { let bytes = val.to_be_bytes(); *self.buf.write(FLOW_LABEL_OFFSET) = (self.buf.read(FLOW_LABEL_OFFSET) & 0xf0) | bytes[1]; *self.buf.write(FLOW_LABEL_OFFSET + 1) = bytes[2]; *self.buf.write(FLOW_LABEL_OFFSET + 2) = bytes[3]; } pub fn set_payload_length(&mut self, val: u16) { self.buf.set_bytes(PAYLOAD_LENGTH_OFFSET, val.to_be_bytes()); } pub fn set_next_header(&mut self, val: IpProtocol) { *self.buf.write(NEXT_HEADER_OFFSET) = val.id(); } pub fn set_hop_limit(&mut self, val: u8) { *self.buf.write(HOP_LIMIT_OFFSET) = val; } pub fn set_source_address(&mut self, val: Ipv6Addr) { self.buf.set_bytes(SOURCE_ADDRESS_OFFSET, val.octets()); } pub fn set_destination_address(&mut self, val: Ipv6Addr) { self.buf.set_bytes(DESTINATION_ADDRESS_OFFSET, val.octets()); } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); debug_assert!( vals.len() <= self.get_payload_length() as usize, "vals.len() <= len" ); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()].copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { let start = Self::minimum_packet_size(); let end = std::cmp::min( Self::minimum_packet_size() + self.get_payload_length() as usize, self.buf.as_slice().len(), ); if self.buf.as_slice().len() <= start { return &[]; } &self.buf.as_slice()[start..end] } } impl Debug for Ipv6Packet<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("Ipv6Packet") .field("version", &self.get_version()) .field("traffic_class", &self.get_traffic_class()) .field("flow_label", &self.get_flow_label()) .field("payload_length", &self.get_payload_length()) .field("next_header", &self.get_next_header()) .field("hop_limit", &self.get_hop_limit()) .field("source_address", &self.get_source_address()) .field("destination_address", &self.get_destination_address()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; use std::str::FromStr; #[test] fn test_version() { let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()]; let mut packet = Ipv6Packet::new(&mut buf).unwrap(); packet.set_version(5); assert_eq!(5, packet.get_version()); assert_eq!([0x50], packet.packet()[..1]); packet.set_version(15); assert_eq!(15, packet.get_version()); assert_eq!([0xF0], packet.packet()[..1]); } #[test] fn test_traffic_class() { let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()]; let mut packet = Ipv6Packet::new(&mut buf).unwrap(); packet.set_traffic_class(0); assert_eq!(0, packet.get_traffic_class()); assert_eq!([0x00, 0x00], packet.packet()[..2]); packet.set_traffic_class(63); assert_eq!(63, packet.get_traffic_class()); assert_eq!([0x03, 0xF0], packet.packet()[..2]); } #[test] fn test_version_and_traffic_class() { let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()]; let mut packet = Ipv6Packet::new(&mut buf).unwrap(); packet.set_version(15); packet.set_traffic_class(63); assert_eq!(15, packet.get_version()); assert_eq!(63, packet.get_traffic_class()); assert_eq!([0xF3, 0xF0], packet.packet()[..2]); } #[test] fn test_flow_label() { let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()]; let mut packet = Ipv6Packet::new(&mut buf).unwrap(); packet.set_flow_label(0); assert_eq!(0, packet.get_flow_label()); assert_eq!([0x00, 0x00, 0x00], packet.packet()[1..=3]); packet.set_flow_label(500_000); assert_eq!(500_000, packet.get_flow_label()); assert_eq!([0x07, 0xA1, 0x20], packet.packet()[1..=3]); packet.set_flow_label(1_048_575); assert_eq!(1_048_575, packet.get_flow_label()); assert_eq!([0x0F, 0xFF, 0xFF], packet.packet()[1..=3]); } #[test] fn test_payload_length() { let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()]; let mut packet = Ipv6Packet::new(&mut buf).unwrap(); packet.set_payload_length(0); assert_eq!(0, packet.get_payload_length()); assert_eq!([0x00, 0x00], packet.packet()[4..=5]); packet.set_payload_length(120); assert_eq!(120, packet.get_payload_length()); assert_eq!([0x00, 0x78], packet.packet()[4..=5]); packet.set_payload_length(65535); assert_eq!(65535, packet.get_payload_length()); assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]); } #[test] fn test_next_header() { let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()]; let mut packet = Ipv6Packet::new(&mut buf).unwrap(); packet.set_next_header(IpProtocol::Icmp); assert_eq!(IpProtocol::Icmp, packet.get_next_header()); assert_eq!([0x01], packet.packet()[6..7]); packet.set_next_header(IpProtocol::IcmpV6); assert_eq!(IpProtocol::IcmpV6, packet.get_next_header()); assert_eq!([0x3A], packet.packet()[6..7]); packet.set_next_header(IpProtocol::Udp); assert_eq!(IpProtocol::Udp, packet.get_next_header()); assert_eq!([0x11], packet.packet()[6..7]); packet.set_next_header(IpProtocol::Tcp); assert_eq!(IpProtocol::Tcp, packet.get_next_header()); assert_eq!([0x06], packet.packet()[6..7]); packet.set_next_header(IpProtocol::Other(123)); assert_eq!(IpProtocol::Other(123), packet.get_next_header()); assert_eq!([0x7B], packet.packet()[6..7]); packet.set_next_header(IpProtocol::Other(255)); assert_eq!(IpProtocol::Other(255), packet.get_next_header()); assert_eq!([0xFF], packet.packet()[6..7]); } #[test] fn test_hop_limit() { let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()]; let mut packet = Ipv6Packet::new(&mut buf).unwrap(); packet.set_hop_limit(0); assert_eq!(0, packet.get_hop_limit()); assert_eq!([0x00], packet.packet()[7..8]); packet.set_hop_limit(120); assert_eq!(120, packet.get_hop_limit()); assert_eq!([0x78], packet.packet()[7..8]); packet.set_hop_limit(255); assert_eq!(255, packet.get_hop_limit()); assert_eq!([0xFF], packet.packet()[7..8]); } #[test] fn test_source_address() { let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()]; let mut packet = Ipv6Packet::new(&mut buf).unwrap(); packet.set_source_address(Ipv6Addr::LOCALHOST); assert_eq!(Ipv6Addr::LOCALHOST, packet.get_source_address()); assert_eq!( [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 ], packet.packet()[8..=23] ); packet.set_source_address(Ipv6Addr::from_str("2404:6800:4005:812::200e").unwrap()); assert_eq!( Ipv6Addr::from_str("2404:6800:4005:812::200e").unwrap(), packet.get_source_address() ); assert_eq!( [ 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0E ], packet.packet()[8..=23] ); } #[test] fn test_destination_address() { let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()]; let mut packet = Ipv6Packet::new(&mut buf).unwrap(); packet.set_destination_address(Ipv6Addr::LOCALHOST); assert_eq!(Ipv6Addr::LOCALHOST, packet.get_destination_address()); assert_eq!( [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 ], packet.packet()[24..=39] ); packet.set_destination_address(Ipv6Addr::from_str("2404:6800:4005:812::200e").unwrap()); assert_eq!( Ipv6Addr::from_str("2404:6800:4005:812::200e").unwrap(), packet.get_destination_address() ); assert_eq!( [ 0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0E ], packet.packet()[24..=39] ); } #[test] fn test_view() { let buf = [ 0x60, 0x06, 0x05, 0x00, 0x00, 0x20, 0x06, 0x40, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x8d, 0x7d, 0x69, 0xd0, 0xb6, 0x81, 0x82, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x11, 0x03, 0xf6, 0x76, 0x01, 0x6c, 0x3f, ]; let packet = Ipv6Packet::new_view(&buf).unwrap(); assert_eq!(6, packet.get_version()); assert_eq!(0, packet.get_traffic_class()); assert_eq!(394_496, packet.get_flow_label()); assert_eq!(32, packet.get_payload_length()); assert_eq!(IpProtocol::Tcp, packet.get_next_header()); assert_eq!(64, packet.get_hop_limit()); assert_eq!( Ipv6Addr::from_str("fe80::1c8d:7d69:d0b6:8182").unwrap(), packet.get_source_address() ); assert_eq!( Ipv6Addr::from_str("fe80::811:3f6:7601:6c3f").unwrap(), packet.get_destination_address() ); assert!(packet.payload().is_empty()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = Ipv6Packet::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = Ipv6Packet::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("Ipv6Packet"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = Ipv6Packet::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = Ipv6Packet::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("Ipv6Packet"), SIZE, SIZE - 1), err ); } } ================================================ FILE: crates/trippy-packet/src/lib.rs ================================================ //! Packet wire format parsing and building. //! //! The following packet are supported: //! - `IP` //! - `ICMPv4` //! - `ICMPv6` //! - `IPv4` //! - `IPv6` //! - `UDP` //! - `TCP` //! - `ICMP` extensions //! //! # Endianness //! //! The internal representation is held in network byte order (big-endian) and //! all accessor methods take and return data in host byte order, converting as //! necessary for the given architecture. //! //! # Example //! //! The following example parses an `UDP` packet and asserts its fields: //! //! ```rust //! # fn main() -> anyhow::Result<()> { //! use trippy_packet::udp::UdpPacket; //! //! let buf = hex_literal::hex!("68 bf 81 b6 00 40 ac be"); //! let packet = UdpPacket::new_view(&buf)?; //! assert_eq!(26815, packet.get_source()); //! assert_eq!(33206, packet.get_destination()); //! assert_eq!(64, packet.get_length()); //! assert_eq!(44222, packet.get_checksum()); //! assert!(packet.payload().is_empty()); //! # Ok(()) //! # } //! ``` //! //! The following example builds an `ICMPv4` echo request packet: //! //! ```rust //! # fn main() -> anyhow::Result<()> { //! use trippy_packet::checksum::icmp_ipv4_checksum; //! use trippy_packet::icmpv4::echo_request::EchoRequestPacket; //! use trippy_packet::icmpv4::{IcmpCode, IcmpPacket, IcmpType}; //! //! let mut buf = [0; IcmpPacket::minimum_packet_size()]; //! let mut icmp = EchoRequestPacket::new(&mut buf)?; //! icmp.set_icmp_type(IcmpType::EchoRequest); //! icmp.set_icmp_code(IcmpCode(0)); //! icmp.set_identifier(1234); //! icmp.set_sequence(10); //! icmp.set_checksum(icmp_ipv4_checksum(icmp.packet())); //! assert_eq!(icmp.packet(), &hex_literal::hex!("08 00 f3 23 04 d2 00 0a")); //! # Ok(()) //! # } //! ``` #![forbid(unsafe_code)] mod buffer; /// Packet errors. pub mod error; /// Functions for calculating network checksums. pub mod checksum; /// `ICMPv4` packets. pub mod icmpv4; /// `ICMPv6` packets. pub mod icmpv6; /// `ICMP` extensions. pub mod icmp_extension; /// `IP` packets. pub mod ip; /// `IPv4` packets. pub mod ipv4; /// `IPv6` packets. pub mod ipv6; /// `UDP` packets. pub mod udp; /// `TCP` packets. pub mod tcp; /// The IP packet next layer protocol. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum IpProtocol { Icmp, IcmpV6, Udp, Tcp, Other(u8), } impl IpProtocol { #[must_use] pub const fn id(self) -> u8 { match self { Self::Icmp => 1, Self::IcmpV6 => 58, Self::Udp => 17, Self::Tcp => 6, Self::Other(id) => id, } } #[must_use] pub const fn new(value: u8) -> Self { Self::Other(value) } } impl From for IpProtocol { fn from(id: u8) -> Self { match id { 1 => Self::Icmp, 58 => Self::IcmpV6, 17 => Self::Udp, 6 => Self::Tcp, p => Self::Other(p), } } } /// Format a payload as a hexadecimal string. #[must_use] pub fn fmt_payload(bytes: &[u8]) -> String { use itertools::Itertools as _; format!("{:02x}", bytes.iter().format(" ")) } ================================================ FILE: crates/trippy-packet/src/tcp.rs ================================================ use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use std::fmt::{Debug, Formatter}; const SOURCE_PORT_OFFSET: usize = 0; const DESTINATION_PORT_OFFSET: usize = 2; const SEQUENCE_OFFSET: usize = 4; const ACKNOWLEDGEMENT_OFFSET: usize = 8; const DATA_OFFSET_OFFSET: usize = 12; const RESERVED_OFFSET: usize = 12; const FLAGS_OFFSET: usize = 12; const WINDOW_SIZE_OFFSET: usize = 14; const CHECKSUM_OFFSET: usize = 16; const URGENT_POINTER_OFFSET: usize = 18; /// Represents an TCP Packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor methods /// take and return data in host byte order, converting as necessary for the given architecture. pub struct TcpPacket<'a> { buf: Buffer<'a>, } impl TcpPacket<'_> { pub fn new(packet: &mut [u8]) -> Result> { if packet.len() >= Self::minimum_packet_size() { Ok(TcpPacket { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("TcpPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &[u8]) -> Result> { if packet.len() >= Self::minimum_packet_size() { Ok(TcpPacket { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("TcpPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 20 } #[must_use] pub fn get_source(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(SOURCE_PORT_OFFSET)) } #[must_use] pub fn get_destination(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(DESTINATION_PORT_OFFSET)) } #[must_use] pub fn get_sequence(&self) -> u32 { u32::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET)) } #[must_use] pub fn get_acknowledgement(&self) -> u32 { u32::from_be_bytes(self.buf.get_bytes(ACKNOWLEDGEMENT_OFFSET)) } #[must_use] pub fn get_data_offset(&self) -> u8 { (self.buf.read(DATA_OFFSET_OFFSET) & 0xf0) >> 4 } #[must_use] pub fn get_reserved(&self) -> u8 { (self.buf.read(RESERVED_OFFSET) & 0xe) >> 1 } #[must_use] pub fn get_flags(&self) -> u16 { u16::from_be_bytes([ self.buf.read(FLAGS_OFFSET) & 0x1, self.buf.read(FLAGS_OFFSET + 1), ]) } #[must_use] pub fn get_window_size(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(WINDOW_SIZE_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } #[must_use] pub fn get_urgent_pointer(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(URGENT_POINTER_OFFSET)) } #[must_use] pub fn get_options_raw(&self) -> &[u8] { let current_offset = Self::minimum_packet_size(); let end = std::cmp::min( current_offset + self.tcp_options_length(), self.buf.as_slice().len(), ); &self.buf.as_slice()[current_offset..end] } pub fn set_source(&mut self, val: u16) { self.buf.set_bytes(SOURCE_PORT_OFFSET, val.to_be_bytes()); } pub fn set_destination(&mut self, val: u16) { self.buf .set_bytes(DESTINATION_PORT_OFFSET, val.to_be_bytes()); } pub fn set_sequence(&mut self, val: u32) { self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes()); } pub fn set_acknowledgement(&mut self, val: u32) { self.buf .set_bytes(ACKNOWLEDGEMENT_OFFSET, val.to_be_bytes()); } pub fn set_data_offset(&mut self, val: u8) { *self.buf.write(DATA_OFFSET_OFFSET) = (self.buf.read(DATA_OFFSET_OFFSET) & 0xf) | ((val & 0xf) << 4); } pub fn set_reserved(&mut self, val: u8) { *self.buf.write(RESERVED_OFFSET) = (self.buf.read(RESERVED_OFFSET) & 0xf1) | ((val & 0x7) << 1); } pub fn set_flags(&mut self, val: u16) { let bytes = val.to_be_bytes(); *self.buf.write(FLAGS_OFFSET) = (self.buf.read(FLAGS_OFFSET) & 0xfe) | (bytes[0] & 0x1); *self.buf.write(FLAGS_OFFSET + 1) = bytes[1]; } pub fn set_window_size(&mut self, val: u16) { self.buf.set_bytes(WINDOW_SIZE_OFFSET, val.to_be_bytes()); } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_urgent_pointer(&mut self, val: u16) { self.buf.set_bytes(URGENT_POINTER_OFFSET, val.to_be_bytes()); } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size() + self.tcp_options_length(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()].copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { let start = Self::minimum_packet_size() + self.tcp_options_length(); if self.buf.as_slice().len() <= start { return &[]; } &self.buf.as_slice()[start..] } fn tcp_options_length(&self) -> usize { let data_offset = self.get_data_offset(); if data_offset > 5 { data_offset as usize * 4 - 20 } else { 0 } } } impl Debug for TcpPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("TcpPacket") .field("source", &self.get_source()) .field("destination", &self.get_destination()) .field("sequence", &self.get_sequence()) .field("acknowledgement", &self.get_acknowledgement()) .field("data_offset", &self.get_data_offset()) .field("reserved", &self.get_reserved()) .field("flags", &self.get_flags()) .field("window_size", &self.get_window_size()) .field("checksum", &self.get_checksum()) .field("urgent_pointer", &self.get_urgent_pointer()) .field("options", &self.get_options_raw()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_source() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_source(0); assert_eq!(0, packet.get_source()); assert_eq!([0x00, 0x00], packet.packet()[..=1]); packet.set_source(80); assert_eq!(80, packet.get_source()); assert_eq!([0x00, 0x50], packet.packet()[..=1]); packet.set_source(443); assert_eq!(443, packet.get_source()); assert_eq!([0x01, 0xBB], packet.packet()[..=1]); packet.set_source(u16::MAX); assert_eq!(u16::MAX, packet.get_source()); assert_eq!([0xFF, 0xFF], packet.packet()[..=1]); } #[test] fn test_destination() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_destination(0); assert_eq!(0, packet.get_destination()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_destination(80); assert_eq!(80, packet.get_destination()); assert_eq!([0x00, 0x50], packet.packet()[2..=3]); packet.set_destination(443); assert_eq!(443, packet.get_destination()); assert_eq!([0x01, 0xBB], packet.packet()[2..=3]); packet.set_destination(u16::MAX); assert_eq!(u16::MAX, packet.get_destination()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_sequence() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_sequence(0); assert_eq!(0, packet.get_sequence()); assert_eq!([0x00, 0x00, 0x00, 0x00], packet.packet()[4..=7]); packet.set_sequence(123_456); assert_eq!(123_456, packet.get_sequence()); assert_eq!([0x00, 0x01, 0xE2, 0x40], packet.packet()[4..=7]); packet.set_sequence(u32::MAX); assert_eq!(u32::MAX, packet.get_sequence()); assert_eq!([0xFF, 0xFF, 0xFF, 0xFF], packet.packet()[4..=7]); } #[test] fn test_acknowledgement() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_acknowledgement(0); assert_eq!(0, packet.get_acknowledgement()); assert_eq!([0x00, 0x00, 0x00, 0x00], packet.packet()[8..=11]); packet.set_acknowledgement(123_456); assert_eq!(123_456, packet.get_acknowledgement()); assert_eq!([0x00, 0x01, 0xE2, 0x40], packet.packet()[8..=11]); packet.set_acknowledgement(u32::MAX); assert_eq!(u32::MAX, packet.get_acknowledgement()); assert_eq!([0xFF, 0xFF, 0xFF, 0xFF], packet.packet()[8..=11]); } #[test] fn test_data_offset() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_data_offset(0); assert_eq!(0, packet.get_data_offset()); assert_eq!([0x00], packet.packet()[12..13]); packet.set_data_offset(15); assert_eq!(15, packet.get_data_offset()); assert_eq!([0xf0], packet.packet()[12..13]); } #[test] fn test_reserved() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_reserved(0); assert_eq!(0, packet.get_reserved()); assert_eq!([0x00], packet.packet()[12..13]); packet.set_reserved(7); assert_eq!(7, packet.get_reserved()); assert_eq!([0x0e], packet.packet()[12..13]); } #[test] fn test_flags() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_flags(0); assert_eq!(0, packet.get_flags()); assert_eq!([0x00, 0x00], packet.packet()[12..=13]); packet.set_flags(511); assert_eq!(511, packet.get_flags()); assert_eq!([0x01, 0xff], packet.packet()[12..=13]); } #[test] fn test_data_offset_and_reserved() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_data_offset(0); packet.set_reserved(0); assert_eq!(0, packet.get_data_offset()); assert_eq!(0, packet.get_reserved()); assert_eq!([0x00], packet.packet()[12..13]); packet.set_data_offset(15); packet.set_reserved(7); assert_eq!(15, packet.get_data_offset()); assert_eq!(7, packet.get_reserved()); assert_eq!([0xfe], packet.packet()[12..13]); } #[test] fn test_reserved_and_flags() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_reserved(0); packet.set_flags(0); assert_eq!(0, packet.get_flags()); assert_eq!([0x00, 0x00], packet.packet()[12..=13]); packet.set_reserved(7); packet.set_flags(511); assert_eq!(511, packet.get_flags()); assert_eq!([0x0f, 0xff], packet.packet()[12..=13]); } #[test] fn test_data_offset_and_reserved_and_flags() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_data_offset(0); packet.set_reserved(0); packet.set_flags(0); assert_eq!(0, packet.get_flags()); assert_eq!([0x00, 0x00], packet.packet()[12..=13]); packet.set_data_offset(15); packet.set_reserved(7); packet.set_flags(511); assert_eq!(511, packet.get_flags()); assert_eq!([0xff, 0xff], packet.packet()[12..=13]); } #[test] fn test_window_size() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_window_size(0); assert_eq!(0, packet.get_window_size()); assert_eq!([0x00, 0x00], packet.packet()[14..=15]); packet.set_window_size(80); assert_eq!(80, packet.get_window_size()); assert_eq!([0x00, 0x50], packet.packet()[14..=15]); packet.set_window_size(443); assert_eq!(443, packet.get_window_size()); assert_eq!([0x01, 0xBB], packet.packet()[14..=15]); packet.set_window_size(u16::MAX); assert_eq!(u16::MAX, packet.get_window_size()); assert_eq!([0xFF, 0xFF], packet.packet()[14..=15]); } #[test] fn test_checksum() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[16..=17]); packet.set_checksum(80); assert_eq!(80, packet.get_checksum()); assert_eq!([0x00, 0x50], packet.packet()[16..=17]); packet.set_checksum(443); assert_eq!(443, packet.get_checksum()); assert_eq!([0x01, 0xBB], packet.packet()[16..=17]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[16..=17]); } #[test] fn test_urgent_pointer() { let mut buf = [0_u8; TcpPacket::minimum_packet_size()]; let mut packet = TcpPacket::new(&mut buf).unwrap(); packet.set_urgent_pointer(0); assert_eq!(0, packet.get_urgent_pointer()); assert_eq!([0x00, 0x00], packet.packet()[18..=19]); packet.set_urgent_pointer(80); assert_eq!(80, packet.get_urgent_pointer()); assert_eq!([0x00, 0x50], packet.packet()[18..=19]); packet.set_urgent_pointer(443); assert_eq!(443, packet.get_urgent_pointer()); assert_eq!([0x01, 0xBB], packet.packet()[18..=19]); packet.set_urgent_pointer(u16::MAX); assert_eq!(u16::MAX, packet.get_urgent_pointer()); assert_eq!([0xFF, 0xFF], packet.packet()[18..=19]); } #[test] fn test_view() { let buf = [ 0x01, 0xbb, 0xe5, 0xd7, 0x60, 0xb0, 0x76, 0x50, 0x8e, 0x03, 0x46, 0xa2, 0x80, 0x10, 0x00, 0x80, 0x3e, 0xdc, 0x00, 0x00, 0x01, 0x01, 0x08, 0x0a, 0x10, 0x52, 0xf6, 0xd4, 0xea, 0x3a, 0x2a, 0x51, ]; let packet = TcpPacket::new_view(&buf).unwrap(); assert_eq!(443, packet.get_source()); assert_eq!(58839, packet.get_destination()); assert_eq!(1_622_177_360, packet.get_sequence()); assert_eq!(2_382_579_362, packet.get_acknowledgement()); assert_eq!(8, packet.get_data_offset()); assert_eq!(0, packet.get_reserved()); assert_eq!(0x10, packet.get_flags()); assert_eq!(128, packet.get_window_size()); assert_eq!(0x3edc, packet.get_checksum()); assert_eq!(0, packet.get_urgent_pointer()); assert_eq!(12, packet.tcp_options_length()); assert_eq!( &[ 0x01, 0x01, 0x08, 0x0a, 0x10, 0x52, 0xf6, 0xd4, 0xea, 0x3a, 0x2a, 0x51 ], packet.get_options_raw() ); assert!(packet.payload().is_empty()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = TcpPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = TcpPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("TcpPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = TcpPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = TcpPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("TcpPacket"), SIZE, SIZE - 1), err ); } } ================================================ FILE: crates/trippy-packet/src/udp.rs ================================================ use crate::buffer::Buffer; use crate::error::{Error, Result}; use crate::fmt_payload; use std::fmt::{Debug, Formatter}; const SOURCE_PORT_OFFSET: usize = 0; const DESTINATION_PORT_OFFSET: usize = 2; const LENGTH_OFFSET: usize = 4; const CHECKSUM_OFFSET: usize = 6; /// Represents a UDP Packet. /// /// The internal representation is held in network byte order (big-endian) and all accessor methods /// take and return data in host byte order, converting as necessary for the given architecture. pub struct UdpPacket<'a> { buf: Buffer<'a>, } impl UdpPacket<'_> { pub fn new(packet: &mut [u8]) -> Result> { if packet.len() >= UdpPacket::minimum_packet_size() { Ok(UdpPacket { buf: Buffer::Mutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("UdpPacket"), Self::minimum_packet_size(), packet.len(), )) } } pub fn new_view(packet: &[u8]) -> Result> { if packet.len() >= UdpPacket::minimum_packet_size() { Ok(UdpPacket { buf: Buffer::Immutable(packet), }) } else { Err(Error::InsufficientPacketBuffer( String::from("UdpPacket"), Self::minimum_packet_size(), packet.len(), )) } } #[must_use] pub const fn minimum_packet_size() -> usize { 8 } #[must_use] pub fn get_source(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(SOURCE_PORT_OFFSET)) } #[must_use] pub fn get_destination(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(DESTINATION_PORT_OFFSET)) } #[must_use] pub fn get_length(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(LENGTH_OFFSET)) } #[must_use] pub fn get_checksum(&self) -> u16 { u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET)) } pub fn set_source(&mut self, val: u16) { self.buf.set_bytes(SOURCE_PORT_OFFSET, val.to_be_bytes()); } pub fn set_destination(&mut self, val: u16) { self.buf .set_bytes(DESTINATION_PORT_OFFSET, val.to_be_bytes()); } pub fn set_length(&mut self, val: u16) { self.buf.set_bytes(LENGTH_OFFSET, val.to_be_bytes()); } pub fn set_checksum(&mut self, val: u16) { self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes()); } pub fn set_payload(&mut self, vals: &[u8]) { let current_offset = Self::minimum_packet_size(); self.buf.as_slice_mut()[current_offset..current_offset + vals.len()].copy_from_slice(vals); } #[must_use] pub fn packet(&self) -> &[u8] { self.buf.as_slice() } #[must_use] pub fn payload(&self) -> &[u8] { &self.buf.as_slice()[Self::minimum_packet_size()..] } } impl Debug for UdpPacket<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("UdpPacket") .field("source", &self.get_source()) .field("destination", &self.get_destination()) .field("length", &self.get_length()) .field("checksum", &self.get_checksum()) .field("payload", &fmt_payload(self.payload())) .finish() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_source() { let mut buf = [0_u8; UdpPacket::minimum_packet_size()]; let mut packet = UdpPacket::new(&mut buf).unwrap(); packet.set_source(0); assert_eq!(0, packet.get_source()); assert_eq!([0x00, 0x00], packet.packet()[..=1]); packet.set_source(80); assert_eq!(80, packet.get_source()); assert_eq!([0x00, 0x50], packet.packet()[..=1]); packet.set_source(443); assert_eq!(443, packet.get_source()); assert_eq!([0x01, 0xBB], packet.packet()[..=1]); packet.set_source(u16::MAX); assert_eq!(u16::MAX, packet.get_source()); assert_eq!([0xFF, 0xFF], packet.packet()[..=1]); } #[test] fn test_destination() { let mut buf = [0_u8; UdpPacket::minimum_packet_size()]; let mut packet = UdpPacket::new(&mut buf).unwrap(); packet.set_destination(0); assert_eq!(0, packet.get_destination()); assert_eq!([0x00, 0x00], packet.packet()[2..=3]); packet.set_destination(80); assert_eq!(80, packet.get_destination()); assert_eq!([0x00, 0x50], packet.packet()[2..=3]); packet.set_destination(443); assert_eq!(443, packet.get_destination()); assert_eq!([0x01, 0xBB], packet.packet()[2..=3]); packet.set_destination(u16::MAX); assert_eq!(u16::MAX, packet.get_destination()); assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]); } #[test] fn test_length() { let mut buf = [0_u8; UdpPacket::minimum_packet_size()]; let mut packet = UdpPacket::new(&mut buf).unwrap(); packet.set_length(0); assert_eq!(0, packet.get_length()); assert_eq!([0x00, 0x00], packet.packet()[4..=5]); packet.set_length(202); assert_eq!(202, packet.get_length()); assert_eq!([0x00, 0xCA], packet.packet()[4..=5]); packet.set_length(1025); assert_eq!(1025, packet.get_length()); assert_eq!([0x04, 0x01], packet.packet()[4..=5]); packet.set_length(u16::MAX); assert_eq!(u16::MAX, packet.get_length()); assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]); } #[test] fn test_checksum() { let mut buf = [0_u8; UdpPacket::minimum_packet_size()]; let mut packet = UdpPacket::new(&mut buf).unwrap(); packet.set_checksum(0); assert_eq!(0, packet.get_checksum()); assert_eq!([0x00, 0x00], packet.packet()[6..=7]); packet.set_checksum(202); assert_eq!(202, packet.get_checksum()); assert_eq!([0x00, 0xCA], packet.packet()[6..=7]); packet.set_checksum(1025); assert_eq!(1025, packet.get_checksum()); assert_eq!([0x04, 0x01], packet.packet()[6..=7]); packet.set_checksum(u16::MAX); assert_eq!(u16::MAX, packet.get_checksum()); assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]); } #[test] fn test_view() { let buf = [0x68, 0xbf, 0x81, 0xb6, 0x00, 0x40, 0xac, 0xbe]; let packet = UdpPacket::new_view(&buf).unwrap(); assert_eq!(26815, packet.get_source()); assert_eq!(33206, packet.get_destination()); assert_eq!(64, packet.get_length()); assert_eq!(44222, packet.get_checksum()); assert!(packet.payload().is_empty()); } #[test] fn test_new_insufficient_buffer() { const SIZE: usize = UdpPacket::minimum_packet_size(); let mut buf = [0_u8; SIZE - 1]; let err = UdpPacket::new(&mut buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("UdpPacket"), SIZE, SIZE - 1), err ); } #[test] fn test_new_view_insufficient_buffer() { const SIZE: usize = UdpPacket::minimum_packet_size(); let buf = [0_u8; SIZE - 1]; let err = UdpPacket::new_view(&buf).unwrap_err(); assert_eq!( Error::InsufficientPacketBuffer(String::from("UdpPacket"), SIZE, SIZE - 1), err ); } } ================================================ FILE: crates/trippy-privilege/Cargo.toml ================================================ [package] name = "trippy-privilege" description = "Discover platform privileges" version.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true readme.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true keywords.workspace = true categories.workspace = true [dependencies] thiserror.workspace = true [target.'cfg(target_os = "linux")'.dependencies] caps.workspace = true [target.'cfg(unix)'.dependencies] nix = { workspace = true, default-features = false, features = ["user"] } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_System_Threading"] } paste.workspace = true [dev-dependencies] anyhow.workspace = true [lints] workspace = true ================================================ FILE: crates/trippy-privilege/src/lib.rs ================================================ //! Discover platform privileges. //! //! A cross-platform library to discover and manage platform privileges needed //! for sending ICMP packets via RAW and `IPPROTO_ICMP` sockets. //! //! [`Privilege::acquire_privileges`]: //! //! - On Linux we check if `CAP_NET_RAW` is in the permitted set and if so raise it to the effective //! set //! - On other Unix platforms this is a no-op //! - On Windows this is a no-op //! //! [`Privilege::has_privileges`] (obtained via [`Privilege::discover`]): //! //! - On Linux we check if `CAP_NET_RAW` is in the effective set //! - On other Unix platforms we check that the effective user is root //! - On Windows we check if the current process has an elevated token //! //! [`Privilege::needs_privileges`] (obtained via [`Privilege::discover`]): //! //! - On macOS we do not always need privileges to send ICMP packets as we can use `IPPROTO_ICMP` //! sockets with the `IP_HDRINCL` socket option. //! - On Linux we always need privileges to send ICMP packets even though it supports the //! `IPPROTO_ICMP` socket type but not the `IP_HDRINCL` socket option //! - On Windows we always need privileges to send ICMP packets //! //! [`Privilege::drop_privileges`]: //! //! - On Linux we clear the effective set //! - On other Unix platforms this is a no-op //! - On Windows this is a no-op //! //! # Examples //! //! Acquire the required privileges if we can: //! //! ```rust //! # fn main() -> anyhow::Result<()> { //! # use trippy_privilege::Privilege; //! let privilege = Privilege::acquire_privileges()?; //! if privilege.has_privileges() { //! println!("You have the required privileges for raw sockets"); //! } else { //! println!("You do not have the required privileges for raw sockets"); //! } //! if privilege.needs_privileges() { //! println!("You always need privileges to send ICMP packets."); //! } else { //! println!("You do not always need privileges to send ICMP packets."); //! } //! # Ok(()) //! # } //! ``` //! //! Discover the current privileges: //! //! ```rust //! # fn main() -> anyhow::Result<()> { //! # use trippy_privilege::Privilege; //! let privilege = Privilege::discover()?; //! if privilege.has_privileges() { //! println!("You have the required privileges for raw sockets"); //! } else { //! println!("You do not have the required privileges for raw sockets"); //! } //! if privilege.needs_privileges() { //! println!("You always need privileges to send ICMP packets."); //! } else { //! println!("You do not always need privileges to send ICMP packets."); //! } //! # Ok(()) //! # } //! ``` //! //! Drop all privileges: //! //! ```rust //! # fn main() -> anyhow::Result<()> { //! # use trippy_privilege::Privilege; //! Privilege::drop_privileges()?; //! # Ok(()) //! # } //! ``` /// A privilege error result. pub type Result = std::result::Result; /// A privilege error. #[derive(thiserror::Error, Debug)] pub enum Error { #[cfg(target_os = "linux")] #[error("caps error: {0}")] CapsError(#[from] caps::errors::CapsError), #[cfg(windows)] #[error("OpenProcessToken failed")] OpenProcessTokenError, #[cfg(windows)] #[error("GetTokenInformation failed")] GetTokenInformationError, } /// Run-time platform privilege information. #[derive(Debug)] pub struct Privilege { has_privileges: bool, needs_privileges: bool, } impl Privilege { /// Discover information about the platform privileges. pub fn discover() -> Result { let has_privileges = Self::check_has_privileges()?; let needs_privileges = Self::check_needs_privileges(); Ok(Self { has_privileges, needs_privileges, }) } /// Create a new Privilege instance. #[must_use] pub const fn new(has_privileges: bool, needs_privileges: bool) -> Self { Self { has_privileges, needs_privileges, } } /// Are we running with the privileges required for raw sockets? #[must_use] pub const fn has_privileges(&self) -> bool { self.has_privileges } /// Does our platform always need privileges for `ICMP`? /// /// Specifically, each platform requires privileges unless it supports the `IPPROTO_ICMP` socket /// type which _also_ allows the `IP_HDRINCL` socket option to be set. #[must_use] pub const fn needs_privileges(&self) -> bool { self.needs_privileges } // Linux #[cfg(target_os = "linux")] /// Acquire privileges, if possible. /// /// Check if `CAP_NET_RAW` is in the permitted set and if so raise it to the effective set. pub fn acquire_privileges() -> Result { if caps::has_cap(None, caps::CapSet::Permitted, caps::Capability::CAP_NET_RAW)? { caps::raise(None, caps::CapSet::Effective, caps::Capability::CAP_NET_RAW)?; } Self::discover() } #[cfg(target_os = "linux")] /// Do we have the required privileges? /// /// Check if `CAP_NET_RAW` is in the effective set. fn check_has_privileges() -> Result { Ok(caps::has_cap( None, caps::CapSet::Effective, caps::Capability::CAP_NET_RAW, )?) } #[cfg(target_os = "linux")] /// Drop all privileges. /// /// Clears the effective set. pub fn drop_privileges() -> Result<()> { caps::clear(None, caps::CapSet::Effective)?; Ok(()) } // Unix (excl. Linux) #[cfg(all(unix, not(target_os = "linux")))] /// Acquire privileges, if possible. /// /// This is a no-op on non-Linux unix systems. pub fn acquire_privileges() -> Result { Self::discover() } #[cfg(all(unix, not(target_os = "linux")))] #[expect(clippy::unnecessary_wraps)] /// Do we have the required privileges? /// /// Checks if the effective user is root. fn check_has_privileges() -> Result { Ok(nix::unistd::Uid::effective().is_root()) } #[cfg(all(unix, not(target_os = "linux")))] /// Drop all privileges. /// /// This is a no-op on non-Linux unix systems. pub const fn drop_privileges() -> Result<()> { Ok(()) } // Unix (excl. macOS) #[cfg(all(unix, not(target_os = "macos")))] /// Does the platform always require privileges? /// /// Whilst Linux supports the `IPPROTO_ICMP` socket type, it does not allow using it with the /// `IP_HDRINCL` socket option and is therefore not supported. This may be supported in the /// future. /// /// `NetBSD`, `OpenBSD` and `FreeBSD` do not support `IPPROTO_ICMP`. const fn check_needs_privileges() -> bool { true } // macOS #[cfg(target_os = "macos")] /// Does the platform always require privileges? /// /// `macOS` supports both privileged and unprivileged modes. const fn check_needs_privileges() -> bool { false } // Windows #[cfg(windows)] /// Acquire privileges, if possible. /// /// This is a no-op on `Windows`. pub fn acquire_privileges() -> Result { Self::discover() } #[cfg(windows)] /// Do we have the required privileges? /// /// Check if the current process has an elevated token. fn check_has_privileges() -> Result { macro_rules! syscall { ($p: path, $fn: ident ( $($arg: expr),* $(,)* ) ) => {{ #[expect(unsafe_code)] unsafe { paste::paste!(windows_sys::Win32::$p::$fn) ($($arg, )*) } }}; } /// Window elevated privilege checker. pub struct Privileged { handle: windows_sys::Win32::Foundation::HANDLE, } impl Privileged { /// Create a new `ElevationChecker` for the current process. pub fn current_process() -> Result { use windows_sys::Win32::Security::TOKEN_QUERY; let mut handle: windows_sys::Win32::Foundation::HANDLE = 0; let current_process = syscall!(System::Threading, GetCurrentProcess()); let res = syscall!( System::Threading, OpenProcessToken(current_process, TOKEN_QUERY, std::ptr::addr_of_mut!(handle)) ); if res == 0 { Err(Error::OpenProcessTokenError) } else { Ok(Self { handle }) } } /// Check if the current process has elevated privileged. pub fn is_elevated(&self) -> Result { use windows_sys::Win32::Security::TOKEN_ELEVATION; use windows_sys::Win32::Security::TokenElevation; let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 }; #[expect(clippy::cast_possible_truncation)] let size = std::mem::size_of::() as u32; let mut ret_size = 0u32; let ret = syscall!( Security, GetTokenInformation( self.handle, TokenElevation, std::ptr::addr_of_mut!(elevation).cast(), size, std::ptr::addr_of_mut!(ret_size), ) ); if ret == 0 { Err(Error::GetTokenInformationError) } else { Ok(elevation.TokenIsElevated != 0) } } } impl Drop for Privileged { fn drop(&mut self) { if self.handle != 0 { syscall!(Foundation, CloseHandle(self.handle)); } } } Privileged::current_process()?.is_elevated() } #[cfg(windows)] /// Drop all capabilities. /// /// This is a no-op on `Windows`. pub const fn drop_privileges() -> Result<()> { Ok(()) } #[cfg(target_os = "windows")] /// Does the platform always require privileges? /// /// Privileges are always required on `Windows`. const fn check_needs_privileges() -> bool { true } } ================================================ FILE: crates/trippy-tui/Cargo.toml ================================================ [package] name = "trippy-tui" description = "A network diagnostic tool" version.workspace = true authors.workspace = true documentation.workspace = true homepage.workspace = true repository.workspace = true readme.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true keywords.workspace = true categories.workspace = true [dependencies] trippy-core.workspace = true trippy-privilege.workspace = true trippy-dns.workspace = true anyhow.workspace = true chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } chrono-tz.workspace = true clap = { workspace = true, default-features = false, features = ["cargo", "derive", "wrap_help", "usage", "unstable-styles", "color", "suggestions", "error-context", "env"] } clap_complete.workspace = true clap_mangen.workspace = true comfy-table.workspace = true crossterm = { workspace = true, default-features = false, features = ["events", "windows"] } csv.workspace = true encoding_rs_io.workspace = true etcetera.workspace = true humantime.workspace = true itertools.workspace = true maxminddb.workspace = true petgraph.workspace = true ratatui.workspace = true serde = { workspace = true, default-features = false, features = ["derive"] } serde_json.workspace = true serde_with.workspace = true strum = { workspace = true, default-features = false, features = ["std", "derive"] } sys-locale.workspace = true toml = { workspace = true, default-features = false, features = ["parse"] } tracing-chrome.workspace = true tracing-subscriber = { workspace = true, default-features = false, features = ["env-filter", "json"] } tracing.workspace = true unic-langid.workspace = true unicode-width.workspace = true clap-cargo.workspace = true [dev-dependencies] insta = { workspace = true, features = ["serde"] } pretty_assertions.workspace = true test-case.workspace = true [lints] workspace = true ================================================ FILE: crates/trippy-tui/build.rs ================================================ pub fn main() { println!("cargo:rerun-if-changed=locales.toml"); } ================================================ FILE: crates/trippy-tui/locales.toml ================================================ [trippy] en = "trippy" fr = "trippy" tr = "trippy" it = "trippy" pt = "trippy" zh = "trippy" zh-TW = "trippy" sv = "trippy" ru = "trippy" es = "trippy" de = "trippy" [auto] en = "auto" fr = "automatique" tr = "otomatik" it = "auto" pt = "automático" zh = "自动" zh-TW = "自動" sv = "automatisk" ru = "авто" es = "automático" de = "auto" [on] en = "on" fr = "activé" tr = "açık" it = "on" pt = "ligado" zh = "开" zh-TW = "開" sv = "på" ru = "вкл" es = "activo" de = "an" [off] en = "off" fr = "désactivé" tr = "kapalı" it = "off" pt = "desligado" zh = "关" zh-TW = "關" sv = "av" ru = "выкл" es = "inactivo" de = "aus" [yes] en = "Yes" fr = "Oui" tr = "Evet" it = "Sì" pt = "Sim" zh = "是" zh-TW = "是" sv = "Ja" ru = "Да" es = "Sí" de = "Ja" [no] en = "No" fr = "Non" tr = "Hayır" it = "No" pt = "Não" zh = "否" zh-TW = "否" sv = "Nej" ru = "Нет" es = "No" de = "Nein" [none] en = "none" fr = "aucun" tr = "hiçbiri" it = "nessuno" pt = "nenhum" zh = "无" zh-TW = "無" sv = "ingen" ru = "нет" es = "ninguno" de = "keiner" [hidden] en = "Hidden" fr = "Caché" tr = "Gizli" it = "Nascosto" pt = "Oculto" zh = "隐藏" zh-TW = "隱藏" sv = "Dold" ru = "Скрыто" es = "Oculto" de = "Versteckt" [flow] en = "flow" fr = "flux" tr = "akış" it = "flusso" pt = "fluxo" zh = "流量" zh-TW = "流量" sv = "flöde" ru = "поток" es = "flujo" de = "fluss" [flows] en = "flows" fr = "flux" tr = "akışlar" it = "flussi" pt = "fluxos" zh = "流量" zh-TW = "流量" sv = "flöden" ru = "потоки" es = "flujos" de = "flüsse" [target] en = "Target" fr = "Cible" tr = "Hedef" it = "Target" pt = "Alvo" zh = "目标" zh-TW = "目標" sv = "Mål" ru = "Цель" es = "Objetivo" de = "Ziel" [status] en = "Status" fr = "Statut" tr = "Durum" it = "Stato" pt = "Estado" zh = "状态" zh-TW = "狀態" sv = "Status" ru = "Статус" es = "Estado" de = "Status" [details] en = "detail" fr = "détail" tr = "ayrıntılar" it = "dettagli" pt = "detalhe" zh = "详情" zh-TW = "詳情" sv = "detaljer" ru = "детали" es = "detalles" de = "detail" [privileged] en = "privileged" fr = "privilégié" tr = "ayrıcalıklı" it = "privilegiato" pt = "privilegiado" zh = "特权" zh-TW = "特權" sv = "privilegierad" ru = "привилегированный" es = "privilegiado" de = "privilegiert" [unprivileged] en = "unprivileged" fr = "non privilégié" tr = "ayrıcalıksız" it = "non privilegiato" pt = "não privilegiado" zh = "非特权" zh-TW = "非特權" sv = "oprivilegierad" ru = "непривилегированный" es = "no privilegiado" de = "unprivilegiert" [privacy] en = "privacy" fr = "confidentialité" tr = "gizlilik" it = "privacy" pt = "privacidade" zh = "隐私" zh-TW = "隱私" sv = "integritet" ru = "конфиденциальность" es = "privacidad" de = "datenschutz" [na] en = "n/a" fr = "non disponible" tr = "yok" it = "n/d" pt = "n/d" zh = "无" zh-TW = "無" sv = "ej tillgänglig" ru = "н/д" es = "n/d" de = "n/a" [discovered] en = "discovered %{hop_count} hops" fr = "%{hop_count} sauts découverts" tr = "%{hop_count} atlanan keşfedildi" it = "%{hop_count} salti trovati" pt = "descobriu %{hop_count} saltos" zh = "发现 %{hop_count} 跳" zh-TW = "發現 %{hop_count} 跳" sv = "upptäckte %{hop_count} hopp" ru = "обнаружено %{hop_count} прыжков" es = "se descubrieron %{hop_count} saltos" de = "%{hop_count} gefundene hops" [discovered_flows] en = "discovered %{hop_count} hops and %{flow_count} unique %{plural_flows}" fr = "%{hop_count} sauts et %{flow_count} %{plural_flows} uniques découverts" tr = "%{hop_count} atlanan ve %{flow_count} benzersiz %{plural_flows} keşfedildi" it = "scoperti %{hop_count} salti e %{flow_count} %{plural_flows} unici" pt = "descobriu %{hop_count} saltos e %{flow_count} %{plural_flows} únicos" zh = "发现 %{hop_count} 跳,%{flow_count} 个唯一%{plural_flow}" zh-TW = "發現 %{hop_count} 跳,%{flow_count} 個唯一%{plural_flow}" sv = "%{hop_count} hopp och unika %{flow_count} %{plural_flows} upptäckta" ru = "обнаружено %{hop_count} прыжков и %{flow_count} уникальных %{plural_flows}" es = "se descubrieron %{hop_count} saltos y %{flow_count} únicos %{plural_flows}" de = "%{hop_count} gefundene hops und %{flow_count} eindeutige %{plural_flows}" [unknown] en = "unknown" fr = "inconnu" tr = "bilinmeyen" it = "sconosciuto" pt = "desconhecido" zh = "未知" zh-TW = "未知" sv = "okänd" ru = "неизвестно" es = "desconocido" de = "unbekannt" [icmp] en = "icmp" fr = "icmp" tr = "icmp" it = "icmp" pt = "icmp" zh = "icmp" zh-TW = "icmp" sv = "icmp" ru = "icmp" es = "icmp" de = "icmp" [udp] en = "udp" fr = "udp" tr = "udp" it = "udp" pt = "udp" zh = "udp" zh-TW = "udp" sv = "udp" ru = "udp" es = "udp" de = "udp" [tcp] en = "tcp" fr = "tcp" tr = "tcp" it = "tcp" pt = "tcp" zh = "tcp" zh-TW = "tcp" sv = "tcp" ru = "tcp" es = "tcp" de = "tcp" [status_failures] en = "%{failure_count} of %{total_probes} (%{failure_rate}%) probes failed" fr = "%{failure_count} sur %{total_probes} (%{failure_rate}%) sondes ont échoué" tr = "%{failure_count} / %{total_probes} (%{failure_rate}%) sondajın başarısız olması" it = "%{failure_count} di %{total_probes} (%{failure_rate}%) prove fallite" pt = "%{failure_count} de %{total_probes} (%{failure_rate}%) sondas falharam" zh = "%{failure_count} 个失败,共探测到 %{total_probes} 个(%{failure_rate}%)" zh-TW = "%{failure_count} 個失敗,共探測到 %{total_probes} 個(%{failure_rate}%)" sv = "%{failure_count} av %{total_probes} (%{failure_rate}%) misslyckade prober" ru = "%{failure_count} из %{total_probes} (%{failure_rate}%) зондов не удалось" es = "%{failure_count} de %{total_probes} (%{failure_rate}%) sondas fallaron" de = "%{failure_count} von %{total_probes} (%{failure_rate}%) sonden sind fehlgeschlagen" [status_failed] en = "Failed" fr = "Échec" tr = "Başarısız" it = "Fallito" pt = "Falhou" zh = "失败" zh-TW = "失敗" sv = "Misslyckades" ru = "Не удалось" es = "Fallido" de = "Fehlgeschlagen" [status_running] en = "Running" fr = "En cours" tr = "Çalışıyor" it = "In esecuzione" pt = "Executando" zh = "运行中" zh-TW = "執行中" sv = "Kör" ru = "Запущен" es = "En ejecución" de = "Läuft" [status_frozen] en = "Frozen" fr = "Gelé" tr = "Dondurulmuş" it = "Congelato" pt = "Congelado" zh = "冻结" zh-TW = "凍結" sv = "Frusen" ru = "Заморожен" es = "Congelado" de = "Eingefroren" [awaiting_data] en = "Awaiting data..." fr = "En attente de données..." tr = "Veri bekleniyor..." it = "In attesa di dati..." pt = "Aguardando dados..." zh = "等待数据……" zh-TW = "等待資料……" sv = "Väntar på data..." ru = "Ожидание данных..." es = "Esperando datos..." de = "Warten auf Daten..." [header_help] en = "help" fr = "aide" tr = "yardım" it = "aiuto" pt = "ajuda" zh = "帮助" zh-TW = "幫助" sv = "hjälp" ru = "помощь" es = "ayuda" de = "hilfe" [header_settings] en = "settings" fr = "paramètres" tr = "ayarlar" it = "impostazioni" pt = "configurações" zh = "设置" zh-TW = "設定" sv = "inställningar" ru = "настройки" es = "configuraciones" de = "einstellungen" [header_quit] en = "quit" fr = "quitter" tr = "cıkış" it = "uscita" pt = "sair" zh = "退出" zh-TW = "退出" sv = "avsluta" ru = "Выход" es = "salir" de = "beenden" [title_hops] en = "Hops" fr = "Sauts" tr = "Atlananlar" it = "Salti" pt = "Saltos" zh = "跳" zh-TW = "跳" sv = "Hopp" ru = "Прыжки" es = "Saltos" de = "Hops" [title_frequency] en = "Frequency" fr = "Fréquence" tr = "Sıklık" it = "Frequenza" pt = "Frequência" zh = "频率" zh-TW = "頻率" sv = "Frekvens" ru = "Частота" es = "Frecuencia" de = "Frequenz" [title_samples] en = "Samples" fr = "Échantillons" tr = "Örnekler" it = "Campioni" pt = "Amostras" zh = "样本" zh-TW = "樣本" sv = "Prover" ru = "Образцы" es = "Muestras" de = "Proben" [title_traces] en = "Traces" fr = "Traces" tr = "İzler" it = "Tracce" pt = "Rastreios" zh = "跟踪" zh-TW = "追蹤" sv = "Spår" ru = "Следы" es = "Trazas" de = "Spuren" [title_flows] en = "Flows" fr = "Flux" tr = "Akışlar" it = "Flussi" pt = "Fluxos" zh = "流量" zh-TW = "流量" sv = "Flöden" ru = "Потоки" es = "Flujos" de = "Flüsse" [title_map] en = "Map" fr = "Carte" tr = "Harita" it = "Mappa" pt = "Mapa" zh = "地图" zh-TW = "地圖" sv = "Karta" ru = "Карта" es = "Mapa" de = "Karte" [title_help] en = "Help" fr = "Aide" tr = "Yardım" it = "Aiuto" pt = "Ajuda" zh = "帮助" zh-TW = "幫助" sv = "Hjälp" ru = "Помощь" es = "Ayuda" de = "Hilfe" [title_settings] en = "Settings" fr = "Paramètres" tr = "Ayarlar" it = "Impostazioni" pt = "Configurações" zh = "设置" zh-TW = "設定" sv = "Inställningar" ru = "Настройки" es = "Configuraciones" de = "Einstellungen" [bsod_failed] en = "Trippy Failed :(" fr = "Trippy a échoué :(" tr = "Trippy Başarısız :(" it = "Trippy ha avuto un problema :(" pt = "Trippy falhou :(" zh = "Trippy 失败 :(" zh-TW = "Trippy 失敗 :(" sv = "Trippy misslyckades :(" ru = "Trippy не удалось :(" es = "Trippy tuvo un problema :(" de = "Trippy ist fehlgeschlagen :(" [bsod_quit] en = "Press q to quit" fr = "Appuyez sur q pour quitter" tr = "Çıkmak için q tuşuna basın" it = "Premi q per uscire" pt = "Pressione q para sair" zh = "按 q 退出" zh-TW = "按 q 退出" sv = "Tryck på q för att avsluta" ru = "Нажмите q для выхода" es = "Presiona q para salir" de = "Drücken Sie q, um zu beenden" [hop] en = "Hop" fr = "Saut" tr = "Atla" it = "Salto" pt = "Salto" zh = "跳" zh-TW = "跳" sv = "Hopp" ru = "Прыжок" es = "Salto" de = "Hop" [rtt] en = "RTT" fr = "RTT" tr = "RTT" it = "RTT" pt = "RTT" zh = "往返时间" zh-TW = "往返時間" sv = "RTT" ru = "RTT" es = "RTT" de = "RTT" [title_chart] en = "Chart" fr = "Graphique" tr = "Grafik" it = "Grafico" pt = "Gráfico" zh = "图表" zh-TW = "圖表" sv = "Diagram" ru = "Диаграмма" es = "Gráfico" de = "Diagramm" [samples] en = "Samples" fr = "Échantillons" tr = "Örnekler" it = "Campioni" pt = "Amostras" zh = "样本" zh-TW = "樣本" sv = "Prover" ru = "Образцы" es = "Muestras" de = "Proben" [host] en = "Host" fr = "Hôte" tr = "Ana bilgisayar" it = "Host" pt = "Host" zh = "主机" zh-TW = "主機" sv = "Värd" ru = "Хост" es = "Host" de = "Host" [no_response] en = "No response" fr = "Pas de réponse" tr = "Yanıt yok" it = "Nessuna risposta" pt = "Sem resposta" zh = "无响应" zh-TW = "無回應" sv = "Inget svar" ru = "Нет ответа" es = "Sin respuesta" de = "Keine Antwort" [dns_failed] en = "Failed" fr = "Échec" tr = "Başarısız" it = "Fallito" pt = "Falhou" zh = "失败" zh-TW = "失敗" sv = "Misslyckades" ru = "Не удалось" es = "Fallido" de = "Fehlgeschlagen" [dns_timeout] en = "Timeout" fr = "Délai dépassé" tr = "Zaman aşımı" it = "Tempo scaduto" pt = "Tempo esgotado" zh = "超时" zh-TW = "逾時" sv = "Tidsgräns nådd" ru = "Тайм-аут" es = "Tiempo de espera" de = "Zeitüberschreitung" [labels] en = "labels" fr = "étiquettes" tr = "etiketler" it = "etichette" pt = "etiquetas" zh = "标签" zh-TW = "標籤" sv = "etiketter" ru = "метки" es = "etiquetas" de = "etiketten" [not_enabled] en = "not enabled" fr = "non activé" tr = "etkin değil" it = "non abilitato" pt = "não ativado" zh = "未启用" zh-TW = "未啟用" sv = "ej aktiverad" ru = "не активирован" es = "no habilitado" de = "nicht aktiviert" [not_found] en = "not found" fr = "non trouvé" tr = "bulunamadı" it = "non trovato" pt = "não encontrado" zh = "未找到" zh-TW = "未找到" sv = "hittades inte" ru = "не найдено" es = "no encontrado" de = "nicht gefunden" [awaited] en = "awaited" fr = "attendu" tr = "beklenen" it = "atteso" pt = "aguardado" zh = "等待" zh-TW = "等待" sv = "väntade" ru = "ожидаемый" es = "esperado" de = "erwartet" [name] en = "Name" fr = "Nom" tr = "Ad" it = "Nome" pt = "Nome" zh = "名称" zh-TW = "名稱" sv = "Namn" ru = "Имя" es = "Nombre" de = "Name" [info] en = "Info" fr = "Information" tr = "Bilgi" it = "Info" pt = "Informação" zh = "信息" zh-TW = "資訊" sv = "Information" ru = "Информация" es = "Información" de = "Info" [geo] en = "Geo" fr = "Géo" tr = "Coğrafi" it = "Geo" pt = "Geo" zh = "地理坐标" zh-TW = "地理座標" sv = "Geo" ru = "Гео" es = "Geo" de = "Geo" [pos] en = "Pos" fr = "Pos" tr = "Poz" it = "Pos" pt = "Pos" zh = "位置" zh-TW = "位置" sv = "Pos" ru = "Поз" es = "Pos" de = "Pos" [ext] en = "Ext" fr = "Ext" tr = "Uzantı" it = "Est" pt = "Ext" zh = "扩展" zh-TW = "擴充" sv = "Ext" ru = "Расширение" es = "Ext" de = "Ext" [help_tagline] en = "A network diagnostic tool" fr = "Un outil de diagnostic réseau" tr = "Bir ağ analiz aracı" it = "Uno strumento diagnostico di rete" pt = "Uma ferramenta de diagnóstico de rede" zh = "网络诊断工具" zh-TW = "網路診斷工具" sv = "Ett nätverksdiagnostikverktyg" ru = "Инструмент диагностики сети" es = "Una herramienta de diagnóstico de red" de = "Ein Netzwerkdiagnosetool" [help_show_settings] en = "Press [%{key}] to show all settings" fr = "Appuyez sur [%{key}] pour afficher tous les paramètres" tr = "Tüm ayarları görmek için [%{key}] tuşuna basın" it = "Premi [%{key}] per visualizzare tutte le impostazioni" pt = "Pressione [%{key}] para mostrar todas as configurações" zh = "按 [%{key}] 显示所有设置" zh-TW = "按 [%{key}] 顯示所有設定" sv = "Tryck på [%{key}] för att visa alla inställningar" ru = "Нажмите [%{key}], чтобы показать все настройки" es = "Presiona [%{key}] para mostrar todas las configuraciones" de = "Drücken Sie [%{key}], um alle Einstellungen anzuzeigen" [help_show_bindings] en = "Press [%{key}] to show all bindings" fr = "Appuyez sur [%{key}] pour afficher tous les raccourcis clavier" tr = "Tüm bağlantıları görmek için [%{key}] tuşuna basın" it = "Premi [%{key}] per visualizzare tutti i collegamenti" pt = "Pressione [%{key}] para mostrar todos os atalhos" zh = "按 [%{key}] 显示所有绑定" zh-TW = "按 [%{key}] 顯示所有綁定" sv = "Tryck på [%{key}] för att visa alla kortkommando" ru = "Нажмите [%{key}], чтобы показать все привязки" es = "Presiona [%{key}] para mostrar todos los atajos" de = "Drücken Sie [%{key}], um alle Tastenbelegungen anzuzeigen" [help_show_columns] en = "Press [%{key}] to show all columns" fr = "Appuyez sur [%{key}] pour afficher toutes les colonnes" tr = "Tüm sütunları görmek için [%{key}] tuşuna basın" it = "Premi [%{key}] per visualizzare tutte le colonne" pt = "Pressione [%{key}] para mostrar todas as colunas" zh = "按 [%{key}] 显示所有列" zh-TW = "按 [%{key}] 顯示所有欄" sv = "Tryck på [%{key}] för att visa alla kolumner" ru = "Нажмите [%{key}], чтобы показать все столбцы" es = "Presiona [%{key}] para mostrar todas las columnas" de = "Drücken Sie [%{key}], um alle Spalten anzuzeigen" [help_license] en = "Distributed under the Apache License 2.0" fr = "Distribué sous licence Apache 2.0" tr = "Apache Lisansı 2.0 altında dağıtılmıştır" it = "Distribuito con licenza Apache 2.0" pt = "Distribuído sob a licença Apache 2.0" zh = "以 Apache-2.0 许可分发" zh-TW = "以 Apache-2.0 授權分發" sv = "Distribueras under Apache License 2.0" ru = "Распространяется под лицензией Apache 2.0" es = "Distribuido bajo la Licencia Apache 2.0" de = "Verteilt unter der Apache-Lizenz 2.0" [help_copyright] en = "Copyright 2022 Trippy Contributors" fr = "Copyright 2022 Contributeurs de Trippy" tr = "Telif Hakkı 2022 - Trippy'ye Katkıda Bulunanlar" it = "Copyright 2022 - Collaboratori di Trippy" pt = "Direitos autorais 2022 Colaboradores do Trippy" zh = "版权所有 2022 Trippy 贡献者" zh-TW = "版權所有 2022 Trippy 貢獻者" sv = "Upphovsrätt 2022 Trippy-bidragsgivare" ru = "Авторское право 2022 Участники Trippy" es = "Derechos de autor 2022 Contribuidores de Trippy" de = "Copyright 2022 Trippy Mitwirkende" [geoip_not_enabled] en = "GeoIp not enabled" fr = "GeoIp non activé" tr = "GeoIp etkin değil" it = "GeoIp non abilitato" pt = "GeoIp não ativado" zh = "GeoIp 未启用" zh-TW = "GeoIp 未啟用" sv = "GeoIp inte aktiverad" ru = "GeoIp не активирован" es = "GeoIp no habilitado" de = "GeoIp nicht aktiviert" [geoip_no_data_for_hop] en = "No GeoIp data for hop" fr = "Pas de données GeoIp pour le saut" tr = "Hop için GeoIp verisi yok" it = "Nessun dato GeoIp per il salto" pt = "Nenhum dado GeoIp para o salto" zh = "无 GeoIp 数据" zh-TW = "無 GeoIp 資料" sv = "Inga GeoIp-data för hopp" ru = "Нет данных GeoIp для прыжка" es = "No hay datos de GeoIp para el salto" de = "Keine GeoIp-Daten für den Hop" [geoip_multiple_data_for_hop] en = "Multiple GeoIp locations for hop" fr = "Emplacements GeoIp multiples pour le saut" tr = "Hop için birden fazla GeoIp konumu" it = "Posizioni GeoIp multiple per il salto" pt = "Múltiplas localizações GeoIp para o salto" zh = "多个 GeoIp 位置" zh-TW = "多個 GeoIp 位置" sv = "Flera GeoIp-platser för hopp" ru = "Несколько местоположений GeoIp для прыжка" es = "Múltiples ubicaciones de GeoIp para el salto" de = "Mehrere GeoIp-Standorte für den Hop" [kilometer] en = "km" fr = "km" tr = "km" it = "km" pt = "km" zh = "公里" zh-TW = "公里" sv = "km" ru = "км" es = "km" de = "km" [settings_info] en = "Info" fr = "Information" tr = "Bilgi" it = "Info" pt = "Informação" zh = "信息" zh-TW = "資訊" sv = "Information" ru = "Информация" es = "Información" de = "Info" [settings_tab_tui_title] en = "Tui" fr = "Tui" tr = "Tui" it = "Tui" pt = "Tui" zh = "终端用户界面" zh-TW = "終端使用者介面" sv = "Tui" ru = "Tui" es = "Tui" de = "Tui" [settings_tab_trace_title] en = "Trace" fr = "Tracer" tr = "İz" it = "Traccia" pt = "Rastrear" zh = "跟踪" zh-TW = "追蹤" sv = "Spåra" ru = "След" es = "Rastrear" de = "Trace" [settings_tab_dns_title] en = "DNS" fr = "DNS" tr = "DNS" it = "DNS" pt = "DNS" zh = "DNS" zh-TW = "DNS" sv = "DNS" ru = "DNS" es = "DNS" de = "DNS" [settings_tab_geoip_title] en = "GeoIp" fr = "GeoIp" tr = "GeoIp" it = "GeoIp" pt = "GeoIp" zh = "GeoIp" zh-TW = "GeoIp" sv = "GeoIp" ru = "GeoIp" es = "GeoIp" de = "GeoIp" [settings_tab_bindings_title] en = "Bindings" fr = "Raccourcis clavier" tr = "Bağlantılar" it = "Collegamenti" pt = "Atalhos" zh = "绑定" zh-TW = "綁定" sv = "Kortkommando" ru = "Привязки" es = "Atajos" de = "Tastenbelegungen" [settings_tab_theme_title] en = "Theme" fr = "Thème" tr = "Tema" it = "Tema" pt = "Tema" zh = "主题" zh-TW = "主題" sv = "Tema" ru = "Тема" es = "Tema" de = "Darstellung" [settings_tab_columns_title] en = "Columns" fr = "Colonnes" tr = "Sütunlar" it = "Colonne" pt = "Colunas" zh = "列" zh-TW = "欄" es = "Columnas" sv = "Kolumner" ru = "Столбцы" de = "Spalten" [settings_tab_tui_desc] en = "Settings which control how data is displayed in this Tui" fr = "Paramètres qui contrôlent la façon dont les données sont affichées dans ce Tui" tr = "Arayüzde verilerin nasıl görüntülendiğini kontrol eden ayarlar" it = "Impostazioni che controllano come i dati vengono visualizzati in questo Tui" pt = "Configurações que controlam como os dados são exibidos neste Tui" zh = "数据显示方式设置" zh-TW = "資料顯示方式設定" sv = "Inställningar som styr hur data visas i detta Tui" ru = "Настройки, которые контролируют, как данные отображаются в этом Tui" es = "Configuraciones que controlan cómo se muestran los datos en este Tui" de = "Einstellungen, die steuern, wie Daten in diesem Tui angezeigt werden" [settings_tab_trace_desc] en = "Settings which control the tracing strategy" fr = "Paramètres qui contrôlent la stratégie de traçage" tr = "İzleme stratejisini kontrol eden ayarlar" it = "Impostazioni che controllano la strategia di tracciamento" pt = "Configurações que controlam a estratégia de rastreamento" zh = "跟踪策略设置" zh-TW = "追蹤策略設定" sv = "Inställningar som styr spårningsstrategin" ru = "Настройки, которые контролируют стратегию трассировки" es = "Configuraciones que controlan la estrategia de rastreo" de = "Einstellungen, die die Tracing-Strategie steuern" [settings_tab_dns_desc] en = "Settings which control how DNS lookups are performed" fr = "Paramètres qui contrôlent la façon dont les recherches DNS sont effectuées" tr = "DNS aramalarının nasıl yapıldığını kontrol eden ayarlar" it = "Impostazioni che controllano come vengono eseguite le ricerche DNS" pt = "Configurações que controlam como as pesquisas DNS são realizadas" zh = "DNS 查询设置" zh-TW = "DNS 查詢設定" sv = "Inställningar som styr hur DNS-uppslag utförs" ru = "Настройки, которые контролируют, как выполняются DNS-запросы" es = "Configuraciones que controlan cómo se realizan las búsquedas de DNS" de = "Einstellungen, die steuern, wie DNS-Lookups durchgeführt werden" [settings_tab_geoip_desc] en = "Settings relating to GeoIp" fr = "Paramètres relatifs à GeoIp" tr = "GeoIp ile ilgili ayarlar" it = "Impostazioni relative a GeoIp" pt = "Configurações relacionadas ao GeoIp" zh = "GeoIp 设置" zh-TW = "GeoIp 設定" sv = "Inställningar som rör GeoIp" ru = "Настройки, касающиеся GeoIp" es = "Configuraciones relacionadas con GeoIp" de = "Einstellungen im Zusammenhang mit GeoIp" [settings_tab_bindings_desc] en = "Tui key bindings" fr = "Raccourcis clavier Tui" tr = "Tui tuş ayarları" it = "Collegamenti chiave Tui" pt = "Atalhos de teclado Tui" zh = "按键绑定设置" zh-TW = "按鍵綁定設定" sv = "Tui-kortkommando" ru = "Привязки клавиш Tui" es = "Atajos de teclado Tui" de = "Tui-Tastenbelegungen" [settings_tab_theme_desc] en = "Tui theme colors" fr = "Couleurs du thème Tui" tr = "Tui tema renkleri" it = "Colori del tema Tui" pt = "Cores do tema Tui" zh = "主题颜色设置" zh-TW = "主題顏色設定" sv = "Tui-temafärger" ru = "Цвета темы Tui" es = "Colores del tema Tui" de = "Tui-Themefarben" [settings_tab_columns_desc] en = "Tui table columns. Press [%{c}] to toggle a column on or off and use the [%{d}] and [%{u}] keys to change the column order." fr = "Colonnes de table Tui. Appuyez sur [%{c}] pour activer ou désactiver une colonne et utilisez les touches [%{d}] et [%{u}] pour changer l'ordre des colonnes." tr = "Tui tablo sütunları. Bir sütunu açmak veya kapatmak için [%{c}] tuşuna basın ve sütun sırasını değiştirmek için [%{d}] ve [%{u}] tuşlarını kullanın." it = "Colonne della tabella Tui. Premi [%{c}] per attivare o disattivare una colonna e usa i tasti [%{d}] e [%{u}] per cambiare l'ordine delle colonne." pt = "Colunas da tabela Tui. Pressione [%{c}] para ativar ou desativar uma coluna e use as teclas [%{d}] e [%{u}] para alterar a ordem das colunas." zh = "终端用户界面表格列。按 [%{c}] 切换列的显示和隐藏,使用 [%{d}] 和 [%{u}] 键更改列的顺序。" zh-TW = "終端使用者介面表格欄。按 [%{c}] 切換欄的顯示和隱藏,使用 [%{d}] 和 [%{u}] 鍵更改欄的順序。" sv = "Tui-tabellkolumner. Tryck på [%{c}] för att slå på eller av en kolumn och använd [%{d}] och [%{u}] för att ändra kolumnordningen." ru = "Столбцы таблицы Tui. Нажмите [%{c}], чтобы включить или отключить столбец, и используйте клавиши [%{d}] и [%{u}], чтобы изменить порядок столбцов." es = "Columnas de la tabla Tui. Presiona [%{c}] para activar o desactivar una columna y usa las teclas [%{d}] y [%{u}] para cambiar el orden de las columnas." de = "Tui-Tabellenspalten. Drücken Sie [%{c}], um eine Spalte ein- oder auszuschalten, und verwenden Sie die Tasten [%{d}] und [%{u}], um die Spaltenreihenfolge zu ändern." [settings_table_header_setting] en = "Setting" fr = "Paramètres" tr = "Ayar" it = "Impostazione" pt = "Configuração" zh = "设置" zh-TW = "設定" sv = "Inställning" ru = "Настройка" es = "Configuración" de = "Einstellung" [settings_table_header_value] en = "Value" fr = "Valeur" tr = "Değer" it = "Valore" pt = "Valor" zh = "值" zh-TW = "值" sv = "Värde" ru = "Значение" es = "Valor" de = "Wert" [column_host] en = "Host" fr = "Hôte" tr = "Ana bilgisayar" it = "Host" pt = "Host" zh = "主机" zh-TW = "主機" sv = "Värd" ru = "Хост" es = "Host" de = "Host" [column_loss_pct] en = "Loss%" fr = "% Perdus" tr = "Kayıp%" it = "Persi%" pt = "% Perdidos" zh = "丢包率" zh-TW = "封包遺失率" sv = "Förlust%" ru = "Потери%" es = "% Perdidos" de = "Verlust%" [column_snd] en = "Snd" fr = "Envoyés" tr = "Gönderilen" it = "Snd" pt = "Enviados" zh = "发出" zh-TW = "發送" sv = "Skickat" ru = "Отпр" es = "Enviados" de = "Snd" [column_recv] en = "Recv" fr = "Reçus" tr = "Alınan" it = "Recv" pt = "Recebidos" zh = "接收" zh-TW = "接收" sv = "Mottagna" ru = "Получ" es = "Recibidos" de = "Recv" [column_last] en = "Last" fr = "Dernier" tr = "Son" it = "Ultimo" pt = "Último" zh = "最后" zh-TW = "最後" sv = "Senast" ru = "Посл" es = "Último" de = "Letzte" [column_avg] en = "Avg" fr = "Moyenne" tr = "Ort" it = "Media" pt = "Média" zh = "平均" zh-TW = "平均" sv = "Genomsnitt" ru = "Сред" es = "Prom" de = "Durchschnitt" [column_best] en = "Best" fr = "Meilleur" tr = "En iyi" it = "Migliore" pt = "Melhor" zh = "最佳" zh-TW = "最佳" sv = "Bäst" ru = "Луч" es = "Mejor" de = "Beste" [column_wrst] en = "Wrst" fr = "Pire" tr = "En kötü" it = "Peggiore" pt = "Pior" zh = "最差" zh-TW = "最差" sv = "Sämst" ru = "Худ" es = "Peor" de = "Schlechteste" [column_stdev] en = "StDev" fr = "ÉcTyp" tr = "StDev" it = "StDev" pt = "DesvPad" zh = "标准差" zh-TW = "標準差" sv = "StDev" ru = "СКО" es = "DesvE" de = "StdAbw" [column_sts] en = "Sts" fr = "État" tr = "Sts" it = "Stato" pt = "Est" zh = "状态" zh-TW = "狀態" sv = "Sts" ru = "Статус" es = "Est" de = "Sts" [column_jttr] en = "Jttr" fr = "Gigue" tr = "Jttr" it = "Jttr" pt = "Jttr" zh = "抖动" zh-TW = "抖動" sv = "Jttr" ru = "Джитр" es = "Jttr" de = "Jttr" [column_javg] en = "Javg" fr = "GigMoy" tr = "Javg" it = "Javg" pt = "Javg" zh = "均抖" zh-TW = "均抖" sv = "Javg" ru = "СреднДжитр" es = "PromJit" de = "Javg" [column_jmax] en = "Jmax" fr = "GigMax" tr = "Jmax" it = "Jmax" pt = "Jmax" zh = "最大抖" zh-TW = "最大抖" sv = "Jmax" ru = "МаксДжитр" es = "JitMax" de = "Jmax" [column_jint] en = "Jint" fr = "GigInt" tr = "Jint" it = "Jint" pt = "Jint" zh = "抖动间隔" zh-TW = "抖動間隔" sv = "Jint" ru = "ИнтДжитр" es = "JitInt" de = "Jint" [column_sprt] en = "Sprt" fr = "Psrc" tr = "Sprt" it = "Sprt" zh = "源端" zh-TW = "來源端" sv = "Sprt" ru = "Исх" es = "Sprt" de = "Sprt" [column_dprt] en = "Dprt" fr = "Pdest" tr = "Dprt" it = "Dprt" pt = "Dprt" zh = "目标" zh-TW = "目標" sv = "Dprt" ru = "Назн" es = "Dprt" de = "Dprt" [column_seq] en = "Seq" fr = "Seq" tr = "Seq" it = "Seq" pt = "Seq" zh = "序列" zh-TW = "序列" sv = "Seq" ru = "Посл" es = "Seq" de = "Seq" [column_type] en = "Type" fr = "Type" tr = "Type" it = "Tipo" pt = "Tipo" zh = "类型" zh-TW = "類型" sv = "Typ" ru = "Тип" es = "Tipo" de = "Typ" [column_code] en = "Code" fr = "Code" tr = "Code" it = "Codice" pt = "Código" zh = "代码" zh-TW = "代碼" sv = "Kod" ru = "Код" es = "Código" de = "Code" [column_nat] en = "NAT" fr = "NAT" tr = "NAT" it = "NAT" pt = "NAT" zh = "网络地址转换" zh-TW = "網路位址轉換" sv = "NAT" ru = "NAT" es = "NAT" de = "NAT" [column_fail] en = "Fail" fr = "Échec" tr = "Başarısız" it = "Falliti" pt = "Falha" zh = "失败" zh-TW = "失敗" sv = "Misslyckades" ru = "Неуд" es = "Falló" de = "Fehlgeschlagen" [column_floss] en = "Floss" fr = "Floss" tr = "Floss" it = "Floss" pt = "Floss" zh = "Floss" zh-TW = "Floss" sv = "Floss" ru = "Floss" es = "Floss" de = "Floss" [column_bloss] en = "Bloss" fr = "Bloss" tr = "Bloss" it = "Bloss" pt = "Bloss" zh = "Bloss" zh-TW = "Bloss" sv = "Bloss" ru = "Bloss" es = "Bloss" de = "Bloss" [column_floss_pct] en = "Floss%" fr = "Floss%" tr = "Floss%" it = "Floss%" pt = "Floss%" zh = "Floss%" zh-TW = "Floss%" sv = "Floss%" ru = "Floss%" es = "Floss%" de = "Floss%" [column_dscp] en = "DSCP" fr = "DSCP" tr = "DSCP" it = "DSCP" pt = "DSCP" zh = "DSCP" zh-TW = "DSCP" sv = "DSCP" ru = "DSCP" es = "DSCP" de = "DSCP" [column_ecn] en = "ECN" fr = "ECN" tr = "ECN" it = "ECN" pt = "ECN" zh = "ECN" zh-TW = "ECN" sv = "ECN" ru = "ECN" es = "ECN" de = "ECN" [column_asn] en = "ASN" fr = "ASN" tr = "ASN" it = "ASN" pt = "ASN" zh = "ASN" zh-TW = "ASN" sv = "ASN" ru = "ASN" es = "ASN" de = "ASN" ================================================ FILE: crates/trippy-tui/src/app.rs ================================================ use crate::config::{LogFormat, LogSpanEvents, Mode, TrippyConfig}; use crate::frontend::TuiConfig; use crate::geoip::GeoIpLookup; use crate::locale; use crate::{frontend, report}; use anyhow::{Error, anyhow}; use std::net::IpAddr; use tracing::instrument; use tracing_chrome::{ChromeLayerBuilder, FlushGuard}; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use trippy_core::{Builder, Tracer}; use trippy_dns::{DnsResolver, Resolver}; use trippy_privilege::Privilege; /// Run the trippy application. pub fn run_trippy(cfg: &TrippyConfig, pid: u16) -> anyhow::Result<()> { let locale = locale::set_locale(cfg.tui_locale.as_deref()); let _guard = configure_logging(cfg); tracing::debug!(?cfg); let resolver = start_dns_resolver(cfg)?; let geoip_lookup = create_geoip_lookup(cfg, &locale)?; let addrs = resolve_targets(cfg, &resolver)?; if addrs.is_empty() { return Err(anyhow!( "failed to find any valid IP addresses for {} for address family {}", cfg.targets.join(", "), cfg.addr_family, )); } let traces = start_tracers(cfg, &addrs, pid)?; Privilege::drop_privileges()?; run_frontend(cfg, &locale, resolver, geoip_lookup, traces) } /// Start all tracers. #[instrument(skip(cfg), level = "trace")] fn start_tracers( cfg: &TrippyConfig, addrs: &[TargetInfo], pid: u16, ) -> anyhow::Result> { addrs .iter() .enumerate() .map(|(i, TargetInfo { hostname, addr })| { start_tracer(cfg, hostname, *addr, pid + i as u16) }) .collect::>>() } /// Start a tracer to a given target. #[instrument(skip(cfg), level = "trace")] fn start_tracer( cfg: &TrippyConfig, target_host: &str, target_addr: IpAddr, trace_identifier: u16, ) -> Result { let (tracer, _) = Builder::new(target_addr) .interface(cfg.interface.clone()) .source_addr(cfg.source_addr) .privilege_mode(cfg.privilege_mode) .protocol(cfg.protocol) .packet_size(cfg.packet_size) .payload_pattern(cfg.payload_pattern) .tos(cfg.tos) .icmp_extension_parse_mode(cfg.icmp_extension_parse_mode) .read_timeout(cfg.read_timeout) .tcp_connect_timeout(cfg.min_round_duration) .trace_identifier(trace_identifier) .max_rounds(cfg.max_rounds) .first_ttl(cfg.first_ttl) .max_ttl(cfg.max_ttl) .grace_duration(cfg.grace_duration) .max_inflight(cfg.max_inflight) .initial_sequence(cfg.initial_sequence) .multipath_strategy(cfg.multipath_strategy) .port_direction(cfg.port_direction) .min_round_duration(cfg.min_round_duration) .max_round_duration(cfg.max_round_duration) .max_flows(cfg.max_flows()) .max_samples(cfg.max_samples) .drop_privileges(true) .build()? .spawn()?; Ok(make_trace_info(tracer, target_host.to_string())) } /// Run the TUI, stream or report. #[instrument(skip_all, level = "trace")] fn run_frontend( args: &TrippyConfig, locale: &str, resolver: DnsResolver, geoip_lookup: GeoIpLookup, traces: Vec, ) -> anyhow::Result<()> { match args.mode { Mode::Tui => frontend::run_frontend( traces, make_tui_config(args, locale.to_string()), resolver, geoip_lookup, )?, Mode::Stream => report::stream::report(&traces[0], &resolver)?, Mode::Csv => report::csv::report(&traces[0], args.report_cycles, &resolver)?, Mode::Json => report::json::report(&traces[0], args.report_cycles, &resolver)?, Mode::Pretty => report::table::report_pretty(&traces[0], args.report_cycles, &resolver)?, Mode::Markdown => report::table::report_md(&traces[0], args.report_cycles, &resolver)?, Mode::Dot => report::dot::report(&traces[0], args.report_cycles)?, Mode::Flows => report::flows::report(&traces[0], args.report_cycles)?, Mode::Silent => report::silent::report(&traces[0], args.report_cycles)?, } Ok(()) } /// Resolve targets. #[instrument(skip_all, level = "trace")] fn resolve_targets(cfg: &TrippyConfig, resolver: &DnsResolver) -> anyhow::Result> { cfg.targets .iter() .flat_map(|target| match resolver.lookup(target) { Ok(addrs) => addrs .into_iter() .enumerate() .take_while(|(i, _)| if cfg.dns_resolve_all { true } else { *i == 0 }) .map(|(i, addr)| { let hostname = if cfg.dns_resolve_all { format!("{} [{}]", target, i + 1) } else { target.clone() }; Ok(TargetInfo { hostname, addr }) }) .collect::>() .into_iter(), Err(e) => vec![Err(anyhow!("failed to resolve target: {target} ({e})"))].into_iter(), }) .collect::>>() } /// Start the DNS resolver. #[instrument(skip_all, level = "trace")] fn start_dns_resolver(cfg: &TrippyConfig) -> anyhow::Result { Ok(DnsResolver::start(trippy_dns::Config::new( cfg.dns_resolve_method, cfg.addr_family, cfg.dns_timeout, cfg.dns_ttl, ))?) } #[instrument(skip_all, level = "trace")] fn create_geoip_lookup(cfg: &TrippyConfig, locale: &str) -> anyhow::Result { if let Some(path) = cfg.geoip_mmdb_file.as_ref() { GeoIpLookup::from_file(path, String::from(locale)) } else { Ok(GeoIpLookup::empty()) } } fn configure_logging(cfg: &TrippyConfig) -> Option { if cfg.verbose { let fmt_span = match cfg.log_span_events { LogSpanEvents::Off => FmtSpan::NONE, LogSpanEvents::Active => FmtSpan::ACTIVE, LogSpanEvents::Full => FmtSpan::FULL, }; match cfg.log_format { LogFormat::Compact => { tracing_subscriber::fmt() .with_span_events(fmt_span) .with_env_filter(&cfg.log_filter) .compact() .init(); } LogFormat::Pretty => { tracing_subscriber::fmt() .with_span_events(fmt_span) .with_env_filter(&cfg.log_filter) .pretty() .init(); } LogFormat::Json => { tracing_subscriber::fmt() .with_span_events(fmt_span) .with_env_filter(&cfg.log_filter) .json() .init(); } LogFormat::Chrome => { let (chrome_layer, guard) = ChromeLayerBuilder::new() .writer(std::io::stdout()) .include_args(true) .build(); tracing_subscriber::registry().with(chrome_layer).init(); return Some(guard); } } } None } /// Make the TUI configuration. fn make_tui_config(args: &TrippyConfig, locale: String) -> TuiConfig { TuiConfig::new( args.tui_refresh_rate, args.tui_privacy_max_ttl, args.tui_preserve_screen, args.tui_address_mode, args.dns_lookup_as_info, args.tui_as_mode, args.tui_icmp_extension_mode, args.tui_geoip_mode, args.tui_max_addrs, args.tui_theme, &args.tui_bindings, &args.tui_custom_columns, args.geoip_mmdb_file.clone(), args.dns_resolve_all, locale, args.tui_timezone, ) } /// Make the per-trace information. const fn make_trace_info(tracer: Tracer, target: String) -> TraceInfo { TraceInfo::new(tracer, target) } /// Information about a `Trace` needed for the Tui, stream and reports. #[derive(Debug, Clone)] pub struct TraceInfo { pub data: Tracer, pub target_hostname: String, } impl TraceInfo { #[must_use] pub const fn new(data: Tracer, target_hostname: String) -> Self { Self { data, target_hostname, } } } /// Information about a tracing target. #[derive(Debug, Clone)] struct TargetInfo { pub hostname: String, pub addr: IpAddr, } ================================================ FILE: crates/trippy-tui/src/config/binding.rs ================================================ use crate::config::file::ConfigBindings; use anyhow::anyhow; use crossterm::event::{KeyCode, KeyModifiers}; use serde::Deserialize; use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::str::FromStr; use strum::{AsRefStr, EnumString, VariantNames}; /// Tui keyboard bindings. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct TuiBindings { pub toggle_help: TuiKeyBinding, pub toggle_help_alt: TuiKeyBinding, pub toggle_settings: TuiKeyBinding, pub toggle_settings_tui: TuiKeyBinding, pub toggle_settings_trace: TuiKeyBinding, pub toggle_settings_dns: TuiKeyBinding, pub toggle_settings_geoip: TuiKeyBinding, pub toggle_settings_bindings: TuiKeyBinding, pub toggle_settings_theme: TuiKeyBinding, pub toggle_settings_columns: TuiKeyBinding, pub previous_hop: TuiKeyBinding, pub next_hop: TuiKeyBinding, pub previous_trace: TuiKeyBinding, pub next_trace: TuiKeyBinding, pub previous_hop_address: TuiKeyBinding, pub next_hop_address: TuiKeyBinding, pub address_mode_ip: TuiKeyBinding, pub address_mode_host: TuiKeyBinding, pub address_mode_both: TuiKeyBinding, pub toggle_freeze: TuiKeyBinding, pub toggle_chart: TuiKeyBinding, pub toggle_map: TuiKeyBinding, pub toggle_flows: TuiKeyBinding, pub expand_privacy: TuiKeyBinding, pub contract_privacy: TuiKeyBinding, pub expand_hosts: TuiKeyBinding, pub contract_hosts: TuiKeyBinding, pub expand_hosts_max: TuiKeyBinding, pub contract_hosts_min: TuiKeyBinding, pub chart_zoom_in: TuiKeyBinding, pub chart_zoom_out: TuiKeyBinding, pub clear_trace_data: TuiKeyBinding, pub clear_dns_cache: TuiKeyBinding, pub clear_selection: TuiKeyBinding, pub toggle_as_info: TuiKeyBinding, pub toggle_hop_details: TuiKeyBinding, pub quit: TuiKeyBinding, pub quit_preserve_screen: TuiKeyBinding, } impl Default for TuiBindings { fn default() -> Self { Self { toggle_help: TuiKeyBinding::new(KeyCode::Char('h')), toggle_help_alt: TuiKeyBinding::new(KeyCode::Char('?')), toggle_settings: TuiKeyBinding::new(KeyCode::Char('s')), toggle_settings_tui: TuiKeyBinding::new(KeyCode::Char('1')), toggle_settings_trace: TuiKeyBinding::new(KeyCode::Char('2')), toggle_settings_dns: TuiKeyBinding::new(KeyCode::Char('3')), toggle_settings_geoip: TuiKeyBinding::new(KeyCode::Char('4')), toggle_settings_bindings: TuiKeyBinding::new(KeyCode::Char('5')), toggle_settings_theme: TuiKeyBinding::new(KeyCode::Char('6')), toggle_settings_columns: TuiKeyBinding::new(KeyCode::Char('7')), previous_hop: TuiKeyBinding::new(KeyCode::Up), next_hop: TuiKeyBinding::new(KeyCode::Down), previous_trace: TuiKeyBinding::new(KeyCode::Left), next_trace: TuiKeyBinding::new(KeyCode::Right), previous_hop_address: TuiKeyBinding::new(KeyCode::Char(',')), next_hop_address: TuiKeyBinding::new(KeyCode::Char('.')), address_mode_ip: TuiKeyBinding::new(KeyCode::Char('i')), address_mode_host: TuiKeyBinding::new(KeyCode::Char('n')), address_mode_both: TuiKeyBinding::new(KeyCode::Char('b')), toggle_freeze: TuiKeyBinding::new_with_modifier( KeyCode::Char('f'), KeyModifiers::CONTROL, ), toggle_chart: TuiKeyBinding::new(KeyCode::Char('c')), toggle_map: TuiKeyBinding::new(KeyCode::Char('m')), toggle_flows: TuiKeyBinding::new(KeyCode::Char('f')), expand_privacy: TuiKeyBinding::new(KeyCode::Char('p')), contract_privacy: TuiKeyBinding::new(KeyCode::Char('o')), expand_hosts: TuiKeyBinding::new(KeyCode::Char(']')), contract_hosts: TuiKeyBinding::new(KeyCode::Char('[')), expand_hosts_max: TuiKeyBinding::new(KeyCode::Char('}')), contract_hosts_min: TuiKeyBinding::new(KeyCode::Char('{')), chart_zoom_in: TuiKeyBinding::new(KeyCode::Char('=')), chart_zoom_out: TuiKeyBinding::new(KeyCode::Char('-')), clear_trace_data: TuiKeyBinding::new_with_modifier( KeyCode::Char('r'), KeyModifiers::CONTROL, ), clear_dns_cache: TuiKeyBinding::new_with_modifier( KeyCode::Char('k'), KeyModifiers::CONTROL, ), clear_selection: TuiKeyBinding::new(KeyCode::Esc), toggle_as_info: TuiKeyBinding::new(KeyCode::Char('z')), toggle_hop_details: TuiKeyBinding::new(KeyCode::Char('d')), quit: TuiKeyBinding::new(KeyCode::Char('q')), quit_preserve_screen: TuiKeyBinding::new_with_modifier( KeyCode::Char('q'), KeyModifiers::SHIFT, ), } } } impl TuiBindings { /// Validate the bindings. /// /// Returns any duplicate bindings. pub fn find_duplicates(&self) -> Vec { let (_, duplicates) = [ (self.toggle_help, TuiCommandItem::ToggleHelp), (self.toggle_help_alt, TuiCommandItem::ToggleHelpAlt), (self.toggle_settings, TuiCommandItem::ToggleSettings), (self.toggle_settings_tui, TuiCommandItem::ToggleSettings), (self.toggle_settings_trace, TuiCommandItem::ToggleSettings), (self.toggle_settings_dns, TuiCommandItem::ToggleSettings), (self.toggle_settings_geoip, TuiCommandItem::ToggleSettings), ( self.toggle_settings_bindings, TuiCommandItem::ToggleSettings, ), (self.toggle_settings_theme, TuiCommandItem::ToggleSettings), (self.toggle_settings_columns, TuiCommandItem::ToggleSettings), (self.previous_hop, TuiCommandItem::PreviousHop), (self.next_hop, TuiCommandItem::NextHop), (self.previous_trace, TuiCommandItem::PreviousTrace), (self.next_trace, TuiCommandItem::NextTrace), ( self.previous_hop_address, TuiCommandItem::PreviousHopAddress, ), (self.next_hop_address, TuiCommandItem::NextHopAddress), (self.address_mode_ip, TuiCommandItem::AddressModeIp), (self.address_mode_host, TuiCommandItem::AddressModeHost), (self.address_mode_both, TuiCommandItem::AddressModeBoth), (self.toggle_freeze, TuiCommandItem::ToggleFreeze), (self.toggle_chart, TuiCommandItem::ToggleChart), (self.toggle_map, TuiCommandItem::ToggleMap), (self.toggle_flows, TuiCommandItem::ToggleFlows), (self.expand_privacy, TuiCommandItem::ExpandPrivacy), (self.contract_privacy, TuiCommandItem::ContractPrivacy), (self.expand_hosts, TuiCommandItem::ExpandHosts), (self.expand_hosts_max, TuiCommandItem::ExpandHostsMax), (self.contract_hosts, TuiCommandItem::ContractHosts), (self.contract_hosts_min, TuiCommandItem::ContractHostsMin), (self.chart_zoom_in, TuiCommandItem::ChartZoomIn), (self.chart_zoom_out, TuiCommandItem::ChartZoomOut), (self.clear_trace_data, TuiCommandItem::ClearTraceData), (self.clear_dns_cache, TuiCommandItem::ClearDnsCache), (self.clear_selection, TuiCommandItem::ClearSelection), (self.toggle_as_info, TuiCommandItem::ToggleASInfo), (self.toggle_hop_details, TuiCommandItem::ToggleHopDetails), (self.quit, TuiCommandItem::Quit), ( self.quit_preserve_screen, TuiCommandItem::QuitPreserveScreen, ), ] .iter() .fold( (HashMap::::new(), Vec::new()), |(mut all, mut dups), (binding, item)| { if let Some(existing) = all.get(binding) { dups.push(format!( "{}: [{} and {}]", binding, item.as_ref(), existing.as_ref() )); } else { all.insert(*binding, *item); } (all, dups) }, ); duplicates } } impl From<(HashMap, ConfigBindings)> for TuiBindings { #[expect(clippy::too_many_lines, clippy::or_fun_call)] fn from(value: (HashMap, ConfigBindings)) -> Self { let (cmd_items, cfg) = value; Self { toggle_help: *cmd_items .get(&TuiCommandItem::ToggleHelp) .or(cfg.toggle_help.as_ref()) .unwrap_or(&Self::default().toggle_help), toggle_help_alt: *cmd_items .get(&TuiCommandItem::ToggleHelpAlt) .or(cfg.toggle_help_alt.as_ref()) .unwrap_or(&Self::default().toggle_help_alt), toggle_settings: *cmd_items .get(&TuiCommandItem::ToggleSettings) .or(cfg.toggle_settings.as_ref()) .unwrap_or(&Self::default().toggle_settings), toggle_settings_tui: *cmd_items .get(&TuiCommandItem::ToggleSettingsTui) .or(cfg.toggle_settings_tui.as_ref()) .unwrap_or(&Self::default().toggle_settings_tui), toggle_settings_trace: *cmd_items .get(&TuiCommandItem::ToggleSettingsTrace) .or(cfg.toggle_settings_trace.as_ref()) .unwrap_or(&Self::default().toggle_settings_trace), toggle_settings_dns: *cmd_items .get(&TuiCommandItem::ToggleSettingsDns) .or(cfg.toggle_settings_dns.as_ref()) .unwrap_or(&Self::default().toggle_settings_dns), toggle_settings_geoip: *cmd_items .get(&TuiCommandItem::ToggleSettingsGeoip) .or(cfg.toggle_settings_geoip.as_ref()) .unwrap_or(&Self::default().toggle_settings_geoip), toggle_settings_bindings: *cmd_items .get(&TuiCommandItem::ToggleSettingsBindings) .or(cfg.toggle_settings_bindings.as_ref()) .unwrap_or(&Self::default().toggle_settings_bindings), toggle_settings_theme: *cmd_items .get(&TuiCommandItem::ToggleSettingsTheme) .or(cfg.toggle_settings_theme.as_ref()) .unwrap_or(&Self::default().toggle_settings_theme), toggle_settings_columns: *cmd_items .get(&TuiCommandItem::ToggleSettingsColumns) .or(cfg.toggle_settings_columns.as_ref()) .unwrap_or(&Self::default().toggle_settings_columns), previous_hop: *cmd_items .get(&TuiCommandItem::PreviousHop) .or(cfg.previous_hop.as_ref()) .unwrap_or(&Self::default().previous_hop), next_hop: *cmd_items .get(&TuiCommandItem::NextHop) .or(cfg.next_hop.as_ref()) .unwrap_or(&Self::default().next_hop), previous_trace: *cmd_items .get(&TuiCommandItem::PreviousTrace) .or(cfg.previous_trace.as_ref()) .unwrap_or(&Self::default().previous_trace), next_trace: *cmd_items .get(&TuiCommandItem::NextTrace) .or(cfg.next_trace.as_ref()) .unwrap_or(&Self::default().next_trace), previous_hop_address: *cmd_items .get(&TuiCommandItem::PreviousHopAddress) .or(cfg.previous_hop_address.as_ref()) .unwrap_or(&Self::default().previous_hop_address), next_hop_address: *cmd_items .get(&TuiCommandItem::NextHopAddress) .or(cfg.next_hop_address.as_ref()) .unwrap_or(&Self::default().next_hop_address), address_mode_ip: *cmd_items .get(&TuiCommandItem::AddressModeIp) .or(cfg.address_mode_ip.as_ref()) .unwrap_or(&Self::default().address_mode_ip), address_mode_host: *cmd_items .get(&TuiCommandItem::AddressModeHost) .or(cfg.address_mode_host.as_ref()) .unwrap_or(&Self::default().address_mode_host), address_mode_both: *cmd_items .get(&TuiCommandItem::AddressModeBoth) .or(cfg.address_mode_both.as_ref()) .unwrap_or(&Self::default().address_mode_both), toggle_freeze: *cmd_items .get(&TuiCommandItem::ToggleFreeze) .or(cfg.toggle_freeze.as_ref()) .unwrap_or(&Self::default().toggle_freeze), toggle_chart: *cmd_items .get(&TuiCommandItem::ToggleChart) .or(cfg.toggle_chart.as_ref()) .unwrap_or(&Self::default().toggle_chart), toggle_flows: *cmd_items .get(&TuiCommandItem::ToggleFlows) .or(cfg.toggle_flows.as_ref()) .unwrap_or(&Self::default().toggle_flows), expand_privacy: *cmd_items .get(&TuiCommandItem::ExpandPrivacy) .or(cfg.expand_privacy.as_ref()) .unwrap_or(&Self::default().expand_privacy), contract_privacy: *cmd_items .get(&TuiCommandItem::ContractPrivacy) .or(cfg.contract_privacy.as_ref()) .unwrap_or(&Self::default().contract_privacy), toggle_map: *cmd_items .get(&TuiCommandItem::ToggleMap) .or(cfg.toggle_map.as_ref()) .unwrap_or(&Self::default().toggle_map), expand_hosts: *cmd_items .get(&TuiCommandItem::ExpandHosts) .or(cfg.expand_hosts.as_ref()) .unwrap_or(&Self::default().expand_hosts), contract_hosts: *cmd_items .get(&TuiCommandItem::ContractHosts) .or(cfg.contract_hosts.as_ref()) .unwrap_or(&Self::default().contract_hosts), expand_hosts_max: *cmd_items .get(&TuiCommandItem::ExpandHostsMax) .or(cfg.expand_hosts_max.as_ref()) .unwrap_or(&Self::default().expand_hosts_max), contract_hosts_min: *cmd_items .get(&TuiCommandItem::ContractHostsMin) .or(cfg.contract_hosts_min.as_ref()) .unwrap_or(&Self::default().contract_hosts_min), chart_zoom_in: *cmd_items .get(&TuiCommandItem::ChartZoomIn) .or(cfg.chart_zoom_in.as_ref()) .unwrap_or(&Self::default().chart_zoom_in), chart_zoom_out: *cmd_items .get(&TuiCommandItem::ChartZoomOut) .or(cfg.chart_zoom_out.as_ref()) .unwrap_or(&Self::default().chart_zoom_out), clear_trace_data: *cmd_items .get(&TuiCommandItem::ClearTraceData) .or(cfg.clear_trace_data.as_ref()) .unwrap_or(&Self::default().clear_trace_data), clear_dns_cache: *cmd_items .get(&TuiCommandItem::ClearDnsCache) .or(cfg.clear_dns_cache.as_ref()) .unwrap_or(&Self::default().clear_dns_cache), clear_selection: *cmd_items .get(&TuiCommandItem::ClearSelection) .or(cfg.clear_selection.as_ref()) .unwrap_or(&Self::default().clear_selection), toggle_as_info: *cmd_items .get(&TuiCommandItem::ToggleASInfo) .or(cfg.toggle_as_info.as_ref()) .unwrap_or(&Self::default().toggle_as_info), toggle_hop_details: *cmd_items .get(&TuiCommandItem::ToggleHopDetails) .or(cfg.toggle_hop_details.as_ref()) .unwrap_or(&Self::default().toggle_hop_details), quit: *cmd_items .get(&TuiCommandItem::Quit) .or(cfg.quit.as_ref()) .unwrap_or(&Self::default().quit), quit_preserve_screen: *cmd_items .get(&TuiCommandItem::QuitPreserveScreen) .or(cfg.quit_preserve_screen.as_ref()) .unwrap_or(&Self::default().quit_preserve_screen), } } } /// Tui key binding. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize)] #[serde(try_from = "String")] pub struct TuiKeyBinding { pub code: KeyCode, pub modifier: KeyModifiers, } impl TuiKeyBinding { pub const fn new(code: KeyCode) -> Self { Self { code, modifier: KeyModifiers::NONE, } } pub const fn new_with_modifier(code: KeyCode, modifier: KeyModifiers) -> Self { Self { code, modifier } } } impl TryFrom for TuiKeyBinding { type Error = anyhow::Error; fn try_from(value: String) -> Result { Self::try_from(value.as_ref()) } } impl TryFrom<&str> for TuiKeyBinding { type Error = anyhow::Error; fn try_from(value: &str) -> Result { const ALL_MODIFIERS: [(&str, KeyModifiers); 6] = [ ("shift", KeyModifiers::SHIFT), ("ctrl", KeyModifiers::CONTROL), ("alt", KeyModifiers::ALT), ("super", KeyModifiers::SUPER), ("hyper", KeyModifiers::HYPER), ("meta", KeyModifiers::META), ]; const ALL_SPECIAL_KEYS: [(&str, KeyCode); 16] = [ ("backspace", KeyCode::Backspace), ("enter", KeyCode::Enter), ("left", KeyCode::Left), ("right", KeyCode::Right), ("up", KeyCode::Up), ("down", KeyCode::Down), ("home", KeyCode::Home), ("end", KeyCode::End), ("pageup", KeyCode::PageUp), ("pagedown", KeyCode::PageDown), ("tab", KeyCode::Tab), ("backtab", KeyCode::BackTab), ("delete", KeyCode::Delete), ("insert", KeyCode::Insert), ("null", KeyCode::Null), ("esc", KeyCode::Esc), ]; fn parse_keycode(value: &str) -> anyhow::Result { Ok(if value.len() == 1 { KeyCode::Char(char::from_str(value)?.to_ascii_lowercase()) } else { ALL_SPECIAL_KEYS .iter() .find_map(|(keycode_str, keycode)| { if keycode_str.eq_ignore_ascii_case(value) { Some(*keycode) } else { None } }) .ok_or_else(|| anyhow!("unknown key binding '{value}'"))? }) } fn parse_modifiers(modifiers: &str) -> anyhow::Result { modifiers .split('+') .try_fold(KeyModifiers::NONE, |modifiers, token| { ALL_MODIFIERS .iter() .find_map(|(modifier_token, modifier)| { if modifier_token.eq_ignore_ascii_case(token) { Some(modifiers | *modifier) } else { None } }) .ok_or_else(|| anyhow!("unknown modifier '{token}'",)) }) } match value.rsplit_once('+') { Some((modifiers, value)) => Ok(Self { code: parse_keycode(value)?, modifier: parse_modifiers(modifiers)?, }), None => Ok(Self { code: parse_keycode(value)?, modifier: KeyModifiers::NONE, }), } } } #[cfg(test)] mod binding_tests { use super::*; use test_case::test_case; #[test_case("c", KeyCode::Char('c'), KeyModifiers::NONE; "char without any modifier")] #[test_case("1", KeyCode::Char('1'), KeyModifiers::NONE; "number without any modifier")] #[test_case(",", KeyCode::Char(','), KeyModifiers::NONE; "punctuation without any modifier")] #[test_case("backspace", KeyCode::Backspace, KeyModifiers::NONE; "backspace without any modifier")] #[test_case("enter", KeyCode::Enter, KeyModifiers::NONE; "enter without any modifier")] #[test_case("left", KeyCode::Left, KeyModifiers::NONE; "left without any modifier")] #[test_case("right", KeyCode::Right, KeyModifiers::NONE; "right without any modifier")] #[test_case("up", KeyCode::Up, KeyModifiers::NONE; "up without any modifier")] #[test_case("down", KeyCode::Down, KeyModifiers::NONE; "down without any modifier")] #[test_case("home", KeyCode::Home, KeyModifiers::NONE; "home without any modifier")] #[test_case("end", KeyCode::End, KeyModifiers::NONE; "end without any modifier")] #[test_case("pageup", KeyCode::PageUp, KeyModifiers::NONE; "pageup without any modifier")] #[test_case("pagedown", KeyCode::PageDown, KeyModifiers::NONE; "pagedown without any modifier")] #[test_case("tab", KeyCode::Tab, KeyModifiers::NONE; "tab without any modifier")] #[test_case("backtab", KeyCode::BackTab, KeyModifiers::NONE; "backtab without any modifier")] #[test_case("delete", KeyCode::Delete, KeyModifiers::NONE; "delete without any modifier")] #[test_case("insert", KeyCode::Insert, KeyModifiers::NONE; "insert without any modifier")] #[test_case("null", KeyCode::Null, KeyModifiers::NONE; "null without any modifier")] #[test_case("esc", KeyCode::Esc, KeyModifiers::NONE; "escape without any modifier")] #[test_case("shift+c", KeyCode::Char('c'), KeyModifiers::SHIFT; "with shift modifier")] #[test_case("ctrl+i", KeyCode::Char('i'), KeyModifiers::CONTROL; "i with ctrl modifier")] #[test_case("shift+I", KeyCode::Char('i'), KeyModifiers::SHIFT; "I with shift modifier")] #[test_case("alt+c", KeyCode::Char('c'), KeyModifiers::ALT; "with alt modifier")] #[test_case("super+c", KeyCode::Char('c'), KeyModifiers::SUPER; "with super modifier")] #[test_case("hyper+c", KeyCode::Char('c'), KeyModifiers::HYPER; "with hyper modifier")] #[test_case("meta+c", KeyCode::Char('c'), KeyModifiers::META; "with meta modifier")] #[test_case("alt+shift+k", KeyCode::Char('k'), KeyModifiers::ALT | KeyModifiers::SHIFT; "with alt shift modifier")] #[test_case("ctrl+up", KeyCode::Up, KeyModifiers::CONTROL; "up with ctrl modifier")] #[test_case("shift+ctrl+alt+super+hyper+meta+k", KeyCode::Char('k'), KeyModifiers::all(); "with all modifiers")] fn test_key_binding(input: &str, code: KeyCode, modifiers: KeyModifiers) -> anyhow::Result<()> { let binding = TuiKeyBinding::try_from(input)?; assert_eq!(binding.code, code); assert_eq!(binding.modifier, modifiers); Ok(()) } #[test] fn test_unknown_modifier() { let binding = TuiKeyBinding::try_from("foo+c"); assert!(binding.is_err()); assert_eq!(&binding.unwrap_err().to_string(), "unknown modifier 'foo'"); } #[test] fn test_unknown_second_modifier() { let binding = TuiKeyBinding::try_from("alt+foo+c"); assert!(binding.is_err()); assert_eq!(&binding.unwrap_err().to_string(), "unknown modifier 'foo'"); } #[test] fn test_unknown_key() { let binding = TuiKeyBinding::try_from("foo"); assert!(binding.is_err()); assert_eq!( &binding.unwrap_err().to_string(), "unknown key binding 'foo'" ); } } impl Display for TuiKeyBinding { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.modifier.contains(KeyModifiers::SHIFT) { write!(f, "shift+")?; } if self.modifier.contains(KeyModifiers::CONTROL) { write!(f, "ctrl+")?; } if self.modifier.contains(KeyModifiers::ALT) { write!(f, "alt+")?; } if self.modifier.contains(KeyModifiers::SUPER) { write!(f, "super+")?; } if self.modifier.contains(KeyModifiers::HYPER) { write!(f, "hyper+")?; } if self.modifier.contains(KeyModifiers::META) { write!(f, "meta+")?; } match self.code { KeyCode::Backspace => write!(f, "backspace"), KeyCode::Enter => write!(f, "enter"), KeyCode::Left => write!(f, "left"), KeyCode::Right => write!(f, "right"), KeyCode::Up => write!(f, "up"), KeyCode::Down => write!(f, "down"), KeyCode::Home => write!(f, "home"), KeyCode::End => write!(f, "end"), KeyCode::PageUp => write!(f, "pageup"), KeyCode::PageDown => write!(f, "pagedown"), KeyCode::Tab => write!(f, "tab"), KeyCode::BackTab => write!(f, "backtab"), KeyCode::Delete => write!(f, "delete"), KeyCode::Insert => write!(f, "insert"), KeyCode::Char(c) => write!(f, "{c}"), KeyCode::Null => write!(f, "null"), KeyCode::Esc => write!(f, "esc"), _ => write!(f, "unknown"), } } } /// A Tui command that can be bound to a key. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, EnumString, VariantNames)] #[strum(serialize_all = "kebab-case")] #[derive(AsRefStr)] pub enum TuiCommandItem { /// Toggle the help dialog. ToggleHelp, /// Alternative command to toggle the help dialog. ToggleHelpAlt, /// Toggle the settings dialog. ToggleSettings, /// Toggle the TUI settings dialog tab. ToggleSettingsTui, /// Toggle the trace settings dialog tab. ToggleSettingsTrace, /// Toggle the DNS settings dialog tab. ToggleSettingsDns, /// Toggle the `GeoIp` settings dialog tab. ToggleSettingsGeoip, /// Toggle the bindings settings dialog tab. ToggleSettingsBindings, /// Toggle the theme settings dialog tab. ToggleSettingsTheme, /// Toggle the columns settings dialog tab. ToggleSettingsColumns, /// Move down to the next hop. NextHop, /// Move up to the previous hop. PreviousHop, /// Move right to the next trace. NextTrace, /// Move left to the previous trace. PreviousTrace, /// Move to the next hop address. NextHopAddress, /// Move to the previous hop address. PreviousHopAddress, /// Show IP address mode. AddressModeIp, /// Show hostname mode. AddressModeHost, /// Show hostname and IP address mode. AddressModeBoth, /// Toggle freezing the display. ToggleFreeze, /// Toggle the chart. ToggleChart, /// Toggle the map. ToggleMap, /// Toggle the flows panel. ToggleFlows, /// Toggle hop privacy mode. /// /// Deprecated: use `ExpandPrivacy` and `ContractPrivacy` instead. #[strum(serialize = "toggle-privacy")] DeprecatedTogglePrivacy, /// Expand hop privacy. ExpandPrivacy, /// Contract hop privacy. ContractPrivacy, /// Expand hosts. ExpandHosts, /// Expand hosts to max. ExpandHostsMax, /// Contract hosts. ContractHosts, /// Contract hosts to min. ContractHostsMin, /// Zoom chart in. ChartZoomIn, /// Zoom chart out. ChartZoomOut, /// Clear all tracing data. ClearTraceData, /// Clear DNS cache. ClearDnsCache, /// Clear hop selection. ClearSelection, /// Toggle autonomous system (AS) info. ToggleASInfo, /// Toggle hop details. ToggleHopDetails, /// Quit the application. Quit, /// Quit the application and preserve the screen. QuitPreserveScreen, } ================================================ FILE: crates/trippy-tui/src/config/cmd.rs ================================================ use crate::config::binding::TuiCommandItem; use crate::config::theme::TuiThemeItem; use crate::config::{ AddressFamilyConfig, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, IcmpExtensionMode, LogFormat, LogSpanEvents, Mode, MultipathStrategyConfig, ProtocolConfig, TuiColor, TuiKeyBinding, }; use anyhow::anyhow; use clap::Parser; use clap_complete::Shell; use std::net::IpAddr; use std::str::FromStr; use std::time::Duration; /// Trace a route to a host and record statistics #[expect(clippy::doc_markdown)] #[derive(Parser, Debug)] #[command(name = "trip", author, version, about, long_about = None, arg_required_else_help(true), styles=clap_cargo::style::CLAP_STYLING)] pub struct Args { /// A space delimited list of hostnames and IPs to trace #[arg(required_unless_present_any(["print_tui_theme_items", "print_tui_binding_commands", "print_config_template", "generate", "generate_man", "print_locales"]), env = "TRIP_TARGETS")] pub targets: Vec, /// Config file #[arg(value_enum, short = 'c', long, value_hint = clap::ValueHint::FilePath, env = "TRIP_CONFIG_FILE")] pub config_file: Option, /// Output mode [default: tui] #[arg(value_enum, short = 'm', long, env = "TRIP_MODE")] pub mode: Option, /// Trace without requiring elevated privileges on supported platforms [default: false] #[arg(short = 'u', long, env = "TRIP_UNPRIVILEGED")] pub unprivileged: bool, /// Tracing protocol [default: icmp] #[arg(value_enum, short = 'p', long, env = "TRIP_PROTOCOL")] pub protocol: Option, /// Trace using the UDP protocol #[arg( long, conflicts_with = "protocol", conflicts_with = "tcp", conflicts_with = "icmp", env = "TRIP_UDP" )] pub udp: bool, /// Trace using the TCP protocol #[arg( long, conflicts_with = "protocol", conflicts_with = "udp", conflicts_with = "icmp", env = "TRIP_TCP" )] pub tcp: bool, /// Trace using the ICMP protocol #[arg( long, conflicts_with = "protocol", conflicts_with = "udp", conflicts_with = "tcp", env = "TRIP_ICMP" )] pub icmp: bool, /// The address family [default: system] #[arg(value_enum, short = 'F', long, env = "TRIP_ADDR_FAMILY")] pub addr_family: Option, /// Use IPv4 only #[arg( short = '4', long, conflicts_with = "ipv6", conflicts_with = "addr_family", env = "TRIP_IPV4" )] pub ipv4: bool, /// Use IPv6 only #[arg( short = '6', long, conflicts_with = "ipv4", conflicts_with = "addr_family", env = "TRIP_IPV6" )] pub ipv6: bool, /// The target port (TCP & UDP only) [default: 80] #[arg(long, short = 'P', env = "TRIP_TARGET_PORT")] pub target_port: Option, /// The source port (TCP & UDP only) [default: auto] #[arg(long, short = 'S', env = "TRIP_SOURCE_PORT")] pub source_port: Option, /// The source IP address [default: auto] #[arg(short = 'A', long, value_parser = parse_addr, conflicts_with = "interface", env = "TRIP_SOURCE_ADDRESS")] pub source_address: Option, /// The network interface [default: auto] #[arg(short = 'I', long, env = "TRIP_INTERFACE")] pub interface: Option, /// The minimum duration of every round [default: 1s] #[arg(short = 'i', long, value_parser = parse_duration, env = "TRIP_MIN_ROUND_DURATION")] pub min_round_duration: Option, /// The maximum duration of every round [default: 1s] #[arg(short = 'T', long, value_parser = parse_duration, env = "TRIP_MAX_ROUND_DURATION")] pub max_round_duration: Option, /// The period of time to wait for additional ICMP responses after the target has responded /// [default: 100ms] #[arg(short = 'g', long, value_parser = parse_duration, env = "TRIP_GRACE_DURATION")] pub grace_duration: Option, /// The initial sequence number [default: 33434] #[arg(long, env = "TRIP_INITIAL_SEQUENCE")] pub initial_sequence: Option, /// The Equal-cost Multi-Path routing strategy (UDP only) [default: classic] #[arg(value_enum, short = 'R', long, env = "TRIP_MULTIPATH_STRATEGY")] pub multipath_strategy: Option, /// The maximum number of in-flight ICMP echo requests [default: 24] #[arg(short = 'U', long, env = "TRIP_MAX_INFLIGHT")] pub max_inflight: Option, /// The TTL to start from [default: 1] #[arg(short = 'f', long, env = "TRIP_FIRST_TTL")] pub first_ttl: Option, /// The maximum number of TTL hops [default: 64] #[arg(short = 't', long, env = "TRIP_MAX_TTL")] pub max_ttl: Option, /// The size of IP packet to send (IP header + ICMP header + payload) [default: 84] #[arg(long, env = "TRIP_PACKET_SIZE")] pub packet_size: Option, /// The repeating pattern in the payload of the ICMP packet [default: 0] #[arg(long, env = "TRIP_PAYLOAD_PATTERN")] pub payload_pattern: Option, /// The TOS (i.e. DSCP+ECN) IP header value (IPv4 only) [default: 0] #[arg(short = 'Q', long, env = "TRIP_TOS")] pub tos: Option, /// Parse ICMP extensions #[arg(short = 'e', long, env = "TRIP_ICMP_EXTENSIONS")] pub icmp_extensions: bool, /// The socket read timeout [default: 10ms] #[arg(long, value_parser = parse_duration, env = "TRIP_READ_TIMEOUT")] pub read_timeout: Option, /// How to perform DNS queries [default: system] #[arg(value_enum, short = 'r', long, env = "TRIP_DNS_RESOLVE_METHOD")] pub dns_resolve_method: Option, /// Trace to all IPs resolved from DNS lookup [default: false] #[arg(short = 'y', long, env = "TRIP_DNS_RESOLVE_ALL")] pub dns_resolve_all: bool, /// The maximum time to wait to perform DNS queries [default: 5s] #[arg(long, value_parser = parse_duration, env = "TRIP_DNS_TIMEOUT")] pub dns_timeout: Option, /// The time-to-live (TTL) of DNS entries [default: 300s] #[arg(long, value_parser = parse_duration, env = "TRIP_DNS_TTL")] pub dns_ttl: Option, /// Lookup autonomous system (AS) information during DNS queries [default: false] #[arg(long, short = 'z', env = "TRIP_DNS_LOOKUP_AS_INFO")] pub dns_lookup_as_info: bool, /// The maximum number of samples to record per hop [default: 256] #[arg(long, short = 's', env = "TRIP_MAX_SAMPLES")] pub max_samples: Option, /// The maximum number of flows to record [default: 64] #[arg(long, env = "TRIP_MAX_FLOWS")] pub max_flows: Option, /// How to render addresses [default: host] #[arg(value_enum, short = 'a', long, env = "TRIP_TUI_ADDRESS_MODE")] pub tui_address_mode: Option, /// How to render autonomous system (AS) information [default: asn] #[arg(value_enum, long, env = "TRIP_TUI_AS_MODE")] pub tui_as_mode: Option, /// Custom columns to be displayed in the TUI hops table [default: holsravbwdt] #[arg(long, env = "TRIP_TUI_CUSTOM_COLUMNS")] pub tui_custom_columns: Option, /// How to render ICMP extensions [default: off] #[arg(value_enum, long, env = "TRIP_TUI_ICMP_EXTENSION_MODE")] pub tui_icmp_extension_mode: Option, /// How to render GeoIp information [default: short] #[arg(value_enum, long, env = "TRIP_TUI_GEOIP_MODE")] pub tui_geoip_mode: Option, /// The maximum number of addresses to show per hop [default: auto] #[arg(short = 'M', long, env = "TRIP_TUI_MAX_ADDRS")] pub tui_max_addrs: Option, /// Preserve the screen on exit [default: false] #[arg(long, env = "TRIP_TUI_PRESERVE_SCREEN")] pub tui_preserve_screen: bool, /// The TUI refresh rate [default: 100ms] #[arg(long, value_parser = parse_duration, env = "TRIP_TUI_REFRESH_RATE")] pub tui_refresh_rate: Option, /// The maximum ttl of hops which will be masked for privacy [default: none] /// /// If set, the source IP address and hostname will also be hidden. #[arg(long, env = "TRIP_TUI_PRIVACY_MAX_TTL")] pub tui_privacy_max_ttl: Option, /// The locale to use for the TUI [default: auto] #[arg(long, env = "TRIP_TUI_LOCALE")] pub tui_locale: Option, /// The timezone to use for the TUI [default: auto] /// /// The timezone must be a valid IANA timezone identifier. #[arg(long, env = "TRIP_TUI_TIMEZONE")] pub tui_timezone: Option, /// The TUI theme colors [item=color,item=color,..] #[arg(long, value_delimiter(','), value_parser = parse_tui_theme_color_value, env = "TRIP_TUI_THEME_COLORS")] pub tui_theme_colors: Vec<(TuiThemeItem, TuiColor)>, /// Print all TUI theme items and exit #[arg(long, env = "TRIP_PRINT_TUI_THEME_ITEMS")] pub print_tui_theme_items: bool, /// The TUI key bindings [command=key,command=key,..] #[arg(long, value_delimiter(','), value_parser = parse_tui_binding_value, env = "TRIP_TUI_KEY_BINDINGS")] pub tui_key_bindings: Vec<(TuiCommandItem, TuiKeyBinding)>, /// Print all TUI commands that can be bound and exit #[arg(long, env = "TRIP_PRINT_TUI_BINDING_COMMANDS")] pub print_tui_binding_commands: bool, /// The number of report cycles to run [default: 10] #[arg(short = 'C', long, env = "TRIP_REPORT_CYCLES")] pub report_cycles: Option, /// The supported MaxMind or IPinfo GeoIp mmdb file #[arg(short = 'G', long, value_hint = clap::ValueHint::FilePath, env = "TRIP_GEOIP_MMDB_FILE")] pub geoip_mmdb_file: Option, /// Generate shell completion #[arg(long, env = "TRIP_GENERATE")] pub generate: Option, /// Generate ROFF man page #[arg(long, env = "TRIP_GENERATE_MAN")] pub generate_man: bool, /// Print a template toml config file and exit #[arg(long, env = "TRIP_PRINT_CONFIG_TEMPLATE")] pub print_config_template: bool, /// Print all available TUI locales and exit #[arg(long, env = "TRIP_PRINT_LOCALES")] pub print_locales: bool, /// The debug log format [default: pretty] #[arg(long, env = "TRIP_LOG_FORMAT")] pub log_format: Option, /// The debug log filter [default: trippy=debug] #[arg(long, env = "TRIP_LOG_FILTER")] pub log_filter: Option, /// The debug log format [default: off] #[arg(long, env = "TRIP_LOG_SPAN_EVENTS")] pub log_span_events: Option, /// Enable verbose debug logging #[arg(short = 'v', long, default_value_t = false, env = "TRIP_VERBOSE")] pub verbose: bool, } fn parse_tui_theme_color_value(value: &str) -> anyhow::Result<(TuiThemeItem, TuiColor)> { let pos = value .find('=') .ok_or_else(|| anyhow!("invalid theme value: expected format `item=value`"))?; let item = TuiThemeItem::try_from(&value[..pos])?; let color = TuiColor::try_from(&value[pos + 1..])?; Ok((item, color)) } fn parse_tui_binding_value(value: &str) -> anyhow::Result<(TuiCommandItem, TuiKeyBinding)> { let pos = value .find('=') .ok_or_else(|| anyhow!("invalid binding value: expected format `item=value`"))?; let item = TuiCommandItem::try_from(&value[..pos])?; let binding = TuiKeyBinding::try_from(&value[pos + 1..])?; if item == TuiCommandItem::DeprecatedTogglePrivacy { return Err(anyhow!( "toggle-privacy is deprecated, use expand-privacy and contract-privacy instead" )); } Ok((item, binding)) } fn parse_duration(value: &str) -> anyhow::Result { Ok(humantime::parse_duration(value)?) } fn parse_addr(value: &str) -> anyhow::Result { Ok(IpAddr::from_str(value)?) } ================================================ FILE: crates/trippy-tui/src/config/columns.rs ================================================ use anyhow::anyhow; use itertools::Itertools; use std::collections::HashSet; use std::fmt::{Display, Formatter}; /// The columns to display in the hops table of the TUI. #[derive(Debug, Clone, Eq, PartialEq)] pub struct TuiColumns(pub Vec); impl TryFrom<&str> for TuiColumns { type Error = anyhow::Error; fn try_from(value: &str) -> Result { Ok(Self( value .chars() .map(TuiColumn::try_from) .collect::, Self::Error>>()?, )) } } impl Default for TuiColumns { fn default() -> Self { Self::try_from(super::constants::DEFAULT_CUSTOM_COLUMNS).expect("custom columns") } } impl TuiColumns { /// Validate the columns. /// /// Returns any duplicate columns. pub fn find_duplicates(&self) -> Vec { let (_, duplicates) = self.0.iter().fold( (HashSet::::new(), Vec::new()), |(mut all, mut dups), column| { if all.iter().contains(column) { dups.push(column.to_string()); } else { all.insert(*column); } (all, dups) }, ); duplicates } } /// A TUI hops table column. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum TuiColumn { /// The ttl for a hop. Ttl, /// The hostname for a hostname. Host, /// The packet loss % for a hop. LossPct, /// The number of probes sent for a hop. Sent, /// The number of responses received for a hop. Received, /// The last RTT for a hop. Last, /// The rolling average RTT for a hop. Average, /// The best RTT for a hop. Best, /// The worst RTT for a hop. Worst, /// The stddev of RTT for a hop. StdDev, /// The status of a hop. Status, /// The current jitter i.e. round-trip difference with the last round-trip. Jitter, /// The average jitter time for all probes at this hop. Javg, /// The worst round-trip jitter time for all probes at this hop. Jmax, /// The smoothed jitter value for all probes at this hop. Jinta, /// The source port for last probe for this hop. LastSrcPort, /// The destination port for last probe for this hop. LastDestPort, /// The sequence number for the last probe for this hop. LastSeq, /// The icmp packet type for the last probe for this hop. LastIcmpPacketType, /// The icmp packet code for the last probe for this hop. LastIcmpPacketCode, /// The NAT detection status for the last probe for this hop. LastNatStatus, /// The number of probes that failed for a hop. Failed, /// The number of probes with forward loss for a hop. Floss, /// The number of probes with backward loss for a hop. Bloss, /// The forward loss % for a hop. FlossPct, /// The Differentiated Services Code Point of the Original Datagram for a hop. Dscp, /// The Explicit Congestion Notification of the Original Datagram for a hop. Ecn, /// The autonomous system number for a hop. Asn, } impl TryFrom for TuiColumn { type Error = anyhow::Error; fn try_from(value: char) -> Result { match value { 'h' => Ok(Self::Ttl), 'o' => Ok(Self::Host), 'l' => Ok(Self::LossPct), 's' => Ok(Self::Sent), 'r' => Ok(Self::Received), 'a' => Ok(Self::Last), 'v' => Ok(Self::Average), 'b' => Ok(Self::Best), 'w' => Ok(Self::Worst), 'd' => Ok(Self::StdDev), 't' => Ok(Self::Status), 'j' => Ok(Self::Jitter), 'g' => Ok(Self::Javg), 'x' => Ok(Self::Jmax), 'i' => Ok(Self::Jinta), 'S' => Ok(Self::LastSrcPort), 'P' => Ok(Self::LastDestPort), 'Q' => Ok(Self::LastSeq), 'T' => Ok(Self::LastIcmpPacketType), 'C' => Ok(Self::LastIcmpPacketCode), 'N' => Ok(Self::LastNatStatus), 'f' => Ok(Self::Failed), 'F' => Ok(Self::Floss), 'B' => Ok(Self::Bloss), 'D' => Ok(Self::FlossPct), 'K' => Ok(Self::Dscp), 'M' => Ok(Self::Ecn), 'A' => Ok(Self::Asn), c => Err(anyhow!(format!("unknown column code: {c}"))), } } } impl Display for TuiColumn { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Ttl => write!(f, "h"), Self::Host => write!(f, "o"), Self::LossPct => write!(f, "l"), Self::Sent => write!(f, "s"), Self::Received => write!(f, "r"), Self::Last => write!(f, "a"), Self::Average => write!(f, "v"), Self::Best => write!(f, "b"), Self::Worst => write!(f, "w"), Self::StdDev => write!(f, "d"), Self::Status => write!(f, "t"), Self::Jitter => write!(f, "j"), Self::Javg => write!(f, "g"), Self::Jmax => write!(f, "x"), Self::Jinta => write!(f, "i"), Self::LastSrcPort => write!(f, "S"), Self::LastDestPort => write!(f, "P"), Self::LastSeq => write!(f, "Q"), Self::LastIcmpPacketType => write!(f, "T"), Self::LastIcmpPacketCode => write!(f, "C"), Self::LastNatStatus => write!(f, "N"), Self::Failed => write!(f, "f"), Self::Floss => write!(f, "F"), Self::Bloss => write!(f, "B"), Self::FlossPct => write!(f, "D"), Self::Dscp => write!(f, "K"), Self::Ecn => write!(f, "M"), Self::Asn => write!(f, "A"), } } } #[cfg(test)] mod tests { use super::*; use test_case::test_case; ///Test for expected column matches to characters #[test_case('h', TuiColumn::Ttl)] #[test_case('o', TuiColumn::Host)] #[test_case('l', TuiColumn::LossPct)] #[test_case('s', TuiColumn::Sent)] #[test_case('r', TuiColumn::Received)] #[test_case('a', TuiColumn::Last)] #[test_case('v', TuiColumn::Average)] #[test_case('b', TuiColumn::Best)] #[test_case('w', TuiColumn::Worst)] #[test_case('d', TuiColumn::StdDev)] #[test_case('t', TuiColumn::Status)] #[test_case('A', TuiColumn::Asn)] fn test_try_from_char_for_tui_column(c: char, t: TuiColumn) { assert_eq!(TuiColumn::try_from(c).unwrap(), t); } ///Negative test for invalid characters #[test_case('k' ; "invalid k")] #[test_case('z' ; "invalid z")] fn test_try_invalid_char_for_tui_column(c: char) { // Negative test for an unknown character assert!(TuiColumn::try_from(c).is_err()); } ///Test for `TuiColumn` type match of Display #[test_case(TuiColumn::Ttl, "h")] #[test_case(TuiColumn::Host, "o")] #[test_case(TuiColumn::LossPct, "l")] #[test_case(TuiColumn::Sent, "s")] #[test_case(TuiColumn::Received, "r")] #[test_case(TuiColumn::Last, "a")] #[test_case(TuiColumn::Average, "v")] #[test_case(TuiColumn::Best, "b")] #[test_case(TuiColumn::Worst, "w")] #[test_case(TuiColumn::StdDev, "d")] #[test_case(TuiColumn::Status, "t")] #[test_case(TuiColumn::Asn, "A")] fn test_display_formatting_for_tui_column(t: TuiColumn, letter: &'static str) { assert_eq!(format!("{t}"), letter); } #[test] fn test_try_from_str_for_tui_columns() { let valid_input = "hol"; let tui_columns = TuiColumns::try_from(valid_input).unwrap(); assert_eq!( tui_columns, TuiColumns(vec![TuiColumn::Ttl, TuiColumn::Host, TuiColumn::LossPct]) ); // Test for invalid characters in the input let invalid_input = "xyz"; assert!(TuiColumns::try_from(invalid_input).is_err()); } #[test] fn test_default_for_tui_columns() { let default_columns = TuiColumns::default(); assert_eq!( default_columns, TuiColumns(vec![ TuiColumn::Ttl, TuiColumn::Host, TuiColumn::LossPct, TuiColumn::Sent, TuiColumn::Received, TuiColumn::Last, TuiColumn::Average, TuiColumn::Best, TuiColumn::Worst, TuiColumn::StdDev, TuiColumn::Status ]) ); } #[test] fn test_find_duplicates_for_tui_columns() { let columns_with_duplicates = TuiColumns(vec![ TuiColumn::Ttl, TuiColumn::Host, TuiColumn::LossPct, TuiColumn::Host, // Duplicate ]); let duplicates = columns_with_duplicates.find_duplicates(); assert_eq!(duplicates, vec!["o".to_string()]); } } ================================================ FILE: crates/trippy-tui/src/config/constants.rs ================================================ use crate::config::{ AddressFamilyConfig, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, IcmpExtensionMode, LogFormat, LogSpanEvents, Mode, }; use std::time::Duration; /// The default value for `mode`. pub const DEFAULT_MODE: Mode = Mode::Tui; /// The default value for `dns-resolve-all`. pub const DEFAULT_DNS_RESOLVE_ALL: bool = false; /// The default value for `log-format`. pub const DEFAULT_LOG_FORMAT: LogFormat = LogFormat::Pretty; /// The default value for `log-span-events`. pub const DEFAULT_LOG_SPAN_EVENTS: LogSpanEvents = LogSpanEvents::Off; /// The default value for `log-filter`. pub const DEFAULT_LOG_FILTER: &str = "trippy=debug"; /// The default value for `tui-preserve-screen`. pub const DEFAULT_TUI_PRESERVE_SCREEN: bool = false; /// The default value for `tui-as-mode`. pub const DEFAULT_TUI_AS_MODE: AsMode = AsMode::Asn; /// The default value for `tui-custom-columns`. pub const DEFAULT_CUSTOM_COLUMNS: &str = "holsravbwdt"; /// The default value for `tui-icmp-extension-mode`. pub const DEFAULT_TUI_ICMP_EXTENSION_MODE: IcmpExtensionMode = IcmpExtensionMode::Off; /// The default value for `tui-geoip-mode`. pub const DEFAULT_TUI_GEOIP_MODE: GeoIpMode = GeoIpMode::Off; /// The default value for `tui-max-addrs`. pub const DEFAULT_TUI_MAX_ADDRS: u8 = 0; /// The default value for `tui-address-mode`. pub const DEFAULT_TUI_ADDRESS_MODE: AddressMode = AddressMode::Host; /// The default value for `tui-refresh-rate`. pub const DEFAULT_TUI_REFRESH_RATE: Duration = Duration::from_millis(100); /// The default value for `dns-resolve-method`. pub const DEFAULT_DNS_RESOLVE_METHOD: DnsResolveMethodConfig = DnsResolveMethodConfig::System; /// The default value for `addr-family`. pub const DEFAULT_ADDR_FAMILY: AddressFamilyConfig = AddressFamilyConfig::System; /// The default value for `dns-lookup-as-info`. pub const DEFAULT_DNS_LOOKUP_AS_INFO: bool = false; /// The default value for `dns-timeout`. pub const DEFAULT_DNS_TIMEOUT: Duration = Duration::from_millis(5000); /// The default value for `dns-ttl`. pub const DEFAULT_DNS_TTL: Duration = Duration::from_secs(300); /// The default value for `report-cycles`. pub const DEFAULT_REPORT_CYCLES: usize = 10; /// The minimum TUI refresh rate. pub const TUI_MIN_REFRESH_RATE_MS: Duration = Duration::from_millis(50); /// The maximum TUI refresh rate. pub const TUI_MAX_REFRESH_RATE_MS: Duration = Duration::from_millis(1000); /// The minimum socket read timeout. pub const MIN_READ_TIMEOUT_MS: Duration = Duration::from_millis(10); /// The maximum socket read timeout. pub const MAX_READ_TIMEOUT_MS: Duration = Duration::from_millis(100); /// The minimum grace duration. pub const MIN_GRACE_DURATION_MS: Duration = Duration::from_millis(10); /// The maximum grace duration. pub const MAX_GRACE_DURATION_MS: Duration = Duration::from_millis(1000); /// The minimum IPv4 packet size we allow. pub const MIN_PACKET_SIZE_IPV4: u16 = 28; /// The minimum IPv6 packet size we allow. pub const MIN_PACKET_SIZE_IPV6: u16 = 48; /// The maximum packet size we allow. pub const MAX_PACKET_SIZE: u16 = 1024; ================================================ FILE: crates/trippy-tui/src/config/file.rs ================================================ use crate::config::binding::TuiKeyBinding; use crate::config::theme::TuiColor; use crate::config::{ AddressFamilyConfig, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, IcmpExtensionMode, LogFormat, LogSpanEvents, Mode, MultipathStrategyConfig, ProtocolConfig, }; use anyhow::Context; use encoding_rs_io::DecodeReaderBytes; use etcetera::BaseStrategy; use serde::Deserialize; use std::fs::File; use std::io::{BufReader, Read}; use std::net::IpAddr; use std::path::Path; use std::str::FromStr; use std::time::Duration; use trippy_core::defaults; const DEFAULT_CONFIG_FILE: &str = "trippy.toml"; const DEFAULT_HIDDEN_CONFIG_FILE: &str = ".trippy.toml"; /// Read the config from the default location of user config for the platform. /// /// Returns the parsed `Some(ConfigFile)` if the config file exists, `None` otherwise. /// /// Trippy will attempt to locate a `trippy.toml` or `.trippy.toml` /// config file in one of the following locations: /// - the current directory /// - the user home directory /// - the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config` /// - the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy` /// - the Windows data directory (Windows only): `%APPDATA%` /// /// Note that only the first config file found is used, no attempt is /// made to merge the values from multiple files. pub fn read_default_config_file() -> anyhow::Result> { use etcetera::base_strategy as base; if let Some(file) = read_files("")? { Ok(Some(file)) } else { let basedirs = base::choose_base_strategy()?; if let Some(file) = read_files(basedirs.home_dir())? { Ok(Some(file)) } else if let Some(file) = read_files(basedirs.config_dir())? { Ok(Some(file)) } else if let Some(file) = read_files(basedirs.config_dir().join("trippy"))? { Ok(Some(file)) } else { Ok(None) } } } /// Read the config from the given path. pub fn read_config_file>(path: P) -> anyhow::Result { let file = File::open(path.as_ref()) .with_context(|| format!("config file not found: {}", path.as_ref().display()))?; let mut decoder = DecodeReaderBytes::new(BufReader::new(file)); let mut dest = String::new(); decoder.read_to_string(&mut dest)?; Ok(toml::from_str(&dest)?) } fn read_files>(dir: P) -> anyhow::Result> { if let Some(file) = read_file(dir.as_ref(), DEFAULT_CONFIG_FILE)? { Ok(Some(file)) } else if let Some(file) = read_file(dir.as_ref(), DEFAULT_HIDDEN_CONFIG_FILE)? { Ok(Some(file)) } else { Ok(None) } } fn read_file>(dir: P, file: &str) -> anyhow::Result> { let path = dir.as_ref().join(file); if path.exists() { Ok(Some(read_config_file(path)?)) } else { Ok(None) } } #[derive(Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ConfigFile { pub trippy: Option, pub strategy: Option, pub theme_colors: Option, pub bindings: Option, pub tui: Option, pub dns: Option, pub report: Option, } impl Default for ConfigFile { fn default() -> Self { Self { trippy: Some(ConfigTrippy::default()), strategy: Some(ConfigStrategy::default()), theme_colors: Some(ConfigThemeColors::default()), bindings: Some(ConfigBindings::default()), tui: Some(ConfigTui::default()), dns: Some(ConfigDns::default()), report: Some(ConfigReport::default()), } } } #[derive(Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ConfigTrippy { pub mode: Option, pub unprivileged: Option, pub log_format: Option, pub log_filter: Option, pub log_span_events: Option, } impl Default for ConfigTrippy { fn default() -> Self { Self { mode: Some(super::constants::DEFAULT_MODE), unprivileged: Some(defaults::DEFAULT_PRIVILEGE_MODE.is_unprivileged()), log_format: Some(super::constants::DEFAULT_LOG_FORMAT), log_filter: Some(String::from(super::constants::DEFAULT_LOG_FILTER)), log_span_events: Some(super::constants::DEFAULT_LOG_SPAN_EVENTS), } } } #[derive(Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ConfigStrategy { pub protocol: Option, pub addr_family: Option, pub target_port: Option, pub source_port: Option, #[serde(default)] #[serde(deserialize_with = "addr_deser")] pub source_address: Option, pub interface: Option, #[serde(default)] #[serde(deserialize_with = "humantime_deser")] pub min_round_duration: Option, #[serde(default)] #[serde(deserialize_with = "humantime_deser")] pub max_round_duration: Option, pub initial_sequence: Option, pub multipath_strategy: Option, #[serde(default)] #[serde(deserialize_with = "humantime_deser")] pub grace_duration: Option, pub max_inflight: Option, pub first_ttl: Option, pub max_ttl: Option, pub packet_size: Option, pub payload_pattern: Option, pub tos: Option, pub icmp_extensions: Option, #[serde(default)] #[serde(deserialize_with = "humantime_deser")] pub read_timeout: Option, pub max_samples: Option, pub max_flows: Option, } impl Default for ConfigStrategy { fn default() -> Self { Self { protocol: Some(ProtocolConfig::from(defaults::DEFAULT_STRATEGY_PROTOCOL)), addr_family: Some(super::constants::DEFAULT_ADDR_FAMILY), target_port: None, source_port: None, source_address: None, interface: None, min_round_duration: Some(defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION), max_round_duration: Some(defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION), initial_sequence: Some(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE), multipath_strategy: Some(MultipathStrategyConfig::from( defaults::DEFAULT_STRATEGY_MULTIPATH, )), grace_duration: Some(defaults::DEFAULT_STRATEGY_GRACE_DURATION), max_inflight: Some(defaults::DEFAULT_STRATEGY_MAX_INFLIGHT), first_ttl: Some(defaults::DEFAULT_STRATEGY_FIRST_TTL), max_ttl: Some(defaults::DEFAULT_STRATEGY_MAX_TTL), packet_size: Some(defaults::DEFAULT_STRATEGY_PACKET_SIZE), payload_pattern: Some(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN), tos: Some(defaults::DEFAULT_STRATEGY_TOS), icmp_extensions: Some(defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE.is_enabled()), read_timeout: Some(defaults::DEFAULT_STRATEGY_READ_TIMEOUT), max_samples: Some(defaults::DEFAULT_MAX_SAMPLES), max_flows: Some(defaults::DEFAULT_MAX_FLOWS), } } } #[derive(Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[expect(clippy::struct_field_names)] pub struct ConfigDns { pub dns_resolve_method: Option, pub dns_resolve_all: Option, pub dns_lookup_as_info: Option, #[serde(default)] #[serde(deserialize_with = "humantime_deser")] pub dns_timeout: Option, #[serde(default)] #[serde(deserialize_with = "humantime_deser")] pub dns_ttl: Option, } impl Default for ConfigDns { fn default() -> Self { Self { dns_resolve_method: Some(super::constants::DEFAULT_DNS_RESOLVE_METHOD), dns_resolve_all: Some(super::constants::DEFAULT_DNS_RESOLVE_ALL), dns_lookup_as_info: Some(super::constants::DEFAULT_DNS_LOOKUP_AS_INFO), dns_timeout: Some(super::constants::DEFAULT_DNS_TIMEOUT), dns_ttl: Some(super::constants::DEFAULT_DNS_TTL), } } } #[derive(Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ConfigReport { pub report_cycles: Option, } impl Default for ConfigReport { fn default() -> Self { Self { report_cycles: Some(super::constants::DEFAULT_REPORT_CYCLES), } } } #[derive(Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ConfigTui { pub tui_preserve_screen: Option, #[serde(default)] #[serde(deserialize_with = "humantime_deser")] pub tui_refresh_rate: Option, pub tui_privacy_max_ttl: Option, pub tui_address_mode: Option, pub tui_as_mode: Option, pub tui_icmp_extension_mode: Option, pub tui_geoip_mode: Option, pub tui_max_addrs: Option, pub geoip_mmdb_file: Option, pub tui_custom_columns: Option, pub tui_locale: Option, pub tui_timezone: Option, #[serde(rename = "tui-max-samples")] pub deprecated_tui_max_samples: Option, #[serde(rename = "tui-max-flows")] pub deprecated_tui_max_flows: Option, } impl Default for ConfigTui { fn default() -> Self { Self { tui_preserve_screen: Some(super::constants::DEFAULT_TUI_PRESERVE_SCREEN), tui_refresh_rate: Some(super::constants::DEFAULT_TUI_REFRESH_RATE), tui_privacy_max_ttl: None, tui_address_mode: Some(super::constants::DEFAULT_TUI_ADDRESS_MODE), tui_as_mode: Some(super::constants::DEFAULT_TUI_AS_MODE), tui_custom_columns: Some(String::from(super::constants::DEFAULT_CUSTOM_COLUMNS)), tui_icmp_extension_mode: Some(super::constants::DEFAULT_TUI_ICMP_EXTENSION_MODE), tui_geoip_mode: Some(super::constants::DEFAULT_TUI_GEOIP_MODE), tui_max_addrs: Some(super::constants::DEFAULT_TUI_MAX_ADDRS), tui_locale: None, tui_timezone: None, geoip_mmdb_file: None, deprecated_tui_max_samples: None, deprecated_tui_max_flows: None, } } } #[derive(Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] #[expect(clippy::struct_field_names)] pub struct ConfigThemeColors { pub bg_color: Option, pub border_color: Option, pub text_color: Option, pub tab_text_color: Option, pub hops_table_header_bg_color: Option, pub hops_table_header_text_color: Option, pub hops_table_row_active_text_color: Option, pub hops_table_row_inactive_text_color: Option, pub hops_chart_selected_color: Option, pub hops_chart_unselected_color: Option, pub hops_chart_axis_color: Option, pub frequency_chart_bar_color: Option, pub frequency_chart_text_color: Option, pub flows_chart_bar_selected_color: Option, pub flows_chart_bar_unselected_color: Option, pub flows_chart_text_current_color: Option, pub flows_chart_text_non_current_color: Option, pub samples_chart_color: Option, pub samples_chart_lost_color: Option, pub help_dialog_bg_color: Option, pub help_dialog_text_color: Option, pub settings_dialog_bg_color: Option, pub settings_tab_text_color: Option, pub settings_table_header_text_color: Option, pub settings_table_header_bg_color: Option, pub settings_table_row_text_color: Option, pub map_world_color: Option, pub map_radius_color: Option, pub map_selected_color: Option, pub map_info_panel_border_color: Option, pub map_info_panel_bg_color: Option, pub map_info_panel_text_color: Option, pub info_bar_bg_color: Option, pub info_bar_text_color: Option, } impl Default for ConfigThemeColors { fn default() -> Self { let theme = super::theme::TuiTheme::default(); Self { bg_color: Some(theme.bg), border_color: Some(theme.border), text_color: Some(theme.text), tab_text_color: Some(theme.tab_text), hops_table_header_bg_color: Some(theme.hops_table_header_bg), hops_table_header_text_color: Some(theme.hops_table_header_text), hops_table_row_active_text_color: Some(theme.hops_table_row_active_text), hops_table_row_inactive_text_color: Some(theme.hops_table_row_inactive_text), hops_chart_selected_color: Some(theme.hops_chart_selected), hops_chart_unselected_color: Some(theme.hops_chart_unselected), hops_chart_axis_color: Some(theme.hops_chart_axis), frequency_chart_bar_color: Some(theme.frequency_chart_bar), frequency_chart_text_color: Some(theme.frequency_chart_text), flows_chart_bar_selected_color: Some(theme.flows_chart_bar_selected), flows_chart_bar_unselected_color: Some(theme.flows_chart_bar_unselected), flows_chart_text_current_color: Some(theme.flows_chart_text_current), flows_chart_text_non_current_color: Some(theme.flows_chart_text_non_current), samples_chart_color: Some(theme.samples_chart), samples_chart_lost_color: Some(theme.samples_chart_lost), help_dialog_bg_color: Some(theme.help_dialog_bg), help_dialog_text_color: Some(theme.help_dialog_text), settings_dialog_bg_color: Some(theme.settings_dialog_bg), settings_tab_text_color: Some(theme.settings_tab_text), settings_table_header_text_color: Some(theme.settings_table_header_text), settings_table_header_bg_color: Some(theme.settings_table_header_bg), settings_table_row_text_color: Some(theme.settings_table_row_text), map_world_color: Some(theme.map_world), map_radius_color: Some(theme.map_radius), map_selected_color: Some(theme.map_selected), map_info_panel_border_color: Some(theme.map_info_panel_border), map_info_panel_bg_color: Some(theme.map_info_panel_bg), map_info_panel_text_color: Some(theme.map_info_panel_text), info_bar_bg_color: Some(theme.info_bar_bg), info_bar_text_color: Some(theme.info_bar_text), } } } #[derive(Debug, Eq, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ConfigBindings { pub toggle_help: Option, pub toggle_help_alt: Option, pub toggle_settings: Option, pub toggle_settings_tui: Option, pub toggle_settings_trace: Option, pub toggle_settings_dns: Option, pub toggle_settings_geoip: Option, pub toggle_settings_bindings: Option, pub toggle_settings_theme: Option, pub toggle_settings_columns: Option, pub previous_hop: Option, pub next_hop: Option, pub previous_trace: Option, pub next_trace: Option, pub previous_hop_address: Option, pub next_hop_address: Option, pub address_mode_ip: Option, pub address_mode_host: Option, pub address_mode_both: Option, pub toggle_freeze: Option, pub toggle_chart: Option, pub toggle_flows: Option, #[serde(rename = "toggle-privacy")] pub deprecated_toggle_privacy: Option, pub expand_privacy: Option, pub contract_privacy: Option, pub toggle_map: Option, pub expand_hosts: Option, pub contract_hosts: Option, pub expand_hosts_max: Option, pub contract_hosts_min: Option, pub chart_zoom_in: Option, pub chart_zoom_out: Option, pub clear_trace_data: Option, pub clear_dns_cache: Option, pub clear_selection: Option, pub toggle_as_info: Option, pub toggle_hop_details: Option, pub quit: Option, pub quit_preserve_screen: Option, } impl Default for ConfigBindings { fn default() -> Self { let bindings = super::binding::TuiBindings::default(); Self { toggle_help: Some(bindings.toggle_help), toggle_help_alt: Some(bindings.toggle_help_alt), toggle_settings: Some(bindings.toggle_settings), toggle_settings_tui: Some(bindings.toggle_settings_tui), toggle_settings_trace: Some(bindings.toggle_settings_trace), toggle_settings_dns: Some(bindings.toggle_settings_dns), toggle_settings_geoip: Some(bindings.toggle_settings_geoip), toggle_settings_bindings: Some(bindings.toggle_settings_bindings), toggle_settings_theme: Some(bindings.toggle_settings_theme), toggle_settings_columns: Some(bindings.toggle_settings_columns), previous_hop: Some(bindings.previous_hop), next_hop: Some(bindings.next_hop), previous_trace: Some(bindings.previous_trace), next_trace: Some(bindings.next_trace), previous_hop_address: Some(bindings.previous_hop_address), next_hop_address: Some(bindings.next_hop_address), address_mode_ip: Some(bindings.address_mode_ip), address_mode_host: Some(bindings.address_mode_host), address_mode_both: Some(bindings.address_mode_both), toggle_freeze: Some(bindings.toggle_freeze), toggle_chart: Some(bindings.toggle_chart), toggle_flows: Some(bindings.toggle_flows), deprecated_toggle_privacy: None, expand_privacy: Some(bindings.expand_privacy), contract_privacy: Some(bindings.contract_privacy), toggle_map: Some(bindings.toggle_map), expand_hosts: Some(bindings.expand_hosts), contract_hosts: Some(bindings.contract_hosts), expand_hosts_max: Some(bindings.expand_hosts_max), contract_hosts_min: Some(bindings.contract_hosts_min), chart_zoom_in: Some(bindings.chart_zoom_in), chart_zoom_out: Some(bindings.chart_zoom_out), clear_trace_data: Some(bindings.clear_trace_data), clear_dns_cache: Some(bindings.clear_dns_cache), clear_selection: Some(bindings.clear_selection), toggle_as_info: Some(bindings.toggle_as_info), toggle_hop_details: Some(bindings.toggle_hop_details), quit: Some(bindings.quit), quit_preserve_screen: Some(bindings.quit_preserve_screen), } } } fn humantime_deser<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { humantime::parse_duration(&String::deserialize(deserializer)?) .map_err(serde::de::Error::custom) .map(Some) } fn addr_deser<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { IpAddr::from_str(&String::deserialize(deserializer)?) .map_err(serde::de::Error::custom) .map(Some) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_config_sample() { let config: ConfigFile = toml::from_str(include_str!("../../trippy-config-sample.toml")).unwrap(); pretty_assertions::assert_eq!(ConfigFile::default(), config); } } ================================================ FILE: crates/trippy-tui/src/config/theme.rs ================================================ use crate::config::file::ConfigThemeColors; use anyhow::anyhow; use serde::Deserialize; use std::collections::HashMap; use strum::{EnumString, VariantNames}; /// Tui color theme. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct TuiTheme { /// The default background color. /// /// This may be overridden for specific components. pub bg: TuiColor, /// The default color of borders. /// /// This may be overridden for specific components. pub border: TuiColor, /// The default color of text. /// /// This may be overridden for specific components. pub text: TuiColor, /// The color of the text in traces tabs. pub tab_text: TuiColor, /// The background color of the hops table header. pub hops_table_header_bg: TuiColor, /// The color of text in the hops table header. pub hops_table_header_text: TuiColor, /// The color of text of active rows in the hops table. pub hops_table_row_active_text: TuiColor, /// The color of text of inactive rows in the hops table. pub hops_table_row_inactive_text: TuiColor, /// The color of the selected series in the hops chart. pub hops_chart_selected: TuiColor, /// The color of the unselected series in the hops chart. pub hops_chart_unselected: TuiColor, /// The color of the axis in the hops chart. pub hops_chart_axis: TuiColor, /// The color of bars in the frequency chart. pub frequency_chart_bar: TuiColor, /// The color of text in the bars of the frequency chart. pub frequency_chart_text: TuiColor, /// The color of the selected flow bar in the flows chart. pub flows_chart_bar_selected: TuiColor, /// The color of the unselected flow bar in the flows chart. pub flows_chart_bar_unselected: TuiColor, /// The color of the current flow text in the flows chart. pub flows_chart_text_current: TuiColor, /// The color of the non-current flow text in the flows chart. pub flows_chart_text_non_current: TuiColor, /// The color of the samples chart. pub samples_chart: TuiColor, /// The color of the samples chart for lost probes. pub samples_chart_lost: TuiColor, /// The background color of the help dialog. pub help_dialog_bg: TuiColor, /// The color of the text in the help dialog. pub help_dialog_text: TuiColor, /// The background color of the settings dialog. pub settings_dialog_bg: TuiColor, /// The color of the text in settings dialog tabs. pub settings_tab_text: TuiColor, /// The color of text in the settings table header. pub settings_table_header_text: TuiColor, /// The background color of the settings table header. pub settings_table_header_bg: TuiColor, /// The color of text of rows in the settings table. pub settings_table_row_text: TuiColor, /// The color of the map world diagram. pub map_world: TuiColor, /// The color of the map accuracy radius circle. pub map_radius: TuiColor, /// The color of the map selected item box. pub map_selected: TuiColor, /// The color of border of the map info panel. pub map_info_panel_border: TuiColor, /// The background color of the map info panel. pub map_info_panel_bg: TuiColor, /// The color of text in the map info panel. pub map_info_panel_text: TuiColor, /// The color of the info bar background. pub info_bar_bg: TuiColor, /// The color of the info bar text. pub info_bar_text: TuiColor, } impl Default for TuiTheme { fn default() -> Self { Self { bg: TuiColor::Black, border: TuiColor::Gray, text: TuiColor::Gray, tab_text: TuiColor::Green, hops_table_header_bg: TuiColor::White, hops_table_header_text: TuiColor::Black, hops_table_row_active_text: TuiColor::Gray, hops_table_row_inactive_text: TuiColor::DarkGray, hops_chart_selected: TuiColor::Green, hops_chart_unselected: TuiColor::Gray, hops_chart_axis: TuiColor::DarkGray, frequency_chart_bar: TuiColor::Green, frequency_chart_text: TuiColor::Gray, flows_chart_bar_selected: TuiColor::Green, flows_chart_bar_unselected: TuiColor::DarkGray, flows_chart_text_current: TuiColor::LightGreen, flows_chart_text_non_current: TuiColor::White, samples_chart: TuiColor::Yellow, samples_chart_lost: TuiColor::Red, help_dialog_bg: TuiColor::Blue, help_dialog_text: TuiColor::Gray, settings_dialog_bg: TuiColor::Blue, settings_tab_text: TuiColor::Green, settings_table_header_text: TuiColor::Black, settings_table_header_bg: TuiColor::White, settings_table_row_text: TuiColor::Gray, map_world: TuiColor::White, map_radius: TuiColor::Yellow, map_selected: TuiColor::Green, map_info_panel_border: TuiColor::Gray, map_info_panel_bg: TuiColor::Black, map_info_panel_text: TuiColor::Gray, info_bar_bg: TuiColor::White, info_bar_text: TuiColor::Black, } } } impl From<(HashMap, ConfigThemeColors)> for TuiTheme { #[expect(clippy::too_many_lines, clippy::or_fun_call)] fn from(value: (HashMap, ConfigThemeColors)) -> Self { let (color_map, cfg) = value; Self { bg: *color_map .get(&TuiThemeItem::BgColor) .or(cfg.bg_color.as_ref()) .unwrap_or(&Self::default().bg), border: *color_map .get(&TuiThemeItem::BorderColor) .or(cfg.border_color.as_ref()) .unwrap_or(&Self::default().border), text: *color_map .get(&TuiThemeItem::TextColor) .or(cfg.text_color.as_ref()) .unwrap_or(&Self::default().text), tab_text: *color_map .get(&TuiThemeItem::TabTextColor) .or(cfg.tab_text_color.as_ref()) .unwrap_or(&Self::default().tab_text), hops_table_header_bg: *color_map .get(&TuiThemeItem::HopsTableHeaderBgColor) .or(cfg.hops_table_header_bg_color.as_ref()) .unwrap_or(&Self::default().hops_table_header_bg), hops_table_header_text: *color_map .get(&TuiThemeItem::HopsTableHeaderTextColor) .or(cfg.hops_table_header_text_color.as_ref()) .unwrap_or(&Self::default().hops_table_header_text), hops_table_row_active_text: *color_map .get(&TuiThemeItem::HopsTableRowActiveTextColor) .or(cfg.hops_table_row_active_text_color.as_ref()) .unwrap_or(&Self::default().hops_table_row_active_text), hops_table_row_inactive_text: *color_map .get(&TuiThemeItem::HopsTableRowInactiveTextColor) .or(cfg.hops_table_row_inactive_text_color.as_ref()) .unwrap_or(&Self::default().hops_table_row_inactive_text), hops_chart_selected: *color_map .get(&TuiThemeItem::HopsChartSelectedColor) .or(cfg.hops_chart_selected_color.as_ref()) .unwrap_or(&Self::default().hops_chart_selected), hops_chart_unselected: *color_map .get(&TuiThemeItem::HopsChartUnselectedColor) .or(cfg.hops_chart_unselected_color.as_ref()) .unwrap_or(&Self::default().hops_chart_unselected), hops_chart_axis: *color_map .get(&TuiThemeItem::HopsChartAxisColor) .or(cfg.hops_chart_axis_color.as_ref()) .unwrap_or(&Self::default().hops_chart_axis), frequency_chart_bar: *color_map .get(&TuiThemeItem::FrequencyChartBarColor) .or(cfg.frequency_chart_bar_color.as_ref()) .unwrap_or(&Self::default().frequency_chart_bar), frequency_chart_text: *color_map .get(&TuiThemeItem::FrequencyChartTextColor) .or(cfg.frequency_chart_text_color.as_ref()) .unwrap_or(&Self::default().frequency_chart_text), flows_chart_bar_selected: *color_map .get(&TuiThemeItem::FlowsChartBarSelectedColor) .or(cfg.flows_chart_bar_selected_color.as_ref()) .unwrap_or(&Self::default().flows_chart_bar_selected), flows_chart_bar_unselected: *color_map .get(&TuiThemeItem::FlowsChartBarUnselectedColor) .or(cfg.flows_chart_bar_unselected_color.as_ref()) .unwrap_or(&Self::default().flows_chart_bar_unselected), flows_chart_text_current: *color_map .get(&TuiThemeItem::FlowsChartTextCurrentColor) .or(cfg.flows_chart_text_current_color.as_ref()) .unwrap_or(&Self::default().flows_chart_text_current), flows_chart_text_non_current: *color_map .get(&TuiThemeItem::FlowsChartTextNonCurrentColor) .or(cfg.flows_chart_text_non_current_color.as_ref()) .unwrap_or(&Self::default().flows_chart_text_non_current), samples_chart: *color_map .get(&TuiThemeItem::SamplesChartColor) .or(cfg.samples_chart_color.as_ref()) .unwrap_or(&Self::default().samples_chart), samples_chart_lost: *color_map .get(&TuiThemeItem::SamplesChartLostColor) .or(cfg.samples_chart_lost_color.as_ref()) .unwrap_or(&Self::default().samples_chart_lost), help_dialog_bg: *color_map .get(&TuiThemeItem::HelpDialogBgColor) .or(cfg.help_dialog_bg_color.as_ref()) .unwrap_or(&Self::default().help_dialog_bg), help_dialog_text: *color_map .get(&TuiThemeItem::HelpDialogTextColor) .or(cfg.help_dialog_text_color.as_ref()) .unwrap_or(&Self::default().help_dialog_text), settings_dialog_bg: *color_map .get(&TuiThemeItem::SettingsDialogBgColor) .or(cfg.settings_dialog_bg_color.as_ref()) .unwrap_or(&Self::default().settings_dialog_bg), settings_tab_text: *color_map .get(&TuiThemeItem::SettingsTabTextColor) .or(cfg.settings_tab_text_color.as_ref()) .unwrap_or(&Self::default().settings_tab_text), settings_table_header_text: *color_map .get(&TuiThemeItem::SettingsTableHeaderTextColor) .or(cfg.settings_table_header_text_color.as_ref()) .unwrap_or(&Self::default().settings_table_header_text), settings_table_header_bg: *color_map .get(&TuiThemeItem::SettingsTableHeaderBgColor) .or(cfg.settings_table_header_bg_color.as_ref()) .unwrap_or(&Self::default().settings_table_header_bg), settings_table_row_text: *color_map .get(&TuiThemeItem::SettingsTableRowTextColor) .or(cfg.settings_table_row_text_color.as_ref()) .unwrap_or(&Self::default().settings_table_row_text), map_world: *color_map .get(&TuiThemeItem::MapWorldColor) .or(cfg.map_world_color.as_ref()) .unwrap_or(&Self::default().map_world), map_radius: *color_map .get(&TuiThemeItem::MapRadiusColor) .or(cfg.map_radius_color.as_ref()) .unwrap_or(&Self::default().map_radius), map_selected: *color_map .get(&TuiThemeItem::MapSelectedColor) .or(cfg.map_selected_color.as_ref()) .unwrap_or(&Self::default().map_selected), map_info_panel_border: *color_map .get(&TuiThemeItem::MapInfoPanelBorderColor) .or(cfg.map_info_panel_border_color.as_ref()) .unwrap_or(&Self::default().map_info_panel_border), map_info_panel_bg: *color_map .get(&TuiThemeItem::MapInfoPanelBgColor) .or(cfg.map_info_panel_bg_color.as_ref()) .unwrap_or(&Self::default().map_info_panel_bg), map_info_panel_text: *color_map .get(&TuiThemeItem::MapInfoPanelTextColor) .or(cfg.map_info_panel_text_color.as_ref()) .unwrap_or(&Self::default().map_info_panel_text), info_bar_bg: *color_map .get(&TuiThemeItem::InfoBarBgColor) .or(cfg.info_bar_bg_color.as_ref()) .unwrap_or(&Self::default().info_bar_bg), info_bar_text: *color_map .get(&TuiThemeItem::InfoBarTextColor) .or(cfg.info_bar_text_color.as_ref()) .unwrap_or(&Self::default().info_bar_text), } } } /// A TUI theme item. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, EnumString, VariantNames)] #[strum(serialize_all = "kebab-case")] #[expect(clippy::enum_variant_names)] pub enum TuiThemeItem { /// The default background color. BgColor, /// The default color of borders. BorderColor, /// The default color of text. TextColor, /// The color of the text in traces tabs. TabTextColor, /// The background color of the hops table header. HopsTableHeaderBgColor, /// The color of text in the hops table header. HopsTableHeaderTextColor, /// The color of text of active rows in the hops table. HopsTableRowActiveTextColor, /// The color of text of inactive rows in the hops table. HopsTableRowInactiveTextColor, /// The color of the selected series in the hops chart. HopsChartSelectedColor, /// The color of the unselected series in the hops chart. HopsChartUnselectedColor, /// The color of the axis in the hops chart. HopsChartAxisColor, /// The color of bars in the frequency chart. FrequencyChartBarColor, /// The color of text in the bars of the frequency chart. FrequencyChartTextColor, /// The color of the selected flow bar in the flows chart. FlowsChartBarSelectedColor, /// The color of the unselected flow bar in the flows chart. FlowsChartBarUnselectedColor, /// The color of the current flow text in the flows chart. FlowsChartTextCurrentColor, /// The color of the non-current flow text in the flows chart. FlowsChartTextNonCurrentColor, /// The color of the samples chart. SamplesChartColor, /// The color of the samples chart for lost probes. SamplesChartLostColor, /// The background color of the help dialog. HelpDialogBgColor, /// The color of the text in the help dialog. HelpDialogTextColor, /// The color of the text in settings tabs. SettingsTabTextColor, /// The background color of the settings dialog. SettingsDialogBgColor, /// The color of text in the settings table header. SettingsTableHeaderTextColor, /// The background color of the settings table header. SettingsTableHeaderBgColor, /// The color of text of rows in the settings table. SettingsTableRowTextColor, /// The color of the map world diagram. MapWorldColor, /// The color of the map accuracy radius circle. MapRadiusColor, /// The color of the map selected item box. MapSelectedColor, /// The color of border of the map info panel. MapInfoPanelBorderColor, /// The background color of the map info panel. MapInfoPanelBgColor, /// The color of text in the map info panel. MapInfoPanelTextColor, /// The color of the info bar background. InfoBarBgColor, /// The color of the info bar text. InfoBarTextColor, } /// A TUI color. #[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize)] #[serde(try_from = "String")] pub enum TuiColor { // ANSI colors Black, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, DarkGray, LightRed, LightGreen, LightYellow, LightBlue, LightMagenta, LightCyan, White, // Other colors AliceBlue, AntiqueWhite, Aqua, Aquamarine, Azure, Beige, Bisque, BlanchedAlmond, BlueViolet, Brown, BurlyWood, CadetBlue, Chartreuse, Chocolate, Coral, CornflowerBlue, CornSilk, Crimson, DarkBlue, DarkCyan, DarkGoldenrod, DarkGreen, DarkKhaki, DarkMagenta, DarkOliveGreen, DarkOrange, DarkOrchid, DarkRed, DarkSalmon, DarkSeaGreen, DarkSlateBlue, DarkSlateGray, DarkTurquoise, DarkViolet, DeepPink, DeepSkyBlue, DimGray, DodgerBlue, Firebrick, FloralWhite, ForestGreen, Fuchsia, Gainsboro, GhostWhite, Gold, Goldenrod, GreenYellow, Honeydew, HotPink, IndianRed, Indigo, Ivory, Khaki, Lavender, LavenderBlush, LawnGreen, LemonChiffon, LightCoral, LightGoldenrodYellow, LightGray, LightPink, LightSalmon, LightSeaGreen, LightSkyBlue, LightSlateGray, LightSteelBlue, Lime, LimeGreen, Linen, Maroon, MediumAquamarine, MediumBlue, MediumOrchid, MediumPurple, MediumSeaGreen, MediumSlateBlue, MediumSpringGreen, MediumTurquoise, MediumVioletRed, MidnightBlue, MintCream, MistyRose, Moccasin, NavajoWhite, Navy, OldLace, Olive, OliveDrab, Orange, OrangeRed, Orchid, PaleGoldenrod, PaleGreen, PaleTurquoise, PaleVioletRed, PapayaWhip, PeachPuff, Peru, Pink, Plum, PowderBlue, Purple, RebeccaPurple, RosyBrown, RoyalBlue, SaddleBrown, Salmon, SandyBrown, SeaGreen, SeaShell, Sienna, Silver, SkyBlue, SlateBlue, SlateGray, Snow, SpringGreen, SteelBlue, Tan, Teal, Thistle, Tomato, Turquoise, Violet, Wheat, WhiteSmoke, YellowGreen, Rgb(u8, u8, u8), } impl TryFrom for TuiColor { type Error = anyhow::Error; fn try_from(value: String) -> Result { Self::try_from(value.as_ref()) } } impl TryFrom<&str> for TuiColor { type Error = anyhow::Error; #[expect(clippy::too_many_lines)] fn try_from(value: &str) -> Result { match value.to_ascii_lowercase().replace('-', "").as_ref() { "black" => Ok(Self::Black), "red" => Ok(Self::Red), "green" => Ok(Self::Green), "yellow" => Ok(Self::Yellow), "blue" => Ok(Self::Blue), "magenta" => Ok(Self::Magenta), "cyan" => Ok(Self::Cyan), "gray" => Ok(Self::Gray), "darkgray" => Ok(Self::DarkGray), "lightred" => Ok(Self::LightRed), "lightgreen" => Ok(Self::LightGreen), "lightyellow" => Ok(Self::LightYellow), "lightblue" => Ok(Self::LightBlue), "lightmagenta" => Ok(Self::LightMagenta), "lightcyan" => Ok(Self::LightCyan), "white" => Ok(Self::White), "aliceblue" => Ok(Self::AliceBlue), "antiquewhite" => Ok(Self::AntiqueWhite), "aqua" => Ok(Self::Aqua), "aquamarine" => Ok(Self::Aquamarine), "azure" => Ok(Self::Azure), "beige" => Ok(Self::Beige), "bisque" => Ok(Self::Bisque), "blanchedalmond" => Ok(Self::BlanchedAlmond), "blueviolet" => Ok(Self::BlueViolet), "brown" => Ok(Self::Brown), "burlywood" => Ok(Self::BurlyWood), "cadetblue" => Ok(Self::CadetBlue), "chartreuse" => Ok(Self::Chartreuse), "chocolate" => Ok(Self::Chocolate), "coral" => Ok(Self::Coral), "cornflowerblue" => Ok(Self::CornflowerBlue), "cornsilk" => Ok(Self::CornSilk), "crimson" => Ok(Self::Crimson), "darkblue" => Ok(Self::DarkBlue), "darkcyan" => Ok(Self::DarkCyan), "darkgoldenrod" => Ok(Self::DarkGoldenrod), "darkgreen" => Ok(Self::DarkGreen), "darkkhaki" => Ok(Self::DarkKhaki), "darkmagenta" => Ok(Self::DarkMagenta), "darkolivegreen" => Ok(Self::DarkOliveGreen), "darkorange" => Ok(Self::DarkOrange), "darkorchid" => Ok(Self::DarkOrchid), "darkred" => Ok(Self::DarkRed), "darksalmon" => Ok(Self::DarkSalmon), "darkseagreen" => Ok(Self::DarkSeaGreen), "darkslateblue" => Ok(Self::DarkSlateBlue), "darkslategray" | "darkslategrey" => Ok(Self::DarkSlateGray), "darkturquoise" => Ok(Self::DarkTurquoise), "darkviolet" => Ok(Self::DarkViolet), "deeppink" => Ok(Self::DeepPink), "deepskyblue" => Ok(Self::DeepSkyBlue), "dimgray" | "dimgrey" => Ok(Self::DimGray), "dodgerblue" => Ok(Self::DodgerBlue), "firebrick" => Ok(Self::Firebrick), "floralwhite" => Ok(Self::FloralWhite), "forestgreen" => Ok(Self::ForestGreen), "fuchsia" => Ok(Self::Fuchsia), "gainsboro" => Ok(Self::Gainsboro), "ghostwhite" => Ok(Self::GhostWhite), "gold" => Ok(Self::Gold), "goldenrod" => Ok(Self::Goldenrod), "greenyellow" => Ok(Self::GreenYellow), "honeydew" => Ok(Self::Honeydew), "hotpink" => Ok(Self::HotPink), "indianred" => Ok(Self::IndianRed), "indigo" => Ok(Self::Indigo), "ivory" => Ok(Self::Ivory), "khaki" => Ok(Self::Khaki), "lavender" => Ok(Self::Lavender), "lavenderblush" => Ok(Self::LavenderBlush), "lawngreen" => Ok(Self::LawnGreen), "lemonchiffon" => Ok(Self::LemonChiffon), "lightcoral" => Ok(Self::LightCoral), "lightgoldenrodyellow" => Ok(Self::LightGoldenrodYellow), "lightgray" | "lightgrey" => Ok(Self::LightGray), "lightpink" => Ok(Self::LightPink), "lightsalmon" => Ok(Self::LightSalmon), "lightseagreen" => Ok(Self::LightSeaGreen), "lightskyblue" => Ok(Self::LightSkyBlue), "lightslategray" | "lightslategrey" => Ok(Self::LightSlateGray), "lightsteelblue" => Ok(Self::LightSteelBlue), "lime" => Ok(Self::Lime), "limegreen" => Ok(Self::LimeGreen), "linen" => Ok(Self::Linen), "maroon" => Ok(Self::Maroon), "mediumaquamarine" => Ok(Self::MediumAquamarine), "mediumblue" => Ok(Self::MediumBlue), "mediumorchid" => Ok(Self::MediumOrchid), "mediumpurple" => Ok(Self::MediumPurple), "mediumseagreen" => Ok(Self::MediumSeaGreen), "mediumslateblue" => Ok(Self::MediumSlateBlue), "mediumspringgreen" => Ok(Self::MediumSpringGreen), "mediumturquoise" => Ok(Self::MediumTurquoise), "mediumvioletred" => Ok(Self::MediumVioletRed), "midnightblue" => Ok(Self::MidnightBlue), "mintcream" => Ok(Self::MintCream), "mistyrose" => Ok(Self::MistyRose), "moccasin" => Ok(Self::Moccasin), "navajowhite" => Ok(Self::NavajoWhite), "navy" => Ok(Self::Navy), "oldlace" => Ok(Self::OldLace), "olive" => Ok(Self::Olive), "olivedrab" => Ok(Self::OliveDrab), "orange" => Ok(Self::Orange), "orangered" => Ok(Self::OrangeRed), "orchid" => Ok(Self::Orchid), "palegoldenrod" => Ok(Self::PaleGoldenrod), "palegreen" => Ok(Self::PaleGreen), "paleturquoise" => Ok(Self::PaleTurquoise), "palevioletred" => Ok(Self::PaleVioletRed), "papayawhip" => Ok(Self::PapayaWhip), "peachpuff" => Ok(Self::PeachPuff), "peru" => Ok(Self::Peru), "pink" => Ok(Self::Pink), "plum" => Ok(Self::Plum), "powderblue" => Ok(Self::PowderBlue), "purple" => Ok(Self::Purple), "rebeccapurple" => Ok(Self::RebeccaPurple), "rosybrown" => Ok(Self::RosyBrown), "royalblue" => Ok(Self::RoyalBlue), "saddlebrown" => Ok(Self::SaddleBrown), "salmon" => Ok(Self::Salmon), "sandybrown" => Ok(Self::SandyBrown), "seagreen" => Ok(Self::SeaGreen), "seashell" => Ok(Self::SeaShell), "sienna" => Ok(Self::Sienna), "silver" => Ok(Self::Silver), "skyblue" => Ok(Self::SkyBlue), "slateblue" => Ok(Self::SlateBlue), "slategray" | "slategrey" => Ok(Self::SlateGray), "snow" => Ok(Self::Snow), "springgreen" => Ok(Self::SpringGreen), "steelblue" => Ok(Self::SteelBlue), "tan" => Ok(Self::Tan), "teal" => Ok(Self::Teal), "thistle" => Ok(Self::Thistle), "tomato" => Ok(Self::Tomato), "turquoise" => Ok(Self::Turquoise), "violet" => Ok(Self::Violet), "wheat" => Ok(Self::Wheat), "whitesmoke" => Ok(Self::WhiteSmoke), "yellowgreen" => Ok(Self::YellowGreen), rgb_hex if value.len() == 6 && value.chars().all(|c| c.is_ascii_hexdigit()) => { let red = u8::from_str_radix(&rgb_hex[0..2], 16)?; let green = u8::from_str_radix(&rgb_hex[2..4], 16)?; let blue = u8::from_str_radix(&rgb_hex[4..6], 16)?; Ok(Self::Rgb(red, green, blue)) } _ => Err(anyhow!("unknown color: {value}")), } } } ================================================ FILE: crates/trippy-tui/src/config.rs ================================================ use anyhow::anyhow; use clap::ValueEnum; use clap_complete::Shell; use file::ConfigFile; use itertools::Itertools; use serde::Deserialize; use std::collections::HashMap; use std::net::IpAddr; use std::str::FromStr; use std::time::Duration; use trippy_core::{ IcmpExtensionParseMode, MAX_TTL, MultipathStrategy, PortDirection, PrivilegeMode, Protocol, defaults, }; use trippy_dns::{IpAddrFamily, ResolveMethod}; mod binding; mod cmd; mod columns; mod constants; mod file; mod theme; use crate::config::file::{ConfigBindings, ConfigTui}; pub use binding::{TuiBindings, TuiCommandItem, TuiKeyBinding}; pub use cmd::Args; pub use columns::{TuiColumn, TuiColumns}; pub use theme::{TuiColor, TuiTheme, TuiThemeItem}; use trippy_privilege::Privilege; /// The tool mode. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Mode { /// Display interactive TUI. Tui, /// Display a continuous stream of tracing data Stream, /// Generate a pretty text table report for N cycles. Pretty, /// Generate a Markdown text table report for N cycles. Markdown, /// Generate a CSV report for N cycles. Csv, /// Generate a JSON report for N cycles. Json, /// Generate a Graphviz DOT file for N cycles. Dot, /// Display all flows for N cycles. Flows, /// Do not generate any tracing output for N cycles. Silent, } /// The tracing protocol. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum ProtocolConfig { /// Internet Control Message Protocol Icmp, /// User Datagram Protocol Udp, /// Transmission Control Protocol Tcp, } impl From for ProtocolConfig { fn from(value: Protocol) -> Self { match value { Protocol::Icmp => Self::Icmp, Protocol::Udp => Self::Udp, Protocol::Tcp => Self::Tcp, } } } /// The address family. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum AddressFamilyConfig { /// IPv4 only. Ipv4, /// IPv6 only. Ipv6, /// IPv6 with a fallback to IPv4. #[serde(rename = "ipv6-then-ipv4")] Ipv6ThenIpv4, /// IPv4 with a fallback to IPv6. #[serde(rename = "ipv4-then-ipv6")] Ipv4ThenIpv6, /// If the OS resolver is being used then use the first IP address returned, /// otherwise lookup IPv4 with a fallback to IPv6. System, } impl From for AddressFamilyConfig { fn from(value: IpAddrFamily) -> Self { match value { IpAddrFamily::Ipv4Only => Self::Ipv4, IpAddrFamily::Ipv6Only => Self::Ipv6, IpAddrFamily::Ipv6thenIpv4 => Self::Ipv6ThenIpv4, IpAddrFamily::Ipv4thenIpv6 => Self::Ipv4ThenIpv6, IpAddrFamily::System => Self::System, } } } /// The strategy Equal-cost Multi-Path routing strategy. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum MultipathStrategyConfig { /// The src or dest port is used to store the sequence number. Classic, /// The UDP `checksum` field is used to store the sequence number. Paris, /// The IP `identifier` field is used to store the sequence number. Dublin, } impl From for MultipathStrategyConfig { fn from(value: MultipathStrategy) -> Self { match value { MultipathStrategy::Classic => Self::Classic, MultipathStrategy::Paris => Self::Paris, MultipathStrategy::Dublin => Self::Dublin, } } } /// How to render the addresses. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum AddressMode { /// Show IP address only. Ip, /// Show reverse-lookup DNS hostname only. Host, /// Show both IP address and reverse-lookup DNS hostname. Both, } /// How to render autonomous system (AS) information. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum AsMode { /// Show the ASN. Asn, /// Display the AS prefix. Prefix, /// Display the country code. CountryCode, /// Display the registry name. Registry, /// Display the allocated date. Allocated, /// Display the AS name. Name, } /// How to render `icmp` extensions in the hops table. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum IcmpExtensionMode { /// Do not show `icmp` extensions. Off, /// Show MPLS label(s) only. Mpls, /// Show full `icmp` extension data for all known extensions. /// /// For MPLS the fields shown are `label`, `ttl`, `exp` & `bos`. Full, /// Show full `icmp` extension data for all classes. /// /// This is the same as `Full`, but also shows `class`, `subtype` and /// `object` for unknown extensions. All, } /// How to render `GeoIp` information in the hop table. /// /// Note that the hop details view is always shown using the `Long` representation. #[expect(clippy::doc_markdown)] #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum GeoIpMode { /// Do not display GeoIp data. Off, /// Show short format. /// /// The `city` name is shown, `subdivision` and `country` codes are shown, `continent` is not /// displayed. /// /// For example: /// /// `Los Angeles, CA, US` Short, /// Show long format. /// /// The `city`, `subdivision`, `country` and `continent` names are shown. /// /// `Los Angeles, California, United States, North America` Long, /// Show latitude and Longitude format. /// /// `lat=34.0544, long=-118.2441` Location, } /// How DNS queries will be resolved. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum DnsResolveMethodConfig { /// Resolve using the OS resolver. System, /// Resolve using the `/etc/resolv.conf` DNS configuration. Resolv, /// Resolve using the Google `8.8.8.8` DNS service. Google, /// Resolve using the Cloudflare `1.1.1.1` DNS service. Cloudflare, } /// How to format log data. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum LogFormat { /// Display log data in a compact format. Compact, /// Display log data in a pretty format. Pretty, /// Display log data in a json format. Json, /// Display log data in Chrome trace format. Chrome, } /// How to log event spans. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum LogSpanEvents { /// Do not display event spans. Off, /// Display enter and exit event spans. Active, /// Display all event spans. Full, } /// The action to perform. #[derive(Debug, Eq, PartialEq)] pub enum TrippyAction { /// Run Trippy. Trippy(Box), /// Print all TUI theme items and exit. PrintTuiThemeItems, /// Print all TUI commands that can be bound and exit. PrintTuiBindingCommands, /// Print a template toml config file and exit. PrintConfigTemplate, /// Generate shell completion and exit. PrintShellCompletions(Shell), /// Generate a man page and exit. PrintManPage, /// Print all available locales and exit. PrintLocales, } impl TrippyAction { pub fn from(args: Args, privilege: &Privilege, pid: u16) -> anyhow::Result { Ok(if args.print_tui_theme_items { Self::PrintTuiThemeItems } else if args.print_tui_binding_commands { Self::PrintTuiBindingCommands } else if args.print_config_template { Self::PrintConfigTemplate } else if let Some(shell) = args.generate { Self::PrintShellCompletions(shell) } else if args.generate_man { Self::PrintManPage } else if args.print_locales { Self::PrintLocales } else { Self::Trippy(Box::new(TrippyConfig::from(args, privilege, pid)?)) }) } } /// Fully parsed and validated configuration. #[derive(Debug, Eq, PartialEq)] pub struct TrippyConfig { pub targets: Vec, pub protocol: Protocol, pub addr_family: IpAddrFamily, pub first_ttl: u8, pub max_ttl: u8, pub min_round_duration: Duration, pub max_round_duration: Duration, pub grace_duration: Duration, pub max_inflight: u8, pub initial_sequence: u16, pub tos: u8, pub icmp_extension_parse_mode: IcmpExtensionParseMode, pub read_timeout: Duration, pub packet_size: u16, pub payload_pattern: u8, pub source_addr: Option, pub interface: Option, pub multipath_strategy: MultipathStrategy, pub port_direction: PortDirection, pub dns_timeout: Duration, pub dns_ttl: Duration, pub dns_resolve_method: ResolveMethod, pub dns_lookup_as_info: bool, pub max_samples: usize, pub max_flows: usize, pub tui_preserve_screen: bool, pub tui_refresh_rate: Duration, pub tui_privacy_max_ttl: Option, pub tui_address_mode: AddressMode, pub tui_as_mode: AsMode, pub tui_custom_columns: TuiColumns, pub tui_icmp_extension_mode: IcmpExtensionMode, pub tui_geoip_mode: GeoIpMode, pub tui_max_addrs: Option, pub tui_locale: Option, pub tui_timezone: Option, pub tui_theme: TuiTheme, pub tui_bindings: TuiBindings, pub mode: Mode, pub privilege_mode: PrivilegeMode, pub dns_resolve_all: bool, pub report_cycles: usize, pub geoip_mmdb_file: Option, pub max_rounds: Option, pub verbose: bool, pub log_format: LogFormat, pub log_filter: String, pub log_span_events: LogSpanEvents, } impl TrippyConfig { pub fn from(args: Args, privilege: &Privilege, pid: u16) -> anyhow::Result { let cfg_file = if let Some(cfg) = &args.config_file { file::read_config_file(cfg)? } else { file::read_default_config_file()?.unwrap_or_default() }; Self::build_config(args, cfg_file, privilege, pid) } /// The maximum number of flows allowed. /// /// This is restricted to 1 for the classic strategy. pub const fn max_flows(&self) -> usize { match self.multipath_strategy { MultipathStrategy::Classic => 1, _ => self.max_flows, } } #[expect(clippy::too_many_lines)] fn build_config( args: Args, cfg_file: ConfigFile, privilege: &Privilege, pid: u16, ) -> anyhow::Result { let has_privileges = privilege.has_privileges(); let needs_privileges = privilege.needs_privileges(); let cfg_file_trace = cfg_file.trippy.unwrap_or_default(); let cfg_file_strategy = cfg_file.strategy.unwrap_or_default(); let cfg_file_tui_bindings = cfg_file.bindings.unwrap_or_default(); let cfg_file_tui_theme_colors = cfg_file.theme_colors.unwrap_or_default(); let cfg_file_tui = cfg_file.tui.unwrap_or_default(); let cfg_file_dns = cfg_file.dns.unwrap_or_default(); let cfg_file_report = cfg_file.report.unwrap_or_default(); validate_deprecated(&cfg_file_tui, &cfg_file_tui_bindings)?; let mode = cfg_layer(args.mode, cfg_file_trace.mode, constants::DEFAULT_MODE); let unprivileged = cfg_layer_bool_flag( args.unprivileged, cfg_file_trace.unprivileged, defaults::DEFAULT_PRIVILEGE_MODE.is_unprivileged(), ); let privilege_mode = if unprivileged { PrivilegeMode::Unprivileged } else { PrivilegeMode::Privileged }; let dns_resolve_all = cfg_layer_bool_flag( args.dns_resolve_all, cfg_file_dns.dns_resolve_all, constants::DEFAULT_DNS_RESOLVE_ALL, ); let verbose = args.verbose; let log_format = cfg_layer( args.log_format, cfg_file_trace.log_format, constants::DEFAULT_LOG_FORMAT, ); let log_filter = cfg_layer( args.log_filter, cfg_file_trace.log_filter, String::from(constants::DEFAULT_LOG_FILTER), ); let log_span_events = cfg_layer( args.log_span_events, cfg_file_trace.log_span_events, constants::DEFAULT_LOG_SPAN_EVENTS, ); let protocol = cfg_layer( args.protocol, cfg_file_strategy.protocol, ProtocolConfig::from(defaults::DEFAULT_STRATEGY_PROTOCOL), ); let addr_family_cfg = cfg_layer( args.addr_family, cfg_file_strategy.addr_family, constants::DEFAULT_ADDR_FAMILY, ); let target_port = cfg_layer_opt(args.target_port, cfg_file_strategy.target_port); let source_port = cfg_layer_opt(args.source_port, cfg_file_strategy.source_port); let source_addr = cfg_layer_opt(args.source_address, cfg_file_strategy.source_address); let interface = cfg_layer_opt(args.interface, cfg_file_strategy.interface); let min_round_duration = cfg_layer( args.min_round_duration, cfg_file_strategy.min_round_duration, defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION, ); let max_round_duration = cfg_layer( args.max_round_duration, cfg_file_strategy.max_round_duration, defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION, ); let initial_sequence = cfg_layer( args.initial_sequence, cfg_file_strategy.initial_sequence, defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE, ); let multipath_strategy_cfg = cfg_layer( args.multipath_strategy, cfg_file_strategy.multipath_strategy, MultipathStrategyConfig::from(defaults::DEFAULT_STRATEGY_MULTIPATH), ); let grace_duration = cfg_layer( args.grace_duration, cfg_file_strategy.grace_duration, defaults::DEFAULT_STRATEGY_GRACE_DURATION, ); let max_inflight = cfg_layer( args.max_inflight, cfg_file_strategy.max_inflight, defaults::DEFAULT_STRATEGY_MAX_INFLIGHT, ); let first_ttl = cfg_layer( args.first_ttl, cfg_file_strategy.first_ttl, defaults::DEFAULT_STRATEGY_FIRST_TTL, ); let max_ttl = cfg_layer( args.max_ttl, cfg_file_strategy.max_ttl, defaults::DEFAULT_STRATEGY_MAX_TTL, ); let packet_size = cfg_layer( args.packet_size, cfg_file_strategy.packet_size, defaults::DEFAULT_STRATEGY_PACKET_SIZE, ); let payload_pattern = cfg_layer( args.payload_pattern, cfg_file_strategy.payload_pattern, defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN, ); let tos = cfg_layer( args.tos, cfg_file_strategy.tos, defaults::DEFAULT_STRATEGY_TOS, ); let icmp_extensions = cfg_layer_bool_flag( args.icmp_extensions, cfg_file_strategy.icmp_extensions, defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE.is_enabled(), ); let icmp_extension_parse_mode = if icmp_extensions { IcmpExtensionParseMode::Enabled } else { IcmpExtensionParseMode::Disabled }; let read_timeout = cfg_layer( args.read_timeout, cfg_file_strategy.read_timeout, defaults::DEFAULT_STRATEGY_READ_TIMEOUT, ); let max_samples = cfg_layer( args.max_samples, cfg_file_strategy.max_samples, defaults::DEFAULT_MAX_SAMPLES, ); let max_flows = cfg_layer( args.max_flows, cfg_file_strategy.max_flows, defaults::DEFAULT_MAX_FLOWS, ); let tui_preserve_screen = cfg_layer_bool_flag( args.tui_preserve_screen, cfg_file_tui.tui_preserve_screen, constants::DEFAULT_TUI_PRESERVE_SCREEN, ); let tui_refresh_rate = cfg_layer( args.tui_refresh_rate, cfg_file_tui.tui_refresh_rate, constants::DEFAULT_TUI_REFRESH_RATE, ); let tui_privacy_max_ttl = cfg_layer_opt(args.tui_privacy_max_ttl, cfg_file_tui.tui_privacy_max_ttl); let tui_address_mode = cfg_layer( args.tui_address_mode, cfg_file_tui.tui_address_mode, constants::DEFAULT_TUI_ADDRESS_MODE, ); let tui_as_mode = cfg_layer( args.tui_as_mode, cfg_file_tui.tui_as_mode, constants::DEFAULT_TUI_AS_MODE, ); let columns = cfg_layer( args.tui_custom_columns, cfg_file_tui.tui_custom_columns, String::from(constants::DEFAULT_CUSTOM_COLUMNS), ); let tui_custom_columns = TuiColumns::try_from(columns.as_str())?; let tui_icmp_extension_mode = cfg_layer( args.tui_icmp_extension_mode, cfg_file_tui.tui_icmp_extension_mode, constants::DEFAULT_TUI_ICMP_EXTENSION_MODE, ); let tui_geoip_mode = cfg_layer( args.tui_geoip_mode, cfg_file_tui.tui_geoip_mode, constants::DEFAULT_TUI_GEOIP_MODE, ); let tui_max_addrs = cfg_layer_opt(args.tui_max_addrs, cfg_file_tui.tui_max_addrs); let dns_resolve_method_config = cfg_layer( args.dns_resolve_method, cfg_file_dns.dns_resolve_method, constants::DEFAULT_DNS_RESOLVE_METHOD, ); let tui_locale = cfg_layer_opt(args.tui_locale, cfg_file_tui.tui_locale); let timezone = cfg_layer_opt(args.tui_timezone, cfg_file_tui.tui_timezone); let tui_timezone = timezone .as_deref() .map(chrono_tz::Tz::from_str) .transpose()?; let dns_lookup_as_info = cfg_layer_bool_flag( args.dns_lookup_as_info, cfg_file_dns.dns_lookup_as_info, constants::DEFAULT_DNS_LOOKUP_AS_INFO, ); let dns_timeout = cfg_layer( args.dns_timeout, cfg_file_dns.dns_timeout, constants::DEFAULT_DNS_TIMEOUT, ); let dns_ttl = cfg_layer( args.dns_ttl, cfg_file_dns.dns_ttl, constants::DEFAULT_DNS_TTL, ); let report_cycles = cfg_layer( args.report_cycles, cfg_file_report.report_cycles, constants::DEFAULT_REPORT_CYCLES, ); let geoip_mmdb_file = cfg_layer_opt(args.geoip_mmdb_file, cfg_file_tui.geoip_mmdb_file); let protocol = match (args.udp, args.tcp, args.icmp, protocol) { (false, false, false, ProtocolConfig::Udp) | (true, _, _, _) => Protocol::Udp, (false, false, false, ProtocolConfig::Tcp) | (_, true, _, _) => Protocol::Tcp, (false, false, false, ProtocolConfig::Icmp) | (_, _, true, _) => Protocol::Icmp, }; #[expect(clippy::match_same_arms)] let addr_family = match ( args.ipv4, args.ipv6, addr_family_cfg, multipath_strategy_cfg, ) { (false, false, AddressFamilyConfig::Ipv4, _) => IpAddrFamily::Ipv4Only, (false, false, AddressFamilyConfig::Ipv6, _) => IpAddrFamily::Ipv6Only, (false, false, AddressFamilyConfig::Ipv4ThenIpv6, _) => IpAddrFamily::Ipv4thenIpv6, (false, false, AddressFamilyConfig::Ipv6ThenIpv4, _) => IpAddrFamily::Ipv6thenIpv4, (false, false, AddressFamilyConfig::System, _) => IpAddrFamily::System, (true, _, _, _) => IpAddrFamily::Ipv4Only, (_, true, _, _) => IpAddrFamily::Ipv6Only, }; let multipath_strategy = match multipath_strategy_cfg { MultipathStrategyConfig::Classic => MultipathStrategy::Classic, MultipathStrategyConfig::Paris => MultipathStrategy::Paris, MultipathStrategyConfig::Dublin => MultipathStrategy::Dublin, }; let port_direction = match (protocol, source_port, target_port, multipath_strategy_cfg) { (Protocol::Icmp, _, _, _) => PortDirection::None, (Protocol::Udp, None, None, _) => PortDirection::new_fixed_src(pid.max(1024)), (Protocol::Udp, Some(src), None, _) => { validate_source_port(src)?; PortDirection::new_fixed_src(src) } (Protocol::Tcp, None, None, _) => PortDirection::new_fixed_dest(80), (Protocol::Tcp, Some(src), None, _) => PortDirection::new_fixed_src(src), (_, None, Some(dest), _) => PortDirection::new_fixed_dest(dest), ( Protocol::Udp, Some(src), Some(dest), MultipathStrategyConfig::Dublin | MultipathStrategyConfig::Paris, ) => { validate_source_port(src)?; PortDirection::new_fixed_both(src, dest) } (_, Some(_), Some(_), _) => { return Err(anyhow!( "only one of source-port and target-port may be fixed (except IPv4/udp protocol with dublin or paris strategy)" )); } }; let dns_resolve_method = match dns_resolve_method_config { DnsResolveMethodConfig::System => ResolveMethod::System, DnsResolveMethodConfig::Resolv => ResolveMethod::Resolv, DnsResolveMethodConfig::Google => ResolveMethod::Google, DnsResolveMethodConfig::Cloudflare => ResolveMethod::Cloudflare, }; let max_rounds = match mode { Mode::Stream | Mode::Tui => None, Mode::Pretty | Mode::Markdown | Mode::Csv | Mode::Json | Mode::Dot | Mode::Flows | Mode::Silent => Some(report_cycles), }; let tui_max_addrs = match tui_max_addrs { Some(n) if n > 0 => Some(n), _ => None, }; validate_privilege(privilege_mode, has_privileges, needs_privileges)?; validate_logging(mode, verbose)?; validate_strategy(multipath_strategy, unprivileged)?; validate_protocol_strategy(protocol, multipath_strategy)?; validate_multi(mode, protocol, &args.targets, dns_resolve_all)?; validate_flows(mode, multipath_strategy)?; validate_ttl(first_ttl, max_ttl)?; validate_max_inflight(max_inflight)?; validate_read_timeout(read_timeout)?; validate_round_duration(min_round_duration, max_round_duration)?; validate_grace_duration(grace_duration)?; validate_packet_size(addr_family, packet_size)?; validate_tos(addr_family, tos)?; validate_tui_refresh_rate(tui_refresh_rate)?; validate_report_cycles(report_cycles)?; validate_dns(dns_resolve_method, dns_lookup_as_info)?; validate_geoip(tui_geoip_mode, geoip_mmdb_file.as_ref())?; validate_tui_custom_columns(&tui_custom_columns)?; let tui_theme_items = args .tui_theme_colors .into_iter() .collect::>(); let tui_theme = TuiTheme::from((tui_theme_items, cfg_file_tui_theme_colors)); let tui_binding_items = args .tui_key_bindings .into_iter() .collect::>(); let tui_bindings = TuiBindings::from((tui_binding_items, cfg_file_tui_bindings)); validate_bindings(&tui_bindings)?; Ok(Self { targets: args.targets, protocol, addr_family, first_ttl, max_ttl, min_round_duration, max_round_duration, grace_duration, max_inflight, initial_sequence, multipath_strategy, read_timeout, packet_size, payload_pattern, tos, icmp_extension_parse_mode, source_addr, interface, port_direction, dns_timeout, dns_ttl, dns_resolve_method, dns_lookup_as_info, max_samples, max_flows, tui_preserve_screen, tui_refresh_rate, tui_privacy_max_ttl, tui_address_mode, tui_as_mode, tui_custom_columns, tui_icmp_extension_mode, tui_geoip_mode, tui_max_addrs, tui_locale, tui_timezone, tui_theme, tui_bindings, mode, privilege_mode, dns_resolve_all, report_cycles, geoip_mmdb_file, max_rounds, verbose, log_format, log_filter, log_span_events, }) } } impl Default for TrippyConfig { fn default() -> Self { Self { targets: vec![], protocol: defaults::DEFAULT_STRATEGY_PROTOCOL, addr_family: dns_resolve_family(constants::DEFAULT_ADDR_FAMILY), first_ttl: defaults::DEFAULT_STRATEGY_FIRST_TTL, max_ttl: defaults::DEFAULT_STRATEGY_MAX_TTL, min_round_duration: defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION, max_round_duration: defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION, grace_duration: defaults::DEFAULT_STRATEGY_GRACE_DURATION, max_inflight: defaults::DEFAULT_STRATEGY_MAX_INFLIGHT, initial_sequence: defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE, tos: defaults::DEFAULT_STRATEGY_TOS, icmp_extension_parse_mode: defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE, read_timeout: defaults::DEFAULT_STRATEGY_READ_TIMEOUT, packet_size: defaults::DEFAULT_STRATEGY_PACKET_SIZE, payload_pattern: defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN, source_addr: None, interface: None, multipath_strategy: defaults::DEFAULT_STRATEGY_MULTIPATH, port_direction: PortDirection::None, dns_timeout: constants::DEFAULT_DNS_TIMEOUT, dns_ttl: constants::DEFAULT_DNS_TTL, dns_resolve_method: dns_resolve_method(constants::DEFAULT_DNS_RESOLVE_METHOD), dns_lookup_as_info: constants::DEFAULT_DNS_LOOKUP_AS_INFO, max_samples: defaults::DEFAULT_MAX_SAMPLES, max_flows: defaults::DEFAULT_MAX_FLOWS, tui_preserve_screen: constants::DEFAULT_TUI_PRESERVE_SCREEN, tui_refresh_rate: constants::DEFAULT_TUI_REFRESH_RATE, tui_privacy_max_ttl: None, tui_address_mode: constants::DEFAULT_TUI_ADDRESS_MODE, tui_as_mode: constants::DEFAULT_TUI_AS_MODE, tui_icmp_extension_mode: constants::DEFAULT_TUI_ICMP_EXTENSION_MODE, tui_geoip_mode: constants::DEFAULT_TUI_GEOIP_MODE, tui_max_addrs: None, tui_locale: None, tui_timezone: None, tui_theme: TuiTheme::default(), tui_bindings: TuiBindings::default(), mode: constants::DEFAULT_MODE, privilege_mode: defaults::DEFAULT_PRIVILEGE_MODE, dns_resolve_all: constants::DEFAULT_DNS_RESOLVE_ALL, report_cycles: constants::DEFAULT_REPORT_CYCLES, geoip_mmdb_file: None, max_rounds: None, verbose: false, log_format: constants::DEFAULT_LOG_FORMAT, log_filter: String::from(constants::DEFAULT_LOG_FILTER), log_span_events: constants::DEFAULT_LOG_SPAN_EVENTS, tui_custom_columns: TuiColumns::default(), } } } const fn dns_resolve_method(dns_resolve_method: DnsResolveMethodConfig) -> ResolveMethod { match dns_resolve_method { DnsResolveMethodConfig::System => ResolveMethod::System, DnsResolveMethodConfig::Resolv => ResolveMethod::Resolv, DnsResolveMethodConfig::Google => ResolveMethod::Google, DnsResolveMethodConfig::Cloudflare => ResolveMethod::Cloudflare, } } const fn dns_resolve_family(dns_resolve_family: AddressFamilyConfig) -> IpAddrFamily { match dns_resolve_family { AddressFamilyConfig::Ipv4 => IpAddrFamily::Ipv4Only, AddressFamilyConfig::Ipv6 => IpAddrFamily::Ipv6Only, AddressFamilyConfig::Ipv6ThenIpv4 => IpAddrFamily::Ipv6thenIpv4, AddressFamilyConfig::Ipv4ThenIpv6 => IpAddrFamily::Ipv4thenIpv6, AddressFamilyConfig::System => IpAddrFamily::System, } } fn cfg_layer(fst: Option, snd: Option, def: T) -> T { match (fst, snd) { (Some(val), _) | (None, Some(val)) => val, (None, None) => def, } } fn cfg_layer_opt(fst: Option, snd: Option) -> Option { match (fst, snd) { (Some(val), _) | (None, Some(val)) => Some(val), (None, None) => None, } } const fn cfg_layer_bool_flag(fst: bool, snd: Option, default: bool) -> bool { match (fst, snd) { (true, _) => true, (false, Some(val)) => val, (false, None) => default, } } /// Check for deprecated fields. fn validate_deprecated( cfg_file_tui: &ConfigTui, cfg_file_tui_bindings: &ConfigBindings, ) -> anyhow::Result<()> { if cfg_file_tui.deprecated_tui_max_samples.is_some() { Err(anyhow!( "tui-max-samples in [tui] section is deprecated, use max-samples in [strategy] section instead" )) } else if cfg_file_tui.deprecated_tui_max_flows.is_some() { Err(anyhow!( "tui-max-flows in [tui] section is deprecated, use max-flows in [strategy] section instead" )) } else if cfg_file_tui_bindings.deprecated_toggle_privacy.is_some() { Err(anyhow!( "toggle-privacy in [bindings] section is deprecated, use expand-privacy and contract-privacy instead" )) } else { Ok(()) } } /// Validate privileges. fn validate_privilege( privilege_mode: PrivilegeMode, has_privileges: bool, needs_privileges: bool, ) -> anyhow::Result<()> { const PRIVILEGE_URL: &str = "https://github.com/fujiapple852/trippy#privileges"; match (privilege_mode, has_privileges, needs_privileges) { (PrivilegeMode::Privileged, true, _) | (PrivilegeMode::Unprivileged, _, false) => Ok(()), (PrivilegeMode::Privileged, false, true) => Err(anyhow!(format!( "privileges are required\n\nsee {PRIVILEGE_URL} for details" ))), (PrivilegeMode::Privileged, false, false) => Err(anyhow!(format!( "privileges are required (hint: try adding -u to run in unprivileged mode)\n\nsee {PRIVILEGE_URL} for details" ))), (PrivilegeMode::Unprivileged, false, true) => Err(anyhow!(format!( "unprivileged mode not supported on this platform\n\nsee {PRIVILEGE_URL} for details" ))), (PrivilegeMode::Unprivileged, true, true) => Err(anyhow!(format!( "unprivileged mode not supported on this platform (hint: process is privileged so disable unprivileged mode)\n\nsee {PRIVILEGE_URL} for details" ))), } } /// Validate the TUI custom columns. fn validate_tui_custom_columns(tui_custom_columns: &TuiColumns) -> anyhow::Result<()> { let duplicates = tui_custom_columns.find_duplicates(); if tui_custom_columns.0.is_empty() { Err(anyhow!( "Missing or no custom columns - The command line or config file value is blank" )) } else if duplicates.is_empty() { Ok(()) } else { let dup_str = duplicates.iter().join(", "); Err(anyhow!("Duplicate custom columns: {dup_str}")) } } /// Validate the logging mode. fn validate_logging(mode: Mode, verbose: bool) -> anyhow::Result<()> { if matches!(mode, Mode::Tui) && verbose { Err(anyhow!("cannot enable verbose logging in tui mode")) } else { Ok(()) } } /// Validate the multipath strategy against the privilege mode. fn validate_strategy(strategy: MultipathStrategy, unprivileged: bool) -> anyhow::Result<()> { match (strategy, unprivileged) { (MultipathStrategy::Dublin, true) => Err(anyhow!( "Dublin tracing strategy cannot be used in unprivileged mode" )), (MultipathStrategy::Paris, true) => Err(anyhow!( "Paris tracing strategy cannot be used in unprivileged mode" )), _ => Ok(()), } } /// Validate the protocol against the multipath strategy. fn validate_protocol_strategy( protocol: Protocol, strategy: MultipathStrategy, ) -> anyhow::Result<()> { match (protocol, strategy) { (Protocol::Tcp | Protocol::Icmp, MultipathStrategy::Classic) | (Protocol::Udp, _) => Ok(()), (Protocol::Icmp, MultipathStrategy::Paris) => { Err(anyhow!("Paris multipath strategy not support for icmp")) } (Protocol::Icmp, MultipathStrategy::Dublin) => { Err(anyhow!("Dublin multipath strategy not support for icmp")) } (Protocol::Tcp, MultipathStrategy::Paris) => Err(anyhow!( "Paris multipath strategy not yet supported for tcp" )), (Protocol::Tcp, MultipathStrategy::Dublin) => Err(anyhow!( "Dublin multipath strategy not yet supported for tcp" )), } } /// We only allow multiple targets to be specified for the Tui and for `Icmp` tracing. fn validate_multi( mode: Mode, protocol: Protocol, targets: &[String], dns_resolve_all: bool, ) -> anyhow::Result<()> { match (mode, protocol) { (Mode::Stream | Mode::Pretty | Mode::Markdown | Mode::Csv | Mode::Json, _) if targets.len() > 1 || dns_resolve_all => { Err(anyhow!( "only a single target may be specified for this mode" )) } (_, Protocol::Tcp | Protocol::Udp) if targets.len() > 1 || dns_resolve_all => Err(anyhow!( "only a single target may be specified for TCP and UDP tracing" )), _ => Ok(()), } } /// Validate that flows and dot mode are only used with paris or dublin /// multipath strategy. fn validate_flows(mode: Mode, strategy: MultipathStrategy) -> anyhow::Result<()> { match (mode, strategy) { (Mode::Flows | Mode::Dot, MultipathStrategy::Classic) => Err(anyhow!( "this mode requires the paris or dublin multipath strategy" )), _ => Ok(()), } } /// Validate `first_ttl` and `max_ttl`. fn validate_ttl(first_ttl: u8, max_ttl: u8) -> anyhow::Result<()> { if !(1..=MAX_TTL).contains(&first_ttl) { Err(anyhow!( "first-ttl ({first_ttl}) must be in the range 1..{MAX_TTL}" )) } else if !(1..=MAX_TTL).contains(&max_ttl) { Err(anyhow!( "max-ttl ({max_ttl}) must be in the range 1..{MAX_TTL}" )) } else if first_ttl > max_ttl { Err(anyhow!( "first-ttl ({first_ttl}) must be less than or equal to max-ttl ({max_ttl})" )) } else { Ok(()) } } /// Validate `max_inflight`. fn validate_max_inflight(max_inflight: u8) -> anyhow::Result<()> { if max_inflight == 0 { Err(anyhow!( "max-inflight ({max_inflight}) must be greater than zero" )) } else { Ok(()) } } /// Validate `read_timeout`. fn validate_read_timeout(read_timeout: Duration) -> anyhow::Result<()> { if read_timeout < constants::MIN_READ_TIMEOUT_MS || read_timeout > constants::MAX_READ_TIMEOUT_MS { Err(anyhow!( "read-timeout ({:?}) must be between {:?} and {:?} inclusive", read_timeout, constants::MIN_READ_TIMEOUT_MS, constants::MAX_READ_TIMEOUT_MS )) } else { Ok(()) } } /// Validate `min_round_duration` and `max_round_duration`. fn validate_round_duration( min_round_duration: Duration, max_round_duration: Duration, ) -> anyhow::Result<()> { if min_round_duration > max_round_duration { Err(anyhow!( "max-round-duration ({max_round_duration:?}) must not be less than min-round-duration ({min_round_duration:?})" )) } else { Ok(()) } } /// Validate `grace_duration`. fn validate_grace_duration(grace_duration: Duration) -> anyhow::Result<()> { if grace_duration < constants::MIN_GRACE_DURATION_MS || grace_duration > constants::MAX_GRACE_DURATION_MS { Err(anyhow!( "grace-duration ({:?}) must be between {:?} and {:?} inclusive", grace_duration, constants::MIN_GRACE_DURATION_MS, constants::MAX_GRACE_DURATION_MS )) } else { Ok(()) } } /// Validate `packet_size`. fn validate_packet_size(address_family: IpAddrFamily, packet_size: u16) -> anyhow::Result<()> { let min_size = match address_family { IpAddrFamily::Ipv4Only => constants::MIN_PACKET_SIZE_IPV4, IpAddrFamily::Ipv6Only | IpAddrFamily::Ipv6thenIpv4 | IpAddrFamily::Ipv4thenIpv6 | IpAddrFamily::System => constants::MIN_PACKET_SIZE_IPV6, }; if (min_size..=constants::MAX_PACKET_SIZE).contains(&packet_size) { Ok(()) } else { Err(anyhow!( "packet-size ({}) must be between {} and {} inclusive for {}", packet_size, min_size, constants::MAX_PACKET_SIZE, address_family, )) } } /// Validate `source_port`. fn validate_source_port(source_port: u16) -> anyhow::Result<()> { if source_port < 1024 { Err(anyhow!("source-port ({source_port}) must be >= 1024")) } else { Ok(()) } } /// Validate `tui_refresh_rate`. fn validate_tui_refresh_rate(tui_refresh_rate: Duration) -> anyhow::Result<()> { if tui_refresh_rate < constants::TUI_MIN_REFRESH_RATE_MS || tui_refresh_rate > constants::TUI_MAX_REFRESH_RATE_MS { Err(anyhow!( "tui-refresh-rate ({:?}) must be between {:?} and {:?} inclusive", tui_refresh_rate, constants::TUI_MIN_REFRESH_RATE_MS, constants::TUI_MAX_REFRESH_RATE_MS )) } else { Ok(()) } } /// Validate `report_cycles`. fn validate_report_cycles(report_cycles: usize) -> anyhow::Result<()> { if report_cycles == 0 { Err(anyhow!( "report-cycles ({report_cycles}) must be greater than zero" )) } else { Ok(()) } } /// Validate `dns_resolve_method` and `dns_lookup_as_info`. fn validate_dns(dns_resolve_method: ResolveMethod, dns_lookup_as_info: bool) -> anyhow::Result<()> { match dns_resolve_method { ResolveMethod::System if dns_lookup_as_info => Err(anyhow!( "AS lookup not supported by resolver `system` (use '-r' to choose another resolver)" )), _ => Ok(()), } } fn validate_geoip( tui_geoip_mode: GeoIpMode, geoip_mmdb_file: Option<&String>, ) -> anyhow::Result<()> { if matches!( tui_geoip_mode, GeoIpMode::Short | GeoIpMode::Long | GeoIpMode::Location ) && geoip_mmdb_file.is_none() { Err(anyhow!( "geoip-mmdb-file must be given for tui-geoip-mode of `{tui_geoip_mode:?}`" )) } else { Ok(()) } } /// Validate key bindings. fn validate_bindings(bindings: &TuiBindings) -> anyhow::Result<()> { let duplicates = bindings.find_duplicates(); if duplicates.is_empty() { Ok(()) } else { let dup_str = duplicates.iter().join(", "); Err(anyhow!("Duplicate key bindings: {dup_str}")) } } /// Validate `tos`. fn validate_tos(address_family: IpAddrFamily, tos: u8) -> anyhow::Result<()> { if cfg!(target_os = "windows") && address_family != IpAddrFamily::Ipv4Only && tos != 0 { Err(anyhow!( "setting tos is only supported for IPv4 on Windows (hint: try setting --ipv4 to enforce IPv4)" )) } else { Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::util::{insta, remove_whitespace}; use crossterm::event::KeyCode; use std::net::{Ipv4Addr, Ipv6Addr}; use std::str::FromStr; use test_case::test_case; use trippy_core::Port; #[test] fn test_config_default() { let args = args(&["trip", "example.com"]).unwrap(); let cfg_file = ConfigFile::default(); let platform = dummy_platform(); let config = TrippyConfig::build_config(args, cfg_file, &platform, 0).unwrap(); let expected = TrippyConfig { targets: vec![String::from("example.com")], ..TrippyConfig::default() }; pretty_assertions::assert_eq!(expected, config); } #[test] fn test_config_sample() { let args = args(&["trip", "example.com"]).unwrap(); let cfg_file: ConfigFile = toml::from_str(include_str!("../trippy-config-sample.toml")).unwrap(); let platform = dummy_platform(); let config = TrippyConfig::build_config(args, cfg_file, &platform, 0).unwrap(); let expected = TrippyConfig { targets: vec![String::from("example.com")], ..TrippyConfig::default() }; pretty_assertions::assert_eq!(expected, config); } #[test_case("trip"; "show default help")] #[test_case("trip -h"; "show short help")] #[test_case("trip --help"; "show long help")] fn test_help(cmd: &str) { compare_snapshot(cmd, parse_config(cmd)); } #[test_case("trip --version", Err(anyhow!(format!("trip {}", clap::crate_version!()))); "show version")] #[test_case("trip -V", Err(anyhow!(format!("trip {}", clap::crate_version!()))); "show version short")] fn test_version_help(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com --config-file trippy.toml", Ok(cfg().build()); "custom config file")] #[test_case("trip example.com -c trippy.toml", Ok(cfg().build()); "custom config file short")] fn test_config(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().mode(Mode::Tui).build()); "default mode")] #[test_case("trip example.com --mode tui", Ok(cfg().mode(Mode::Tui).build()); "tui mode")] #[test_case("trip example.com --mode stream", Ok(cfg().mode(Mode::Stream).build()); "stream mode")] #[test_case("trip example.com --mode pretty", Ok(cfg().mode(Mode::Pretty).max_rounds(Some(10)).build()); "pretty mode")] #[test_case("trip example.com --mode markdown", Ok(cfg().mode(Mode::Markdown).max_rounds(Some(10)).build()); "markdown mode")] #[test_case("trip example.com --mode csv", Ok(cfg().mode(Mode::Csv).max_rounds(Some(10)).build()); "csv mode")] #[test_case("trip example.com --mode json", Ok(cfg().mode(Mode::Json).max_rounds(Some(10)).build()); "json mode")] #[test_case("trip example.com --mode dot --udp -R paris", Ok(cfg().mode(Mode::Dot).max_rounds(Some(10)).multipath_strategy(MultipathStrategy::Paris).protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "dot mode")] #[test_case("trip example.com --mode flows --udp -R paris", Ok(cfg().mode(Mode::Flows).max_rounds(Some(10)).multipath_strategy(MultipathStrategy::Paris).protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "flows mode")] #[test_case("trip example.com --mode silent", Ok(cfg().mode(Mode::Silent).max_rounds(Some(10)).build()); "silent mode")] #[test_case("trip example.com -m tui", Ok(cfg().mode(Mode::Tui).build()); "tui mode short")] #[test_case("trip example.com --mode foo", Err(anyhow!(format!("error: invalid value 'foo' for '--mode ' [possible values: tui, stream, pretty, markdown, csv, json, dot, flows, silent] For more information, try '--help'."))); "invalid mode")] #[test_case("trip example.com --mode dot", Err(anyhow!(format!("this mode requires the paris or dublin multipath strategy"))); "invalid dot mode")] #[test_case("trip example.com --mode flows", Err(anyhow!(format!("this mode requires the paris or dublin multipath strategy"))); "invalid flows mode")] fn test_mode(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().build()); "single target")] #[test_case("trip example.com foo.com bar.com", Ok(cfg_multi().build()); "multiple targets")] #[test_case("trip example.com -U 20", Ok(cfg().max_inflight(20).build()); "single target before args")] #[test_case("trip -U 20 example.com", Ok(cfg().max_inflight(20).build()); "single target after args")] #[test_case("trip example.com foo.com bar.com -U 20", Ok(cfg_multi().max_inflight(20).build()); "multiple targets before args")] #[test_case("trip -U 20 example.com foo.com bar.com", Ok(cfg_multi().max_inflight(20).build()); "multiple targets after args")] #[test_case("trip example.com -U 20 foo.com --payload-pattern 255 bar.com", Ok(cfg_multi().max_inflight(20).payload_pattern(255).build()); "multiple targets between args")] fn test_target(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com --dummy", Err(anyhow!("error: unexpected argument '--dummy' found")); "invalid argument")] fn test_unexpected(cmd: &str, expected: anyhow::Result) { compare_lines(parse_config(cmd), expected, Some(1)); } #[test_case("trip example.com", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); "default strategy")] #[test_case("trip example.com --multipath-strategy classic", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); "classic strategy")] #[test_case("trip example.com -R classic", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); "classic strategy short")] #[test_case("trip example.com --multipath-strategy paris --udp", Ok(cfg().multipath_strategy(MultipathStrategy::Paris).protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "paris strategy")] #[test_case("trip example.com --multipath-strategy dublin --udp", Ok(cfg().multipath_strategy(MultipathStrategy::Dublin).protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "dublin strategy")] #[test_case("trip example.com --multipath-strategy tokyo", Err(anyhow!("error: invalid value 'tokyo' for '--multipath-strategy ' [possible values: classic, paris, dublin] For more information, try '--help'.")); "invalid strategy")] #[test_case("trip example.com --icmp --multipath-strategy paris", Err(anyhow!("Paris multipath strategy not support for icmp")); "paris with invalid protocol icmp")] #[test_case("trip example.com --icmp --multipath-strategy dublin", Err(anyhow!("Dublin multipath strategy not support for icmp")); "dublin with invalid protocol icmp")] #[test_case("trip example.com --tcp --multipath-strategy paris", Err(anyhow!("Paris multipath strategy not yet supported for tcp")); "paris with invalid protocol tcp")] #[test_case("trip example.com --tcp --multipath-strategy dublin", Err(anyhow!("Dublin multipath strategy not yet supported for tcp")); "dublin with invalid protocol tcp")] fn test_multipath(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "default protocol")] #[test_case("trip example.com --protocol icmp", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "icmp protocol")] #[test_case("trip example.com --protocol udp", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol")] #[test_case("trip example.com --protocol tcp", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); "tcp protocol")] #[test_case("trip example.com --protocol foo", Err(anyhow!("error: invalid value 'foo' for '--protocol ' [possible values: icmp, udp, tcp] For more information, try '--help'.")); "invalid protocol")] #[test_case("trip example.com -p icmp", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "icmp protocol short")] #[test_case("trip example.com -p udp", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol short")] #[test_case("trip example.com -p tcp", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); "tcp protocol short")] #[test_case("trip example.com -p foo", Err(anyhow!("error: invalid value 'foo' for '--protocol ' [possible values: icmp, udp, tcp] For more information, try '--help'.")); "invalid protocol short")] #[test_case("trip example.com --icmp", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); "icmp protocol shortcut")] #[test_case("trip example.com --udp", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol shortcut")] #[test_case("trip example.com --tcp", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); "tcp protocol shortcut")] fn test_protocol(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com --udp --source-port 2222", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(2222))).build()); "udp protocol custom src port")] #[test_case("trip example.com --udp --target-port 8888", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedDest(Port(8888))).build()); "udp protocol custom target port")] #[test_case("trip example.com --udp -S 2222", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(2222))).build()); "udp protocol custom src port short")] #[test_case("trip example.com --udp -P 8888", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedDest(Port(8888))).build()); "udp protocol custom target port short")] #[test_case("trip example.com --udp --source-port 123", Err(anyhow!("source-port (123) must be >= 1024")); "udp protocol invalid src port")] #[test_case("trip example.com --tcp --source-port 3333", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedSrc(Port(3333))).build()); "tcp protocol custom src port")] #[test_case("trip example.com --tcp --target-port 7777", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(7777))).build()); "tcp protocol custom target port")] #[test_case("trip example.com --udp --multipath-strategy paris", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Paris).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol paris strategy default ports")] #[test_case("trip example.com --udp --multipath-strategy paris --source-port 33434", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Paris).port_direction(PortDirection::FixedSrc(Port(33434))).build()); "udp protocol paris strategy custom src port")] #[test_case("trip example.com --udp --multipath-strategy paris --target-port 5000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Paris).port_direction(PortDirection::FixedDest(Port(5000))).build()); "udp protocol paris strategy custom target port")] #[test_case("trip example.com --udp --multipath-strategy paris --source-port 33434 --target-port 5000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Paris).port_direction(PortDirection::FixedBoth(Port(33434), Port(5000))).build()); "udp protocol paris strategy custom both ports")] #[test_case("trip example.com --udp --multipath-strategy dublin", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Dublin).port_direction(PortDirection::FixedSrc(Port(1024))).build()); "udp protocol dublin strategy default ports")] #[test_case("trip example.com --udp --multipath-strategy dublin --source-port 33434", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Dublin).port_direction(PortDirection::FixedSrc(Port(33434))).build()); "udp protocol dublin strategy custom src port")] #[test_case("trip example.com --udp --multipath-strategy dublin --target-port 5000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Dublin).port_direction(PortDirection::FixedDest(Port(5000))).build()); "udp protocol dublin strategy custom target port")] #[test_case("trip example.com --udp --multipath-strategy dublin --source-port 33434 --target-port 5000", Ok(cfg().protocol(Protocol::Udp).multipath_strategy(MultipathStrategy::Dublin).port_direction(PortDirection::FixedBoth(Port(33434), Port(5000))).build()); "udp protocol dublin strategy custom both ports")] #[test_case("trip example.com --udp --source-port 33434 --target-port 5000", Err(anyhow!("only one of source-port and target-port may be fixed (except IPv4/udp protocol with dublin or paris strategy)")); "udp protocol custom both ports with invalid strategy")] fn test_ports(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().addr_family(IpAddrFamily::System).build()); "default address family")] #[test_case("trip example.com --addr-family ipv4", Ok(cfg().addr_family(IpAddrFamily::Ipv4Only).build()); "ipv4 address family")] #[test_case("trip example.com --addr-family ipv6", Ok(cfg().addr_family(IpAddrFamily::Ipv6Only).build()); "ipv6 address family")] #[test_case("trip example.com --addr-family ipv4-then-ipv6", Ok(cfg().addr_family(IpAddrFamily::Ipv4thenIpv6).build()); "ipv4 then ipv6 address family")] #[test_case("trip example.com --addr-family ipv6-then-ipv4", Ok(cfg().addr_family(IpAddrFamily::Ipv6thenIpv4).build()); "ipv6 then ipv4 address family")] #[test_case("trip example.com --addr-family system", Ok(cfg().addr_family(IpAddrFamily::System).build()); "system address family")] #[test_case("trip example.com -F ipv4", Ok(cfg().addr_family(IpAddrFamily::Ipv4Only).build()); "custom address family short")] #[test_case("trip example.com --addr-family foo", Err(anyhow!("error: invalid value 'foo' for '--addr-family ' [possible values: ipv4, ipv6, ipv6-then-ipv4, ipv4-then-ipv6, system] For more information, try '--help'.")); "invalid address family")] #[test_case("trip example.com -4", Ok(cfg().addr_family(IpAddrFamily::Ipv4Only).build()); "ipv4 address family shortcut")] #[test_case("trip example.com -6", Ok(cfg().addr_family(IpAddrFamily::Ipv6Only).build()); "ipv6 address family shortcut")] #[test_case("trip example.com -5", Err(anyhow!("error: unexpected argument '-5' found tip: to pass '-5' as a value, use '-- -5' Usage: trip [OPTIONS] [TARGETS]... For more information, try '--help'.")); "invalid address family shortcut")] fn test_addr_family(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().first_ttl(1).build()); "default first ttl")] #[test_case("trip example.com --first-ttl 5", Ok(cfg().first_ttl(5).build()); "custom first ttl")] #[test_case("trip example.com -f 5", Ok(cfg().first_ttl(5).build()); "custom first ttl short")] #[test_case("trip example.com --first-ttl 0", Err(anyhow!("first-ttl (0) must be in the range 1..254")); "invalid low first ttl")] #[test_case("trip example.com --first-ttl 500", Err(anyhow!("error: invalid value '500' for '--first-ttl ': 500 is not in 0..=255 For more information, try '--help'.")); "invalid high first ttl")] #[test_case("trip example.com", Ok(cfg().first_ttl(1).build()); "default max ttl")] #[test_case("trip example.com --max-ttl 5", Ok(cfg().max_ttl(5).build()); "custom max ttl")] #[test_case("trip example.com -t 5", Ok(cfg().max_ttl(5).build()); "custom max ttl short")] #[test_case("trip example.com --max-ttl 0", Err(anyhow!("max-ttl (0) must be in the range 1..254")); "invalid low max ttl")] #[test_case("trip example.com --max-ttl 500", Err(anyhow!("error: invalid value '500' for '--max-ttl ': 500 is not in 0..=255 For more information, try '--help'.")); "invalid high max ttl")] #[test_case("trip example.com --first-ttl 3 --max-ttl 2", Err(anyhow!("first-ttl (3) must be less than or equal to max-ttl (2)")); "first ttl greater than max ttl")] #[test_case("trip example.com --first-ttl 5 --max-ttl 5", Ok(cfg().first_ttl(5).max_ttl(5).build()); "custom first and max ttl")] fn test_ttl(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().min_round_duration(Duration::from_millis(1000)).build()); "default min round duration")] #[test_case("trip example.com --min-round-duration 250ms", Ok(cfg().min_round_duration(Duration::from_millis(250)).build()); "custom min round duration")] #[test_case("trip example.com -i 250ms", Ok(cfg().min_round_duration(Duration::from_millis(250)).build()); "custom min round duration short")] #[test_case("trip example.com --min-round-duration 0", Ok(cfg().min_round_duration(Duration::from_millis(0)).build()); "zero min round duration")] #[test_case("trip example.com", Ok(cfg().min_round_duration(Duration::from_millis(1000)).build()); "default max round duration")] #[test_case("trip example.com --max-round-duration 1250ms", Ok(cfg().max_round_duration(Duration::from_millis(1250)).build()); "custom max round duration")] #[test_case("trip example.com -T 2s", Ok(cfg().max_round_duration(Duration::from_millis(2000)).build()); "custom max round duration short")] #[test_case("trip example.com --max-round-duration 0", Err(anyhow!("max-round-duration (0ns) must not be less than min-round-duration (1s)")); "invalid format max round duration")] #[test_case("trip example.com -i 250ms -T 250ms", Ok(cfg().min_round_duration(Duration::from_millis(250)).max_round_duration(Duration::from_millis(250)).build()); "custom min and max round duration")] #[test_case("trip example.com -i 300ms -T 250ms", Err(anyhow!("max-round-duration (250ms) must not be less than min-round-duration (300ms)")); "min round duration greater than max")] fn test_round_duration(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().grace_duration(Duration::from_millis(100)).build()); "default grace duration")] #[test_case("trip example.com --grace-duration 10ms", Ok(cfg().grace_duration(Duration::from_millis(10)).build()); "custom grace duration")] #[test_case("trip example.com -g 50ms", Ok(cfg().grace_duration(Duration::from_millis(50)).build()); "custom grace duration short")] #[test_case("trip example.com --grace-duration 0", Err(anyhow!("grace-duration (0ns) must be between 10ms and 1s inclusive")); "invalid format grace duration")] #[test_case("trip example.com --grace-duration 9ms", Err(anyhow!("grace-duration (9ms) must be between 10ms and 1s inclusive")); "invalid low grace duration")] #[test_case("trip example.com --grace-duration 1001ms", Err(anyhow!("grace-duration (1.001s) must be between 10ms and 1s inclusive")); "invalid high grace duration")] fn test_grace_duration(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().max_inflight(24).build()); "default max inflight")] #[test_case("trip example.com --max-inflight 12", Ok(cfg().max_inflight(12).build()); "custom max inflight")] #[test_case("trip example.com -U 20", Ok(cfg().max_inflight(20).build()); "custom max inflight short")] #[test_case("trip example.com --max-inflight foo", Err(anyhow!("error: invalid value 'foo' for '--max-inflight ': invalid digit found in string For more information, try '--help'.")); "invalid format max inflight")] #[test_case("trip example.com --max-inflight 0", Err(anyhow!("max-inflight (0) must be greater than zero")); "invalid low max inflight")] #[test_case("trip example.com --max-inflight 300", Err(anyhow!("error: invalid value '300' for '--max-inflight ': 300 is not in 0..=255 For more information, try '--help'.")); "invalid high max inflight")] fn test_max_inflight(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().initial_sequence(33434).build()); "default initial sequence")] #[test_case("trip example.com --initial-sequence 5000", Ok(cfg().initial_sequence(5000).build()); "custom initial sequence")] #[test_case("trip example.com --initial-sequence foo", Err(anyhow!("error: invalid value 'foo' for '--initial-sequence ': invalid digit found in string For more information, try '--help'. ")); "invalid format initial sequence")] #[test_case("trip example.com --initial-sequence 100000", Err(anyhow!("error: invalid value '100000' for '--initial-sequence ': 100000 is not in 0..=65535 For more information, try '--help'.")); "invalid high initial sequence")] fn test_init_seq(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tos(0).build()); "default tos")] #[test_case("trip example.com --tos 255 -4", Ok(cfg().tos(0xFF).addr_family(IpAddrFamily::Ipv4Only).build()); "custom tos")] #[test_case("trip example.com -Q 255 -4", Ok(cfg().tos(0xFF).addr_family(IpAddrFamily::Ipv4Only).build()); "custom tos short")] #[test_case("trip example.com --tos foo", Err(anyhow!("error: invalid value 'foo' for '--tos ': invalid digit found in string For more information, try '--help'.")); "invalid format tos")] #[test_case("trip example.com --tos 300", Err(anyhow!("error: invalid value '300' for '--tos ': 300 is not in 0..=255 For more information, try '--help'.")); "invalid high tos")] #[cfg_attr(target_os = "windows", test_case("trip example.com --tos 123 --ipv6", Err(anyhow!("setting tos is only supported for IPv4 on Windows (hint: try setting --ipv4 to enforce IPv4)")); "invalid windows ipv6"))] fn test_tos(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().icmp_extension_parse_mode(IcmpExtensionParseMode::Disabled).build()); "default icmp extensions")] #[test_case("trip example.com --icmp-extensions", Ok(cfg().icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled).build()); "enabled icmp extensions")] #[test_case("trip example.com -e", Ok(cfg().icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled).build()); "enabled icmp extensions short")] fn test_icmp_extensions(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().read_timeout(Duration::from_millis(10)).build()); "default read timeout")] #[test_case("trip example.com --read-timeout 20ms", Ok(cfg().read_timeout(Duration::from_millis(20)).build()); "custom read timeout")] #[test_case("trip example.com --read-timeout 20", Err(anyhow!("error: invalid value '20' for '--read-timeout ': time unit needed, for example 20sec or 20ms For more information, try '--help'.")); "invalid custom read timeout")] #[test_case("trip example.com --read-timeout 9ms", Err(anyhow!("read-timeout (9ms) must be between 10ms and 100ms inclusive")); "invalid low custom read timeout")] #[test_case("trip example.com --read-timeout 101ms", Err(anyhow!("read-timeout (101ms) must be between 10ms and 100ms inclusive")); "invalid high custom read timeout")] fn test_read_timeout(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().packet_size(84).build()); "default packet size")] #[test_case("trip example.com --packet-size 120", Ok(cfg().packet_size(120).build()); "custom packet size")] #[test_case("trip example.com --packet-size foo", Err(anyhow!("error: invalid value 'foo' for '--packet-size ': invalid digit found in string For more information, try '--help'.")); "invalid format packet size")] #[test_case("trip example.com --packet-size 47 -F ipv4-then-ipv6", Err(anyhow!("packet-size (47) must be between 48 and 1024 inclusive for Ipv4thenIpv6")); "invalid low packet size for ipv4 then ipv6")] #[test_case("trip example.com --packet-size 47 -F ipv6-then-ipv4", Err(anyhow!("packet-size (47) must be between 48 and 1024 inclusive for Ipv6thenIpv4")); "invalid low packet size for ipv6 then ipv4")] #[test_case("trip example.com --packet-size 27 -F ipv4", Err(anyhow!("packet-size (27) must be between 28 and 1024 inclusive for Ipv4Only")); "invalid low packet size for ipv4")] #[test_case("trip example.com --packet-size 1025 -F ipv4", Err(anyhow!("packet-size (1025) must be between 28 and 1024 inclusive for Ipv4Only")); "invalid high packet size for ipv4")] #[test_case("trip example.com --packet-size 47 -F ipv6", Err(anyhow!("packet-size (47) must be between 48 and 1024 inclusive for Ipv6Only")); "invalid low packet size for ipv6")] #[test_case("trip example.com --packet-size 1025 -F ipv6", Err(anyhow!("packet-size (1025) must be between 48 and 1024 inclusive for Ipv6Only")); "invalid high packet size for ipv6")] #[test_case("trip example.com --packet-size 100000", Err(anyhow!("error: invalid value '100000' for '--packet-size ': 100000 is not in 0..=65535 For more information, try '--help'.")); "invalid out of range packet size")] fn test_packet_size(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().payload_pattern(0).build()); "default payload pattern size")] #[test_case("trip example.com --payload-pattern 255", Ok(cfg().payload_pattern(0xFF).build()); "custom payload pattern")] #[test_case("trip example.com --payload-pattern foo", Err(anyhow!("error: invalid value 'foo' for '--payload-pattern ': invalid digit found in string For more information, try '--help'.")); "invalid format payload pattern")] #[test_case("trip example.com --payload-pattern 256", Err(anyhow!("error: invalid value '256' for '--payload-pattern ': 256 is not in 0..=255 For more information, try '--help'.")); "invalid out of range payload pattern")] fn test_payload_pattern(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().source_addr(None).build()); "default source address")] #[test_case("trip example.com --source-address 10.0.0.1", Ok(cfg().source_addr(Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))).build()); "custom ipv4 source address")] #[test_case("trip example.com --source-address 2404:6800:4005:81a::200e", Ok(cfg().source_addr(Some(IpAddr::V6(Ipv6Addr::from_str("2404:6800:4005:81a::200e").unwrap()))).build()); "custom ipv6 source address")] #[test_case("trip example.com -A 10.0.0.1", Ok(cfg().source_addr(Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)))).build()); "custom ipv4 source address short")] #[test_case("trip example.com --source-address foobar", Err(anyhow!("error: invalid value 'foobar' for '--source-address ': invalid IP address syntax For more information, try '--help'.")); "invalid source address")] fn test_source_address(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().interface(None).build()); "default interface")] #[test_case("trip example.com --interface en0", Ok(cfg().interface(Some(String::from("en0"))).build()); "custom interface")] #[test_case("trip example.com -I tun0", Ok(cfg().interface(Some(String::from("tun0"))).build()); "custom interface short")] fn test_interface(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().dns_timeout(Duration::from_millis(5000)).build()); "default dns timeout")] #[test_case("trip example.com --dns-timeout 20ms", Ok(cfg().dns_timeout(Duration::from_millis(20)).build()); "custom dns timeout")] #[test_case("trip example.com --dns-timeout 20", Err(anyhow!("error: invalid value '20' for '--dns-timeout ': time unit needed, for example 20sec or 20ms For more information, try '--help'.")); "invalid custom dns timeout")] fn test_dns_timeout(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().dns_ttl(Duration::from_secs(300)).build()); "default dns ttl")] #[test_case("trip example.com --dns-ttl 10secs", Ok(cfg().dns_ttl(Duration::from_secs(10)).build()); "custom dns ttl")] #[test_case("trip example.com --dns-ttl 20", Err(anyhow!("error: invalid value '20' for '--dns-ttl ': time unit needed, for example 20sec or 20ms For more information, try '--help'.")); "invalid custom dns ttl")] fn test_dns_ttl(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().dns_resolve_method(ResolveMethod::System).build()); "default resolve method")] #[test_case("trip example.com --dns-resolve-method system", Ok(cfg().dns_resolve_method(ResolveMethod::System).build()); "custom resolve method system")] #[test_case("trip example.com -r system", Ok(cfg().dns_resolve_method(ResolveMethod::System).build()); "custom resolve method system short")] #[test_case("trip example.com --dns-resolve-method google", Ok(cfg().dns_resolve_method(ResolveMethod::Google).build()); "custom resolve method google")] #[test_case("trip example.com --dns-resolve-method cloudflare", Ok(cfg().dns_resolve_method(ResolveMethod::Cloudflare).build()); "custom resolve method cloudflare")] #[test_case("trip example.com --dns-resolve-method resolv", Ok(cfg().dns_resolve_method(ResolveMethod::Resolv).build()); "custom resolve method resolv")] #[test_case("trip example.com --dns-resolve-method foobar", Err(anyhow!("error: invalid value 'foobar' for '--dns-resolve-method ' [possible values: system, resolv, google, cloudflare] For more information, try '--help'.")); "invalid resolve method")] fn test_dns_resolve(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().dns_resolve_all(false).build()); "default dns resolve all")] #[test_case("trip example.com --dns-resolve-all", Ok(cfg().dns_resolve_all(true).build()); "custom dns resolve all")] #[test_case("trip example.com -y", Ok(cfg().dns_resolve_all(true).build()); "custom dns resolve all short")] fn test_dns_resolve_all(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().dns_lookup_as_info(false).build()); "default dns lookup as info")] #[test_case("trip example.com --dns-lookup-as-info -r resolv", Ok(cfg().dns_lookup_as_info(true).dns_resolve_method(ResolveMethod::Resolv).build()); "custom dns lookup as info")] #[test_case("trip example.com -z -r resolv", Ok(cfg().dns_lookup_as_info(true).dns_resolve_method(ResolveMethod::Resolv).build()); "custom dns lookup as info short")] #[test_case("trip example.com --dns-lookup-as-info", Err(anyhow!("AS lookup not supported by resolver `system` (use '-r' to choose another resolver)")); "invalid resolve method for as info")] fn test_lookup_as_info(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().max_samples(256).build()); "default max samples")] #[test_case("trip example.com --max-samples 100", Ok(cfg().max_samples(100).build()); "custom max samples")] #[test_case("trip example.com -s 100", Ok(cfg().max_samples(100).build()); "custom max samples short")] #[test_case("trip example.com --max-samples foo", Err(anyhow!("error: invalid value 'foo' for '--max-samples ': invalid digit found in string For more information, try '--help'.")); "invalid max samples")] fn test_max_samples(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().max_flows(64).build()); "default max flows")] #[test_case("trip example.com --max-flows 100", Ok(cfg().max_flows(100).build()); "custom max flows")] #[test_case("trip example.com --max-flows foo", Err(anyhow!("error: invalid value 'foo' for '--max-flows ': invalid digit found in string For more information, try '--help'.")); "invalid max flows")] fn test_max_flows(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_preserve_screen(false).build()); "default tui preserve screen")] #[test_case("trip example.com --tui-preserve-screen", Ok(cfg().tui_preserve_screen(true).build()); "enable tui preserve screen")] fn test_tui_preserve_screen(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_refresh_rate(Duration::from_millis(100)).build()); "default tui refresh rate")] #[test_case("trip example.com --tui-refresh-rate 200ms", Ok(cfg().tui_refresh_rate(Duration::from_millis(200)).build()); "custom tui refresh rate")] #[test_case("trip example.com --tui-refresh-rate 49ms", Err(anyhow!("tui-refresh-rate (49ms) must be between 50ms and 1s inclusive")); "invalid low tui refresh rate")] #[test_case("trip example.com --tui-refresh-rate 1001ms", Err(anyhow!("tui-refresh-rate (1.001s) must be between 50ms and 1s inclusive")); "invalid high tui refresh rate")] #[test_case("trip example.com --tui-refresh-rate foo", Err(anyhow!("error: invalid value 'foo' for '--tui-refresh-rate ': expected number at 0 For more information, try '--help'.")); "invalid format tui refresh rate")] #[test_case("trip example.com --tui-refresh-rate 10xx", Err(anyhow!("error: invalid value '10xx' for '--tui-refresh-rate ': unknown time unit \"xx\", supported units: ns, us/µs, ms, sec, min, hours, days, weeks, months, years (and few variations)\nFor more information, try '--help'.")); "invalid time unit tui refresh rate")] fn test_tui_refresh_rate(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_privacy_max_ttl(None).build()); "default tui privacy max ttl")] #[test_case("trip example.com --tui-privacy-max-ttl 4", Ok(cfg().tui_privacy_max_ttl(Some(4)).build()); "custom tui privacy max ttl")] #[test_case("trip example.com --tui-privacy-max-ttl foo", Err(anyhow!("error: invalid value 'foo' for '--tui-privacy-max-ttl ': invalid digit found in string For more information, try '--help'.")); "invalid tui privacy max ttl")] fn test_tui_privacy_max_ttl(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_locale(None).build()); "default tui locale")] #[test_case("trip example.com --tui-locale fr", Ok(cfg().tui_locale(Some(String::from("fr"))).build()); "custom tui locale")] fn test_tui_locale(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_timezone(None).build()); "default tui timezone")] #[test_case("trip example.com --tui-timezone UTC", Ok(cfg().tui_timezone(Some(chrono_tz::Tz::UTC)).build()); "custom tui timezone UTC")] #[test_case("trip example.com --tui-timezone Antarctica/South_Pole", Ok(cfg().tui_timezone(Some(chrono_tz::Tz::Antarctica__South_Pole)).build()); "custom tui timezone Antarctica/South_Pole")] #[test_case("trip example.com --tui-timezone xxx", Err(anyhow!("failed to parse timezone")); "invalid tui timezone")] fn test_tui_timezone(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_address_mode(AddressMode::Host).build()); "default tui address mode")] #[test_case("trip example.com --tui-address-mode ip", Ok(cfg().tui_address_mode(AddressMode::Ip).build()); "ip tui address mode")] #[test_case("trip example.com --tui-address-mode host", Ok(cfg().tui_address_mode(AddressMode::Host).build()); "host tui address mode")] #[test_case("trip example.com --tui-address-mode both", Ok(cfg().tui_address_mode(AddressMode::Both).build()); "both tui address mode")] #[test_case("trip example.com -a both", Ok(cfg().tui_address_mode(AddressMode::Both).build()); "custom tui address mode short")] #[test_case("trip example.com --tui-address-mode foo", Err(anyhow!("error: invalid value 'foo' for '--tui-address-mode ' [possible values: ip, host, both] For more information, try '--help'.")); "invalid tui address mode")] fn test_tui_address_mode(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_as_mode(AsMode::Asn).build()); "default tui as mode")] #[test_case("trip example.com --tui-as-mode asn", Ok(cfg().tui_as_mode(AsMode::Asn).build()); "asn tui as mode")] #[test_case("trip example.com --tui-as-mode prefix", Ok(cfg().tui_as_mode(AsMode::Prefix).build()); "prefix tui as mode")] #[test_case("trip example.com --tui-as-mode country-code", Ok(cfg().tui_as_mode(AsMode::CountryCode).build()); "country code tui as mode")] #[test_case("trip example.com --tui-as-mode registry", Ok(cfg().tui_as_mode(AsMode::Registry).build()); "registry tui as mode")] #[test_case("trip example.com --tui-as-mode allocated", Ok(cfg().tui_as_mode(AsMode::Allocated).build()); "allocated tui as mode")] #[test_case("trip example.com --tui-as-mode name", Ok(cfg().tui_as_mode(AsMode::Name).build()); "name tui as mode")] #[test_case("trip example.com --tui-as-mode foo", Err(anyhow!("error: invalid value 'foo' for '--tui-as-mode ' [possible values: asn, prefix, country-code, registry, allocated, name] For more information, try '--help'.")); "invalid tui as mode")] fn test_tui_as_mode(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_custom_columns(TuiColumns::default()).build()); "default tui custom columns")] #[test_case("trip example.com --tui-custom-columns hol", Ok(cfg().tui_custom_columns(TuiColumns(vec![TuiColumn::Ttl, TuiColumn::Host, TuiColumn::LossPct])).build()); "custom tui custom columns")] #[test_case("trip example.com --tui-custom-columns hh", Err(anyhow!("Duplicate custom columns: h")); "invalid duplicate tui custom columns")] #[test_case("trip example.com --tui-custom-columns u", Err(anyhow!("unknown column code: u")); "invalid unknown tui custom columns")] fn test_tui_custom_columns(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Off).build()); "default tui icmp extension mode")] #[test_case("trip example.com --tui-icmp-extension-mode off", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Off).build()); "off tui icmp extension mode")] #[test_case("trip example.com --tui-icmp-extension-mode mpls", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Mpls).build()); "mpls tui icmp extension mode")] #[test_case("trip example.com --tui-icmp-extension-mode full", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Full).build()); "full tui icmp extension mode")] #[test_case("trip example.com --tui-icmp-extension-mode all", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::All).build()); "all tui icmp extension mode")] #[test_case("trip example.com --tui-icmp-extension-mode foo", Err(anyhow!("error: invalid value 'foo' for '--tui-icmp-extension-mode ' [possible values: off, mpls, full, all] For more information, try '--help'.")); "invalid tui icmp extension mode")] fn test_tui_icmp_extension_mode(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_geoip_mode(GeoIpMode::Off).build()); "default tui geoip mode")] #[test_case("trip example.com --tui-geoip-mode off", Ok(cfg().tui_geoip_mode(GeoIpMode::Off).build()); "off tui geoip mode")] #[test_case("trip example.com --tui-geoip-mode short --geoip-mmdb-file foo.mmdb", Ok(cfg().tui_geoip_mode(GeoIpMode::Short).geoip_mmdb_file(Some(String::from("foo.mmdb"))).build()); "short tui geoip mode")] #[test_case("trip example.com --tui-geoip-mode long --geoip-mmdb-file foo.mmdb", Ok(cfg().tui_geoip_mode(GeoIpMode::Long).geoip_mmdb_file(Some(String::from("foo.mmdb"))).build()); "long tui geoip mode")] #[test_case("trip example.com --tui-geoip-mode location --geoip-mmdb-file foo.mmdb", Ok(cfg().tui_geoip_mode(GeoIpMode::Location).geoip_mmdb_file(Some(String::from("foo.mmdb"))).build()); "location tui geoip mode")] #[test_case("trip example.com --tui-geoip-mode short", Err(anyhow!("geoip-mmdb-file must be given for tui-geoip-mode of `Short`")); "custom tui geoip mode without geoip")] #[test_case("trip example.com --tui-geoip-mode foo", Err(anyhow!("error: invalid value 'foo' for '--tui-geoip-mode ' [possible values: off, short, long, location] For more information, try '--help'.")); "invalid tui geoip mode")] fn test_tui_geoip_mode(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_max_addrs(None).build()); "default tui max addrs")] #[test_case("trip example.com --tui-max-addrs 5", Ok(cfg().tui_max_addrs(Some(5)).build()); "custom tui max addrs")] #[test_case("trip example.com -M 7", Ok(cfg().tui_max_addrs(Some(7)).build()); "custom tui max addrs short")] #[test_case("trip example.com --tui-max-addrs foo", Err(anyhow!("error: invalid value 'foo' for '--tui-max-addrs ': invalid digit found in string For more information, try '--help'.")); "invalid tui max addrs")] fn test_tui_max_addrs(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_theme(TuiTheme::default()).build()); "default tui theme")] #[test_case("trip example.com --tui-theme-colors bg-color=red", Ok(cfg().tui_theme(TuiTheme { bg: TuiColor::Red, ..Default::default() }).build()); "custom tui theme named color")] #[test_case("trip example.com --tui-theme-colors bg-color=010203", Ok(cfg().tui_theme(TuiTheme { bg: TuiColor::Rgb(1, 2, 3), ..Default::default() }).build()); "custom tui theme hex color")] #[test_case("trip example.com --tui-theme-colors bg-color=red,text-color=blue", Ok(cfg().tui_theme(TuiTheme { bg: TuiColor::Red, text: TuiColor::Blue, ..Default::default() }).build()); "custom tui theme multiple")] #[test_case("trip example.com --tui-theme-colors bg-color=0", Err(anyhow!("error: invalid value 'bg-color=0' for '--tui-theme-colors ': unknown color: 0 For more information, try '--help'.")); "invalid tui theme truncated hex value")] #[test_case("trip example.com --tui-theme-colors bg-color=foo", Err(anyhow!("error: invalid value 'bg-color=foo' for '--tui-theme-colors ': unknown color: foo For more information, try '--help'. ")); "invalid tui theme invalid named color")] #[test_case("trip example.com --tui-theme-colors foo-color=red", Err(anyhow!("error: invalid value 'foo-color=red' for '--tui-theme-colors ': Matching variant not found For more information, try '--help'.")); "invalid tui theme invalid item")] #[test_case("trip example.com --tui-theme-colors foo", Err(anyhow!("error: invalid value 'foo' for '--tui-theme-colors ': invalid theme value: expected format `item=value` For more information, try '--help'.")); "invalid tui theme invalid syntax")] #[test_case("trip example.com --tui-theme-colors bg-color=red, text-color=blue", Err(anyhow!("error: invalid value '' for '--tui-theme-colors ': invalid theme value: expected format `item=value` For more information, try '--help'.")); "invalid tui theme invalid multiple with space")] fn test_tui_theme(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().tui_bindings(TuiBindings::default()).build()); "default tui bindings")] #[test_case("trip example.com --tui-key-bindings toggle-help=h", Ok(cfg().tui_bindings(TuiBindings { toggle_help: TuiKeyBinding::new(KeyCode::Char('h')), ..Default::default() }).build()); "custom tui bindings")] #[test_case("trip example.com --tui-key-bindings toggle-help=h,toggle-map=m", Ok(cfg().tui_bindings(TuiBindings { toggle_help: TuiKeyBinding::new(KeyCode::Char('h')), toggle_map: TuiKeyBinding::new(KeyCode::Char('m')), ..Default::default() }).build()); "custom tui bindings multiple")] #[test_case("trip example.com --tui-key-bindings foo=h", Err(anyhow!("error: invalid value 'foo=h' for '--tui-key-bindings ': Matching variant not found For more information, try '--help'.")); "invalid tui binding command")] #[test_case("trip example.com --tui-key-bindings toggle-help=123", Err(anyhow!("error: invalid value 'toggle-help=123' for '--tui-key-bindings ': unknown key binding '123' For more information, try '--help'.")); "invalid tui binding key")] #[test_case("trip example.com --tui-key-bindings toggle-help=h,toggle-map=h", Err(anyhow!("Duplicate key bindings: h: [toggle-map and toggle-help]")); "invalid tui binding duplicate binding")] #[test_case("trip example.com --tui-key-bindings toggle-help=h, toggle-map=m", Err(anyhow!("error: invalid value '' for '--tui-key-bindings ': invalid binding value: expected format `item=value` For more information, try '--help'.")); "invalid tui binding multiple with space")] fn test_tui_bindings(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().report_cycles(10).max_rounds(None).build()); "default report cycles")] #[test_case("trip example.com --mode csv --report-cycles 5", Ok(cfg().report_cycles(5).mode(Mode::Csv).max_rounds(Some(5)).build()); "custom report cycles")] #[test_case("trip example.com --mode pretty -C 5", Ok(cfg().report_cycles(5).mode(Mode::Pretty).max_rounds(Some(5)).build()); "custom report cycles short")] #[test_case("trip example.com --report-cycles 0", Err(anyhow!("report-cycles (0) must be greater than zero")); "invalid low report cycles")] #[test_case("trip example.com --report-cycles foo", Err(anyhow!("error: invalid value 'foo' for '--report-cycles ': invalid digit found in string For more information, try '--help'.")); "invalid report cycles")] fn test_report_cycles(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().geoip_mmdb_file(None).build()); "default geoip mmdb file")] #[test_case("trip example.com --geoip-mmdb-file foo.mmdb", Ok(cfg().geoip_mmdb_file(Some(String::from("foo.mmdb"))).build()); "custom geoip mmdb file")] #[test_case("trip example.com -G foo.mmdb", Ok(cfg().geoip_mmdb_file(Some(String::from("foo.mmdb"))).build()); "custom geoip mmdb file short")] fn test_geoip_mmdb_file(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().verbose(false).build()); "default verbose")] #[test_case("trip example.com --mode silent --verbose", Ok(cfg().verbose(true).mode(Mode::Silent).max_rounds(Some(10)).build()); "enable verbose")] #[test_case("trip example.com --mode silent -v", Ok(cfg().verbose(true).mode(Mode::Silent).max_rounds(Some(10)).build()); "enable verbose short")] #[test_case("trip example.com --mode tui --verbose", Err(anyhow!("cannot enable verbose logging in tui mode")); "invalid verbose mode")] fn test_verbose(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().log_filter(String::from("trippy=debug")).build()); "default log filter")] #[test_case("trip example.com --log-filter info,trippy=trace", Ok(cfg().log_filter(String::from("info,trippy=trace")).build()); "custom log filter")] fn test_log_filter(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().log_format(LogFormat::Pretty).build()); "default log format")] #[test_case("trip example.com --log-format compact", Ok(cfg().log_format(LogFormat::Compact).build()); "compact log format")] #[test_case("trip example.com --log-format pretty", Ok(cfg().log_format(LogFormat::Pretty).build()); "pretty log format")] #[test_case("trip example.com --log-format json", Ok(cfg().log_format(LogFormat::Json).build()); "json log format")] #[test_case("trip example.com --log-format chrome", Ok(cfg().log_format(LogFormat::Chrome).build()); "chrome log format")] #[test_case("trip example.com --log-format foo", Err(anyhow!("error: invalid value 'foo' for '--log-format ' [possible values: compact, pretty, json, chrome] For more information, try '--help'.")); "invalid log format")] fn test_log_format(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", Ok(cfg().log_span_events(LogSpanEvents::Off).build()); "default log span")] #[test_case("trip example.com --log-span-events off", Ok(cfg().log_span_events(LogSpanEvents::Off).build()); "off log span")] #[test_case("trip example.com --log-span-events active", Ok(cfg().log_span_events(LogSpanEvents::Active).build()); "active log span")] #[test_case("trip example.com --log-span-events full", Ok(cfg().log_span_events(LogSpanEvents::Full).build()); "full log span")] #[test_case("trip example.com --log-span-events foo", Err(anyhow!("error: invalid value 'foo' for '--log-span-events ' [possible values: off, active, full] For more information, try '--help'.")); "invalid log span")] fn test_log_span(cmd: &str, expected: anyhow::Result) { compare(parse_config(cmd), expected); } #[test_case("trip example.com", true, false, Ok(cfg().privilege_mode(PrivilegeMode::Privileged).build()); "default privilege mode")] #[test_case("trip example.com --unprivileged", true, false, Ok(cfg().privilege_mode(PrivilegeMode::Unprivileged).build()); "unprivileged mode")] #[test_case("trip example.com -u", true, false, Ok(cfg().privilege_mode(PrivilegeMode::Unprivileged).build()); "unprivileged mode short")] #[test_case("trip example.com --unprivileged --udp --multipath-strategy paris", true, false, Err(anyhow!(format!("Paris tracing strategy cannot be used in unprivileged mode"))); "invalid unprivileged mode for paris")] #[test_case("trip example.com --unprivileged --udp --multipath-strategy dublin", true, false, Err(anyhow!(format!("Dublin tracing strategy cannot be used in unprivileged mode"))); "invalid unprivileged mode for dublin")] #[test_case("trip example.com", true, true, Ok(cfg().privilege_mode(PrivilegeMode::Privileged).build()); "has privilege and needs")] #[test_case("trip example.com", false, false, Err(anyhow!("privileges are required (hint: try adding -u to run in unprivileged mode)\n\nsee https://github.com/fujiapple852/trippy#privileges for details")); "no privilege and not needs")] #[test_case("trip example.com", false, true, Err(anyhow!("privileges are required\n\nsee https://github.com/fujiapple852/trippy#privileges for details")); "no privilege and needs")] #[test_case("trip example.com --unprivileged", false, false, Ok(cfg().privilege_mode(PrivilegeMode::Unprivileged).build()); "no privilege and not needs in unprivileged mode")] #[test_case("trip example.com --unprivileged", false, true, Err(anyhow!("unprivileged mode not supported on this platform\n\nsee https://github.com/fujiapple852/trippy#privileges for details")); "no privilege and needs in unprivileged mode")] #[test_case("trip example.com --unprivileged", true, true, Err(anyhow!("unprivileged mode not supported on this platform (hint: process is privileged so disable unprivileged mode)\n\nsee https://github.com/fujiapple852/trippy#privileges for details")); "has privilege and needs in unprivileged mode")] fn test_privilege( cmd: &str, has_privileges: bool, needs_privileges: bool, expected: anyhow::Result, ) { compare( parse_config_with_privileges(cmd, has_privileges, needs_privileges), expected, ); } #[test_case("trip --print-config-template", Ok(TrippyAction::PrintConfigTemplate); "print config template")] #[test_case("trip --print-tui-binding-commands", Ok(TrippyAction::PrintTuiBindingCommands); "print the tui binding commands")] #[test_case("trip --print-tui-theme-items", Ok(TrippyAction::PrintTuiThemeItems); "print the tui theme items")] #[test_case("trip --generate elvish", Ok(TrippyAction::PrintShellCompletions(Shell::Elvish)); "generate elvish shell completions")] #[test_case("trip --generate fish", Ok(TrippyAction::PrintShellCompletions(Shell::Fish)); "generate fish shell completions")] #[test_case("trip --generate powershell", Ok(TrippyAction::PrintShellCompletions(Shell::PowerShell)); "generate powershell shell completions")] #[test_case("trip --generate zsh", Ok(TrippyAction::PrintShellCompletions(Shell::Zsh)); "generate zsh shell completions")] #[test_case("trip --generate bash", Ok(TrippyAction::PrintShellCompletions(Shell::Bash)); "generate bash shell completions")] #[test_case("trip --generate foo", Err(anyhow!("error: invalid value 'foo' for '--generate ' [possible values: bash, elvish, fish, powershell, zsh] For more information, try '--help'.")); "generate invalid shell completions")] #[test_case("trip --generate-man", Ok(TrippyAction::PrintManPage); "generate man page")] #[test_case("trip --print-locales", Ok(TrippyAction::PrintLocales); "print all locales")] fn test_action(cmd: &str, expected: anyhow::Result) { compare(parse_action(cmd), expected); } #[test_case("trip example.com --tui-max-samples foo", Err(anyhow!("error: unexpected argument '--tui-max-samples' found")); "deprecated tui max samples")] #[test_case("trip example.com --tui-max-flows foo", Err(anyhow!("error: unexpected argument '--tui-max-flows' found")); "deprecated tui max flows")] #[test_case("trip example.com --tui-key-bindings toggle-privacy=o", Err(anyhow!("error: invalid value 'toggle-privacy=o' for '--tui-key-bindings ': toggle-privacy is deprecated, use expand-privacy and contract-privacy instead")); "deprecated toggle-privacy key binding")] fn test_deprecated(cmd: &str, expected: anyhow::Result) { compare_lines(parse_config(cmd), expected, Some(0)); } fn parse_action(cmd: &str) -> anyhow::Result { TrippyAction::from(parse(cmd)?, &dummy_platform(), 0) } fn parse_config(cmd: &str) -> anyhow::Result { let args = parse(cmd)?; let cfg_file = ConfigFile::default(); let platform = dummy_platform(); TrippyConfig::build_config(args, cfg_file, &platform, 0) } fn parse_config_with_privileges( cmd: &str, has_privileges: bool, needs_privileges: bool, ) -> anyhow::Result { let args = parse(cmd)?; let cfg_file = ConfigFile::default(); let privilege = Privilege::new(has_privileges, needs_privileges); TrippyConfig::build_config(args, cfg_file, &privilege, 0) } fn parse(cmd: &str) -> anyhow::Result { use clap::Parser; Ok(Args::try_parse_from( cmd.split(' ').map(std::ffi::OsString::from), )?) } fn compare(actual: anyhow::Result, expected: anyhow::Result) where T: PartialEq + Eq + std::fmt::Debug, { compare_lines(actual, expected, None); } fn compare_lines( actual: anyhow::Result, expected: anyhow::Result, lines: Option, ) where T: PartialEq + Eq + std::fmt::Debug, { match (actual, expected) { (Ok(cfg), Ok(exp)) => { pretty_assertions::assert_eq!(cfg, exp); } (Err(err), Err(exp_err)) => { if let Some(lines) = lines { let fst = err .to_string() .lines() .nth(lines) .map(ToString::to_string) .unwrap_or_default(); let snd = exp_err .to_string() .lines() .nth(lines) .map(ToString::to_string) .unwrap_or_default(); if remove_whitespace(fst) != remove_whitespace(snd) { pretty_assertions::assert_eq!(err.to_string(), exp_err.to_string()); } } else if remove_whitespace(err.to_string()) != remove_whitespace(exp_err.to_string()) { pretty_assertions::assert_eq!(err.to_string(), exp_err.to_string()); } } (Ok(_), Err(exp_err)) => { panic!("expected err {}", exp_err.to_string().trim()); } (Err(err), Ok(_)) => { panic!("unexpected err {}", err.to_string().trim()); } } } fn compare_snapshot(name: &str, actual: anyhow::Result) where T: std::fmt::Debug, { insta(name, || match actual { Ok(act) => { insta::assert_debug_snapshot!(act); } Err(err) => { insta::assert_snapshot!(remove_whitespace(err.to_string())); } }); } fn cfg() -> TrippyConfigBuilder { TrippyConfigBuilder::new(vec![String::from("example.com")]) } fn cfg_multi() -> TrippyConfigBuilder { TrippyConfigBuilder::new(vec![ String::from("example.com"), String::from("foo.com"), String::from("bar.com"), ]) } const fn dummy_platform() -> Privilege { Privilege::new(true, false) } fn args(args: &[&str]) -> anyhow::Result { use clap::Parser; Ok(Args::try_parse_from( args.iter().map(std::ffi::OsString::from), )?) } pub struct TrippyConfigBuilder { config: TrippyConfig, } impl TrippyConfigBuilder { pub fn new(targets: Vec) -> Self { Self { config: TrippyConfig { targets, ..TrippyConfig::default() }, } } pub fn mode(self, mode: Mode) -> Self { Self { config: TrippyConfig { mode, ..self.config }, } } pub fn privilege_mode(self, privilege_mode: PrivilegeMode) -> Self { Self { config: TrippyConfig { privilege_mode, ..self.config }, } } pub fn protocol(self, protocol: Protocol) -> Self { Self { config: TrippyConfig { protocol, ..self.config }, } } pub fn addr_family(self, addr_family: IpAddrFamily) -> Self { Self { config: TrippyConfig { addr_family, ..self.config }, } } pub fn first_ttl(self, first_ttl: u8) -> Self { Self { config: TrippyConfig { first_ttl, ..self.config }, } } pub fn max_ttl(self, max_ttl: u8) -> Self { Self { config: TrippyConfig { max_ttl, ..self.config }, } } pub fn min_round_duration(self, min_round_duration: Duration) -> Self { Self { config: TrippyConfig { min_round_duration, ..self.config }, } } pub fn max_round_duration(self, max_round_duration: Duration) -> Self { Self { config: TrippyConfig { max_round_duration, ..self.config }, } } pub fn grace_duration(self, grace_duration: Duration) -> Self { Self { config: TrippyConfig { grace_duration, ..self.config }, } } pub fn max_inflight(self, max_inflight: u8) -> Self { Self { config: TrippyConfig { max_inflight, ..self.config }, } } pub fn initial_sequence(self, initial_sequence: u16) -> Self { Self { config: TrippyConfig { initial_sequence, ..self.config }, } } pub fn tos(self, tos: u8) -> Self { Self { config: TrippyConfig { tos, ..self.config }, } } pub fn icmp_extension_parse_mode( self, icmp_extension_parse_mode: IcmpExtensionParseMode, ) -> Self { Self { config: TrippyConfig { icmp_extension_parse_mode, ..self.config }, } } pub fn read_timeout(self, read_timeout: Duration) -> Self { Self { config: TrippyConfig { read_timeout, ..self.config }, } } pub fn packet_size(self, packet_size: u16) -> Self { Self { config: TrippyConfig { packet_size, ..self.config }, } } pub fn payload_pattern(self, payload_pattern: u8) -> Self { Self { config: TrippyConfig { payload_pattern, ..self.config }, } } pub fn source_addr(self, source_addr: Option) -> Self { Self { config: TrippyConfig { source_addr, ..self.config }, } } pub fn interface(self, interface: Option) -> Self { Self { config: TrippyConfig { interface, ..self.config }, } } pub fn port_direction(self, port_direction: PortDirection) -> Self { Self { config: TrippyConfig { port_direction, ..self.config }, } } pub fn multipath_strategy(self, multipath_strategy: MultipathStrategy) -> Self { Self { config: TrippyConfig { multipath_strategy, ..self.config }, } } pub fn dns_timeout(self, dns_timeout: Duration) -> Self { Self { config: TrippyConfig { dns_timeout, ..self.config }, } } pub fn dns_ttl(self, dns_ttl: Duration) -> Self { Self { config: TrippyConfig { dns_ttl, ..self.config }, } } pub fn dns_resolve_method(self, dns_resolve_method: ResolveMethod) -> Self { Self { config: TrippyConfig { dns_resolve_method, ..self.config }, } } pub fn dns_lookup_as_info(self, dns_lookup_as_info: bool) -> Self { Self { config: TrippyConfig { dns_lookup_as_info, ..self.config }, } } pub fn dns_resolve_all(self, dns_resolve_all: bool) -> Self { Self { config: TrippyConfig { dns_resolve_all, ..self.config }, } } pub fn max_samples(self, tui_max_samples: usize) -> Self { Self { config: TrippyConfig { max_samples: tui_max_samples, ..self.config }, } } pub fn max_flows(self, max_flows: usize) -> Self { Self { config: TrippyConfig { max_flows, ..self.config }, } } pub fn tui_preserve_screen(self, tui_preserve_screen: bool) -> Self { Self { config: TrippyConfig { tui_preserve_screen, ..self.config }, } } pub fn tui_refresh_rate(self, tui_refresh_rate: Duration) -> Self { Self { config: TrippyConfig { tui_refresh_rate, ..self.config }, } } pub fn tui_privacy_max_ttl(self, tui_privacy_max_ttl: Option) -> Self { Self { config: TrippyConfig { tui_privacy_max_ttl, ..self.config }, } } pub fn tui_locale(self, tui_locale: Option) -> Self { Self { config: TrippyConfig { tui_locale, ..self.config }, } } pub fn tui_timezone(self, tui_timezone: Option) -> Self { Self { config: TrippyConfig { tui_timezone, ..self.config }, } } pub fn tui_address_mode(self, tui_address_mode: AddressMode) -> Self { Self { config: TrippyConfig { tui_address_mode, ..self.config }, } } pub fn tui_as_mode(self, tui_as_mode: AsMode) -> Self { Self { config: TrippyConfig { tui_as_mode, ..self.config }, } } pub fn tui_custom_columns(self, tui_custom_columns: TuiColumns) -> Self { Self { config: TrippyConfig { tui_custom_columns, ..self.config }, } } pub fn tui_icmp_extension_mode(self, tui_icmp_extension_mode: IcmpExtensionMode) -> Self { Self { config: TrippyConfig { tui_icmp_extension_mode, ..self.config }, } } pub fn tui_geoip_mode(self, tui_geoip_mode: GeoIpMode) -> Self { Self { config: TrippyConfig { tui_geoip_mode, ..self.config }, } } pub fn tui_max_addrs(self, tui_max_addrs: Option) -> Self { Self { config: TrippyConfig { tui_max_addrs, ..self.config }, } } pub fn tui_theme(self, tui_theme: TuiTheme) -> Self { Self { config: TrippyConfig { tui_theme, ..self.config }, } } #[expect(clippy::large_types_passed_by_value)] pub fn tui_bindings(self, tui_bindings: TuiBindings) -> Self { Self { config: TrippyConfig { tui_bindings, ..self.config }, } } pub fn report_cycles(self, report_cycles: usize) -> Self { Self { config: TrippyConfig { report_cycles, ..self.config }, } } pub fn geoip_mmdb_file(self, geoip_mmdb_file: Option) -> Self { Self { config: TrippyConfig { geoip_mmdb_file, ..self.config }, } } pub fn max_rounds(self, max_rounds: Option) -> Self { Self { config: TrippyConfig { max_rounds, ..self.config }, } } pub fn verbose(self, verbose: bool) -> Self { Self { config: TrippyConfig { verbose, ..self.config }, } } pub fn log_format(self, log_format: LogFormat) -> Self { Self { config: TrippyConfig { log_format, ..self.config }, } } pub fn log_filter(self, log_filter: String) -> Self { Self { config: TrippyConfig { log_filter, ..self.config }, } } pub fn log_span_events(self, log_span_events: LogSpanEvents) -> Self { Self { config: TrippyConfig { log_span_events, ..self.config }, } } pub fn build(self) -> TrippyConfig { self.config } } } ================================================ FILE: crates/trippy-tui/src/frontend/binding.rs ================================================ use crate::config::{TuiBindings, TuiKeyBinding}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use itertools::Itertools; use std::fmt::{Display, Formatter}; /// Tui key bindings. #[derive(Debug, Clone, Copy)] pub struct Bindings { pub toggle_help: KeyBinding, pub toggle_help_alt: KeyBinding, pub toggle_settings: KeyBinding, pub toggle_settings_tui: KeyBinding, pub toggle_settings_trace: KeyBinding, pub toggle_settings_dns: KeyBinding, pub toggle_settings_geoip: KeyBinding, pub toggle_settings_bindings: KeyBinding, pub toggle_settings_theme: KeyBinding, pub toggle_settings_columns: KeyBinding, pub previous_hop: KeyBinding, pub next_hop: KeyBinding, pub previous_trace: KeyBinding, pub next_trace: KeyBinding, pub previous_hop_address: KeyBinding, pub next_hop_address: KeyBinding, pub address_mode_ip: KeyBinding, pub address_mode_host: KeyBinding, pub address_mode_both: KeyBinding, pub toggle_freeze: KeyBinding, pub toggle_chart: KeyBinding, pub toggle_map: KeyBinding, pub toggle_flows: KeyBinding, pub expand_privacy: KeyBinding, pub contract_privacy: KeyBinding, pub expand_hosts: KeyBinding, pub contract_hosts: KeyBinding, pub expand_hosts_max: KeyBinding, pub contract_hosts_min: KeyBinding, pub chart_zoom_in: KeyBinding, pub chart_zoom_out: KeyBinding, pub clear_trace_data: KeyBinding, pub clear_dns_cache: KeyBinding, pub clear_selection: KeyBinding, pub toggle_as_info: KeyBinding, pub toggle_hop_details: KeyBinding, pub quit: KeyBinding, pub quit_preserve_screen: KeyBinding, } impl From for Bindings { fn from(value: TuiBindings) -> Self { Self { toggle_help: KeyBinding::from(value.toggle_help), toggle_help_alt: KeyBinding::from(value.toggle_help_alt), toggle_settings: KeyBinding::from(value.toggle_settings), toggle_settings_tui: KeyBinding::from(value.toggle_settings_tui), toggle_settings_trace: KeyBinding::from(value.toggle_settings_trace), toggle_settings_dns: KeyBinding::from(value.toggle_settings_dns), toggle_settings_geoip: KeyBinding::from(value.toggle_settings_geoip), toggle_settings_bindings: KeyBinding::from(value.toggle_settings_bindings), toggle_settings_theme: KeyBinding::from(value.toggle_settings_theme), toggle_settings_columns: KeyBinding::from(value.toggle_settings_columns), previous_hop: KeyBinding::from(value.previous_hop), next_hop: KeyBinding::from(value.next_hop), previous_trace: KeyBinding::from(value.previous_trace), next_trace: KeyBinding::from(value.next_trace), previous_hop_address: KeyBinding::from(value.previous_hop_address), next_hop_address: KeyBinding::from(value.next_hop_address), address_mode_ip: KeyBinding::from(value.address_mode_ip), address_mode_host: KeyBinding::from(value.address_mode_host), address_mode_both: KeyBinding::from(value.address_mode_both), toggle_freeze: KeyBinding::from(value.toggle_freeze), toggle_chart: KeyBinding::from(value.toggle_chart), toggle_map: KeyBinding::from(value.toggle_map), toggle_flows: KeyBinding::from(value.toggle_flows), expand_privacy: KeyBinding::from(value.expand_privacy), contract_privacy: KeyBinding::from(value.contract_privacy), expand_hosts: KeyBinding::from(value.expand_hosts), contract_hosts: KeyBinding::from(value.contract_hosts), expand_hosts_max: KeyBinding::from(value.expand_hosts_max), contract_hosts_min: KeyBinding::from(value.contract_hosts_min), chart_zoom_in: KeyBinding::from(value.chart_zoom_in), chart_zoom_out: KeyBinding::from(value.chart_zoom_out), clear_trace_data: KeyBinding::from(value.clear_trace_data), clear_dns_cache: KeyBinding::from(value.clear_dns_cache), clear_selection: KeyBinding::from(value.clear_selection), toggle_as_info: KeyBinding::from(value.toggle_as_info), toggle_hop_details: KeyBinding::from(value.toggle_hop_details), quit: KeyBinding::from(value.quit), quit_preserve_screen: KeyBinding::from(value.quit_preserve_screen), } } } /// Tui key binding. #[derive(Debug, Clone, Copy)] pub struct KeyBinding { pub code: KeyCode, pub modifiers: KeyModifiers, } impl KeyBinding { pub fn check(&self, event: KeyEvent) -> bool { let code_match = match (event.code, self.code) { (KeyCode::Char(c1), KeyCode::Char(c2)) => c1.eq_ignore_ascii_case(&c2), (c1, c2) => c1 == c2, }; code_match && self.modifiers == event.modifiers } } impl From for KeyBinding { fn from(value: TuiKeyBinding) -> Self { Self { code: value.code, modifiers: value.modifier, } } } impl Display for KeyBinding { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let modifiers = &[ self.modifiers .contains(KeyModifiers::SHIFT) .then_some("shift"), self.modifiers .contains(KeyModifiers::CONTROL) .then_some("ctrl"), self.modifiers.contains(KeyModifiers::ALT).then_some("alt"), self.modifiers .contains(KeyModifiers::SUPER) .then_some("super"), self.modifiers .contains(KeyModifiers::HYPER) .then_some("hyper"), self.modifiers .contains(KeyModifiers::META) .then_some("meta"), ] .into_iter() .flatten() .join("+"); if !modifiers.is_empty() { write!(f, "{modifiers}+")?; } match self.code { KeyCode::Backspace => write!(f, "backspace"), KeyCode::Enter => write!(f, "enter"), KeyCode::Left => write!(f, "left"), KeyCode::Right => write!(f, "right"), KeyCode::Up => write!(f, "up"), KeyCode::Down => write!(f, "down"), KeyCode::Home => write!(f, "home"), KeyCode::End => write!(f, "end"), KeyCode::PageUp => write!(f, "pageup"), KeyCode::PageDown => write!(f, "pagedown"), KeyCode::Tab => write!(f, "tab"), KeyCode::BackTab => write!(f, "backtab"), KeyCode::Delete => write!(f, "delete"), KeyCode::Insert => write!(f, "insert"), KeyCode::Char(c) => write!(f, "{c}"), KeyCode::Esc => write!(f, "esc"), _ => write!(f, "unknown"), } } } pub const CTRL_C: KeyBinding = KeyBinding { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, }; ================================================ FILE: crates/trippy-tui/src/frontend/columns.rs ================================================ use crate::config::{TuiColumn, TuiColumns}; use crate::t; use ratatui::layout::{Constraint, Rect}; use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use strum::{EnumIter, IntoEnumIterator}; use unicode_width::UnicodeWidthStr; /// The columns to display in the hops table of the TUI. #[derive(Debug, Clone, Eq, PartialEq)] pub struct Columns(Vec); impl Columns { /// Column width constraints. /// /// All columns are returned as `Constraint::Min(width)`. /// /// For `Fixed(n)` columns the width is as specified in `n`. /// For `Variable` columns the width is calculated by subtracting the total /// size of all `Fixed` columns from the width of the containing `Rect` and /// dividing by the number of `Variable` columns. pub fn constraints(&self, rect: Rect) -> Vec { let total_fixed_width = self .columns() .map(|c| match c.typ.width() { ColumnWidth::Fixed(width) => width, ColumnWidth::Variable => 0, }) .sum(); let variable_width_count = self .columns() .filter(|c| matches!(c.typ.width(), ColumnWidth::Variable)) .count() as u16; let variable_width = rect.width.saturating_sub(total_fixed_width) / variable_width_count.max(1); self.columns() .map(|c| match c.typ.width() { ColumnWidth::Fixed(width) => Constraint::Min(width), ColumnWidth::Variable => Constraint::Min(variable_width), }) .collect() } pub fn columns(&self) -> impl Iterator { self.0 .iter() .filter(|c| matches!(c.status, ColumnStatus::Shown)) } pub fn all_columns(&self) -> impl Iterator { self.0.iter() } pub fn all_columns_count(&self) -> usize { self.0.len() } pub fn toggle(&mut self, index: usize) { self.0[index].status = match self.0[index].status { ColumnStatus::Shown => ColumnStatus::Hidden, ColumnStatus::Hidden => ColumnStatus::Shown, }; } pub fn move_down(&mut self, index: usize) { if index < self.0.len() { let removed = self.0.remove(index); self.0.insert(index + 1, removed); } } pub fn move_up(&mut self, index: usize) { if index > 0 { let removed = self.0.remove(index); self.0.insert(index - 1, removed); } } } impl From for Columns { fn from(value: TuiColumns) -> Self { let enabled: Vec<_> = value.0.into_iter().map(Column::from).collect(); let disabled: Vec<_> = ColumnType::iter() .filter(|ct| enabled.iter().all(|c| c.typ != *ct)) .map(Column::new_hidden) .collect(); let all = enabled.into_iter().chain(disabled).collect(); Self(all) } } impl Display for Columns { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let output: Vec = self .0 .iter() .filter_map(|c| { if c.status == ColumnStatus::Shown { Some(c.typ.into()) } else { None } }) .collect(); write!(f, "{}", String::from_iter(output)) } } #[derive(Debug, Clone, Eq, PartialEq)] pub struct Column { pub typ: ColumnType, pub status: ColumnStatus, } impl Column { pub const fn new_shown(typ: ColumnType) -> Self { Self { typ, status: ColumnStatus::Shown, } } pub const fn new_hidden(typ: ColumnType) -> Self { Self { typ, status: ColumnStatus::Hidden, } } } #[derive(Debug, Clone, Eq, PartialEq)] pub enum ColumnStatus { Shown, Hidden, } impl Display for ColumnStatus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Shown => write!(f, "{}", t!("on")), Self::Hidden => write!(f, "{}", t!("off")), } } } /// A TUI hops table column. #[derive(Debug, Copy, Clone, Eq, PartialEq, EnumIter)] pub enum ColumnType { /// The ttl for a hop. Ttl, /// The hostname for a hostname. Host, /// The packet loss % for a hop. LossPct, /// The number of probes sent for a hop. Sent, /// The number of responses received for a hop. Received, /// The last RTT for a hop. Last, /// The rolling average RTT for a hop. Average, /// The best RTT for a hop. Best, /// The worst RTT for a hop. Worst, /// The stddev of RTT for a hop. StdDev, /// The status of a hop. Status, /// The current jitter i.e. round-trip difference with the last round-trip. Jitter, /// The average jitter time for all probes at this hop. Javg, /// The worst round-trip jitter time for all probes at this hop. Jmax, /// The smoothed jitter value for all probes at this hop. Jinta, /// The source port for last probe for this hop. LastSrcPort, /// The destination port for last probe for this hop. LastDestPort, /// The sequence number for the last probe for this hop. LastSeq, /// The icmp packet type for the last probe for this hop. LastIcmpPacketType, /// The icmp packet code for the last probe for this hop. LastIcmpPacketCode, /// The NAT detection status for the last probe for this hop. LastNatStatus, /// The number of probes that failed for a hop. Failed, /// The number of probes with forward loss for a hop. Floss, /// The number of probes with backward loss for a hop. Bloss, /// The forward loss % for a hop. FlossPct, /// The Differentiated Services Code Point of the Original Datagram for a hop. Dscp, /// The Explicit Congestion Notification of the Original Datagram for a hop. Ecn, /// The autonomous system number for a hop. Asn, } impl From for char { fn from(col_type: ColumnType) -> Self { match col_type { ColumnType::Ttl => 'h', ColumnType::Host => 'o', ColumnType::LossPct => 'l', ColumnType::Sent => 's', ColumnType::Received => 'r', ColumnType::Last => 'a', ColumnType::Average => 'v', ColumnType::Best => 'b', ColumnType::Worst => 'w', ColumnType::StdDev => 'd', ColumnType::Status => 't', ColumnType::Jitter => 'j', ColumnType::Javg => 'g', ColumnType::Jmax => 'x', ColumnType::Jinta => 'i', ColumnType::LastSrcPort => 'S', ColumnType::LastDestPort => 'P', ColumnType::LastSeq => 'Q', ColumnType::LastIcmpPacketType => 'T', ColumnType::LastIcmpPacketCode => 'C', ColumnType::LastNatStatus => 'N', ColumnType::Failed => 'f', ColumnType::Floss => 'F', ColumnType::Bloss => 'B', ColumnType::FlossPct => 'D', ColumnType::Dscp => 'K', ColumnType::Ecn => 'M', ColumnType::Asn => 'A', } } } impl From for Column { fn from(value: TuiColumn) -> Self { match value { TuiColumn::Ttl => Self::new_shown(ColumnType::Ttl), TuiColumn::Host => Self::new_shown(ColumnType::Host), TuiColumn::LossPct => Self::new_shown(ColumnType::LossPct), TuiColumn::Sent => Self::new_shown(ColumnType::Sent), TuiColumn::Received => Self::new_shown(ColumnType::Received), TuiColumn::Last => Self::new_shown(ColumnType::Last), TuiColumn::Average => Self::new_shown(ColumnType::Average), TuiColumn::Best => Self::new_shown(ColumnType::Best), TuiColumn::Worst => Self::new_shown(ColumnType::Worst), TuiColumn::StdDev => Self::new_shown(ColumnType::StdDev), TuiColumn::Status => Self::new_shown(ColumnType::Status), TuiColumn::Jitter => Self::new_shown(ColumnType::Jitter), TuiColumn::Javg => Self::new_shown(ColumnType::Javg), TuiColumn::Jmax => Self::new_shown(ColumnType::Jmax), TuiColumn::Jinta => Self::new_shown(ColumnType::Jinta), TuiColumn::LastSrcPort => Self::new_shown(ColumnType::LastSrcPort), TuiColumn::LastDestPort => Self::new_shown(ColumnType::LastDestPort), TuiColumn::LastSeq => Self::new_shown(ColumnType::LastSeq), TuiColumn::LastIcmpPacketType => Self::new_shown(ColumnType::LastIcmpPacketType), TuiColumn::LastIcmpPacketCode => Self::new_shown(ColumnType::LastIcmpPacketCode), TuiColumn::LastNatStatus => Self::new_shown(ColumnType::LastNatStatus), TuiColumn::Failed => Self::new_shown(ColumnType::Failed), TuiColumn::Floss => Self::new_shown(ColumnType::Floss), TuiColumn::Bloss => Self::new_shown(ColumnType::Bloss), TuiColumn::FlossPct => Self::new_shown(ColumnType::FlossPct), TuiColumn::Dscp => Self::new_shown(ColumnType::Dscp), TuiColumn::Ecn => Self::new_shown(ColumnType::Ecn), TuiColumn::Asn => Self::new_shown(ColumnType::Asn), } } } impl Display for ColumnType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name()) } } impl ColumnType { /// The name of the column in the current locale. pub(self) fn name(&self) -> Cow<'_, str> { match self { Self::Ttl => Cow::Borrowed("#"), Self::Host => t!("column_host"), Self::LossPct => t!("column_loss_pct"), Self::Sent => t!("column_snd"), Self::Received => t!("column_recv"), Self::Last => t!("column_last"), Self::Average => t!("column_avg"), Self::Best => t!("column_best"), Self::Worst => t!("column_wrst"), Self::StdDev => t!("column_stdev"), Self::Status => t!("column_sts"), Self::Jitter => t!("column_jttr"), Self::Javg => t!("column_javg"), Self::Jmax => t!("column_jmax"), Self::Jinta => t!("column_jint"), Self::LastSrcPort => t!("column_sprt"), Self::LastDestPort => t!("column_dprt"), Self::LastSeq => t!("column_seq"), Self::LastIcmpPacketType => t!("column_type"), Self::LastIcmpPacketCode => t!("column_code"), Self::LastNatStatus => t!("column_nat"), Self::Failed => t!("column_fail"), Self::Floss => t!("column_floss"), Self::Bloss => t!("column_bloss"), Self::FlossPct => t!("column_floss_pct"), Self::Dscp => t!("column_dscp"), Self::Ecn => t!("column_ecn"), Self::Asn => t!("column_asn"), } } /// The width of the column. /// /// For most columns the width is calculated based on the column name in /// the current locale. /// /// For the `Ttl` column the width is fixed as it is always a single /// character. /// /// The `Host` column is variable as it should use the remaining space. pub(self) fn width(self) -> ColumnWidth { let width = self.name().width() as u16 + 2; #[expect(clippy::match_same_arms)] match self { Self::Ttl => ColumnWidth::Fixed(4), Self::Host => ColumnWidth::Variable, Self::LossPct => ColumnWidth::Fixed(width.max(8)), Self::Sent => ColumnWidth::Fixed(width.max(7)), Self::Received => ColumnWidth::Fixed(width.max(7)), Self::Last => ColumnWidth::Fixed(width.max(7)), Self::Average => ColumnWidth::Fixed(width.max(7)), Self::Best => ColumnWidth::Fixed(width.max(7)), Self::Worst => ColumnWidth::Fixed(width.max(7)), Self::StdDev => ColumnWidth::Fixed(width.max(8)), Self::Status => ColumnWidth::Fixed(width.max(7)), Self::Jitter => ColumnWidth::Fixed(width.max(7)), Self::Javg => ColumnWidth::Fixed(width.max(7)), Self::Jmax => ColumnWidth::Fixed(width.max(7)), Self::Jinta => ColumnWidth::Fixed(width.max(8)), Self::LastSrcPort => ColumnWidth::Fixed(width.max(7)), Self::LastDestPort => ColumnWidth::Fixed(width.max(7)), Self::LastSeq => ColumnWidth::Fixed(width.max(7)), Self::LastIcmpPacketType => ColumnWidth::Fixed(width.max(7)), Self::LastIcmpPacketCode => ColumnWidth::Fixed(width.max(7)), Self::LastNatStatus => ColumnWidth::Fixed(width.max(7)), Self::Failed => ColumnWidth::Fixed(width.max(7)), Self::Floss => ColumnWidth::Fixed(width.max(7)), Self::Bloss => ColumnWidth::Fixed(width.max(7)), Self::FlossPct => ColumnWidth::Fixed(width.max(8)), Self::Dscp => ColumnWidth::Fixed(width.max(7)), Self::Ecn => ColumnWidth::Fixed(width.max(7)), Self::Asn => ColumnWidth::Fixed(width.max(8)), } } } /// Table column layout constraints. #[derive(Debug, PartialEq)] enum ColumnWidth { /// A fixed size column. Fixed(u16), /// A column that will use the remaining space. Variable, } #[cfg(test)] mod tests { use super::*; use ratatui::layout::Constraint::Min; use test_case::test_case; #[test] fn test_columns_conversion_from_tui_columns() { let tui_columns = TuiColumns(vec![ TuiColumn::Ttl, TuiColumn::Host, TuiColumn::LossPct, TuiColumn::Sent, TuiColumn::Received, TuiColumn::Last, TuiColumn::Average, TuiColumn::Best, TuiColumn::Worst, TuiColumn::StdDev, TuiColumn::Status, ]); let columns = Columns::from(tui_columns); assert_eq!( columns, Columns(vec![ Column::new_shown(ColumnType::Ttl), Column::new_shown(ColumnType::Host), Column::new_shown(ColumnType::LossPct), Column::new_shown(ColumnType::Sent), Column::new_shown(ColumnType::Received), Column::new_shown(ColumnType::Last), Column::new_shown(ColumnType::Average), Column::new_shown(ColumnType::Best), Column::new_shown(ColumnType::Worst), Column::new_shown(ColumnType::StdDev), Column::new_shown(ColumnType::Status), Column::new_hidden(ColumnType::Jitter), Column::new_hidden(ColumnType::Javg), Column::new_hidden(ColumnType::Jmax), Column::new_hidden(ColumnType::Jinta), Column::new_hidden(ColumnType::LastSrcPort), Column::new_hidden(ColumnType::LastDestPort), Column::new_hidden(ColumnType::LastSeq), Column::new_hidden(ColumnType::LastIcmpPacketType), Column::new_hidden(ColumnType::LastIcmpPacketCode), Column::new_hidden(ColumnType::LastNatStatus), Column::new_hidden(ColumnType::Failed), Column::new_hidden(ColumnType::Floss), Column::new_hidden(ColumnType::Bloss), Column::new_hidden(ColumnType::FlossPct), Column::new_hidden(ColumnType::Dscp), Column::new_hidden(ColumnType::Ecn), Column::new_hidden(ColumnType::Asn), ]) ); } #[test] fn test_column_conversion_from_tui_column() { let tui_column = TuiColumn::Received; let column = Column::from(tui_column); assert_eq!(column.typ, ColumnType::Received); assert_eq!(column.status, ColumnStatus::Shown); } #[test_case(ColumnType::Ttl, "#")] #[test_case(ColumnType::Host, "Host")] #[test_case(ColumnType::LossPct, "Loss%")] #[test_case(ColumnType::Sent, "Snd")] #[test_case(ColumnType::Received, "Recv")] #[test_case(ColumnType::Last, "Last")] #[test_case(ColumnType::Average, "Avg")] #[test_case(ColumnType::Best, "Best")] #[test_case(ColumnType::Worst, "Wrst")] #[test_case(ColumnType::StdDev, "StDev")] #[test_case(ColumnType::Status, "Sts")] #[test_case(ColumnType::Asn, "ASN")] fn test_column_display_formatting(c: ColumnType, heading: &'static str) { assert_eq!(format!("{c}"), heading); } #[test_case(ColumnType::Ttl, & ColumnWidth::Fixed(4))] #[test_case(ColumnType::Host, & ColumnWidth::Variable)] #[test_case(ColumnType::Asn, & ColumnWidth::Fixed(8))] #[test_case(ColumnType::LossPct, & ColumnWidth::Fixed(8))] fn test_column_width(column_type: ColumnType, width: &ColumnWidth) { assert_eq!(column_type.width(), *width); } #[test] fn test_column_constraints() { let columns = Columns::from(TuiColumns::default()); let constraints = columns.constraints(Rect::new(0, 0, 80, 0)); assert_eq!( vec![ Min(4), Min(11), Min(8), Min(7), Min(7), Min(7), Min(7), Min(7), Min(7), Min(8), Min(7) ], constraints ); } /// Expect to test the Column Into flow. #[test] fn test_columns_into_string_short() { let cols = Columns(vec![ Column::new_shown(ColumnType::Ttl), Column::new_shown(ColumnType::Host), Column::new_shown(ColumnType::LossPct), Column::new_shown(ColumnType::Sent), ]); assert_eq!("hols", format!("{cols}")); } /// Happy path test for full set of columns. #[test] fn test_columns_into_string_happy_path() { let cols = Columns(vec![ Column::new_shown(ColumnType::Ttl), Column::new_shown(ColumnType::Host), Column::new_shown(ColumnType::LossPct), Column::new_shown(ColumnType::Sent), Column::new_shown(ColumnType::Received), Column::new_shown(ColumnType::Last), Column::new_shown(ColumnType::Average), Column::new_shown(ColumnType::Best), Column::new_shown(ColumnType::Worst), Column::new_shown(ColumnType::StdDev), Column::new_shown(ColumnType::Status), ]); assert_eq!("holsravbwdt", format!("{cols}")); } /// Reverse subset test for subset of columns. #[test] fn test_columns_into_string_reverse_str() { let cols = Columns(vec![ Column::new_shown(ColumnType::Status), Column::new_shown(ColumnType::Last), Column::new_shown(ColumnType::StdDev), Column::new_shown(ColumnType::Worst), Column::new_shown(ColumnType::Best), Column::new_shown(ColumnType::Average), Column::new_shown(ColumnType::Received), ]); assert_eq!("tadwbvr", format!("{cols}")); } } ================================================ FILE: crates/trippy-tui/src/frontend/config.rs ================================================ use crate::config::{AddressMode, AsMode, GeoIpMode, TuiColumns, TuiTheme}; use crate::config::{IcmpExtensionMode, TuiBindings}; use crate::frontend::binding::Bindings; use crate::frontend::columns::Columns; use crate::frontend::theme::Theme; use chrono_tz::Tz; use std::time::Duration; /// Tui configuration. #[derive(Debug)] pub struct TuiConfig { /// Refresh rate. pub refresh_rate: Duration, /// The maximum ttl of hops which will be masked for privacy. pub privacy_max_ttl: Option, /// Preserve screen on exit. pub preserve_screen: bool, /// How to render addresses. pub address_mode: AddressMode, /// Lookup autonomous system (AS) information. pub lookup_as_info: bool, /// How to render autonomous system (AS) data. pub as_mode: AsMode, /// How to render ICMP extensions. pub icmp_extension_mode: IcmpExtensionMode, /// How to render `GeoIp` data. pub geoip_mode: GeoIpMode, /// The maximum number of addresses to show per hop. pub max_addrs: Option, /// The Tui color theme. pub theme: Theme, /// The Tui keyboard bindings. pub bindings: Bindings, /// The columns to display in the hops table. pub tui_columns: Columns, pub geoip_mmdb_file: Option, pub dns_resolve_all: bool, /// The current locale. pub locale: String, pub timezone: Option, } impl TuiConfig { #[expect(clippy::too_many_arguments)] pub fn new( refresh_rate: Duration, privacy_max_ttl: Option, preserve_screen: bool, address_mode: AddressMode, lookup_as_info: bool, as_mode: AsMode, icmp_extension_mode: IcmpExtensionMode, geoip_mode: GeoIpMode, max_addrs: Option, tui_theme: TuiTheme, tui_bindings: &TuiBindings, tui_columns: &TuiColumns, geoip_mmdb_file: Option, dns_resolve_all: bool, locale: String, timezone: Option, ) -> Self { Self { refresh_rate, privacy_max_ttl, preserve_screen, address_mode, lookup_as_info, as_mode, icmp_extension_mode, geoip_mode, max_addrs, theme: Theme::from(tui_theme), bindings: Bindings::from(*tui_bindings), tui_columns: Columns::from(tui_columns.clone()), geoip_mmdb_file, dns_resolve_all, locale, timezone, } } } ================================================ FILE: crates/trippy-tui/src/frontend/render/app.rs ================================================ use crate::frontend::render::{bar, body, flows, footer, header, help, settings, tabs}; use crate::frontend::tui_app::TuiApp; use ratatui::Frame; use ratatui::layout::{Constraint, Direction, Layout}; /// Render the application main screen. /// /// The layout of the TUI is as follows: /// /// ____________________________________ /// | Header | /// ------------------------------------ /// | Tabs | /// ------------------------------------ /// | Flows | /// ------------------------------------ /// | | /// | | /// | | /// | Hops / Chart / Map | /// | | /// | | /// | | /// ------------------------------------ /// | History | Frequency | /// | | | /// ------------------------------------ /// ====== info configuration bar ====== /// /// - Header: the title, target, clock and basic keyboard controls /// - Tab: a tab for each target (shown if > 1 target requested, can't be used with flows) /// - Flows: a navigable chart of individual trace flows (toggled on/off, can't be used with tabs) /// - Hops: a table where each row represents a single hop (time-to-live) in the trace /// - History: a graph of historic round-trip ping samples for the target host /// - Frequency: a histogram of sample frequencies by round-trip time for the target host /// - Info bar: a bar showing the current value for configurable items /// /// On startup a splash screen is shown in place of the hops table, until the completion of the /// first round. pub fn render(f: &mut Frame<'_>, app: &mut TuiApp) { let constraints = if app.trace_info.len() > 1 { LAYOUT_WITH_TABS.as_slice() } else if app.show_flows { LAYOUT_WITH_FLOWS.as_slice() } else { LAYOUT_WITHOUT_TABS.as_slice() }; let chunks = Layout::default() .direction(Direction::Vertical) .constraints(constraints.as_ref()) .split(f.area()); header::render(f, app, chunks[0]); if app.trace_info.len() > 1 { tabs::render(f, chunks[1], app); body::render(f, chunks[2], app); footer::render(f, chunks[3], app); bar::render(f, chunks[4], app); } else if app.show_flows { flows::render(f, chunks[1], app); body::render(f, chunks[2], app); footer::render(f, chunks[3], app); bar::render(f, chunks[4], app); } else { body::render(f, chunks[1], app); footer::render(f, chunks[2], app); bar::render(f, chunks[3], app); } if app.show_settings { settings::render(f, app); } else if app.show_help { help::render(f, app); } } const LAYOUT_WITHOUT_TABS: [Constraint; 4] = [ Constraint::Length(4), Constraint::Min(10), Constraint::Length(6), Constraint::Length(1), ]; const LAYOUT_WITH_TABS: [Constraint; 5] = [ Constraint::Length(4), Constraint::Length(3), Constraint::Min(10), Constraint::Length(6), Constraint::Length(1), ]; const LAYOUT_WITH_FLOWS: [Constraint; 5] = [ Constraint::Length(4), Constraint::Length(6), Constraint::Min(10), Constraint::Length(6), Constraint::Length(1), ]; ================================================ FILE: crates/trippy-tui/src/frontend/render/bar.rs ================================================ use crate::config::AddressMode; use crate::frontend::tui_app::TuiApp; use crate::t; use ratatui::Frame; use ratatui::layout::{Alignment, Rect}; use ratatui::prelude::{Line, Span, Style}; use ratatui::widgets::Paragraph; use std::borrow::Cow; use std::net::IpAddr; use trippy_core::{PrivilegeMode, Protocol}; use trippy_dns::ResolveMethod; pub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) { let protocol = Span::raw(match app.tracer_config().data.protocol() { Protocol::Icmp => format!( "{}/ICMP", fmt_target_family(app.tracer_config().data.target_addr()), ), Protocol::Udp => format!( "{}/UDP/{}", fmt_target_family(app.tracer_config().data.target_addr()), app.tracer_config().data.multipath_strategy(), ), Protocol::Tcp => format!( "{}/TCP", fmt_target_family(app.tracer_config().data.target_addr()), ), }); let privilege_mode = Span::raw(fmt_privilege_mode( app.tracer_config().data.privilege_mode(), )); let as_mode = match app.resolver.config().resolve_method { ResolveMethod::System => Span::raw("□ ASN"), ResolveMethod::Resolv | ResolveMethod::Google | ResolveMethod::Cloudflare => { if app.tui_config.lookup_as_info { Span::raw("■ ASN") } else { Span::raw("□ ASN") } } }; let details = if app.show_hop_details { Span::raw(format!("■ {}", t!("details"))) } else { Span::raw(format!("□ {}", t!("details"))) }; let max_hosts = if let Some(m) = app.tui_config.max_addrs { Span::raw(format!("»:{m:2}")) } else { Span::raw("»: -") }; let privacy = if let Some(ttl) = app.tui_config.privacy_max_ttl { Span::raw(format!("{}:{ttl:2}", t!("privacy"))) } else { Span::raw(format!("{}: -", t!("privacy"))) }; let address_mode = match app.tui_config.address_mode { AddressMode::Ip => Span::raw(" ip "), AddressMode::Host => Span::raw("host"), AddressMode::Both => Span::raw("both"), }; let locale = Span::raw(app.tui_config.locale.as_str()); // these are configuration items that cannot be changed at runtime. let left_line = Line::from(vec![ Span::raw(" ["), protocol, Span::raw("] ["), privilege_mode, Span::raw("] ["), locale, Span::raw("]"), ]); // these are configuration items that can be toggled at runtime. let right_line = Line::from(vec![ Span::raw(" ["), as_mode, Span::raw("] ["), details, Span::raw("] ["), address_mode, Span::raw("] ["), privacy, Span::raw("] ["), max_hosts, Span::raw("] "), ]); let bar_style = Style::default() .bg(app.tui_config.theme.info_bar_bg) .fg(app.tui_config.theme.info_bar_text); let left = Paragraph::new(left_line) .style(bar_style) .alignment(Alignment::Left); let right = Paragraph::new(right_line) .style(bar_style) .alignment(Alignment::Right); f.render_widget(right, rect); f.render_widget(left, rect); } fn fmt_privilege_mode(privilege_mode: PrivilegeMode) -> Cow<'static, str> { match privilege_mode { PrivilegeMode::Privileged => t!("privileged"), PrivilegeMode::Unprivileged => t!("unprivileged"), } } const fn fmt_target_family(target: IpAddr) -> &'static str { match target { IpAddr::V4(_) => "IPv4", IpAddr::V6(_) => "IPv6", } } ================================================ FILE: crates/trippy-tui/src/frontend/render/body.rs ================================================ use crate::frontend::render::{bsod, chart, splash, table, world}; use crate::frontend::tui_app::TuiApp; use ratatui::Frame; use ratatui::layout::Rect; /// Render the body. /// /// This is either an BSOD if there wa san error or the table of hop data or, if there is no data, /// the splash screen. pub fn render(f: &mut Frame<'_>, rec: Rect, app: &mut TuiApp) { if let Some(err) = app.selected_tracer_data.error() { bsod::render(f, rec, err); } else if app.tracer_data().hops().is_empty() { splash::render(f, app, rec); } else if app.show_chart { chart::render(f, app, rec); } else if app.show_map { world::render(f, app, rec); } else { table::render(f, app, rec); } } ================================================ FILE: crates/trippy-tui/src/frontend/render/bsod.rs ================================================ use crate::t; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; /// Render a blue screen of death. pub fn render(f: &mut Frame<'_>, rect: Rect, error: &str) { let chunks = Layout::default() .constraints([Constraint::Percentage(35), Constraint::Percentage(65)].as_ref()) .split(rect); let block = Block::default() .title(Line::raw(t!("title_hops"))) .borders(Borders::ALL) .border_type(BorderType::Rounded) .style(Style::default().bg(Color::Blue)); let line = vec![ Line::from(Span::styled( t!("bsod_failed"), Style::default().add_modifier(Modifier::REVERSED), )), Line::from(""), Line::from(error), Line::from(""), Line::raw(t!("bsod_quit")), ]; let paragraph = Paragraph::new(line).alignment(Alignment::Center); f.render_widget(block, rect); f.render_widget(paragraph, chunks[1]); } ================================================ FILE: crates/trippy-tui/src/frontend/render/chart.rs ================================================ use crate::frontend::tui_app::TuiApp; use crate::t; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::style::Style; use ratatui::symbols::Marker; use ratatui::text::{Line, Span}; use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType}; /// Render the ping history for all hops as a chart. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let selected_hop = app.selected_hop_or_target(); let samples = app.selected_tracer_data.max_samples() / app.zoom_factor; let series_data = app .selected_tracer_data .hops_for_flow(app.selected_flow) .iter() .map(|hop| { hop.samples() .iter() .enumerate() .take(samples) .map(|(i, s)| (i as f64, s.as_secs_f64() * 1000_f64)) .collect::>() }) .collect::>(); let max_sample = series_data .iter() .flatten() .map(|&(_, s)| s) .max_by_key(|&c| (c * 1000.0) as u64) .unwrap_or_default(); let sets = series_data .iter() .enumerate() .map(|(i, s)| { Dataset::default() .name(format!("{} {}", t!("hop"), i + 1)) .data(s) .graph_type(GraphType::Line) .marker(Marker::Braille) .style(Style::default().fg({ match i { i if i + 1 == selected_hop.ttl() as usize => { app.tui_config.theme.hops_chart_selected } _ => app.tui_config.theme.hops_chart_unselected, } })) }) .collect::>(); let constraints = (Constraint::Ratio(1, 1), Constraint::Ratio(1, 1)); let chart = Chart::new(sets) .x_axis( Axis::default() .title(Line::raw(t!("samples"))) .bounds([0_f64, samples as f64]) .labels_alignment(Alignment::Right) .labels( ["0".to_string(), format!("{samples} ({}x)", app.zoom_factor)] .into_iter() .map(Span::from), ) .style(Style::default().fg(app.tui_config.theme.hops_chart_axis)), ) .y_axis( Axis::default() .title(Line::raw(t!("rtt"))) .bounds([0_f64, max_sample]) .labels( [ String::from("0.0"), format!("{:.1}", max_sample / 2_f64), format!("{max_sample:.1}"), ] .into_iter() .map(Span::from), ) .style(Style::default().fg(app.tui_config.theme.hops_chart_axis)), ) .hidden_legend_constraints(constraints) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)) .title(Line::raw(t!("chart"))), ); f.render_widget(chart, rect); } ================================================ FILE: crates/trippy-tui/src/frontend/render/flows.rs ================================================ use crate::frontend::tui_app::TuiApp; use crate::t; use ratatui::Frame; use ratatui::layout::{Alignment, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::Line; use ratatui::widgets::{Bar, BarChart, BarGroup, Block, BorderType, Borders}; /// Render the flows. pub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) { let round_flow_id = app.tracer_data().round_flow_id(); let data: Vec<_> = app .flow_counts .iter() .map(|(flow_id, count)| { let bar_color = if flow_id == &app.selected_flow { app.tui_config.theme.flows_chart_bar_selected } else { app.tui_config.theme.flows_chart_bar_unselected }; let label_color = if flow_id == &round_flow_id { app.tui_config.theme.flows_chart_text_current } else { app.tui_config.theme.flows_chart_text_non_current }; Bar::default() .label(Line::from(format!("{flow_id}"))) .value(*count as u64) .style(Style::default().fg(bar_color)) .value_style( Style::default() .bg(bar_color) .fg(label_color) .add_modifier(Modifier::BOLD), ) }) .collect(); let block = Block::default() .title(Line::raw(t!("title_flows"))) .title_alignment(Alignment::Left) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ); let group = BarGroup::default().bars(&data); let flow_counts = BarChart::default() .block(block) .data(group) .bar_width(4) .bar_gap(1); f.render_widget(flow_counts, rect); } ================================================ FILE: crates/trippy-tui/src/frontend/render/footer.rs ================================================ use crate::frontend::render::{histogram, history}; use crate::frontend::tui_app::TuiApp; use ratatui::Frame; use ratatui::layout::{Constraint, Direction, Layout, Rect}; /// Render the footer. /// /// This contains the history and frequency charts. pub fn render(f: &mut Frame<'_>, rec: Rect, app: &TuiApp) { let bottom_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(75), Constraint::Percentage(25)].as_ref()) .split(rec); history::render(f, app, bottom_chunks[0]); histogram::render(f, app, bottom_chunks[1]); } ================================================ FILE: crates/trippy-tui/src/frontend/render/header.rs ================================================ use crate::frontend::tui_app::TuiApp; use crate::t; use chrono::SecondsFormat; use humantime::format_duration; use ratatui::Frame; use ratatui::layout::{Alignment, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; use std::net::IpAddr; use std::str::FromStr; use std::time::Duration; use trippy_core::{Hop, PortDirection}; use trippy_dns::Resolver; /// Render the title, target, clock and basic keyboard controls. #[expect(clippy::too_many_lines)] pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let header_block = Block::default() .title(format!(" {} v{} ", t!("trippy"), clap::crate_version!())) .title_alignment(Alignment::Center) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ); let now = if let Some(tz) = &app.tui_config.timezone { chrono::Utc::now() .with_timezone(tz) .to_rfc3339_opts(SecondsFormat::Secs, true) } else { chrono::Local::now().to_rfc3339_opts(SecondsFormat::Secs, true) }; let clock_span = Line::from(Span::raw(now)); let bold = Style::default().add_modifier(Modifier::BOLD); let help_binding = app.tui_config.bindings.toggle_help.to_string(); let header_help = t!("header_help"); let help_line_help = if header_help.starts_with(&help_binding) { vec![ Span::styled(&help_binding, bold), Span::raw(&header_help[help_binding.len()..]), ] } else { vec![ Span::raw("["), Span::styled(help_binding, bold), Span::raw("]"), Span::raw(header_help), ] }; let settings_binding = app.tui_config.bindings.toggle_settings.to_string(); let header_settings = t!("header_settings"); let help_line_settings = if header_settings.starts_with(&settings_binding) { vec![ Span::styled(&settings_binding, bold), Span::raw(&header_settings[settings_binding.len()..]), ] } else { vec![ Span::raw("["), Span::styled(settings_binding, bold), Span::raw("]"), Span::raw(header_settings), ] }; let quit_binding = app.tui_config.bindings.quit.to_string(); let header_quit = t!("header_quit"); let help_line_quit = if header_quit.starts_with(&quit_binding) { vec![ Span::styled(&quit_binding, bold), Span::raw(&header_quit[quit_binding.len()..]), ] } else { vec![ Span::raw("["), Span::styled(quit_binding, bold), Span::raw("]"), Span::raw(header_quit), ] }; let help_span = Line::from( [ help_line_help, vec![Span::raw(" ")], help_line_settings, vec![Span::raw(" ")], help_line_quit, ] .into_iter() .flatten() .collect::>(), ); let right_line = vec![clock_span, help_span]; let right = Paragraph::new(right_line) .style(Style::default()) .block(header_block.clone()) .alignment(Alignment::Right); let source = render_source(app); let dest = render_destination(app); let target = format!("{source} -> {dest}"); let hop_count = app.tracer_data().hops_for_flow(app.selected_flow).len(); let discovered = if app.selected_tracer_data.max_flows() > 1 { let plural_flows = if app.tracer_data().flows().len() > 1 { t!("flows") } else { t!("flow") }; let flow_count = app.tracer_data().flows().len(); format!( ", {}", t!("discovered_flows", "hop_count" => hop_count, "flow_count" => flow_count, "plural_flows" => plural_flows ) ) } else { format!(", {}", t!("discovered", "hop_count" => hop_count)) }; let left_line = vec![ Line::from(vec![ Span::styled( format!("{}: ", t!("target")), Style::default().add_modifier(Modifier::BOLD), ), Span::raw(target), ]), Line::from(vec![ Span::styled( format!("{}: ", t!("status")), Style::default().add_modifier(Modifier::BOLD), ), Span::raw(render_status(app)), Span::raw(discovered), ]), ]; let left = Paragraph::new(left_line) .style(Style::default()) .block(header_block) .alignment(Alignment::Left); f.render_widget(right, rect); f.render_widget(left, rect); } /// Render the source address of the trace. fn render_source(app: &TuiApp) -> String { fn format_ip(app: &TuiApp, src_addr: IpAddr) -> String { match app.tracer_config().data.port_direction() { PortDirection::None => { format!("{src_addr}") } PortDirection::FixedDest(_) => { format!("{src_addr}:*") } PortDirection::FixedSrc(src) | PortDirection::FixedBoth(src, _) => { format!("{src_addr}:{}", src.0) } } } fn format_both(app: &TuiApp, src_hostname: &str, src_addr: IpAddr) -> String { match app.tracer_config().data.port_direction() { PortDirection::None => { format!("{src_addr} ({src_hostname})") } PortDirection::FixedDest(_) => { format!("{src_addr}:* ({src_hostname})") } PortDirection::FixedSrc(src) | PortDirection::FixedBoth(src, _) => { format!("{src_addr}:{} ({src_hostname})", src.0) } } } if app.tui_config.privacy_max_ttl.is_some() { format!("**{}**", t!("hidden")) } else if let Some(addr) = app.tracer_config().data.source_addr() { let entry = app.resolver.lazy_reverse_lookup_with_asinfo(addr); if let Some(hostname) = entry.hostnames().next() { format_both(app, hostname, addr) } else { format_ip(app, addr) } } else { String::from(t!("unknown")) } } /// Render the destination address. fn render_destination(app: &TuiApp) -> String { fn format_ip(app: &TuiApp, dest_addr: IpAddr) -> String { match app.tracer_config().data.port_direction() { PortDirection::None => { format!("{dest_addr}") } PortDirection::FixedSrc(_) => { format!("{dest_addr}:*") } PortDirection::FixedDest(dest) | PortDirection::FixedBoth(_, dest) => { format!("{dest_addr}:{}", dest.0) } } } fn format_both(app: &TuiApp, dest_hostname: &str, dest_addr: IpAddr) -> String { match app.tracer_config().data.port_direction() { PortDirection::None => { format!("{dest_addr} ({dest_hostname})") } PortDirection::FixedSrc(_) => { format!("{dest_addr}:* ({dest_hostname})") } PortDirection::FixedDest(dest) | PortDirection::FixedBoth(_, dest) => { format!("{dest_addr}:{} ({dest_hostname})", dest.0) } } } let dest_addr = app.tracer_config().data.target_addr(); let target_hostname = &app.tracer_config().target_hostname; if let Ok(addr) = IpAddr::from_str(target_hostname) { let entry = app.resolver.lazy_reverse_lookup_with_asinfo(addr); let hostname = entry.hostnames().next().unwrap_or_else(|| target_hostname); if hostname == target_hostname { format_ip(app, addr) } else { format_both(app, hostname, addr) } } else { format_both(app, target_hostname, dest_addr) } } /// Render the headline status of the tracing. fn render_status(app: &TuiApp) -> String { let failure_count: usize = app .tracer_data() .hops_for_flow(app.selected_flow) .iter() .map(Hop::total_failed) .sum(); let failures = if failure_count > 0 { let total_probes: usize = app .tracer_data() .hops_for_flow(app.selected_flow) .iter() .map(Hop::total_sent) .sum(); let failure_rate = if total_probes > 0 { (failure_count as f64 / total_probes as f64) * 100.0 } else { 0_f64 }; let failure_rate = format!("{failure_rate:.1}"); format!( " [{}❗]", t!("status_failures", "failure_count" => failure_count, "total_probes" => total_probes, "failure_rate" => failure_rate) ) } else { String::new() }; if app.selected_tracer_data.error().is_some() { String::from(t!("status_failed")) } else if let Some(start) = app.frozen_start { let frozen = format_duration(Duration::from_secs( start.elapsed().unwrap_or_default().as_secs(), )); format!("{} ({frozen}){failures}", t!("status_frozen")) } else { format!("{}{failures}", t!("status_running")) } } ================================================ FILE: crates/trippy-tui/src/frontend/render/help.rs ================================================ use crate::frontend::render::util; use crate::frontend::tui_app::TuiApp; use crate::t; use ratatui::Frame; use ratatui::layout::Alignment; use ratatui::style::Style; use ratatui::text::Line; use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph}; /// Render help dialog. pub fn render(f: &mut Frame<'_>, app: &TuiApp) { let s = app.tui_config.bindings.toggle_settings; let b = app.tui_config.bindings.toggle_settings_bindings; let c = app.tui_config.bindings.toggle_settings_columns; #[expect(clippy::needless_raw_string_hashes)] let help_lines = vec![ Line::raw(r#" "#), Line::raw(r#" _____ _ "#), Line::raw(r#"|_ _| _(_)_ __ _ __ _ _ "#), Line::raw(r#" | || '_| | '_ \ '_ \ || |"#), Line::raw(r#" |_||_| |_| .__/ .__/\_, |"#), Line::raw(r#" |_| |_| |__/ "#), Line::raw(r#" "#), Line::raw(t!("help_tagline")), Line::raw(r#" "#), Line::raw(t!("help_show_settings", key = s)), Line::raw(t!("help_show_bindings", key = b)), Line::raw(t!("help_show_columns", key = c)), Line::raw(r#" "#), Line::raw(r#" https://github.com/fujiapple852/trippy "#), Line::raw(r#" "#), Line::raw(t!("help_license")), Line::raw(r#" "#), Line::raw(t!("help_copyright")), ]; let block = Block::default() .title(format!(" {} ", t!("title_help"))) .title_alignment(Alignment::Center) .borders(Borders::ALL) .style(Style::default().bg(app.tui_config.theme.help_dialog_bg)) .border_type(BorderType::Double); let control = Paragraph::new(help_lines) .style(Style::default().fg(app.tui_config.theme.help_dialog_text)) .block(block.clone()) .alignment(Alignment::Center); let area = util::centered_rect(60, 60, f.area()); f.render_widget(Clear, area); f.render_widget(block, area); f.render_widget(control, area); } ================================================ FILE: crates/trippy-tui/src/frontend/render/histogram.rs ================================================ use crate::frontend::tui_app::TuiApp; use crate::t; use ratatui::Frame; use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; use ratatui::widgets::{BarChart, Block, BorderType, Borders}; use std::collections::BTreeMap; use std::time::Duration; /// Render a histogram of ping frequencies. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let selected_hop = app.selected_hop_or_target(); let freq_data = sample_frequency(selected_hop.samples()); let freq_data_ref: Vec<_> = freq_data.iter().map(|(b, c)| (b.as_str(), *c)).collect(); let barchart = BarChart::default() .block( Block::default() .title(format!("{} #{}", t!("title_frequency"), selected_hop.ttl())) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)), ) .data(freq_data_ref.as_slice()) .bar_width(4) .bar_gap(1) .bar_style(Style::default().fg(app.tui_config.theme.frequency_chart_bar)) .value_style( Style::default() .bg(app.tui_config.theme.frequency_chart_bar) .fg(app.tui_config.theme.frequency_chart_text) .add_modifier(Modifier::BOLD), ); f.render_widget(barchart, rect); } /// Return the frequency % grouped by sample duration. fn sample_frequency(samples: &[Duration]) -> Vec<(String, u64)> { let sample_count = samples.len(); let mut count_by_duration: BTreeMap = BTreeMap::new(); for sample in samples { if !sample.is_zero() { *count_by_duration.entry(sample.as_millis()).or_default() += 1; } } count_by_duration .iter() .map(|(ping, count)| { let ping = format!("{ping}"); let freq_pct = ((*count as f64 / sample_count as f64) * 100_f64) as u64; (ping, freq_pct) }) .collect() } ================================================ FILE: crates/trippy-tui/src/frontend/render/history.rs ================================================ use crate::frontend::tui_app::TuiApp; use crate::t; use ratatui::Frame; use ratatui::layout::Rect; use ratatui::style::Style; use ratatui::widgets::{Block, BorderType, Borders, Sparkline}; /// Render the ping history for the final hop which is typically the target. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let selected_hop = app.selected_hop_or_target(); let data = selected_hop .samples() .iter() .take(rect.width as usize) .map(|s| { if s.is_zero() { None } else { Some(s.as_micros() as u64) } }) .collect::>(); let history = Sparkline::default() .block( Block::default() .title(format!("{} #{}", t!("title_samples"), selected_hop.ttl())) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)), ) .data(data.as_slice()) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.samples_chart), ) .absent_value_style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.samples_chart_lost), ) .absent_value_symbol(ratatui::symbols::bar::FULL); f.render_widget(history, rect); } ================================================ FILE: crates/trippy-tui/src/frontend/render/settings.rs ================================================ use crate::config::{AddressMode, AsMode, GeoIpMode, IcmpExtensionMode}; use crate::frontend::render::util; use crate::frontend::theme; use crate::frontend::tui_app::TuiApp; use crate::t; use humantime::format_duration; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{ Block, BorderType, Borders, Cell, Clear, Paragraph, Row, Table, Tabs, Wrap, }; use trippy_core::PortDirection; use trippy_dns::ResolveMethod; /// Render settings dialog. pub fn render(f: &mut Frame<'_>, app: &mut TuiApp) { let all_settings = format_all_settings(app); let (name, info, items) = &all_settings[app.settings_tab_selected]; let area = util::centered_rect(60, 60, f.area()); let chunks = Layout::default() .direction(Direction::Vertical) .constraints(SETTINGS_TABLE_WIDTH.as_ref()) .split(area); f.render_widget(Clear, area); render_settings_tabs(f, app, chunks[0]); render_settings_table(f, app, chunks[1], name, items); render_settings_info(f, app, chunks[2], info); } /// Render settings tabs. fn render_settings_tabs(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let titles: Vec<_> = settings_tabs() .into_iter() .map(|(title, _)| { Line::from(Span::styled( title, Style::default().fg(app.tui_config.theme.settings_tab_text), )) }) .collect(); let tabs = Tabs::new(titles) .block( Block::default() .title(format!(" {} ", t!("title_settings"))) .title_alignment(Alignment::Center) .borders(Borders::ALL) .style(Style::default().bg(app.tui_config.theme.settings_dialog_bg)) .border_type(BorderType::Double), ) .select(app.settings_tab_selected) .style(Style::default()) .highlight_style(Style::default().add_modifier(Modifier::BOLD)); f.render_widget(tabs, rect); } /// Render settings table. fn render_settings_table( f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect, name: &str, items: &[SettingsItem], ) { let header_cells = settings_table_header().into_iter().map(|h| { Cell::from(h).style(Style::default().fg(app.tui_config.theme.settings_table_header_text)) }); let header = Row::new(header_cells) .style(Style::default().bg(app.tui_config.theme.settings_table_header_bg)) .height(1) .bottom_margin(0); let rows = items.iter().map(|item| { Row::new(vec![ Cell::from(item.item.as_str()), Cell::from(item.value.as_str()), ]) .style(Style::default().fg(app.tui_config.theme.settings_table_row_text)) }); let item_width = items .iter() .map(|item| item.item.len() as u16) .max() .unwrap_or_default() .max(30); let table_widths = [Constraint::Min(item_width), Constraint::Length(60)]; let table = Table::new(rows, table_widths) .header(header) .block( Block::default() .title(format!(" {name} ")) .title_alignment(Alignment::Left) .borders(Borders::ALL) .style(Style::default().bg(app.tui_config.theme.settings_dialog_bg)) .border_type(BorderType::Plain), ) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ) .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED)); f.render_stateful_widget(table, rect, &mut app.setting_table_state); } /// Render settings info footer. fn render_settings_info(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, info: &str) { let info = Paragraph::new(info) .style(Style::default()) .wrap(Wrap::default()) .block( Block::default() .title(format!(" {} ", t!("settings_info"))) .title_alignment(Alignment::Center) .borders(Borders::ALL) .style(Style::default().bg(app.tui_config.theme.settings_dialog_bg)) .border_type(BorderType::Plain), ) .alignment(Alignment::Left); f.render_widget(info, rect); } /// Format all settings. fn format_all_settings(app: &TuiApp) -> Vec<(String, String, Vec)> { let tui_settings = format_tui_settings(app); let trace_settings = format_trace_settings(app); let dns_settings = format_dns_settings(app); let geoip_settings = format_geoip_settings(app); let bindings_settings = format_binding_settings(app); let theme_settings = format_theme_settings(app); let columns_settings = format_columns_settings(app); let toggle_column = app.tui_config.bindings.toggle_chart.to_string(); let move_down = app.tui_config.bindings.next_hop_address.to_string(); let move_up = app.tui_config.bindings.previous_hop_address.to_string(); vec![ ( t!("settings_tab_tui_title").to_string(), t!("settings_tab_tui_desc").to_string(), tui_settings, ), ( t!("settings_tab_trace_title").to_string(), t!("settings_tab_trace_desc").to_string(), trace_settings, ), ( t!("settings_tab_dns_title").to_string(), t!("settings_tab_dns_desc").to_string(), dns_settings, ), ( t!("settings_tab_geoip_title").to_string(), t!("settings_tab_geoip_desc").to_string(), geoip_settings, ), ( t!("settings_tab_bindings_title").to_string(), t!("settings_tab_bindings_desc").to_string(), bindings_settings, ), ( t!("settings_tab_theme_title").to_string(), t!("settings_tab_theme_desc").to_string(), theme_settings, ), ( t!("settings_tab_columns_title").to_string(), t!( "settings_tab_columns_desc", c = toggle_column, d = move_down, u = move_up ), columns_settings, ), ] } /// Format Tui settings. fn format_tui_settings(app: &TuiApp) -> Vec { vec![ SettingsItem::new( "tui-preserve-screen", format!("{}", app.tui_config.preserve_screen), ), SettingsItem::new( "tui-refresh-rate", format!("{}", format_duration(app.tui_config.refresh_rate)), ), SettingsItem::new( "tui-privacy-max-ttl", app.tui_config .privacy_max_ttl .map_or_else(|| t!("off").to_string(), |m| m.to_string()), ), SettingsItem::new( "tui-address-mode", format_address_mode(app.tui_config.address_mode), ), SettingsItem::new("tui-as-mode", format_as_mode(app.tui_config.as_mode)), SettingsItem::new( "tui-icmp-extension-mode", format_extension_mode(app.tui_config.icmp_extension_mode), ), SettingsItem::new( "tui-geoip-mode", format_geoip_mode(app.tui_config.geoip_mode), ), SettingsItem::new( "tui-max-addrs", app.tui_config .max_addrs .map_or_else(|| t!("auto").to_string(), |m| m.to_string()), ), SettingsItem::new( "tui-custom-columns", format!("{}", app.tui_config.tui_columns), ), SettingsItem::new( "tui-timezone", app.tui_config .timezone .map_or_else(|| t!("auto").to_string(), |tz| tz.to_string()), ), ] } /// Format trace settings. fn format_trace_settings(app: &TuiApp) -> Vec { let cfg = app.tracer_config(); let interface = if let Some(iface) = cfg.data.interface() { iface.to_string() } else { t!("auto").to_string() }; let (src_port, dst_port) = match cfg.data.port_direction() { PortDirection::None => (t!("na").to_string(), t!("na").to_string()), PortDirection::FixedDest(dst) => (t!("auto").to_string(), format!("{}", dst.0)), PortDirection::FixedSrc(src) => (format!("{}", src.0), t!("auto").to_string()), PortDirection::FixedBoth(src, dst) => (format!("{}", src.0), format!("{}", dst.0)), }; vec![ SettingsItem::new("first-ttl", format!("{}", cfg.data.first_ttl().0)), SettingsItem::new("max-ttl", format!("{}", cfg.data.max_ttl().0)), SettingsItem::new( "min-round-duration", format!("{}", format_duration(cfg.data.min_round_duration())), ), SettingsItem::new( "max-round-duration", format!("{}", format_duration(cfg.data.max_round_duration())), ), SettingsItem::new( "grace-duration", format!("{}", format_duration(cfg.data.grace_duration())), ), SettingsItem::new("max-inflight", format!("{}", cfg.data.max_inflight().0)), SettingsItem::new( "initial-sequence", format!("{}", cfg.data.initial_sequence().0), ), SettingsItem::new( "read-timeout", format!("{}", format_duration(cfg.data.read_timeout())), ), SettingsItem::new("packet-size", format!("{}", cfg.data.packet_size().0)), SettingsItem::new( "payload-pattern", format!("{}", cfg.data.payload_pattern().0), ), SettingsItem::new("tos", format!("{}", cfg.data.tos().0)), SettingsItem::new( "icmp-extensions", format!("{}", cfg.data.icmp_extension_parse_mode()), ), SettingsItem::new("interface", interface), SettingsItem::new( "multipath-strategy", cfg.data.multipath_strategy().to_string(), ), SettingsItem::new("target-port", dst_port), SettingsItem::new("source-port", src_port), SettingsItem::new( "max-samples", format!("{}", app.selected_tracer_data.max_samples()), ), SettingsItem::new( "max-flows", format!("{}", app.selected_tracer_data.max_flows()), ), ] } /// Format DNS settings. fn format_dns_settings(app: &TuiApp) -> Vec { vec![ SettingsItem::new( "dns-timeout", format!("{}", format_duration(app.resolver.config().timeout)), ), SettingsItem::new( "dns-ttl", format!("{}", format_duration(app.resolver.config().ttl)), ), SettingsItem::new( "dns-resolve-method", format_dns_method(app.resolver.config().resolve_method), ), SettingsItem::new( "dns-resolve-all", format!("{}", app.tui_config.dns_resolve_all), ), SettingsItem::new( "dns-lookup-as-info", format!("{}", app.tui_config.lookup_as_info), ), ] } /// Format `GeoIp` settings. fn format_geoip_settings(app: &TuiApp) -> Vec { vec![SettingsItem::new( "geoip-mmdb-file", app.tui_config .geoip_mmdb_file .as_deref() .map_or_else(|| t!("none").to_string(), ToString::to_string), )] } /// Format binding settings. fn format_binding_settings(app: &TuiApp) -> Vec { let binds = &app.tui_config.bindings; vec![ SettingsItem::new("toggle-help", format!("{}", binds.toggle_help)), SettingsItem::new("toggle-help-alt", format!("{}", binds.toggle_help_alt)), SettingsItem::new("toggle-settings", format!("{}", binds.toggle_settings)), SettingsItem::new( "toggle-settings-tui", format!("{}", binds.toggle_settings_tui), ), SettingsItem::new( "toggle-settings-trace", format!("{}", binds.toggle_settings_trace), ), SettingsItem::new( "toggle-settings-dns", format!("{}", binds.toggle_settings_dns), ), SettingsItem::new( "toggle-settings-geoip", format!("{}", binds.toggle_settings_geoip), ), SettingsItem::new( "toggle-settings-bindings", format!("{}", binds.toggle_settings_bindings), ), SettingsItem::new( "toggle-settings-theme", format!("{}", binds.toggle_settings_theme), ), SettingsItem::new( "toggle-settings-columns", format!("{}", binds.toggle_settings_columns), ), SettingsItem::new("next-hop", format!("{}", binds.next_hop)), SettingsItem::new("previous-hop", format!("{}", binds.previous_hop)), SettingsItem::new("next-trace", format!("{}", binds.next_trace)), SettingsItem::new("previous-trace", format!("{}", binds.previous_trace)), SettingsItem::new("next-hop-address", format!("{}", binds.next_hop_address)), SettingsItem::new( "previous-hop-address", format!("{}", binds.previous_hop_address), ), SettingsItem::new("address-mode-ip", format!("{}", binds.address_mode_ip)), SettingsItem::new("address-mode-host", format!("{}", binds.address_mode_host)), SettingsItem::new("address-mode-both", format!("{}", binds.address_mode_both)), SettingsItem::new("toggle-freeze", format!("{}", binds.toggle_freeze)), SettingsItem::new("toggle-chart", format!("{}", binds.toggle_chart)), SettingsItem::new("toggle-map", format!("{}", binds.toggle_map)), SettingsItem::new("toggle-flows", format!("{}", binds.toggle_flows)), SettingsItem::new("expand-privacy", format!("{}", binds.expand_privacy)), SettingsItem::new("contract-privacy", format!("{}", binds.contract_privacy)), SettingsItem::new("expand-hosts", format!("{}", binds.expand_hosts)), SettingsItem::new("expand-hosts-max", format!("{}", binds.expand_hosts_max)), SettingsItem::new("contract-hosts", format!("{}", binds.contract_hosts)), SettingsItem::new( "contract-hosts-min", format!("{}", binds.contract_hosts_min), ), SettingsItem::new("chart-zoom-in", format!("{}", binds.chart_zoom_in)), SettingsItem::new("chart-zoom-out", format!("{}", binds.chart_zoom_out)), SettingsItem::new("clear-trace-data", format!("{}", binds.clear_trace_data)), SettingsItem::new("clear-dns-cache", format!("{}", binds.clear_dns_cache)), SettingsItem::new("clear-selection", format!("{}", binds.clear_selection)), SettingsItem::new("toggle-as-info", format!("{}", binds.toggle_as_info)), SettingsItem::new( "toggle-hop-details", format!("{}", binds.toggle_hop_details), ), SettingsItem::new("quit", format!("{}", binds.quit)), SettingsItem::new( "quit-preserve-screen", format!("{}", binds.quit_preserve_screen), ), ] } /// Format theme settings. #[expect(clippy::too_many_lines)] fn format_theme_settings(app: &TuiApp) -> Vec { let theme = &app.tui_config.theme; vec![ SettingsItem::new("bg-color", theme::fmt_color(theme.bg)), SettingsItem::new("border-color", theme::fmt_color(theme.border)), SettingsItem::new("text-color", theme::fmt_color(theme.text)), SettingsItem::new("tab-text-color", theme::fmt_color(theme.tab_text)), SettingsItem::new( "hops-table-header-bg-color", theme::fmt_color(theme.hops_table_header_bg), ), SettingsItem::new( "hops-table-header-text-color", theme::fmt_color(theme.hops_table_header_text), ), SettingsItem::new( "hops-table-row-active-text-color", theme::fmt_color(theme.hops_table_row_active_text), ), SettingsItem::new( "hops-table-row-inactive-text-color", theme::fmt_color(theme.hops_table_row_inactive_text), ), SettingsItem::new( "hops-chart-selected-color", theme::fmt_color(theme.hops_chart_selected), ), SettingsItem::new( "hops-chart-unselected-color", theme::fmt_color(theme.hops_chart_unselected), ), SettingsItem::new( "hops-chart-axis-color", theme::fmt_color(theme.hops_chart_axis), ), SettingsItem::new( "frequency-chart-bar-color", theme::fmt_color(theme.frequency_chart_bar), ), SettingsItem::new( "frequency-chart-text-color", theme::fmt_color(theme.frequency_chart_text), ), SettingsItem::new( "flows-chart-bar-selected-color", theme::fmt_color(theme.flows_chart_bar_selected), ), SettingsItem::new( "flows-chart-bar-unselected-color", theme::fmt_color(theme.flows_chart_bar_unselected), ), SettingsItem::new( "flows-chart-text-current-color", theme::fmt_color(theme.flows_chart_text_current), ), SettingsItem::new( "flows-chart-text-non-current-color", theme::fmt_color(theme.flows_chart_text_non_current), ), SettingsItem::new( "samples-chart-color ", theme::fmt_color(theme.samples_chart), ), SettingsItem::new( "help-dialog-bg-color", theme::fmt_color(theme.help_dialog_bg), ), SettingsItem::new( "help-dialog-text-color", theme::fmt_color(theme.help_dialog_text), ), SettingsItem::new( "settings-dialog-bg-color", theme::fmt_color(theme.settings_dialog_bg), ), SettingsItem::new( "settings-tab-text-color", theme::fmt_color(theme.settings_tab_text), ), SettingsItem::new( "settings-table-header-text-color", theme::fmt_color(theme.settings_table_header_text), ), SettingsItem::new( "settings-table-header-bg-color", theme::fmt_color(theme.settings_table_header_bg), ), SettingsItem::new( "settings-table-row-text-color", theme::fmt_color(theme.settings_table_row_text), ), SettingsItem::new("map-world-color", theme::fmt_color(theme.map_world)), SettingsItem::new("map-radius-color", theme::fmt_color(theme.map_radius)), SettingsItem::new("map-selected-color", theme::fmt_color(theme.map_selected)), SettingsItem::new( "map-info-panel-border-color", theme::fmt_color(theme.map_info_panel_border), ), SettingsItem::new( "map-info-panel-bg-color", theme::fmt_color(theme.map_info_panel_bg), ), SettingsItem::new( "map-info-panel-text-color", theme::fmt_color(theme.map_info_panel_text), ), SettingsItem::new("info-bar-bg-color", theme::fmt_color(theme.info_bar_bg)), SettingsItem::new("info-bar-text-color", theme::fmt_color(theme.info_bar_text)), ] } /// Format columns settings. fn format_columns_settings(app: &TuiApp) -> Vec { app.tui_config .tui_columns .all_columns() .map(|c| SettingsItem::new(c.typ.to_string(), c.status.to_string())) .collect() } /// The index of the columns tab. pub const SETTINGS_TAB_COLUMNS: usize = 6; /// The name and number of items for each tabs in the setting dialog. pub fn settings_tabs() -> [(String, usize); 7] { [ (t!("settings_tab_tui_title").to_string(), 10), (t!("settings_tab_trace_title").to_string(), 18), (t!("settings_tab_dns_title").to_string(), 5), (t!("settings_tab_geoip_title").to_string(), 1), (t!("settings_tab_bindings_title").to_string(), 37), (t!("settings_tab_theme_title").to_string(), 33), (t!("settings_tab_columns_title").to_string(), 0), ] } /// The settings table header. pub fn settings_table_header() -> [String; 2] { [ t!("settings_table_header_setting").to_string(), t!("settings_table_header_value").to_string(), ] } const SETTINGS_TABLE_WIDTH: [Constraint; 3] = [ Constraint::Length(3), Constraint::Min(1), Constraint::Length(4), ]; struct SettingsItem { item: String, value: String, } impl SettingsItem { pub fn new(item: impl Into, value: String) -> Self { Self { item: item.into(), value, } } } /// Format the `DnsResolveMethod`. fn format_dns_method(resolve_method: ResolveMethod) -> String { match resolve_method { ResolveMethod::System => String::from("system"), ResolveMethod::Resolv => String::from("resolv"), ResolveMethod::Google => String::from("google"), ResolveMethod::Cloudflare => String::from("cloudflare"), } } fn format_extension_mode(icmp_extension_mode: IcmpExtensionMode) -> String { match icmp_extension_mode { IcmpExtensionMode::Off => "off".to_string(), IcmpExtensionMode::Mpls => "mpls".to_string(), IcmpExtensionMode::Full => "full".to_string(), IcmpExtensionMode::All => "all".to_string(), } } /// Format the `AsMode`. fn format_as_mode(as_mode: AsMode) -> String { match as_mode { AsMode::Asn => "asn".to_string(), AsMode::Prefix => "prefix".to_string(), AsMode::CountryCode => "country-code".to_string(), AsMode::Registry => "registry".to_string(), AsMode::Allocated => "allocated".to_string(), AsMode::Name => "name".to_string(), } } /// Format the `AddressMode`. fn format_address_mode(address_mode: AddressMode) -> String { match address_mode { AddressMode::Ip => "ip".to_string(), AddressMode::Host => "host".to_string(), AddressMode::Both => "both".to_string(), } } /// Format the `GeoIpMode`. fn format_geoip_mode(geoip_mode: GeoIpMode) -> String { match geoip_mode { GeoIpMode::Off => "off".to_string(), GeoIpMode::Short => "short".to_string(), GeoIpMode::Long => "long".to_string(), GeoIpMode::Location => "location".to_string(), } } ================================================ FILE: crates/trippy-tui/src/frontend/render/splash.rs ================================================ use crate::frontend::tui_app::TuiApp; use crate::t; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::Style; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; use std::borrow::Cow; /// Render the splash screen. /// /// This is shown on startup whilst we await the first round of data to be available. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let chunks = Layout::default() .constraints([Constraint::Percentage(35), Constraint::Percentage(65)].as_ref()) .split(rect); let block = Block::default() .title(Line::raw(t!("title_hops"))) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ); #[expect(clippy::needless_raw_string_hashes)] let splash: Vec> = vec![ r#" _____ _ "#.into(), r#"|_ _| _(_)_ __ _ __ _ _ "#.into(), r#" | || '_| | '_ \ '_ \ || |"#.into(), r#" |_||_| |_| .__/ .__/\_, |"#.into(), r#" |_| |_| |__/ "#.into(), "".into(), t!("awaiting_data"), ]; let line: Vec<_> = splash .into_iter() .map(|line| Line::from(Span::styled(line, Style::default()))) .collect(); let paragraph = Paragraph::new(line).alignment(Alignment::Center); f.render_widget(block, rect); f.render_widget(paragraph, chunks[1]); } ================================================ FILE: crates/trippy-tui/src/frontend/render/table.rs ================================================ use crate::config::{AddressMode, AsMode, GeoIpMode, IcmpExtensionMode}; use crate::frontend::columns::{ColumnType, Columns}; use crate::frontend::config::TuiConfig; use crate::frontend::theme::Theme; use crate::frontend::tui_app::TuiApp; use crate::geoip::{GeoIpCity, GeoIpLookup}; use crate::t; use itertools::Itertools; use ratatui::Frame; use ratatui::layout::Rect; use ratatui::prelude::Line; use ratatui::style::{Modifier, Style}; use ratatui::widgets::{Block, BorderType, Borders, Cell, Row, Table}; use std::fmt::Write; use std::net::IpAddr; use std::rc::Rc; use trippy_core::{ Dscp, Ecn, Extension, Extensions, IcmpPacketType, MplsLabelStackMember, UnknownExtension, }; use trippy_core::{Hop, NatStatus}; use trippy_dns::{AsInfo, DnsEntry, DnsResolver, Resolved, Resolver, Unresolved}; /// Render the table of data about the hops. /// /// For each hop, we show by default: /// /// - The time-to-live (indexed from 1) at this hop (`#`) /// - The host(s) reported at this hop (`Host`) /// - The packet loss % for all probes at this hop (`Loss%`) /// - The number of requests sent for all probes at this hop (`Snt`) /// - The number of replies received for all probes at this hop (`Recv`) /// - The round-trip time of the most recent probe at this hop (`Last`) /// - The average round-trip time for all probes at this hop (`Avg`) /// - The best round-trip time for all probes at this hop (`Best`) /// - The worst round-trip time for all probes at this hop (`Wrst`) /// - The standard deviation round-trip time for all probes at this hop (`StDev`) /// - The status of this hop (`Sts`) /// /// Optional columns that can be added: /// /// - The current jitter i.e. round-trip difference with the last round-trip ('Jttr') /// - The average jitter time for all probes at this hop ('Javg') /// - The worst round-trip jitter time for all probes at this hop ('Jmax') /// - The smoothed jitter value for all probes at this hop ('Jinta') pub fn render(f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect) { let config = &app.tui_config; let widths = config.tui_columns.constraints(rect); let header = render_table_header(app.tui_config.theme, &config.tui_columns); let selected_style = Style::default().add_modifier(Modifier::REVERSED); let rows = app .tracer_data() .hops_for_flow(app.selected_flow) .iter() .map(|hop| { render_table_row( app, hop, &app.resolver, &app.geoip_lookup, &app.tui_config, &config.tui_columns, ) }); let table = Table::new(rows, widths.as_slice()) .header(header) .block( Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)) .title(Line::raw(t!("title_hops"))), ) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ) .row_highlight_style(selected_style) .column_spacing(1); f.render_stateful_widget(table, rect, &mut app.table_state); } /// Render the table header. fn render_table_header(theme: Theme, table_columns: &Columns) -> Row<'static> { let header_cells = table_columns.columns().map(|c| { Cell::from(c.typ.to_string()).style(Style::default().fg(theme.hops_table_header_text)) }); Row::new(header_cells) .style(Style::default().bg(theme.hops_table_header_bg)) .height(1) .bottom_margin(0) } /// Render a single row in the table of hops. fn render_table_row( app: &TuiApp, hop: &Hop, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, custom_columns: &Columns, ) -> Row<'static> { let is_selected_hop = app.selected_hop().is_some_and(|h| h.ttl() == hop.ttl()); let is_in_round = app.tracer_data().is_in_round(hop, app.selected_flow); let (_, row_height) = if is_selected_hop && app.show_hop_details { render_hostname_with_details(app, hop, dns, geoip_lookup, config) } else { render_hostname(app, hop, dns, geoip_lookup) }; let cells: Vec> = custom_columns .columns() .map(|column| { new_cell( column.typ, is_selected_hop, app, hop, dns, geoip_lookup, config, ) }) .collect(); let row_color = if is_in_round { config.theme.hops_table_row_active_text } else { config.theme.hops_table_row_inactive_text }; Row::new(cells) .height(row_height) .bottom_margin(0) .style(Style::default().fg(row_color)) } ///Returns a Cell matched on short char of the Column fn new_cell( column: ColumnType, is_selected_hop: bool, app: &TuiApp, hop: &Hop, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> Cell<'static> { let is_target = app.tracer_data().is_target(hop, app.selected_flow); let total_recv = hop.total_recv(); match column { ColumnType::Ttl => render_usize_cell(hop.ttl().into()), ColumnType::Host => { let (host_cell, _) = if is_selected_hop && app.show_hop_details { render_hostname_with_details(app, hop, dns, geoip_lookup, config) } else { render_hostname(app, hop, dns, geoip_lookup) }; host_cell } ColumnType::LossPct => render_pct_cell(hop.loss_pct()), ColumnType::Sent => render_usize_cell(hop.total_sent()), ColumnType::Received => render_usize_cell(hop.total_recv()), ColumnType::Failed => render_usize_cell(hop.total_failed()), ColumnType::Last => render_float_cell(hop.last_ms(), 1, total_recv), ColumnType::Average => render_avg_cell(hop), ColumnType::Best => render_float_cell(hop.best_ms(), 1, total_recv), ColumnType::Worst => render_float_cell(hop.worst_ms(), 1, total_recv), ColumnType::StdDev => render_stddev_cell(hop), ColumnType::Status => render_status_cell(hop, is_target), ColumnType::Jitter => render_float_cell(hop.jitter_ms(), 1, total_recv), ColumnType::Javg => render_float_cell(Some(hop.javg_ms()), 1, total_recv), ColumnType::Jmax => render_float_cell(hop.jmax_ms(), 1, total_recv), ColumnType::Jinta => render_float_cell(Some(hop.jinta()), 1, total_recv), ColumnType::LastSrcPort => render_port_cell(hop.last_src_port()), ColumnType::LastDestPort => render_port_cell(hop.last_dest_port()), ColumnType::LastSeq => render_usize_cell(usize::from(hop.last_sequence())), ColumnType::LastIcmpPacketType => render_icmp_packet_type_cell(hop.last_icmp_packet_type()), ColumnType::LastIcmpPacketCode => render_icmp_packet_code_cell(hop.last_icmp_packet_type()), ColumnType::LastNatStatus => render_nat_cell(hop.last_nat_status()), ColumnType::Floss => render_usize_cell(hop.total_forward_loss()), ColumnType::Bloss => render_usize_cell(hop.total_backward_loss()), ColumnType::FlossPct => render_pct_cell(hop.forward_loss_pct()), ColumnType::Dscp => render_dscp_cell(hop.dscp()), ColumnType::Ecn => render_ecn_cell(hop.ecn()), ColumnType::Asn => render_asn_cell(hop, dns, config), } } fn render_usize_cell(value: usize) -> Cell<'static> { Cell::from(format!("{value}")) } fn render_nat_cell(value: NatStatus) -> Cell<'static> { Cell::from(match value { NatStatus::NotApplicable => t!("na"), NatStatus::NotDetected => t!("no"), NatStatus::Detected => t!("yes"), }) } fn render_pct_cell(value: f64) -> Cell<'static> { Cell::from(format!("{value:.1}%")) } fn render_avg_cell(hop: &Hop) -> Cell<'static> { Cell::from(if hop.total_recv() > 0 { format!("{:.1}", hop.avg_ms()) } else { String::default() }) } fn render_stddev_cell(hop: &Hop) -> Cell<'static> { Cell::from(if hop.total_recv() > 1 { format!("{:.1}", hop.stddev_ms()) } else { String::default() }) } fn render_float_cell(value: Option, places: usize, total_recv: usize) -> Cell<'static> { Cell::from(if total_recv > 0 { value.map(|v| format!("{v:.places$}")).unwrap_or_default() } else { String::default() }) } fn render_status_cell(hop: &Hop, is_target: bool) -> Cell<'static> { let lost = hop.total_sent() - hop.total_recv(); Cell::from(match (lost, is_target) { (lost, target) if target && lost == hop.total_sent() => "🔴", (lost, target) if target && lost > 0 => "🟡", (lost, target) if !target && lost == hop.total_sent() => "🟤", (lost, target) if !target && lost > 0 => "🔵", _ => "🟢", }) } fn render_icmp_packet_type_cell(icmp_packet_type: Option) -> Cell<'static> { match icmp_packet_type { None => Cell::from("n/a"), Some(IcmpPacketType::TimeExceeded(_)) => Cell::from("TE"), Some(IcmpPacketType::EchoReply(_)) => Cell::from("ER"), Some(IcmpPacketType::Unreachable(_)) => Cell::from("DU"), Some(IcmpPacketType::NotApplicable) => Cell::from("NA"), } } fn render_icmp_packet_code_cell(icmp_packet_type: Option) -> Cell<'static> { match icmp_packet_type { Some( IcmpPacketType::Unreachable(code) | IcmpPacketType::TimeExceeded(code) | IcmpPacketType::EchoReply(code), ) => Cell::from(format!("{}", code.0)), _ => Cell::from(t!("na")), } } fn render_port_cell(port: u16) -> Cell<'static> { if port > 0 { Cell::from(format!("{port}")) } else { Cell::from(t!("na")) } } fn render_dscp_cell(dscp: Option) -> Cell<'static> { match dscp { Some(Dscp::DF) => Cell::from("DF"), Some(Dscp::AF11) => Cell::from("AF11"), Some(Dscp::AF12) => Cell::from("AF12"), Some(Dscp::AF13) => Cell::from("AF13"), Some(Dscp::AF21) => Cell::from("AF21"), Some(Dscp::AF22) => Cell::from("AF22"), Some(Dscp::AF23) => Cell::from("AF23"), Some(Dscp::AF31) => Cell::from("AF31"), Some(Dscp::AF32) => Cell::from("AF32"), Some(Dscp::AF33) => Cell::from("AF33"), Some(Dscp::AF41) => Cell::from("AF41"), Some(Dscp::AF42) => Cell::from("AF42"), Some(Dscp::AF43) => Cell::from("AF43"), Some(Dscp::CS1) => Cell::from("CS1"), Some(Dscp::CS2) => Cell::from("CS2"), Some(Dscp::CS3) => Cell::from("CS3"), Some(Dscp::CS4) => Cell::from("CS4"), Some(Dscp::CS5) => Cell::from("CS5"), Some(Dscp::CS6) => Cell::from("CS6"), Some(Dscp::CS7) => Cell::from("CS7"), Some(Dscp::EF) => Cell::from("EF"), Some(Dscp::VA) => Cell::from("VA"), Some(Dscp::LE) => Cell::from("LE"), Some(Dscp::Other(other)) => Cell::from(format!("0x{other:02x}")), None => Cell::from(t!("na")), } } fn render_ecn_cell(ecn: Option) -> Cell<'static> { match ecn { Some(Ecn::NotECT) => Cell::from("NotECT"), Some(Ecn::ECT1) => Cell::from("ECT1"), Some(Ecn::ECT0) => Cell::from("ECT0"), Some(Ecn::CE) => Cell::from("CE"), None => Cell::from(t!("na")), } } fn render_asn_cell(hop: &Hop, dns: &DnsResolver, config: &TuiConfig) -> Cell<'static> { if hop.total_recv() == 0 { Cell::from(t!("na")) } else if config.privacy_max_ttl >= Some(hop.ttl()) { Cell::from("****".to_string()) } else if !config.lookup_as_info { Cell::from(t!("na")) } else { let (addrs, _) = visible_addresses(hop, config.max_addrs); let content = addrs .into_iter() .map(|(addr, _)| format_asinfo_cell(*addr, dns)) .join("\n"); Cell::from(content) } } /// Render hostname table cell (normal mode). fn render_hostname( app: &TuiApp, hop: &Hop, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, ) -> (Cell<'static>, u16) { let (hostname, count) = if hop.total_recv() > 0 { if app.tui_config.privacy_max_ttl >= Some(hop.ttl()) { (format!("**{}**", t!("hidden")), 1) } else { let (addrs, count) = visible_addresses(hop, app.tui_config.max_addrs); let hostnames = addrs .into_iter() .map(|(addr, &freq)| { format_address(addr, freq, hop, dns, geoip_lookup, &app.tui_config) }) .join("\n"); (hostnames, count) } } else { (format!("{}", t!("no_response")), 1) }; (Cell::from(hostname), count) } /// calculate which addresses will be visible. fn visible_addresses(hop: &Hop, max_addrs: Option) -> (Vec<(&IpAddr, &usize)>, u16) { match max_addrs { None => { let addrs: Vec<_> = hop.addrs_with_counts().collect(); let count = hop.addr_count().clamp(1, u8::MAX as usize); (addrs, count as u16) } Some(max_addr) => { let addrs: Vec<_> = hop .addrs_with_counts() .sorted_unstable_by_key(|&(_, cnt)| cnt) .rev() .take(max_addr as usize) .collect(); let count = hop.addr_count().clamp(1, max_addr as usize); (addrs, count as u16) } } } /// Perform a reverse DNS lookup for an address and format the result. fn format_address( addr: &IpAddr, freq: usize, hop: &Hop, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> String { let addr_fmt = match config.address_mode { AddressMode::Ip => addr.to_string(), AddressMode::Host => { if config.lookup_as_info { let entry = dns.lazy_reverse_lookup_with_asinfo(*addr); format_dns_entry(entry, true, config.as_mode) } else { let entry = dns.lazy_reverse_lookup(*addr); format_dns_entry(entry, false, config.as_mode) } } AddressMode::Both => { let hostname = if config.lookup_as_info { let entry = dns.lazy_reverse_lookup_with_asinfo(*addr); format_dns_entry(entry, true, config.as_mode) } else { let entry = dns.lazy_reverse_lookup(*addr); format_dns_entry(entry, false, config.as_mode) }; format!("{hostname} ({addr})") } }; let exp_fmt = format_extensions(config, hop); let geo_fmt = match config.geoip_mode { GeoIpMode::Off => None, GeoIpMode::Short => geoip_lookup .lookup(*addr) .unwrap_or_default() .map(|geo| geo.short_name()), GeoIpMode::Long => geoip_lookup .lookup(*addr) .unwrap_or_default() .map(|geo| geo.long_name()), GeoIpMode::Location => geoip_lookup .lookup(*addr) .unwrap_or_default() .map(|geo| geo.location()), }; let freq_fmt = if hop.addr_count() > 1 { Some(format!( "{:.1}%", (freq as f64 / hop.total_recv() as f64) * 100_f64 )) } else { None }; let nat = match hop.last_nat_status() { NatStatus::Detected => Some("NAT"), _ => None, }; let mut address = addr_fmt; if let Some(geo) = geo_fmt.as_deref() { let _ = write!(address, " [{geo}]"); } if let Some(exp) = exp_fmt { let _ = write!(address, " [{exp}]"); } if let Some(nat) = nat { let _ = write!(address, " [{nat}]"); } if let Some(freq) = freq_fmt { let _ = write!(address, " [{freq}]"); } address } /// Format a `DnsEntry` with or without autonomous system (AS) information (if available) fn format_dns_entry(dns_entry: DnsEntry, lookup_as_info: bool, as_mode: AsMode) -> String { match dns_entry { DnsEntry::Resolved(Resolved::Normal(_, hosts)) => hosts.join(" "), DnsEntry::Resolved(Resolved::WithAsInfo(_, hosts, asinfo)) => { if lookup_as_info && !asinfo.asn.is_empty() { format!("{} {}", format_asinfo(&asinfo, as_mode), hosts.join(" ")) } else { hosts.join(" ") } } DnsEntry::NotFound(Unresolved::Normal(ip)) | DnsEntry::Pending(ip) => format!("{ip}"), DnsEntry::NotFound(Unresolved::WithAsInfo(ip, asinfo)) => { if lookup_as_info && !asinfo.asn.is_empty() { format!("{} {}", format_asinfo(&asinfo, as_mode), ip) } else { format!("{ip}") } } DnsEntry::Failed(ip) => format!("{}: {ip}", t!("dns_failed")), DnsEntry::Timeout(ip) => format!("{}: {ip}", t!("dns_timeout")), } } /// Format `AsInfo` based on the `ASDisplayMode`. fn format_asinfo(asinfo: &AsInfo, as_mode: AsMode) -> String { match as_mode { AsMode::Asn => format!("AS{}", asinfo.asn), AsMode::Prefix => format!("AS{} [{}]", asinfo.asn, asinfo.prefix), AsMode::CountryCode => format!("AS{} [{}]", asinfo.asn, asinfo.cc), AsMode::Registry => format!("AS{} [{}]", asinfo.asn, asinfo.registry), AsMode::Allocated => format!("AS{} [{}]", asinfo.asn, asinfo.allocated), AsMode::Name => format!("AS{} [{}]", asinfo.asn, asinfo.name), } } fn format_asinfo_cell(addr: IpAddr, dns: &DnsResolver) -> String { match dns.lazy_reverse_lookup_with_asinfo(addr) { DnsEntry::Resolved(Resolved::WithAsInfo(_, _, asinfo)) | DnsEntry::NotFound(Unresolved::WithAsInfo(_, asinfo)) if !asinfo.asn.is_empty() => { format_asinfo(&asinfo, AsMode::Asn) } DnsEntry::Pending(_) => String::new(), DnsEntry::Failed(_) | DnsEntry::Timeout(_) => "?????".to_string(), _ => t!("na").to_string(), } } /// Format `icmp` extensions. fn format_extensions(config: &TuiConfig, hop: &Hop) -> Option { if let Some(extensions) = hop.extensions() { match config.icmp_extension_mode { IcmpExtensionMode::Off => None, IcmpExtensionMode::Mpls => format_extensions_mpls(extensions), IcmpExtensionMode::Full => format_extensions_full(extensions), IcmpExtensionMode::All => Some(format_extensions_all(extensions)), } } else { None } } /// Format MPLS extensions as: `labels: 12345, 6789`. /// /// If not MPLS extensions are present then None is returned. fn format_extensions_mpls(extensions: &Extensions) -> Option { let labels = extensions .extensions .iter() .filter_map(|ext| match ext { Extension::Unknown(_) => None, Extension::Mpls(stack) => Some(stack), }) .flat_map(|ext| &ext.members) .map(|mem| mem.label) .format(", ") .to_string(); if labels.is_empty() { None } else { Some(format!("{}: {labels}", t!("labels"))) } } /// Format all known extensions with full details. /// /// For MPLS: `mpls(label=48320, ttl=1, exp=0, bos=1), mpls(...)` fn format_extensions_full(extensions: &Extensions) -> Option { let formatted = extensions .extensions .iter() .filter_map(|ext| match ext { Extension::Unknown(_) => None, Extension::Mpls(stack) => Some(stack), }) .flat_map(|ext| &ext.members) .map(format_ext_mpls_stack_member) .format(", ") .to_string(); if formatted.is_empty() { None } else { Some(formatted) } } /// Format a list all known and unknown extensions with full details. /// /// `mpls(label=48320, ttl=1, exp=0, bos=1), unknown(class=1, sub=1, object=0b c8 c1 01), ...` fn format_extensions_all(extensions: &Extensions) -> String { extensions .extensions .iter() .flat_map(|ext| match ext { Extension::Unknown(unknown) => vec![format_ext_unknown(unknown)], Extension::Mpls(stack) => stack .members .iter() .map(format_ext_mpls_stack_member) .collect::>(), }) .format(", ") .to_string() } /// Format a MPLS `icmp` extension object. pub fn format_ext_mpls_stack_member(member: &MplsLabelStackMember) -> String { format!( "mpls(label={}, ttl={}, exp={}, bos={})", member.label, member.ttl, member.exp, member.bos ) } /// Format an unknown `icmp` extension object. pub fn format_ext_unknown(unknown: &UnknownExtension) -> String { format!( "unknown(class={}, subtype={}, object={:02x})", unknown.class_num, unknown.class_subtype, unknown.bytes.iter().format(" ") ) } /// Render hostname table cell (detailed mode). fn render_hostname_with_details( app: &TuiApp, hop: &Hop, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> (Cell<'static>, u16) { let rendered = if hop.total_recv() > 0 { if config.privacy_max_ttl >= Some(hop.ttl()) { format!("**{}**", t!("hidden")) } else { let index = app.selected_hop_address; format_details(hop, index, dns, geoip_lookup, config) } } else { format!("{}", t!("no_response")) }; (Cell::from(rendered), 7) } /// Format hop details. fn format_details( hop: &Hop, offset: usize, dns: &DnsResolver, geoip_lookup: &GeoIpLookup, config: &TuiConfig, ) -> String { let Some(addr) = hop.addrs().nth(offset) else { return format!("Error: no addr for index {offset}"); }; let count = hop.addr_count(); let index = offset + 1; let geoip = geoip_lookup.lookup(*addr).unwrap_or_default(); let dns_entry = if config.lookup_as_info { dns.lazy_reverse_lookup_with_asinfo(*addr) } else { dns.lazy_reverse_lookup(*addr) }; let ext = hop.extensions(); let nat = hop.last_nat_status(); match dns_entry { DnsEntry::Pending(addr) => { fmt_details_line(addr, index, count, None, None, geoip, ext, nat, config) } DnsEntry::Resolved(Resolved::WithAsInfo(addr, hosts, asinfo)) => fmt_details_line( addr, index, count, Some(hosts), Some(asinfo), geoip, ext, nat, config, ), DnsEntry::NotFound(Unresolved::WithAsInfo(addr, asinfo)) => fmt_details_line( addr, index, count, Some(vec![]), Some(asinfo), geoip, ext, nat, config, ), DnsEntry::Resolved(Resolved::Normal(addr, hosts)) => fmt_details_line( addr, index, count, Some(hosts), None, geoip, ext, nat, config, ), DnsEntry::NotFound(Unresolved::Normal(addr)) => fmt_details_line( addr, index, count, Some(vec![]), None, geoip, ext, nat, config, ), DnsEntry::Failed(ip) => { format!("{}: {ip}", t!("dns_failed")) } DnsEntry::Timeout(ip) => { format!("{}: {ip}", t!("dns_timeout")) } } } /// Format hostname detail lines. /// /// Format as follows: /// /// ```text /// 172.217.24.78 [1 of 2] /// Host: hkg07s50-in-f14.1e100.net /// AS Name: AS15169 GOOGLE, US /// AS Info: 142.250.0.0/15 arin 2012-05-24 /// Geo: United States, North America /// Pos: 37.751, -97.822 (~1000km) /// Ext: [mpls(label=48268, ttl=1, exp=0, bos=1)] /// ``` #[expect(clippy::too_many_arguments)] fn fmt_details_line( addr: IpAddr, index: usize, count: usize, hostnames: Option>, asinfo: Option, geoip: Option>, extensions: Option<&Extensions>, nat: NatStatus, config: &TuiConfig, ) -> String { let as_fmt = match (config.lookup_as_info, asinfo) { (false, _) => format!( "AS {}: <{}>\nAS {}: <{}>", t!("name"), t!("not_enabled"), t!("info"), t!("not_enabled") ), (true, None) => format!( "AS {}: <{}>\nAS {}: <{}>", t!("name"), t!("info"), t!("awaited"), t!("awaited") ), (true, Some(info)) if info.asn.is_empty() => { format!( "AS {}: <{}>\nAS {}: <{}>", t!("name"), t!("not_found"), t!("info"), t!("not_found") ) } (true, Some(info)) => format!( "AS {}: AS{} {}\nAS {}: {} {} {}", t!("name"), info.asn, info.name, t!("info"), info.prefix, info.registry, info.allocated ), }; let hosts_rendered = if let Some(hosts) = hostnames { if hosts.is_empty() { format!("{}: <{}>", t!("host"), t!("not_found")) } else { format!("{}: {}", t!("host"), hosts.join(" ")) } } else { format!("{}: <{}>", t!("host"), t!("awaited")) }; let geoip_fmt = if let Some(geo) = geoip { let (lat, long, radius) = geo.coordinates().unwrap_or_default(); format!( "{}: {}\n{}: {}, {} (~{}{})", t!("geo"), geo.long_name(), t!("pos"), lat, long, radius, t!("kilometer"), ) } else { format!( "{}: <{}>\n{}: <{}>", t!("geo"), t!("not_found"), t!("pos"), t!("not_found") ) }; let ext_fmt = if let Some(extensions) = extensions { format!("{}: [{}]", t!("ext"), format_extensions_all(extensions)) } else { format!("{}: <{}>", t!("ext"), t!("none")) }; let nat_fmt = match nat { NatStatus::Detected => " [NAT]", _ => "", }; format!( "{addr}{nat_fmt} [{index} of {count}]\n{hosts_rendered}\n{as_fmt}\n{geoip_fmt}\n{ext_fmt}" ) } ================================================ FILE: crates/trippy-tui/src/frontend/render/tabs.rs ================================================ use crate::frontend::tui_app::TuiApp; use crate::t; use ratatui::Frame; use ratatui::layout::{Alignment, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, BorderType, Borders, Tabs}; /// Render the tabs, one per trace. pub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) { let tabs_block = Block::default() .title(Line::raw(t!("title_traces"))) .title_alignment(Alignment::Left) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(app.tui_config.theme.border)) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ); let titles: Vec<_> = app .trace_info .iter() .map(|trace| { Line::from(Span::styled( &trace.target_hostname, Style::default().fg(app.tui_config.theme.tab_text), )) }) .collect(); let tabs = Tabs::new(titles) .block(tabs_block) .select(app.trace_selected) .style(Style::default()) .highlight_style(Style::default().add_modifier(Modifier::BOLD)); f.render_widget(tabs, rect); } ================================================ FILE: crates/trippy-tui/src/frontend/render/util.rs ================================================ use ratatui::layout::{Constraint, Direction, Layout, Rect}; pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) .constraints( [ Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2), ] .as_ref(), ) .split(r); Layout::default() .direction(Direction::Horizontal) .constraints( [ Constraint::Percentage((100 - percent_x) / 2), Constraint::Percentage(percent_x), Constraint::Percentage((100 - percent_x) / 2), ] .as_ref(), ) .split(popup_layout[1])[1] } ================================================ FILE: crates/trippy-tui/src/frontend/render/world.rs ================================================ use crate::frontend::tui_app::TuiApp; use crate::t; use itertools::Itertools; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}; use ratatui::prelude::Line; use ratatui::style::{Color, Style}; use ratatui::symbols::Marker; use ratatui::text::Span; use ratatui::widgets::canvas::{Canvas, Circle, Context, Map, MapResolution, Rectangle}; use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph}; use std::collections::HashMap; use trippy_core::Hop; /// Render the `GeoIp` map. pub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) { let entries = build_map_entries(app); let chunks = Layout::default() .direction(Direction::Vertical) .constraints(MAP_LAYOUT) .split(rect); let info_rect = chunks[1].inner(Margin { vertical: 0, horizontal: 16, }); render_map_canvas(f, app, rect, &entries); render_map_info_panel(f, app, info_rect, &entries); } /// Render the map canvas. fn render_map_canvas(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: &[MapEntry]) { let theme = app.tui_config.theme; let map = Canvas::default() .background_color(app.tui_config.theme.bg) .block( Block::default() .title(Line::raw(t!("title_map"))) .borders(Borders::ALL) .border_style(Style::default().fg(app.tui_config.theme.border)) .style( Style::default() .bg(app.tui_config.theme.bg) .fg(app.tui_config.theme.text), ), ) .paint(|ctx| { render_map_canvas_world(ctx, theme.map_world); ctx.layer(); for entry in entries { let any_show = entry .hops .iter() .any(|hop| Some(*hop) > app.tui_config.privacy_max_ttl); if any_show { render_map_canvas_pin(ctx, entry); render_map_canvas_radius(ctx, entry, theme.map_radius); render_map_canvas_selected( ctx, entry, app.selected_hop_or_target(), theme.map_selected, ); } } }) .marker(Marker::Braille) .x_bounds([-180.0, 180.0]) .y_bounds([-90.0, 90.0]); f.render_widget(Clear, rect); f.render_widget(map, rect); } /// Render the map canvas world. fn render_map_canvas_world(ctx: &mut Context<'_>, color: Color) { ctx.draw(&Map { color, resolution: MapResolution::High, }); } /// Render the map canvas pin. fn render_map_canvas_pin(ctx: &mut Context<'_>, entry: &MapEntry) { let MapEntry { latitude, longitude, .. } = entry; ctx.print(*longitude, *latitude, Span::styled("📍", Style::default())); } /// Render the map canvas accuracy radius circle. fn render_map_canvas_radius(ctx: &mut Context<'_>, entry: &MapEntry, color: Color) { let MapEntry { latitude, longitude, radius, .. } = entry; let radius_degrees = f64::from(*radius) / 110_f64; if radius_degrees > 2_f64 { let circle_widget = Circle { x: *longitude, y: *latitude, radius: radius_degrees, color, }; ctx.draw(&circle_widget); } } /// Render the map canvas selected item box. fn render_map_canvas_selected( ctx: &mut Context<'_>, entry: &MapEntry, selected_hop: &Hop, color: Color, ) { let MapEntry { latitude, longitude, hops, .. } = entry; if hops.contains(&selected_hop.ttl()) { ctx.draw(&Rectangle { x: longitude - 5.0_f64, y: latitude - 5.0_f64, width: 10.0_f64, height: 10.0_f64, color, }); } } /// Render the map info panel. fn render_map_info_panel(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: &[MapEntry]) { let theme = app.tui_config.theme; let selected_hop = app.selected_hop_or_target(); let locations = entries .iter() .filter_map(|entry| { if entry.hops.contains(&selected_hop.ttl()) { Some(format!("{} [{}]", entry.long_name, entry.location)) } else { None } }) .collect::>(); let info = if app.tui_config.privacy_max_ttl >= Some(selected_hop.ttl()) { format!("**{}**", t!("hidden")) } else { match locations.as_slice() { _ if app.tui_config.geoip_mmdb_file.is_none() => t!("geoip_not_enabled").to_string(), [] if selected_hop.addr_count() > 0 => format!( "{} {} ({})", t!("geoip_no_data_for_hop"), selected_hop.ttl(), selected_hop.addrs().join(", ") ), [] => format!("{} {}", t!("geoip_no_data_for_hop"), selected_hop.ttl()), [loc] => loc.clone(), _ => format!( "{} {}", t!("geoip_multiple_data_for_hop"), selected_hop.ttl() ), } }; let info_panel = Paragraph::new(info) .block( Block::default() .title(format!("{} {}", t!("hop"), selected_hop.ttl())) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(theme.map_info_panel_border)) .style( Style::default() .bg(theme.map_info_panel_bg) .fg(theme.map_info_panel_text), ), ) .alignment(Alignment::Left); f.render_widget(Clear, rect); f.render_widget(info_panel, rect); } /// An entry to render on the map. struct MapEntry { long_name: String, location: String, latitude: f64, longitude: f64, radius: u16, hops: Vec, } /// Build a vec of `MapEntry` for all hops. /// /// Each entry represent a single `GeoIp` location, which may be associated with multiple hops. fn build_map_entries(app: &TuiApp) -> Vec { let mut geo_map: HashMap = HashMap::new(); for hop in app.tracer_data().hops_for_flow(app.selected_flow) { for addr in hop.addrs() { if let Some(geo) = app.geoip_lookup.lookup(*addr).unwrap_or_default() { if let Some((latitude, longitude, radius)) = geo.coordinates() { let entry = geo_map.entry(geo.long_name()).or_insert_with(|| MapEntry { long_name: geo.long_name(), location: format!("{latitude}, {longitude} ~{radius}{}", t!("kilometer")), latitude, longitude, radius, hops: vec![], }); entry.hops.push(hop.ttl()); } } } } geo_map.into_values().collect_vec() } const MAP_LAYOUT: [Constraint; 3] = [ Constraint::Min(1), Constraint::Length(3), Constraint::Length(1), ]; ================================================ FILE: crates/trippy-tui/src/frontend/render.rs ================================================ pub mod app; pub mod bar; pub mod body; pub mod bsod; pub mod chart; pub mod flows; pub mod footer; pub mod header; pub mod help; pub mod histogram; pub mod history; pub mod settings; pub mod splash; pub mod table; pub mod tabs; pub mod util; pub mod world; ================================================ FILE: crates/trippy-tui/src/frontend/theme.rs ================================================ use crate::config::{TuiColor, TuiTheme}; use ratatui::style::Color; /// Tui color theme. #[derive(Debug, Clone, Copy)] pub struct Theme { /// The default background color. /// /// This may be overridden for specific components. pub bg: Color, /// The default color of borders. /// /// This may be overridden for specific components. pub border: Color, /// The default color of text. /// /// This may be overridden for specific components. pub text: Color, /// The color of the text in traces tabs. pub tab_text: Color, /// The background color of the hops table header. pub hops_table_header_bg: Color, /// The color of text in the hops table header. pub hops_table_header_text: Color, /// The color of text of active rows in the hops table. pub hops_table_row_active_text: Color, /// The color of text of inactive rows in the hops table. pub hops_table_row_inactive_text: Color, /// The color of the selected series in the hops chart. pub hops_chart_selected: Color, /// The color of the unselected series in the hops chart. pub hops_chart_unselected: Color, /// The color of the axis in the hops chart. pub hops_chart_axis: Color, /// The color of bars in the frequency chart. pub frequency_chart_bar: Color, /// The color of text in the bars of the frequency chart. pub frequency_chart_text: Color, /// The color of the selected flow bar in the flows chart. pub flows_chart_bar_selected: Color, /// The color of the unselected flow bar in the flows chart. pub flows_chart_bar_unselected: Color, /// The color of the current flow text in the flows chart. pub flows_chart_text_current: Color, /// The color of the non-current flow text in the flows chart. pub flows_chart_text_non_current: Color, /// The color of the samples chart. pub samples_chart: Color, /// The color of the samples chart for lost probes. pub samples_chart_lost: Color, /// The background color of the help dialog. pub help_dialog_bg: Color, /// The color of the text in the help dialog. pub help_dialog_text: Color, /// The background color of the settings dialog. pub settings_dialog_bg: Color, /// The color of the text in settings dialog tabs. pub settings_tab_text: Color, /// The color of text in the settings table header. pub settings_table_header_text: Color, /// The background color of the settings table header. pub settings_table_header_bg: Color, /// The color of text of rows in the settings table. pub settings_table_row_text: Color, /// The color of the map world diagram. pub map_world: Color, /// The color of the map accuracy radius circle. pub map_radius: Color, /// The color of the map selected item box. pub map_selected: Color, /// The color of border of the map info panel. pub map_info_panel_border: Color, /// The background color of the map info panel. pub map_info_panel_bg: Color, /// The color of text in the map info panel. pub map_info_panel_text: Color, /// The color of the info bar background. pub info_bar_bg: Color, /// The color of the info bar text. pub info_bar_text: Color, } impl From for Theme { fn from(value: TuiTheme) -> Self { Self { bg: Color::from(value.bg), border: Color::from(value.border), text: Color::from(value.text), tab_text: Color::from(value.tab_text), hops_table_header_bg: Color::from(value.hops_table_header_bg), hops_table_header_text: Color::from(value.hops_table_header_text), hops_table_row_active_text: Color::from(value.hops_table_row_active_text), hops_table_row_inactive_text: Color::from(value.hops_table_row_inactive_text), hops_chart_selected: Color::from(value.hops_chart_selected), hops_chart_unselected: Color::from(value.hops_chart_unselected), hops_chart_axis: Color::from(value.hops_chart_axis), frequency_chart_bar: Color::from(value.frequency_chart_bar), frequency_chart_text: Color::from(value.frequency_chart_text), flows_chart_bar_selected: Color::from(value.flows_chart_bar_selected), flows_chart_bar_unselected: Color::from(value.flows_chart_bar_unselected), flows_chart_text_current: Color::from(value.flows_chart_text_current), flows_chart_text_non_current: Color::from(value.flows_chart_text_non_current), samples_chart: Color::from(value.samples_chart), samples_chart_lost: Color::from(value.samples_chart_lost), help_dialog_bg: Color::from(value.help_dialog_bg), help_dialog_text: Color::from(value.help_dialog_text), settings_dialog_bg: Color::from(value.settings_dialog_bg), settings_tab_text: Color::from(value.settings_tab_text), settings_table_header_text: Color::from(value.settings_table_header_text), settings_table_header_bg: Color::from(value.settings_table_header_bg), settings_table_row_text: Color::from(value.settings_table_row_text), map_world: Color::from(value.map_world), map_radius: Color::from(value.map_radius), map_selected: Color::from(value.map_selected), map_info_panel_border: Color::from(value.map_info_panel_border), map_info_panel_bg: Color::from(value.map_info_panel_bg), map_info_panel_text: Color::from(value.map_info_panel_text), info_bar_bg: Color::from(value.info_bar_bg), info_bar_text: Color::from(value.info_bar_text), } } } impl From for Color { #[expect(clippy::too_many_lines)] fn from(value: TuiColor) -> Self { match value { TuiColor::Black => Self::Black, TuiColor::Red => Self::Red, TuiColor::Green => Self::Green, TuiColor::Yellow => Self::Yellow, TuiColor::Blue => Self::Blue, TuiColor::Magenta => Self::Magenta, TuiColor::Cyan => Self::Cyan, TuiColor::Gray => Self::Gray, TuiColor::DarkGray => Self::DarkGray, TuiColor::LightRed => Self::LightRed, TuiColor::LightGreen => Self::LightGreen, TuiColor::LightYellow => Self::LightYellow, TuiColor::LightBlue => Self::LightBlue, TuiColor::LightMagenta => Self::LightMagenta, TuiColor::LightCyan => Self::LightCyan, TuiColor::White => Self::White, TuiColor::AliceBlue => Self::from_u32(0x00f0_f8ff), TuiColor::AntiqueWhite => Self::from_u32(0x00fa_ebd7), TuiColor::Aqua => Self::from_u32(0x0000_ffff), TuiColor::Aquamarine => Self::from_u32(0x007f_ffd4), TuiColor::Azure => Self::from_u32(0x00f0_ffff), TuiColor::Beige => Self::from_u32(0x00f5_f5dc), TuiColor::Bisque => Self::from_u32(0x00ff_e4c4), TuiColor::BlanchedAlmond => Self::from_u32(0x00ff_ebcd), TuiColor::BlueViolet => Self::from_u32(0x008a_2be2), TuiColor::Brown => Self::from_u32(0x00a5_2a2a), TuiColor::BurlyWood => Self::from_u32(0x00de_b887), TuiColor::CadetBlue => Self::from_u32(0x005f_9ea0), TuiColor::Chartreuse => Self::from_u32(0x007f_ff00), TuiColor::Chocolate => Self::from_u32(0x00d2_691e), TuiColor::Coral => Self::from_u32(0x00ff_7f50), TuiColor::CornflowerBlue => Self::from_u32(0x0064_95ed), TuiColor::CornSilk => Self::from_u32(0x00ff_f8dc), TuiColor::Crimson => Self::from_u32(0x00dc_143c), TuiColor::DarkBlue => Self::from_u32(0x0000_008b), TuiColor::DarkCyan => Self::from_u32(0x0000_8b8b), TuiColor::DarkGoldenrod => Self::from_u32(0x00b8_860b), TuiColor::DarkGreen => Self::from_u32(0x0000_6400), TuiColor::DarkKhaki => Self::from_u32(0x00bd_b76b), TuiColor::DarkMagenta => Self::from_u32(0x008b_008b), TuiColor::DarkOliveGreen => Self::from_u32(0x0055_6b2f), TuiColor::DarkOrange => Self::from_u32(0x00ff_8c00), TuiColor::DarkOrchid => Self::from_u32(0x0099_32cc), TuiColor::DarkRed => Self::from_u32(0x008b_0000), TuiColor::DarkSalmon => Self::from_u32(0x00e9_967a), TuiColor::DarkSeaGreen => Self::from_u32(0x008f_bc8f), TuiColor::DarkSlateBlue => Self::from_u32(0x0048_3d8b), TuiColor::DarkSlateGray => Self::from_u32(0x002f_4f4f), TuiColor::DarkTurquoise => Self::from_u32(0x0000_ced1), TuiColor::DarkViolet => Self::from_u32(0x0094_00d3), TuiColor::DeepPink => Self::from_u32(0x00ff_1493), TuiColor::DeepSkyBlue => Self::from_u32(0x0000_bfff), TuiColor::DimGray => Self::from_u32(0x0069_6969), TuiColor::DodgerBlue => Self::from_u32(0x001e_90ff), TuiColor::Firebrick => Self::from_u32(0x00b2_2222), TuiColor::FloralWhite => Self::from_u32(0x00ff_faf0), TuiColor::ForestGreen => Self::from_u32(0x0022_8b22), TuiColor::Fuchsia => Self::from_u32(0x00ff_00ff), TuiColor::Gainsboro => Self::from_u32(0x00dc_dcdc), TuiColor::GhostWhite => Self::from_u32(0x00f8_f8ff), TuiColor::Gold => Self::from_u32(0x00ff_d700), TuiColor::Goldenrod => Self::from_u32(0x00da_a520), TuiColor::GreenYellow => Self::from_u32(0x00ad_ff2f), TuiColor::Honeydew => Self::from_u32(0x00f0_fff0), TuiColor::HotPink => Self::from_u32(0x00ff_69b4), TuiColor::IndianRed => Self::from_u32(0x00cd_5c5c), TuiColor::Indigo => Self::from_u32(0x004b_0082), TuiColor::Ivory => Self::from_u32(0x00ff_fff0), TuiColor::Khaki => Self::from_u32(0x00f0_e68c), TuiColor::Lavender => Self::from_u32(0x00e6_e6fa), TuiColor::LavenderBlush => Self::from_u32(0x00ff_f0f5), TuiColor::LawnGreen => Self::from_u32(0x007c_fc00), TuiColor::LemonChiffon => Self::from_u32(0x00ff_facd), TuiColor::LightCoral => Self::from_u32(0x00f0_8080), TuiColor::LightGoldenrodYellow => Self::from_u32(0x00fa_fad2), TuiColor::LightGray => Self::from_u32(0x00d3_d3d3), TuiColor::LightPink => Self::from_u32(0x00ff_b6c1), TuiColor::LightSalmon => Self::from_u32(0x00ff_a07a), TuiColor::LightSeaGreen => Self::from_u32(0x0020_b2aa), TuiColor::LightSkyBlue => Self::from_u32(0x0087_cefa), TuiColor::LightSlateGray => Self::from_u32(0x0077_8899), TuiColor::LightSteelBlue => Self::from_u32(0x00b0_c4de), TuiColor::Lime => Self::from_u32(0x0000_ff00), TuiColor::LimeGreen => Self::from_u32(0x0032_cd32), TuiColor::Linen => Self::from_u32(0x00fa_f0e6), TuiColor::Maroon => Self::from_u32(0x0080_0000), TuiColor::MediumAquamarine => Self::from_u32(0x0066_cdaa), TuiColor::MediumBlue => Self::from_u32(0x0000_00cd), TuiColor::MediumOrchid => Self::from_u32(0x00ba_55d3), TuiColor::MediumPurple => Self::from_u32(0x0093_70db), TuiColor::MediumSeaGreen => Self::from_u32(0x003c_b371), TuiColor::MediumSlateBlue => Self::from_u32(0x007b_68ee), TuiColor::MediumSpringGreen => Self::from_u32(0x0000_fa9a), TuiColor::MediumTurquoise => Self::from_u32(0x0048_d1cc), TuiColor::MediumVioletRed => Self::from_u32(0x00c7_1585), TuiColor::MidnightBlue => Self::from_u32(0x0019_1970), TuiColor::MintCream => Self::from_u32(0x00f5_fffa), TuiColor::MistyRose => Self::from_u32(0x00ff_e4e1), TuiColor::Moccasin => Self::from_u32(0x00ff_e4b5), TuiColor::NavajoWhite => Self::from_u32(0x00ff_dead), TuiColor::Navy => Self::from_u32(0x0000_0080), TuiColor::OldLace => Self::from_u32(0x00fd_f5e6), TuiColor::Olive => Self::from_u32(0x0080_8000), TuiColor::OliveDrab => Self::from_u32(0x006b_8e23), TuiColor::Orange => Self::from_u32(0x00ff_a500), TuiColor::OrangeRed => Self::from_u32(0x00ff_4500), TuiColor::Orchid => Self::from_u32(0x00da_70d6), TuiColor::PaleGoldenrod => Self::from_u32(0x00ee_e8aa), TuiColor::PaleGreen => Self::from_u32(0x0098_fb98), TuiColor::PaleTurquoise => Self::from_u32(0x00af_eeee), TuiColor::PaleVioletRed => Self::from_u32(0x00db_7093), TuiColor::PapayaWhip => Self::from_u32(0x00ff_efd5), TuiColor::PeachPuff => Self::from_u32(0x00ff_dab9), TuiColor::Peru => Self::from_u32(0x00cd_853f), TuiColor::Pink => Self::from_u32(0x00ff_c0cb), TuiColor::Plum => Self::from_u32(0x00dd_a0dd), TuiColor::PowderBlue => Self::from_u32(0x00b0_e0e6), TuiColor::Purple => Self::from_u32(0x0080_0080), TuiColor::RebeccaPurple => Self::from_u32(0x0066_3399), TuiColor::RosyBrown => Self::from_u32(0x00bc_8f8f), TuiColor::RoyalBlue => Self::from_u32(0x0041_69e1), TuiColor::SaddleBrown => Self::from_u32(0x008b_4513), TuiColor::Salmon => Self::from_u32(0x00fa_8072), TuiColor::SandyBrown => Self::from_u32(0x00f4_a460), TuiColor::SeaGreen => Self::from_u32(0x002e_8b57), TuiColor::SeaShell => Self::from_u32(0x00ff_f5ee), TuiColor::Sienna => Self::from_u32(0x00a0_522d), TuiColor::Silver => Self::from_u32(0x00c0_c0c0), TuiColor::SkyBlue => Self::from_u32(0x0087_ceeb), TuiColor::SlateBlue => Self::from_u32(0x006a_5acd), TuiColor::SlateGray => Self::from_u32(0x0070_8090), TuiColor::Snow => Self::from_u32(0x00ff_fafa), TuiColor::SpringGreen => Self::from_u32(0x0000_ff7f), TuiColor::SteelBlue => Self::from_u32(0x0046_82b4), TuiColor::Tan => Self::from_u32(0x00d2_b48c), TuiColor::Teal => Self::from_u32(0x0000_8080), TuiColor::Thistle => Self::from_u32(0x00d8_bfd8), TuiColor::Tomato => Self::from_u32(0x00ff_6347), TuiColor::Turquoise => Self::from_u32(0x0040_e0d0), TuiColor::Violet => Self::from_u32(0x00ee_82ee), TuiColor::Wheat => Self::from_u32(0x00f5_deb3), TuiColor::WhiteSmoke => Self::from_u32(0x00f5_f5f5), TuiColor::YellowGreen => Self::from_u32(0x009a_cd32), TuiColor::Rgb(r, g, b) => Self::Rgb(r, g, b), } } } pub fn fmt_color(color: Color) -> String { match color { Color::Black => "black".to_string(), Color::Red => "red".to_string(), Color::Green => "green".to_string(), Color::Yellow => "yellow".to_string(), Color::Blue => "blue".to_string(), Color::Magenta => "magenta".to_string(), Color::Cyan => "cyan".to_string(), Color::Gray => "gray".to_string(), Color::DarkGray => "darkgray".to_string(), Color::LightRed => "lightred".to_string(), Color::LightGreen => "lightgreen".to_string(), Color::LightYellow => "lightyellow".to_string(), Color::LightBlue => "lightblue".to_string(), Color::LightMagenta => "lightmagenta".to_string(), Color::LightCyan => "lightcyan".to_string(), Color::White => "white".to_string(), Color::Rgb(r, g, b) => format!("{r:02x}{g:02x}{b:02x}"), _ => "unknown".to_string(), } } ================================================ FILE: crates/trippy-tui/src/frontend/tui_app.rs ================================================ use crate::app::TraceInfo; use crate::frontend::config::TuiConfig; use crate::frontend::render::settings::{SETTINGS_TAB_COLUMNS, settings_tabs}; use crate::geoip::GeoIpLookup; use itertools::Itertools; use ratatui::widgets::TableState; use std::time::SystemTime; use trippy_core::FlowId; use trippy_core::Hop; use trippy_core::State; use trippy_dns::{DnsResolver, ResolveMethod}; pub struct TuiApp { pub selected_tracer_data: State, pub trace_info: Vec, pub tui_config: TuiConfig, /// The state of the hop table. pub table_state: TableState, /// The state of the settings table. pub setting_table_state: TableState, /// The selected trace. pub trace_selected: usize, /// The selected tab in the settings dialog. pub settings_tab_selected: usize, /// The index of the current address to show for the selected hop. /// /// Only used in detail mode. pub selected_hop_address: usize, /// The `FlowId` of the selected flow. /// /// FlowId(0) represents the unified flow for the trace. pub selected_flow: FlowId, /// Ordered flow ids with counts. pub flow_counts: Vec<(FlowId, usize)>, pub resolver: DnsResolver, pub geoip_lookup: GeoIpLookup, pub show_help: bool, pub show_settings: bool, pub show_hop_details: bool, pub show_flows: bool, pub show_chart: bool, pub show_map: bool, pub frozen_start: Option, pub zoom_factor: usize, } impl TuiApp { pub fn new( tui_config: TuiConfig, resolver: DnsResolver, geoip_lookup: GeoIpLookup, trace_info: Vec, ) -> Self { Self { selected_tracer_data: State::default(), trace_info, tui_config, table_state: TableState::default(), setting_table_state: TableState::default(), trace_selected: 0, settings_tab_selected: 0, selected_hop_address: 0, selected_flow: State::default_flow_id(), flow_counts: vec![], resolver, geoip_lookup, show_help: false, show_settings: false, show_hop_details: false, show_flows: false, show_chart: false, show_map: false, frozen_start: None, zoom_factor: 1, } } pub const fn tracer_data(&self) -> &State { &self.selected_tracer_data } pub fn snapshot_trace_data(&mut self) { self.selected_tracer_data = self.trace_info[self.trace_selected].data.snapshot(); } pub fn clear_trace_data(&self) { self.trace_info[self.trace_selected].data.clear(); } pub fn selected_hop_or_target(&self) -> &Hop { self.table_state.selected().map_or_else( || self.tracer_data().target_hop(self.selected_flow), |s| &self.tracer_data().hops_for_flow(self.selected_flow)[s], ) } pub fn selected_hop(&self) -> Option<&Hop> { self.table_state .selected() .map(|s| &self.tracer_data().hops_for_flow(self.selected_flow)[s]) } pub fn tracer_config(&self) -> &TraceInfo { &self.trace_info[self.trace_selected] } pub fn clamp_selected_hop(&mut self) { let hop_count = self.tracer_data().hops_for_flow(self.selected_flow).len(); if let Some(selected) = self.table_state.selected() { if selected > hop_count - 1 { self.table_state.select(Some(hop_count - 1)); } } } pub fn update_order_flow_counts(&mut self) { pub fn order_flows( &(flow_id1, count1): &(FlowId, usize), &(flow_id2, count2): &(FlowId, usize), ) -> std::cmp::Ordering { match count1.cmp(&count2) { std::cmp::Ordering::Equal => flow_id2.cmp(&flow_id1), ord => ord, } } self.flow_counts = self .tracer_data() .flows() .iter() .map(|&(_, flow_id)| { let count = self.tracer_data().round_count(flow_id); (flow_id, count) }) .sorted_by(order_flows) .rev() .take(self.selected_tracer_data.max_flows()) .collect::>(); } pub fn next_hop(&mut self) { let hop_count = self.tracer_data().hops_for_flow(self.selected_flow).len(); if hop_count == 0 { return; } let max_index = 0.max(hop_count.saturating_sub(1)); let i = match self.table_state.selected() { Some(i) => { if i < max_index { i + 1 } else { i } } None => 0, }; self.table_state.select(Some(i)); self.selected_hop_address = 0; } pub fn previous_hop(&mut self) { let hop_count = self.tracer_data().hops_for_flow(self.selected_flow).len(); if hop_count == 0 { return; } let i = match self.table_state.selected() { Some(i) => { if i > 0 { i - 1 } else { i } } None => 0.max(hop_count.saturating_sub(1)), }; self.table_state.select(Some(i)); self.selected_hop_address = 0; } pub fn next_trace(&mut self) { if self.trace_info.len() > 1 && self.trace_selected < self.trace_info.len() - 1 { self.trace_selected += 1; self.clear(); } } pub fn previous_trace(&mut self) { if self.trace_info.len() > 1 && self.trace_selected > 0 { self.trace_selected -= 1; self.clear(); } } pub fn next_hop_address(&mut self) { if let Some(hop) = self.selected_hop() { if self.selected_hop_address < hop.addr_count() - 1 { self.selected_hop_address += 1; } } } pub fn previous_hop_address(&mut self) { if self.selected_hop().is_some() && self.selected_hop_address > 0 { self.selected_hop_address -= 1; } } pub fn flow_count(&self) -> usize { self.selected_tracer_data.flows().len() } pub fn next_flow(&mut self) { if self.show_flows { let (cur_index, _) = self .flow_counts .iter() .find_position(|(flow_id, _)| *flow_id == self.selected_flow) .unwrap(); if cur_index < self.flow_counts.len() - 1 { self.selected_flow = self.flow_counts[cur_index + 1].0; } } } pub fn previous_flow(&mut self) { if self.show_flows { let (cur_index, _) = self .flow_counts .iter() .find_position(|(flow_id, _)| *flow_id == self.selected_flow) .unwrap(); if cur_index > 0 { self.selected_flow = self.flow_counts[cur_index - 1].0; } } } pub fn next_settings_tab(&mut self) { if self.settings_tab_selected < settings_tabs().len() - 1 { self.settings_tab_selected += 1; } self.setting_table_state.select(Some(0)); } pub fn previous_settings_tab(&mut self) { if self.settings_tab_selected > 0 { self.settings_tab_selected -= 1; } self.setting_table_state.select(Some(0)); } pub fn next_settings_item(&mut self) { let count = self.get_settings_items_count(); let max_index = 0.max(count.saturating_sub(1)); let i = match self.setting_table_state.selected() { Some(i) => { if i < max_index { i + 1 } else { i } } None => 0, }; self.setting_table_state.select(Some(i)); } pub fn previous_settings_item(&mut self) { let count = self.get_settings_items_count(); let i = match self.setting_table_state.selected() { Some(i) => { if i > 0 { i - 1 } else { i } } None => 0.max(count.saturating_sub(1)), }; self.setting_table_state.select(Some(i)); } fn get_settings_items_count(&self) -> usize { if self.settings_tab_selected == SETTINGS_TAB_COLUMNS { self.tui_config.tui_columns.all_columns_count() } else { settings_tabs()[self.settings_tab_selected].1 } } pub fn toggle_column_visibility(&mut self) { if self.settings_tab_selected == SETTINGS_TAB_COLUMNS { if let Some(selected) = self.setting_table_state.selected() { self.tui_config.tui_columns.toggle(selected); } } } pub fn move_column_down(&mut self) { if self.settings_tab_selected == SETTINGS_TAB_COLUMNS { let count = self.tui_config.tui_columns.all_columns_count(); if let Some(selected) = self.setting_table_state.selected() { if selected < count - 1 { self.tui_config.tui_columns.move_down(selected); self.setting_table_state.select(Some(selected + 1)); } } } } pub fn move_column_up(&mut self) { if self.settings_tab_selected == SETTINGS_TAB_COLUMNS { if let Some(selected) = self.setting_table_state.selected() { if selected > 0 { self.tui_config.tui_columns.move_up(selected); self.setting_table_state.select(Some(selected - 1)); } } } } pub fn clear(&mut self) { self.table_state.select(None); self.selected_hop_address = 0; } pub fn toggle_help(&mut self) { self.show_help = !self.show_help; } pub fn toggle_settings(&mut self) { self.show_settings = !self.show_settings; } pub fn show_settings_columns(&mut self, column_index: usize) { self.show_settings = true; if self.settings_tab_selected != column_index { self.settings_tab_selected = column_index; self.setting_table_state.select(Some(0)); } } pub fn toggle_hop_details(&mut self) { if self.show_hop_details { self.tui_config.max_addrs = None; } else { self.tui_config.max_addrs = Some(1); } self.show_hop_details = !self.show_hop_details; } pub fn toggle_freeze(&mut self) { self.frozen_start = match self.frozen_start { None => Some(SystemTime::now()), Some(_) => None, }; } pub fn toggle_chart(&mut self) { self.show_chart = !self.show_chart; self.show_map = false; } pub fn toggle_map(&mut self) { self.show_map = !self.show_map; self.show_chart = false; } pub fn toggle_flows(&mut self) { if self.trace_info.len() == 1 && self.selected_tracer_data.max_flows() > 1 { if self.show_flows { self.selected_flow = FlowId(0); self.show_flows = false; self.selected_hop_address = 0; } else if self.flow_count() > 0 { self.selected_flow = FlowId(1); self.show_flows = true; self.selected_hop_address = 0; } } } pub fn expand_privacy(&mut self) { let hop_count = self.tracer_data().hops_for_flow(self.selected_flow).len(); if let Some(privacy_max_ttl) = self.tui_config.privacy_max_ttl { if usize::from(privacy_max_ttl) < hop_count { self.tui_config.privacy_max_ttl = Some(privacy_max_ttl + 1); } } else { self.tui_config.privacy_max_ttl = Some(0); } } pub fn contract_privacy(&mut self) { if let Some(privacy_max_ttl) = self.tui_config.privacy_max_ttl { if privacy_max_ttl > 0 { self.tui_config.privacy_max_ttl = Some(privacy_max_ttl - 1); } else { self.tui_config.privacy_max_ttl = None; } } } pub fn toggle_asinfo(&mut self) { match self.resolver.config().resolve_method { ResolveMethod::Resolv | ResolveMethod::Google | ResolveMethod::Cloudflare => { self.tui_config.lookup_as_info = !self.tui_config.lookup_as_info; self.resolver.flush(); } ResolveMethod::System => {} } } pub fn expand_hosts(&mut self) { self.tui_config.max_addrs = match self.tui_config.max_addrs { None => Some(1), Some(i) if Some(i) < self.max_hosts() => Some(i + 1), Some(i) => Some(i), } } pub fn contract_hosts(&mut self) { self.tui_config.max_addrs = match self.tui_config.max_addrs { Some(i) if i > 1 => Some(i - 1), _ => None, } } pub fn zoom_in(&mut self) { if self.zoom_factor < MAX_ZOOM_FACTOR { self.zoom_factor += 1; } } pub fn zoom_out(&mut self) { if self.zoom_factor > 1 { self.zoom_factor -= 1; } } pub fn expand_hosts_max(&mut self) { self.tui_config.max_addrs = self.max_hosts(); } pub fn contract_hosts_min(&mut self) { self.tui_config.max_addrs = Some(1); } /// The maximum number of hosts per hop for the currently selected trace. pub fn max_hosts(&self) -> Option { self.selected_tracer_data .hops_for_flow(self.selected_flow) .iter() .map(|h| h.addrs().count()) .max() .and_then(|i| u8::try_from(i).ok()) } } const MAX_ZOOM_FACTOR: usize = 16; ================================================ FILE: crates/trippy-tui/src/frontend.rs ================================================ use crate::app::TraceInfo; use crate::config::AddressMode; use crate::frontend::binding::CTRL_C; use crate::geoip::GeoIpLookup; pub use config::TuiConfig; use crossterm::event::KeyEventKind; use crossterm::{ event::{self, Event}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use ratatui::layout::Position; use ratatui::{ Terminal, backend::{Backend, CrosstermBackend}, }; use std::io; use trippy_dns::DnsResolver; use tui_app::TuiApp; mod binding; mod columns; mod config; mod render; mod theme; mod tui_app; /// Run the frontend TUI. pub fn run_frontend( traces: Vec, tui_config: TuiConfig, resolver: DnsResolver, geoip_lookup: GeoIpLookup, ) -> anyhow::Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; let original_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |panic| { disable_raw_mode().expect("disable_raw_mode"); execute!(io::stdout(), LeaveAlternateScreen).expect("execute LeaveAlternateScreen"); original_hook(panic); })); let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let preserve_screen = tui_config.preserve_screen; let mut app = TuiApp::new(tui_config, resolver, geoip_lookup, traces); let res = run_app(&mut terminal, &mut app); disable_raw_mode()?; if preserve_screen || matches!(res, Ok(ExitAction::PreserveScreen)) { terminal.set_cursor_position(Position::new(0, terminal.size()?.height))?; terminal.backend_mut().append_lines(1)?; } else { execute!(terminal.backend_mut(), LeaveAlternateScreen)?; } terminal.show_cursor()?; if let Err(err) = res { println!("{err:?}"); } Ok(()) } /// The exit action to take when the frontend exits. enum ExitAction { /// Exit the frontend normally. Normal, /// Preserve the screen on exit. PreserveScreen, } #[expect(clippy::too_many_lines)] fn run_app(terminal: &mut Terminal, app: &mut TuiApp) -> io::Result { loop { if app.frozen_start.is_none() { app.snapshot_trace_data(); app.clamp_selected_hop(); app.update_order_flow_counts(); } terminal.draw(|f| render::app::render(f, app))?; if event::poll(app.tui_config.refresh_rate)? { if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { let bindings = &app.tui_config.bindings; if app.show_help { if bindings.toggle_help.check(key) || bindings.toggle_help_alt.check(key) || bindings.clear_selection.check(key) || bindings.quit.check(key) { app.toggle_help(); } else if bindings.toggle_settings.check(key) { app.toggle_help(); app.toggle_settings(); } else if bindings.toggle_settings_tui.check(key) { app.toggle_help(); app.show_settings_columns(0); } else if bindings.toggle_settings_trace.check(key) { app.toggle_help(); app.show_settings_columns(1); } else if bindings.toggle_settings_dns.check(key) { app.toggle_help(); app.show_settings_columns(2); } else if bindings.toggle_settings_geoip.check(key) { app.toggle_help(); app.show_settings_columns(3); } else if bindings.toggle_settings_bindings.check(key) { app.toggle_help(); app.show_settings_columns(4); } else if bindings.toggle_settings_theme.check(key) { app.toggle_help(); app.show_settings_columns(5); } else if bindings.toggle_settings_columns.check(key) { app.toggle_help(); app.show_settings_columns(6); } } else if app.show_settings { if bindings.toggle_settings.check(key) || bindings.clear_selection.check(key) || bindings.quit.check(key) { app.toggle_settings(); } else if bindings.toggle_settings_tui.check(key) { app.show_settings_columns(0); } else if bindings.toggle_settings_trace.check(key) { app.show_settings_columns(1); } else if bindings.toggle_settings_dns.check(key) { app.show_settings_columns(2); } else if bindings.toggle_settings_geoip.check(key) { app.show_settings_columns(3); } else if bindings.toggle_settings_bindings.check(key) { app.show_settings_columns(4); } else if bindings.toggle_settings_theme.check(key) { app.show_settings_columns(5); } else if bindings.toggle_settings_columns.check(key) { app.show_settings_columns(6); } else if bindings.previous_trace.check(key) { app.previous_settings_tab(); } else if bindings.next_trace.check(key) { app.next_settings_tab(); } else if bindings.next_hop.check(key) { app.next_settings_item(); } else if bindings.previous_hop.check(key) { app.previous_settings_item(); } else if bindings.toggle_chart.check(key) { app.toggle_column_visibility(); } else if bindings.next_hop_address.check(key) { app.move_column_down(); } else if bindings.previous_hop_address.check(key) { app.move_column_up(); } } else if bindings.toggle_help.check(key) || bindings.toggle_help_alt.check(key) { app.toggle_help(); } else if bindings.toggle_settings.check(key) { app.toggle_settings(); } else if bindings.toggle_settings_tui.check(key) { app.show_settings_columns(0); } else if bindings.toggle_settings_trace.check(key) { app.show_settings_columns(1); } else if bindings.toggle_settings_dns.check(key) { app.show_settings_columns(2); } else if bindings.toggle_settings_geoip.check(key) { app.show_settings_columns(3); } else if bindings.toggle_settings_bindings.check(key) { app.show_settings_columns(4); } else if bindings.toggle_settings_theme.check(key) { app.show_settings_columns(5); } else if bindings.toggle_settings_columns.check(key) { app.show_settings_columns(6); } else if bindings.next_hop.check(key) { app.next_hop(); } else if bindings.previous_hop.check(key) { app.previous_hop(); } else if bindings.previous_trace.check(key) { if app.show_flows { app.previous_flow(); } else { app.previous_trace(); } } else if bindings.next_trace.check(key) { if app.show_flows { app.next_flow(); } else { app.next_trace(); } } else if bindings.next_hop_address.check(key) { app.next_hop_address(); } else if bindings.previous_hop_address.check(key) { app.previous_hop_address(); } else if bindings.address_mode_ip.check(key) { app.tui_config.address_mode = AddressMode::Ip; } else if bindings.address_mode_host.check(key) { app.tui_config.address_mode = AddressMode::Host; } else if bindings.address_mode_both.check(key) { app.tui_config.address_mode = AddressMode::Both; } else if bindings.toggle_freeze.check(key) { app.toggle_freeze(); } else if bindings.toggle_chart.check(key) { app.toggle_chart(); } else if bindings.toggle_map.check(key) { app.toggle_map(); } else if bindings.toggle_flows.check(key) { app.toggle_flows(); } else if bindings.expand_privacy.check(key) { app.expand_privacy(); } else if bindings.contract_privacy.check(key) { app.contract_privacy(); } else if bindings.contract_hosts_min.check(key) { app.contract_hosts_min(); } else if bindings.expand_hosts_max.check(key) { app.expand_hosts_max(); } else if bindings.contract_hosts.check(key) { app.contract_hosts(); } else if bindings.expand_hosts.check(key) { app.expand_hosts(); } else if bindings.chart_zoom_in.check(key) { app.zoom_in(); } else if bindings.chart_zoom_out.check(key) { app.zoom_out(); } else if bindings.clear_trace_data.check(key) { app.clear(); app.clear_trace_data(); } else if bindings.clear_dns_cache.check(key) { app.resolver.flush(); } else if bindings.clear_selection.check(key) { app.clear(); } else if bindings.toggle_as_info.check(key) { app.toggle_asinfo(); } else if bindings.toggle_hop_details.check(key) { app.toggle_hop_details(); } else if bindings.quit.check(key) || CTRL_C.check(key) { return Ok(ExitAction::Normal); } else if bindings.quit_preserve_screen.check(key) { return Ok(ExitAction::PreserveScreen); } } } } } } ================================================ FILE: crates/trippy-tui/src/geoip.rs ================================================ use anyhow::Context; use itertools::Itertools; use maxminddb::Reader; use std::cell::RefCell; use std::collections::HashMap; use std::net::IpAddr; use std::path::Path; use std::rc::Rc; use std::str::FromStr; #[derive(Debug, Clone, Default)] pub struct GeoIpCity { latitude: Option, longitude: Option, accuracy_radius: Option, city: Option, subdivision: Option, subdivision_code: Option, country: Option, country_code: Option, continent: Option, } impl GeoIpCity { pub fn short_name(&self) -> String { [ self.city.as_ref(), self.subdivision_code.as_ref(), self.country_code.as_ref(), ] .into_iter() .flatten() .join(", ") } pub fn long_name(&self) -> String { [ self.city.as_ref(), self.subdivision.as_ref(), self.country.as_ref(), self.continent.as_ref(), ] .into_iter() .flatten() .join(", ") } pub fn location(&self) -> String { format!( "{}, {} (~{}km)", self.latitude.unwrap_or_default(), self.longitude.unwrap_or_default(), self.accuracy_radius.unwrap_or_default(), ) } pub const fn coordinates(&self) -> Option<(f64, f64, u16)> { match (self.latitude, self.longitude, self.accuracy_radius) { (Some(lat), Some(long), Some(radius)) => Some((lat, long, radius)), _ => None, } } } mod ipinfo { use serde::{Deserialize, Serialize}; use serde_with::serde_as; /// The `IPinfo` mmdb database format. /// /// Support both the "IP to Geolocation Extended" and "IP to Country + ASN" database formats. /// /// IP to Geolocation Extended Database: /// See /// /// IP to Country + ASN Database; /// See #[serde_as] #[derive(Debug, Serialize, Deserialize)] pub struct IpInfoGeoIp { /// "42.48948" #[serde(default)] #[serde_as(as = "serde_with::NoneAsEmptyString")] pub latitude: Option, /// "-83.14465" #[serde(default)] #[serde_as(as = "serde_with::NoneAsEmptyString")] pub longitude: Option, /// "500" #[serde(default)] #[serde_as(as = "serde_with::NoneAsEmptyString")] pub radius: Option, /// "Royal Oak" #[serde(default)] #[serde_as(as = "serde_with::NoneAsEmptyString")] pub city: Option, /// "Michigan" #[serde(default)] #[serde_as(as = "serde_with::NoneAsEmptyString")] pub region: Option, /// "48067" #[serde(default)] #[serde_as(as = "serde_with::NoneAsEmptyString")] pub postal_code: Option, /// "US" #[serde(default)] #[serde_as(as = "serde_with::NoneAsEmptyString")] pub country: Option, /// "Japan" #[serde(default)] #[serde_as(as = "serde_with::NoneAsEmptyString")] pub country_name: Option, /// "Asia" #[serde(default)] #[serde_as(as = "serde_with::NoneAsEmptyString")] pub continent_name: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn test_empty() { let json = "{}"; let value: IpInfoGeoIp = serde_json::from_str(json).unwrap(); assert_eq!(None, value.latitude); assert_eq!(None, value.longitude); assert_eq!(None, value.radius); assert_eq!(None, value.city); assert_eq!(None, value.region); assert_eq!(None, value.postal_code); assert_eq!(None, value.country.as_deref()); assert_eq!(None, value.country_name.as_deref()); assert_eq!(None, value.continent_name.as_deref()); } #[test] fn test_country_asn_db_format() { let json = r#" { "start_ip": "40.96.54.192", "end_ip": "40.96.54.255", "country": "JP", "country_name": "Japan", "continent": "AS", "continent_name": "Asia", "asn": "AS8075", "as_name": "Microsoft Corporation", "as_domain": "microsoft.com" } "#; let value: IpInfoGeoIp = serde_json::from_str(json).unwrap(); assert_eq!(None, value.latitude); assert_eq!(None, value.longitude); assert_eq!(None, value.radius); assert_eq!(None, value.city); assert_eq!(None, value.region); assert_eq!(None, value.postal_code); assert_eq!(Some("JP"), value.country.as_deref()); assert_eq!(Some("Japan"), value.country_name.as_deref()); assert_eq!(Some("Asia"), value.continent_name.as_deref()); } #[test] fn test_extended_db_format() { let json = r#" { "start_ip": "60.127.10.249", "end_ip": "60.127.10.249", "join_key": "60.127.0.0", "city": "Yokohama", "region": "Kanagawa", "country": "JP", "latitude": "35.43333", "longitude": "139.65", "postal_code": "220-8588", "timezone": "Asia/Tokyo", "geoname_id": "1848354", "radius": "500" } "#; let value: IpInfoGeoIp = serde_json::from_str(json).unwrap(); assert_eq!(Some("35.43333"), value.latitude.as_deref()); assert_eq!(Some("139.65"), value.longitude.as_deref()); assert_eq!(Some("500"), value.radius.as_deref()); assert_eq!(Some("Yokohama"), value.city.as_deref()); assert_eq!(Some("Kanagawa"), value.region.as_deref()); assert_eq!(Some("220-8588"), value.postal_code.as_deref()); assert_eq!(Some("JP"), value.country.as_deref()); assert_eq!(None, value.country_name.as_deref()); assert_eq!(None, value.continent_name.as_deref()); } } } impl From for GeoIpCity { fn from(value: ipinfo::IpInfoGeoIp) -> Self { Self { latitude: value.latitude.and_then(|val| f64::from_str(&val).ok()), longitude: value.longitude.and_then(|val| f64::from_str(&val).ok()), accuracy_radius: value.radius.and_then(|val| u16::from_str(&val).ok()), city: value.city, subdivision: value.region, subdivision_code: value.postal_code, country: value.country_name, country_code: value.country, continent: value.continent_name, } } } impl From<(maxminddb::geoip2::City<'_>, &str)> for GeoIpCity { fn from((value, locale): (maxminddb::geoip2::City<'_>, &str)) -> Self { let city = localized_name(&value.city.names, locale); let subdivision = value .subdivisions .first() .and_then(|c| localized_name(&c.names, locale)); let subdivision_code = value .subdivisions .first() .and_then(|c| c.iso_code.as_ref().map(ToString::to_string)); let country = localized_name(&value.country.names, locale); let country_code = value.country.iso_code.map(ToString::to_string); let continent = localized_name(&value.continent.names, locale); let latitude = value.location.latitude; let longitude = value.location.longitude; let accuracy_radius = value.location.accuracy_radius; Self { latitude, longitude, accuracy_radius, city, subdivision, subdivision_code, country, country_code, continent, } } } /// The fallback locale. /// /// The `MaxMind` support documentation says: /// /// > Our geolocation name data includes the names of the continent, country, city, and /// > subdivisions of the location of the IP address. We include the country names in /// > English, Simplified Chinese, Spanish, Brazilian Portuguese, Russian, Japanese, French, /// > and German. /// > /// > Please note: Not every place name is always available in each language. We recommend checking /// > English names as a default for cases where a localized name is not available in your preferred /// > language. const FALLBACK_LOCALE: &str = "en"; /// Alias for a cache of `GeoIp` data. type Cache = RefCell>>>; /// Lookup `GeoIpCity` data form an `IpAddr`. #[derive(Debug)] pub struct GeoIpLookup { reader: Option>>, cache: Cache, locale: String, } impl GeoIpLookup { /// Create a new `GeoIpLookup` from a `MaxMind` DB file. pub fn from_file>(path: P, locale: String) -> anyhow::Result { let reader = maxminddb::Reader::open_readfile(path.as_ref()) .context(format!("{}", path.as_ref().display()))?; Ok(Self { reader: Some(reader), cache: RefCell::new(HashMap::new()), locale, }) } /// Create a `GeoIpLookup` that returns `None` for all `IpAddr` lookups. pub fn empty() -> Self { Self { reader: None, cache: RefCell::new(HashMap::new()), locale: FALLBACK_LOCALE.to_string(), } } /// Lookup an `GeoIpCity` for an `IpAddr`. /// /// If an entry is found it is cached and returned, otherwise None is returned. pub fn lookup(&self, addr: IpAddr) -> anyhow::Result>> { if let Some(reader) = &self.reader { if let Some(geo) = self.cache.borrow().get(&addr) { return Ok(geo.clone()); } let lookup_result = reader.lookup(addr)?; let city_data = if reader.metadata.database_type.starts_with("ipinfo") { lookup_result .decode::()? .map(GeoIpCity::from) } else { lookup_result .decode::>()? .map(|city| GeoIpCity::from((city, self.locale.as_ref()))) }; let cached = city_data.map(Rc::new); self.cache.borrow_mut().insert(addr, cached.clone()); Ok(cached) } else { Ok(None) } } } fn localized_name(names: &maxminddb::geoip2::Names<'_>, locale: &str) -> Option { lookup_locale(names, locale) .or_else(|| lookup_locale(names, FALLBACK_LOCALE)) .map(ToString::to_string) } /// Map a Trippy locale code to the closest `maxminddb` locale field. /// /// - `pt*` (e.g. `pt`, `pt-BR`, `pt-PT`) use `brazilian_portuguese` /// - `zh*` (e.g. `zh`, `zh-TW`) use `simplified_chinese` /// - Other languages that are supported map directly (`en`, `de`, `es`, `fr`, `ja`, `ru`). fn lookup_locale<'a>(names: &maxminddb::geoip2::Names<'a>, code: &str) -> Option<&'a str> { if code.starts_with("pt") { names.brazilian_portuguese } else if code.starts_with("zh") { names.simplified_chinese } else { match code { "de" => names.german, "en" => names.english, "es" => names.spanish, "fr" => names.french, "ja" => names.japanese, "ru" => names.russian, _ => None, } } } ================================================ FILE: crates/trippy-tui/src/lib.rs ================================================ #![allow( clippy::struct_excessive_bools, clippy::cast_sign_loss, clippy::struct_field_names )] #![forbid(unsafe_code)] use crate::config::TrippyAction; use clap::Parser; use config::Args; use std::process; use trippy_privilege::Privilege; mod app; mod config; mod frontend; mod geoip; mod locale; mod print; mod report; mod util; /// Run the Trippy application. pub fn trippy() -> anyhow::Result<()> { let args = Args::parse(); let privilege = Privilege::acquire_privileges()?; let pid = u16::try_from(process::id() % u32::from(u16::MAX))?; match TrippyAction::from(args, &privilege, pid)? { TrippyAction::Trippy(cfg) => app::run_trippy(&cfg, pid)?, TrippyAction::PrintTuiThemeItems => print::print_tui_theme_items(), TrippyAction::PrintTuiBindingCommands => print::print_tui_binding_commands(), TrippyAction::PrintConfigTemplate => print::print_config_template(), TrippyAction::PrintManPage => print::print_man_page()?, TrippyAction::PrintShellCompletions(shell) => print::print_shell_completions(shell)?, TrippyAction::PrintLocales => print::print_locales(), } Ok(()) } ================================================ FILE: crates/trippy-tui/src/locale.rs ================================================ use itertools::Itertools; use serde::Deserialize; use std::cell::RefCell; use std::collections::HashMap; use std::str::FromStr; use std::sync::OnceLock; use unic_langid::LanguageIdentifier; const FALLBACK_LOCALE: &str = "en"; /// Set the locale for the application. /// /// If the given locale is `None` the system locale is tried. If the system locale cannot be /// determined then the fallback locale is used. /// /// In all cases, the language part of the locale is used if the full locale is not supported. pub fn set_locale(locale: Option<&str>) -> String { let new_locale = calculate_locale(locale, sys_locale::get_locale().as_deref()); store_locale(&new_locale); new_locale } /// Get the available locales. pub fn available_locales() -> Vec<&'static str> { data() .0 .iter() .flat_map(|(_, v)| v.0.keys().map(AsRef::as_ref)) .unique() .sorted_unstable() .collect::>() } /// A macro to translate an item to the current locale. #[macro_export] macro_rules! t { ($key:expr) => { std::borrow::Cow::Borrowed($crate::locale::__translate($key)) }; ($key:expr, $($kt:ident = $kv:expr),+) => { { let string = t!($key); $( let string = string.replace(concat!("%{", stringify!($kt), "}"), &$kv.to_string()); )+ string } }; ($key:expr, $($kt:literal => $kv:expr),+) => { { let string = t!($key); $( let string = string.replace(concat!("%{", $kt, "}"), &$kv.to_string()); )+ string } }; } /// Translate an item to the current locale. /// /// This function is public as it is used by the `t!` macro, however is not considered part of the /// public interface. #[doc(hidden)] pub fn __translate(item: &str) -> &str { let locale = CURRENT_LOCALE.with(Clone::clone); let binding = locale.borrow(); translate_locale(item, binding.as_str()) } /// Translate an item to a specific locale. /// /// If the item does not exists, the key is returned. Otherwise, if item does not contain the /// locale, the fallback locale is used. If the fallback locale does not exist, the key is /// returned. fn translate_locale<'a>(item: &'a str, locale: &str) -> &'a str { if let Some(key) = data().0.get(item) { if let Some(value) = key.0.get(locale) { value } else if let Some(value) = key.0.get(FALLBACK_LOCALE) { value } else { item } } else { item } } /// Get the locale data. fn data() -> &'static Data { static DATA: OnceLock = OnceLock::new(); DATA.get_or_init(|| { toml::from_str(include_str!("../locales.toml")).expect("Failed to parse locales.toml") }) } /// This is a map of a item name (i.e. `title_hops`, `awaiting_data`, etc.) to the locale `Item`. #[derive(Debug, Deserialize)] struct Data(HashMap); /// This is a map of locale keys (i.e. `en`, `zh`, etc.) to the translated value. #[derive(Debug, Deserialize)] struct Item(HashMap); /// calculate the locale to use. fn calculate_locale(cfg_locale: Option<&str>, sys_locale: Option<&str>) -> String { let preferred = cfg_locale.or(sys_locale).unwrap_or(FALLBACK_LOCALE); let locales = available_locales(); locales .contains(&preferred) .then(|| preferred.to_string()) .or_else(|| { LanguageIdentifier::from_str(preferred).ok().and_then(|id| { let lang = id.language.to_string(); id.region .map(|r| format!("{lang}-{r}")) .filter(|s| locales.contains(&s.as_str())) .or_else(|| locales.contains(&lang.as_str()).then_some(lang)) }) }) .unwrap_or_else(|| FALLBACK_LOCALE.to_string()) } thread_local! { static CURRENT_LOCALE: RefCell = RefCell::new(String::from(FALLBACK_LOCALE)); } fn store_locale(new_locale: &str) { CURRENT_LOCALE.with(|locale| *locale.borrow_mut() = String::from(new_locale)); } #[cfg(test)] mod tests { use super::*; use test_case::test_case; #[test_case(None, None, "en"; "no_locale")] #[test_case(Some("en"), None, "en"; "cfg_locale")] #[test_case(None, Some("en"), "en"; "sys_locale")] #[test_case(Some("en"), Some("en"), "en"; "both_locales")] #[test_case(Some("en"), Some("zh"), "en"; "both_locales_mismatch")] #[test_case(Some("zh"), Some("en"), "zh"; "both_locales_mismatch_reverse")] #[test_case(Some("en-US"), None, "en"; "cfg_locale_dash")] #[test_case(None, Some("en-US"), "en"; "sys_locale_dash")] #[test_case(Some("en-US"), Some("en-US"), "en"; "both_locales_dash")] #[test_case(Some("en-US"), Some("zh-CN"), "en"; "both_locales_mismatch_dash")] #[test_case(Some("zh-CN"), Some("en-US"), "zh"; "both_locales_mismatch_reverse_dash")] #[test_case(Some("en_US"), None, "en"; "cfg_locale_underscore")] #[test_case(None, Some("en_US"), "en"; "sys_locale_underscore")] #[test_case(Some("xx"), None, "en"; "cfg_locale_unknown")] #[test_case(None, Some("xx"), "en"; "sys_locale_unknown")] #[test_case(Some("xx"), Some("xx"), "en"; "both_locales_unknown")] #[test_case(Some("en-"), None, "en"; "cfg_locale_invalid_dash")] #[test_case(Some("en_"), None, "en"; "cfg_locale_invalid_underscore")] #[test_case(Some("en?"), None, "en"; "cfg_locale_invalid_accepted")] #[test_case(Some("zh-Hant-TW"), None, "zh-TW"; "cfg_locale_ignore_script")] #[test_case(None, Some("zh-Hant-TW"), "zh-TW"; "sys_locale_ignore_script")] #[test_case(Some("zh-Hant-TW"), Some("zh-Hant-TW"), "zh-TW"; "both_locales_ignore_script")] fn test_set_locale(cfg_locale: Option<&str>, sys_locale: Option<&str>, expected: &str) { assert_eq!(calculate_locale(cfg_locale, sys_locale), expected); } #[test] fn test_available_languages() { assert_eq!( available_locales(), vec![ "de", "en", "es", "fr", "it", "pt", "ru", "sv", "tr", "zh", "zh-TW" ] ); } #[test] fn test_data_deserialize() { assert!(!data().0.is_empty()); } #[test] fn test_translate() { assert_eq!(translate_locale("title_hops", "en"), "Hops"); assert_eq!(translate_locale("title_hops", "zh"), "跳"); assert_eq!(translate_locale("title_hops", "zh-TW"), "跳"); assert_eq!(translate_locale("unknown_item", "en"), "unknown_item"); assert_eq!(translate_locale("unknown_locale", "xx"), "unknown_locale"); } #[test] fn test_translate_macro() { assert_eq!(t!("title_hops"), "Hops"); assert_eq!(t!("awaiting_data"), "Awaiting data..."); assert_eq!(t!("unknown_item"), "unknown_item"); } #[test] fn test_zh_tw_translations() { // Test key Traditional Chinese translations assert_eq!(translate_locale("auto", "zh-TW"), "自動"); assert_eq!(translate_locale("status_failed", "zh-TW"), "失敗"); assert_eq!(translate_locale("status_running", "zh-TW"), "執行中"); assert_eq!(translate_locale("title_settings", "zh-TW"), "設定"); assert_eq!(translate_locale("help_tagline", "zh-TW"), "網路診斷工具"); assert_eq!(translate_locale("column_loss_pct", "zh-TW"), "封包遺失率"); assert_eq!(translate_locale("rtt", "zh-TW"), "往返時間"); } } ================================================ FILE: crates/trippy-tui/src/print.rs ================================================ use crate::config::{Args, TuiCommandItem, TuiThemeItem}; use crate::locale::available_locales; use clap::CommandFactory; use clap_complete::Shell; use std::process; use strum::VariantNames; pub fn print_tui_theme_items() { println!("{}", tui_theme_items()); process::exit(0); } pub fn print_tui_binding_commands() { println!("{}", tui_binding_commands()); process::exit(0); } pub fn print_config_template() { println!("{}", include_str!("../trippy-config-sample.toml")); process::exit(0); } pub fn print_shell_completions(shell: Shell) -> anyhow::Result<()> { println!("{}", shell_completions(shell)?); process::exit(0); } pub fn print_man_page() -> anyhow::Result<()> { println!("{}", man_page()?); process::exit(0); } pub fn print_locales() { println!("TUI locales: {}", available_locales().join(", ")); process::exit(0); } fn tui_theme_items() -> String { format!( "TUI theme color items: {}", TuiThemeItem::VARIANTS.join(", ") ) } fn tui_binding_commands() -> String { format!( "TUI binding commands: {}", TuiCommandItem::VARIANTS.join(", ") ) } fn shell_completions(shell: Shell) -> anyhow::Result { let mut cmd = Args::command(); let name = cmd.get_name().to_string(); let mut buffer: Vec = vec![]; clap_complete::generate(shell, &mut cmd, name, &mut buffer); Ok(String::from_utf8(buffer)?) } fn man_page() -> anyhow::Result { let cmd = Args::command(); let mut buffer: Vec = vec![]; clap_mangen::Man::new(cmd).render(&mut buffer)?; Ok(String::from_utf8(buffer)?) } #[cfg(test)] pub mod tests { use super::*; use crate::util::{insta, remove_whitespace}; use test_case::test_case; #[test_case(&tui_theme_items(), "tui theme items match"; "tui theme items match")] #[test_case(&tui_binding_commands(), "tui binding commands match"; "tui binding commands match")] #[test_case(&shell_completions(Shell::Bash).unwrap(), "generate bash shell completions"; "generate bash shell completions")] #[test_case(&shell_completions(Shell::Elvish).unwrap(), "generate elvish shell completions"; "generate elvish shell completions")] #[test_case(&shell_completions(Shell::Fish).unwrap(), "generate fish shell completions"; "generate fish shell completions")] #[test_case(&shell_completions(Shell::PowerShell).unwrap(), "generate powershell shell completions"; "generate powershell shell completions")] #[test_case(&shell_completions(Shell::Zsh).unwrap(), "generate zsh shell completions"; "generate zsh shell completions")] #[test_case(&man_page().unwrap(), "generate man page"; "generate man page")] fn test_output(actual: &str, name: &str) { insta(name, || { insta::assert_snapshot!(remove_whitespace(actual.to_string())); }); } } ================================================ FILE: crates/trippy-tui/src/report/csv.rs ================================================ use crate::app::TraceInfo; use crate::report::types::fixed_width; use itertools::Itertools; use serde::Serialize; use std::net::IpAddr; use tracing::instrument; use trippy_dns::Resolver; /// Generate a CSV report of trace data. #[instrument(skip_all, level = "trace")] pub fn report( info: &TraceInfo, report_cycles: usize, resolver: &R, ) -> anyhow::Result<()> { let trace = super::wait_for_round(&info.data, report_cycles)?; let mut writer = csv::Writer::from_writer(std::io::stdout()); for hop in trace.hops() { let row = CsvRow::new( &info.target_hostname, info.data.target_addr(), hop, resolver, ); writer.serialize(row)?; } Ok(()) } #[derive(Serialize)] pub struct CsvRow { #[serde(rename = "Target")] pub target_hostname: String, #[serde(rename = "TargetIp")] pub target_addr: IpAddr, #[serde(rename = "Hop")] pub ttl: u8, #[serde(rename = "IPs")] pub ip: String, #[serde(rename = "Addrs")] pub host: String, #[serde(rename = "Loss%")] #[serde(serialize_with = "fixed_width")] pub loss_pct: f64, #[serde(rename = "Snt")] pub sent: usize, #[serde(rename = "Recv")] pub recv: usize, #[serde(rename = "Last")] pub last: String, #[serde(rename = "Avg")] #[serde(serialize_with = "fixed_width")] pub avg: f64, #[serde(rename = "Best")] pub best: String, #[serde(rename = "Wrst")] pub worst: String, #[serde(rename = "StdDev")] #[serde(serialize_with = "fixed_width")] pub stddev: f64, } impl CsvRow { fn new( target: &str, target_addr: IpAddr, hop: &trippy_core::Hop, resolver: &R, ) -> Self { let ttl = hop.ttl(); let ips = hop.addrs().join(":"); let ip = if ips.is_empty() { String::from("???") } else { ips }; let hosts = hop.addrs().map(|ip| resolver.reverse_lookup(*ip)).join(":"); let host = if hosts.is_empty() { String::from("???") } else { hosts }; let sent = hop.total_sent(); let recv = hop.total_recv(); let last = hop .last_ms() .map_or_else(|| String::from("???"), |last| format!("{last:.1}")); let best = hop .best_ms() .map_or_else(|| String::from("???"), |best| format!("{best:.1}")); let worst = hop .worst_ms() .map_or_else(|| String::from("???"), |worst| format!("{worst:.1}")); let stddev = hop.stddev_ms(); let avg = hop.avg_ms(); let loss_pct = hop.loss_pct(); Self { target_hostname: String::from(target), target_addr, ttl, ip, host, loss_pct, sent, last, recv, avg, best, worst, stddev, } } } ================================================ FILE: crates/trippy-tui/src/report/dot.rs ================================================ use crate::app::TraceInfo; use petgraph::dot::{Config, Dot}; use petgraph::graphmap::DiGraphMap; use std::fmt::{Debug, Formatter}; use std::net::{IpAddr, Ipv4Addr}; use tracing::instrument; use trippy_core::FlowEntry; /// Run a trace and generate a dot file. #[instrument(skip_all, level = "trace")] pub fn report(info: &TraceInfo, report_cycles: usize) -> anyhow::Result<()> { struct DotWrapper<'a>(Dot<'a, &'a DiGraphMap>); impl Debug for DotWrapper<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } super::wait_for_round(&info.data, report_cycles)?; let trace = info.data.snapshot(); let mut graph: DiGraphMap = DiGraphMap::new(); for (flow, _id) in trace.flows() { for (fst, snd) in flow.entries.windows(2).map(|pair| (pair[0], pair[1])) { match (fst, snd) { (FlowEntry::Known(addr1), FlowEntry::Known(addr2)) => { graph.add_edge(addr1, addr2, ()); } (FlowEntry::Known(addr1), FlowEntry::Unknown) => { graph.add_edge(addr1, IpAddr::V4(Ipv4Addr::UNSPECIFIED), ()); } (FlowEntry::Unknown, FlowEntry::Known(addr2)) => { graph.add_edge(IpAddr::V4(Ipv4Addr::UNSPECIFIED), addr2, ()); } _ => {} } } } let dot = DotWrapper(Dot::with_config(&graph, &[Config::EdgeNoLabel])); print!("{dot:?}"); Ok(()) } ================================================ FILE: crates/trippy-tui/src/report/flows.rs ================================================ use crate::app::TraceInfo; use tracing::instrument; /// Run a trace and report all flows observed. #[instrument(skip_all, level = "trace")] pub fn report(info: &TraceInfo, report_cycles: usize) -> anyhow::Result<()> { super::wait_for_round(&info.data, report_cycles)?; let trace = info.data.snapshot(); for (flow, flow_id) in trace.flows() { println!("flow {flow_id}: {flow}"); } Ok(()) } ================================================ FILE: crates/trippy-tui/src/report/json.rs ================================================ use crate::app::TraceInfo; use crate::report::types::{Hop, Host, Info, Report}; use tracing::instrument; use trippy_dns::Resolver; /// Generate a json report of trace data. #[instrument(skip_all, level = "trace")] pub fn report( info: &TraceInfo, report_cycles: usize, resolver: &R, ) -> anyhow::Result<()> { let start_timestamp = chrono::Utc::now(); let trace = super::wait_for_round(&info.data, report_cycles)?; let end_timestamp = chrono::Utc::now(); let hops: Vec = trace .hops() .iter() .map(|hop| Hop::from((hop, resolver))) .collect(); let report = Report { info: Info { target: Host { ip: info.data.target_addr(), hostname: info.target_hostname.clone(), }, start_timestamp, end_timestamp, }, hops, }; serde_json::to_writer_pretty(std::io::stdout(), &report)?; Ok(()) } ================================================ FILE: crates/trippy-tui/src/report/silent.rs ================================================ use crate::app::TraceInfo; use tracing::instrument; /// Run a trace without generating any output. #[instrument(skip_all, level = "trace")] pub fn report(info: &TraceInfo, report_cycles: usize) -> anyhow::Result<()> { super::wait_for_round(&info.data, report_cycles)?; Ok(()) } ================================================ FILE: crates/trippy-tui/src/report/stream.rs ================================================ use crate::app::TraceInfo; use crate::report::types::Hop; use anyhow::anyhow; use std::thread::sleep; use tracing::instrument; use trippy_dns::Resolver; /// Display a continuous stream of trace data. #[instrument(skip_all, level = "trace")] pub fn report(info: &TraceInfo, resolver: &R) -> anyhow::Result<()> { println!( "Tracing to {} ({})", info.target_hostname, info.data.target_addr() ); loop { let trace_data = &info.data.snapshot(); if let Some(err) = trace_data.error() { return Err(anyhow!("error: {err}")); } for hop in trace_data.hops() { let hop = Hop::from((hop, resolver)); let ttl = hop.ttl; let addrs = hop.hosts.to_string(); let exts = hop.extensions.to_string(); let sent = hop.sent; let recv = hop.recv; let last = hop.last; let best = hop.best; let worst = hop.worst; let stddev = hop.stddev; let avg = hop.avg; let loss_pct = hop.loss_pct; println!( "ttl={ttl} addrs={addrs} exts={exts} loss_pct={loss_pct:.1} sent={sent} recv={recv} last={last:.1} best={best:.1} worst={worst:.1} avg={avg:.1} stddev={stddev:.1}" ); } sleep(info.data.min_round_duration()); } } ================================================ FILE: crates/trippy-tui/src/report/table.rs ================================================ use crate::app::TraceInfo; use comfy_table::presets::{ASCII_MARKDOWN, UTF8_FULL}; use comfy_table::{ContentArrangement, Table}; use itertools::Itertools; use tracing::instrument; use trippy_dns::Resolver; /// Generate a Markdown table report of trace data. #[instrument(skip_all, level = "trace")] pub fn report_md( info: &TraceInfo, report_cycles: usize, resolver: &R, ) -> anyhow::Result<()> { run_report_table(info, report_cycles, resolver, ASCII_MARKDOWN) } /// Generate a pretty table report of trace data. #[instrument(skip_all, level = "trace")] pub fn report_pretty( info: &TraceInfo, report_cycles: usize, resolver: &R, ) -> anyhow::Result<()> { run_report_table(info, report_cycles, resolver, UTF8_FULL) } fn run_report_table( info: &TraceInfo, report_cycles: usize, resolver: &R, preset: &str, ) -> anyhow::Result<()> { let trace = super::wait_for_round(&info.data, report_cycles)?; let columns = vec![ "Hop", "IPs", "Addrs", "Loss%", "Snt", "Recv", "Last", "Avg", "Best", "Wrst", "StdDev", ]; let mut table = Table::new(); table .load_preset(preset) .set_content_arrangement(ContentArrangement::Dynamic) .set_header(columns); for hop in trace.hops() { let ttl = hop.ttl().to_string(); let ips = hop.addrs().join("\n"); let ip = if ips.is_empty() { String::from("???") } else { ips }; let hosts = hop .addrs() .map(|ip| resolver.reverse_lookup(*ip).to_string()) .join("\n"); let host = if hosts.is_empty() { String::from("???") } else { hosts }; let sent = hop.total_sent().to_string(); let recv = hop.total_recv().to_string(); let last = hop .last_ms() .map_or_else(|| String::from("???"), |last| format!("{last:.1}")); let best = hop .best_ms() .map_or_else(|| String::from("???"), |best| format!("{best:.1}")); let worst = hop .worst_ms() .map_or_else(|| String::from("???"), |worst| format!("{worst:.1}")); let stddev = format!("{:.1}", hop.stddev_ms()); let avg = format!("{:.1}", hop.avg_ms()); let loss_pct = format!("{:.1}", hop.loss_pct()); table.add_row(vec![ &ttl, &ip, &host, &loss_pct, &sent, &recv, &last, &avg, &best, &worst, &stddev, ]); } println!("{table}"); Ok(()) } ================================================ FILE: crates/trippy-tui/src/report/types.rs ================================================ use chrono::Utc; use itertools::Itertools; use serde::{Serialize, Serializer}; use std::fmt::{Display, Formatter}; use std::net::IpAddr; use trippy_core::NatStatus; use trippy_dns::Resolver; #[derive(Serialize)] pub struct Report { pub info: Info, pub hops: Vec, } #[derive(Serialize)] pub struct Info { pub target: Host, pub start_timestamp: chrono::DateTime, pub end_timestamp: chrono::DateTime, } #[derive(Serialize)] pub struct Hop { pub ttl: u8, pub hosts: Hosts, pub extensions: Extensions, #[serde(serialize_with = "fixed_width")] pub loss_pct: f64, pub sent: usize, #[serde(serialize_with = "fixed_width")] pub last: f64, pub recv: usize, #[serde(serialize_with = "fixed_width")] pub avg: f64, #[serde(serialize_with = "fixed_width")] pub best: f64, #[serde(serialize_with = "fixed_width")] pub worst: f64, #[serde(serialize_with = "fixed_width")] pub stddev: f64, #[serde(serialize_with = "fixed_width")] pub jitter: f64, #[serde(serialize_with = "fixed_width")] pub javg: f64, #[serde(serialize_with = "fixed_width")] pub jmax: f64, #[serde(serialize_with = "fixed_width")] pub jinta: f64, pub nat: Option, pub tos: u8, } impl From<(&trippy_core::Hop, &R)> for Hop { fn from((value, resolver): (&trippy_core::Hop, &R)) -> Self { let hosts = Hosts::from((value.addrs(), resolver)); let extensions = value.extensions().map(Extensions::from).unwrap_or_default(); Self { ttl: value.ttl(), hosts, extensions, loss_pct: value.loss_pct(), sent: value.total_sent(), last: value.last_ms().unwrap_or_default(), recv: value.total_recv(), avg: value.avg_ms(), best: value.best_ms().unwrap_or_default(), worst: value.worst_ms().unwrap_or_default(), stddev: value.stddev_ms(), jitter: value.jitter_ms().unwrap_or_default(), javg: value.javg_ms(), jmax: value.jmax_ms().unwrap_or_default(), jinta: value.jinta(), nat: match value.last_nat_status() { NatStatus::NotApplicable => None, NatStatus::NotDetected => Some(false), NatStatus::Detected => Some(true), }, tos: value.tos().unwrap_or_default().0, } } } #[derive(Serialize)] pub struct Hosts(pub Vec); impl<'a, R: Resolver, I: Iterator> From<(I, &R)> for Hosts { fn from((value, resolver): (I, &R)) -> Self { Self( value .map(|ip| Host { ip: *ip, hostname: resolver.reverse_lookup(*ip).to_string(), }) .collect(), ) } } impl Display for Hosts { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0.iter().format(", ")) } } #[derive(Serialize)] pub struct Host { pub ip: IpAddr, pub hostname: String, } impl Display for Host { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.ip) } } #[derive(Default, Serialize)] #[serde(transparent)] pub struct Extensions { pub extensions: Vec, } impl From<&trippy_core::Extensions> for Extensions { fn from(value: &trippy_core::Extensions) -> Self { Self { extensions: value .extensions .iter() .cloned() .map(Extension::from) .collect(), } } } impl Display for Extensions { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.extensions.iter().format(" + ")) } } #[derive(Serialize)] pub enum Extension { #[serde(rename = "unknown")] Unknown(UnknownExtension), #[serde(rename = "mpls")] Mpls(MplsLabelStack), } impl From for Extension { fn from(value: trippy_core::Extension) -> Self { match value { trippy_core::Extension::Unknown(unknown) => { Self::Unknown(UnknownExtension::from(unknown)) } trippy_core::Extension::Mpls(mpls) => Self::Mpls(MplsLabelStack::from(mpls)), } } } impl Display for Extension { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::Unknown(unknown) => unknown.fmt(f), Self::Mpls(mpls) => mpls.fmt(f), } } } #[derive(Serialize)] pub struct MplsLabelStack { pub members: Vec, } impl From for MplsLabelStack { fn from(value: trippy_core::MplsLabelStack) -> Self { Self { members: value .members .into_iter() .map(MplsLabelStackMember::from) .collect(), } } } impl Display for MplsLabelStack { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "mpls(labels={})", self.members.iter().format(", ")) } } #[derive(Serialize)] pub struct MplsLabelStackMember { pub label: u32, pub exp: u8, pub bos: u8, pub ttl: u8, } impl From for MplsLabelStackMember { fn from(value: trippy_core::MplsLabelStackMember) -> Self { Self { label: value.label, exp: value.exp, bos: value.bos, ttl: value.ttl, } } } impl Display for MplsLabelStackMember { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.label) } } #[derive(Serialize)] pub struct UnknownExtension { pub class_num: u8, pub class_subtype: u8, pub bytes: Vec, } impl From for UnknownExtension { fn from(value: trippy_core::UnknownExtension) -> Self { Self { class_num: value.class_num, class_subtype: value.class_subtype, bytes: value.bytes, } } } impl Display for UnknownExtension { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "unknown(class={}, subtype={}, bytes=[{:02x}])", self.class_num, self.class_subtype, self.bytes.iter().format(" ") ) } } #[expect(clippy::trivially_copy_pass_by_ref)] pub fn fixed_width(val: &f64, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&format!("{val:.2}")) } ================================================ FILE: crates/trippy-tui/src/report.rs ================================================ use anyhow::anyhow; use trippy_core::State; use trippy_core::Tracer; pub mod csv; pub mod dot; pub mod flows; pub mod json; pub mod silent; pub mod stream; pub mod table; mod types; /// Block until trace data for round `round` is available. fn wait_for_round(trace_data: &Tracer, report_cycles: usize) -> anyhow::Result { let mut trace = trace_data.snapshot(); while trace.round(State::default_flow_id()).is_none() || trace.round(State::default_flow_id()) < Some(report_cycles - 1) { trace = trace_data.snapshot(); if let Some(err) = trace.error() { return Err(anyhow!("error: {err}")); } } Ok(trace) } ================================================ FILE: crates/trippy-tui/src/util.rs ================================================ #[cfg(test)] pub fn insta(name: &str, f: F) { let mut settings = insta::Settings::new(); settings.set_snapshot_suffix(name.replace(' ', "_")); settings.set_snapshot_path("../tests/resources/snapshots"); settings.set_omit_expression(true); settings.bind(f); } #[cfg(test)] pub fn remove_whitespace(mut s: String) -> String { s.retain(|c| !c.is_whitespace()); s } ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__config__tests__compare_snapshot@trip.snap ================================================ --- source: crates/trippy-tui/src/config.rs --- AnetworkdiagnostictoolUsage:trip[OPTIONS][TARGETS]...Arguments:[TARGETS]...AspacedelimitedlistofhostnamesandIPstotrace[env:TRIP_TARGETS=]Options:-c,--config-fileConfigfile[env:TRIP_CONFIG_FILE=]-m,--modeOutputmode[default:tui][env:TRIP_MODE=][possiblevalues:tui,stream,pretty,markdown,csv,json,dot,flows,silent]-u,--unprivilegedTracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false][env:TRIP_UNPRIVILEGED=]-p,--protocolTracingprotocol[default:icmp][env:TRIP_PROTOCOL=][possiblevalues:icmp,udp,tcp]--udpTraceusingtheUDPprotocol[env:TRIP_UDP=]--tcpTraceusingtheTCPprotocol[env:TRIP_TCP=]--icmpTraceusingtheICMPprotocol[env:TRIP_ICMP=]-F,--addr-familyTheaddressfamily[default:system][env:TRIP_ADDR_FAMILY=][possiblevalues:ipv4,ipv6,ipv6-then-ipv4,ipv4-then-ipv6,system]-4,--ipv4UseIPv4only[env:TRIP_IPV4=]-6,--ipv6UseIPv6only[env:TRIP_IPV6=]-P,--target-portThetargetport(TCP&UDPonly)[default:80][env:TRIP_TARGET_PORT=]-S,--source-portThesourceport(TCP&UDPonly)[default:auto][env:TRIP_SOURCE_PORT=]-A,--source-addressThesourceIPaddress[default:auto][env:TRIP_SOURCE_ADDRESS=]-I,--interfaceThenetworkinterface[default:auto][env:TRIP_INTERFACE=]-i,--min-round-durationTheminimumdurationofeveryround[default:1s][env:TRIP_MIN_ROUND_DURATION=]-T,--max-round-durationThemaximumdurationofeveryround[default:1s][env:TRIP_MAX_ROUND_DURATION=]-g,--grace-durationTheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms][env:TRIP_GRACE_DURATION=]--initial-sequenceTheinitialsequencenumber[default:33434][env:TRIP_INITIAL_SEQUENCE=]-R,--multipath-strategyTheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic][env:TRIP_MULTIPATH_STRATEGY=][possiblevalues:classic,paris,dublin]-U,--max-inflightThemaximumnumberofin-flightICMPechorequests[default:24][env:TRIP_MAX_INFLIGHT=]-f,--first-ttlTheTTLtostartfrom[default:1][env:TRIP_FIRST_TTL=]-t,--max-ttlThemaximumnumberofTTLhops[default:64][env:TRIP_MAX_TTL=]--packet-sizeThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84][env:TRIP_PACKET_SIZE=]--payload-patternTherepeatingpatterninthepayloadoftheICMPpacket[default:0][env:TRIP_PAYLOAD_PATTERN=]-Q,--tosTheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0][env:TRIP_TOS=]-e,--icmp-extensionsParseICMPextensions[env:TRIP_ICMP_EXTENSIONS=]--read-timeoutThesocketreadtimeout[default:10ms][env:TRIP_READ_TIMEOUT=]-r,--dns-resolve-methodHowtoperformDNSqueries[default:system][env:TRIP_DNS_RESOLVE_METHOD=][possiblevalues:system,resolv,google,cloudflare]-y,--dns-resolve-allTracetoallIPsresolvedfromDNSlookup[default:false][env:TRIP_DNS_RESOLVE_ALL=]--dns-timeoutThemaximumtimetowaittoperformDNSqueries[default:5s][env:TRIP_DNS_TIMEOUT=]--dns-ttlThetime-to-live(TTL)ofDNSentries[default:300s][env:TRIP_DNS_TTL=]-z,--dns-lookup-as-infoLookupautonomoussystem(AS)informationduringDNSqueries[default:false][env:TRIP_DNS_LOOKUP_AS_INFO=]-s,--max-samplesThemaximumnumberofsamplestorecordperhop[default:256][env:TRIP_MAX_SAMPLES=]--max-flowsThemaximumnumberofflowstorecord[default:64][env:TRIP_MAX_FLOWS=]-a,--tui-address-modeHowtorenderaddresses[default:host][env:TRIP_TUI_ADDRESS_MODE=][possiblevalues:ip,host,both]--tui-as-modeHowtorenderautonomoussystem(AS)information[default:asn][env:TRIP_TUI_AS_MODE=][possiblevalues:asn,prefix,country-code,registry,allocated,name]--tui-custom-columnsCustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt][env:TRIP_TUI_CUSTOM_COLUMNS=]--tui-icmp-extension-modeHowtorenderICMPextensions[default:off][env:TRIP_TUI_ICMP_EXTENSION_MODE=][possiblevalues:off,mpls,full,all]--tui-geoip-modeHowtorenderGeoIpinformation[default:short][env:TRIP_TUI_GEOIP_MODE=][possiblevalues:off,short,long,location]-M,--tui-max-addrsThemaximumnumberofaddressestoshowperhop[default:auto][env:TRIP_TUI_MAX_ADDRS=]--tui-preserve-screenPreservethescreenonexit[default:false][env:TRIP_TUI_PRESERVE_SCREEN=]--tui-refresh-rateTheTUIrefreshrate[default:100ms][env:TRIP_TUI_REFRESH_RATE=]--tui-privacy-max-ttlThemaximumttlofhopswhichwillbemaskedforprivacy[default:none][env:TRIP_TUI_PRIVACY_MAX_TTL=]--tui-localeThelocaletousefortheTUI[default:auto][env:TRIP_TUI_LOCALE=]--tui-timezoneThetimezonetousefortheTUI[default:auto][env:TRIP_TUI_TIMEZONE=]--tui-theme-colorsTheTUIthemecolors[item=color,item=color,..][env:TRIP_TUI_THEME_COLORS=]--print-tui-theme-itemsPrintallTUIthemeitemsandexit[env:TRIP_PRINT_TUI_THEME_ITEMS=]--tui-key-bindingsTheTUIkeybindings[command=key,command=key,..][env:TRIP_TUI_KEY_BINDINGS=]--print-tui-binding-commandsPrintallTUIcommandsthatcanbeboundandexit[env:TRIP_PRINT_TUI_BINDING_COMMANDS=]-C,--report-cyclesThenumberofreportcyclestorun[default:10][env:TRIP_REPORT_CYCLES=]-G,--geoip-mmdb-fileThesupportedMaxMindorIPinfoGeoIpmmdbfile[env:TRIP_GEOIP_MMDB_FILE=]--generateGenerateshellcompletion[env:TRIP_GENERATE=][possiblevalues:bash,elvish,fish,powershell,zsh]--generate-manGenerateROFFmanpage[env:TRIP_GENERATE_MAN=]--print-config-templatePrintatemplatetomlconfigfileandexit[env:TRIP_PRINT_CONFIG_TEMPLATE=]--print-localesPrintallavailableTUIlocalesandexit[env:TRIP_PRINT_LOCALES=]--log-formatThedebuglogformat[default:pretty][env:TRIP_LOG_FORMAT=][possiblevalues:compact,pretty,json,chrome]--log-filterThedebuglogfilter[default:trippy=debug][env:TRIP_LOG_FILTER=]--log-span-eventsThedebuglogformat[default:off][env:TRIP_LOG_SPAN_EVENTS=][possiblevalues:off,active,full]-v,--verboseEnableverbosedebuglogging[env:TRIP_VERBOSE=]-h,--helpPrinthelp(seemorewith'--help')-V,--versionPrintversion ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__config__tests__compare_snapshot@trip_--help.snap ================================================ --- source: crates/trippy-tui/src/config.rs --- AnetworkdiagnostictoolUsage:trip[OPTIONS][TARGETS]...Arguments:[TARGETS]...AspacedelimitedlistofhostnamesandIPstotrace[env:TRIP_TARGETS=]Options:-c,--config-fileConfigfile[env:TRIP_CONFIG_FILE=]-m,--modeOutputmode[default:tui]Possiblevalues:-tui:DisplayinteractiveTUI-stream:Displayacontinuousstreamoftracingdata-pretty:GenerateaprettytexttablereportforNcycles-markdown:GenerateaMarkdowntexttablereportforNcycles-csv:GenerateaCSVreportforNcycles-json:GenerateaJSONreportforNcycles-dot:GenerateaGraphvizDOTfileforNcycles-flows:DisplayallflowsforNcycles-silent:DonotgenerateanytracingoutputforNcycles[env:TRIP_MODE=]-u,--unprivilegedTracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false][env:TRIP_UNPRIVILEGED=]-p,--protocolTracingprotocol[default:icmp]Possiblevalues:-icmp:InternetControlMessageProtocol-udp:UserDatagramProtocol-tcp:TransmissionControlProtocol[env:TRIP_PROTOCOL=]--udpTraceusingtheUDPprotocol[env:TRIP_UDP=]--tcpTraceusingtheTCPprotocol[env:TRIP_TCP=]--icmpTraceusingtheICMPprotocol[env:TRIP_ICMP=]-F,--addr-familyTheaddressfamily[default:system]Possiblevalues:-ipv4:IPv4only-ipv6:IPv6only-ipv6-then-ipv4:IPv6withafallbacktoIPv4-ipv4-then-ipv6:IPv4withafallbacktoIPv6-system:IftheOSresolverisbeingusedthenusethefirstIPaddressreturned,otherwiselookupIPv4withafallbacktoIPv6[env:TRIP_ADDR_FAMILY=]-4,--ipv4UseIPv4only[env:TRIP_IPV4=]-6,--ipv6UseIPv6only[env:TRIP_IPV6=]-P,--target-portThetargetport(TCP&UDPonly)[default:80][env:TRIP_TARGET_PORT=]-S,--source-portThesourceport(TCP&UDPonly)[default:auto][env:TRIP_SOURCE_PORT=]-A,--source-addressThesourceIPaddress[default:auto][env:TRIP_SOURCE_ADDRESS=]-I,--interfaceThenetworkinterface[default:auto][env:TRIP_INTERFACE=]-i,--min-round-durationTheminimumdurationofeveryround[default:1s][env:TRIP_MIN_ROUND_DURATION=]-T,--max-round-durationThemaximumdurationofeveryround[default:1s][env:TRIP_MAX_ROUND_DURATION=]-g,--grace-durationTheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms][env:TRIP_GRACE_DURATION=]--initial-sequenceTheinitialsequencenumber[default:33434][env:TRIP_INITIAL_SEQUENCE=]-R,--multipath-strategyTheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic]Possiblevalues:-classic:Thesrcordestportisusedtostorethesequencenumber-paris:TheUDP`checksum`fieldisusedtostorethesequencenumber-dublin:TheIP`identifier`fieldisusedtostorethesequencenumber[env:TRIP_MULTIPATH_STRATEGY=]-U,--max-inflightThemaximumnumberofin-flightICMPechorequests[default:24][env:TRIP_MAX_INFLIGHT=]-f,--first-ttlTheTTLtostartfrom[default:1][env:TRIP_FIRST_TTL=]-t,--max-ttlThemaximumnumberofTTLhops[default:64][env:TRIP_MAX_TTL=]--packet-sizeThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84][env:TRIP_PACKET_SIZE=]--payload-patternTherepeatingpatterninthepayloadoftheICMPpacket[default:0][env:TRIP_PAYLOAD_PATTERN=]-Q,--tosTheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0][env:TRIP_TOS=]-e,--icmp-extensionsParseICMPextensions[env:TRIP_ICMP_EXTENSIONS=]--read-timeoutThesocketreadtimeout[default:10ms][env:TRIP_READ_TIMEOUT=]-r,--dns-resolve-methodHowtoperformDNSqueries[default:system]Possiblevalues:-system:ResolveusingtheOSresolver-resolv:Resolveusingthe`/etc/resolv.conf`DNSconfiguration-google:ResolveusingtheGoogle`8.8.8.8`DNSservice-cloudflare:ResolveusingtheCloudflare`1.1.1.1`DNSservice[env:TRIP_DNS_RESOLVE_METHOD=]-y,--dns-resolve-allTracetoallIPsresolvedfromDNSlookup[default:false][env:TRIP_DNS_RESOLVE_ALL=]--dns-timeoutThemaximumtimetowaittoperformDNSqueries[default:5s][env:TRIP_DNS_TIMEOUT=]--dns-ttlThetime-to-live(TTL)ofDNSentries[default:300s][env:TRIP_DNS_TTL=]-z,--dns-lookup-as-infoLookupautonomoussystem(AS)informationduringDNSqueries[default:false][env:TRIP_DNS_LOOKUP_AS_INFO=]-s,--max-samplesThemaximumnumberofsamplestorecordperhop[default:256][env:TRIP_MAX_SAMPLES=]--max-flowsThemaximumnumberofflowstorecord[default:64][env:TRIP_MAX_FLOWS=]-a,--tui-address-modeHowtorenderaddresses[default:host]Possiblevalues:-ip:ShowIPaddressonly-host:Showreverse-lookupDNShostnameonly-both:ShowbothIPaddressandreverse-lookupDNShostname[env:TRIP_TUI_ADDRESS_MODE=]--tui-as-modeHowtorenderautonomoussystem(AS)information[default:asn]Possiblevalues:-asn:ShowtheASN-prefix:DisplaytheASprefix-country-code:Displaythecountrycode-registry:Displaytheregistryname-allocated:Displaytheallocateddate-name:DisplaytheASname[env:TRIP_TUI_AS_MODE=]--tui-custom-columnsCustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt][env:TRIP_TUI_CUSTOM_COLUMNS=]--tui-icmp-extension-modeHowtorenderICMPextensions[default:off]Possiblevalues:-off:Donotshow`icmp`extensions-mpls:ShowMPLSlabel(s)only-full:Showfull`icmp`extensiondataforallknownextensions-all:Showfull`icmp`extensiondataforallclasses[env:TRIP_TUI_ICMP_EXTENSION_MODE=]--tui-geoip-modeHowtorenderGeoIpinformation[default:short]Possiblevalues:-off:DonotdisplayGeoIpdata-short:Showshortformat-long:Showlongformat-location:ShowlatitudeandLongitudeformat[env:TRIP_TUI_GEOIP_MODE=]-M,--tui-max-addrsThemaximumnumberofaddressestoshowperhop[default:auto][env:TRIP_TUI_MAX_ADDRS=]--tui-preserve-screenPreservethescreenonexit[default:false][env:TRIP_TUI_PRESERVE_SCREEN=]--tui-refresh-rateTheTUIrefreshrate[default:100ms][env:TRIP_TUI_REFRESH_RATE=]--tui-privacy-max-ttlThemaximumttlofhopswhichwillbemaskedforprivacy[default:none]Ifset,thesourceIPaddressandhostnamewillalsobehidden.[env:TRIP_TUI_PRIVACY_MAX_TTL=]--tui-localeThelocaletousefortheTUI[default:auto][env:TRIP_TUI_LOCALE=]--tui-timezoneThetimezonetousefortheTUI[default:auto]ThetimezonemustbeavalidIANAtimezoneidentifier.[env:TRIP_TUI_TIMEZONE=]--tui-theme-colorsTheTUIthemecolors[item=color,item=color,..][env:TRIP_TUI_THEME_COLORS=]--print-tui-theme-itemsPrintallTUIthemeitemsandexit[env:TRIP_PRINT_TUI_THEME_ITEMS=]--tui-key-bindingsTheTUIkeybindings[command=key,command=key,..][env:TRIP_TUI_KEY_BINDINGS=]--print-tui-binding-commandsPrintallTUIcommandsthatcanbeboundandexit[env:TRIP_PRINT_TUI_BINDING_COMMANDS=]-C,--report-cyclesThenumberofreportcyclestorun[default:10][env:TRIP_REPORT_CYCLES=]-G,--geoip-mmdb-fileThesupportedMaxMindorIPinfoGeoIpmmdbfile[env:TRIP_GEOIP_MMDB_FILE=]--generateGenerateshellcompletion[env:TRIP_GENERATE=][possiblevalues:bash,elvish,fish,powershell,zsh]--generate-manGenerateROFFmanpage[env:TRIP_GENERATE_MAN=]--print-config-templatePrintatemplatetomlconfigfileandexit[env:TRIP_PRINT_CONFIG_TEMPLATE=]--print-localesPrintallavailableTUIlocalesandexit[env:TRIP_PRINT_LOCALES=]--log-formatThedebuglogformat[default:pretty]Possiblevalues:-compact:Displaylogdatainacompactformat-pretty:Displaylogdatainaprettyformat-json:Displaylogdatainajsonformat-chrome:DisplaylogdatainChrometraceformat[env:TRIP_LOG_FORMAT=]--log-filterThedebuglogfilter[default:trippy=debug][env:TRIP_LOG_FILTER=]--log-span-eventsThedebuglogformat[default:off]Possiblevalues:-off:Donotdisplayeventspans-active:Displayenterandexiteventspans-full:Displayalleventspans[env:TRIP_LOG_SPAN_EVENTS=]-v,--verboseEnableverbosedebuglogging[env:TRIP_VERBOSE=]-h,--helpPrinthelp(seeasummarywith'-h')-V,--versionPrintversion ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__config__tests__compare_snapshot@trip_-h.snap ================================================ --- source: crates/trippy-tui/src/config.rs --- AnetworkdiagnostictoolUsage:trip[OPTIONS][TARGETS]...Arguments:[TARGETS]...AspacedelimitedlistofhostnamesandIPstotrace[env:TRIP_TARGETS=]Options:-c,--config-fileConfigfile[env:TRIP_CONFIG_FILE=]-m,--modeOutputmode[default:tui][env:TRIP_MODE=][possiblevalues:tui,stream,pretty,markdown,csv,json,dot,flows,silent]-u,--unprivilegedTracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false][env:TRIP_UNPRIVILEGED=]-p,--protocolTracingprotocol[default:icmp][env:TRIP_PROTOCOL=][possiblevalues:icmp,udp,tcp]--udpTraceusingtheUDPprotocol[env:TRIP_UDP=]--tcpTraceusingtheTCPprotocol[env:TRIP_TCP=]--icmpTraceusingtheICMPprotocol[env:TRIP_ICMP=]-F,--addr-familyTheaddressfamily[default:system][env:TRIP_ADDR_FAMILY=][possiblevalues:ipv4,ipv6,ipv6-then-ipv4,ipv4-then-ipv6,system]-4,--ipv4UseIPv4only[env:TRIP_IPV4=]-6,--ipv6UseIPv6only[env:TRIP_IPV6=]-P,--target-portThetargetport(TCP&UDPonly)[default:80][env:TRIP_TARGET_PORT=]-S,--source-portThesourceport(TCP&UDPonly)[default:auto][env:TRIP_SOURCE_PORT=]-A,--source-addressThesourceIPaddress[default:auto][env:TRIP_SOURCE_ADDRESS=]-I,--interfaceThenetworkinterface[default:auto][env:TRIP_INTERFACE=]-i,--min-round-durationTheminimumdurationofeveryround[default:1s][env:TRIP_MIN_ROUND_DURATION=]-T,--max-round-durationThemaximumdurationofeveryround[default:1s][env:TRIP_MAX_ROUND_DURATION=]-g,--grace-durationTheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms][env:TRIP_GRACE_DURATION=]--initial-sequenceTheinitialsequencenumber[default:33434][env:TRIP_INITIAL_SEQUENCE=]-R,--multipath-strategyTheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic][env:TRIP_MULTIPATH_STRATEGY=][possiblevalues:classic,paris,dublin]-U,--max-inflightThemaximumnumberofin-flightICMPechorequests[default:24][env:TRIP_MAX_INFLIGHT=]-f,--first-ttlTheTTLtostartfrom[default:1][env:TRIP_FIRST_TTL=]-t,--max-ttlThemaximumnumberofTTLhops[default:64][env:TRIP_MAX_TTL=]--packet-sizeThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84][env:TRIP_PACKET_SIZE=]--payload-patternTherepeatingpatterninthepayloadoftheICMPpacket[default:0][env:TRIP_PAYLOAD_PATTERN=]-Q,--tosTheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0][env:TRIP_TOS=]-e,--icmp-extensionsParseICMPextensions[env:TRIP_ICMP_EXTENSIONS=]--read-timeoutThesocketreadtimeout[default:10ms][env:TRIP_READ_TIMEOUT=]-r,--dns-resolve-methodHowtoperformDNSqueries[default:system][env:TRIP_DNS_RESOLVE_METHOD=][possiblevalues:system,resolv,google,cloudflare]-y,--dns-resolve-allTracetoallIPsresolvedfromDNSlookup[default:false][env:TRIP_DNS_RESOLVE_ALL=]--dns-timeoutThemaximumtimetowaittoperformDNSqueries[default:5s][env:TRIP_DNS_TIMEOUT=]--dns-ttlThetime-to-live(TTL)ofDNSentries[default:300s][env:TRIP_DNS_TTL=]-z,--dns-lookup-as-infoLookupautonomoussystem(AS)informationduringDNSqueries[default:false][env:TRIP_DNS_LOOKUP_AS_INFO=]-s,--max-samplesThemaximumnumberofsamplestorecordperhop[default:256][env:TRIP_MAX_SAMPLES=]--max-flowsThemaximumnumberofflowstorecord[default:64][env:TRIP_MAX_FLOWS=]-a,--tui-address-modeHowtorenderaddresses[default:host][env:TRIP_TUI_ADDRESS_MODE=][possiblevalues:ip,host,both]--tui-as-modeHowtorenderautonomoussystem(AS)information[default:asn][env:TRIP_TUI_AS_MODE=][possiblevalues:asn,prefix,country-code,registry,allocated,name]--tui-custom-columnsCustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt][env:TRIP_TUI_CUSTOM_COLUMNS=]--tui-icmp-extension-modeHowtorenderICMPextensions[default:off][env:TRIP_TUI_ICMP_EXTENSION_MODE=][possiblevalues:off,mpls,full,all]--tui-geoip-modeHowtorenderGeoIpinformation[default:short][env:TRIP_TUI_GEOIP_MODE=][possiblevalues:off,short,long,location]-M,--tui-max-addrsThemaximumnumberofaddressestoshowperhop[default:auto][env:TRIP_TUI_MAX_ADDRS=]--tui-preserve-screenPreservethescreenonexit[default:false][env:TRIP_TUI_PRESERVE_SCREEN=]--tui-refresh-rateTheTUIrefreshrate[default:100ms][env:TRIP_TUI_REFRESH_RATE=]--tui-privacy-max-ttlThemaximumttlofhopswhichwillbemaskedforprivacy[default:none][env:TRIP_TUI_PRIVACY_MAX_TTL=]--tui-localeThelocaletousefortheTUI[default:auto][env:TRIP_TUI_LOCALE=]--tui-timezoneThetimezonetousefortheTUI[default:auto][env:TRIP_TUI_TIMEZONE=]--tui-theme-colorsTheTUIthemecolors[item=color,item=color,..][env:TRIP_TUI_THEME_COLORS=]--print-tui-theme-itemsPrintallTUIthemeitemsandexit[env:TRIP_PRINT_TUI_THEME_ITEMS=]--tui-key-bindingsTheTUIkeybindings[command=key,command=key,..][env:TRIP_TUI_KEY_BINDINGS=]--print-tui-binding-commandsPrintallTUIcommandsthatcanbeboundandexit[env:TRIP_PRINT_TUI_BINDING_COMMANDS=]-C,--report-cyclesThenumberofreportcyclestorun[default:10][env:TRIP_REPORT_CYCLES=]-G,--geoip-mmdb-fileThesupportedMaxMindorIPinfoGeoIpmmdbfile[env:TRIP_GEOIP_MMDB_FILE=]--generateGenerateshellcompletion[env:TRIP_GENERATE=][possiblevalues:bash,elvish,fish,powershell,zsh]--generate-manGenerateROFFmanpage[env:TRIP_GENERATE_MAN=]--print-config-templatePrintatemplatetomlconfigfileandexit[env:TRIP_PRINT_CONFIG_TEMPLATE=]--print-localesPrintallavailableTUIlocalesandexit[env:TRIP_PRINT_LOCALES=]--log-formatThedebuglogformat[default:pretty][env:TRIP_LOG_FORMAT=][possiblevalues:compact,pretty,json,chrome]--log-filterThedebuglogfilter[default:trippy=debug][env:TRIP_LOG_FILTER=]--log-span-eventsThedebuglogformat[default:off][env:TRIP_LOG_SPAN_EVENTS=][possiblevalues:off,active,full]-v,--verboseEnableverbosedebuglogging[env:TRIP_VERBOSE=]-h,--helpPrinthelp(seemorewith'--help')-V,--versionPrintversion ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_bash_shell_completions.snap ================================================ --- source: crates/trippy-tui/src/print.rs --- _trip(){localicurprevoptscmdCOMPREPLY=()if[["${BASH_VERSINFO[0]}"-ge4]];thencur="$2"elsecur="${COMP_WORDS[COMP_CWORD]}"fiprev="$3"cmd=""opts=""foriin"${COMP_WORDS[@]:0:COMP_CWORD}"docase"${cmd},${i}"in",$1")cmd="trip";;*);;esacdonecase"${cmd}"intrip)opts="-c-m-u-p-F-4-6-P-S-A-I-i-T-g-R-U-f-t-Q-e-r-y-z-s-a-M-C-G-v-h-V--config-file--mode--unprivileged--protocol--udp--tcp--icmp--addr-family--ipv4--ipv6--target-port--source-port--source-address--interface--min-round-duration--max-round-duration--grace-duration--initial-sequence--multipath-strategy--max-inflight--first-ttl--max-ttl--packet-size--payload-pattern--tos--icmp-extensions--read-timeout--dns-resolve-method--dns-resolve-all--dns-timeout--dns-ttl--dns-lookup-as-info--max-samples--max-flows--tui-address-mode--tui-as-mode--tui-custom-columns--tui-icmp-extension-mode--tui-geoip-mode--tui-max-addrs--tui-preserve-screen--tui-refresh-rate--tui-privacy-max-ttl--tui-locale--tui-timezone--tui-theme-colors--print-tui-theme-items--tui-key-bindings--print-tui-binding-commands--report-cycles--geoip-mmdb-file--generate--generate-man--print-config-template--print-locales--log-format--log-filter--log-span-events--verbose--help--version[TARGETS]..."if[[${cur}==-*||${COMP_CWORD}-eq1]];thenCOMPREPLY=($(compgen-W"${opts}"--"${cur}"))return0ficase"${prev}"in--config-file)localoldifsif[-n"${IFS+x}"];thenoldifs="$IFS"fiIFS=$'\n'COMPREPLY=($(compgen-f"${cur}"))if[-n"${oldifs+x}"];thenIFS="$oldifs"fiif[["${BASH_VERSINFO[0]}"-ge4]];thencompopt-ofilenamesfireturn0;;-c)localoldifsif[-n"${IFS+x}"];thenoldifs="$IFS"fiIFS=$'\n'COMPREPLY=($(compgen-f"${cur}"))if[-n"${oldifs+x}"];thenIFS="$oldifs"fiif[["${BASH_VERSINFO[0]}"-ge4]];thencompopt-ofilenamesfireturn0;;--mode)COMPREPLY=($(compgen-W"tuistreamprettymarkdowncsvjsondotflowssilent"--"${cur}"))return0;;-m)COMPREPLY=($(compgen-W"tuistreamprettymarkdowncsvjsondotflowssilent"--"${cur}"))return0;;--protocol)COMPREPLY=($(compgen-W"icmpudptcp"--"${cur}"))return0;;-p)COMPREPLY=($(compgen-W"icmpudptcp"--"${cur}"))return0;;--addr-family)COMPREPLY=($(compgen-W"ipv4ipv6ipv6-then-ipv4ipv4-then-ipv6system"--"${cur}"))return0;;-F)COMPREPLY=($(compgen-W"ipv4ipv6ipv6-then-ipv4ipv4-then-ipv6system"--"${cur}"))return0;;--target-port)COMPREPLY=($(compgen-f"${cur}"))return0;;-P)COMPREPLY=($(compgen-f"${cur}"))return0;;--source-port)COMPREPLY=($(compgen-f"${cur}"))return0;;-S)COMPREPLY=($(compgen-f"${cur}"))return0;;--source-address)COMPREPLY=($(compgen-f"${cur}"))return0;;-A)COMPREPLY=($(compgen-f"${cur}"))return0;;--interface)COMPREPLY=($(compgen-f"${cur}"))return0;;-I)COMPREPLY=($(compgen-f"${cur}"))return0;;--min-round-duration)COMPREPLY=($(compgen-f"${cur}"))return0;;-i)COMPREPLY=($(compgen-f"${cur}"))return0;;--max-round-duration)COMPREPLY=($(compgen-f"${cur}"))return0;;-T)COMPREPLY=($(compgen-f"${cur}"))return0;;--grace-duration)COMPREPLY=($(compgen-f"${cur}"))return0;;-g)COMPREPLY=($(compgen-f"${cur}"))return0;;--initial-sequence)COMPREPLY=($(compgen-f"${cur}"))return0;;--multipath-strategy)COMPREPLY=($(compgen-W"classicparisdublin"--"${cur}"))return0;;-R)COMPREPLY=($(compgen-W"classicparisdublin"--"${cur}"))return0;;--max-inflight)COMPREPLY=($(compgen-f"${cur}"))return0;;-U)COMPREPLY=($(compgen-f"${cur}"))return0;;--first-ttl)COMPREPLY=($(compgen-f"${cur}"))return0;;-f)COMPREPLY=($(compgen-f"${cur}"))return0;;--max-ttl)COMPREPLY=($(compgen-f"${cur}"))return0;;-t)COMPREPLY=($(compgen-f"${cur}"))return0;;--packet-size)COMPREPLY=($(compgen-f"${cur}"))return0;;--payload-pattern)COMPREPLY=($(compgen-f"${cur}"))return0;;--tos)COMPREPLY=($(compgen-f"${cur}"))return0;;-Q)COMPREPLY=($(compgen-f"${cur}"))return0;;--read-timeout)COMPREPLY=($(compgen-f"${cur}"))return0;;--dns-resolve-method)COMPREPLY=($(compgen-W"systemresolvgooglecloudflare"--"${cur}"))return0;;-r)COMPREPLY=($(compgen-W"systemresolvgooglecloudflare"--"${cur}"))return0;;--dns-timeout)COMPREPLY=($(compgen-f"${cur}"))return0;;--dns-ttl)COMPREPLY=($(compgen-f"${cur}"))return0;;--max-samples)COMPREPLY=($(compgen-f"${cur}"))return0;;-s)COMPREPLY=($(compgen-f"${cur}"))return0;;--max-flows)COMPREPLY=($(compgen-f"${cur}"))return0;;--tui-address-mode)COMPREPLY=($(compgen-W"iphostboth"--"${cur}"))return0;;-a)COMPREPLY=($(compgen-W"iphostboth"--"${cur}"))return0;;--tui-as-mode)COMPREPLY=($(compgen-W"asnprefixcountry-coderegistryallocatedname"--"${cur}"))return0;;--tui-custom-columns)COMPREPLY=($(compgen-f"${cur}"))return0;;--tui-icmp-extension-mode)COMPREPLY=($(compgen-W"offmplsfullall"--"${cur}"))return0;;--tui-geoip-mode)COMPREPLY=($(compgen-W"offshortlonglocation"--"${cur}"))return0;;--tui-max-addrs)COMPREPLY=($(compgen-f"${cur}"))return0;;-M)COMPREPLY=($(compgen-f"${cur}"))return0;;--tui-refresh-rate)COMPREPLY=($(compgen-f"${cur}"))return0;;--tui-privacy-max-ttl)COMPREPLY=($(compgen-f"${cur}"))return0;;--tui-locale)COMPREPLY=($(compgen-f"${cur}"))return0;;--tui-timezone)COMPREPLY=($(compgen-f"${cur}"))return0;;--tui-theme-colors)COMPREPLY=($(compgen-f"${cur}"))return0;;--tui-key-bindings)COMPREPLY=($(compgen-f"${cur}"))return0;;--report-cycles)COMPREPLY=($(compgen-f"${cur}"))return0;;-C)COMPREPLY=($(compgen-f"${cur}"))return0;;--geoip-mmdb-file)localoldifsif[-n"${IFS+x}"];thenoldifs="$IFS"fiIFS=$'\n'COMPREPLY=($(compgen-f"${cur}"))if[-n"${oldifs+x}"];thenIFS="$oldifs"fiif[["${BASH_VERSINFO[0]}"-ge4]];thencompopt-ofilenamesfireturn0;;-G)localoldifsif[-n"${IFS+x}"];thenoldifs="$IFS"fiIFS=$'\n'COMPREPLY=($(compgen-f"${cur}"))if[-n"${oldifs+x}"];thenIFS="$oldifs"fiif[["${BASH_VERSINFO[0]}"-ge4]];thencompopt-ofilenamesfireturn0;;--generate)COMPREPLY=($(compgen-W"bashelvishfishpowershellzsh"--"${cur}"))return0;;--log-format)COMPREPLY=($(compgen-W"compactprettyjsonchrome"--"${cur}"))return0;;--log-filter)COMPREPLY=($(compgen-f"${cur}"))return0;;--log-span-events)COMPREPLY=($(compgen-W"offactivefull"--"${cur}"))return0;;*)COMPREPLY=();;esacCOMPREPLY=($(compgen-W"${opts}"--"${cur}"))return0;;esac}if[["${BASH_VERSINFO[0]}"-eq4&&"${BASH_VERSINFO[1]}"-ge4||"${BASH_VERSINFO[0]}"-gt4]];thencomplete-F_trip-onosort-obashdefault-odefaulttripelsecomplete-F_trip-obashdefault-odefaulttripfi ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_elvish_shell_completions.snap ================================================ --- source: crates/trippy-tui/src/print.rs --- usebuiltin;usestr;setedit:completion:arg-completer[trip]={|@words|fnspaces{|n|builtin:repeat$n''|str:join''}fncand{|textdesc|edit:complex-candidate$text&display=$text''(spaces(-14(wcswidth$text)))$desc}varcommand='trip'forword$words[1..-1]{if(str:has-prefix$word'-'){break}setcommand=$command';'$word}varcompletions=[&'trip'={cand-c'Configfile'cand--config-file'Configfile'cand-m'Outputmode[default:tui]'cand--mode'Outputmode[default:tui]'cand-p'Tracingprotocol[default:icmp]'cand--protocol'Tracingprotocol[default:icmp]'cand-F'Theaddressfamily[default:system]'cand--addr-family'Theaddressfamily[default:system]'cand-P'Thetargetport(TCP&UDPonly)[default:80]'cand--target-port'Thetargetport(TCP&UDPonly)[default:80]'cand-S'Thesourceport(TCP&UDPonly)[default:auto]'cand--source-port'Thesourceport(TCP&UDPonly)[default:auto]'cand-A'ThesourceIPaddress[default:auto]'cand--source-address'ThesourceIPaddress[default:auto]'cand-I'Thenetworkinterface[default:auto]'cand--interface'Thenetworkinterface[default:auto]'cand-i'Theminimumdurationofeveryround[default:1s]'cand--min-round-duration'Theminimumdurationofeveryround[default:1s]'cand-T'Themaximumdurationofeveryround[default:1s]'cand--max-round-duration'Themaximumdurationofeveryround[default:1s]'cand-g'TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms]'cand--grace-duration'TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms]'cand--initial-sequence'Theinitialsequencenumber[default:33434]'cand-R'TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic]'cand--multipath-strategy'TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic]'cand-U'Themaximumnumberofin-flightICMPechorequests[default:24]'cand--max-inflight'Themaximumnumberofin-flightICMPechorequests[default:24]'cand-f'TheTTLtostartfrom[default:1]'cand--first-ttl'TheTTLtostartfrom[default:1]'cand-t'ThemaximumnumberofTTLhops[default:64]'cand--max-ttl'ThemaximumnumberofTTLhops[default:64]'cand--packet-size'ThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84]'cand--payload-pattern'TherepeatingpatterninthepayloadoftheICMPpacket[default:0]'cand-Q'TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0]'cand--tos'TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0]'cand--read-timeout'Thesocketreadtimeout[default:10ms]'cand-r'HowtoperformDNSqueries[default:system]'cand--dns-resolve-method'HowtoperformDNSqueries[default:system]'cand--dns-timeout'ThemaximumtimetowaittoperformDNSqueries[default:5s]'cand--dns-ttl'Thetime-to-live(TTL)ofDNSentries[default:300s]'cand-s'Themaximumnumberofsamplestorecordperhop[default:256]'cand--max-samples'Themaximumnumberofsamplestorecordperhop[default:256]'cand--max-flows'Themaximumnumberofflowstorecord[default:64]'cand-a'Howtorenderaddresses[default:host]'cand--tui-address-mode'Howtorenderaddresses[default:host]'cand--tui-as-mode'Howtorenderautonomoussystem(AS)information[default:asn]'cand--tui-custom-columns'CustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt]'cand--tui-icmp-extension-mode'HowtorenderICMPextensions[default:off]'cand--tui-geoip-mode'HowtorenderGeoIpinformation[default:short]'cand-M'Themaximumnumberofaddressestoshowperhop[default:auto]'cand--tui-max-addrs'Themaximumnumberofaddressestoshowperhop[default:auto]'cand--tui-refresh-rate'TheTUIrefreshrate[default:100ms]'cand--tui-privacy-max-ttl'Themaximumttlofhopswhichwillbemaskedforprivacy[default:none]'cand--tui-locale'ThelocaletousefortheTUI[default:auto]'cand--tui-timezone'ThetimezonetousefortheTUI[default:auto]'cand--tui-theme-colors'TheTUIthemecolors[item=color,item=color,..]'cand--tui-key-bindings'TheTUIkeybindings[command=key,command=key,..]'cand-C'Thenumberofreportcyclestorun[default:10]'cand--report-cycles'Thenumberofreportcyclestorun[default:10]'cand-G'ThesupportedMaxMindorIPinfoGeoIpmmdbfile'cand--geoip-mmdb-file'ThesupportedMaxMindorIPinfoGeoIpmmdbfile'cand--generate'Generateshellcompletion'cand--log-format'Thedebuglogformat[default:pretty]'cand--log-filter'Thedebuglogfilter[default:trippy=debug]'cand--log-span-events'Thedebuglogformat[default:off]'cand-u'Tracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false]'cand--unprivileged'Tracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false]'cand--udp'TraceusingtheUDPprotocol'cand--tcp'TraceusingtheTCPprotocol'cand--icmp'TraceusingtheICMPprotocol'cand-4'UseIPv4only'cand--ipv4'UseIPv4only'cand-6'UseIPv6only'cand--ipv6'UseIPv6only'cand-e'ParseICMPextensions'cand--icmp-extensions'ParseICMPextensions'cand-y'TracetoallIPsresolvedfromDNSlookup[default:false]'cand--dns-resolve-all'TracetoallIPsresolvedfromDNSlookup[default:false]'cand-z'Lookupautonomoussystem(AS)informationduringDNSqueries[default:false]'cand--dns-lookup-as-info'Lookupautonomoussystem(AS)informationduringDNSqueries[default:false]'cand--tui-preserve-screen'Preservethescreenonexit[default:false]'cand--print-tui-theme-items'PrintallTUIthemeitemsandexit'cand--print-tui-binding-commands'PrintallTUIcommandsthatcanbeboundandexit'cand--generate-man'GenerateROFFmanpage'cand--print-config-template'Printatemplatetomlconfigfileandexit'cand--print-locales'PrintallavailableTUIlocalesandexit'cand-v'Enableverbosedebuglogging'cand--verbose'Enableverbosedebuglogging'cand-h'Printhelp(seemorewith''--help'')'cand--help'Printhelp(seemorewith''--help'')'cand-V'Printversion'cand--version'Printversion'}]$completions[$command]} ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_fish_shell_completions.snap ================================================ --- source: crates/trippy-tui/src/print.rs --- complete-ctrip-sc-lconfig-file-d'Configfile'-r-Fcomplete-ctrip-sm-lmode-d'Outputmode[default:tui]'-r-f-a"tui\t'DisplayinteractiveTUI'stream\t'Displayacontinuousstreamoftracingdata'pretty\t'GenerateaprettytexttablereportforNcycles'markdown\t'GenerateaMarkdowntexttablereportforNcycles'csv\t'GenerateaCSVreportforNcycles'json\t'GenerateaJSONreportforNcycles'dot\t'GenerateaGraphvizDOTfileforNcycles'flows\t'DisplayallflowsforNcycles'silent\t'DonotgenerateanytracingoutputforNcycles'"complete-ctrip-sp-lprotocol-d'Tracingprotocol[default:icmp]'-r-f-a"icmp\t'InternetControlMessageProtocol'udp\t'UserDatagramProtocol'tcp\t'TransmissionControlProtocol'"complete-ctrip-sF-laddr-family-d'Theaddressfamily[default:system]'-r-f-a"ipv4\t'IPv4only'ipv6\t'IPv6only'ipv6-then-ipv4\t'IPv6withafallbacktoIPv4'ipv4-then-ipv6\t'IPv4withafallbacktoIPv6'system\t'IftheOSresolverisbeingusedthenusethefirstIPaddressreturned,otherwiselookupIPv4withafallbacktoIPv6'"complete-ctrip-sP-ltarget-port-d'Thetargetport(TCP&UDPonly)[default:80]'-rcomplete-ctrip-sS-lsource-port-d'Thesourceport(TCP&UDPonly)[default:auto]'-rcomplete-ctrip-sA-lsource-address-d'ThesourceIPaddress[default:auto]'-rcomplete-ctrip-sI-linterface-d'Thenetworkinterface[default:auto]'-rcomplete-ctrip-si-lmin-round-duration-d'Theminimumdurationofeveryround[default:1s]'-rcomplete-ctrip-sT-lmax-round-duration-d'Themaximumdurationofeveryround[default:1s]'-rcomplete-ctrip-sg-lgrace-duration-d'TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms]'-rcomplete-ctrip-linitial-sequence-d'Theinitialsequencenumber[default:33434]'-rcomplete-ctrip-sR-lmultipath-strategy-d'TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic]'-r-f-a"classic\t'Thesrcordestportisusedtostorethesequencenumber'paris\t'TheUDP`checksum`fieldisusedtostorethesequencenumber'dublin\t'TheIP`identifier`fieldisusedtostorethesequencenumber'"complete-ctrip-sU-lmax-inflight-d'Themaximumnumberofin-flightICMPechorequests[default:24]'-rcomplete-ctrip-sf-lfirst-ttl-d'TheTTLtostartfrom[default:1]'-rcomplete-ctrip-st-lmax-ttl-d'ThemaximumnumberofTTLhops[default:64]'-rcomplete-ctrip-lpacket-size-d'ThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84]'-rcomplete-ctrip-lpayload-pattern-d'TherepeatingpatterninthepayloadoftheICMPpacket[default:0]'-rcomplete-ctrip-sQ-ltos-d'TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0]'-rcomplete-ctrip-lread-timeout-d'Thesocketreadtimeout[default:10ms]'-rcomplete-ctrip-sr-ldns-resolve-method-d'HowtoperformDNSqueries[default:system]'-r-f-a"system\t'ResolveusingtheOSresolver'resolv\t'Resolveusingthe`/etc/resolv.conf`DNSconfiguration'google\t'ResolveusingtheGoogle`8.8.8.8`DNSservice'cloudflare\t'ResolveusingtheCloudflare`1.1.1.1`DNSservice'"complete-ctrip-ldns-timeout-d'ThemaximumtimetowaittoperformDNSqueries[default:5s]'-rcomplete-ctrip-ldns-ttl-d'Thetime-to-live(TTL)ofDNSentries[default:300s]'-rcomplete-ctrip-ss-lmax-samples-d'Themaximumnumberofsamplestorecordperhop[default:256]'-rcomplete-ctrip-lmax-flows-d'Themaximumnumberofflowstorecord[default:64]'-rcomplete-ctrip-sa-ltui-address-mode-d'Howtorenderaddresses[default:host]'-r-f-a"ip\t'ShowIPaddressonly'host\t'Showreverse-lookupDNShostnameonly'both\t'ShowbothIPaddressandreverse-lookupDNShostname'"complete-ctrip-ltui-as-mode-d'Howtorenderautonomoussystem(AS)information[default:asn]'-r-f-a"asn\t'ShowtheASN'prefix\t'DisplaytheASprefix'country-code\t'Displaythecountrycode'registry\t'Displaytheregistryname'allocated\t'Displaytheallocateddate'name\t'DisplaytheASname'"complete-ctrip-ltui-custom-columns-d'CustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt]'-rcomplete-ctrip-ltui-icmp-extension-mode-d'HowtorenderICMPextensions[default:off]'-r-f-a"off\t'Donotshow`icmp`extensions'mpls\t'ShowMPLSlabel(s)only'full\t'Showfull`icmp`extensiondataforallknownextensions'all\t'Showfull`icmp`extensiondataforallclasses'"complete-ctrip-ltui-geoip-mode-d'HowtorenderGeoIpinformation[default:short]'-r-f-a"off\t'DonotdisplayGeoIpdata'short\t'Showshortformat'long\t'Showlongformat'location\t'ShowlatitudeandLongitudeformat'"complete-ctrip-sM-ltui-max-addrs-d'Themaximumnumberofaddressestoshowperhop[default:auto]'-rcomplete-ctrip-ltui-refresh-rate-d'TheTUIrefreshrate[default:100ms]'-rcomplete-ctrip-ltui-privacy-max-ttl-d'Themaximumttlofhopswhichwillbemaskedforprivacy[default:none]'-rcomplete-ctrip-ltui-locale-d'ThelocaletousefortheTUI[default:auto]'-rcomplete-ctrip-ltui-timezone-d'ThetimezonetousefortheTUI[default:auto]'-rcomplete-ctrip-ltui-theme-colors-d'TheTUIthemecolors[item=color,item=color,..]'-rcomplete-ctrip-ltui-key-bindings-d'TheTUIkeybindings[command=key,command=key,..]'-rcomplete-ctrip-sC-lreport-cycles-d'Thenumberofreportcyclestorun[default:10]'-rcomplete-ctrip-sG-lgeoip-mmdb-file-d'ThesupportedMaxMindorIPinfoGeoIpmmdbfile'-r-Fcomplete-ctrip-lgenerate-d'Generateshellcompletion'-r-f-a"bash\t''elvish\t''fish\t''powershell\t''zsh\t''"complete-ctrip-llog-format-d'Thedebuglogformat[default:pretty]'-r-f-a"compact\t'Displaylogdatainacompactformat'pretty\t'Displaylogdatainaprettyformat'json\t'Displaylogdatainajsonformat'chrome\t'DisplaylogdatainChrometraceformat'"complete-ctrip-llog-filter-d'Thedebuglogfilter[default:trippy=debug]'-rcomplete-ctrip-llog-span-events-d'Thedebuglogformat[default:off]'-r-f-a"off\t'Donotdisplayeventspans'active\t'Displayenterandexiteventspans'full\t'Displayalleventspans'"complete-ctrip-su-lunprivileged-d'Tracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false]'complete-ctrip-ludp-d'TraceusingtheUDPprotocol'complete-ctrip-ltcp-d'TraceusingtheTCPprotocol'complete-ctrip-licmp-d'TraceusingtheICMPprotocol'complete-ctrip-s4-lipv4-d'UseIPv4only'complete-ctrip-s6-lipv6-d'UseIPv6only'complete-ctrip-se-licmp-extensions-d'ParseICMPextensions'complete-ctrip-sy-ldns-resolve-all-d'TracetoallIPsresolvedfromDNSlookup[default:false]'complete-ctrip-sz-ldns-lookup-as-info-d'Lookupautonomoussystem(AS)informationduringDNSqueries[default:false]'complete-ctrip-ltui-preserve-screen-d'Preservethescreenonexit[default:false]'complete-ctrip-lprint-tui-theme-items-d'PrintallTUIthemeitemsandexit'complete-ctrip-lprint-tui-binding-commands-d'PrintallTUIcommandsthatcanbeboundandexit'complete-ctrip-lgenerate-man-d'GenerateROFFmanpage'complete-ctrip-lprint-config-template-d'Printatemplatetomlconfigfileandexit'complete-ctrip-lprint-locales-d'PrintallavailableTUIlocalesandexit'complete-ctrip-sv-lverbose-d'Enableverbosedebuglogging'complete-ctrip-sh-lhelp-d'Printhelp(seemorewith\'--help\')'complete-ctrip-sV-lversion-d'Printversion' ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_man_page.snap ================================================ --- source: crates/trippy-tui/src/print.rs --- .ie\n(.g.dsAq\(aq.el.dsAq'.THtrip1"trip0.14.0-dev".SHNAMEtrip\-Anetworkdiagnostictool.SHSYNOPSIS\fBtrip\fR[\fB\-c\fR|\fB\-\-config\-file\fR][\fB\-m\fR|\fB\-\-mode\fR][\fB\-u\fR|\fB\-\-unprivileged\fR][\fB\-p\fR|\fB\-\-protocol\fR][\fB\-\-udp\fR][\fB\-\-tcp\fR][\fB\-\-icmp\fR][\fB\-F\fR|\fB\-\-addr\-family\fR][\fB\-4\fR|\fB\-\-ipv4\fR][\fB\-6\fR|\fB\-\-ipv6\fR][\fB\-P\fR|\fB\-\-target\-port\fR][\fB\-S\fR|\fB\-\-source\-port\fR][\fB\-A\fR|\fB\-\-source\-address\fR][\fB\-I\fR|\fB\-\-interface\fR][\fB\-i\fR|\fB\-\-min\-round\-duration\fR][\fB\-T\fR|\fB\-\-max\-round\-duration\fR][\fB\-g\fR|\fB\-\-grace\-duration\fR][\fB\-\-initial\-sequence\fR][\fB\-R\fR|\fB\-\-multipath\-strategy\fR][\fB\-U\fR|\fB\-\-max\-inflight\fR][\fB\-f\fR|\fB\-\-first\-ttl\fR][\fB\-t\fR|\fB\-\-max\-ttl\fR][\fB\-\-packet\-size\fR][\fB\-\-payload\-pattern\fR][\fB\-Q\fR|\fB\-\-tos\fR][\fB\-e\fR|\fB\-\-icmp\-extensions\fR][\fB\-\-read\-timeout\fR][\fB\-r\fR|\fB\-\-dns\-resolve\-method\fR][\fB\-y\fR|\fB\-\-dns\-resolve\-all\fR][\fB\-\-dns\-timeout\fR][\fB\-\-dns\-ttl\fR][\fB\-z\fR|\fB\-\-dns\-lookup\-as\-info\fR][\fB\-s\fR|\fB\-\-max\-samples\fR][\fB\-\-max\-flows\fR][\fB\-a\fR|\fB\-\-tui\-address\-mode\fR][\fB\-\-tui\-as\-mode\fR][\fB\-\-tui\-custom\-columns\fR][\fB\-\-tui\-icmp\-extension\-mode\fR][\fB\-\-tui\-geoip\-mode\fR][\fB\-M\fR|\fB\-\-tui\-max\-addrs\fR][\fB\-\-tui\-preserve\-screen\fR][\fB\-\-tui\-refresh\-rate\fR][\fB\-\-tui\-privacy\-max\-ttl\fR][\fB\-\-tui\-locale\fR][\fB\-\-tui\-timezone\fR][\fB\-\-tui\-theme\-colors\fR][\fB\-\-print\-tui\-theme\-items\fR][\fB\-\-tui\-key\-bindings\fR][\fB\-\-print\-tui\-binding\-commands\fR][\fB\-C\fR|\fB\-\-report\-cycles\fR][\fB\-G\fR|\fB\-\-geoip\-mmdb\-file\fR][\fB\-\-generate\fR][\fB\-\-generate\-man\fR][\fB\-\-print\-config\-template\fR][\fB\-\-print\-locales\fR][\fB\-\-log\-format\fR][\fB\-\-log\-filter\fR][\fB\-\-log\-span\-events\fR][\fB\-v\fR|\fB\-\-verbose\fR][\fB\-h\fR|\fB\-\-help\fR][\fB\-V\fR|\fB\-\-version\fR][\fITARGETS\fR].SHDESCRIPTIONAnetworkdiagnostictool.SHOPTIONS.TP\fB\-c\fR,\fB\-\-config\-file\fR\fI\fRConfigfile.RSMayalsobespecifiedwiththe\fBTRIP_CONFIG_FILE\fRenvironmentvariable..RE.TP\fB\-m\fR,\fB\-\-mode\fR\fI\fROutputmode[default:tui].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2tui:DisplayinteractiveTUI.IP\(bu2stream:Displayacontinuousstreamoftracingdata.IP\(bu2pretty:GenerateaprettytexttablereportforNcycles.IP\(bu2markdown:GenerateaMarkdowntexttablereportforNcycles.IP\(bu2csv:GenerateaCSVreportforNcycles.IP\(bu2json:GenerateaJSONreportforNcycles.IP\(bu2dot:GenerateaGraphvizDOTfileforNcycles.IP\(bu2flows:DisplayallflowsforNcycles.IP\(bu2silent:DonotgenerateanytracingoutputforNcycles.RE.RSMayalsobespecifiedwiththe\fBTRIP_MODE\fRenvironmentvariable..RE.TP\fB\-u\fR,\fB\-\-unprivileged\fRTracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false].RSMayalsobespecifiedwiththe\fBTRIP_UNPRIVILEGED\fRenvironmentvariable..RE.TP\fB\-p\fR,\fB\-\-protocol\fR\fI\fRTracingprotocol[default:icmp].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2icmp:InternetControlMessageProtocol.IP\(bu2udp:UserDatagramProtocol.IP\(bu2tcp:TransmissionControlProtocol.RE.RSMayalsobespecifiedwiththe\fBTRIP_PROTOCOL\fRenvironmentvariable..RE.TP\fB\-\-udp\fRTraceusingtheUDPprotocol.RSMayalsobespecifiedwiththe\fBTRIP_UDP\fRenvironmentvariable..RE.TP\fB\-\-tcp\fRTraceusingtheTCPprotocol.RSMayalsobespecifiedwiththe\fBTRIP_TCP\fRenvironmentvariable..RE.TP\fB\-\-icmp\fRTraceusingtheICMPprotocol.RSMayalsobespecifiedwiththe\fBTRIP_ICMP\fRenvironmentvariable..RE.TP\fB\-F\fR,\fB\-\-addr\-family\fR\fI\fRTheaddressfamily[default:system].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2ipv4:IPv4only.IP\(bu2ipv6:IPv6only.IP\(bu2ipv6\-then\-ipv4:IPv6withafallbacktoIPv4.IP\(bu2ipv4\-then\-ipv6:IPv4withafallbacktoIPv6.IP\(bu2system:IftheOSresolverisbeingusedthenusethefirstIPaddressreturned,otherwiselookupIPv4withafallbacktoIPv6.RE.RSMayalsobespecifiedwiththe\fBTRIP_ADDR_FAMILY\fRenvironmentvariable..RE.TP\fB\-4\fR,\fB\-\-ipv4\fRUseIPv4only.RSMayalsobespecifiedwiththe\fBTRIP_IPV4\fRenvironmentvariable..RE.TP\fB\-6\fR,\fB\-\-ipv6\fRUseIPv6only.RSMayalsobespecifiedwiththe\fBTRIP_IPV6\fRenvironmentvariable..RE.TP\fB\-P\fR,\fB\-\-target\-port\fR\fI\fRThetargetport(TCP&UDPonly)[default:80].RSMayalsobespecifiedwiththe\fBTRIP_TARGET_PORT\fRenvironmentvariable..RE.TP\fB\-S\fR,\fB\-\-source\-port\fR\fI\fRThesourceport(TCP&UDPonly)[default:auto].RSMayalsobespecifiedwiththe\fBTRIP_SOURCE_PORT\fRenvironmentvariable..RE.TP\fB\-A\fR,\fB\-\-source\-address\fR\fI\fRThesourceIPaddress[default:auto].RSMayalsobespecifiedwiththe\fBTRIP_SOURCE_ADDRESS\fRenvironmentvariable..RE.TP\fB\-I\fR,\fB\-\-interface\fR\fI\fRThenetworkinterface[default:auto].RSMayalsobespecifiedwiththe\fBTRIP_INTERFACE\fRenvironmentvariable..RE.TP\fB\-i\fR,\fB\-\-min\-round\-duration\fR\fI\fRTheminimumdurationofeveryround[default:1s].RSMayalsobespecifiedwiththe\fBTRIP_MIN_ROUND_DURATION\fRenvironmentvariable..RE.TP\fB\-T\fR,\fB\-\-max\-round\-duration\fR\fI\fRThemaximumdurationofeveryround[default:1s].RSMayalsobespecifiedwiththe\fBTRIP_MAX_ROUND_DURATION\fRenvironmentvariable..RE.TP\fB\-g\fR,\fB\-\-grace\-duration\fR\fI\fRTheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms].RSMayalsobespecifiedwiththe\fBTRIP_GRACE_DURATION\fRenvironmentvariable..RE.TP\fB\-\-initial\-sequence\fR\fI\fRTheinitialsequencenumber[default:33434].RSMayalsobespecifiedwiththe\fBTRIP_INITIAL_SEQUENCE\fRenvironmentvariable..RE.TP\fB\-R\fR,\fB\-\-multipath\-strategy\fR\fI\fRTheEqual\-costMulti\-Pathroutingstrategy(UDPonly)[default:classic].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2classic:Thesrcordestportisusedtostorethesequencenumber.IP\(bu2paris:TheUDP`checksum`fieldisusedtostorethesequencenumber.IP\(bu2dublin:TheIP`identifier`fieldisusedtostorethesequencenumber.RE.RSMayalsobespecifiedwiththe\fBTRIP_MULTIPATH_STRATEGY\fRenvironmentvariable..RE.TP\fB\-U\fR,\fB\-\-max\-inflight\fR\fI\fRThemaximumnumberofin\-flightICMPechorequests[default:24].RSMayalsobespecifiedwiththe\fBTRIP_MAX_INFLIGHT\fRenvironmentvariable..RE.TP\fB\-f\fR,\fB\-\-first\-ttl\fR\fI\fRTheTTLtostartfrom[default:1].RSMayalsobespecifiedwiththe\fBTRIP_FIRST_TTL\fRenvironmentvariable..RE.TP\fB\-t\fR,\fB\-\-max\-ttl\fR\fI\fRThemaximumnumberofTTLhops[default:64].RSMayalsobespecifiedwiththe\fBTRIP_MAX_TTL\fRenvironmentvariable..RE.TP\fB\-\-packet\-size\fR\fI\fRThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84].RSMayalsobespecifiedwiththe\fBTRIP_PACKET_SIZE\fRenvironmentvariable..RE.TP\fB\-\-payload\-pattern\fR\fI\fRTherepeatingpatterninthepayloadoftheICMPpacket[default:0].RSMayalsobespecifiedwiththe\fBTRIP_PAYLOAD_PATTERN\fRenvironmentvariable..RE.TP\fB\-Q\fR,\fB\-\-tos\fR\fI\fRTheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0].RSMayalsobespecifiedwiththe\fBTRIP_TOS\fRenvironmentvariable..RE.TP\fB\-e\fR,\fB\-\-icmp\-extensions\fRParseICMPextensions.RSMayalsobespecifiedwiththe\fBTRIP_ICMP_EXTENSIONS\fRenvironmentvariable..RE.TP\fB\-\-read\-timeout\fR\fI\fRThesocketreadtimeout[default:10ms].RSMayalsobespecifiedwiththe\fBTRIP_READ_TIMEOUT\fRenvironmentvariable..RE.TP\fB\-r\fR,\fB\-\-dns\-resolve\-method\fR\fI\fRHowtoperformDNSqueries[default:system].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2system:ResolveusingtheOSresolver.IP\(bu2resolv:Resolveusingthe`/etc/resolv.conf`DNSconfiguration.IP\(bu2google:ResolveusingtheGoogle`8.8.8.8`DNSservice.IP\(bu2cloudflare:ResolveusingtheCloudflare`1.1.1.1`DNSservice.RE.RSMayalsobespecifiedwiththe\fBTRIP_DNS_RESOLVE_METHOD\fRenvironmentvariable..RE.TP\fB\-y\fR,\fB\-\-dns\-resolve\-all\fRTracetoallIPsresolvedfromDNSlookup[default:false].RSMayalsobespecifiedwiththe\fBTRIP_DNS_RESOLVE_ALL\fRenvironmentvariable..RE.TP\fB\-\-dns\-timeout\fR\fI\fRThemaximumtimetowaittoperformDNSqueries[default:5s].RSMayalsobespecifiedwiththe\fBTRIP_DNS_TIMEOUT\fRenvironmentvariable..RE.TP\fB\-\-dns\-ttl\fR\fI\fRThetime\-to\-live(TTL)ofDNSentries[default:300s].RSMayalsobespecifiedwiththe\fBTRIP_DNS_TTL\fRenvironmentvariable..RE.TP\fB\-z\fR,\fB\-\-dns\-lookup\-as\-info\fRLookupautonomoussystem(AS)informationduringDNSqueries[default:false].RSMayalsobespecifiedwiththe\fBTRIP_DNS_LOOKUP_AS_INFO\fRenvironmentvariable..RE.TP\fB\-s\fR,\fB\-\-max\-samples\fR\fI\fRThemaximumnumberofsamplestorecordperhop[default:256].RSMayalsobespecifiedwiththe\fBTRIP_MAX_SAMPLES\fRenvironmentvariable..RE.TP\fB\-\-max\-flows\fR\fI\fRThemaximumnumberofflowstorecord[default:64].RSMayalsobespecifiedwiththe\fBTRIP_MAX_FLOWS\fRenvironmentvariable..RE.TP\fB\-a\fR,\fB\-\-tui\-address\-mode\fR\fI\fRHowtorenderaddresses[default:host].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2ip:ShowIPaddressonly.IP\(bu2host:Showreverse\-lookupDNShostnameonly.IP\(bu2both:ShowbothIPaddressandreverse\-lookupDNShostname.RE.RSMayalsobespecifiedwiththe\fBTRIP_TUI_ADDRESS_MODE\fRenvironmentvariable..RE.TP\fB\-\-tui\-as\-mode\fR\fI\fRHowtorenderautonomoussystem(AS)information[default:asn].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2asn:ShowtheASN.IP\(bu2prefix:DisplaytheASprefix.IP\(bu2country\-code:Displaythecountrycode.IP\(bu2registry:Displaytheregistryname.IP\(bu2allocated:Displaytheallocateddate.IP\(bu2name:DisplaytheASname.RE.RSMayalsobespecifiedwiththe\fBTRIP_TUI_AS_MODE\fRenvironmentvariable..RE.TP\fB\-\-tui\-custom\-columns\fR\fI\fRCustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt].RSMayalsobespecifiedwiththe\fBTRIP_TUI_CUSTOM_COLUMNS\fRenvironmentvariable..RE.TP\fB\-\-tui\-icmp\-extension\-mode\fR\fI\fRHowtorenderICMPextensions[default:off].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2off:Donotshow`icmp`extensions.IP\(bu2mpls:ShowMPLSlabel(s)only.IP\(bu2full:Showfull`icmp`extensiondataforallknownextensions.IP\(bu2all:Showfull`icmp`extensiondataforallclasses.RE.RSMayalsobespecifiedwiththe\fBTRIP_TUI_ICMP_EXTENSION_MODE\fRenvironmentvariable..RE.TP\fB\-\-tui\-geoip\-mode\fR\fI\fRHowtorenderGeoIpinformation[default:short].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2off:DonotdisplayGeoIpdata.IP\(bu2short:Showshortformat.IP\(bu2long:Showlongformat.IP\(bu2location:ShowlatitudeandLongitudeformat.RE.RSMayalsobespecifiedwiththe\fBTRIP_TUI_GEOIP_MODE\fRenvironmentvariable..RE.TP\fB\-M\fR,\fB\-\-tui\-max\-addrs\fR\fI\fRThemaximumnumberofaddressestoshowperhop[default:auto].RSMayalsobespecifiedwiththe\fBTRIP_TUI_MAX_ADDRS\fRenvironmentvariable..RE.TP\fB\-\-tui\-preserve\-screen\fRPreservethescreenonexit[default:false].RSMayalsobespecifiedwiththe\fBTRIP_TUI_PRESERVE_SCREEN\fRenvironmentvariable..RE.TP\fB\-\-tui\-refresh\-rate\fR\fI\fRTheTUIrefreshrate[default:100ms].RSMayalsobespecifiedwiththe\fBTRIP_TUI_REFRESH_RATE\fRenvironmentvariable..RE.TP\fB\-\-tui\-privacy\-max\-ttl\fR\fI\fRThemaximumttlofhopswhichwillbemaskedforprivacy[default:none]Ifset,thesourceIPaddressandhostnamewillalsobehidden..RSMayalsobespecifiedwiththe\fBTRIP_TUI_PRIVACY_MAX_TTL\fRenvironmentvariable..RE.TP\fB\-\-tui\-locale\fR\fI\fRThelocaletousefortheTUI[default:auto].RSMayalsobespecifiedwiththe\fBTRIP_TUI_LOCALE\fRenvironmentvariable..RE.TP\fB\-\-tui\-timezone\fR\fI\fRThetimezonetousefortheTUI[default:auto]ThetimezonemustbeavalidIANAtimezoneidentifier..RSMayalsobespecifiedwiththe\fBTRIP_TUI_TIMEZONE\fRenvironmentvariable..RE.TP\fB\-\-tui\-theme\-colors\fR\fI\fRTheTUIthemecolors[item=color,item=color,..].RSMayalsobespecifiedwiththe\fBTRIP_TUI_THEME_COLORS\fRenvironmentvariable..RE.TP\fB\-\-print\-tui\-theme\-items\fRPrintallTUIthemeitemsandexit.RSMayalsobespecifiedwiththe\fBTRIP_PRINT_TUI_THEME_ITEMS\fRenvironmentvariable..RE.TP\fB\-\-tui\-key\-bindings\fR\fI\fRTheTUIkeybindings[command=key,command=key,..].RSMayalsobespecifiedwiththe\fBTRIP_TUI_KEY_BINDINGS\fRenvironmentvariable..RE.TP\fB\-\-print\-tui\-binding\-commands\fRPrintallTUIcommandsthatcanbeboundandexit.RSMayalsobespecifiedwiththe\fBTRIP_PRINT_TUI_BINDING_COMMANDS\fRenvironmentvariable..RE.TP\fB\-C\fR,\fB\-\-report\-cycles\fR\fI\fRThenumberofreportcyclestorun[default:10].RSMayalsobespecifiedwiththe\fBTRIP_REPORT_CYCLES\fRenvironmentvariable..RE.TP\fB\-G\fR,\fB\-\-geoip\-mmdb\-file\fR\fI\fRThesupportedMaxMindorIPinfoGeoIpmmdbfile.RSMayalsobespecifiedwiththe\fBTRIP_GEOIP_MMDB_FILE\fRenvironmentvariable..RE.TP\fB\-\-generate\fR\fI\fRGenerateshellcompletion.br.br[\fIpossiblevalues:\fRbash,elvish,fish,powershell,zsh].RSMayalsobespecifiedwiththe\fBTRIP_GENERATE\fRenvironmentvariable..RE.TP\fB\-\-generate\-man\fRGenerateROFFmanpage.RSMayalsobespecifiedwiththe\fBTRIP_GENERATE_MAN\fRenvironmentvariable..RE.TP\fB\-\-print\-config\-template\fRPrintatemplatetomlconfigfileandexit.RSMayalsobespecifiedwiththe\fBTRIP_PRINT_CONFIG_TEMPLATE\fRenvironmentvariable..RE.TP\fB\-\-print\-locales\fRPrintallavailableTUIlocalesandexit.RSMayalsobespecifiedwiththe\fBTRIP_PRINT_LOCALES\fRenvironmentvariable..RE.TP\fB\-\-log\-format\fR\fI\fRThedebuglogformat[default:pretty].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2compact:Displaylogdatainacompactformat.IP\(bu2pretty:Displaylogdatainaprettyformat.IP\(bu2json:Displaylogdatainajsonformat.IP\(bu2chrome:DisplaylogdatainChrometraceformat.RE.RSMayalsobespecifiedwiththe\fBTRIP_LOG_FORMAT\fRenvironmentvariable..RE.TP\fB\-\-log\-filter\fR\fI\fRThedebuglogfilter[default:trippy=debug].RSMayalsobespecifiedwiththe\fBTRIP_LOG_FILTER\fRenvironmentvariable..RE.TP\fB\-\-log\-span\-events\fR\fI\fRThedebuglogformat[default:off].br.br\fIPossiblevalues:\fR.RS14.IP\(bu2off:Donotdisplayeventspans.IP\(bu2active:Displayenterandexiteventspans.IP\(bu2full:Displayalleventspans.RE.RSMayalsobespecifiedwiththe\fBTRIP_LOG_SPAN_EVENTS\fRenvironmentvariable..RE.TP\fB\-v\fR,\fB\-\-verbose\fREnableverbosedebuglogging.RSMayalsobespecifiedwiththe\fBTRIP_VERBOSE\fRenvironmentvariable..RE.TP\fB\-h\fR,\fB\-\-help\fRPrinthelp(seeasummarywith\*(Aq\-h\*(Aq).TP\fB\-V\fR,\fB\-\-version\fRPrintversion.TP[\fITARGETS\fR]AspacedelimitedlistofhostnamesandIPstotrace.RSMayalsobespecifiedwiththe\fBTRIP_TARGETS\fRenvironmentvariable..RE.SHVERSIONv0.14.0\-dev.SHAUTHORSFujiApple ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_powershell_shell_completions.snap ================================================ --- source: crates/trippy-tui/src/print.rs --- usingnamespaceSystem.Management.AutomationusingnamespaceSystem.Management.Automation.LanguageRegister-ArgumentCompleter-Native-CommandName'trip'-ScriptBlock{param($wordToComplete,$commandAst,$cursorPosition)$commandElements=$commandAst.CommandElements$command=@('trip'for($i=1;$i-lt$commandElements.Count;$i++){$element=$commandElements[$i]if($element-isnot[StringConstantExpressionAst]-or$element.StringConstantType-ne[StringConstantType]::BareWord-or$element.Value.StartsWith('-')-or$element.Value-eq$wordToComplete){break}$element.Value})-join';'$completions=@(switch($command){'trip'{[CompletionResult]::new('-c','-c',[CompletionResultType]::ParameterName,'Configfile')[CompletionResult]::new('--config-file','--config-file',[CompletionResultType]::ParameterName,'Configfile')[CompletionResult]::new('-m','-m',[CompletionResultType]::ParameterName,'Outputmode[default:tui]')[CompletionResult]::new('--mode','--mode',[CompletionResultType]::ParameterName,'Outputmode[default:tui]')[CompletionResult]::new('-p','-p',[CompletionResultType]::ParameterName,'Tracingprotocol[default:icmp]')[CompletionResult]::new('--protocol','--protocol',[CompletionResultType]::ParameterName,'Tracingprotocol[default:icmp]')[CompletionResult]::new('-F','-F',[CompletionResultType]::ParameterName,'Theaddressfamily[default:system]')[CompletionResult]::new('--addr-family','--addr-family',[CompletionResultType]::ParameterName,'Theaddressfamily[default:system]')[CompletionResult]::new('-P','-P',[CompletionResultType]::ParameterName,'Thetargetport(TCP&UDPonly)[default:80]')[CompletionResult]::new('--target-port','--target-port',[CompletionResultType]::ParameterName,'Thetargetport(TCP&UDPonly)[default:80]')[CompletionResult]::new('-S','-S',[CompletionResultType]::ParameterName,'Thesourceport(TCP&UDPonly)[default:auto]')[CompletionResult]::new('--source-port','--source-port',[CompletionResultType]::ParameterName,'Thesourceport(TCP&UDPonly)[default:auto]')[CompletionResult]::new('-A','-A',[CompletionResultType]::ParameterName,'ThesourceIPaddress[default:auto]')[CompletionResult]::new('--source-address','--source-address',[CompletionResultType]::ParameterName,'ThesourceIPaddress[default:auto]')[CompletionResult]::new('-I','-I',[CompletionResultType]::ParameterName,'Thenetworkinterface[default:auto]')[CompletionResult]::new('--interface','--interface',[CompletionResultType]::ParameterName,'Thenetworkinterface[default:auto]')[CompletionResult]::new('-i','-i',[CompletionResultType]::ParameterName,'Theminimumdurationofeveryround[default:1s]')[CompletionResult]::new('--min-round-duration','--min-round-duration',[CompletionResultType]::ParameterName,'Theminimumdurationofeveryround[default:1s]')[CompletionResult]::new('-T','-T',[CompletionResultType]::ParameterName,'Themaximumdurationofeveryround[default:1s]')[CompletionResult]::new('--max-round-duration','--max-round-duration',[CompletionResultType]::ParameterName,'Themaximumdurationofeveryround[default:1s]')[CompletionResult]::new('-g','-g',[CompletionResultType]::ParameterName,'TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms]')[CompletionResult]::new('--grace-duration','--grace-duration',[CompletionResultType]::ParameterName,'TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms]')[CompletionResult]::new('--initial-sequence','--initial-sequence',[CompletionResultType]::ParameterName,'Theinitialsequencenumber[default:33434]')[CompletionResult]::new('-R','-R',[CompletionResultType]::ParameterName,'TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic]')[CompletionResult]::new('--multipath-strategy','--multipath-strategy',[CompletionResultType]::ParameterName,'TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic]')[CompletionResult]::new('-U','-U',[CompletionResultType]::ParameterName,'Themaximumnumberofin-flightICMPechorequests[default:24]')[CompletionResult]::new('--max-inflight','--max-inflight',[CompletionResultType]::ParameterName,'Themaximumnumberofin-flightICMPechorequests[default:24]')[CompletionResult]::new('-f','-f',[CompletionResultType]::ParameterName,'TheTTLtostartfrom[default:1]')[CompletionResult]::new('--first-ttl','--first-ttl',[CompletionResultType]::ParameterName,'TheTTLtostartfrom[default:1]')[CompletionResult]::new('-t','-t',[CompletionResultType]::ParameterName,'ThemaximumnumberofTTLhops[default:64]')[CompletionResult]::new('--max-ttl','--max-ttl',[CompletionResultType]::ParameterName,'ThemaximumnumberofTTLhops[default:64]')[CompletionResult]::new('--packet-size','--packet-size',[CompletionResultType]::ParameterName,'ThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84]')[CompletionResult]::new('--payload-pattern','--payload-pattern',[CompletionResultType]::ParameterName,'TherepeatingpatterninthepayloadoftheICMPpacket[default:0]')[CompletionResult]::new('-Q','-Q',[CompletionResultType]::ParameterName,'TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0]')[CompletionResult]::new('--tos','--tos',[CompletionResultType]::ParameterName,'TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0]')[CompletionResult]::new('--read-timeout','--read-timeout',[CompletionResultType]::ParameterName,'Thesocketreadtimeout[default:10ms]')[CompletionResult]::new('-r','-r',[CompletionResultType]::ParameterName,'HowtoperformDNSqueries[default:system]')[CompletionResult]::new('--dns-resolve-method','--dns-resolve-method',[CompletionResultType]::ParameterName,'HowtoperformDNSqueries[default:system]')[CompletionResult]::new('--dns-timeout','--dns-timeout',[CompletionResultType]::ParameterName,'ThemaximumtimetowaittoperformDNSqueries[default:5s]')[CompletionResult]::new('--dns-ttl','--dns-ttl',[CompletionResultType]::ParameterName,'Thetime-to-live(TTL)ofDNSentries[default:300s]')[CompletionResult]::new('-s','-s',[CompletionResultType]::ParameterName,'Themaximumnumberofsamplestorecordperhop[default:256]')[CompletionResult]::new('--max-samples','--max-samples',[CompletionResultType]::ParameterName,'Themaximumnumberofsamplestorecordperhop[default:256]')[CompletionResult]::new('--max-flows','--max-flows',[CompletionResultType]::ParameterName,'Themaximumnumberofflowstorecord[default:64]')[CompletionResult]::new('-a','-a',[CompletionResultType]::ParameterName,'Howtorenderaddresses[default:host]')[CompletionResult]::new('--tui-address-mode','--tui-address-mode',[CompletionResultType]::ParameterName,'Howtorenderaddresses[default:host]')[CompletionResult]::new('--tui-as-mode','--tui-as-mode',[CompletionResultType]::ParameterName,'Howtorenderautonomoussystem(AS)information[default:asn]')[CompletionResult]::new('--tui-custom-columns','--tui-custom-columns',[CompletionResultType]::ParameterName,'CustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt]')[CompletionResult]::new('--tui-icmp-extension-mode','--tui-icmp-extension-mode',[CompletionResultType]::ParameterName,'HowtorenderICMPextensions[default:off]')[CompletionResult]::new('--tui-geoip-mode','--tui-geoip-mode',[CompletionResultType]::ParameterName,'HowtorenderGeoIpinformation[default:short]')[CompletionResult]::new('-M','-M',[CompletionResultType]::ParameterName,'Themaximumnumberofaddressestoshowperhop[default:auto]')[CompletionResult]::new('--tui-max-addrs','--tui-max-addrs',[CompletionResultType]::ParameterName,'Themaximumnumberofaddressestoshowperhop[default:auto]')[CompletionResult]::new('--tui-refresh-rate','--tui-refresh-rate',[CompletionResultType]::ParameterName,'TheTUIrefreshrate[default:100ms]')[CompletionResult]::new('--tui-privacy-max-ttl','--tui-privacy-max-ttl',[CompletionResultType]::ParameterName,'Themaximumttlofhopswhichwillbemaskedforprivacy[default:none]')[CompletionResult]::new('--tui-locale','--tui-locale',[CompletionResultType]::ParameterName,'ThelocaletousefortheTUI[default:auto]')[CompletionResult]::new('--tui-timezone','--tui-timezone',[CompletionResultType]::ParameterName,'ThetimezonetousefortheTUI[default:auto]')[CompletionResult]::new('--tui-theme-colors','--tui-theme-colors',[CompletionResultType]::ParameterName,'TheTUIthemecolors[item=color,item=color,..]')[CompletionResult]::new('--tui-key-bindings','--tui-key-bindings',[CompletionResultType]::ParameterName,'TheTUIkeybindings[command=key,command=key,..]')[CompletionResult]::new('-C','-C',[CompletionResultType]::ParameterName,'Thenumberofreportcyclestorun[default:10]')[CompletionResult]::new('--report-cycles','--report-cycles',[CompletionResultType]::ParameterName,'Thenumberofreportcyclestorun[default:10]')[CompletionResult]::new('-G','-G',[CompletionResultType]::ParameterName,'ThesupportedMaxMindorIPinfoGeoIpmmdbfile')[CompletionResult]::new('--geoip-mmdb-file','--geoip-mmdb-file',[CompletionResultType]::ParameterName,'ThesupportedMaxMindorIPinfoGeoIpmmdbfile')[CompletionResult]::new('--generate','--generate',[CompletionResultType]::ParameterName,'Generateshellcompletion')[CompletionResult]::new('--log-format','--log-format',[CompletionResultType]::ParameterName,'Thedebuglogformat[default:pretty]')[CompletionResult]::new('--log-filter','--log-filter',[CompletionResultType]::ParameterName,'Thedebuglogfilter[default:trippy=debug]')[CompletionResult]::new('--log-span-events','--log-span-events',[CompletionResultType]::ParameterName,'Thedebuglogformat[default:off]')[CompletionResult]::new('-u','-u',[CompletionResultType]::ParameterName,'Tracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false]')[CompletionResult]::new('--unprivileged','--unprivileged',[CompletionResultType]::ParameterName,'Tracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false]')[CompletionResult]::new('--udp','--udp',[CompletionResultType]::ParameterName,'TraceusingtheUDPprotocol')[CompletionResult]::new('--tcp','--tcp',[CompletionResultType]::ParameterName,'TraceusingtheTCPprotocol')[CompletionResult]::new('--icmp','--icmp',[CompletionResultType]::ParameterName,'TraceusingtheICMPprotocol')[CompletionResult]::new('-4','-4',[CompletionResultType]::ParameterName,'UseIPv4only')[CompletionResult]::new('--ipv4','--ipv4',[CompletionResultType]::ParameterName,'UseIPv4only')[CompletionResult]::new('-6','-6',[CompletionResultType]::ParameterName,'UseIPv6only')[CompletionResult]::new('--ipv6','--ipv6',[CompletionResultType]::ParameterName,'UseIPv6only')[CompletionResult]::new('-e','-e',[CompletionResultType]::ParameterName,'ParseICMPextensions')[CompletionResult]::new('--icmp-extensions','--icmp-extensions',[CompletionResultType]::ParameterName,'ParseICMPextensions')[CompletionResult]::new('-y','-y',[CompletionResultType]::ParameterName,'TracetoallIPsresolvedfromDNSlookup[default:false]')[CompletionResult]::new('--dns-resolve-all','--dns-resolve-all',[CompletionResultType]::ParameterName,'TracetoallIPsresolvedfromDNSlookup[default:false]')[CompletionResult]::new('-z','-z',[CompletionResultType]::ParameterName,'Lookupautonomoussystem(AS)informationduringDNSqueries[default:false]')[CompletionResult]::new('--dns-lookup-as-info','--dns-lookup-as-info',[CompletionResultType]::ParameterName,'Lookupautonomoussystem(AS)informationduringDNSqueries[default:false]')[CompletionResult]::new('--tui-preserve-screen','--tui-preserve-screen',[CompletionResultType]::ParameterName,'Preservethescreenonexit[default:false]')[CompletionResult]::new('--print-tui-theme-items','--print-tui-theme-items',[CompletionResultType]::ParameterName,'PrintallTUIthemeitemsandexit')[CompletionResult]::new('--print-tui-binding-commands','--print-tui-binding-commands',[CompletionResultType]::ParameterName,'PrintallTUIcommandsthatcanbeboundandexit')[CompletionResult]::new('--generate-man','--generate-man',[CompletionResultType]::ParameterName,'GenerateROFFmanpage')[CompletionResult]::new('--print-config-template','--print-config-template',[CompletionResultType]::ParameterName,'Printatemplatetomlconfigfileandexit')[CompletionResult]::new('--print-locales','--print-locales',[CompletionResultType]::ParameterName,'PrintallavailableTUIlocalesandexit')[CompletionResult]::new('-v','-v',[CompletionResultType]::ParameterName,'Enableverbosedebuglogging')[CompletionResult]::new('--verbose','--verbose',[CompletionResultType]::ParameterName,'Enableverbosedebuglogging')[CompletionResult]::new('-h','-h',[CompletionResultType]::ParameterName,'Printhelp(seemorewith''--help'')')[CompletionResult]::new('--help','--help',[CompletionResultType]::ParameterName,'Printhelp(seemorewith''--help'')')[CompletionResult]::new('-V','-V',[CompletionResultType]::ParameterName,'Printversion')[CompletionResult]::new('--version','--version',[CompletionResultType]::ParameterName,'Printversion')break}})$completions.Where{$_.CompletionText-like"$wordToComplete*"}|Sort-Object-PropertyListItemText} ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_zsh_shell_completions.snap ================================================ --- source: crates/trippy-tui/src/print.rs --- #compdeftripautoload-Uis-at-least_trip(){typeset-Aopt_argstypeset-a_arguments_optionslocalret=1ifis-at-least5.2;then_arguments_options=(-s-S-C)else_arguments_options=(-s-C)filocalcontextcurcontext="$curcontext"stateline_arguments"${_arguments_options[@]}":\'-c+[Configfile]:CONFIG_FILE:_files'\'--config-file=[Configfile]:CONFIG_FILE:_files'\'-m+[Outputmode\[default\:tui\]]:MODE:((tui\:"DisplayinteractiveTUI"stream\:"Displayacontinuousstreamoftracingdata"pretty\:"GenerateaprettytexttablereportforNcycles"markdown\:"GenerateaMarkdowntexttablereportforNcycles"csv\:"GenerateaCSVreportforNcycles"json\:"GenerateaJSONreportforNcycles"dot\:"GenerateaGraphvizDOTfileforNcycles"flows\:"DisplayallflowsforNcycles"silent\:"DonotgenerateanytracingoutputforNcycles"))'\'--mode=[Outputmode\[default\:tui\]]:MODE:((tui\:"DisplayinteractiveTUI"stream\:"Displayacontinuousstreamoftracingdata"pretty\:"GenerateaprettytexttablereportforNcycles"markdown\:"GenerateaMarkdowntexttablereportforNcycles"csv\:"GenerateaCSVreportforNcycles"json\:"GenerateaJSONreportforNcycles"dot\:"GenerateaGraphvizDOTfileforNcycles"flows\:"DisplayallflowsforNcycles"silent\:"DonotgenerateanytracingoutputforNcycles"))'\'-p+[Tracingprotocol\[default\:icmp\]]:PROTOCOL:((icmp\:"InternetControlMessageProtocol"udp\:"UserDatagramProtocol"tcp\:"TransmissionControlProtocol"))'\'--protocol=[Tracingprotocol\[default\:icmp\]]:PROTOCOL:((icmp\:"InternetControlMessageProtocol"udp\:"UserDatagramProtocol"tcp\:"TransmissionControlProtocol"))'\'-F+[Theaddressfamily\[default\:system\]]:ADDR_FAMILY:((ipv4\:"IPv4only"ipv6\:"IPv6only"ipv6-then-ipv4\:"IPv6withafallbacktoIPv4"ipv4-then-ipv6\:"IPv4withafallbacktoIPv6"system\:"IftheOSresolverisbeingusedthenusethefirstIPaddressreturned,otherwiselookupIPv4withafallbacktoIPv6"))'\'--addr-family=[Theaddressfamily\[default\:system\]]:ADDR_FAMILY:((ipv4\:"IPv4only"ipv6\:"IPv6only"ipv6-then-ipv4\:"IPv6withafallbacktoIPv4"ipv4-then-ipv6\:"IPv4withafallbacktoIPv6"system\:"IftheOSresolverisbeingusedthenusethefirstIPaddressreturned,otherwiselookupIPv4withafallbacktoIPv6"))'\'-P+[Thetargetport(TCP&UDPonly)\[default\:80\]]:TARGET_PORT:_default'\'--target-port=[Thetargetport(TCP&UDPonly)\[default\:80\]]:TARGET_PORT:_default'\'-S+[Thesourceport(TCP&UDPonly)\[default\:auto\]]:SOURCE_PORT:_default'\'--source-port=[Thesourceport(TCP&UDPonly)\[default\:auto\]]:SOURCE_PORT:_default'\'(-I--interface)-A+[ThesourceIPaddress\[default\:auto\]]:SOURCE_ADDRESS:_default'\'(-I--interface)--source-address=[ThesourceIPaddress\[default\:auto\]]:SOURCE_ADDRESS:_default'\'-I+[Thenetworkinterface\[default\:auto\]]:INTERFACE:_default'\'--interface=[Thenetworkinterface\[default\:auto\]]:INTERFACE:_default'\'-i+[Theminimumdurationofeveryround\[default\:1s\]]:MIN_ROUND_DURATION:_default'\'--min-round-duration=[Theminimumdurationofeveryround\[default\:1s\]]:MIN_ROUND_DURATION:_default'\'-T+[Themaximumdurationofeveryround\[default\:1s\]]:MAX_ROUND_DURATION:_default'\'--max-round-duration=[Themaximumdurationofeveryround\[default\:1s\]]:MAX_ROUND_DURATION:_default'\'-g+[TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded\[default\:100ms\]]:GRACE_DURATION:_default'\'--grace-duration=[TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded\[default\:100ms\]]:GRACE_DURATION:_default'\'--initial-sequence=[Theinitialsequencenumber\[default\:33434\]]:INITIAL_SEQUENCE:_default'\'-R+[TheEqual-costMulti-Pathroutingstrategy(UDPonly)\[default\:classic\]]:MULTIPATH_STRATEGY:((classic\:"Thesrcordestportisusedtostorethesequencenumber"paris\:"TheUDP\`checksum\`fieldisusedtostorethesequencenumber"dublin\:"TheIP\`identifier\`fieldisusedtostorethesequencenumber"))'\'--multipath-strategy=[TheEqual-costMulti-Pathroutingstrategy(UDPonly)\[default\:classic\]]:MULTIPATH_STRATEGY:((classic\:"Thesrcordestportisusedtostorethesequencenumber"paris\:"TheUDP\`checksum\`fieldisusedtostorethesequencenumber"dublin\:"TheIP\`identifier\`fieldisusedtostorethesequencenumber"))'\'-U+[Themaximumnumberofin-flightICMPechorequests\[default\:24\]]:MAX_INFLIGHT:_default'\'--max-inflight=[Themaximumnumberofin-flightICMPechorequests\[default\:24\]]:MAX_INFLIGHT:_default'\'-f+[TheTTLtostartfrom\[default\:1\]]:FIRST_TTL:_default'\'--first-ttl=[TheTTLtostartfrom\[default\:1\]]:FIRST_TTL:_default'\'-t+[ThemaximumnumberofTTLhops\[default\:64\]]:MAX_TTL:_default'\'--max-ttl=[ThemaximumnumberofTTLhops\[default\:64\]]:MAX_TTL:_default'\'--packet-size=[ThesizeofIPpackettosend(IPheader+ICMPheader+payload)\[default\:84\]]:PACKET_SIZE:_default'\'--payload-pattern=[TherepeatingpatterninthepayloadoftheICMPpacket\[default\:0\]]:PAYLOAD_PATTERN:_default'\'-Q+[TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)\[default\:0\]]:TOS:_default'\'--tos=[TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)\[default\:0\]]:TOS:_default'\'--read-timeout=[Thesocketreadtimeout\[default\:10ms\]]:READ_TIMEOUT:_default'\'-r+[HowtoperformDNSqueries\[default\:system\]]:DNS_RESOLVE_METHOD:((system\:"ResolveusingtheOSresolver"resolv\:"Resolveusingthe\`/etc/resolv.conf\`DNSconfiguration"google\:"ResolveusingtheGoogle\`8.8.8.8\`DNSservice"cloudflare\:"ResolveusingtheCloudflare\`1.1.1.1\`DNSservice"))'\'--dns-resolve-method=[HowtoperformDNSqueries\[default\:system\]]:DNS_RESOLVE_METHOD:((system\:"ResolveusingtheOSresolver"resolv\:"Resolveusingthe\`/etc/resolv.conf\`DNSconfiguration"google\:"ResolveusingtheGoogle\`8.8.8.8\`DNSservice"cloudflare\:"ResolveusingtheCloudflare\`1.1.1.1\`DNSservice"))'\'--dns-timeout=[ThemaximumtimetowaittoperformDNSqueries\[default\:5s\]]:DNS_TIMEOUT:_default'\'--dns-ttl=[Thetime-to-live(TTL)ofDNSentries\[default\:300s\]]:DNS_TTL:_default'\'-s+[Themaximumnumberofsamplestorecordperhop\[default\:256\]]:MAX_SAMPLES:_default'\'--max-samples=[Themaximumnumberofsamplestorecordperhop\[default\:256\]]:MAX_SAMPLES:_default'\'--max-flows=[Themaximumnumberofflowstorecord\[default\:64\]]:MAX_FLOWS:_default'\'-a+[Howtorenderaddresses\[default\:host\]]:TUI_ADDRESS_MODE:((ip\:"ShowIPaddressonly"host\:"Showreverse-lookupDNShostnameonly"both\:"ShowbothIPaddressandreverse-lookupDNShostname"))'\'--tui-address-mode=[Howtorenderaddresses\[default\:host\]]:TUI_ADDRESS_MODE:((ip\:"ShowIPaddressonly"host\:"Showreverse-lookupDNShostnameonly"both\:"ShowbothIPaddressandreverse-lookupDNShostname"))'\'--tui-as-mode=[Howtorenderautonomoussystem(AS)information\[default\:asn\]]:TUI_AS_MODE:((asn\:"ShowtheASN"prefix\:"DisplaytheASprefix"country-code\:"Displaythecountrycode"registry\:"Displaytheregistryname"allocated\:"Displaytheallocateddate"name\:"DisplaytheASname"))'\'--tui-custom-columns=[CustomcolumnstobedisplayedintheTUIhopstable\[default\:holsravbwdt\]]:TUI_CUSTOM_COLUMNS:_default'\'--tui-icmp-extension-mode=[HowtorenderICMPextensions\[default\:off\]]:TUI_ICMP_EXTENSION_MODE:((off\:"Donotshow\`icmp\`extensions"mpls\:"ShowMPLSlabel(s)only"full\:"Showfull\`icmp\`extensiondataforallknownextensions"all\:"Showfull\`icmp\`extensiondataforallclasses"))'\'--tui-geoip-mode=[HowtorenderGeoIpinformation\[default\:short\]]:TUI_GEOIP_MODE:((off\:"DonotdisplayGeoIpdata"short\:"Showshortformat"long\:"Showlongformat"location\:"ShowlatitudeandLongitudeformat"))'\'-M+[Themaximumnumberofaddressestoshowperhop\[default\:auto\]]:TUI_MAX_ADDRS:_default'\'--tui-max-addrs=[Themaximumnumberofaddressestoshowperhop\[default\:auto\]]:TUI_MAX_ADDRS:_default'\'--tui-refresh-rate=[TheTUIrefreshrate\[default\:100ms\]]:TUI_REFRESH_RATE:_default'\'--tui-privacy-max-ttl=[Themaximumttlofhopswhichwillbemaskedforprivacy\[default\:none\]]:TUI_PRIVACY_MAX_TTL:_default'\'--tui-locale=[ThelocaletousefortheTUI\[default\:auto\]]:TUI_LOCALE:_default'\'--tui-timezone=[ThetimezonetousefortheTUI\[default\:auto\]]:TUI_TIMEZONE:_default'\'*--tui-theme-colors=[TheTUIthemecolors\[item=color,item=color,..\]]:TUI_THEME_COLORS:_default'\'*--tui-key-bindings=[TheTUIkeybindings\[command=key,command=key,..\]]:TUI_KEY_BINDINGS:_default'\'-C+[Thenumberofreportcyclestorun\[default\:10\]]:REPORT_CYCLES:_default'\'--report-cycles=[Thenumberofreportcyclestorun\[default\:10\]]:REPORT_CYCLES:_default'\'-G+[ThesupportedMaxMindorIPinfoGeoIpmmdbfile]:GEOIP_MMDB_FILE:_files'\'--geoip-mmdb-file=[ThesupportedMaxMindorIPinfoGeoIpmmdbfile]:GEOIP_MMDB_FILE:_files'\'--generate=[Generateshellcompletion]:GENERATE:(bashelvishfishpowershellzsh)'\'--log-format=[Thedebuglogformat\[default\:pretty\]]:LOG_FORMAT:((compact\:"Displaylogdatainacompactformat"pretty\:"Displaylogdatainaprettyformat"json\:"Displaylogdatainajsonformat"chrome\:"DisplaylogdatainChrometraceformat"))'\'--log-filter=[Thedebuglogfilter\[default\:trippy=debug\]]:LOG_FILTER:_default'\'--log-span-events=[Thedebuglogformat\[default\:off\]]:LOG_SPAN_EVENTS:((off\:"Donotdisplayeventspans"active\:"Displayenterandexiteventspans"full\:"Displayalleventspans"))'\'-u[Tracewithoutrequiringelevatedprivilegesonsupportedplatforms\[default\:false\]]'\'--unprivileged[Tracewithoutrequiringelevatedprivilegesonsupportedplatforms\[default\:false\]]'\'(-p--protocol--tcp--icmp)--udp[TraceusingtheUDPprotocol]'\'(-p--protocol--udp--icmp)--tcp[TraceusingtheTCPprotocol]'\'(-p--protocol--udp--tcp)--icmp[TraceusingtheICMPprotocol]'\'(-6--ipv6-F--addr-family)-4[UseIPv4only]'\'(-6--ipv6-F--addr-family)--ipv4[UseIPv4only]'\'(-4--ipv4-F--addr-family)-6[UseIPv6only]'\'(-4--ipv4-F--addr-family)--ipv6[UseIPv6only]'\'-e[ParseICMPextensions]'\'--icmp-extensions[ParseICMPextensions]'\'-y[TracetoallIPsresolvedfromDNSlookup\[default\:false\]]'\'--dns-resolve-all[TracetoallIPsresolvedfromDNSlookup\[default\:false\]]'\'-z[Lookupautonomoussystem(AS)informationduringDNSqueries\[default\:false\]]'\'--dns-lookup-as-info[Lookupautonomoussystem(AS)informationduringDNSqueries\[default\:false\]]'\'--tui-preserve-screen[Preservethescreenonexit\[default\:false\]]'\'--print-tui-theme-items[PrintallTUIthemeitemsandexit]'\'--print-tui-binding-commands[PrintallTUIcommandsthatcanbeboundandexit]'\'--generate-man[GenerateROFFmanpage]'\'--print-config-template[Printatemplatetomlconfigfileandexit]'\'--print-locales[PrintallavailableTUIlocalesandexit]'\'-v[Enableverbosedebuglogging]'\'--verbose[Enableverbosedebuglogging]'\'-h[Printhelp(seemorewith'\''--help'\'')]'\'--help[Printhelp(seemorewith'\''--help'\'')]'\'-V[Printversion]'\'--version[Printversion]'\'*::targets--AspacedelimitedlistofhostnamesandIPstotrace:_default'\&&ret=0}(($+functions[_trip_commands]))||_trip_commands(){localcommands;commands=()_describe-tcommands'tripcommands'commands"$@"}if["$funcstack[1]"="_trip"];then_trip"$@"elsecompdef_triptripfi ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@tui_binding_commands_match.snap ================================================ --- source: crates/trippy-tui/src/print.rs --- TUIbindingcommands:toggle-help,toggle-help-alt,toggle-settings,toggle-settings-tui,toggle-settings-trace,toggle-settings-dns,toggle-settings-geoip,toggle-settings-bindings,toggle-settings-theme,toggle-settings-columns,next-hop,previous-hop,next-trace,previous-trace,next-hop-address,previous-hop-address,address-mode-ip,address-mode-host,address-mode-both,toggle-freeze,toggle-chart,toggle-map,toggle-flows,toggle-privacy,expand-privacy,contract-privacy,expand-hosts,expand-hosts-max,contract-hosts,contract-hosts-min,chart-zoom-in,chart-zoom-out,clear-trace-data,clear-dns-cache,clear-selection,toggle-as-info,toggle-hop-details,quit,quit-preserve-screen ================================================ FILE: crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@tui_theme_items_match.snap ================================================ --- source: crates/trippy-tui/src/print.rs --- TUIthemecoloritems:bg-color,border-color,text-color,tab-text-color,hops-table-header-bg-color,hops-table-header-text-color,hops-table-row-active-text-color,hops-table-row-inactive-text-color,hops-chart-selected-color,hops-chart-unselected-color,hops-chart-axis-color,frequency-chart-bar-color,frequency-chart-text-color,flows-chart-bar-selected-color,flows-chart-bar-unselected-color,flows-chart-text-current-color,flows-chart-text-non-current-color,samples-chart-color,samples-chart-lost-color,help-dialog-bg-color,help-dialog-text-color,settings-tab-text-color,settings-dialog-bg-color,settings-table-header-text-color,settings-table-header-bg-color,settings-table-row-text-color,map-world-color,map-radius-color,map-selected-color,map-info-panel-border-color,map-info-panel-bg-color,map-info-panel-text-color,info-bar-bg-color,info-bar-text-color ================================================ FILE: deny.toml ================================================ [licenses] version = 2 allow = ["Apache-2.0", "MIT", "Unicode-DFS-2016", "ISC", "BSD-2-Clause", "BSD-3-Clause", "WTFPL", "Unicode-3.0", "Zlib"] confidence-threshold = 0.8 exceptions = [] [advisories] version = 2 db-path = "~/.cargo/advisory-db" db-urls = ["https://github.com/rustsec/advisory-db"] ignore = [ # allow unmaintained paste crate { id = "RUSTSEC-2024-0436" }, ] ================================================ FILE: docs/.gitignore ================================================ # build output dist/ # generated types .astro/ # dependencies node_modules/ # logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # environment variables .env .env.production # macOS-specific files .DS_Store ================================================ FILE: docs/README.md ================================================ # Starlight Starter Kit: Basics [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) ``` npm create astro@latest -- --template starlight ``` [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics) [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! ## 🚀 Project Structure Inside of your Astro + Starlight project, you'll see the following folders and files: ``` . ├── public/ ├── src/ │ ├── assets/ │ ├── content/ │ │ ├── docs/ │ │ └── config.ts │ └── env.d.ts ├── astro.config.mjs ├── package.json └── tsconfig.json ``` Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. Images can be added to `src/assets/` and embedded in Markdown with a relative link. Static assets, like favicons, can be placed in the `public/` directory. ## 🧞 Commands All commands are run from the root of the project, from a terminal: | Command | Action | | :------------------------ | :----------------------------------------------- | | `npm install` | Installs dependencies | | `npm run dev` | Starts local dev server at `localhost:4321` | | `npm run build` | Build your production site to `./dist/` | | `npm run preview` | Preview your build locally, before deploying | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | | `npm run astro -- --help` | Get help using the Astro CLI | ## 👀 Want to learn more? Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). ================================================ FILE: docs/astro.config.mjs ================================================ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import starlightVersions from 'starlight-versions' // https://astro.build/config export default defineConfig({ site: 'https://trippy.rs', integrations: [ starlight({ plugins: [ starlightVersions({ versions: [{ slug: '0.12.2' }, { slug: '0.13.0' }], }), ], title: 'Trippy', customCss: [ // Relative path to your custom CSS file './src/styles/custom.css', ], editLink: { baseUrl: 'https://github.com/fujiapple852/trippy/edit/master/docs/', }, logo: { light: './src/assets/Trippy-Horizontal.svg', dark: './src/assets/Trippy-Horizontal-DarkMode.svg', replacesTitle: true, }, head: [ { tag: 'link', attrs: { rel: 'apple-touch-icon', href: '/apple-touch-icon.png', }, }, { tag: 'script', attrs: { defer: true, src: 'https://cloud.umami.is/script.js', 'data-website-id': '02e6fe53-a5b1-4f2a-b3e6-87124b1b276b', 'data-astro-rerun': true } } ], social: [ { icon: 'github', label: 'github', href: 'https://github.com/fujiapple852/trippy' }, { icon: 'zulip', label: 'zulip', href: 'https://trippy.zulipchat.com' }, { icon: 'matrix', label: 'matrix', href: 'https://matrix.to/#/#trippy-dev:matrix.org' }, { icon: 'x.com', label: 'x.com', href: 'https://x.com/FujiApple852v2' }, ], sidebar: [ { label: 'Start Here', autogenerate: { directory: 'start' } }, { label: 'Guides', autogenerate: { directory: 'guides' } }, { label: 'Reference', autogenerate: { directory: 'reference' }, }, { label: 'Development', autogenerate: { directory: 'development' }, }, ], }), ], }); ================================================ FILE: docs/package.json ================================================ { "name": "trippy", "type": "module", "version": "0.0.1", "scripts": { "dev": "astro dev", "start": "astro dev", "build": "astro build", "preview": "astro preview", "astro": "astro" }, "dependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.8.1", "sharp": "^0.32.5", "starlight-versions": "^0.5.3" } } ================================================ FILE: docs/public/CNAME ================================================ trippy.rs ================================================ FILE: docs/src/content/config.ts ================================================ import { defineCollection } from 'astro:content'; import { docsSchema } from '@astrojs/starlight/schema'; import { docsVersionsLoader } from 'starlight-versions/loader' export const collections = { docs: defineCollection({ schema: docsSchema() }), versions: defineCollection({ loader: docsVersionsLoader() }), }; ================================================ FILE: docs/src/content/docs/0.12.2/development/crates.md ================================================ --- title: Crates description: A reference for the Trippy crates. slug: 0.12.2/development/crates --- The following table lists the crates that are provided by Trippy. See [crates](crates/README.md) for more information. | Crate | Description | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | [trippy](https://crates.io/crates/trippy) | A binary crate for the Trippy application and a library crate | | [trippy-core](https://crates.io/crates/trippy-core) | A library crate providing the core Trippy tracing functionality | | [trippy-packet](https://crates.io/crates/trippy-packet) | A library crate which provides packet wire formats and packet parsing functionality | | [trippy-dns](https://crates.io/crates/trippy-dns) | A library crate for performing forward and reverse lazy DNS resolution | | [trippy-privilege](https://crates.io/crates/trippy-privilege) | A library crate for discovering platform privileges | | [trippy-tui](https://crates.io/crates/trippy-tui) | A library crate for the Trippy terminal user interface | ================================================ FILE: docs/src/content/docs/0.12.2/guides/faq.md ================================================ --- title: Frequently Asked Questions description: Frequently asked questions about Trippy. sidebar: order: 5 slug: 0.12.2/guides/faq --- ## Why does Trippy show "Awaiting data..."? :::caution If you are using Windows you _must_ [configure](/guides/windows_firewall) the Windows Defender firewall to allow incoming ICMP traffic ::: When Trippy shows “Awaiting data...” it means that it has received zero responses for the probes sent in a trace. This indicates that either probes are not being sent or, more typically, responses are not being received. Check that local and network firewalls allow ICMP traffic and that the system `traceroute` (or `tracert.exe` on Windows) works as expected. Note that on Windows, even if `tracert.exe` works as expected, you _must_ [configure](/guides/windows_firewall) the Windows Defender firewall to allow incoming ICMP traffic. For deeper diagnostics you can run tools such as https://www.wireshark.org and https://www.tcpdump.org to verify that icmp requests and responses are being send and received. ================================================ FILE: docs/src/content/docs/0.12.2/guides/privileges.md ================================================ --- title: Privileges description: A reference for the Trippy privileges. sidebar: order: 2 slug: 0.12.2/guides/privileges --- Trippy normally requires elevated privileges due to the use of raw sockets. Enabling the required privileges for your platform can be achieved in several ways, as outlined below. Trippy can also be used without elevated privileged on certain platforms, with some limitations. ## Unix 1: Run as `root` user via `sudo`: ```shell sudo trip example.com ``` 2: `chown` `trip` as the `root` user and set the `setuid` bit: ```shell sudo chown root $(which trip) && sudo chmod +s $(which trip) ``` 3: [Linux only] Set the `CAP_NET_RAW` capability: ```shell sudo setcap CAP_NET_RAW+p $(which trip) ``` :::note Trippy is a capability aware application and will add `CAP_NET_RAW` to the effective set if it is present in the allowed set. Trippy will drop all capabilities after creating the raw sockets. ::: ## Windows Trippy must be run with Administrator privileges on Windows. ## Unprivileged mode Trippy allows running in an unprivileged mode for all tracing modes (`ICMP`, `UDP` and `TCP`) on platforms which support that feature. :::note Unprivileged mode is currently only supported on macOS. Linux support is possible and may be added in the future. Unprivileged mode is not supported on NetBSD, FreeBSD or Windows as these platforms do not support the `IPPROTO_ICMP` socket type. See [#101](https://github.com/fujiapple852/trippy/issues/101) for further information. ::: The unprivileged mode can be enabled by adding the `--unprivileged` (`-u`) command line flag or by adding the `unprivileged` entry in the `trippy` section of the [configuration file](/0.12.2/reference/configuration): ```toml [trippy] unprivileged = true ``` :::note The `paris` and `dublin` `ECMP` strategies are not supported in unprivileged mode as these require manipulating the `UDP` and `IP` and headers which in turn requires the use of a raw socket. ::: ================================================ FILE: docs/src/content/docs/0.12.2/guides/recommendation.md ================================================ --- title: Recommended Tracing Settings description: Recommended settings for Trippy. sidebar: order: 3 slug: 0.12.2/guides/recommendation --- Trippy provides a variety of configurable features which can be used to perform different types of analysis. The choice of settings will depend on the analysis you wish to perform and the environment in which you are working. This guide lists some common options along with some basic guidance on when they might be appropriate. :::note The Windows `tracert` tool uses ICMP by default, whereas most Unix `traceroute` tools use UDP by default. ::: ## ICMP By default Trippy will run an ICMP trace to the target. This will typically produce a consistent path to the target (a single flow) for each round of tracing which makes it easy to read and analyse. This is a useful mode for general network troubleshooting. However, many routers are configured to rate-limit ICMP traffic which can make it difficult to get an accurate picture of packet loss. In addition, ICMP traffic is not typically subject to ECMP routing and so may not reflect the path that would taken by other protocols such as UDP and TCP. To run a simple ICMP trace: ```shell trip example.com ``` Due to the rate-limiting of ICMP traffic, some people prefer to hide the `Loss%` and `Recv` columns in the Tui as these are easy to misinterpret. ```shell trip example.com --tui-custom-columns hosavbwdt ``` These settings can be made permanent by adding them to the Trippy configuration file: ```toml [tui] custom-columns = "hosavbwdt" ``` :::note The `Sts` column shows different color codes to reflect packet loss at intermediate vs the target hop, see the [Column Reference](/0.12.2/reference/column) for more information. ::: #### UDP/Dublin with fixed ports UDP tracing provides a more realistic view of the path taken by traffic that is subject to ECMP routing. Setting a fixed target port in the range 33434-33534 may allow Trippy to determine that the probe has reached the target as many routers and firewalls are configured to allow UDP probes in that range and will respond with a Destination Unreachable response. However, running a UDP trace with a fixed target port and a variable source port will typically result in different paths being followed for each probe within each round of tracing. This can make it difficult to interpret the output as different hosts will reply for a given hop (time-to-live) across rounds. By using the `dublin` ECMP strategy, which encodes the sequence number in the IP `identifier` field, Trippy can fix both the source and target ports, typically resulting in a _single_ path for each probe within each round of tracing. :::note UDP/Dublin for IPv6 encodes the sequence number as the payload length as the IP `identifier` field is not available in IPv6. ::: :::note Keep in mind that every probe is an _independent trial_ and each may traverse a completely different path. In practice, ICMP probes often follow a single path, whereas the path of UDP and TCP probes is typically determined by the 5-tuple of protocol, source and destination IP addresses and ports. Also beware that the return path may not be the same as the forward path, and may also differ for each probe. Strategies such as `dublin` and `paris` assist in controlling the path taken by the forward probes, but do not help control the return path. Therefore it is recommended to run a trace in both directions to get a complete picture. ::: To run a UDP trace with fixed source and target ports using the `dublin` ECMP strategy: ```shell trip example.com --udp --multipath-strategy dublin --source-port 5000 --target-port 33434 ``` :::note The source port can be any valid port number, but the target port should usually be in the range 33434-33534 or whatever range is open to UDP probes on the target host. ::: These settings can be made permanent by adding them to the Trippy configuration file: ```toml [strategy] protocol = "udp" multipath-strategy = "dublin" source-port = 5000 target-port = 33434 ``` ## UDP/Dublin with fixed target port and variable source port As an extension to the above, if you do not fix the source port when using the `dublin` ECMP strategy, Trippy will vary the source port per _round_ of tracing (i.e. each probe within a given round will share the same source port, and the source port will vary for each round). This will typically result in the _same_ path being followed for _each_ probe within a given round, but _different_ paths being followed for each round. These individual flows can be explored in the Trippy Tui by pressing the `toggle-flows` key binding (`f` key by default). Adding the columns `Seq`, `Sprt` and `Dprt` to the Tui will show the sequence number, source port and destination port respectively which makes this easier to visualize. ```shell trip example.com --udp --multipath-strategy dublin --target-port 33434 --tui-custom-columns holsravbwdtSPQ ``` These settings can be made permanent by adding them to the Trippy configuration file: ```toml [strategy] protocol = "udp" multipath-strategy = "dublin" target-port = 33434 [tui] custom-columns = "holsravbwdtSPQ" ``` To make the flows easier to visualize, you can generate a Graphviz DOT file report of all tracing flows: ```shell trip example.com --udp --multipath-strategy dublin --target-port 33434 -m dot -C 5 ``` ## UDP/Paris UDP with the `paris` ECMP strategy offers the same benefits as the `dublin` strategy with fixed ports and can be used in the same way. They differ in the way they encode the sequence number in the probe. The `dublin` strategy uses the IP `identifier` field, whereas the `paris` strategy uses the UDP `checksum` field. To run a UDP trace with fixed source and target ports using the `paris` ECMP strategy: ```shell trip example.com --udp --multipath-strategy paris --source-port 5000 --target-port 33434 ``` The `paris` strategy does not work behind NAT as the UDP `checksum` field is typically modified by NAT devices. Therefore the `dublin` strategy is recommended when NAT is present. :::note Trippy can detect the presence of NAT devices in some circumstances when using the `dublin` strategy and the `Nat` column can be shown in the Tui to indicate when NAT is detected. See the [Column Reference](/0.12.2/reference/column) for more information. ::: #### TCP TCP tracing is similar to UDP tracing in that it provides a more realistic view of the path taken by traffic that is subject to ECMP routing. TCP tracing defaults to using a target port of 80 and sets the source port as the sequence number which will typically result in a different path being followed for each probe within each round of tracing. To run a TCP trace: ```shell trip example.com --tcp ``` TCP tracing is useful for diagnosing issues with TCP connections and higher layer protocols such as HTTP. Often UDP tracing can be used in place of TCP to diagnose IP layer network issues and, as it provides ways to control the path taken by the probes, it is often preferred. :::note Trippy does not support the `dublin` or `paris` ECMP strategies for TCP tracing and so you cannot fix both the source and target ports. See the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details. ::: ================================================ FILE: docs/src/content/docs/0.12.2/guides/usage.md ================================================ --- title: Usage Examples description: Examples of how to use the Trippy command line interface. sidebar: order: 1 slug: 0.12.2/guides/usage --- Basic usage with default parameters: ```shell trip example.com ``` Trace without requiring elevated privileges (supported platforms only, see [privileges](/0.12.2/guides/privileges)): ```shell trip example.com --unprivileged ``` Trace using the `udp` (or `tcp` or `icmp`) protocol (also aliases `--icmp`, `--udp` & `--tcp`): ```shell trip example.com -p udp ``` Trace to multiple targets simultaneously (`icmp` protocol only, see [#72](https://github.com/fujiapple852/trippy/issues/72)): ```shell trip example.com google.com crates.io ``` Trace with a minimum round time of `250ms` and a grace period of `50ms`: ```shell trip example.com -i 250ms -g 50ms ``` Trace with a custom first and maximum `time-to-live`: ```shell trip example.com --first-ttl 2 --max-ttl 10 ``` Use custom destination port `443` for `tcp` tracing: ```shell trip example.com -p tcp -P 443 ``` Use custom source port `5000` for `udp` tracing: ```shell trip example.com -p udp -S 5000 ``` Use the `dublin` (or `paris`) ECMP routing strategy for `udp` with fixed source and destination ports: ```shell trip example.com -p udp -R dublin -S 5000 -P 3500 ``` Trace with a custom source address: ```shell trip example.com -p tcp -A 127.0.0.1 ``` Trace with a source address determined by the IPv4 address for interface `en0`: ```shell trip example.com -p tcp -I en0 ``` Trace using `IPv6`: ```shell trip example.com -6 ``` Trace using `ipv4-then-ipv6` fallback (or `ipv6-then-ipv4` or `ipv4` or `ipv6`): ```shell trip example.com --addr-family ipv4-then-ipv6 ``` Generate a `json` (or `csv`, `pretty`, `markdown`) tracing report with 5 rounds of data: ```shell trip example.com -m json -C 5 ``` Generate a [Graphviz](https://graphviz.org) `DOT` file report of all tracing flows for a TCP trace after 5 rounds: ```shell trip example.com --tcp -m dot -C 5 ``` Generate a textual report of all tracing flows for a UDP trace after 5 rounds: ```shell trip example.com --udp -m flows -C 5 ``` Perform DNS queries using the `google` DNS resolver (or `cloudflare`, `system`, `resolv`): ```shell trip example.com -r google ``` Lookup [AS][autonomous_system] information for all discovered IP addresses (not yet available for the `system` resolver, see [#66](https://github.com/fujiapple852/trippy/issues/66)): ```shell trip example.com -r google -z ``` Set the reverse DNS lookup cache time-to-live to be 60 seconds: ```shell trip example.com --dns-ttl 60sec ``` Lookup and display `short` (or `long` or `location` or `off`) GeoIp information from a `mmdb` file: ```shell trip example.com --geoip-mmdb-file GeoLite2-City.mmdb --tui-geoip-mode short ``` Parse `icmp` extensions: ```shell trip example.com -e ``` Hide the IP address, hostname and GeoIp for the first two hops: ```shell trip example.com --tui-privacy-max-ttl 2 ``` Customize Tui columns (see [Column Reference](/0.12.2/reference/column)): ```shell trip example.com --tui-custom-columns holsravbwdt ``` Customize the color theme: ```shell trip example.com --tui-theme-colors bg-color=blue,text-color=ffff00 ``` List all Tui items that can have a custom color theme: ```shell trip --print-tui-theme-items ``` Customize the key bindings: ```shell trip example.com --tui-key-bindings previous-hop=k,next-hop=j,quit=shift-q ``` List all Tui commands that can have a custom key binding: ```shell trip --print-tui-binding-commands ``` Specify the location of the Trippy config file: ```shell trip example.com --config-file /path/to/trippy.toml ``` Generate a template configuration file: ```shell trip --print-config-template > trippy.toml ``` Generate `bash` shell completions (or `fish`, `powershell`, `zsh`, `elvish`): ```shell trip --generate bash ``` Generate `ROFF` man page: ```shell trip --generate-man ``` Use the `de` Tui locale: ```shell trip example.com --tui-locale de ``` List supported Tui locales: ```shell trip --print-locales ``` Run in `silent` tracing mode and output `compact` trace logging with `full` span events: ```shell trip example.com -m silent -v --log-format compact --log-span-events full ``` ================================================ FILE: docs/src/content/docs/0.12.2/guides/windows_firewall.md ================================================ --- title: Windows Defender Firewall description: Allow incoming ICMP traffic in the Windows Defender firewall. sidebar: order: 4 slug: 0.12.2/guides/windows_firewall --- The Windows Defender firewall rule can be created using PowerShell. ```shell New-NetFirewallRule -DisplayName "ICMPv4 Trippy Allow" -Name ICMPv4_TRIPPY_ALLOW -Protocol ICMPv4 -Action Allow New-NetFirewallRule -DisplayName "ICMPv6 Trippy Allow" -Name ICMPv6_TRIPPY_ALLOW -Protocol ICMPv6 -Action Allow ``` The rules can be enabled as follows: ```shell Enable-NetFirewallRule ICMPv4_TRIPPY_ALLOW Enable-NetFirewallRule ICMPv6_TRIPPY_ALLOW ``` The rules can be disabled as follows: ```shell Disable-NetFirewallRule ICMPv4_TRIPPY_ALLOW Disable-NetFirewallRule ICMPv6_TRIPPY_ALLOW ``` There is a [step-by-step guide to manually configure the Windows Defender firewall rule](https://github.com/fujiapple852/trippy/issues/578#issuecomment-1565149826). ================================================ FILE: docs/src/content/docs/0.12.2/index.mdx ================================================ --- title: "Trippy: a network diagnostic tool" description: a network diagnostic tool. template: splash hero: tagline: Trippy combines the functionality of traceroute and ping and is designed to assist with the analysis of networking issues. image: alt: Trippy, man! light: ../../../assets/0.12.2/Trippy-Emblem.svg dark: ../../../assets/0.12.2/Trippy-Emblem-DarkMode.svg actions: - text: Get Started link: /0.12.2/start/getting-started/ icon: right-arrow - text: Read the docs link: /0.12.2/reference/cli/ icon: open-book variant: secondary - text: View on GitHub link: https://github.com/fujiapple852/trippy icon: github variant: secondary slug: 0.12.2 --- import { Card, CardGrid } from '@astrojs/starlight/components'; import { Icon } from '@astrojs/starlight/components'; - `ICMP`, `UDP` & `TCP` over `IPv4` & `IPv6` protocols - Fully customizable tracing options - `dublin` and `paris` `ECMP` strategies - `ICMP` extensions objects (i.e. `MPLS`) - Reverse `DNS` and `ASN` lookups - `NAT` detection ![Trippy main screen](../../../assets/0.12.2/main_screen.png) - Lookup GeoIp information and show on world map - Support for both `MaxMind` and `IPinfo` databases ![Trippy GeoIp world map](../../../assets/0.12.2/world_map.png) - Runs on `Linux`, `macOS`, `Windows`, `*BSD` - Supports `x86_64`, `aarch64`, `arm7` architectures - Available from most native package managers - Run in unprivileged mode ![Trippy on Windows](../../../assets/0.12.2/windows.png) - Customizable columns, color themes and key bindings - Hop detail navigation mode - Hop privacy mode - Show individual tracing flows - Various charts and statistics - Persist configuration to file ![Trippy settings](../../../assets/0.12.2/settings.png) TUI available in 10 languages: - Chinese 🇨🇳, English 🇺🇸, French 🇫🇷, German 🇩🇪, Italian 🇮🇹, Portuguese 🇵🇹, Russian 🇷🇺, Spanish 🇪🇸, Swedish 🇸🇪 and Turkish 🇹🇷 ![Trippy main screen in Chinese](../../../assets/0.12.2/help_screen_zh.png) ================================================ FILE: docs/src/content/docs/0.12.2/reference/bindings.md ================================================ --- title: Key Bindings Reference description: A reference for customizing the Trippy TUI key bindings. sidebar: order: 3 slug: 0.12.2/reference/bindings --- The following table lists the default Tui command key bindings. These can be overridden with the `--tui-key-bindings` command line option or in the `bindings` section of the configuration file. | Command | Description | Default | | -------------------------- | ----------------------------------------------- | --------- | | `toggle-help` | Toggle help | `h` | | `toggle-help-alt` | Toggle help (alternative binding) | `?` | | `toggle-settings` | Toggle settings | `s` | | `toggle-settings-tui` | Open settings (Tui tab) | `1` | | `toggle-settings-trace` | Open settings (Trace tab) | `2` | | `toggle-settings-dns` | Open settings (Dns tab) | `3` | | `toggle-settings-geoip` | Open settings (GeoIp tab) | `4` | | `toggle-settings-bindings` | Open settings (Bindings tab) | `5` | | `toggle-settings-theme` | Open settings (Theme tab) | `6` | | `toggle-settings-columns` | Open settings (Columns tab) | `7` | | `next-hop` | Select next hop | `down` | | `previous-hop` | Select previous hop | `up` | | `next-trace` | Select next trace | `right` | | `previous-trace` | Select previous trace | `left` | | `next-hop-address` | Select next hop address | `.` | | `previous-hop-address` | Select previous hop address | `,` | | `address-mode-ip` | Show IP address only | `i` | | `address-mode-host` | Show hostname only | `n` | | `address-mode-both` | Show both IP address and hostname | `b` | | `toggle-freeze` | Toggle freezing the display | `ctrl+f` | | `toggle-chart` | Toggle the chart | `c` | | `toggle-map` | Toggle the GeoIp map | `m` | | `toggle-flows` | Toggle the flows | `f` | | `expand-privacy` | Expand hop privacy | `p` | | `contract-privacy` | Contract hop privacy | `o` | | `expand-hosts` | Expand the hosts shown per hop | `]` | | `expand-hosts-max` | Expand the hosts shown per hop to the maximum | `}` | | `contract-hosts` | Contract the hosts shown per hop | `[` | | `contract-hosts-min` | Contract the hosts shown per hop to the minimum | `{` | | `chart-zoom-in` | Zoom in the chart | `=` | | `chart-zoom-out` | Zoom out the chart | `-` | | `clear-trace-data` | Clear all trace data | `ctrl+r` | | `clear-dns-cache` | Flush the DNS cache | `ctrl+k` | | `clear-selection` | Clear the current selection | `esc` | | `toggle-as-info` | Toggle AS info display | `z` | | `toggle-hop-details` | Toggle hop details | `d` | | `quit` | Quit the application | `q` | | `quit-preserve-screen` | Quit the application and preserve the screen | `shift+q` | The supported modifiers are: `shift`, `ctrl`, `alt`, `super`, `hyper` & `meta`. Multiple modifiers may be specified, for example `ctrl+shift+b`. ================================================ FILE: docs/src/content/docs/0.12.2/reference/cli.md ================================================ --- title: CLI Reference description: A reference for the Trippy command line interface. sidebar: order: 1 slug: 0.12.2/reference/cli --- ```text A network diagnostic tool Usage: trip [OPTIONS] [TARGETS]... Arguments: [TARGETS]... A space delimited list of hostnames and IPs to trace Options: -c, --config-file Config file -m, --mode Output mode [default: tui] Possible values: - tui: Display interactive TUI - stream: Display a continuous stream of tracing data - pretty: Generate a pretty text table report for N cycles - markdown: Generate a Markdown text table report for N cycles - csv: Generate a CSV report for N cycles - json: Generate a JSON report for N cycles - dot: Generate a Graphviz DOT file for N cycles - flows: Display all flows for N cycles - silent: Do not generate any tracing output for N cycles -u, --unprivileged Trace without requiring elevated privileges on supported platforms [default: false] -p, --protocol Tracing protocol [default: icmp] Possible values: - icmp: Internet Control Message Protocol - udp: User Datagram Protocol - tcp: Transmission Control Protocol --udp Trace using the UDP protocol --tcp Trace using the TCP protocol --icmp Trace using the ICMP protocol -F, --addr-family The address family [default: Ipv4thenIpv6] Possible values: - ipv4: IPv4 only - ipv6: IPv6 only - ipv6-then-ipv4: IPv6 with a fallback to IPv4 - ipv4-then-ipv6: IPv4 with a fallback to IPv6 -4, --ipv4 Use IPv4 only -6, --ipv6 Use IPv6 only -P, --target-port The target port (TCP & UDP only) [default: 80] -S, --source-port The source port (TCP & UDP only) [default: auto] -A, --source-address The source IP address [default: auto] -I, --interface The network interface [default: auto] -i, --min-round-duration The minimum duration of every round [default: 1s] -T, --max-round-duration The maximum duration of every round [default: 1s] -g, --grace-duration The period of time to wait for additional ICMP responses after the target has responded [default: 100ms] --initial-sequence The initial sequence number [default: 33434] -R, --multipath-strategy The Equal-cost Multi-Path routing strategy (UDP only) [default: classic] Possible values: - classic: The src or dest port is used to store the sequence number - paris: The UDP `checksum` field is used to store the sequence number - dublin: The IP `identifier` field is used to store the sequence number -U, --max-inflight The maximum number of in-flight ICMP echo requests [default: 24] -f, --first-ttl The TTL to start from [default: 1] -t, --max-ttl The maximum number of TTL hops [default: 64] --packet-size The size of IP packet to send (IP header + ICMP header + payload) [default: 84] --payload-pattern The repeating pattern in the payload of the ICMP packet [default: 0] -Q, --tos The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) [default: 0] -e, --icmp-extensions Parse ICMP extensions --read-timeout The socket read timeout [default: 10ms] -r, --dns-resolve-method How to perform DNS queries [default: system] Possible values: - system: Resolve using the OS resolver - resolv: Resolve using the `/etc/resolv.conf` DNS configuration - google: Resolve using the Google `8.8.8.8` DNS service - cloudflare: Resolve using the Cloudflare `1.1.1.1` DNS service -y, --dns-resolve-all Trace to all IPs resolved from DNS lookup [default: false] --dns-timeout The maximum time to wait to perform DNS queries [default: 5s] --dns-ttl The time-to-live (TTL) of DNS entries [default: 300s] -z, --dns-lookup-as-info Lookup autonomous system (AS) information during DNS queries [default: false] -s, --max-samples The maximum number of samples to record per hop [default: 256] --max-flows The maximum number of flows to record [default: 64] -a, --tui-address-mode How to render addresses [default: host] Possible values: - ip: Show IP address only - host: Show reverse-lookup DNS hostname only - both: Show both IP address and reverse-lookup DNS hostname --tui-as-mode How to render autonomous system (AS) information [default: asn] Possible values: - asn: Show the ASN - prefix: Display the AS prefix - country-code: Display the country code - registry: Display the registry name - allocated: Display the allocated date - name: Display the AS name --tui-custom-columns Custom columns to be displayed in the TUI hops table [default: holsravbwdt] --tui-icmp-extension-mode How to render ICMP extensions [default: off] Possible values: - off: Do not show `icmp` extensions - mpls: Show MPLS label(s) only - full: Show full `icmp` extension data for all known extensions - all: Show full `icmp` extension data for all classes --tui-geoip-mode How to render GeoIp information [default: short] Possible values: - off: Do not display GeoIp data - short: Show short format - long: Show long format - location: Show latitude and Longitude format -M, --tui-max-addrs The maximum number of addresses to show per hop [default: auto] --tui-preserve-screen Preserve the screen on exit [default: false] --tui-refresh-rate The TUI refresh rate [default: 100ms] --tui-privacy-max-ttl The maximum ttl of hops which will be masked for privacy [default: none] If set, the source IP address and hostname will also be hidden. --tui-locale The locale to use for the TUI [default: auto] --tui-theme-colors The TUI theme colors [item=color,item=color,..] --print-tui-theme-items Print all TUI theme items and exit --tui-key-bindings The TUI key bindings [command=key,command=key,..] --print-tui-binding-commands Print all TUI commands that can be bound and exit -C, --report-cycles The number of report cycles to run [default: 10] -G, --geoip-mmdb-file The supported MaxMind or IPinfo GeoIp mmdb file --generate Generate shell completion [possible values: bash, elvish, fish, powershell, zsh] --generate-man Generate ROFF man page --print-config-template Print a template toml config file and exit --print-locales Print all available TUI locales and exit --log-format The debug log format [default: pretty] Possible values: - compact: Display log data in a compact format - pretty: Display log data in a pretty format - json: Display log data in a json format - chrome: Display log data in Chrome trace format --log-filter The debug log filter [default: trippy=debug] --log-span-events The debug log format [default: off] Possible values: - off: Do not display event spans - active: Display enter and exit event spans - full: Display all event spans -v, --verbose Enable verbose debug logging -h, --help Print help (see a summary with '-h') -V, --version Print version ``` :::note Trippy command line arguments may be given in any order and my occur both before and after the targets. ::: ================================================ FILE: docs/src/content/docs/0.12.2/reference/column.md ================================================ --- title: Column Reference description: A reference for customizing the Trippy TUI columns. sidebar: order: 4 slug: 0.12.2/reference/column --- The following table lists the columns that are available for display in the Tui. These can be overridden with the `--tui-custom-columns` command line option or in the `tui-custom-columns` attribute in the `tui` section of the configuration file. | Column | Code | Description | | -------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `#` | `h` | The time-to-live (TTL) for the hop | | `Host` | `o` | The hostname(s) and IP address(s) for the host(s) for the hop
May include AS info, GeoIp and ICMP extensions
Shows full hop details in hop detail navigation mode | | `Loss%` | `l` | The packet loss % for the hop | | `Snd` | `s` | The number of probes sent for the hop | | `Recv` | `r` | The number of probe responses received for the hop | | `Last` | `a` | The round-trip-time (RTT) of the last probe for the hop | | `Avg` | `v` | The average RTT of all probes for the hop | | `Best` | `b` | The best RTT of all probes for the hop | | `Wrst` | `w` | The worst RTT of all probes for the hop | | `StDev` | `d` | The standard deviation of all probes for the hop | | `Sts` | `t` | The status for the hop:
- 🟢 Healthy hop
- 🔵 Non-target hop with packet loss (does not necessarily indicate a problem)
- 🟤 Non-target hop is unresponsive (does not necessarily indicate a problem)
- 🟡 Target hop with packet loss (likely indicates a problem)
- 🔴 Target hop is unresponsive (likely indicates a problem) | | `Jttr` | `j` | The round-trip-time (RTT) difference between consecutive rounds for the hop | | `Javg` | `g` | The average jitter of all probes for the hop | | `Jmax` | `x` | The maximum jitter of all probes for the hop | | `Jint` | `i` | The smoothed jitter value of all probes for the hop | | `Seq` | `Q` | The sequence number for the last probe for the hop | | `Sprt` | `S` | The source port for the last probe for the hop | | `Dprt` | `P` | The destination port for the last probe for the hop | | `Type` | `T` | The icmp packet type for the last probe for the hop:
- TE: TimeExceeded
- ER: EchoReply
- DU: DestinationUnreachable
- NA: NotApplicable | | `Code` | `C` | The icmp packet code for the last probe for the hop | | `Nat` | `N` | The NAT detection status for the hop | | `Fail` | `f` | The number of probes which failed to send for the hop | | `Floss` | `F` | A _heuristic_ for the number of probes with _forward loss_ for the hop | | `Bloss` | `B` | A _heuristic_ for the number of probes with _backward loss_ for the hop | | `Floss%` | `D` | The _forward loss_ % for the hop | The default columns are `holsravbwdt`. :::note The columns will be shown in the order specified in the configuration. ::: ================================================ FILE: docs/src/content/docs/0.12.2/reference/configuration.md ================================================ --- title: Configuration Reference description: A reference for customizing the Trippy configuration. sidebar: order: 2 slug: 0.12.2/reference/configuration --- Trippy can be configured with via command line arguments or an optional configuration file. If a given configuration item is specified in both the configuration file and via a command line argument then the latter will take precedence. The configuration file location may be provided to Trippy via the `-c` (`--config-file`) argument. If not provided, Trippy will attempt to locate a `trippy.toml` or `.trippy.toml` configuration file in one of the following locations: - The current directory - The user home directory - the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config` - the Windows data directory (Windows only): `%APPDATA%` A template configuration file for [0.12.2](https://github.com/fujiapple852/trippy/blob/0.12.2/trippy-config-sample.toml) is available to download, or can be generated with the following command: ```shell trip --print-config-template > trippy.toml ``` ================================================ FILE: docs/src/content/docs/0.12.2/reference/locale.md ================================================ --- title: Locale Reference description: A reference for customizing the Trippy TUI locale. sidebar: order: 6 badge: text: New variant: note slug: 0.12.2/reference/locale --- The following table lists the supported locales for the Tui. These can be overridden with the `--tui-locale` command line option or in the `tui-locale` attribute in the `tui` section of the configuration file. | Locale | Language | Region | | ------ | ---------- | ------ | | `zh` | Chinese | all | | `en` | English | all | | `fr` | French | all | | `de` | German | all | | `it` | Italian | all | | `pt` | Portuguese | all | | `ru` | Russian | all | | `es` | Spanish | all | | `sv` | Swedish | all | | `tr` | Turkish | all | :::note If you are able to help validate translations for Trippy, or if you wish to add translations for any additional languages, please see the [tracking issue](https://github.com/fujiapple852/trippy/issues/506) for details of how to contribute. ::: ================================================ FILE: docs/src/content/docs/0.12.2/reference/theme.md ================================================ --- title: Theme Reference description: A reference for customizing the Trippy TUI theme. sidebar: order: 5 slug: 0.12.2/reference/theme --- The following table lists the default Tui color theme. These can be overridden with the `--tui-theme-colors` command line option or in the `theme-colors` section of the configuration file. | Item | Description | Default | | ------------------------------------ | --------------------------------------------------------- | ------------ | | `bg-color` | The default background color | `Black` | | `border-color` | The default color of borders | `Gray` | | `text-color` | The default color of text | `Gray` | | `tab-text-color` | The color of the text in traces tabs | `Green` | | `hops-table-header-bg-color` | The background color of the hops table header | `White` | | `hops-table-header-text-color` | The color of text in the hops table header | `Black` | | `hops-table-row-active-text-color` | The color of text of active rows in the hops table | `Gray` | | `hops-table-row-inactive-text-color` | The color of text of inactive rows in the hops table | `DarkGray` | | `hops-chart-selected-color` | The color of the selected series in the hops chart | `Green` | | `hops-chart-unselected-color` | The color of the unselected series in the hops chart | `Gray` | | `hops-chart-axis-color` | The color of the axis in the hops chart | `DarkGray` | | `frequency-chart-bar-color` | The color of bars in the frequency chart | `Green` | | `frequency-chart-text-color` | The color of text in the bars of the frequency chart | `Gray` | | `flows-chart-bar-selected-color` | The color of the selected flow bar in the flows chart | `Green` | | `flows-chart-bar-unselected-color` | The color of the unselected flow bar in the flows chart | `DarkGray` | | `flows-chart-text-current-color` | The color of the current flow text in the flows chart | `LightGreen` | | `flows-chart-text-non-current-color` | The color of the non-current flow text in the flows chart | `White` | | `samples-chart-color` | The color of the samples chart | `Yellow` | | `samples-chart-lost-color` | The color of the samples chart for lost probes | `Red` | | `help-dialog-bg-color` | The background color of the help dialog | `Blue` | | `help-dialog-text-color` | The color of the text in the help dialog | `Gray` | | `settings-dialog-bg-color` | The background color of the settings dialog | `blue` | | `settings-tab-text-color` | The color of the text in settings dialog tabs | `green` | | `settings-table-header-text-color` | The color of text in the settings table header | `black` | | `settings-table-header-bg-color` | The background color of the settings table header | `white` | | `settings-table-row-text-color` | The color of text of rows in the settings table | `gray` | | `map-world-color` | The color of the map world diagram | `white` | | `map-radius-color` | The color of the map accuracy radius circle | `yellow` | | `map-selected-color` | The color of the map selected item box | `green` | | `map-info-panel-border-color` | The color of border of the map info panel | `gray` | | `map-info-panel-bg-color` | The background color of the map info panel | `black` | | `map-info-panel-text-color` | The color of text in the map info panel | `gray` | | `info-bar-bg-color` | The background color of the information bar | `white` | | `info-bar-text-color` | The color of text in the information bar | `black` | The supported [ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) are: - `Black`, `Red`, `Green`, `Yellow`, `Blue`, `Magenta`, `Cyan`, `Gray`, `DarkGray`, `LightRed`, `LightGreen`, `LightYellow`, `LightBlue`, `LightMagenta`, `LightCyan`, `White` In addition, CSS [named colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color) (i.e. SkyBlue) and raw hex values (i.e. ffffff) may be used but note that these are only supported on some platforms and terminals and may not render correctly elsewhere. Color names are case-insensitive and may contain dashes. ================================================ FILE: docs/src/content/docs/0.12.2/reference/version.md ================================================ --- title: Version Reference description: A reference for the Trippy versions. sidebar: order: 7 slug: 0.12.2/reference/version --- The following table lists this versions of Trippy that are available and links to the corresponding release note and documentation: | Version | Release Date | Status | Release Note | Documentation | | ---------- | ------------ | ----------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | | 0.13.0-dev | 2025-05-05 | Development | n/a | [docs](https://trippy.rs) | | 0.12.2 | 2025-01-03 | Current | [note](https://github.com/fujiapple852/trippy/releases/tag/0.12.2) | [docs](https://trippy.rs/0.12.2) | | 0.11.0 | 2024-08-11 | Previous | [note](https://github.com/fujiapple852/trippy/releases/tag/0.11.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.11.0) | | 0.10.0 | 2024-03-31 | Previous | [note](https://github.com/fujiapple852/trippy/releases/tag/0.10.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.10.0) | | 0.9.0 | 2023-11-30 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.9.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.9.0) | | 0.8.0 | 2023-05-15 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.8.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.8.0) | | 0.7.0 | 2023-03-25 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.7.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.7.0) | | 0.6.0 | 2022-08-19 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.6.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.6.0) | :::note Only the _latest patch versions_ of both the _current_ and _previous_ releases of Trippy are supported. ::: ================================================ FILE: docs/src/content/docs/0.12.2/start/features.md ================================================ --- title: Features description: Learn about the features of Trippy. sidebar: order: 3 slug: 0.12.2/start/features --- - Trace using multiple protocols: - `ICMP`, `UDP` & `TCP` - `IPv4` & `IPv6` - Customizable tracing options: - packet size & payload pattern - start and maximum time-to-live (TTL) - minimum and maximum round duration - round end grace period & maximum number of unknown hops - source & destination port (`TCP` & `UDP`) - source address and source interface - `TOS` (aka `DSCP + ECN`) - Support for `classic`, `paris` and `dublin` [Equal Cost Multi-path Routing](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) strategies ([tracking issue](https://github.com/fujiapple852/trippy/issues/274)) - RFC4884 [ICMP Multi-Part Messages](https://datatracker.ietf.org/doc/html/rfc4884) - Generic Extension Objects - MPLS Label Stacks - Unprivileged mode - NAT detection - Tui interface: - Trace multiple targets simultaneously from a single instance of Trippy - Per hop stats (sent, received, loss%, last, avg, best, worst, stddev, jitter & status) - Per hop round-trip-time (RTT) history and frequency distributing charts - Interactive chart of RTT for all hops in a trace with zooming capability - Interactive GeoIp world map - Isolate and filter by individual tracing flows - Customizable color theme & key bindings - Customizable column order and visibility - Configuration via both command line arguments and a configuration file - Show multiple hosts per hop with ability to cap display to N hosts and show frequency % - Show hop details and navigate hosts within each hop - Freeze/unfreeze the Tui, reset the stats, flush the cache, preserve screen on exit - Responsive UI with adjustable refresh rate - Hop privacy - Multiple language support - DNS: - Use system, external (Google `8.8.8.8` or Cloudflare `1.1.1.1`) or custom resolver - Lazy reverse DNS queries - Lookup [autonomous system][autonomous_system] number (ASN) and name - GeoIp: - Lookup and display GeoIp information from local [MaxMind](https://www.maxmind.com) and [IPinfo](https://ipinfo.io) `mmdb` files - Generate tracing reports: - `json`, `csv` & tabular (pretty-printed and markdown) - Tracing `flows` report - Graphviz `dot` charts - configurable reporting cycles - Runs on multiple platform (macOS, Linux, Windows, NetBSD, FreeBSD, OpenBSD) - Capabilities aware application (Linux only) ================================================ FILE: docs/src/content/docs/0.12.2/start/getting-started.mdx ================================================ --- title: Getting Started description: Get started with Trippy. sidebar: order: 1 slug: 0.12.2/start/getting-started --- import { Steps } from '@astrojs/starlight/components'; The following steps will guide you through the process of installing and running Trippy. 1. Install Trippy: Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled binaries, or source. For example, to install Trippy from `cargo`: ```shell cargo install trippy --locked ``` See the [installation guide](/0.12.2/start/installation) for details of how to install Trippy on your system. 2. Run Trippy: To run a basic trace to `example.com` with default settings, use the following command: ```shell sudo trip example.com ``` See the [usage examples](/0.12.2/guides/usage) and the [CLI reference](/0.12.2/reference/cli) for details of how to use Trippy. To use Trippy without elevated privileges, see the [privileges guide](/0.12.2/guides/privileges). 3. Customize the key bindings, theme and columns: See the [key bindings reference](/0.12.2/reference/bindings), [theme reference](/0.12.2/reference/theme) and [column reference](/0.12.2/reference/column) for details of how to customize the appearance and behavior of Trippy. These settings can be made permanent by adding them to the Trippy configuration file, see the [configuration reference](/0.12.2/reference/configuration) for details. 4. Review the tracing recommendations: To get the most out of Trippy, review the [recommended tracing settings](/0.12.2/guides/recommendation) for guidance on how to configure Trippy for different types of analysis. Happy tracing! ================================================ FILE: docs/src/content/docs/0.12.2/start/installation.md ================================================ --- title: Installation description: Install Trippy on your platform. sidebar: order: 2 slug: 0.12.2/start/installation --- The following sections provide instructions for installing Trippy on your platform. Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled binaries, or source. ## Distributions Trippy is available for a variety of platforms and package managers. ### Cargo [![Crates.io](https://img.shields.io/crates/v/trippy)](https://crates.io/crates/trippy/0.12.2) ```shell cargo install trippy --locked ``` ### APT (Debian) [![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy) ```shell apt install trippy ``` :::note Only available for Debian 13 (`trixie`) and later. ::: ### PPA (Ubuntu) [![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.12.2-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages) ```shell add-apt-repository ppa:fujiapple/trippy apt update && apt install trippy ``` :::note Only available for Ubuntu 24.04 (`Noble`) and 22.04 (`Jammy`). ::: ### Snap (Linux) [![trippy](https://snapcraft.io/trippy/badge.svg)](https://snapcraft.io/trippy) ```shell snap install trippy ``` ### Homebrew (macOS) [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/trippy.svg)](https://formulae.brew.sh/formula/trippy) ```shell brew install trippy ``` ### WinGet (Windows) [![winget package](https://img.shields.io/badge/WinGet-0.12.2-brightgreen)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/f/FujiApple/Trippy/0.12.2) ```shell winget install trippy ``` ### Scoop (Windows) [![Scoop package](https://img.shields.io/scoop/v/trippy?style=flat&labelColor=5c5c5c&color=%234dc71f)](https://github.com/ScoopInstaller/Main/blob/master/bucket/trippy.json) ```shell scoop install trippy ``` ### Chocolatey (Windows) [![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy) ```shell choco install trippy ``` ### NetBSD [![pkgsrc current package](https://repology.org/badge/version-for-repo/pkgsrc_current/trippy.svg)](https://pkgsrc.se/net/trippy) ```shell pkgin install trippy ``` ### FreeBSD [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/trippy.svg)](https://www.freshports.org/net/trippy/) ```shell pkg install trippy ``` ### OpenBSD [![OpenBSD port](https://repology.org/badge/version-for-repo/openbsd/trippy.svg)](https://openports.pl/path/net/trippy) ```shell pkg_add trippy ``` ### Arch Linux [![Arch package](https://repology.org/badge/version-for-repo/arch/trippy.svg)](https://archlinux.org/packages/extra/x86_64/trippy) ```shell pacman -S trippy ``` ### Gentoo Linux [![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/trippy.svg)](https://packages.gentoo.org/packages/net-analyzer/trippy) ```shell emerge -av net-analyzer/trippy ``` ### Nix [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/trippy.svg)](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/tr/trippy/package.nix) ```shell nix-env -iA trippy ``` ### Docker [![Docker Image Version (latest by date)](https://img.shields.io/docker/v/fujiapple/trippy)](https://hub.docker.com/r/fujiapple/trippy/) ```shell docker run -it fujiapple/trippy ``` ### All Repositories [![Packaging status](https://repology.org/badge/vertical-allrepos/trippy.svg)](https://repology.org/project/trippy/versions) ## Downloads Download the latest release for your platform. | OS | Arch | Env | Current (0.12.2) | Previous (0.11.0) | Previous (0.10.0) | | ------- | --------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Linux | `x86_64` | `gnu` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-linux-gnu.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-linux-gnu.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-x86_64-unknown-linux-gnu.tar.gz) | | Linux | `x86_64` | `musl` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-linux-musl.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-linux-musl.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-x86_64-unknown-linux-musl.tar.gz) | | Linux | `aarch64` | `gnu` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-unknown-linux-gnu.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-unknown-linux-gnu.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-aarch64-unknown-linux-gnu.tar.gz) | | Linux | `aarch64` | `musl` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-unknown-linux-musl.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-unknown-linux-musl.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-aarch64-unknown-linux-musl.tar.gz) | | Linux | `arm7` | `gnueabihf` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-armv7-unknown-linux-gnueabihf.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-armv7-unknown-linux-gnueabihf.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-armv7-unknown-linux-gnueabihf.tar.gz) | | Linux | `arm7` | `musleabi` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-armv7-unknown-linux-musleabi.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-armv7-unknown-linux-musleabi.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-armv7-unknown-linux-musleabi.tar.gz) | | Linux | `arm7` | `musleabihf` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-armv7-unknown-linux-musleabihf.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-armv7-unknown-linux-musleabihf.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-armv7-unknown-linux-musleabihf.tar.gz) | | macOS | `x86_64` | `darwin` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-apple-darwin.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-apple-darwin.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-x86_64-apple-darwin.tar.gz) | | macOS | `aarch64` | `darwin` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-apple-darwin.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-apple-darwin.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-aarch64-apple-darwin.tar.gz) | | Windows | `x86_64` | `msvc` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-pc-windows-msvc.zip) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-pc-windows-msvc.zip) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-x86_64-pc-windows-msvc.zip) | | Windows | `x86_64` | `gnu` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-pc-windows-gnu.zip) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-pc-windows-gnu.zip) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-x86_64-pc-windows-gnu.zip) | | Windows | `aarch64` | `msvc` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-pc-windows-msvc.zip) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-pc-windows-msvc.zip) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-aarch64-pc-windows-msvc.zip) | | FreeBSD | `x86_64` | n/a | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-freebsd.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-freebsd.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-x86_64-unknown-freebsd.tar.gz) | | NetBSD | `x86_64` | n/a | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-netbsd.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-netbsd.tar.gz) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-x86_64-unknown-netbsd.tar.gz) | | RPM | `x86_64` | `gnu` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64.rpm) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64.rpm) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy-0.10.0-x86_64.rpm) | | Debian | `x86_64` | `gnu` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy_x86_64-unknown-linux-gnu_0.12.2_amd64.deb) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy_x86_64-unknown-linux-gnu_0.11.0_amd64.deb) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy_x86_64-unknown-linux-gnu_0.10.0_amd64.deb) | | Debian | `x86_64` | `musl` | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy_x86_64-unknown-linux-musl_0.12.2_amd64.deb) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy_x86_64-unknown-linux-musl_0.11.0_amd64.deb) | [0.10.0](https://github.com/fujiapple852/trippy/releases/download/0.10.0/trippy_x86_64-unknown-linux-musl_0.10.0_amd64.deb) | ================================================ FILE: docs/src/content/docs/0.13.0/development/crates.md ================================================ --- title: Crates description: A reference for the Trippy crates. slug: 0.13.0/development/crates --- The following table lists the crates that are provided by Trippy. See [crates](crates/README.md) for more information. | Crate | Description | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | [trippy](https://crates.io/crates/trippy) | A binary crate for the Trippy application and a library crate | | [trippy-core](https://crates.io/crates/trippy-core) | A library crate providing the core Trippy tracing functionality | | [trippy-packet](https://crates.io/crates/trippy-packet) | A library crate which provides packet wire formats and packet parsing functionality | | [trippy-dns](https://crates.io/crates/trippy-dns) | A library crate for performing forward and reverse lazy DNS resolution | | [trippy-privilege](https://crates.io/crates/trippy-privilege) | A library crate for discovering platform privileges | | [trippy-tui](https://crates.io/crates/trippy-tui) | A library crate for the Trippy terminal user interface | ================================================ FILE: docs/src/content/docs/0.13.0/guides/faq.md ================================================ --- title: Frequently Asked Questions description: Frequently asked questions about Trippy. sidebar: order: 5 slug: 0.13.0/guides/faq --- ## Why does Trippy show "Awaiting data..."? :::caution If you are using Windows you _must_ [configure](/0.13.0/guides/windows_firewall) the Windows Defender firewall to allow incoming ICMP traffic ::: When Trippy shows “Awaiting data...” it means that it has received zero responses for the probes sent in a trace. This indicates that either probes are not being sent or, more typically, responses are not being received. Check that local and network firewalls allow ICMP traffic and that the system `traceroute` (or `tracert.exe` on Windows) works as expected. Note that on Windows, even if `tracert.exe` works as expected, you _must_ [configure](/0.13.0/guides/windows_firewall) the Windows Defender firewall to allow incoming ICMP traffic. For deeper diagnostics you can run tools such as https://www.wireshark.org and https://www.tcpdump.org to verify that icmp requests and responses are being send and received. ================================================ FILE: docs/src/content/docs/0.13.0/guides/privileges.md ================================================ --- title: Privileges description: A reference for the Trippy privileges. sidebar: order: 2 slug: 0.13.0/guides/privileges --- Trippy normally requires elevated privileges due to the use of raw sockets. Enabling the required privileges for your platform can be achieved in several ways, as outlined below. Trippy can also be used without elevated privileged on certain platforms, with some limitations. ## Unix 1: Run as `root` user via `sudo`: ```shell sudo trip example.com ``` 2: `chown` `trip` as the `root` user and set the `setuid` bit: ```shell sudo chown root $(which trip) && sudo chmod +s $(which trip) ``` 3: \[Linux only] Set the `CAP_NET_RAW` capability: ```shell sudo setcap CAP_NET_RAW+p $(which trip) ``` :::note Trippy is a capability aware application and will add `CAP_NET_RAW` to the effective set if it is present in the allowed set. Trippy will drop all capabilities after creating the raw sockets. ::: ## Windows Trippy must be run with Administrator privileges on Windows. ## Unprivileged mode Trippy allows running in an unprivileged mode for all tracing modes (`ICMP`, `UDP` and `TCP`) on platforms which support that feature. :::note Unprivileged mode is currently only supported on macOS. Linux support is possible and may be added in the future. Unprivileged mode is not supported on NetBSD, FreeBSD or Windows as these platforms do not support the `IPPROTO_ICMP` socket type. See [#101](https://github.com/fujiapple852/trippy/issues/101) for further information. ::: The unprivileged mode can be enabled by adding the `--unprivileged` (`-u`) command line flag or by adding the `unprivileged` entry in the `trippy` section of the [configuration file](/0.13.0/reference/configuration): ```toml [trippy] unprivileged = true ``` :::note The `paris` and `dublin` `ECMP` strategies are not supported in unprivileged mode as these require manipulating the `UDP` and `IP` and headers which in turn requires the use of a raw socket. ::: ================================================ FILE: docs/src/content/docs/0.13.0/guides/recommendation.md ================================================ --- title: Recommended Tracing Settings description: Recommended settings for Trippy. sidebar: order: 3 slug: 0.13.0/guides/recommendation --- Trippy provides a variety of configurable features which can be used to perform different types of analysis. The choice of settings will depend on the analysis you wish to perform and the environment in which you are working. This guide lists some common options along with some basic guidance on when they might be appropriate. :::note The Windows `tracert` tool uses ICMP by default, whereas most Unix `traceroute` tools use UDP by default. ::: ## ICMP By default Trippy will run an ICMP trace to the target. This will typically produce a consistent path to the target (a single flow) for each round of tracing which makes it easy to read and analyse. This is a useful mode for general network troubleshooting. However, many routers are configured to rate-limit ICMP traffic which can make it difficult to get an accurate picture of packet loss. In addition, ICMP traffic is not typically subject to ECMP routing and so may not reflect the path that would taken by other protocols such as UDP and TCP. To run a simple ICMP trace: ```shell trip example.com ``` Due to the rate-limiting of ICMP traffic, some people prefer to hide the `Loss%` and `Recv` columns in the Tui as these are easy to misinterpret. ```shell trip example.com --tui-custom-columns hosavbwdt ``` These settings can be made permanent by adding them to the Trippy configuration file: ```toml [tui] custom-columns = "hosavbwdt" ``` :::note The `Sts` column shows different color codes to reflect packet loss at intermediate vs the target hop, see the [Column Reference](/0.13.0/reference/column) for more information. ::: #### UDP/Dublin with fixed ports UDP tracing provides a more realistic view of the path taken by traffic that is subject to ECMP routing. Setting a fixed target port in the range 33434-33534 may allow Trippy to determine that the probe has reached the target as many routers and firewalls are configured to allow UDP probes in that range and will respond with a Destination Unreachable response. However, running a UDP trace with a fixed target port and a variable source port will typically result in different paths being followed for each probe within each round of tracing. This can make it difficult to interpret the output as different hosts will reply for a given hop (time-to-live) across rounds. By using the `dublin` ECMP strategy, which encodes the sequence number in the IP `identifier` field, Trippy can fix both the source and target ports, typically resulting in a _single_ path for each probe within each round of tracing. :::note UDP/Dublin for IPv6 encodes the sequence number as the payload length as the IP `identifier` field is not available in IPv6. ::: :::note Keep in mind that every probe is an _independent trial_ and each may traverse a completely different path. In practice, ICMP probes often follow a single path, whereas the path of UDP and TCP probes is typically determined by the 5-tuple of protocol, source and destination IP addresses and ports. Also beware that the return path may not be the same as the forward path, and may also differ for each probe. Strategies such as `dublin` and `paris` assist in controlling the path taken by the forward probes, but do not help control the return path. Therefore it is recommended to run a trace in both directions to get a complete picture. ::: To run a UDP trace with fixed source and target ports using the `dublin` ECMP strategy: ```shell trip example.com --udp --multipath-strategy dublin --source-port 5000 --target-port 33434 ``` :::note The source port can be any valid port number, but the target port should usually be in the range 33434-33534 or whatever range is open to UDP probes on the target host. ::: These settings can be made permanent by adding them to the Trippy configuration file: ```toml [strategy] protocol = "udp" multipath-strategy = "dublin" source-port = 5000 target-port = 33434 ``` ## UDP/Dublin with fixed target port and variable source port As an extension to the above, if you do not fix the source port when using the `dublin` ECMP strategy, Trippy will vary the source port per _round_ of tracing (i.e. each probe within a given round will share the same source port, and the source port will vary for each round). This will typically result in the _same_ path being followed for _each_ probe within a given round, but _different_ paths being followed for each round. These individual flows can be explored in the Trippy Tui by pressing the `toggle-flows` key binding (`f` key by default). Adding the columns `Seq`, `Sprt` and `Dprt` to the Tui will show the sequence number, source port and destination port respectively which makes this easier to visualize. ```shell trip example.com --udp --multipath-strategy dublin --target-port 33434 --tui-custom-columns holsravbwdtSPQ ``` These settings can be made permanent by adding them to the Trippy configuration file: ```toml [strategy] protocol = "udp" multipath-strategy = "dublin" target-port = 33434 [tui] custom-columns = "holsravbwdtSPQ" ``` To make the flows easier to visualize, you can generate a Graphviz DOT file report of all tracing flows: ```shell trip example.com --udp --multipath-strategy dublin --target-port 33434 -m dot -C 5 ``` ## UDP/Paris UDP with the `paris` ECMP strategy offers the same benefits as the `dublin` strategy with fixed ports and can be used in the same way. They differ in the way they encode the sequence number in the probe. The `dublin` strategy uses the IP `identifier` field, whereas the `paris` strategy uses the UDP `checksum` field. To run a UDP trace with fixed source and target ports using the `paris` ECMP strategy: ```shell trip example.com --udp --multipath-strategy paris --source-port 5000 --target-port 33434 ``` The `paris` strategy does not work behind NAT as the UDP `checksum` field is typically modified by NAT devices. Therefore the `dublin` strategy is recommended when NAT is present. :::note Trippy can detect the presence of NAT devices in some circumstances when using the `dublin` strategy and the `Nat` column can be shown in the Tui to indicate when NAT is detected. See the [Column Reference](/0.13.0/reference/column) for more information. ::: #### TCP TCP tracing is similar to UDP tracing in that it provides a more realistic view of the path taken by traffic that is subject to ECMP routing. TCP tracing defaults to using a target port of 80 and sets the source port as the sequence number which will typically result in a different path being followed for each probe within each round of tracing. To run a TCP trace: ```shell trip example.com --tcp ``` TCP tracing is useful for diagnosing issues with TCP connections and higher layer protocols such as HTTP. Often UDP tracing can be used in place of TCP to diagnose IP layer network issues and, as it provides ways to control the path taken by the probes, it is often preferred. :::note Trippy does not support the `dublin` or `paris` ECMP strategies for TCP tracing and so you cannot fix both the source and target ports. See the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details. ::: ================================================ FILE: docs/src/content/docs/0.13.0/guides/usage.md ================================================ --- title: Usage Examples description: Examples of how to use the Trippy command line interface. sidebar: order: 1 slug: 0.13.0/guides/usage --- Basic usage with default parameters: ```shell trip example.com ``` Trace without requiring elevated privileges (supported platforms only, see [privileges](/0.13.0/guides/privileges)): ```shell trip example.com --unprivileged ``` Trace using the `udp` (or `tcp` or `icmp`) protocol (also aliases `--icmp`, `--udp` & `--tcp`): ```shell trip example.com -p udp ``` Trace to multiple targets simultaneously (`icmp` protocol only, see [#72](https://github.com/fujiapple852/trippy/issues/72)): ```shell trip example.com google.com crates.io ``` Trace with a minimum round time of `250ms` and a grace period of `50ms`: ```shell trip example.com -i 250ms -g 50ms ``` Trace with a custom first and maximum `time-to-live`: ```shell trip example.com --first-ttl 2 --max-ttl 10 ``` Use custom destination port `443` for `tcp` tracing: ```shell trip example.com -p tcp -P 443 ``` Use custom source port `5000` for `udp` tracing: ```shell trip example.com -p udp -S 5000 ``` Use the `dublin` (or `paris`) ECMP routing strategy for `udp` with fixed source and destination ports: ```shell trip example.com -p udp -R dublin -S 5000 -P 3500 ``` Trace with a custom source address: ```shell trip example.com -p tcp -A 127.0.0.1 ``` Trace with a source address determined by the IPv4 address for interface `en0`: ```shell trip example.com -p tcp -I en0 ``` Trace using `IPv6`: ```shell trip example.com -6 ``` Trace using `ipv4-then-ipv6` fallback (or `ipv6-then-ipv4` or `ipv4` or `ipv6`): ```shell trip example.com --addr-family ipv4-then-ipv6 ``` Generate a `json` (or `csv`, `pretty`, `markdown`) tracing report with 5 rounds of data: ```shell trip example.com -m json -C 5 ``` Generate a [Graphviz](https://graphviz.org) `DOT` file report of all tracing flows for a TCP trace after 5 rounds: ```shell trip example.com --tcp -m dot -C 5 ``` Generate a textual report of all tracing flows for a UDP trace after 5 rounds: ```shell trip example.com --udp -m flows -C 5 ``` Perform DNS queries using the `google` DNS resolver (or `cloudflare`, `system`, `resolv`): ```shell trip example.com -r google ``` Lookup \[AS]\[autonomous\_system] information for all discovered IP addresses (not yet available for the `system` resolver, see [#66](https://github.com/fujiapple852/trippy/issues/66)): ```shell trip example.com -r google -z ``` Set the reverse DNS lookup cache time-to-live to be 60 seconds: ```shell trip example.com --dns-ttl 60sec ``` Lookup and display `short` (or `long` or `location` or `off`) GeoIp information from a `mmdb` file: ```shell trip example.com --geoip-mmdb-file GeoLite2-City.mmdb --tui-geoip-mode short ``` Parse `icmp` extensions: ```shell trip example.com -e ``` Hide the IP address, hostname and GeoIp for the first two hops: ```shell trip example.com --tui-privacy-max-ttl 2 ``` Customize Tui columns (see [Column Reference](/0.13.0/reference/column)): ```shell trip example.com --tui-custom-columns holsravbwdt ``` Customize the color theme: ```shell trip example.com --tui-theme-colors bg-color=blue,text-color=ffff00 ``` List all Tui items that can have a custom color theme: ```shell trip --print-tui-theme-items ``` Customize the key bindings: ```shell trip example.com --tui-key-bindings previous-hop=k,next-hop=j,quit=shift-q ``` List all Tui commands that can have a custom key binding: ```shell trip --print-tui-binding-commands ``` Specify the location of the Trippy config file: ```shell trip example.com --config-file /path/to/trippy.toml ``` Generate a template configuration file: ```shell trip --print-config-template > trippy.toml ``` Generate `bash` shell completions (or `fish`, `powershell`, `zsh`, `elvish`): ```shell trip --generate bash ``` Generate `ROFF` man page: ```shell trip --generate-man ``` Use the `de` Tui locale: ```shell trip example.com --tui-locale de ``` List supported Tui locales: ```shell trip --print-locales ``` Set the Tui timezone to `UTC`: ```shell trip example.com --tui-timezone UTC ``` Run in `silent` tracing mode and output `compact` trace logging with `full` span events: ```shell trip example.com -m silent -v --log-format compact --log-span-events full ``` ================================================ FILE: docs/src/content/docs/0.13.0/guides/windows_firewall.md ================================================ --- title: Windows Defender Firewall description: Allow incoming ICMP traffic in the Windows Defender firewall. sidebar: order: 4 slug: 0.13.0/guides/windows_firewall --- The Windows Defender firewall rule can be created using PowerShell. ```shell New-NetFirewallRule -DisplayName "ICMPv4 Trippy Allow" -Name ICMPv4_TRIPPY_ALLOW -Protocol ICMPv4 -Action Allow New-NetFirewallRule -DisplayName "ICMPv6 Trippy Allow" -Name ICMPv6_TRIPPY_ALLOW -Protocol ICMPv6 -Action Allow ``` The rules can be enabled as follows: ```shell Enable-NetFirewallRule ICMPv4_TRIPPY_ALLOW Enable-NetFirewallRule ICMPv6_TRIPPY_ALLOW ``` The rules can be disabled as follows: ```shell Disable-NetFirewallRule ICMPv4_TRIPPY_ALLOW Disable-NetFirewallRule ICMPv6_TRIPPY_ALLOW ``` There is a [step-by-step guide to manually configure the Windows Defender firewall rule](https://github.com/fujiapple852/trippy/issues/578#issuecomment-1565149826). ================================================ FILE: docs/src/content/docs/0.13.0/index.mdx ================================================ --- title: "Trippy: a network diagnostic tool" description: a network diagnostic tool. template: splash hero: tagline: Trippy combines the functionality of traceroute and ping and is designed to assist with the analysis of networking issues. image: alt: Trippy, man! light: ../../../assets/0.13.0/Trippy-Emblem.svg dark: ../../../assets/0.13.0/Trippy-Emblem-DarkMode.svg actions: - text: Get Started link: /0.13.0/start/getting-started/ icon: right-arrow - text: Read the docs link: /0.13.0/reference/overview/ icon: open-book variant: secondary - text: View on GitHub link: https://github.com/fujiapple852/trippy icon: github variant: secondary slug: 0.13.0 --- import { Card, CardGrid } from '@astrojs/starlight/components'; import { Icon } from '@astrojs/starlight/components'; * `ICMP`, `UDP` & `TCP` over `IPv4` & `IPv6` protocols * Fully customizable tracing options * `dublin` and `paris` `ECMP` strategies * `ICMP` extensions objects (i.e. `MPLS`) * Reverse `DNS` and `ASN` lookups * `NAT` detection ![Trippy main screen](../../../assets/0.13.0/main_screen.png) * Lookup GeoIp information and show on world map * Support for both `MaxMind` and `IPinfo` databases ![Trippy GeoIp world map](../../../assets/0.13.0/world_map.png) * Runs on `Linux`, `macOS`, `Windows`, `*BSD` * Supports `x86_64`, `aarch64`, `arm7` architectures * Available from most native package managers * Run in unprivileged mode ![Trippy on Windows](../../../assets/0.13.0/windows.png) * Customizable columns, color themes and key bindings * Hop detail navigation mode * Hop privacy mode * Show individual tracing flows * Various charts and statistics * Persist configuration to file ![Trippy settings](../../../assets/0.13.0/settings.png) TUI available in 10 languages: * Chinese 🇨🇳, English 🇺🇸, French 🇫🇷, German 🇩🇪, Italian 🇮🇹, Portuguese 🇵🇹, Russian 🇷🇺, Spanish 🇪🇸, Swedish 🇸🇪 and Turkish 🇹🇷 ![Trippy main screen in Chinese](../../../assets/0.13.0/help_screen_zh.png) ================================================ FILE: docs/src/content/docs/0.13.0/reference/bindings.md ================================================ --- title: Key Bindings Reference description: A reference for customizing the Trippy TUI key bindings. sidebar: order: 3 slug: 0.13.0/reference/bindings --- The following table lists the default Tui command key bindings. These can be overridden with the `--tui-key-bindings` command line option or in the `bindings` section of the configuration file. | Command | Description | Default | | -------------------------- | ----------------------------------------------- | --------- | | `toggle-help` | Toggle help | `h` | | `toggle-help-alt` | Toggle help (alternative binding) | `?` | | `toggle-settings` | Toggle settings | `s` | | `toggle-settings-tui` | Open settings (Tui tab) | `1` | | `toggle-settings-trace` | Open settings (Trace tab) | `2` | | `toggle-settings-dns` | Open settings (Dns tab) | `3` | | `toggle-settings-geoip` | Open settings (GeoIp tab) | `4` | | `toggle-settings-bindings` | Open settings (Bindings tab) | `5` | | `toggle-settings-theme` | Open settings (Theme tab) | `6` | | `toggle-settings-columns` | Open settings (Columns tab) | `7` | | `next-hop` | Select next hop | `down` | | `previous-hop` | Select previous hop | `up` | | `next-trace` | Select next trace | `right` | | `previous-trace` | Select previous trace | `left` | | `next-hop-address` | Select next hop address | `.` | | `previous-hop-address` | Select previous hop address | `,` | | `address-mode-ip` | Show IP address only | `i` | | `address-mode-host` | Show hostname only | `n` | | `address-mode-both` | Show both IP address and hostname | `b` | | `toggle-freeze` | Toggle freezing the display | `ctrl+f` | | `toggle-chart` | Toggle the chart | `c` | | `toggle-map` | Toggle the GeoIp map | `m` | | `toggle-flows` | Toggle the flows | `f` | | `expand-privacy` | Expand hop privacy | `p` | | `contract-privacy` | Contract hop privacy | `o` | | `expand-hosts` | Expand the hosts shown per hop | `]` | | `expand-hosts-max` | Expand the hosts shown per hop to the maximum | `}` | | `contract-hosts` | Contract the hosts shown per hop | `[` | | `contract-hosts-min` | Contract the hosts shown per hop to the minimum | `{` | | `chart-zoom-in` | Zoom in the chart | `=` | | `chart-zoom-out` | Zoom out the chart | `-` | | `clear-trace-data` | Clear all trace data | `ctrl+r` | | `clear-dns-cache` | Flush the DNS cache | `ctrl+k` | | `clear-selection` | Clear the current selection | `esc` | | `toggle-as-info` | Toggle AS info display | `z` | | `toggle-hop-details` | Toggle hop details | `d` | | `quit` | Quit the application | `q` | | `quit-preserve-screen` | Quit the application and preserve the screen | `shift+q` | The supported modifiers are: `shift`, `ctrl`, `alt`, `super`, `hyper` & `meta`. Multiple modifiers may be specified, for example `ctrl+shift+b`. ================================================ FILE: docs/src/content/docs/0.13.0/reference/cli.md ================================================ --- title: CLI Reference description: A reference for the Trippy command line interface. sidebar: order: 1 slug: 0.13.0/reference/cli --- ```text A network diagnostic tool Usage: trip [OPTIONS] [TARGETS]... Arguments: [TARGETS]... A space delimited list of hostnames and IPs to trace Options: -c, --config-file Config file -m, --mode Output mode [default: tui] Possible values: - tui: Display interactive TUI - stream: Display a continuous stream of tracing data - pretty: Generate a pretty text table report for N cycles - markdown: Generate a Markdown text table report for N cycles - csv: Generate a CSV report for N cycles - json: Generate a JSON report for N cycles - dot: Generate a Graphviz DOT file for N cycles - flows: Display all flows for N cycles - silent: Do not generate any tracing output for N cycles -u, --unprivileged Trace without requiring elevated privileges on supported platforms [default: false] -p, --protocol Tracing protocol [default: icmp] Possible values: - icmp: Internet Control Message Protocol - udp: User Datagram Protocol - tcp: Transmission Control Protocol --udp Trace using the UDP protocol --tcp Trace using the TCP protocol --icmp Trace using the ICMP protocol -F, --addr-family The address family [default: ipv4-then-ipv6] Possible values: - ipv4: IPv4 only - ipv6: IPv6 only - ipv6-then-ipv4: IPv6 with a fallback to IPv4 - ipv4-then-ipv6: IPv4 with a fallback to IPv6 - system: If the OS resolver is being used then use the first IP address returned, otherwise lookup IPv6 with a fallback to IPv4 -4, --ipv4 Use IPv4 only -6, --ipv6 Use IPv6 only -P, --target-port The target port (TCP & UDP only) [default: 80] -S, --source-port The source port (TCP & UDP only) [default: auto] -A, --source-address The source IP address [default: auto] -I, --interface The network interface [default: auto] -i, --min-round-duration The minimum duration of every round [default: 1s] -T, --max-round-duration The maximum duration of every round [default: 1s] -g, --grace-duration The period of time to wait for additional ICMP responses after the target has responded [default: 100ms] --initial-sequence The initial sequence number [default: 33434] -R, --multipath-strategy The Equal-cost Multi-Path routing strategy (UDP only) [default: classic] Possible values: - classic: The src or dest port is used to store the sequence number - paris: The UDP `checksum` field is used to store the sequence number - dublin: The IP `identifier` field is used to store the sequence number -U, --max-inflight The maximum number of in-flight ICMP echo requests [default: 24] -f, --first-ttl The TTL to start from [default: 1] -t, --max-ttl The maximum number of TTL hops [default: 64] --packet-size The size of IP packet to send (IP header + ICMP header + payload) [default: 84] --payload-pattern The repeating pattern in the payload of the ICMP packet [default: 0] -Q, --tos The TOS (i.e. DSCP+ECN) IP header value (IPv4 only) [default: 0] -e, --icmp-extensions Parse ICMP extensions --read-timeout The socket read timeout [default: 10ms] -r, --dns-resolve-method How to perform DNS queries [default: system] Possible values: - system: Resolve using the OS resolver - resolv: Resolve using the `/etc/resolv.conf` DNS configuration - google: Resolve using the Google `8.8.8.8` DNS service - cloudflare: Resolve using the Cloudflare `1.1.1.1` DNS service -y, --dns-resolve-all Trace to all IPs resolved from DNS lookup [default: false] --dns-timeout The maximum time to wait to perform DNS queries [default: 5s] --dns-ttl The time-to-live (TTL) of DNS entries [default: 300s] -z, --dns-lookup-as-info Lookup autonomous system (AS) information during DNS queries [default: false] -s, --max-samples The maximum number of samples to record per hop [default: 256] --max-flows The maximum number of flows to record [default: 64] -a, --tui-address-mode How to render addresses [default: host] Possible values: - ip: Show IP address only - host: Show reverse-lookup DNS hostname only - both: Show both IP address and reverse-lookup DNS hostname --tui-as-mode How to render autonomous system (AS) information [default: asn] Possible values: - asn: Show the ASN - prefix: Display the AS prefix - country-code: Display the country code - registry: Display the registry name - allocated: Display the allocated date - name: Display the AS name --tui-custom-columns Custom columns to be displayed in the TUI hops table [default: holsravbwdt] --tui-icmp-extension-mode How to render ICMP extensions [default: off] Possible values: - off: Do not show `icmp` extensions - mpls: Show MPLS label(s) only - full: Show full `icmp` extension data for all known extensions - all: Show full `icmp` extension data for all classes --tui-geoip-mode How to render GeoIp information [default: short] Possible values: - off: Do not display GeoIp data - short: Show short format - long: Show long format - location: Show latitude and Longitude format -M, --tui-max-addrs The maximum number of addresses to show per hop [default: auto] --tui-preserve-screen Preserve the screen on exit [default: false] --tui-refresh-rate The TUI refresh rate [default: 100ms] --tui-privacy-max-ttl The maximum ttl of hops which will be masked for privacy [default: none] If set, the source IP address and hostname will also be hidden. --tui-locale The locale to use for the TUI [default: auto] --tui-timezone The timezone to use for the TUI [default: auto] The timezone must be a valid IANA timezone identifier. --tui-theme-colors The TUI theme colors [item=color,item=color,..] --print-tui-theme-items Print all TUI theme items and exit --tui-key-bindings The TUI key bindings [command=key,command=key,..] --print-tui-binding-commands Print all TUI commands that can be bound and exit -C, --report-cycles The number of report cycles to run [default: 10] -G, --geoip-mmdb-file The supported MaxMind or IPinfo GeoIp mmdb file --generate Generate shell completion [possible values: bash, elvish, fish, powershell, zsh] --generate-man Generate ROFF man page --print-config-template Print a template toml config file and exit --print-locales Print all available TUI locales and exit --log-format The debug log format [default: pretty] Possible values: - compact: Display log data in a compact format - pretty: Display log data in a pretty format - json: Display log data in a json format - chrome: Display log data in Chrome trace format --log-filter The debug log filter [default: trippy=debug] --log-span-events The debug log format [default: off] Possible values: - off: Do not display event spans - active: Display enter and exit event spans - full: Display all event spans -v, --verbose Enable verbose debug logging -h, --help Print help (see a summary with '-h') -V, --version Print version ``` :::note Trippy command line arguments may be given in any order and my occur both before and after the targets. ::: ================================================ FILE: docs/src/content/docs/0.13.0/reference/column.md ================================================ --- title: Column Reference description: A reference for customizing the Trippy TUI columns. sidebar: order: 4 slug: 0.13.0/reference/column --- The following table lists the columns that are available for display in the Tui. These can be overridden with the `--tui-custom-columns` command line option or in the `tui-custom-columns` attribute in the `tui` section of the configuration file. | Column | Code | Description | | -------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `#` | `h` | The time-to-live (TTL) for the hop | | `Host` | `o` | The hostname(s) and IP address(s) for the host(s) for the hop
May include AS info, GeoIp and ICMP extensions
Shows full hop details in hop detail navigation mode | | `Loss%` | `l` | The packet loss % for the hop | | `Snd` | `s` | The number of probes sent for the hop | | `Recv` | `r` | The number of probe responses received for the hop | | `Last` | `a` | The round-trip-time (RTT) of the last probe for the hop | | `Avg` | `v` | The average RTT of all probes for the hop | | `Best` | `b` | The best RTT of all probes for the hop | | `Wrst` | `w` | The worst RTT of all probes for the hop | | `StDev` | `d` | The standard deviation of all probes for the hop | | `Sts` | `t` | The status for the hop:
- 🟢 Healthy hop
- 🔵 Non-target hop with packet loss (does not necessarily indicate a problem)
- 🟤 Non-target hop is unresponsive (does not necessarily indicate a problem)
- 🟡 Target hop with packet loss (likely indicates a problem)
- 🔴 Target hop is unresponsive (likely indicates a problem) | | `Jttr` | `j` | The round-trip-time (RTT) difference between consecutive rounds for the hop | | `Javg` | `g` | The average jitter of all probes for the hop | | `Jmax` | `x` | The maximum jitter of all probes for the hop | | `Jint` | `i` | The smoothed jitter value of all probes for the hop | | `Seq` | `Q` | The sequence number for the last probe for the hop | | `Sprt` | `S` | The source port for the last probe for the hop | | `Dprt` | `P` | The destination port for the last probe for the hop | | `Type` | `T` | The icmp packet type for the last probe for the hop:
- TE: TimeExceeded
- ER: EchoReply
- DU: DestinationUnreachable
- NA: NotApplicable | | `Code` | `C` | The icmp packet code for the last probe for the hop | | `Nat` | `N` | The NAT detection status for the hop | | `Fail` | `f` | The number of probes which failed to send for the hop | | `Floss` | `F` | A _heuristic_ for the number of probes with _forward loss_ for the hop | | `Bloss` | `B` | A _heuristic_ for the number of probes with _backward loss_ for the hop | | `Floss%` | `D` | The _forward loss_ % for the hop | | `Dscp` | `K` | Differentiated Services Code Point (DSCP) of the Original Datagram | | `Ecn` | `M` | Explicit Congestion Notification (ECN) of the Original Datagram | The default columns are `holsravbwdt`. :::note The columns will be shown in the order specified in the configuration. ::: ================================================ FILE: docs/src/content/docs/0.13.0/reference/configuration.md ================================================ --- title: Configuration Reference description: A reference for customizing the Trippy configuration. sidebar: order: 2 slug: 0.13.0/reference/configuration --- Trippy can be configured with via command line arguments or an optional configuration file. If a given configuration item is specified in both the configuration file and via a command line argument then the latter will take precedence. The configuration file location may be provided to Trippy via the `-c` (`--config-file`) argument. If not provided, Trippy will attempt to locate a `trippy.toml` or `.trippy.toml` configuration file in one of the following locations: - The current directory - The user home directory - the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config` - the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy` - the Windows data directory (Windows only): `%APPDATA%` A template configuration file for [0.13.0](https://github.com/fujiapple852/trippy/blob/0.13.0/trippy-config-sample.toml) is available to download, or can be generated with the following command: ```shell trip --print-config-template > trippy.toml ``` ================================================ FILE: docs/src/content/docs/0.13.0/reference/locale.md ================================================ --- title: Locale Reference description: A reference for customizing the Trippy TUI locale. sidebar: order: 6 slug: 0.13.0/reference/locale --- The following table lists the supported locales for the Tui. These can be overridden with the `--tui-locale` command line option or in the `tui-locale` attribute in the `tui` section of the configuration file. | Locale | Language | Region | | ------ | ---------- | ------ | | `zh` | Chinese | all | | `en` | English | all | | `fr` | French | all | | `de` | German | all | | `it` | Italian | all | | `pt` | Portuguese | all | | `ru` | Russian | all | | `es` | Spanish | all | | `sv` | Swedish | all | | `tr` | Turkish | all | :::note If you are able to help validate translations for Trippy, or if you wish to add translations for any additional languages, please see the [tracking issue](https://github.com/fujiapple852/trippy/issues/506) for details of how to contribute. ::: ================================================ FILE: docs/src/content/docs/0.13.0/reference/overview.mdx ================================================ --- title: Trippy Reference description: Reference documentation for Trippy. sidebar: order: 0 badge: text: New variant: note slug: 0.13.0/reference/overview --- This section provides complete reference documentation for Trippy. :::note This reference documentation is intended for users who are already familiar with Trippy and want to learn more about its features and capabilities. If you are new to Trippy, it is recommend that you start by reading the [getting started](/0.13.0/start/getting-started) guide. ::: ### CLI Reference The [CLI reference](/0.13.0/reference/cli) provides a complete list of the command line options available for Trippy. This information is available via the `--help` command line option and also in the man page on supported platforms. ### Configuration Reference Trippy can be configured via an optional configuration file. The [configuration reference](/0.13.0/reference/configuration) provides details of how to configure Trippy via the configuration file. ### Key Bindings Reference The Trippy TUI is highly customizable and allows you to change the key bindings to suit your preferences. The [key bindings reference](/0.13.0/reference/bindings) provides a complete list of the available key bindings and their descriptions. ### Column Reference The list of columns that can be displayed in the TUI can be customized to suit your needs. The [column reference](/0.13.0/reference/column) provides a complete list of the available columns and their descriptions. ### Theme Reference The color schema of the TUI can be fully customized. The [theme reference](/0.13.0/reference/theme) provides a complete list of the items which can be customized and their descriptions. ### Locale Reference The Trippy TUI supports multiple languages and regions. The [locale reference](/0.13.0/reference/locale) provides a complete list of supported locales. ### Version Reference All versions of Trippy and their support status are listed in the [version reference](/0.13.0/reference/version). ================================================ FILE: docs/src/content/docs/0.13.0/reference/theme.md ================================================ --- title: Theme Reference description: A reference for customizing the Trippy TUI theme. sidebar: order: 5 slug: 0.13.0/reference/theme --- The following table lists the default Tui color theme. These can be overridden with the `--tui-theme-colors` command line option or in the `theme-colors` section of the configuration file. | Item | Description | Default | | ------------------------------------ | --------------------------------------------------------- | ------------ | | `bg-color` | The default background color | `Black` | | `border-color` | The default color of borders | `Gray` | | `text-color` | The default color of text | `Gray` | | `tab-text-color` | The color of the text in traces tabs | `Green` | | `hops-table-header-bg-color` | The background color of the hops table header | `White` | | `hops-table-header-text-color` | The color of text in the hops table header | `Black` | | `hops-table-row-active-text-color` | The color of text of active rows in the hops table | `Gray` | | `hops-table-row-inactive-text-color` | The color of text of inactive rows in the hops table | `DarkGray` | | `hops-chart-selected-color` | The color of the selected series in the hops chart | `Green` | | `hops-chart-unselected-color` | The color of the unselected series in the hops chart | `Gray` | | `hops-chart-axis-color` | The color of the axis in the hops chart | `DarkGray` | | `frequency-chart-bar-color` | The color of bars in the frequency chart | `Green` | | `frequency-chart-text-color` | The color of text in the bars of the frequency chart | `Gray` | | `flows-chart-bar-selected-color` | The color of the selected flow bar in the flows chart | `Green` | | `flows-chart-bar-unselected-color` | The color of the unselected flow bar in the flows chart | `DarkGray` | | `flows-chart-text-current-color` | The color of the current flow text in the flows chart | `LightGreen` | | `flows-chart-text-non-current-color` | The color of the non-current flow text in the flows chart | `White` | | `samples-chart-color` | The color of the samples chart | `Yellow` | | `samples-chart-lost-color` | The color of the samples chart for lost probes | `Red` | | `help-dialog-bg-color` | The background color of the help dialog | `Blue` | | `help-dialog-text-color` | The color of the text in the help dialog | `Gray` | | `settings-dialog-bg-color` | The background color of the settings dialog | `blue` | | `settings-tab-text-color` | The color of the text in settings dialog tabs | `green` | | `settings-table-header-text-color` | The color of text in the settings table header | `black` | | `settings-table-header-bg-color` | The background color of the settings table header | `white` | | `settings-table-row-text-color` | The color of text of rows in the settings table | `gray` | | `map-world-color` | The color of the map world diagram | `white` | | `map-radius-color` | The color of the map accuracy radius circle | `yellow` | | `map-selected-color` | The color of the map selected item box | `green` | | `map-info-panel-border-color` | The color of border of the map info panel | `gray` | | `map-info-panel-bg-color` | The background color of the map info panel | `black` | | `map-info-panel-text-color` | The color of text in the map info panel | `gray` | | `info-bar-bg-color` | The background color of the information bar | `white` | | `info-bar-text-color` | The color of text in the information bar | `black` | The supported [ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) are: - `Black`, `Red`, `Green`, `Yellow`, `Blue`, `Magenta`, `Cyan`, `Gray`, `DarkGray`, `LightRed`, `LightGreen`, `LightYellow`, `LightBlue`, `LightMagenta`, `LightCyan`, `White` In addition, CSS [named colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color) (i.e. SkyBlue) and raw hex values (i.e. ffffff) may be used but note that these are only supported on some platforms and terminals and may not render correctly elsewhere. Color names are case-insensitive and may contain dashes. ================================================ FILE: docs/src/content/docs/0.13.0/reference/version.md ================================================ --- title: Version Reference description: A reference for the Trippy versions. sidebar: order: 7 slug: 0.13.0/reference/version --- The following table lists this versions of Trippy that are available and links to the corresponding release note and documentation: | Version | Release Date | Status | Release Note | Documentation | | ---------- | ------------ | ----------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | | 0.14.0-dev | n/a | Development | n/a | [docs](https://trippy.rs) | | 0.13.0 | 2025-05-05 | Current | [note](https://github.com/fujiapple852/trippy/releases/tag/0.13.0) | [docs](https://trippy.rs/0.13.0) | | 0.12.2 | 2025-01-03 | Previous | [note](https://github.com/fujiapple852/trippy/releases/tag/0.12.2) | [docs](https://trippy.rs/0.12.2) | | 0.11.0 | 2024-08-11 | Previous | [note](https://github.com/fujiapple852/trippy/releases/tag/0.11.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.11.0) | | 0.10.0 | 2024-03-31 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.10.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.10.0) | | 0.9.0 | 2023-11-30 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.9.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.9.0) | | 0.8.0 | 2023-05-15 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.8.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.8.0) | | 0.7.0 | 2023-03-25 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.7.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.7.0) | | 0.6.0 | 2022-08-19 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.6.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.6.0) | :::note Only the _latest patch versions_ of both the _current_ and _previous_ releases of Trippy are supported. ::: ================================================ FILE: docs/src/content/docs/0.13.0/start/features.md ================================================ --- title: Features description: Learn about the features of Trippy. sidebar: order: 3 slug: 0.13.0/start/features --- - Trace using multiple protocols: - `ICMP`, `UDP` & `TCP` - `IPv4` & `IPv6` - Customizable tracing options: - packet size & payload pattern - start and maximum time-to-live (TTL) - minimum and maximum round duration - round end grace period & maximum number of unknown hops - source & destination port (`TCP` & `UDP`) - source address and source interface - `TOS` (aka `DSCP + ECN`) - Support for `classic`, `paris` and `dublin` [Equal Cost Multi-path Routing](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) strategies ([tracking issue](https://github.com/fujiapple852/trippy/issues/274)) - RFC4884 [ICMP Multi-Part Messages](https://datatracker.ietf.org/doc/html/rfc4884) - Generic Extension Objects - MPLS Label Stacks - Unprivileged mode - NAT detection - Tui interface: - Trace multiple targets simultaneously from a single instance of Trippy - Per hop stats (sent, received, loss%, last, avg, best, worst, stddev, jitter & status) - Per hop round-trip-time (RTT) history and frequency distributing charts - Interactive chart of RTT for all hops in a trace with zooming capability - Interactive GeoIp world map - Isolate and filter by individual tracing flows - Customizable color theme & key bindings - Customizable column order and visibility - Configuration via both command line arguments and a configuration file - Show multiple hosts per hop with ability to cap display to N hosts and show frequency % - Show hop details and navigate hosts within each hop - Freeze/unfreeze the Tui, reset the stats, flush the cache, preserve screen on exit - Responsive UI with adjustable refresh rate - Hop privacy - Multiple language support - Customizable timezone - DNS: - Use system, external (Google `8.8.8.8` or Cloudflare `1.1.1.1`) or custom resolver - Lazy reverse DNS queries - Lookup \[autonomous system]\[autonomous\_system] number (ASN) and name - GeoIp: - Lookup and display GeoIp information from local [MaxMind](https://www.maxmind.com) and [IPinfo](https://ipinfo.io) `mmdb` files - Generate tracing reports: - `json`, `csv` & tabular (pretty-printed and markdown) - Tracing `flows` report - Graphviz `dot` charts - configurable reporting cycles - Runs on multiple platform (macOS, Linux, Windows, NetBSD, FreeBSD, OpenBSD) - Capabilities aware application (Linux only) ================================================ FILE: docs/src/content/docs/0.13.0/start/getting-started.mdx ================================================ --- title: Getting Started description: Get started with Trippy. sidebar: order: 1 slug: 0.13.0/start/getting-started --- import { Steps } from '@astrojs/starlight/components'; The following steps will guide you through the process of installing and running Trippy. 1. Install Trippy: Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled binaries, or source. For example, to install Trippy from `cargo`: ```shell cargo install trippy --locked ``` See the [installation guide](/0.13.0/start/installation) for details of how to install Trippy on your system. 2. Run Trippy: To run a basic trace to `example.com` with default settings, use the following command: ```shell sudo trip example.com ``` See the [usage examples](/0.13.0/guides/usage) and the [CLI reference](/0.13.0/reference/cli) for details of how to use Trippy. To use Trippy without elevated privileges, see the [privileges guide](/0.13.0/guides/privileges). 3. Customize the key bindings, theme and columns: See the [key bindings reference](/0.13.0/reference/bindings), [theme reference](/0.13.0/reference/theme) and [column reference](/0.13.0/reference/column) for details of how to customize the appearance and behavior of Trippy. These settings can be made permanent by adding them to the Trippy configuration file, see the [configuration reference](/0.13.0/reference/configuration) for details. 4. Review the tracing recommendations: To get the most out of Trippy, review the [recommended tracing settings](/0.13.0/guides/recommendation) for guidance on how to configure Trippy for different types of analysis. Happy tracing! ================================================ FILE: docs/src/content/docs/0.13.0/start/installation.md ================================================ --- title: Installation description: Install Trippy on your platform. sidebar: order: 2 slug: 0.13.0/start/installation --- The following sections provide instructions for installing Trippy on your platform. Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled binaries, or source. ## Distributions Trippy is available for a variety of platforms and package managers. ### Cargo [![Crates.io](https://img.shields.io/crates/v/trippy)](https://crates.io/crates/trippy/0.13.0) ```shell cargo install trippy --locked ``` ### APT (Debian) [![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy) ```shell apt install trippy ``` :::note Only available for Debian 13 (`trixie`) and later. ::: ### PPA (Ubuntu) [![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.13.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages) ```shell add-apt-repository ppa:fujiapple/trippy apt update && apt install trippy ``` :::note Only available for Ubuntu 24.04 (`Noble`) and 22.04 (`Jammy`). ::: ### Snap (Linux) [![trippy](https://snapcraft.io/trippy/badge.svg)](https://snapcraft.io/trippy) ```shell snap install trippy ``` ### Homebrew (macOS) [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/trippy.svg)](https://formulae.brew.sh/formula/trippy) ```shell brew install trippy ``` ### WinGet (Windows) [![winget package](https://img.shields.io/badge/WinGet-0.13.0-brightgreen)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/f/FujiApple/Trippy/0.13.0) ```shell winget install trippy ``` ### Scoop (Windows) [![Scoop package](https://img.shields.io/scoop/v/trippy?style=flat\&labelColor=5c5c5c\&color=%234dc71f)](https://github.com/ScoopInstaller/Main/blob/master/bucket/trippy.json) ```shell scoop install trippy ``` ### Chocolatey (Windows) [![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy) ```shell choco install trippy ``` ### NetBSD [![pkgsrc current package](https://repology.org/badge/version-for-repo/pkgsrc_current/trippy.svg)](https://pkgsrc.se/net/trippy) ```shell pkgin install trippy ``` ### FreeBSD [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/trippy.svg)](https://www.freshports.org/net/trippy/) ```shell pkg install trippy ``` ### OpenBSD [![OpenBSD port](https://repology.org/badge/version-for-repo/openbsd/trippy.svg)](https://openports.pl/path/net/trippy) ```shell pkg_add trippy ``` ### Arch Linux [![Arch package](https://repology.org/badge/version-for-repo/arch/trippy.svg)](https://archlinux.org/packages/extra/x86_64/trippy) ```shell pacman -S trippy ``` ### Gentoo Linux [![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/trippy.svg)](https://packages.gentoo.org/packages/net-analyzer/trippy) ```shell emerge -av net-analyzer/trippy ``` ### Void Linux [![Void Linux x86\_64 package](https://repology.org/badge/version-for-repo/void_x86_64/trippy.svg)](https://github.com/void-linux/void-packages/tree/master/srcpkgs/trippy) ```shell xbps-install -S trippy ``` ### ALT Sisyphus [![ALT Sisyphus package](https://repology.org/badge/version-for-repo/altsisyphus/trippy.svg)](https://packages.altlinux.org/en/sisyphus/srpms/trippy/) ```shell apt-get install trippy ``` ### Chimera Linux [![Chimera Linux package](https://repology.org/badge/version-for-repo/chimera/trippy.svg)](https://github.com/chimera-linux/cports/tree/master/user/trippy) ```shell apk add trippy ``` ### Nix [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/trippy.svg)](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/tr/trippy/package.nix) ```shell nix-env -iA trippy ``` ### Docker [![Docker Image Version (latest by date)](https://img.shields.io/docker/v/fujiapple/trippy)](https://hub.docker.com/r/fujiapple/trippy/) ```shell docker run -it fujiapple/trippy ``` ### All Repositories [![Packaging status](https://repology.org/badge/vertical-allrepos/trippy.svg)](https://repology.org/project/trippy/versions) ## Downloads Download the latest release for your platform. | OS | Arch | Env | Current (0.13.0) | Previous (0.12.2) | Previous (0.11.0) | | ------- | --------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Linux | `x86_64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-unknown-linux-gnu.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-linux-gnu.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-linux-gnu.tar.gz) | | Linux | `x86_64` | `musl` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-unknown-linux-musl.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-linux-musl.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-linux-musl.tar.gz) | | Linux | `aarch64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-aarch64-unknown-linux-gnu.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-unknown-linux-gnu.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-unknown-linux-gnu.tar.gz) | | Linux | `aarch64` | `musl` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-aarch64-unknown-linux-musl.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-unknown-linux-musl.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-unknown-linux-musl.tar.gz) | | Linux | `arm7` | `gnueabihf` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-armv7-unknown-linux-gnueabihf.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-armv7-unknown-linux-gnueabihf.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-armv7-unknown-linux-gnueabihf.tar.gz) | | Linux | `arm7` | `musleabi` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-armv7-unknown-linux-musleabi.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-armv7-unknown-linux-musleabi.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-armv7-unknown-linux-musleabi.tar.gz) | | Linux | `arm7` | `musleabihf` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-armv7-unknown-linux-musleabihf.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-armv7-unknown-linux-musleabihf.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-armv7-unknown-linux-musleabihf.tar.gz) | | macOS | `x86_64` | `darwin` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-apple-darwin.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-apple-darwin.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-apple-darwin.tar.gz) | | macOS | `aarch64` | `darwin` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-aarch64-apple-darwin.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-apple-darwin.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-apple-darwin.tar.gz) | | Windows | `x86_64` | `msvc` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-pc-windows-msvc.zip) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-pc-windows-msvc.zip) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-pc-windows-msvc.zip) | | Windows | `x86_64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-pc-windows-gnu.zip) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-pc-windows-gnu.zip) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-pc-windows-gnu.zip) | | Windows | `aarch64` | `msvc` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-aarch64-pc-windows-msvc.zip) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-pc-windows-msvc.zip) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-pc-windows-msvc.zip) | | FreeBSD | `x86_64` | n/a | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-unknown-freebsd.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-freebsd.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-freebsd.tar.gz) | | NetBSD | `x86_64` | n/a | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-unknown-netbsd.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-netbsd.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-netbsd.tar.gz) | | RPM | `x86_64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64.rpm) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64.rpm) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64.rpm) | | Debian | `x86_64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy_x86_64-unknown-linux-gnu_0.13.0_amd64.deb) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy_x86_64-unknown-linux-gnu_0.12.2_amd64.deb) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy_x86_64-unknown-linux-gnu_0.11.0_amd64.deb) | | Debian | `x86_64` | `musl` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy_x86_64-unknown-linux-musl_0.13.0_amd64.deb) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy_x86_64-unknown-linux-musl_0.12.2_amd64.deb) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy_x86_64-unknown-linux-musl_0.11.0_amd64.deb) | ================================================ FILE: docs/src/content/docs/development/crates.md ================================================ --- title: Crates description: A reference for the Trippy crates. --- The following table lists the crates that are provided by Trippy. See [crates](crates/README.md) for more information. | Crate | Description | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | [trippy](https://crates.io/crates/trippy) | A binary crate for the Trippy application and a library crate | | [trippy-core](https://crates.io/crates/trippy-core) | A library crate providing the core Trippy tracing functionality | | [trippy-packet](https://crates.io/crates/trippy-packet) | A library crate which provides packet wire formats and packet parsing functionality | | [trippy-dns](https://crates.io/crates/trippy-dns) | A library crate for performing forward and reverse lazy DNS resolution | | [trippy-privilege](https://crates.io/crates/trippy-privilege) | A library crate for discovering platform privileges | | [trippy-tui](https://crates.io/crates/trippy-tui) | A library crate for the Trippy terminal user interface | ================================================ FILE: docs/src/content/docs/guides/docker.md ================================================ --- title: Run Trippy with Docker description: Learn how to run the Trippy CLI from the official Docker image. sidebar: order: 5 --- Trippy is distributed as the [`fujiapple/trippy`](https://hub.docker.com/r/fujiapple/trippy/) image on Docker Hub. The image bundles the `trip` binary compiled against Alpine Linux and configures it as the container entrypoint. ## Quick start Run the image interactively and pass any CLI arguments directly after the image name: :::note Because the entrypoint already invokes `trip`, you should not repeat the binary name. ::: ```shell docker run -it --rm fujiapple/trippy example.com ``` To display the built-in help you can pass standard flags: ```shell docker run -it --rm fujiapple/trippy --help ``` ## Configuration To provide a configuration file, mount host directories into the root of the container: ```shell docker run -it --rm -v "/path/to/trippy.toml:/trippy.toml" fujiapple/trippy example.com ``` ## Networking considerations Trippy uses raw sockets to send probes. On Linux hosts Docker grants the required `CAP_NET_RAW` capability by default, so no additional flags are needed. When running inside more restrictive container runtimes ensure that the container retains this capability: ```shell docker run -it --rm --cap-add=NET_RAW fujiapple/trippy example.com ``` :::caution Docker Desktop for macOS has a known limitations with raw sockets. In particular, it resets the `ttl` field on outgoing packets to 64. As a result, intermediate hops are not discovered when tracing from a macOS host via Docker. ::: ================================================ FILE: docs/src/content/docs/guides/faq.md ================================================ --- title: Frequently Asked Questions description: Frequently asked questions about Trippy. sidebar: order: 6 --- ## Why does Trippy show "Awaiting data..."? :::caution If you are using Windows you _must_ [configure](/guides/windows_firewall) the Windows Defender firewall to allow incoming ICMP traffic ::: When Trippy shows “Awaiting data...” it means that it has received zero responses for the probes sent in a trace. This indicates that either probes are not being sent or, more typically, responses are not being received. Check that local and network firewalls allow ICMP traffic and that the system `traceroute` (or `tracert.exe` on Windows) works as expected. Note that on Windows, even if `tracert.exe` works as expected, you _must_ [configure](/guides/windows_firewall) the Windows Defender firewall to allow incoming ICMP traffic. For deeper diagnostics you can run tools such as https://www.wireshark.org and https://www.tcpdump.org to verify that icmp requests and responses are being send and received.
================================================ FILE: docs/src/content/docs/guides/privileges.md ================================================ --- title: Privileges description: A reference for the Trippy privileges. sidebar: order: 2 --- Trippy normally requires elevated privileges due to the use of raw sockets. Enabling the required privileges for your platform can be achieved in several ways, as outlined below. Trippy can also be used without elevated privileged on certain platforms, with some limitations. ## Unix ### Run via `sudo` Run as `root` user via `sudo`: ```shell sudo trip example.com ``` :::note When running `trip` via `sudo` you must ensure that the (optional) configuration file is stored relative to the `root` user or specify the location of the configuration file via the `-c` (`--config-file`) command line argument. See the [configuration reference](/reference/configuration). ::: ### Set the `setuid` bit `chown` `trip` as the `root` user and set the `setuid` bit: ```shell sudo chown root $(which trip) && sudo chmod +s $(which trip) ``` ### [Linux only] Set capabilities Set the `CAP_NET_RAW` capability: ```shell sudo setcap CAP_NET_RAW+p $(which trip) ``` :::note Trippy is a capability aware application and will add `CAP_NET_RAW` to the effective set if it is present in the allowed set. Trippy will drop all capabilities after creating the raw sockets. ::: ## Windows Trippy must be run with Administrator privileges on Windows. ## Unprivileged mode Trippy allows running in an unprivileged mode for all tracing modes (`ICMP`, `UDP` and `TCP`) on platforms which support that feature. :::note Unprivileged mode is currently only supported on macOS. Linux support is possible and may be added in the future. Unprivileged mode is not supported on NetBSD, FreeBSD or Windows as these platforms do not support the `IPPROTO_ICMP` socket type. See [#101](https://github.com/fujiapple852/trippy/issues/101) for further information. ::: The unprivileged mode can be enabled by adding the `--unprivileged` (`-u`) command line flag or by adding the `unprivileged` entry in the `trippy` section of the [configuration file](/reference/configuration): ```toml [trippy] unprivileged = true ``` :::note The `paris` and `dublin` `ECMP` strategies are not supported in unprivileged mode as these require manipulating the `UDP` and `IP` and headers which in turn requires the use of a raw socket. ::: ================================================ FILE: docs/src/content/docs/guides/recommendation.md ================================================ --- title: Recommended Tracing Settings description: Recommended settings for Trippy. sidebar: order: 3 --- Trippy provides a variety of configurable features which can be used to perform different types of analysis. The choice of settings will depend on the analysis you wish to perform and the environment in which you are working. This guide lists some common options along with some basic guidance on when they might be appropriate. :::note The Windows `tracert` tool uses ICMP by default, whereas most Unix `traceroute` tools use UDP by default. ::: ## ICMP By default Trippy will run an ICMP trace to the target. This will typically produce a consistent path to the target (a single flow) for each round of tracing which makes it easy to read and analyse. This is a useful mode for general network troubleshooting. However, many routers are configured to rate-limit ICMP traffic which can make it difficult to get an accurate picture of packet loss. In addition, ICMP traffic is not typically subject to ECMP routing and so may not reflect the path that would taken by other protocols such as UDP and TCP. To run a simple ICMP trace: ```shell trip example.com ``` Due to the rate-limiting of ICMP traffic, some people prefer to hide the `Loss%` and `Recv` columns in the Tui as these are easy to misinterpret. ```shell trip example.com --tui-custom-columns hosavbwdt ``` These settings can be made permanent by adding them to the Trippy configuration file: ```toml [tui] custom-columns = "hosavbwdt" ``` :::note The `Sts` column shows different color codes to reflect packet loss at intermediate vs the target hop, see the [Column Reference](/reference/column) for more information. ::: #### UDP/Dublin with fixed ports UDP tracing provides a more realistic view of the path taken by traffic that is subject to ECMP routing. Setting a fixed target port in the range 33434-33534 may allow Trippy to determine that the probe has reached the target as many routers and firewalls are configured to allow UDP probes in that range and will respond with a Destination Unreachable response. However, running a UDP trace with a fixed target port and a variable source port will typically result in different paths being followed for each probe within each round of tracing. This can make it difficult to interpret the output as different hosts will reply for a given hop (time-to-live) across rounds. By using the `dublin` ECMP strategy, which encodes the sequence number in the IP `identifier` field, Trippy can fix both the source and target ports, typically resulting in a _single_ path for each probe within each round of tracing. :::note UDP/Dublin for IPv6 encodes the sequence number as the payload length as the IP `identifier` field is not available in IPv6. ::: :::note Keep in mind that every probe is an _independent trial_ and each may traverse a completely different path. In practice, ICMP probes often follow a single path, whereas the path of UDP and TCP probes is typically determined by the 5-tuple of protocol, source and destination IP addresses and ports. Also beware that the return path may not be the same as the forward path, and may also differ for each probe. Strategies such as `dublin` and `paris` assist in controlling the path taken by the forward probes, but do not help control the return path. Therefore it is recommended to run a trace in both directions to get a complete picture. ::: To run a UDP trace with fixed source and target ports using the `dublin` ECMP strategy: ```shell trip example.com --udp --multipath-strategy dublin --source-port 5000 --target-port 33434 ``` :::note The source port can be any valid port number, but the target port should usually be in the range 33434-33534 or whatever range is open to UDP probes on the target host. ::: These settings can be made permanent by adding them to the Trippy configuration file: ```toml [strategy] protocol = "udp" multipath-strategy = "dublin" source-port = 5000 target-port = 33434 ``` ## UDP/Dublin with fixed target port and variable source port As an extension to the above, if you do not fix the source port when using the `dublin` ECMP strategy, Trippy will vary the source port per _round_ of tracing (i.e. each probe within a given round will share the same source port, and the source port will vary for each round). This will typically result in the _same_ path being followed for _each_ probe within a given round, but _different_ paths being followed for each round. These individual flows can be explored in the Trippy Tui by pressing the `toggle-flows` key binding (`f` key by default). Adding the columns `Seq`, `Sprt` and `Dprt` to the Tui will show the sequence number, source port and destination port respectively which makes this easier to visualize. ```shell trip example.com --udp --multipath-strategy dublin --target-port 33434 --tui-custom-columns holsravbwdtSPQ ``` These settings can be made permanent by adding them to the Trippy configuration file: ```toml [strategy] protocol = "udp" multipath-strategy = "dublin" target-port = 33434 [tui] custom-columns = "holsravbwdtSPQ" ``` To make the flows easier to visualize, you can generate a Graphviz DOT file report of all tracing flows: ```shell trip example.com --udp --multipath-strategy dublin --target-port 33434 -m dot -C 5 ``` ## UDP/Paris UDP with the `paris` ECMP strategy offers the same benefits as the `dublin` strategy with fixed ports and can be used in the same way. They differ in the way they encode the sequence number in the probe. The `dublin` strategy uses the IP `identifier` field, whereas the `paris` strategy uses the UDP `checksum` field. To run a UDP trace with fixed source and target ports using the `paris` ECMP strategy: ```shell trip example.com --udp --multipath-strategy paris --source-port 5000 --target-port 33434 ``` The `paris` strategy does not work behind NAT as the UDP `checksum` field is typically modified by NAT devices. Therefore the `dublin` strategy is recommended when NAT is present. :::note Trippy can detect the presence of NAT devices in some circumstances when using the `dublin` strategy and the `Nat` column can be shown in the Tui to indicate when NAT is detected. See the [Column Reference](/reference/column) for more information. ::: #### TCP TCP tracing is similar to UDP tracing in that it provides a more realistic view of the path taken by traffic that is subject to ECMP routing. TCP tracing defaults to using a target port of 80 and sets the source port as the sequence number which will typically result in a different path being followed for each probe within each round of tracing. To run a TCP trace: ```shell trip example.com --tcp ``` TCP tracing is useful for diagnosing issues with TCP connections and higher layer protocols such as HTTP. Often UDP tracing can be used in place of TCP to diagnose IP layer network issues and, as it provides ways to control the path taken by the probes, it is often preferred. :::note Trippy does not support the `dublin` or `paris` ECMP strategies for TCP tracing and so you cannot fix both the source and target ports. See the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details. ::: ================================================ FILE: docs/src/content/docs/guides/usage.md ================================================ --- title: Usage Examples description: Examples of how to use the Trippy command line interface. sidebar: order: 1 --- Basic usage with default parameters: ```shell trip example.com ``` Trace without requiring elevated privileges (supported platforms only, see [privileges](/guides/privileges)): ```shell trip example.com --unprivileged ``` Trace using the `udp` (or `tcp` or `icmp`) protocol (also aliases `--icmp`, `--udp` & `--tcp`): ```shell trip example.com -p udp ``` Trace to multiple targets simultaneously (`icmp` protocol only, see [#72](https://github.com/fujiapple852/trippy/issues/72)): ```shell trip example.com google.com crates.io ``` Trace with a minimum round time of `250ms` and a grace period of `50ms`: ```shell trip example.com -i 250ms -g 50ms ``` Trace with a custom first and maximum `time-to-live`: ```shell trip example.com --first-ttl 2 --max-ttl 10 ``` Use custom destination port `443` for `tcp` tracing: ```shell trip example.com -p tcp -P 443 ``` Use custom source port `5000` for `udp` tracing: ```shell trip example.com -p udp -S 5000 ``` Use the `dublin` (or `paris`) ECMP routing strategy for `udp` with fixed source and destination ports: ```shell trip example.com -p udp -R dublin -S 5000 -P 3500 ``` Trace with a custom source address: ```shell trip example.com -p tcp -A 127.0.0.1 ``` Trace with a source address determined by the IPv4 address for interface `en0`: ```shell trip example.com -p tcp -I en0 ``` Trace using `IPv6`: ```shell trip example.com -6 ``` Trace using `ipv4-then-ipv6` fallback (or `ipv6-then-ipv4` or `ipv4` or `ipv6`): ```shell trip example.com --addr-family ipv4-then-ipv6 ``` Generate a `json` (or `csv`, `pretty`, `markdown`) tracing report with 5 rounds of data: ```shell trip example.com -m json -C 5 ``` Generate a [Graphviz](https://graphviz.org) `DOT` file report of all tracing flows for a TCP trace after 5 rounds: ```shell trip example.com --tcp -m dot -C 5 ``` Generate a textual report of all tracing flows for a UDP trace after 5 rounds: ```shell trip example.com --udp -m flows -C 5 ``` Perform DNS queries using the `google` DNS resolver (or `cloudflare`, `system`, `resolv`): ```shell trip example.com -r google ``` Lookup [AS][autonomous_system] information for all discovered IP addresses (not yet available for the `system` resolver, see [#66](https://github.com/fujiapple852/trippy/issues/66)): ```shell trip example.com -r google -z ``` Set the reverse DNS lookup cache time-to-live to be 60 seconds: ```shell trip example.com --dns-ttl 60sec ``` Lookup and display `short` (or `long` or `location` or `off`) GeoIp information from a `mmdb` file: ```shell trip example.com --geoip-mmdb-file GeoLite2-City.mmdb --tui-geoip-mode short ``` Parse `icmp` extensions: ```shell trip example.com -e ``` Hide the IP address, hostname and GeoIp for the first two hops: ```shell trip example.com --tui-privacy-max-ttl 2 ``` Customize Tui columns (see [Column Reference](/reference/column)): ```shell trip example.com --tui-custom-columns holsravbwdt ``` Customize the color theme: ```shell trip example.com --tui-theme-colors bg-color=blue,text-color=ffff00 ``` List all Tui items that can have a custom color theme: ```shell trip --print-tui-theme-items ``` Customize the key bindings: ```shell trip example.com --tui-key-bindings previous-hop=k,next-hop=j,quit=shift-q ``` List all Tui commands that can have a custom key binding: ```shell trip --print-tui-binding-commands ``` Specify the location of the Trippy config file: ```shell trip example.com --config-file /path/to/trippy.toml ``` Generate a template configuration file: ```shell trip --print-config-template > trippy.toml ``` Generate `bash` shell completions (or `fish`, `powershell`, `zsh`, `elvish`): ```shell trip --generate bash ``` Generate `ROFF` man page: ```shell trip --generate-man ``` Use the `de` Tui locale: ```shell trip example.com --tui-locale de ``` List supported Tui locales: ```shell trip --print-locales ``` Set the Tui timezone to `UTC`: ```shell trip example.com --tui-timezone UTC ``` Run in `silent` tracing mode and output `compact` trace logging with `full` span events: ```shell trip example.com -m silent -v --log-format compact --log-span-events full ``` ================================================ FILE: docs/src/content/docs/guides/windows_firewall.md ================================================ --- title: Windows Defender Firewall description: Allow incoming ICMP traffic in the Windows Defender firewall. sidebar: order: 4 --- The Windows Defender firewall rule can be created using PowerShell. ```shell New-NetFirewallRule -DisplayName "ICMPv4 Trippy Allow" -Name ICMPv4_TRIPPY_ALLOW -Protocol ICMPv4 -Action Allow New-NetFirewallRule -DisplayName "ICMPv6 Trippy Allow" -Name ICMPv6_TRIPPY_ALLOW -Protocol ICMPv6 -Action Allow ``` The rules can be enabled as follows: ```shell Enable-NetFirewallRule ICMPv4_TRIPPY_ALLOW Enable-NetFirewallRule ICMPv6_TRIPPY_ALLOW ``` The rules can be disabled as follows: ```shell Disable-NetFirewallRule ICMPv4_TRIPPY_ALLOW Disable-NetFirewallRule ICMPv6_TRIPPY_ALLOW ``` There is a [step-by-step guide to manually configure the Windows Defender firewall rule](https://github.com/fujiapple852/trippy/issues/578#issuecomment-1565149826). ================================================ FILE: docs/src/content/docs/index.mdx ================================================ --- title: "Trippy: a network diagnostic tool" description: a network diagnostic tool. template: splash hero: tagline: Trippy combines the functionality of traceroute and ping and is designed to assist with the analysis of networking issues. image: alt: Trippy, man! light: ../../assets/Trippy-Emblem.svg dark: ../../assets/Trippy-Emblem-DarkMode.svg actions: - text: Get Started link: /start/getting-started/ icon: right-arrow - text: Read the docs link: /reference/overview/ icon: open-book variant: secondary - text: View on GitHub link: https://github.com/fujiapple852/trippy icon: github variant: secondary --- import { Card, CardGrid } from '@astrojs/starlight/components'; import { Icon } from '@astrojs/starlight/components'; - `ICMP`, `UDP` & `TCP` over `IPv4` & `IPv6` protocols - Fully customizable tracing options - `dublin` and `paris` `ECMP` strategies - `ICMP` extensions objects (i.e. `MPLS`) - Reverse `DNS` and `ASN` lookups - `NAT` detection ![Trippy main screen](../../assets/main_screen.png) - Lookup GeoIp information and show on world map - Support for both `MaxMind` and `IPinfo` databases ![Trippy GeoIp world map](../../assets/world_map.png) - Runs on `Linux`, `macOS`, `Windows`, `*BSD` - Supports `x86_64`, `aarch64`, `arm7` architectures - Available from most native package managers - Run in unprivileged mode ![Trippy on Windows](../../assets/windows.png) - Customizable columns, color themes and key bindings - Hop detail navigation mode - Hop privacy mode - Show individual tracing flows - Various charts and statistics - Persist configuration to file ![Trippy settings](../../assets/settings.png) TUI available in 10 languages: - Chinese 🇨🇳, English 🇺🇸, French 🇫🇷, German 🇩🇪, Italian 🇮🇹, Portuguese 🇵🇹, Russian 🇷🇺, Spanish 🇪🇸, Swedish 🇸🇪 and Turkish 🇹🇷 ![Trippy main screen in Chinese](../../assets/help_screen_zh.png) ================================================ FILE: docs/src/content/docs/reference/bindings.md ================================================ --- title: Key Bindings Reference description: A reference for customizing the Trippy TUI key bindings. sidebar: order: 3 --- The following table lists the default Tui command key bindings. These can be overridden with the `--tui-key-bindings` command line option or in the `bindings` section of the configuration file. | Command | Description | Default | | -------------------------- | ----------------------------------------------- | --------- | | `toggle-help` | Toggle help | `h` | | `toggle-help-alt` | Toggle help (alternative binding) | `?` | | `toggle-settings` | Toggle settings | `s` | | `toggle-settings-tui` | Open settings (Tui tab) | `1` | | `toggle-settings-trace` | Open settings (Trace tab) | `2` | | `toggle-settings-dns` | Open settings (Dns tab) | `3` | | `toggle-settings-geoip` | Open settings (GeoIp tab) | `4` | | `toggle-settings-bindings` | Open settings (Bindings tab) | `5` | | `toggle-settings-theme` | Open settings (Theme tab) | `6` | | `toggle-settings-columns` | Open settings (Columns tab) | `7` | | `next-hop` | Select next hop | `down` | | `previous-hop` | Select previous hop | `up` | | `next-trace` | Select next trace | `right` | | `previous-trace` | Select previous trace | `left` | | `next-hop-address` | Select next hop address | `.` | | `previous-hop-address` | Select previous hop address | `,` | | `address-mode-ip` | Show IP address only | `i` | | `address-mode-host` | Show hostname only | `n` | | `address-mode-both` | Show both IP address and hostname | `b` | | `toggle-freeze` | Toggle freezing the display | `ctrl+f` | | `toggle-chart` | Toggle the chart | `c` | | `toggle-map` | Toggle the GeoIp map | `m` | | `toggle-flows` | Toggle the flows | `f` | | `expand-privacy` | Expand hop privacy | `p` | | `contract-privacy` | Contract hop privacy | `o` | | `expand-hosts` | Expand the hosts shown per hop | `]` | | `expand-hosts-max` | Expand the hosts shown per hop to the maximum | `}` | | `contract-hosts` | Contract the hosts shown per hop | `[` | | `contract-hosts-min` | Contract the hosts shown per hop to the minimum | `{` | | `chart-zoom-in` | Zoom in the chart | `=` | | `chart-zoom-out` | Zoom out the chart | `-` | | `clear-trace-data` | Clear all trace data | `ctrl+r` | | `clear-dns-cache` | Flush the DNS cache | `ctrl+k` | | `clear-selection` | Clear the current selection | `esc` | | `toggle-as-info` | Toggle AS info display | `z` | | `toggle-hop-details` | Toggle hop details | `d` | | `quit` | Quit the application | `q` | | `quit-preserve-screen` | Quit the application and preserve the screen | `shift+q` | The supported modifiers are: `shift`, `ctrl`, `alt`, `super`, `hyper` & `meta`. Multiple modifiers may be specified, for example `ctrl+shift+b`. ================================================ FILE: docs/src/content/docs/reference/cli.md ================================================ --- title: CLI Reference description: A reference for the Trippy command line interface. sidebar: order: 1 --- ```text A network diagnostic tool Usage: trip [OPTIONS] [TARGETS]... Arguments: [TARGETS]... A space delimited list of hostnames and IPs to trace Options: -c, --config-file Config file -m, --mode Output mode [default: tui] Possible values: - tui: Display interactive TUI - stream: Display a continuous stream of tracing data - pretty: Generate a pretty text table report for N cycles - markdown: Generate a Markdown text table report for N cycles - csv: Generate a CSV report for N cycles - json: Generate a JSON report for N cycles - dot: Generate a Graphviz DOT file for N cycles - flows: Display all flows for N cycles - silent: Do not generate any tracing output for N cycles -u, --unprivileged Trace without requiring elevated privileges on supported platforms [default: false] -p, --protocol Tracing protocol [default: icmp] Possible values: - icmp: Internet Control Message Protocol - udp: User Datagram Protocol - tcp: Transmission Control Protocol --udp Trace using the UDP protocol --tcp Trace using the TCP protocol --icmp Trace using the ICMP protocol -F, --addr-family The address family [default: ipv4-then-ipv6] Possible values: - ipv4: IPv4 only - ipv6: IPv6 only - ipv6-then-ipv4: IPv6 with a fallback to IPv4 - ipv4-then-ipv6: IPv4 with a fallback to IPv6 - system: If the OS resolver is being used then use the first IP address returned, otherwise lookup IPv4 with a fallback to IPv6 -4, --ipv4 Use IPv4 only -6, --ipv6 Use IPv6 only -P, --target-port The target port (TCP & UDP only) [default: 80] -S, --source-port The source port (TCP & UDP only) [default: auto] -A, --source-address The source IP address [default: auto] -I, --interface The network interface [default: auto] -i, --min-round-duration The minimum duration of every round [default: 1s] -T, --max-round-duration The maximum duration of every round [default: 1s] -g, --grace-duration The period of time to wait for additional ICMP responses after the target has responded [default: 100ms] --initial-sequence The initial sequence number [default: 33434] -R, --multipath-strategy The Equal-cost Multi-Path routing strategy (UDP only) [default: classic] Possible values: - classic: The src or dest port is used to store the sequence number - paris: The UDP `checksum` field is used to store the sequence number - dublin: The IP `identifier` field is used to store the sequence number -U, --max-inflight The maximum number of in-flight ICMP echo requests [default: 24] -f, --first-ttl The TTL to start from [default: 1] -t, --max-ttl The maximum number of TTL hops [default: 64] --packet-size The size of IP packet to send (IP header + ICMP header + payload) [default: 84] --payload-pattern The repeating pattern in the payload of the ICMP packet [default: 0] -Q, --tos The TOS (i.e. DSCP+ECN) IP header value (IPv4 only) [default: 0] -e, --icmp-extensions Parse ICMP extensions --read-timeout The socket read timeout [default: 10ms] -r, --dns-resolve-method How to perform DNS queries [default: system] Possible values: - system: Resolve using the OS resolver - resolv: Resolve using the `/etc/resolv.conf` DNS configuration - google: Resolve using the Google `8.8.8.8` DNS service - cloudflare: Resolve using the Cloudflare `1.1.1.1` DNS service -y, --dns-resolve-all Trace to all IPs resolved from DNS lookup [default: false] --dns-timeout The maximum time to wait to perform DNS queries [default: 5s] --dns-ttl The time-to-live (TTL) of DNS entries [default: 300s] -z, --dns-lookup-as-info Lookup autonomous system (AS) information during DNS queries [default: false] -s, --max-samples The maximum number of samples to record per hop [default: 256] --max-flows The maximum number of flows to record [default: 64] -a, --tui-address-mode How to render addresses [default: host] Possible values: - ip: Show IP address only - host: Show reverse-lookup DNS hostname only - both: Show both IP address and reverse-lookup DNS hostname --tui-as-mode How to render autonomous system (AS) information [default: asn] Possible values: - asn: Show the ASN - prefix: Display the AS prefix - country-code: Display the country code - registry: Display the registry name - allocated: Display the allocated date - name: Display the AS name --tui-custom-columns Custom columns to be displayed in the TUI hops table [default: holsravbwdt] --tui-icmp-extension-mode How to render ICMP extensions [default: off] Possible values: - off: Do not show `icmp` extensions - mpls: Show MPLS label(s) only - full: Show full `icmp` extension data for all known extensions - all: Show full `icmp` extension data for all classes --tui-geoip-mode How to render GeoIp information [default: short] Possible values: - off: Do not display GeoIp data - short: Show short format - long: Show long format - location: Show latitude and Longitude format -M, --tui-max-addrs The maximum number of addresses to show per hop [default: auto] --tui-preserve-screen Preserve the screen on exit [default: false] --tui-refresh-rate The TUI refresh rate [default: 100ms] --tui-privacy-max-ttl The maximum ttl of hops which will be masked for privacy [default: none] If set, the source IP address and hostname will also be hidden. --tui-locale The locale to use for the TUI [default: auto] --tui-timezone The timezone to use for the TUI [default: auto] The timezone must be a valid IANA timezone identifier. --tui-theme-colors The TUI theme colors [item=color,item=color,..] --print-tui-theme-items Print all TUI theme items and exit --tui-key-bindings The TUI key bindings [command=key,command=key,..] --print-tui-binding-commands Print all TUI commands that can be bound and exit -C, --report-cycles The number of report cycles to run [default: 10] -G, --geoip-mmdb-file The supported MaxMind or IPinfo GeoIp mmdb file --generate Generate shell completion [possible values: bash, elvish, fish, powershell, zsh] --generate-man Generate ROFF man page --print-config-template Print a template toml config file and exit --print-locales Print all available TUI locales and exit --log-format The debug log format [default: pretty] Possible values: - compact: Display log data in a compact format - pretty: Display log data in a pretty format - json: Display log data in a json format - chrome: Display log data in Chrome trace format --log-filter The debug log filter [default: trippy=debug] --log-span-events The debug log format [default: off] Possible values: - off: Do not display event spans - active: Display enter and exit event spans - full: Display all event spans -v, --verbose Enable verbose debug logging -h, --help Print help (see a summary with '-h') -V, --version Print version ``` :::note Trippy command line arguments may be given in any order and my occur both before and after the targets. All options can also be provided via environment variables using the `TRIP_` prefix (for example, `TRIP_PROTOCOL=tcp`). CLI flags take precedence over environment variables when both are set. ::: ================================================ FILE: docs/src/content/docs/reference/column.md ================================================ --- title: Column Reference description: A reference for customizing the Trippy TUI columns. sidebar: order: 4 --- The following table lists the columns that are available for display in the Tui. These can be overridden with the `--tui-custom-columns` command line option or in the `tui-custom-columns` attribute in the `tui` section of the configuration file. | Column | Code | Description | | -------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `#` | `h` | The time-to-live (TTL) for the hop | | `Host` | `o` | The hostname(s) and IP address(s) for the host(s) for the hop
May include AS info, GeoIp and ICMP extensions
Shows full hop details in hop detail navigation mode | | `Loss%` | `l` | The packet loss % for the hop | | `Snd` | `s` | The number of probes sent for the hop | | `Recv` | `r` | The number of probe responses received for the hop | | `Last` | `a` | The round-trip-time (RTT) of the last probe for the hop | | `Avg` | `v` | The average RTT of all probes for the hop | | `Best` | `b` | The best RTT of all probes for the hop | | `Wrst` | `w` | The worst RTT of all probes for the hop | | `StDev` | `d` | The standard deviation of all probes for the hop | | `Sts` | `t` | The status for the hop:
- 🟢 Healthy hop
- 🔵 Non-target hop with packet loss (does not necessarily indicate a problem)
- 🟤 Non-target hop is unresponsive (does not necessarily indicate a problem)
- 🟡 Target hop with packet loss (likely indicates a problem)
- 🔴 Target hop is unresponsive (likely indicates a problem) | | `Jttr` | `j` | The round-trip-time (RTT) difference between consecutive rounds for the hop | | `Javg` | `g` | The average jitter of all probes for the hop | | `Jmax` | `x` | The maximum jitter of all probes for the hop | | `Jint` | `i` | The smoothed jitter value of all probes for the hop | | `Seq` | `Q` | The sequence number for the last probe for the hop | | `Sprt` | `S` | The source port for the last probe for the hop | | `Dprt` | `P` | The destination port for the last probe for the hop | | `Type` | `T` | The icmp packet type for the last probe for the hop:
- TE: TimeExceeded
- ER: EchoReply
- DU: DestinationUnreachable
- NA: NotApplicable | | `Code` | `C` | The icmp packet code for the last probe for the hop | | `NAT` | `N` | The NAT detection status for the hop | | `Fail` | `f` | The number of probes which failed to send for the hop | | `Floss` | `F` | A _heuristic_ for the number of probes with _forward loss_ for the hop | | `Bloss` | `B` | A _heuristic_ for the number of probes with _backward loss_ for the hop | | `Floss%` | `D` | The _forward loss_ % for the hop | | `DSCP` | `K` | Differentiated Services Code Point (DSCP) of the Original Datagram | | `ECN` | `M` | Explicit Congestion Notification (ECN) of the Original Datagram | | `ASN` | `A` | Autonomous System Number (ASN) | The default columns are `holsravbwdt`. :::note The columns will be shown in the order specified in the configuration. ::: ================================================ FILE: docs/src/content/docs/reference/configuration.md ================================================ --- title: Configuration Reference description: A reference for customizing the Trippy configuration. sidebar: order: 2 --- Trippy can be configured with via command line arguments or an optional configuration file. If a given configuration item is specified in both the configuration file and via a command line argument then the latter will take precedence. The configuration file location may be provided to Trippy via the `-c` (`--config-file`) argument. If not provided, Trippy will attempt to locate a `trippy.toml` or `.trippy.toml` configuration file in one of the following locations: - The current directory - The user home directory - the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config` - the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy` - the Windows data directory (Windows only): `%APPDATA%` A template configuration file for [0.13.0](https://github.com/fujiapple852/trippy/blob/0.13.0/trippy-config-sample.toml) is available to download, or can be generated with the following command: ```shell trip --print-config-template > trippy.toml ``` ================================================ FILE: docs/src/content/docs/reference/locale.md ================================================ --- title: Locale Reference description: A reference for customizing the Trippy TUI locale. sidebar: order: 6 --- The following table lists the supported locales for the Tui. These can be overridden with the `--tui-locale` command line option or in the `tui-locale` attribute in the `tui` section of the configuration file. | Locale | Language | Region | | ------ | ---------- | ------ | | `zh` | Chinese | all | | `en` | English | all | | `fr` | French | all | | `de` | German | all | | `it` | Italian | all | | `pt` | Portuguese | all | | `ru` | Russian | all | | `es` | Spanish | all | | `sv` | Swedish | all | | `tr` | Turkish | all | :::note If you are able to help validate translations for Trippy, or if you wish to add translations for any additional languages, please see the [tracking issue](https://github.com/fujiapple852/trippy/issues/506) for details of how to contribute. ::: ================================================ FILE: docs/src/content/docs/reference/overview.mdx ================================================ --- title: Trippy Reference description: Reference documentation for Trippy. sidebar: order: 0 badge: text: New variant: note --- This section provides complete reference documentation for Trippy. :::note This reference documentation is intended for users who are already familiar with Trippy and want to learn more about its features and capabilities. If you are new to Trippy, it is recommend that you start by reading the [getting started](/start/getting-started) guide. ::: ### CLI Reference The [CLI reference](/reference/cli) provides a complete list of the command line options available for Trippy. This information is available via the `--help` command line option and also in the man page on supported platforms. ### Configuration Reference Trippy can be configured via an optional configuration file. The [configuration reference](/reference/configuration) provides details of how to configure Trippy via the configuration file. ### Key Bindings Reference The Trippy TUI is highly customizable and allows you to change the key bindings to suit your preferences. The [key bindings reference](/reference/bindings) provides a complete list of the available key bindings and their descriptions. ### Column Reference The list of columns that can be displayed in the TUI can be customized to suit your needs. The [column reference](/reference/column) provides a complete list of the available columns and their descriptions. ### Theme Reference The color schema of the TUI can be fully customized. The [theme reference](/reference/theme) provides a complete list of the items which can be customized and their descriptions. ### Locale Reference The Trippy TUI supports multiple languages and regions. The [locale reference](/reference/locale) provides a complete list of supported locales. ### Version Reference All versions of Trippy and their support status are listed in the [version reference](/reference/version). ================================================ FILE: docs/src/content/docs/reference/theme.md ================================================ --- title: Theme Reference description: A reference for customizing the Trippy TUI theme. sidebar: order: 5 --- The following table lists the default Tui color theme. These can be overridden with the `--tui-theme-colors` command line option or in the `theme-colors` section of the configuration file. | Item | Description | Default | | ------------------------------------ | --------------------------------------------------------- | ------------ | | `bg-color` | The default background color | `Black` | | `border-color` | The default color of borders | `Gray` | | `text-color` | The default color of text | `Gray` | | `tab-text-color` | The color of the text in traces tabs | `Green` | | `hops-table-header-bg-color` | The background color of the hops table header | `White` | | `hops-table-header-text-color` | The color of text in the hops table header | `Black` | | `hops-table-row-active-text-color` | The color of text of active rows in the hops table | `Gray` | | `hops-table-row-inactive-text-color` | The color of text of inactive rows in the hops table | `DarkGray` | | `hops-chart-selected-color` | The color of the selected series in the hops chart | `Green` | | `hops-chart-unselected-color` | The color of the unselected series in the hops chart | `Gray` | | `hops-chart-axis-color` | The color of the axis in the hops chart | `DarkGray` | | `frequency-chart-bar-color` | The color of bars in the frequency chart | `Green` | | `frequency-chart-text-color` | The color of text in the bars of the frequency chart | `Gray` | | `flows-chart-bar-selected-color` | The color of the selected flow bar in the flows chart | `Green` | | `flows-chart-bar-unselected-color` | The color of the unselected flow bar in the flows chart | `DarkGray` | | `flows-chart-text-current-color` | The color of the current flow text in the flows chart | `LightGreen` | | `flows-chart-text-non-current-color` | The color of the non-current flow text in the flows chart | `White` | | `samples-chart-color` | The color of the samples chart | `Yellow` | | `samples-chart-lost-color` | The color of the samples chart for lost probes | `Red` | | `help-dialog-bg-color` | The background color of the help dialog | `Blue` | | `help-dialog-text-color` | The color of the text in the help dialog | `Gray` | | `settings-dialog-bg-color` | The background color of the settings dialog | `blue` | | `settings-tab-text-color` | The color of the text in settings dialog tabs | `green` | | `settings-table-header-text-color` | The color of text in the settings table header | `black` | | `settings-table-header-bg-color` | The background color of the settings table header | `white` | | `settings-table-row-text-color` | The color of text of rows in the settings table | `gray` | | `map-world-color` | The color of the map world diagram | `white` | | `map-radius-color` | The color of the map accuracy radius circle | `yellow` | | `map-selected-color` | The color of the map selected item box | `green` | | `map-info-panel-border-color` | The color of border of the map info panel | `gray` | | `map-info-panel-bg-color` | The background color of the map info panel | `black` | | `map-info-panel-text-color` | The color of text in the map info panel | `gray` | | `info-bar-bg-color` | The background color of the information bar | `white` | | `info-bar-text-color` | The color of text in the information bar | `black` | The supported [ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) are: - `Black`, `Red`, `Green`, `Yellow`, `Blue`, `Magenta`, `Cyan`, `Gray`, `DarkGray`, `LightRed`, `LightGreen`, `LightYellow`, `LightBlue`, `LightMagenta`, `LightCyan`, `White` In addition, CSS [named colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color) (i.e. SkyBlue) and raw hex values (i.e. ffffff) may be used but note that these are only supported on some platforms and terminals and may not render correctly elsewhere. Color names are case-insensitive and may contain dashes. ================================================ FILE: docs/src/content/docs/reference/version.md ================================================ --- title: Version Reference description: A reference for the Trippy versions. sidebar: order: 7 --- The following table lists this versions of Trippy that are available and links to the corresponding release note and documentation: | Version | Release Date | Status | Release Note | Documentation | | ---------- | ------------ | ----------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | | 0.14.0-dev | n/a | Development | n/a | [docs](https://trippy.rs) | | 0.13.0 | 2025-05-05 | Current | [note](https://github.com/fujiapple852/trippy/releases/tag/0.13.0) | [docs](https://trippy.rs/0.13.0) | | 0.12.2 | 2025-01-03 | Previous | [note](https://github.com/fujiapple852/trippy/releases/tag/0.12.2) | [docs](https://trippy.rs/0.12.2) | | 0.11.0 | 2024-08-11 | Previous | [note](https://github.com/fujiapple852/trippy/releases/tag/0.11.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.11.0) | | 0.10.0 | 2024-03-31 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.10.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.10.0) | | 0.9.0 | 2023-11-30 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.9.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.9.0) | | 0.8.0 | 2023-05-15 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.8.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.8.0) | | 0.7.0 | 2023-03-25 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.7.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.7.0) | | 0.6.0 | 2022-08-19 | Deprecated | [note](https://github.com/fujiapple852/trippy/releases/tag/0.6.0) | [docs](https://github.com/fujiapple852/trippy/tree/0.6.0) | :::note Only the _latest patch versions_ of both the _current_ and _previous_ releases of Trippy are supported. ::: ================================================ FILE: docs/src/content/docs/start/features.md ================================================ --- title: Features description: Learn about the features of Trippy. sidebar: order: 3 --- - Trace using multiple protocols: - `ICMP`, `UDP` & `TCP` - `IPv4` & `IPv6` - Customizable tracing options: - packet size & payload pattern - start and maximum time-to-live (TTL) - minimum and maximum round duration - round end grace period & maximum number of unknown hops - source & destination port (`TCP` & `UDP`) - source address and source interface - `TOS` (aka `DSCP + ECN`) - Support for `classic`, `paris` and `dublin` [Equal Cost Multi-path Routing](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) strategies ([tracking issue](https://github.com/fujiapple852/trippy/issues/274)) - RFC4884 [ICMP Multi-Part Messages](https://datatracker.ietf.org/doc/html/rfc4884) - Generic Extension Objects - MPLS Label Stacks - Unprivileged mode - NAT detection - Tui interface: - Trace multiple targets simultaneously from a single instance of Trippy - Per hop stats (sent, received, loss%, last, avg, best, worst, stddev, jitter & status) - Per hop round-trip-time (RTT) history and frequency distributing charts - Interactive chart of RTT for all hops in a trace with zooming capability - Interactive GeoIp world map - Isolate and filter by individual tracing flows - Customizable color theme & key bindings - Customizable column order and visibility - Configuration via both command line arguments and a configuration file - Show multiple hosts per hop with ability to cap display to N hosts and show frequency % - Show hop details and navigate hosts within each hop - Freeze/unfreeze the Tui, reset the stats, flush the cache, preserve screen on exit - Responsive UI with adjustable refresh rate - Hop privacy - Multiple language support - Customizable timezone - DNS: - Use system, external (Google `8.8.8.8` or Cloudflare `1.1.1.1`) or custom resolver - Lazy reverse DNS queries - Lookup [autonomous system][autonomous_system] number (ASN) and name - GeoIp: - Lookup and display GeoIp information from local [MaxMind](https://www.maxmind.com) and [IPinfo](https://ipinfo.io) `mmdb` files - Generate tracing reports: - `json`, `csv` & tabular (pretty-printed and markdown) - Tracing `flows` report - Graphviz `dot` charts - configurable reporting cycles - Runs on multiple platform (macOS, Linux, Windows, NetBSD, FreeBSD, OpenBSD) - Capabilities aware application (Linux only) ================================================ FILE: docs/src/content/docs/start/getting-started.mdx ================================================ --- title: Getting Started description: Get started with Trippy. sidebar: order: 1 --- import { Steps } from '@astrojs/starlight/components'; The following steps will guide you through the process of installing and running Trippy. 1. Install Trippy: Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled binaries, or source. For example, to install Trippy from `cargo`: ```shell cargo install trippy --locked ``` See the [installation guide](/start/installation) for details of how to install Trippy on your system. 2. Run Trippy: To run a basic trace to `example.com` with default settings, use the following command: ```shell sudo trip example.com ``` See the [usage examples](/guides/usage) and the [CLI reference](/reference/cli) for details of how to use Trippy. To use Trippy without elevated privileges, see the [privileges guide](/guides/privileges). 3. Customize the key bindings, theme and columns: See the [key bindings reference](/reference/bindings), [theme reference](/reference/theme) and [column reference](/reference/column) for details of how to customize the appearance and behavior of Trippy. These settings can be made permanent by adding them to the Trippy configuration file, see the [configuration reference](/reference/configuration) for details. 4. Review the tracing recommendations: To get the most out of Trippy, review the [recommended tracing settings](/guides/recommendation) for guidance on how to configure Trippy for different types of analysis. Happy tracing! ================================================ FILE: docs/src/content/docs/start/installation.md ================================================ --- title: Installation description: Install Trippy on your platform. sidebar: order: 2 --- The following sections provide instructions for installing Trippy on your platform. Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled binaries, or source. ## Distributions Trippy is available for a variety of platforms and package managers. ### Cargo [![Crates.io](https://img.shields.io/crates/v/trippy)](https://crates.io/crates/trippy/0.13.0) ```shell cargo install trippy --locked ``` ### APT (Debian) [![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy) ```shell apt install trippy ``` :::note Only available for Debian 13 (`trixie`) and later. ::: ### PPA (Ubuntu) [![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.13.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages) ```shell add-apt-repository ppa:fujiapple/trippy apt update && apt install trippy ``` :::note Only available for Ubuntu 24.04 (`Noble`) and 22.04 (`Jammy`). ::: ### Snap (Linux) [![trippy](https://snapcraft.io/trippy/badge.svg)](https://snapcraft.io/trippy) ```shell snap install trippy ``` ### Homebrew (macOS) [![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/trippy.svg)](https://formulae.brew.sh/formula/trippy) ```shell brew install trippy ``` ### WinGet (Windows) [![winget package](https://img.shields.io/badge/WinGet-0.13.0-brightgreen)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/f/FujiApple/Trippy/0.13.0) ```shell winget install trippy ``` ### Scoop (Windows) [![Scoop package](https://img.shields.io/scoop/v/trippy?style=flat&labelColor=5c5c5c&color=%234dc71f)](https://github.com/ScoopInstaller/Main/blob/master/bucket/trippy.json) ```shell scoop install trippy ``` ### Chocolatey (Windows) [![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy) ```shell choco install trippy ``` ### NetBSD [![pkgsrc current package](https://repology.org/badge/version-for-repo/pkgsrc_current/trippy.svg)](https://pkgsrc.se/net/trippy) ```shell pkgin install trippy ``` ### FreeBSD [![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/trippy.svg)](https://www.freshports.org/net/trippy/) ```shell pkg install trippy ``` ### OpenBSD [![OpenBSD port](https://repology.org/badge/version-for-repo/openbsd/trippy.svg)](https://openports.pl/path/net/trippy) ```shell pkg_add trippy ``` ### Arch Linux [![Arch package](https://repology.org/badge/version-for-repo/arch/trippy.svg)](https://archlinux.org/packages/extra/x86_64/trippy) ```shell pacman -S trippy ``` ### Gentoo Linux [![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/trippy.svg)](https://packages.gentoo.org/packages/net-analyzer/trippy) ```shell emerge -av net-analyzer/trippy ``` ### Void Linux [![Void Linux x86_64 package](https://repology.org/badge/version-for-repo/void_x86_64/trippy.svg)](https://github.com/void-linux/void-packages/tree/master/srcpkgs/trippy) ```shell xbps-install -S trippy ``` ### ALT Sisyphus [![ALT Sisyphus package](https://repology.org/badge/version-for-repo/altsisyphus/trippy.svg)](https://packages.altlinux.org/en/sisyphus/srpms/trippy/) ```shell apt-get install trippy ``` ### Chimera Linux [![Chimera Linux package](https://repology.org/badge/version-for-repo/chimera/trippy.svg)](https://github.com/chimera-linux/cports/tree/master/user/trippy) ```shell apk add trippy ``` ### Nix [![nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/trippy.svg)](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/tr/trippy/package.nix) ```shell nix-env -iA trippy ``` ### Docker [![Docker Image Version (latest by date)](https://img.shields.io/docker/v/fujiapple/trippy)](https://hub.docker.com/r/fujiapple/trippy/) ```shell docker run -it fujiapple/trippy ``` :::note See the [Docker guide](/guides/docker) for more information. ::: ### All Repositories [![Packaging status](https://repology.org/badge/vertical-allrepos/trippy.svg)](https://repology.org/project/trippy/versions) ## Downloads Download the latest release for your platform. | OS | Arch | Env | Current (0.13.0) | Previous (0.12.2) | Previous (0.11.0) | | ------- | --------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Linux | `x86_64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-unknown-linux-gnu.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-linux-gnu.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-linux-gnu.tar.gz) | | Linux | `x86_64` | `musl` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-unknown-linux-musl.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-linux-musl.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-linux-musl.tar.gz) | | Linux | `aarch64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-aarch64-unknown-linux-gnu.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-unknown-linux-gnu.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-unknown-linux-gnu.tar.gz) | | Linux | `aarch64` | `musl` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-aarch64-unknown-linux-musl.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-unknown-linux-musl.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-unknown-linux-musl.tar.gz) | | Linux | `arm7` | `gnueabihf` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-armv7-unknown-linux-gnueabihf.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-armv7-unknown-linux-gnueabihf.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-armv7-unknown-linux-gnueabihf.tar.gz) | | Linux | `arm7` | `musleabi` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-armv7-unknown-linux-musleabi.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-armv7-unknown-linux-musleabi.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-armv7-unknown-linux-musleabi.tar.gz) | | Linux | `arm7` | `musleabihf` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-armv7-unknown-linux-musleabihf.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-armv7-unknown-linux-musleabihf.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-armv7-unknown-linux-musleabihf.tar.gz) | | macOS | `x86_64` | `darwin` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-apple-darwin.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-apple-darwin.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-apple-darwin.tar.gz) | | macOS | `aarch64` | `darwin` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-aarch64-apple-darwin.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-apple-darwin.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-apple-darwin.tar.gz) | | Windows | `x86_64` | `msvc` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-pc-windows-msvc.zip) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-pc-windows-msvc.zip) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-pc-windows-msvc.zip) | | Windows | `x86_64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-pc-windows-gnu.zip) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-pc-windows-gnu.zip) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-pc-windows-gnu.zip) | | Windows | `aarch64` | `msvc` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-aarch64-pc-windows-msvc.zip) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-aarch64-pc-windows-msvc.zip) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-aarch64-pc-windows-msvc.zip) | | FreeBSD | `x86_64` | n/a | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-unknown-freebsd.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-freebsd.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-freebsd.tar.gz) | | NetBSD | `x86_64` | n/a | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64-unknown-netbsd.tar.gz) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64-unknown-netbsd.tar.gz) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64-unknown-netbsd.tar.gz) | | RPM | `x86_64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy-0.13.0-x86_64.rpm) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy-0.12.2-x86_64.rpm) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy-0.11.0-x86_64.rpm) | | Debian | `x86_64` | `gnu` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy_x86_64-unknown-linux-gnu_0.13.0_amd64.deb) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy_x86_64-unknown-linux-gnu_0.12.2_amd64.deb) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy_x86_64-unknown-linux-gnu_0.11.0_amd64.deb) | | Debian | `x86_64` | `musl` | [0.13.0](https://github.com/fujiapple852/trippy/releases/download/0.13.0/trippy_x86_64-unknown-linux-musl_0.13.0_amd64.deb) | [0.12.2](https://github.com/fujiapple852/trippy/releases/download/0.12.2/trippy_x86_64-unknown-linux-musl_0.12.2_amd64.deb) | [0.11.0](https://github.com/fujiapple852/trippy/releases/download/0.11.0/trippy_x86_64-unknown-linux-musl_0.11.0_amd64.deb) | ================================================ FILE: docs/src/content/versions/0.12.2.json ================================================ { "sidebar": [ { "label": "Start Here", "autogenerate": { "directory": "start" } }, { "label": "Guides", "autogenerate": { "directory": "guides" } }, { "label": "Reference", "autogenerate": { "directory": "reference" } }, { "label": "Development", "autogenerate": { "directory": "development" } } ] } ================================================ FILE: docs/src/content/versions/0.13.0.json ================================================ { "sidebar": [ { "label": "Start Here", "autogenerate": { "directory": "start" } }, { "label": "Guides", "autogenerate": { "directory": "guides" } }, { "label": "Reference", "autogenerate": { "directory": "reference" } }, { "label": "Development", "autogenerate": { "directory": "development" } } ] } ================================================ FILE: docs/src/env.d.ts ================================================ /// /// ================================================ FILE: docs/src/styles/custom.css ================================================ /* Dark mode colors. */ :root { --sl-color-accent-low: #131e4f; --sl-color-accent: #3447ff; --sl-color-accent-high: #b3c7ff; --sl-color-white: #ffffff; --sl-color-gray-1: #eceef2; --sl-color-gray-2: #c0c2c7; --sl-color-gray-3: #888b96; --sl-color-gray-4: #545861; --sl-color-gray-5: #353841; --sl-color-gray-6: #24272f; --sl-color-black: #17181c; } /* Light mode colors. */ :root[data-theme='light'] { --sl-color-accent-low: #c7d6ff; --sl-color-accent: #364bff; --sl-color-accent-high: #182775; --sl-color-white: #17181c; --sl-color-gray-1: #24272f; --sl-color-gray-2: #353841; --sl-color-gray-3: #545861; --sl-color-gray-4: #888b96; --sl-color-gray-5: #c0c2c7; --sl-color-gray-6: #eceef2; --sl-color-gray-7: #f5f6f8; --sl-color-black: #ffffff; } ================================================ FILE: docs/tsconfig.json ================================================ { "extends": "astro/tsconfigs/strict" } ================================================ FILE: dprint.json ================================================ { "markdown": { }, "toml": { "cargo.applyConventions": false }, "excludes": [], "plugins": [ "https://plugins.dprint.dev/markdown-0.17.8.wasm", "https://plugins.dprint.dev/dockerfile-0.3.2.wasm", "https://plugins.dprint.dev/toml-0.6.3.wasm" ] } ================================================ FILE: examples/README.md ================================================ ## Examples The following is a list of examples provided. > [!NOTE] > Examples must be run with privileges, see [README.md](../README.md#privileges) for more information. ### `hello-world` A simple example that demonstrates running a traceroute and printing the output of each round. ```shell cargo run -p hello-world ``` ### `toy-traceroute` A toy clone of the BSD4.3 (macOS) system traceroute tool. ```shell cargo run -p toy-traceroute 1.1.1.1 ``` ================================================ FILE: examples/hello-world/Cargo.toml ================================================ [package] name = "hello-world" version = "0.1.0" license = "Apache-2.0" edition = "2024" rust-version = "1.85" publish = false [dependencies] trippy = { version = "0.14.0-dev", path = "../../crates/trippy", default-features = false, features = ["core"] } anyhow = "1.0.86" ================================================ FILE: examples/hello-world/src/main.rs ================================================ use std::str::FromStr; use trippy::core::Builder; fn main() -> anyhow::Result<()> { let addr = std::net::IpAddr::from_str("1.1.1.1")?; Builder::new(addr) .build()? .run_with(|round| println!("{round:?}"))?; Ok(()) } ================================================ FILE: examples/toy-traceroute/Cargo.toml ================================================ [package] name = "toy-traceroute" version = "0.1.0" license = "Apache-2.0" edition = "2024" rust-version = "1.85" publish = false [dependencies] trippy = { version = "0.14.0-dev", path = "../../crates/trippy", default-features = false, features = ["core", "dns"] } anyhow = "1.0.86" itertools = "0.14.0" clap = { version = "4.5.60", features = ["derive"] } ================================================ FILE: examples/toy-traceroute/src/main.rs ================================================ use anyhow::anyhow; use clap::Parser; use itertools::Itertools; use std::net::IpAddr; use std::str::FromStr; use std::time::Duration; use trippy::core::{Builder, PortDirection, Protocol}; use trippy::dns::{Config, DnsResolver, Resolver}; /// A toy clone of BSD4.3 (macOS) traceroute. /// /// *** This is for demonstration purposes only. *** #[derive(Parser, Debug)] #[command(version, about, long_about = None, arg_required_else_help(true))] struct Args { host: String, #[arg(short = 'f')] first_ttl: Option, #[arg(short = 'm')] max_ttl: Option, #[arg(short = 'i')] interface: Option, #[arg(short = 'p')] port: Option, #[arg(short = 'q')] nqueries: Option, #[arg(short = 's')] src_addr: Option, #[arg(short = 't')] tos: Option, #[arg(short = 'z')] pausemsecs: Option, #[arg(short = 'e')] evasion: bool, } fn main() -> anyhow::Result<()> { let args = Args::parse(); let hostname = args.host; let interface = args.interface; let src_addr = args .src_addr .as_ref() .map(|addr| IpAddr::from_str(addr)) .transpose()?; let port = args.port.unwrap_or(33434); let first_ttl = args.first_ttl.unwrap_or(1); let max_ttl = args.max_ttl.unwrap_or(64); let nqueries = args.nqueries.unwrap_or(3); let tos = args.tos.unwrap_or(0); let pausemsecs = args.pausemsecs.unwrap_or(100); let port_direction = if args.evasion { PortDirection::new_fixed_dest(port) } else { PortDirection::new_fixed_src(port) }; let resolver = DnsResolver::start(Config::default())?; let addrs: Vec<_> = resolver .lookup(&hostname) .map_err(|_| anyhow!(format!("traceroute: unknown host {}", hostname)))? .into_iter() .collect(); let addr = match addrs.as_slice() { [] => return Err(anyhow!("traceroute: unknown host {}", hostname)), [addr] => *addr, [addr, ..] => { println!("traceroute: Warning: {hostname} has multiple addresses; using {addr}"); *addr } }; let tracer = Builder::new(addr) .interface(interface) .source_addr(src_addr) .protocol(Protocol::Udp) .port_direction(port_direction) .packet_size(52) .first_ttl(first_ttl) .max_ttl(max_ttl) .tos(tos) .max_flows(1) .max_rounds(Some(nqueries)) .min_round_duration(Duration::from_millis(pausemsecs)) .max_round_duration(Duration::from_millis(pausemsecs)) .build()?; println!( "traceroute to {} ({}), {} hops max, {} byte packets", &hostname, tracer.target_addr(), tracer.max_ttl().0, tracer.packet_size().0 ); tracer.run()?; let snapshot = &tracer.snapshot(); if let Some(err) = snapshot.error() { return Err(anyhow!("error: {}", err)); } for hop in snapshot.hops() { let ttl = hop.ttl(); let samples: String = hop .samples() .iter() .map(|s| format!("{:.3} ms", s.as_secs_f64() * 1000_f64)) .join(" "); if hop.addr_count() > 0 { for (i, addr) in hop.addrs().enumerate() { let host = resolver.reverse_lookup(*addr).to_string(); let address = format!("{host} ({addr})"); if i != 0 { println!(" {address} {samples}"); } else { println!(" {ttl} {address} {samples}"); } } } else { println!(" {ttl} * * * {samples}"); } } Ok(()) } ================================================ FILE: snap/snapcraft.yaml ================================================ name: trippy version: '0.14.0-dev' summary: A network diagnostic tool description: | Trippy combines the functionality of traceroute and ping and is designed to assist with the diagnosis of network issues. Features: - Trace using multiple protocols: - `ICMP`, `UDP` & `TCP` - `IPv4` & `IPv6` - Customizable tracing options: - packet size & payload pattern - start and maximum time-to-live (TTL) - minimum and maximum round duration - round end grace period & maximum number of unknown hops - source & destination port (`TCP` & `UDP`) - source address and source interface - `TOS` (aka `DSCP + ECN`) - Equal Cost Multi-path Routing strategies (`classic`, `paris` and `dublin`) - RFC4884 ICMP Multi-Part Messages - Generic Extension Objects - MPLS Label Stacks - Unprivileged mode - NAT detection - Tui interface: - Trace multiple targets simultaneously from a single instance of Trippy - Per hop stats (sent, received, loss%, last, avg, best, worst, stddev, jitter & status) - Per hop round-trip-time (RTT) history and frequency distributing charts - Interactive chart of RTT for all hops in a trace with zooming capability - Interactive GeoIp world map - Isolate and filter by individual tracing flows - Customizable color theme & key bindings - Customizable column order and visibility - Configuration via both command line arguments and a configuration file - Show multiple hosts per hop with ability to cap display to N hosts and show frequency % - Show hop details and navigate hosts within each hop - Freeze/unfreeze the Tui, reset the stats, flush the cache, preserve screen on exit - Responsive UI with adjustable refresh rate - Hop privacy - DNS: - Use system, external (Google `8.8.8.8` or Cloudflare `1.1.1.1`) or custom resolver - Lazy reverse DNS queries - Lookup [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_(Internet)) number (ASN) and name - GeoIp: - Lookup and display GeoIp information from local [MaxMind](https://www.maxmind.com) and [IPinfo](https://ipinfo.io) `mmdb` files - Generate tracing reports: - `json`, `csv` & tabular (pretty-printed and markdown) - Tracing `flows` report - Graphviz `dot` charts - configurable reporting cycles - Runs on multiple platform (macOS, Linux, NetBSD, FreeBSD, Windows) - Capabilities aware application (Linux only) This package auto-connects to the following snap interfaces: - `network`: to allow general outgoing network access - `network-bind`: to allow binding to local ports - `network-observe`: to allow enabling `CAP_NET_RAW` for using raw sockets - `home`: to allow access to /home for reading the configuration file contact: mailto:fujiapple852@gmail.com issues: https://github.com/fujiapple852/trippy/issues license: Apache-2.0 source-code: https://github.com/fujiapple852/trippy website: https://trippy.rs base: core20 grade: stable confinement: strict parts: trippy: plugin: rust source: . organize: trip: usr/bin/trip apps: trippy: command: usr/bin/trip plugs: - network-bind - network - network-observe - home plugs: home: read: all ================================================ FILE: trippy-config-sample.toml ================================================ # Sample config file for Trippy. # # Copy this template config file to your platform specific config dir. # # Trippy will attempt to locate a `trippy.toml` or `.trippy.toml` config file # in one of the following locations: # the current directory # the user home directory # the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config` # the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy` # the Windows data directory (Windows only): `%APPDATA%` # # You may override the config file name and location by passing the `-c` # (`--config-file`) command line argument. # # All sections and all items within each section are non-mandatory. # # General Trippy configuration. # [trippy] # The Trippy mode. # # Allowed values are: # tui - Display interactive Tui [default] # stream - Display a continuous stream of tracing data # pretty - Generate a pretty text table report for N cycles # markdown - Generate a Markdown text table report for N cycles # csv - Generate a CSV report for N cycles # json - Generate a JSON report for N cycles # dot - Generate a Graphviz DOT report for N cycles # flows - Display all flows for N cycles # silent - Do not generate any output for N cycles # # Note: the dot and flows modes are only allowed with paris or dublin # multipath strategy. mode = "tui" # Trace without requiring elevated privileges [default: false] # # Enabling will cause IPPROTO_ICMP sockets to be used. # # Note: not supported on all platforms. unprivileged = false # How to format log data. # # Allowed values are: # compact - Display log data in a compact format # pretty - Display log data in a pretty format [default] # json - Display log data in a json format # chrome - Display log data in Chrome trace format log-format = "pretty" # The debug log filter [default: trippy=debug] log-filter = "trippy=debug" # How to log event spans. # # Allowed values are: # off - Do not display event spans [default] # active - Display enter and exit event spans # full - Display all event spans log-span-events = "off" # # Tracing strategy configuration. # [strategy] # The tracing protocol. # # Allowed values are: # icmp [default] # udp # tcp protocol = "icmp" # The address family. # # Allowed values are: # ipv4 - Lookup IPv4 only # ipv6 - Lookup IPv6 only # ipv6-then-ipv4 - Lookup IPv6 with a fallback to IPv4 # ipv4-then-ipv6 - Lookup IPv4 with a fallback to IPv6 # system - If the OS resolver is being used then use the first IP address returned, # otherwise lookup IPv4 with a fallback to IPv6 [default] addr-family = "system" # The target port (TCP & UDP only) [default: 80] # # Applicable for TCP and UDP protocols only. # target-port = 80 # The source port (TCP & UDP only) [default: auto] # # Applicable for TCP and UDP protocols only. # source-port = 1234 # The source IP address [default: auto] # # If unspecified the source address will be chosen automatically based on the tracing target. # source-address = "1.2.3.4" # The network interface [default: auto] # # If not specified the interface is chosen based on the source-address. # interface = "en0" # The minimum duration of every round [default: 1s] # # The minimum time that must elapse before a tracing round is considered # complete, regardless of whether the target is discovered or not. min-round-duration = "1s" # The maximum duration of every round [default: 1s] # # The maximum time that may elapse before a tracing round is considered # complete, regardless of whether the target is discovered or not. max-round-duration = "1s" # The round grace period [default: 100ms] # # The period of time to wait for additional probe responses after the target # has responded. grace-duration = "100ms" # The initial sequence number [default: 33434] initial-sequence = 33434 # The Equal-cost Multi-Path routing strategy (UDP only) # # Allowed value are: # classic - The src or dest port is used to store the sequence number [default] # paris - The UDP `checksum` field is used to store the sequence number # dublin - The IP `identifier` field is used to store the sequence number # # See https://github.com/fujiapple852/trippy/issues/274 for more details. multipath-strategy = "classic" # The maximum number of in-flight ICMP echo requests [default: 24] # # The tracing strategy operates a sliding window protocol and will allow a # maximum number of probes to be inflight (sent, and not received or lost) # at any given time. max-inflight = 24 # The TTL to start from [default: 1] first-ttl = 1 # The maximum number of TTL hops [default: 64] max-ttl = 64 # The size of IP packet to send [default: 84] # # For icmp this is the sum of the IP header, ICMP header and the payload. # Trippy will adjust the size of the payload to fill up to the packet size. packet-size = 84 # The repeating pattern in the payload of the ICMP packet [default: 0] payload-pattern = 0 # The TOS IP header value (IPv4 only) [default: 0] # # This is also known as DSCP+ECN. tos = 0 # Whether to parse ICMP extensions. # # If enabled, all extensions attached to incoming ICMP TimeExceeded and DestinationUnavailable messages will be parsed # and provided as part of the trace response data. # # The following ICMP Extension Object Classes are supported: # 1 - MPLS Label Stack Class (RFC4950) # # Extension objects with an unknown class will be parsed to capture generic information including the class, subtype, # length and payload bytes. icmp-extensions = false # The socket read timeout [default: 10ms] read-timeout = "10ms" # The maximum number of samples to record per hop [default: 256] max-samples = 256 # The maximum number of flows to record [default: 64] max-flows = 64 # # DNS configuration. # [dns] # How DNS queries are resolved # # Allowed values are: # system - Resolve using the OS resolver [default] # resolv - Resolve using the `/etc/resolv.conf` DNS configuration # google - Resolve using the Google `8.8.8.8` DNS service # cloudflare - Resolve using the Cloudflare `1.1.1.1` DNS service dns-resolve-method = "system" # Trace to all IPs resolved from DNS lookup (ICMP only) [default: false] # # When set to true a trace will be started for all IPs resolved for all given targets. # When set to false a trace will be started for one arbitrarily chosen IP per given target. dns-resolve-all = false # Whether to lookup AS information [default: false] # # If enabled, AS (autonomous system) information is retrieved during DNS # queries. dns-lookup-as-info = false # The maximum time to wait to perform DNS queries [default: 5s] dns-timeout = "5s" # The time-to-live (TTL) for DNS entries [default: 300s] dns-ttl = "300s" # # Report generation configuration. # [report] # The number of report cycles to run [default: 10] # # Only applicable for modes pretty, markdown, csv and json. report-cycles = 10 # # General Tui Configuration. # [tui] # How to render addresses. # # Allowed values are: # ip - Show IP address only # host - Show reverse-lookup DNS hostname only [default] # both - Show both IP address and reverse-lookup DNS hostname tui-address-mode = "host" # How to render autonomous system (AS) information. # # Allowed values are: # asn - Show the ASN [default] # prefix - Display the AS prefix # country-code - Display the country code # registry - Display the registry name # allocated - Display the allocated date # name - Display the AS name tui-as-mode = "asn" # Custom columns to be displayed in the TUI hops table. # # Default values: # # h - Ttl # o - Hostname # l - Loss % # s - Probes sent # r - Responses received # a - Last RTT # v - Average RTT # b - Best RTT # w - Worst RTT # d - Stddev # t - Status # # Also available: # # j - Jitter # g - Jitter average # x - Jitter max # i - Jitter intra # Q - Last probe sequence number # S - Last probe source port # P - Last probe destination port # T - Last icmp packet type # C - Last icmp packet code # N - Last NAT status # f - Probes failed # F - Forward loss # B - Backward loss # D - Forward loss % # K - Differentiated Services Code Point (DSCP) of the Original Datagram # M - Explicit Congestion Notification (ECN) of the Original Datagram # A - Autonomous System Number (ASN) # # The columns will be shown in the order specified. tui-custom-columns = "holsravbwdt" # How to render ICMP extensions. # # off - Do not show icmp extensions [default] # mpls - Show MPLS label(s) only # full - Show full icmp extension data for all known extensions # all - Show full icmp extension data for all classes tui-icmp-extension-mode = "off" # The mmdb file to use GeoIp lookup [default: none] # # Supported mmdb formats: # MaxMind "GeoLite2 City" # IPinfo "IP to Country + ASN Database" # IPinfo "IP to Geolocation Extended Database" # geoip-mmdb-file = "/path/to/geoip_file.mmdb" # How to render GeoIp information. # # Allowed values are: # off - Do not show GeoIp information [default] # short - Show short format GeoIp information # long - Show long format GeoIp information # location - Show latitude and Longitude format GeoIp information # # Note this value is ignored unless a valid geoip-mmdb-file value is also provided. tui-geoip-mode = "off" # The maximum number of addresses to show per hop [default: auto] # # Use a zero value for `auto`. tui-max-addrs = 0 # Whether to preserve the screen on exit [default: false] tui-preserve-screen = false # The Tui refresh rate [default: 100ms] tui-refresh-rate = "100ms" # The maximum ttl of hops which will be masked for privacy [default: none] # tui-privacy-max-ttl = 0 # The locale to use for Tui [default: auto] # tui-locale = "en-US" # The timezone for displaying dates and time [default: auto] # # The timezone must be a valid IANA timezone identifier. # tui-timezone = "UTC" # Tui color theme configuration. # # The supported ANSI color values are: # Black, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, DarkGray, LightRed, # LightGreen, LightYellow, LightBlue, LightMagenta, LightCyan, White # # In addition, CSS named colors (i.e. SkyBlue) and raw hex values (i.e. ffffff) # may be used but note that these are only supported on some platforms and # terminals and may not render correctly elsewhere. # # Color names are case-insensitive and may contain dashes. # # See https://github.com/fujiapple852/trippy#theme-reference for details. [theme-colors] bg-color = "black" border-color = "gray" text-color = "gray" tab-text-color = "green" hops-table-header-bg-color = "white" hops-table-header-text-color = "black" hops-table-row-active-text-color = "gray" hops-table-row-inactive-text-color = "darkgray" hops-chart-selected-color = "green" hops-chart-unselected-color = "gray" hops-chart-axis-color = "darkgray" frequency-chart-bar-color = "green" frequency-chart-text-color = "gray" flows-chart-bar-selected-color = "green" flows-chart-bar-unselected-color = "darkgray" flows-chart-text-current-color = "lightgreen" flows-chart-text-non-current-color = "white" samples-chart-color = "yellow" samples-chart-lost-color = "red" help-dialog-bg-color = "blue" help-dialog-text-color = "gray" settings-dialog-bg-color = "blue" settings-tab-text-color = "green" settings-table-header-text-color = "black" settings-table-header-bg-color = "white" settings-table-row-text-color = "gray" map-world-color = "white" map-radius-color = "yellow" map-selected-color = "green" map-info-panel-border-color = "gray" map-info-panel-bg-color = "black" map-info-panel-text-color = "gray" info-bar-bg-color = "white" info-bar-text-color = "black" # Tui key bindings Configuration. # # The supported modifiers are: shift, ctrl, alt, super, hyper & meta. Multiple # modifiers may be specified, for example ctrl+shift+b. # # See https://github.com/fujiapple852/trippy#key-bindings-reference for details. [bindings] toggle-help = "h" toggle-help-alt = "?" toggle-settings = "s" toggle-settings-tui = "1" toggle-settings-trace = "2" toggle-settings-dns = "3" toggle-settings-geoip = "4" toggle-settings-bindings = "5" toggle-settings-theme = "6" toggle-settings-columns = "7" next-hop = "down" previous-hop = "up" next-trace = "right" previous-trace = "left" next-hop-address = "." previous-hop-address = "," address-mode-ip = "i" address-mode-host = "n" address-mode-both = "b" toggle-freeze = "ctrl+f" toggle-chart = "c" toggle-map = "m" toggle-flows = "f" expand-privacy = "p" contract-privacy = "o" expand-hosts = "]" expand-hosts-max = "}" contract-hosts = "[" contract-hosts-min = "{" chart-zoom-in = "=" chart-zoom-out = "-" clear-trace-data = "ctrl+r" clear-dns-cache = "ctrl+k" clear-selection = "esc" toggle-as-info = "z" toggle-hop-details = "d" quit = "q" quit-preserve-screen = "shift+q" ================================================ FILE: ubuntu-ppa/Dockerfile ================================================ FROM ubuntu:noble ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ gpg debmake debhelper devscripts equivs \ distro-info-data distro-info software-properties-common cargo cargo-1.85 wget COPY release.sh release.sh WORKDIR /data CMD ["./ubuntu-ppa/release.sh"] ================================================ FILE: ubuntu-ppa/README.debian ================================================ Trippy packaging for Debian and Ubuntu ====================================== TL;DR: to generate your own debian package with your own Rust toolchain, the vendored dependencies need to be generated first with: ./debian/rules vendor then you can simply run: debuild --prepend-path ~/.cargo/bin -sa as long as the original Trippy source archive `trippy_.orig.tar.gz` exists in the parent directory. --- The debian directory contains the necessary files to generate a debian package. In order for the package to be built without network access (a requirement for most automatic build systems, such as Debian's and Canonical's), we cannot rely on the Cargo automatic dependencies resolution. Instead, the `vendor` rule uses [`cargo vendor`] [1] to "vendor" all crates.io dependencies for the project into a `debian/vendor.tar.xz` tarball. This tarball contains all remote sources from dependencies that are specified in the Cargo manifest. It is automatically extracted during the build, which uses the [`--frozen`] [2] option to prevent Cargo from attempting to access the network. Once this tarball is generated you only need to use `vendor` rule again if you want to refresh the sources of the dependencies. --- The creation and administration of a Personal Package Archive (PPA) is beyond the scope of this doc, but if you need to host Trippy in your PPA, you simply need to run: debuild --prepend-path ~/.cargo/bin -S -sa followed by: dput ../.changes The provided `debian` directory targets the Ubuntu Jammy 22.04 LTS distribution. It is possible to target other distributions simply by editing the `debian/changelog` file and changing the version and distribution fields: trippy (0.14.0-dev-1ubuntu0.1~jammy1) jammy; urgency=medium trippy (0.14.0-dev-1ubuntu0.1~mantis1) mantis; urgency=medium trippy (0.14.0-dev-1ubuntu0.1~noble1) noble; urgency=medium It is preferable to use `debchange` for this, eg: debchange --distribution noble --newversion 0.14.0-dev-1ubuntu0.1~noble1 --- NOTES: - all `commands` are relative to the Trippy source directory. - the tarball is compressed with xz as per the [blog post] [3] I used as a reference. TODOS: - remove Windows-specific dependencies from the vendored dependencies, see [Cargo issue #11929] [4] - move the vendor tarball outside of the debian directory, but this can only be done once it's been relieved of the Windows-specific dependencies. REFERENCES: [1]: https://doc.rust-lang.org/cargo/commands/cargo-vendor.html [2]: https://doc.rust-lang.org/cargo/commands/cargo.html?highlight=frozen#manifest-options [3]: https://blog.zhimingwang.org/packaging-rust-project-for-ubuntu-ppa "Packaging a Rust project for Ubuntu PPA" [4]: https://github.com/rust-lang/cargo/issues/11929 ================================================ FILE: ubuntu-ppa/README.md ================================================ # Debian Release ## Prerequisites - update the `cargo-1.xx` version in `ubuntu-ppa/Dockerfile` (note: both `cargo` and `cargo-1.xx` are needed) - update the `cargo-1.xx` and `rust-1.xx` versions in `control` - update the `cargo-1.xx` versions in `rules` - update the `cargo-1.xx` versions in `release.sh` - update the trippy `VERSION` in the `release.sh` script - update the `UPSTREAM` in the `release.sh` script (removing any `+repack{N}` suffix) - reset the `REVISION` to `1` in the `release.sh` script ## Build and release the debian package Copy the pgp key to the repo _root_ directory: ```bash cp /path/to/pgp.key . ``` Build the debian ppa builder Docker image from the `ubuntu-ppa` directory: ```bash docker build . -t fujiapple/trippy-ppa-build:latest ``` Run the debian Docker image (from the _repo_ root directory): ```bash docker run -it -v (pwd):/data fujiapple/trippy-ppa-build ``` Note that the upload is simulated, remove the `-ss` flag from dput to upload the package to the PPA. ================================================ FILE: ubuntu-ppa/cargo.config ================================================ [source.crates-io] replace-with = "vendored-sources" [source.vendored-sources] directory = "vendor" ================================================ FILE: ubuntu-ppa/changelog ================================================ trippy (0.14.0-dev-ppa2~ubuntu24.04) noble; urgency=medium * New upstream release -- Fuji Apple Wed, 21 Dec 2024 12:34:56 +0000 ================================================ FILE: ubuntu-ppa/control ================================================ Source: trippy Section: contrib/net Priority: optional Maintainer: Fuji Apple Rules-Requires-Root: no Build-Depends: debhelper-compat (= 13), cargo-1.85, rustc-1.85, libstd-rust-dev Standards-Version: 4.6.2 Vcs-Browser: https://github.com/fujiapple852/trippy Vcs-Git: https://github.com/fujiapple852/trippy.git Package: trippy Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, Description: network diagnostic tool combining traceroute and ping Trippy combines the functionality of traceroute and ping and is designed to assist with the analysis of network issues. ================================================ FILE: ubuntu-ppa/copyright ================================================ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Source: https://github.com/fujiapple852/trippy Upstream-Name: trippy Upstream-Contact: FujiApple Files: * Copyright: 2022-2024 Trippy Contributors (https://github.com/fujiapple852/trippy/graphs/contributors) License: Apache-2.0 Files: debian/* Copyright: 2024 Fuji Apple License: Apache-2.0 License: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at . https://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Comment: On Debian systems, the complete text of the Apache version 2.0 license can be found in "/usr/share/common-licenses/Apache-2.0". ================================================ FILE: ubuntu-ppa/release.sh ================================================ #!/bin/bash set -o errexit -o pipefail -o nounset # The Trippy version to release VERSION="0.14.0-dev" # The upstream version to use in the PPA # # This should usually be the same as the VERSION, but if the upstream tarball has been repacked # (e.g. to remove unnecessary files or to fix the tarball) then this should be set to the upstream version # with a `+repack{N}` suffix, where `{N}` is the number of times the tarball has been repacked. i.e. `0.1.0+repack1`. UPSTREAM="0.14.0-dev" # The revision number for the PPA # # This is incremented each time a new upload is made to the PPA so will always be one greater than repack number. REVISION=1 # The Ubuntu series to build for SERIES=("noble" "jammy") TARBALL="trippy_${UPSTREAM}.orig.tar.gz" PACKAGE="trippy" CHANGES="New upstream release" export DEBEMAIL="fujiapple852@gmail.com" export DEBFULLNAME="Fuji Apple" # Import GPG key securely if [[ ! -f launchpad_secret_key.pgp ]]; then echo "Error: GPG key file 'launchpad_secret_key.pgp' not found." >&2 exit 1 fi gpg --batch --import launchpad_secret_key.pgp # Extract GPG key ID GPG_KEY_ID=$(gpg --with-colons --import-options show-only --import launchpad_secret_key.pgp | awk -F: '/^sec/ {print $5}') # Check GPG key expiration if gpg --list-keys --with-colons "${GPG_KEY_ID}" | grep '^pub' | grep '[e]'; then echo "GPG key has expired. Please update your GPG key." >&2 exit 1 fi # Download TARBALL wget -O "${TARBALL}" "https://github.com/fujiapple852/trippy/archive/refs/tags/${VERSION}.tar.gz" if [[ ! -f "${TARBALL}" ]]; then echo "Error: Failed to download TARBALL." >&2 exit 1 fi # Vendor the cargo dependencies # We have to ensure we run cargo `vendor --locked` against the src in the tarball, not the src in the current directory. tar -xf "${TARBALL}" pushd "trippy-${VERSION}" rm -f ../ubuntu-ppa/vendor.tar.xz rm -rf vendor cargo-1.85 vendor --locked tar -cJf ../ubuntu-ppa/vendor.tar.xz vendor popd rm -rf "trippy-${VERSION}" for series in "${SERIES[@]}"; do UBUNTU_VERSION=$(distro-info --series "${series}" -r | cut -d' ' -f1) BUILD_DIR="build-${series}" mkdir -p "${BUILD_DIR}" cp -r ubuntu-ppa "${BUILD_DIR}/debian" cd "${BUILD_DIR}" # Update changelog for the specific series rm -f debian/changelog dch --create --distribution "${series}" --PACKAGE "${PACKAGE}" \ --newversion "${UPSTREAM}-ppa${REVISION}~ubuntu${UBUNTU_VERSION}" "$CHANGES" # Build the source PACKAGE debuild --prepend-path ~/.cargo/bin -S -sa cd .. done # The -ss flag can be added to simulate the upload for changes_file in ./*.changes; do dput ppa:fujiapple/trippy "${changes_file}" done ================================================ FILE: ubuntu-ppa/rules ================================================ #!/usr/bin/make -f .PHONY: override_dh_strip %: dh $@ override_dh_auto_build: mkdir .cargo cp debian/cargo.config .cargo/config tar xJf debian/vendor.tar.xz cargo-1.85 build --release --frozen override_dh_auto_clean: cargo-1.85 clean rm -rf .cargo vendor override_dh_strip: dh_strip --no-automatic-dbgsym ================================================ FILE: ubuntu-ppa/source/format ================================================ 3.0 (quilt) ================================================ FILE: ubuntu-ppa/source/include-binaries ================================================ debian/vendor.tar.xz ================================================ FILE: ubuntu-ppa/trippy.docs ================================================ README.md ================================================ FILE: ubuntu-ppa/trippy.install ================================================ target/release/trip usr/bin