[
  {
    "path": ".config/spellcheck.toml",
    "content": "dev_comments = true\nskip_readme = false\n\n[Hunspell]\nlang = \"en_US\"\nsearch_dirs = [\".\"]\nextra_dictionaries = [\"trippy.dic\"]\nskip_os_lookups = false\nuse_builtin = true\n"
  },
  {
    "path": ".config/trippy.dic",
    "content": "100\n%\n'\n+\n100ms\n10ms\n1s\n300s\n5s\n=\n>\nASN\nBSD4\nCSV\nCloudflare\nDF\nDSCP\nECMP\nECN\nEndianness\nFreeBSD\nGeoIp\nGeolocation\nGraphviz\nIANA\nICMPv4\nICMPv6\nIPinfo\nIPs\nIPv4\nIPv6\nMaxMind\nNAT'ed\nNum\nROFF\nRTT\nTBD\nTODO\nTOS\nTXT\nTrippy\nTui\nXDG\naccessor\naddr\naddrs\nasn\nboolean\ncalc\nchecksum\nchecksums\ncidr\ncloneable\nconfig\nconnectionless\ndatagram\ndec\ndeserialization\ndest\ndns\ndublin\nendianness\nenqueue\nenqueued\nenqueuing\nfrontend\ngeolocation\ngetsockname\nholsravbwdt\nhostname\nhostnames\nicmp\nimpl\nip\nipv6\njitter\njson\nlocalhost\nlookups\nmacOS\nmmdb\nmpls\nmultipath\nnewtype\nparis\nrfc1889\nrfc2460\nrfc2474\nrfc2475\nrfc2476\nrfc3168\nrfc3246\nrfc3550\nrfc4443\nrfc4884\nrfc5865\nrfc8622\nsrc\nstddev\nstruct\nsubmodule\nsyscall\ntcp\ntimestamp\ntoml\ntraceroute\ntrippy\nttl\ntui\ntuple\nu8\nudp\nuninitialised\nunix\nunselected"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n\t\"image\": \"mcr.microsoft.com/devcontainers/universal:2\",\n\t\"features\": {\n\t\t\"ghcr.io/devcontainers/features/rust:1\": {}\n\t}\n}"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: fujiapple852\nbuy_me_a_coffee: fujiapple852"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a bug report\ntitle: ''\nlabels: triage\nassignees: ''\n---\n\n**Describe the bug**\n\n<!-- A description of what the bug is. -->\n\n**To Reproduce**\n\n<!-- A description of the steps to reproduce the behavior including the full `trip` or `trip.exe` command line. -->\n\n**Expected behavior**\n\n<!-- A description of what you expected to happen. -->\n\n**Screenshots**\n\n<!-- If applicable, add screenshots to help explain your problem. -->\n\n**Environment Info**\n\n- OS: <!-- e.g. Linux, Windows 11, macOS -->\n- Trippy version: <!-- the output of `trip -V` -->\n- Installation method: <!-- e.g. `brew`, `winget`, `cargo` -->\n- Terminal / Console: <!-- If you are not sure you can use \"About\" or, \"Help\" on the terminal window to gather the requested information. e.g. `iTerm2`, `cmd.exe`, `PowerShell`, 'GNOME Terminal' -->\n\n**Additional context**\n\n<!-- Add any other context about the problem here. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: triage\nassignees: ''\n---\n\n**Describe the feature you'd like**\n\n<!-- A clear and concise description of what you want to happen. -->\n\n**Describe alternatives you've considered**\n\n<!-- A clear and concise description of any alternative solutions or features you've considered. -->\n\n**Additional context**\n\n<!-- Add any other context or screenshots about the feature request here. -->\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"cargo\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    ignore:\n        - dependency-name: \"clap*\"\n          update-types:\n            - \"version-update:semver-patch\"\n        - dependency-name: \"serde*\"\n          update-types:\n            - \"version-update:semver-patch\"\n        - dependency-name: \"anyhow\"\n          update-types:\n            - \"version-update:semver-patch\"\n        - dependency-name: \"thiserror\"\n          update-types:\n            - \"version-update:semver-patch\"\n    allow:\n      - dependency-type: \"direct\"\n    open-pull-requests-limit: 10\n    rebase-strategy: \"disabled\""
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "on:\n  pull_request:\n    branches: [ master ]\n  schedule:\n    - cron: '00 18 * * *'\n\nname: CI\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        build:\n          - linux-stable\n          - linux-musl-stable\n          - linux-beta\n          - linux-nightly\n          - macos-stable\n          - macos-stable-arm64\n          - windows-stable\n        include:\n          - build: linux-stable\n            os: ubuntu-22.04\n            target: x86_64-unknown-linux-gnu\n            rust: stable\n          - build: linux-musl-stable\n            os: ubuntu-22.04\n            target: x86_64-unknown-linux-musl\n            rust: stable\n          - build: linux-beta\n            os: ubuntu-22.04\n            target: x86_64-unknown-linux-gnu\n            rust: beta\n          - build: linux-nightly\n            os: ubuntu-22.04\n            target: x86_64-unknown-linux-gnu\n            rust: nightly\n          - build: macos-stable\n            os: macos-15-intel\n            target: x86_64-apple-darwin\n            rust: stable\n          - build: macos-stable-arm64\n            os: macos-latest\n            target: aarch64-apple-darwin\n            rust: stable\n          - build: windows-stable\n            os: windows-2022\n            target: x86_64-pc-windows-msvc\n            rust: stable\n    steps:\n      - uses: actions/checkout@v4\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ matrix.rust }}\n          target: ${{ matrix.target }}\n      - uses: Swatinem/rust-cache@v2\n      - run: cargo test --target ${{ matrix.target }}\n\n  build-cross:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        build: [ netbsd, freebsd ]\n        include:\n          - build: netbsd\n            os: ubuntu-22.04\n            target: x86_64-unknown-netbsd\n            rust: stable\n          - build: freebsd\n            os: ubuntu-22.04\n            target: x86_64-unknown-freebsd\n            rust: stable\n    steps:\n      - uses: actions/checkout@v4\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ matrix.rust }}\n          target: ${{ matrix.target }}\n      - uses: Swatinem/rust-cache@v2\n      - name: Use Cross\n        shell: bash\n        run: |\n          cargo install cross --git https://github.com/cross-rs/cross\n      - name: Show command used for Cargo\n        run: |\n          echo \"cargo command is: ${{ env.CARGO }}\"\n          echo \"target flag is: ${{ env.TARGET_FLAGS }}\"\n      - name: cross build\n        run: cross build --target ${{ matrix.target }} --verbose\n\n  sim-test:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        build:\n          - linux-stable\n          - macos-stable\n          - windows-stable\n        include:\n          - build: linux-stable\n            os: ubuntu-22.04\n            target: x86_64-unknown-linux-gnu\n            rust: stable\n          - build: macos-stable\n            os: macos-15-intel\n            target: x86_64-apple-darwin\n            rust: stable\n          - build: windows-stable\n            os: windows-2022\n            target: x86_64-pc-windows-msvc\n            rust: stable\n    steps:\n      - uses: actions/checkout@v4\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ matrix.rust }}\n          target: ${{ matrix.target }}\n      - uses: Swatinem/rust-cache@v2\n      - name: Copy wintun.dll to current dir\n        if: startsWith(matrix.build, 'windows')\n        shell: bash\n        # The simulation tests run from the crates/trippy-core directory and so `wintun.dll` needs to be copied there\n        run: |\n          cp \"crates/trippy-core/tests/resources/wintun.dll\" \"./crates/trippy-core/\"\n      - name: Allow ICMPv4 and ICMPv6 in Windows defender firewall\n        if: startsWith(matrix.build, 'windows')\n        shell: pwsh\n        run: |\n          New-NetFirewallRule -DisplayName \"ICMPv4 Trippy Allow\" -Name ICMPv4_TRIPPY_ALLOW -Protocol ICMPv4 -Action Allow\n          New-NetFirewallRule -DisplayName \"ICMPv6 Trippy Allow\" -Name ICMPv6_TRIPPY_ALLOW -Protocol ICMPv6 -Action Allow\n      - name: Build (without root)\n        run: cargo build --target ${{ matrix.target }} --features sim-tests --test sim\n      - name: Run simulation test on ${{ matrix.build }}\n        if: ${{ ! startsWith(matrix.build, 'windows') }}\n        run: sudo -E env \"PATH=$PATH\" cargo test --target ${{ matrix.target }} --features sim-tests --test sim -- --exact --nocapture\n      - name: Run simulation test on ${{ matrix.build }}\n        if: startsWith(matrix.build, 'windows')\n        run: cargo test --target ${{ matrix.target }} --features sim-tests --test sim -- --exact --nocapture\n\n  fmt:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: stable\n          components: rustfmt\n      - uses: Swatinem/rust-cache@v2\n      - run: cargo fmt --all -- --check\n\n  clippy:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        build:\n          - linux-stable\n          - macos-stable\n          - windows-stable\n        include:\n          - build: linux-stable\n            os: ubuntu-22.04\n            target: x86_64-unknown-linux-gnu\n            rust: stable\n          - build: macos-stable\n            os: macos-15-intel\n            target: x86_64-apple-darwin\n            rust: stable\n          - build: windows-stable\n            os: windows-2022\n            target: x86_64-pc-windows-msvc\n            rust: stable\n    steps:\n      - uses: actions/checkout@v4\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ matrix.rust }}\n          components: clippy\n      - uses: Swatinem/rust-cache@v2\n      - run: cargo clippy --workspace --all-features --target ${{ matrix.target }} --tests -- -Dwarnings\n\n  build-docker:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: Swatinem/rust-cache@v2\n      - name: Build Docker image\n        run: docker build -t trippy-docker-image .\n\n  cargo-deny:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: EmbarkStudios/cargo-deny-action@v2\n        with:\n          rust-version: \"1.87.0\"\n          log-level: warn\n          command: check\n          arguments: --all-features\n          command-arguments: \"--hide-inclusion-graph\"\n\n  cargo-msrv:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: Swatinem/rust-cache@v2\n      - name: install cargo-msrv\n        run: cargo install --git https://github.com/foresterre/cargo-msrv.git cargo-msrv\n      - name: check msrv for trippy\n        run: cargo msrv verify --output-format json --manifest-path crates/trippy/Cargo.toml -- cargo check\n      - name: check msrv for trippy-tui\n        run: cargo msrv verify --output-format json --manifest-path crates/trippy-tui/Cargo.toml -- cargo check\n      - name: check msrv for trippy-core\n        run: cargo msrv verify --output-format json --manifest-path crates/trippy-core/Cargo.toml -- cargo check\n      - name: check msrv for trippy-packet\n        run: cargo msrv verify --output-format json --manifest-path crates/trippy-packet/Cargo.toml -- cargo check\n      - name: check msrv for trippy-dns\n        run: cargo msrv verify --output-format json --manifest-path crates/trippy-dns/Cargo.toml -- cargo check\n      - name: check msrv for trippy-privilege\n        run: cargo msrv verify --output-format json --manifest-path crates/trippy-privilege/Cargo.toml -- cargo check\n\n  style:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n      - uses: dprint/check@v2.2\n\n  conventional-commits:\n    runs-on: ubuntu-22.04\n    steps:\n      - uses: actions/checkout@v4\n      - name: Conventional Commits Lint\n        uses: webiny/action-conventional-commits@v1.3.0\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          allowed-commit-types: \"feat,fix,chore,docs,style,refactor,test,build,ci,revert\"\n\n  spelling:\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Install cargo-spellcheck\n        uses: taiki-e/install-action@v2\n        with:\n          tool: cargo-spellcheck\n      - uses: actions/checkout@v4\n      - name: Run cargo-spellcheck\n        run: cargo spellcheck --code 1\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "on:\n  push:\n    branches: [ master ]\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout your repository using git\n        uses: actions/checkout@v4\n      - name: Install, build, and upload your site\n        uses: withastro/action@v3\n        with:\n          path: docs\n\n  deploy:\n    needs: build\n    runs-on: ubuntu-latest\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\non:\n  push:\n    tags:\n      - \"[0-9]+.[0-9]+.[0-9]+\"\njobs:\n  create-release:\n    name: create-release\n    runs-on: ubuntu-latest\n    outputs:\n      upload_url: ${{ steps.release.outputs.upload_url }}\n      trip_version: ${{ env.TRIP_VERSION }}\n    steps:\n      - name: Get the release version from the tag\n        shell: bash\n        if: env.TRIP_VERSION == ''\n        run: |\n          echo \"TRIP_VERSION=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV\n          echo \"version is: ${{ env.TRIP_VERSION }}\"\n      - name: Create GitHub release\n        id: release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ env.TRIP_VERSION }}\n          release_name: Trippy ${{ env.TRIP_VERSION }}\n          body: See [CHANGELOG.md](https://github.com/fujiapple852/trippy/blob/master/CHANGELOG.md) for details.\n          prerelease: false\n\n  build-release:\n    name: build-release\n    needs: ['create-release']\n    runs-on: ${{ matrix.os }}\n    env:\n      CARGO: cargo\n      TARGET_FLAGS: \"\"\n      TARGET_DIR: ./target\n      CROSS_NO_WARNINGS: 0\n      RUST_BACKTRACE: 1\n    strategy:\n      matrix:\n        build: [\n          x86_64-linux-gnu, x86_64-linux-musl, aarch64-linux-gnu, aarch64-linux-musl,\n          armv7-linux-gnueabihf, armv7-linux-musleabihf, armv7-linux-musleabi,\n          x86_64-apple-darwin, aarch64-apple-darwin,\n          x86_64-pc-windows-msvc, x86_64-pc-windows-gnu, aarch64-pc-windows-msvc,\n          x86_64-netbsd, x86_64-freebsd\n        ]\n        include:\n          # Linux (x86_64 & aarch64)\n          - build: x86_64-linux-gnu\n            os: ubuntu-22.04\n            target: x86_64-unknown-linux-gnu\n          - build: x86_64-linux-musl\n            os: ubuntu-22.04\n            target: x86_64-unknown-linux-musl\n          - build: aarch64-linux-gnu\n            os: ubuntu-22.04\n            target: aarch64-unknown-linux-gnu\n          - build: aarch64-linux-musl\n            os: ubuntu-22.04\n            target: aarch64-unknown-linux-musl\n\n          # Linux (armv7)\n          - build: armv7-linux-gnueabihf\n            os: ubuntu-22.04\n            target: armv7-unknown-linux-gnueabihf\n          - build: armv7-linux-musleabihf\n            os: ubuntu-22.04\n            target: armv7-unknown-linux-musleabihf\n          - build: armv7-linux-musleabi\n            os: ubuntu-22.04\n            target: armv7-unknown-linux-musleabi\n\n          # macOS (x86_64 & aarch64)\n          - build: x86_64-apple-darwin\n            os: macos-15-intel\n            target: x86_64-apple-darwin\n          - build: aarch64-apple-darwin\n            os: macos-latest\n            target: aarch64-apple-darwin\n\n          # Windows (x86_64 & aarch64)\n          - build: x86_64-pc-windows-msvc\n            os: windows-2022\n            target: x86_64-pc-windows-msvc\n          - build: x86_64-pc-windows-gnu\n            os: ubuntu-22.04\n            target: x86_64-pc-windows-gnu\n          - build: aarch64-pc-windows-msvc\n            os: windows-2022\n            target: aarch64-pc-windows-msvc\n\n          # BSD (x86_64)\n          - build: x86_64-netbsd\n            os: ubuntu-22.04\n            target: x86_64-unknown-netbsd\n          - build: x86_64-freebsd\n            os: ubuntu-22.04\n            target: x86_64-unknown-freebsd\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Install Rust\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n          profile: minimal\n          override: true\n          target: ${{ matrix.target }}\n\n      - name: Use Cross\n        shell: bash\n        run: |\n          cargo install cross --git https://github.com/cross-rs/cross\n          echo \"CARGO=cross\" >> $GITHUB_ENV\n          echo \"TARGET_FLAGS=--target ${{ matrix.target }}\" >> $GITHUB_ENV\n          echo \"TARGET_DIR=./target/${{ matrix.target }}\" >> $GITHUB_ENV\n\n      - name: Show command used for Cargo\n        run: |\n          echo \"cargo command is: ${{ env.CARGO }}\"\n          echo \"target flag is: ${{ env.TARGET_FLAGS }}\"\n          echo \"target dir is: ${{ env.TARGET_DIR }}\"\n\n      - name: Build release binary\n        run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }}\n\n      - name: Build archive\n        shell: bash\n        run: |\n          staging=\"trippy-${{ needs.create-release.outputs.trip_version }}-${{ matrix.target }}\"\n          mkdir -p \"$staging\"\n          if [ \"${{ matrix.os }}\" = \"windows-2022\" ] || [ \"${{ matrix.build }}\" = \"x86_64-pc-windows-gnu\" ]; then\n            cp \"target/${{ matrix.target }}/release/trip.exe\" \"$staging/\"\n            7z a -tzip \"$staging.zip\" \"$staging\"\n            echo \"ASSET=$staging.zip\" >> $GITHUB_ENV\n          else\n            cp \"target/${{ matrix.target }}/release/trip\" \"$staging/\"\n            tar czf \"$staging.tar.gz\" \"$staging\"\n            echo \"ASSET=$staging.tar.gz\" >> $GITHUB_ENV\n          fi\n\n      - name: Build RPM package\n        shell: bash\n        if: startsWith(matrix.build, 'x86_64-linux-gnu')\n        run: |\n          cargo install cargo-generate-rpm\n          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\n\n      - name: Upload RPM package\n        if: startsWith(matrix.build, 'x86_64-linux-gnu')\n        uses: actions/upload-release-asset@v1.0.1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: target/${{ matrix.target }}/generate-rpm/trippy-${{ needs.create-release.outputs.trip_version }}-x86_64.rpm\n          asset_name: trippy-${{ needs.create-release.outputs.trip_version }}-x86_64.rpm\n          asset_content_type: application/x-rpm\n\n      - name: Create Debian package\n        shell: bash\n        if: startsWith(matrix.build, 'x86_64-linux-gnu') || startsWith(matrix.build, 'x86_64-linux-musl')\n        run: |\n          cargo install cargo-deb\n          cargo deb -p trippy --target ${{ matrix.target }} --deb-version ${{ needs.create-release.outputs.trip_version }}\n          case ${{ matrix.target }} in\n            aarch64-*-linux-*) DPKG_ARCH=arm64 ;;\n            arm-*-linux-*hf) DPKG_ARCH=armhf ;;\n            i686-*-linux-*) DPKG_ARCH=i686 ;;\n            x86_64-*-linux-*) DPKG_ARCH=amd64 ;;\n            *) DPKG_ARCH=notset ;;\n          esac;\n          echo \"DPKG_ARCH=${DPKG_ARCH}\" >> $GITHUB_ENV\n\n      - name: Upload Deb Release Asset\n        if: startsWith(matrix.build, 'x86_64-linux-gnu') || startsWith(matrix.build, 'x86_64-linux-musl')\n        uses: actions/upload-release-asset@v1.0.1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_content_type: application/vnd.debian.binary-package\n          asset_path: target/${{ matrix.target }}/debian/trippy_${{ needs.create-release.outputs.trip_version }}_${{ env.DPKG_ARCH }}.deb\n          asset_name: trippy_${{ matrix.target }}_${{ needs.create-release.outputs.trip_version }}_${{ env.DPKG_ARCH }}.deb\n\n      - name: Upload release archive\n        uses: actions/upload-release-asset@v1.0.1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.ASSET }}\n          asset_name: ${{ env.ASSET }}\n          asset_content_type: application/octet-stream"
  },
  {
    "path": ".gitignore",
    "content": "/target\n.idea\n.DS_Store\n.vscode/launch.json\n*.snap.new\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Trippy Agent Guidelines\n\nThis repository follows the guidance below when making changes.\n\n## Development commands\n\n- Check the code with `cargo check --workspace --all-features --tests`.\n- Test the code with `cargo test`. Do not pass `--all-features`.\n- Format Rust code with `cargo fmt --all`.\n- Format non-Rust code with `dprint fmt` (install with `cargo install --locked dprint`).\n- Lint with `cargo clippy --workspace --all-features --tests -- -Dwarnings`.\n- If CLI arguments, man pages or shell completions change, update snapshots:\n  `cargo test && cargo insta review`.\n- If the `Dockerfile` changes, build it locally using `docker build . -t trippy:dev`.\n\n## Commit messages\n\n- Use the Conventional Commits format:\n  `<type>[optional scope]: <description>` where `<type>` is one of\n  `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `build`, `ci`, or `revert`.\n- For code changes set the scope to one of `core`, `dns`, `packet`, `privilege` or `tui`.\n- Use backquotes for file names and code items in the description.\n- For documentation fixes use `docs: fix <description>`.\n- Prefer small, focused commits. For larger changes, use multiple commits with clear messages.\n\n## Recommendations\n\n- Run test, format and clippy before submitting a pull request and ensure all CI checks pass.\n- Keep documentation and examples in sync with code changes.\n- Use feature branches for separate tasks.\n- Open issues and pull requests through GitHub for discussion and review.\n- Always rebase your branch before when editing an open pull request to keep the history clean.\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres\nto [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Added\n\n- Added translations for locale `zh-TW` ([#1630](https://github.com/fujiapple852/trippy/pull/1630))\n\n### Changed\n\n- [BREAKING CHANGE] Change default `address-family` to be\n  `system` ([#1475](https://github.com/fujiapple852/trippy/issues/1475))\n- Increase MSRV to 1.85 ([#1700](https://github.com/fujiapple852/trippy/issues/1700))\n\n### Fixed\n\n- Default the `system` `address-family` to `ipv4-then-ipv6` for non-`system`\n  resolvers ([#1635](https://github.com/fujiapple852/trippy/issues/1635))\n- Locale parsing fails for valid BCP 47 language tags ([#1631](https://github.com/fujiapple852/trippy/pull/1631))\n\n## [0.13.0] - 2025-05-05\n\n### Added\n\n- Added DSCP and ECN columns ([#1539](https://github.com/fujiapple852/trippy/issues/1539))\n- Added support for setting IPv6 traffic class from `--tos` ([#202](https://github.com/fujiapple852/trippy/issues/202))\n- Added ability to read config from `$XDG_CONFIG_HOME/trippy`\n  directory ([#1528](https://github.com/fujiapple852/trippy/issues/1528))\n- Added `--tui-timezone` flag to set a custom timezone ([#1513](https://github.com/fujiapple852/trippy/issues/1513))\n- Added support for `--addr-family system` to defer address family selection to the OS\n  resolver ([#1469](https://github.com/fujiapple852/trippy/issues/1469))\n- Added tracing start and end timestamps to the `json`\n  report ([#1510](https://github.com/fujiapple852/trippy/issues/1510))\n- Added the Trippy logo! ([#100](https://github.com/fujiapple852/trippy/issues/100))\n\n### Changed\n\n- Remove address family downgrade for `dublin` strategy ([#1476](https://github.com/fujiapple852/trippy/issues/1476))\n- Reduce verbosity of tracing for library users ([#1482](https://github.com/fujiapple852/trippy/issues/1482))\n- Increase MSRV to 1.78 ([#1576](https://github.com/fujiapple852/trippy/issues/1576))\n\n### Fixed\n\n- Tracer panic for large icmp packets ([#1561](https://github.com/fujiapple852/trippy/issues/1561))\n- Memory corruption on Windows ([#1527](https://github.com/fujiapple852/trippy/issues/1527))\n- Socket being closed twice on Windows ([#1443](https://github.com/fujiapple852/trippy/issues/1443))\n- Potential crash on Windows for adapters without unicast\n  addresses ([#1547](https://github.com/fujiapple852/trippy/issues/1547))\n- Potential use-after-free when discovering source address on\n  Windows ([#1558](https://github.com/fujiapple852/trippy/issues/1558))\n- The `--tos` (`-Q`) flag is ignored for `IPv4/udp`\n  tracing ([#1540](https://github.com/fujiapple852/trippy/issues/1540))\n- Items missing from settings dialog ([#1541](https://github.com/fujiapple852/trippy/issues/1541))\n\n## [0.12.2] - 2025-01-03\n\n### Fixed\n\n- Tracer panic when `--first-ttl` is greater than 1 ([#1460](https://github.com/fujiapple852/trippy/issues/1460))\n- IP `--addr-family` not respected for\n  `--dns-resolve-method resolv` ([#1461](https://github.com/fujiapple852/trippy/issues/1461))\n- Incorrect cli help text for `--addr-family` ([#1456](https://github.com/fujiapple852/trippy/issues/1456))\n\n## [0.12.1] - 2024-12-21\n\n### Changed\n\n- Replace use of `yaml` with `toml` dependency ([#1416](https://github.com/fujiapple852/trippy/issues/1416))\n\n### Fixed\n\n- Locale data not copied into docker image ([#1431](https://github.com/fujiapple852/trippy/issues/1431))\n\n## [0.12.0] - 2024-12-04\n\n### Added\n\n- Highlight lost probes in sample history ([#1247](https://github.com/fujiapple852/trippy/issues/1247))\n- Added `quit-preserve-screen` (default: `shift+q`) key binding to quit Tui without clearing the\n  screen ([#1382](https://github.com/fujiapple852/trippy/issues/1382))\n- Added forward add backward loss heuristics ([#860](https://github.com/fujiapple852/trippy/issues/860))\n- Added `--tui-locale` flag to support i18n ([#1319](https://github.com/fujiapple852/trippy/issues/1319))\n- Added translations for locales `en`, `fr`, `tr`, `zh`, `pt`, `sv`, `it`, `ru`, `es` &\n  `de` ([#506](https://github.com/fujiapple852/trippy/issues/506))\n- Added `--print-locales` flag to print all available\n  locales ([#1357](https://github.com/fujiapple852/trippy/issues/1357))\n- Added Debian package ([#1312](https://github.com/fujiapple852/trippy/issues/1312))\n- Added Ubuntu `noble` PPA package ([#1308](https://github.com/fujiapple852/trippy/issues/1308))\n\n### Changed\n\n- Added information bar to Tui ([#1349](https://github.com/fujiapple852/trippy/issues/1349))\n- [BREAKING CHANGE] Remove `Timestamp` from all `DnsEntry`\n  variants ([#1296](https://github.com/fujiapple852/trippy/issues/1296))\n- [BREAKING CHANGE] Replace `toggle-privacy` key binding with `expand-privacy` and\n  `contract-privacy` ([#1347](https://github.com/fujiapple852/trippy/issues/1347))\n- [BREAKING CHANGE] Hide source address when `--tui-privacy-max-ttl` is\n  set ([#1365](https://github.com/fujiapple852/trippy/issues/1365))\n- Only show hostnames if different from IPs ([#1363](https://github.com/fujiapple852/trippy/issues/1363))\n- Lookup GeoIp with current locale ([#1336](https://github.com/fujiapple852/trippy/issues/1336))\n- Enable Link-Time Optimization (LTO) for release builds ([#1341](https://github.com/fujiapple852/trippy/issues/1341))\n\n### Fixed\n\n- Reverse dns enqueued multiple times when dns-ttl expires ([#1290](https://github.com/fujiapple852/trippy/issues/1290))\n- Fixed panic for icmp extensions with malformed length ([#1287](https://github.com/fujiapple852/trippy/issues/1287))\n- Cursor not moved to the bottom on exit when using\n  `--tui-preserve-screen` ([#1375](https://github.com/fujiapple852/trippy/issues/1375))\n- Config item `tui-address-mode` does not accept `ip` ([#1327](https://github.com/fujiapple852/trippy/issues/1327))\n- Icmp extension mode not shown in Tui settings ([#1289](https://github.com/fujiapple852/trippy/issues/1289))\n- Sample history and frequency charts ignore sub-millisecond\n  samples ([#1398](https://github.com/fujiapple852/trippy/issues/1398))\n\n## [0.11.0] - 2024-08-11\n\n### Added\n\n- Added NAT detection for `IPv4/udp/dublin` ([#1104](https://github.com/fujiapple852/trippy/issues/1104))\n- Added public API ([#1192](https://github.com/fujiapple852/trippy/issues/1192))\n- Added support for NAT detection (`N`) column ([#1219](https://github.com/fujiapple852/trippy/issues/1219))\n- Added support for last icmp packet type (`T`) column ([#1105](https://github.com/fujiapple852/trippy/issues/1105))\n- Added support for last icmp packet code (`C`) column ([#1109](https://github.com/fujiapple852/trippy/issues/1109))\n- Added support for the probe failure count (`f`) column ([#1258](https://github.com/fujiapple852/trippy/issues/1258))\n- Added settings dialog tab hotkeys ([#1217](https://github.com/fujiapple852/trippy/issues/1217))\n- Added `--dns-ttl` flag to allow refreshing the reverse DNS\n  results ([#1233](https://github.com/fujiapple852/trippy/issues/1233))\n- Added `--generate-man` flag for generating [ROFF](https://en.wikipedia.org/wiki/Roff_(software)) man\n  page ([#85](https://github.com/fujiapple852/trippy/issues/85))\n- Added Ubuntu PPA package ([#859](https://github.com/fujiapple852/trippy/issues/859))\n- Added Chocolatey package ([#572](https://github.com/fujiapple852/trippy/issues/572))\n\n### Changed\n\n- [BREAKING CHANGE] Changed initial sequence to be `33434` ([#1203](https://github.com/fujiapple852/trippy/issues/1203))\n- [BREAKING CHANGE] Renamed `tui-max-[samples|flows]`\n  as `max-[samples|flows]` ([#1187](https://github.com/fujiapple852/trippy/issues/1187))\n- Separated library and binary crates ([#1141](https://github.com/fujiapple852/trippy/issues/1141))\n- Record `icmp` packet code ([#734](https://github.com/fujiapple852/trippy/issues/734))\n- Transient error handling for `IPv4` on macOS, Linux &\n  Windows ([#1255](https://github.com/fujiapple852/trippy/issues/1255))\n- Improved error messages ([#1150](https://github.com/fujiapple852/trippy/issues/1150))\n- Revamp the help dialog ([#1260](https://github.com/fujiapple852/trippy/issues/1260))\n\n### Fixed\n\n- Fixed `DestinationUnreachable` incorrectly assumed to come from target\n  host ([#1225](https://github.com/fujiapple852/trippy/issues/1225))\n- Fixed incorrect target hop calculation ([#1226](https://github.com/fujiapple852/trippy/issues/1226))\n- Do not conflate `AddressInUse` and `AddrNotAvailable`\n  errors ([#1246](https://github.com/fujiapple852/trippy/issues/1246))\n\n## [0.10.0] - 2024-03-31\n\n### Added\n\n- Added support for calculating and displaying jitter ([#39](https://github.com/fujiapple852/trippy/issues/39))\n- Added support for customizing columns ([#757](https://github.com/fujiapple852/trippy/issues/757))\n- Added support for reordering and toggling column\n  visibility in Tui ([#1026](https://github.com/fujiapple852/trippy/issues/1026))\n- Added support for [dublin](https://github.com/insomniacslk/dublin-traceroute) ECMP routing\n  for `IPv6/udp` ([#272](https://github.com/fujiapple852/trippy/issues/272))\n- Added support for [IPinfo](https://ipinfo.io) flavoured `mmdb`\n  files ([#862](https://github.com/fujiapple852/trippy/issues/862))\n- Added support for `IPv4->IPv6` and `IPv6->IPv4` DNS fallback\n  modes ([#864](https://github.com/fujiapple852/trippy/issues/864))\n- Added [TUN](https://en.wikipedia.org/wiki/TUN/TAP) based simulation\n  tests ([#908](https://github.com/fujiapple852/trippy/issues/908))\n- Added support for last src port (`S`) and last dest port (`P`) custom\n  columns ([#974](https://github.com/fujiapple852/trippy/issues/974))\n- Added support for last sequence (`Q`) custom column ([#976](https://github.com/fujiapple852/trippy/issues/976))\n- Added support for more named theme colors ([#1011](https://github.com/fujiapple852/trippy/issues/1011))\n\n### Changed\n\n- Ensure `paris` and `dublin` ECMP strategy are only used with supported\n  protocols ([#848](https://github.com/fujiapple852/trippy/issues/848))\n- Restrict flows to `paris` and `dublin` ECMP strategies ([#1007](https://github.com/fujiapple852/trippy/issues/1007))\n- Improved Tui table column layout logic ([#925](https://github.com/fujiapple852/trippy/issues/925))\n- Use exclusive reference `&mut` for all Socket operations ([#843](https://github.com/fujiapple852/trippy/issues/843))\n- Reduced maximum sequence per round from 1024 to 512 ([#1067](https://github.com/fujiapple852/trippy/issues/1067))\n\n### Fixed\n\n- Fixed off-by-one bug in max-rounds calculation ([#906](https://github.com/fujiapple852/trippy/issues/906))\n- Fixed panic with `expand-hosts-max` Tui command ([#892](https://github.com/fujiapple852/trippy/issues/892))\n- Fixed failure to parse generated config file on Windows ([#958](https://github.com/fujiapple852/trippy/issues/958))\n- Fixed tracer panic for `icmp` TimeExceeded \"Fragment reassembly time exceeded\"\n  packets ([#979](https://github.com/fujiapple852/trippy/issues/979))\n- Fixed tracer not discarding unrelated `icmp` packets for `udp` and `tcp`\n  protocols ([#982](https://github.com/fujiapple852/trippy/issues/982))\n- Fixed incorrect minimum packet size for `IPv6` ([#985](https://github.com/fujiapple852/trippy/issues/985))\n- Fixed permission denied error reading configuration file from snap\n  installation ([#1058](https://github.com/fujiapple852/trippy/issues/1058))\n\n## [0.9.0] - 2023-11-30\n\n### Added\n\n- Added support for tracing flows ([#776](https://github.com/fujiapple852/trippy/issues/776))\n- Added support for `icmp` extensions ([#33](https://github.com/fujiapple852/trippy/issues/33))\n- Added support for `MPLS` label stack class `icmp` extension\n  objects ([#753](https://github.com/fujiapple852/trippy/issues/753))\n- Added support for [paris](https://github.com/libparistraceroute/libparistraceroute) ECMP routing\n  for `IPv6/udp` ([#749](https://github.com/fujiapple852/trippy/issues/749))\n- Added `--unprivileged` (`-u`) flag to allow tracing without elevated privileges (macOS\n  only) ([#101](https://github.com/fujiapple852/trippy/issues/101))\n- Added `--tui-privacy-max-ttl` flag to hide host and IP details for low ttl\n  hops ([#766](https://github.com/fujiapple852/trippy/issues/766))\n- Added `toggle-privacy` (default: `p`) key binding to show or hide private\n  hops ([#823](https://github.com/fujiapple852/trippy/issues/823))\n- Added `toggle-flows` (default: `f`) key binding to show or hide tracing\n  flows ([#777](https://github.com/fujiapple852/trippy/issues/777))\n- Added `--dns-resolve-all` (`-y`) flag to allow tracing to all IPs resolved from DNS lookup\n  entry ([#743](https://github.com/fujiapple852/trippy/issues/743))\n- Added `dot` report mode (`-m dot`) to output hop graph in Graphviz `DOT`\n  format ([#582](https://github.com/fujiapple852/trippy/issues/582))\n- Added `flows` report mode (`-m flows`) to output a list of all unique tracing\n  flows ([#770](https://github.com/fujiapple852/trippy/issues/770))\n- Added `--icmp-extensions` (`-e`) flag for parsing `IPv4`/`IPv6` `icmp`\n  extensions ([#751](https://github.com/fujiapple852/trippy/issues/751))\n- Added `--tui-icmp-extension-mode` flag to control how `icmp` extensions are\n  rendered ([#752](https://github.com/fujiapple852/trippy/issues/752))\n- Added `--print-config-template` flag to output a template config\n  file ([#792](https://github.com/fujiapple852/trippy/issues/792))\n- Added `--icmp` flag as a shortcut for `--protocol icmp` ([#649](https://github.com/fujiapple852/trippy/issues/649))\n- Added `toggle-help-alt` (default: `?`) key binding to show or hide\n  help ([#694](https://github.com/fujiapple852/trippy/issues/694))\n- Added panic handing to Tui ([#784](https://github.com/fujiapple852/trippy/issues/784))\n- Added official Windows `scoop` package ([#462](https://github.com/fujiapple852/trippy/issues/462))\n- Added official Windows `winget` package ([#460](https://github.com/fujiapple852/trippy/issues/460))\n- Release `musl` Debian `deb` binary asset ([#568](https://github.com/fujiapple852/trippy/issues/568))\n- Release `armv7` Linux binary assets ([#712](https://github.com/fujiapple852/trippy/issues/712))\n- Release `aarch64-apple-darwin` (aka macOS Apple Silicon) binary\n  assets ([#801](https://github.com/fujiapple852/trippy/issues/801))\n- Added additional Rust Tier 1 and Tier 2 binary assets ([#811](https://github.com/fujiapple852/trippy/issues/811))\n\n### Changed\n\n- [BREAKING CHANGE] `icmp` extension object data added to `json` and `stream`\n  reports ([#806](https://github.com/fujiapple852/trippy/issues/806))\n- [BREAKING CHANGE] IPs field added to `csv` and all tabular\n  reports ([#597](https://github.com/fujiapple852/trippy/issues/597))\n- [BREAKING CHANGE] Command line flags `--dns-lookup-as-info` and `--tui-preserve-screen` no longer require a boolean\n  argument ([#708](https://github.com/fujiapple852/trippy/issues/708))\n- [BREAKING CHANGE] Default key binding for `ToggleFreeze` changed from `f`\n  to `ctrl+f` ([#785](https://github.com/fujiapple852/trippy/issues/785))\n- Always render AS lines in hop details mode ([#825](https://github.com/fujiapple852/trippy/issues/825))\n- Expose DNS resolver module as part of `trippy` library ([#754](https://github.com/fujiapple852/trippy/issues/754))\n- Replaced unmaintained `tui-rs` crate with `ratatui` crate ([#569](https://github.com/fujiapple852/trippy/issues/569))\n\n### Fixed\n\n- Reverse DNS lookup not working in reports ([#509](https://github.com/fujiapple852/trippy/issues/509))\n- Crash on NetBSD during window resizing ([#276](https://github.com/fujiapple852/trippy/issues/276))\n- Protocol mismatch causes tracer panic ([#745](https://github.com/fujiapple852/trippy/issues/745))\n- Incorrect row height in Tui hop detail navigation view for hops with no\n  responses ([#765](https://github.com/fujiapple852/trippy/issues/765))\n- Unnecessary socket creation in certain tracing modes ([#647](https://github.com/fujiapple852/trippy/issues/647))\n- Incorrect byte order in `IPv4` packet length calculation ([#686](https://github.com/fujiapple852/trippy/issues/686))\n\n## [0.8.0] - 2023-05-15\n\n### Added\n\n- Added `--tui-as-mode` flag to control how AS information is\n  rendered ([#483](https://github.com/fujiapple852/trippy/issues/483))\n- Added support for configuration files and added a `-c` (`--config-file`)\n  flag ([#412](https://github.com/fujiapple852/trippy/issues/412))\n- Added `--generate` flag for generating shell completions ([#86](https://github.com/fujiapple852/trippy/issues/86))\n- Added support for showing and navigating host detail ([#70](https://github.com/fujiapple852/trippy/issues/70))\n- Added `--geoip-mmdb-file` and `--tui-geoip-mode` flags for looking up and displaying GeoIp information from `mmdb`\n  files ([#503](https://github.com/fujiapple852/trippy/issues/503))\n- Added settings dialog and simplified Tui header display ([#521](https://github.com/fujiapple852/trippy/issues/521))\n- Added interactive GeoIp map display ([#505](https://github.com/fujiapple852/trippy/issues/505))\n- Added support for the [paris](https://github.com/libparistraceroute/libparistraceroute) ECMP traceroute strategy\n  for `IPv4/udp` ([#542](https://github.com/fujiapple852/trippy/issues/542))\n- Added `silent` reporting mode to run tracing without producing any\n  output ([#555](https://github.com/fujiapple852/trippy/issues/555))\n- Added `-v` (`--verbose`), `--log-format`, `--log-filter` & `--log-span-events` flags to support generating debug trace\n  logging output ([#552](https://github.com/fujiapple852/trippy/issues/552))\n\n### Changed\n\n- Show AS information for IP addresses without PTR record ([#479](https://github.com/fujiapple852/trippy/issues/479))\n- Re-enabled musl release builds ([#456](https://github.com/fujiapple852/trippy/issues/456))\n- [BREAKING CHANGE] Renamed short config flag for `report-cycles` from `-c`\n  to `-C` ([#491](https://github.com/fujiapple852/trippy/issues/491))\n- Ensure administrator privileges on Windows ([#451](https://github.com/fujiapple852/trippy/issues/451))\n- Add context information to socket errors ([#153](https://github.com/fujiapple852/trippy/issues/153))\n\n### Fixed\n\n- Do not require passing targets for certain command line\n  flags ([#500](https://github.com/fujiapple852/trippy/issues/500))\n- Key press registering two events on Windows ([#513](https://github.com/fujiapple852/trippy/issues/513))\n- Command line parameter names in error messages should be\n  in `kebab-case` ([#516](https://github.com/fujiapple852/trippy/issues/516))\n\n## [0.7.0] - 2023-03-25\n\n### Added\n\n- Added support for Windows (`icmp`, `udp` & `tcp`\n  for `IPv4` &`IPv6`) ([#98](https://github.com/fujiapple852/trippy/issues/98))\n- Added support for custom Tui key bindings ([#448](https://github.com/fujiapple852/trippy/issues/448))\n- Added support for custom Tui color themes ([#411](https://github.com/fujiapple852/trippy/issues/411))\n- Added RPM packaging ([#95](https://github.com/fujiapple852/trippy/issues/95))\n- Added DEB packaging ([#94](https://github.com/fujiapple852/trippy/issues/94))\n\n### Fixed\n\n- Variable Equal Cost Multi-path Routing (ECMP) causing truncated\n  trace ([#269](https://github.com/fujiapple852/trippy/issues/269))\n- Tracing using the `tcp` may ignore some incoming `icmp`\n  responses ([#407](https://github.com/fujiapple852/trippy/issues/407))\n- Tracer panics with large `--initial-sequence` and delayed TCP probe\n  response ([#435](https://github.com/fujiapple852/trippy/issues/435))\n- Trippy Docker fails to start ([#277](https://github.com/fujiapple852/trippy/issues/277))\n\n## [0.6.0] - 2022-08-19\n\n### Added\n\n- Added support for tracing using `IPv6` for `tcp` ([#191](https://github.com/fujiapple852/trippy/issues/191))\n- Added `-R` (`--multipath-strategy`) flag to allow setting\n  the [Equal Cost Multi-path Routing](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) strategy and added\n  support for the [dublin](https://github.com/insomniacslk/dublin-traceroute)\n  traceroute strategies for `IPv4/udp` ([#158](https://github.com/fujiapple852/trippy/issues/158))\n- Added zoom-able chart showing round trip times for all hops in a\n  trace ([#209](https://github.com/fujiapple852/trippy/issues/209))\n- Added `--udp` and `--tcp` flags as shortcuts to `-p udp` and `-p tcp`\n  respectively ([#205](https://github.com/fujiapple852/trippy/issues/205))\n\n### Changed\n\n- Gray out hops which did not update in the current round ([#216](https://github.com/fujiapple852/trippy/issues/216))\n\n## [0.5.0] - 2022-06-02\n\n### Added\n\n- Added support for tracing using `IPv6` for `icmp` and `udp` ([#35](https://github.com/fujiapple852/trippy/issues/35))\n- Added BSOD error reporting to Tui ([#179](https://github.com/fujiapple852/trippy/issues/179))\n- Added Ctrl-C keyboard command to quit the Tui ([#91](https://github.com/fujiapple852/trippy/issues/91))\n\n### Changed\n\n- Rewrite of network code to use RAW sockets ([#195](https://github.com/fujiapple852/trippy/issues/195),\n  [#192](https://github.com/fujiapple852/trippy/issues/192))\n\n### Fixed\n\n- Setting `-c` (`--report-cycles`) to 1 returns no traces ([#189](https://github.com/fujiapple852/trippy/issues/189))\n- Tracer failures not being shown for reports ([#183](https://github.com/fujiapple852/trippy/issues/183))\n\n## [0.4.0] - 2022-05-18\n\n### Added\n\n- Added `-P` (`--target-port`) flag to allow specifying the target\n  port ([1](https://github.com/fujiapple852/trippy/commit/5773fe5e5323543612be6bd4606db5aa8347d71e),\n  [2](https://github.com/fujiapple852/trippy/commit/9f03047dd231b10b13911fcc7af60afbb8b21473))\n- Added ability to trace with either a fixed source or a fixed destination port for both `udp` and `tcp`\n  tracing ([#43](https://github.com/fujiapple852/trippy/issues/43))\n- Display source and destination ports in Tui ([#156](https://github.com/fujiapple852/trippy/issues/156))\n- Added the `-A` (`--source-address`) flag to allow specifying the source\n  address ([#162](https://github.com/fujiapple852/trippy/issues/162))\n- Added the `-I` (`--interface`) flag to allow specifying the source\n  interface ([#142](https://github.com/fujiapple852/trippy/issues/42))\n- Added the `-Q` (`--tos`) flag to allow specifying the `TOS` (`DSCP`+`ECN`) `IPv4` header\n  value ([#38](https://github.com/fujiapple852/trippy/issues/38))\n\n### Changed\n\n- Changed `tcp` tracing to use a standard (non-raw) socket to be able to detect the\n  target ([#134](https://github.com/fujiapple852/trippy/issues/134))\n- Changed `udp` tracing to use a standard (non-raw) socket ([#155](https://github.com/fujiapple852/trippy/issues/155))\n- Renamed the `--tui-max-addresses-per-hop` flag\n  as `tui-max-addrs` ([#165](https://github.com/fujiapple852/trippy/issues/165))\n- Reorder the cli flags in the help output ([#163](https://github.com/fujiapple852/trippy/issues/163))\n- Change short alias for flag `max_round_duration` from `-I`\n  to `-T` ([1](https://github.com/fujiapple852/trippy/commit/15978b0909139bb2b38baa4c6f6ca969c818fc75))\n- Added short cli flags for `source-port` (`-S`), `first-ttl` (`-f`) and `tui-max-addrs` (\n  `-M`) ([1](https://github.com/fujiapple852/trippy/commit/6a6a490174582c8500972b89407ba8d694c4c6fa))\n\n### Fixed\n\n- Checksums for `udp` packets were not being set (obsoleted\n  by [#155](https://github.com/fujiapple852/trippy/issues/155)) ([#159](https://github.com/fujiapple852/trippy/issues/159))\n- `TimeExceeded` responses _from_ the target address were not being\n  handled ([1](https://github.com/fujiapple852/trippy/commit/3afa41326a33287a3ad9c17713dd7426ca86b481))\n- The largest time-to-live for a given round was being calculated incorrectly in some\n  cases ([1](https://github.com/fujiapple852/trippy/commit/688a8d00d84a816449cfee48b2d6f6dd90946511))\n\n## [0.3.1] - 2022-05-09\n\n### Fixed\n\n- Local IPv4 discovery fails on some platforms ([#133](https://github.com/fujiapple852/trippy/issues/133),\n  [#142](https://github.com/fujiapple852/trippy/issues/142))\n- DNS resolution not filtering for `IPv4` addresses ([#148](https://github.com/fujiapple852/trippy/issues/148))\n  - Note: see [#35](https://github.com/fujiapple852/trippy/issues/35) for the status of `IPv6` support\n\n## [0.3.0] - 2022-05-08\n\n### Added\n\n- Added ability for `icmp` tracing to multiple targets simultaneously in\n  Tui ([#72](https://github.com/fujiapple852/trippy/issues/72))\n- Added ability to enable and disable the `AS` lookup from the\n  Tui ([#126](https://github.com/fujiapple852/trippy/issues/126))\n- Added ability to switch between hop address display modes (ip, hostname or both) in thr\n  Tui ([#124](https://github.com/fujiapple852/trippy/issues/124))\n- Added ability to expand and collapse the number of hosts displays per hop in the\n  Tui ([#124](https://github.com/fujiapple852/trippy/issues/124))\n- Added the `-s` (`--tui-max-samples`) flag to specify the number of samples to keep for analysis and\n  display ([#110](https://github.com/fujiapple852/trippy/issues/110))\n- Added ability to flush the DNS cache from the Tui ([#71](https://github.com/fujiapple852/trippy/issues/371))\n\n### Changed\n\n- Simplified `Tracer` by removing circular buffer ([#106](https://github.com/fujiapple852/trippy/issues/106))\n- Added round end reason indicator to `Tracer` ([#88](https://github.com/fujiapple852/trippy/issues/88))\n- Show better error message for failed DNS resolution ([#119](https://github.com/fujiapple852/trippy/issues/119))\n\n### Fixed\n\n- Tracing with `udp` protocol not showing the target hop due to incorrect handling of `DestinationUnreachable`\n  responses ([#131](https://github.com/fujiapple852/trippy/issues/131))\n- Tui failing on shutdown on Windows due to `DisableMouseCapture` being invoked without a prior `EnableMouseCapture`\n  call ([#116](https://github.com/fujiapple852/trippy/issues/116))\n- Build failing on Windows due to incorrect conditional compilation\n  configuration ([#113](https://github.com/fujiapple852/trippy/issues/113))\n- Tracing not publishing all `Probe` in a round when the round ends without finding the\n  target ([#103](https://github.com/fujiapple852/trippy/issues/103))\n- Tracing with `tcp` protocol not working as the checksum was not\n  set ([#79](https://github.com/fujiapple852/trippy/issues/79))\n- Do not show FQDN for reverse DNS queries from non-system\n  resolvers ([#120](https://github.com/fujiapple852/trippy/issues/120))\n\n## [0.2.0] - 2022-04-29\n\n### Added\n\n- Added the `-r` (`--dns-resolve-method`) flag to specify using either the OS DNS resolver (default), a 3rd party\n  resolver (Google `8.8.8.8` and Cloudflare `1.1.1.1`) or DNS resolver configuration from the `/etc/resolv.conf` file\n- Added the `-z` (`--dns-lookup-as-info`) flag to display the ASN for each discovered host. This is not yet supported\n  for the default `system` resolver, see [#66](https://github.com/fujiapple852/trippy/issues/66).\n- Added the `--dns-timeout` flag to allow setting a timeout on all DNS queries\n- Added additional parameter validation for `first-ttl`, `max-ttl` & `initial-sequence`\n\n### Changed\n\n- All DNS queries are now non-blocking to prevent the Tui from freezing during slow DNS query\n- Renamed `min-sequence` flag as `initial-sequence`\n\n### Fixed\n\n- Fixed the behaviour when the sequence number wraps around at `2^16 - 1`\n\n## [0.1.0] - 2022-04-27\n\n### Added\n\n- Initial WIP release of `trippy`\n\n[Unreleased]: https://github.com/fujiapple852/trippy/compare/0.13.0...master\n[0.13.0]: https://github.com/fujiapple852/trippy/compare/0.12.2...0.13.0\n[0.12.2]: https://github.com/fujiapple852/trippy/compare/0.12.1...0.12.2\n[0.12.1]: https://github.com/fujiapple852/trippy/compare/0.12.0...0.12.1\n[0.12.0]: https://github.com/fujiapple852/trippy/compare/0.11.0...0.12.0\n[0.11.0]: https://github.com/fujiapple852/trippy/compare/0.10.0...0.11.0\n[0.10.0]: https://github.com/fujiapple852/trippy/compare/0.9.0...0.10.0\n[0.9.0]: https://github.com/fujiapple852/trippy/compare/0.8.0...0.9.0\n[0.8.0]: https://github.com/fujiapple852/trippy/compare/0.7.0...0.8.0\n[0.7.0]: https://github.com/fujiapple852/trippy/compare/0.6.0...0.7.0\n[0.6.0]: https://github.com/fujiapple852/trippy/compare/0.5.0...0.6.0\n[0.5.0]: https://github.com/fujiapple852/trippy/compare/0.4.0...0.5.0\n[0.4.0]: https://github.com/fujiapple852/trippy/compare/0.3.1...0.4.0\n[0.3.1]: https://github.com/fujiapple852/trippy/compare/0.3.0...0.3.1\n[0.3.0]: https://github.com/fujiapple852/trippy/compare/0.2.0...0.3.0\n[0.2.0]: https://github.com/fujiapple852/trippy/compare/0.1.0...0.2.0\n[0.1.0]: https://github.com/fujiapple852/trippy/compare/0.0.0...0.1.0\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Trippy\n\nContributions to Trippy are most welcome, whether you wish to report a bug, request a feature, or contribute code.\n\nRaise issues and feature requests in the GitHub [issue tracker](https://github.com/fujiapple852/trippy/issues) and raise\nall changes as GitHub [pull requests](https://github.com/fujiapple852/trippy/pulls).\n\n## Development\n\nThis section describes how to set up a development environment and the development process for Trippy.\n\n### Development tools\n\nThe following tools are needed for local development. Note that most of the following are checked during CI, so it is\nrecommended to run these checks locally before submitting a pull request.\n\n#### Rust\n\nTrippy is written in [`Rust`](https://www.rust-lang.org/tools/install) and requires the Rust toolchain to build and run.\nAs well as default components such as `cargo`, you will need `rustfmt` and `clippy` for code formatting and linting.\n\n> [!NOTE]\n> Trippy uses the `stable` toolchain.\n\nTo install `rustfmt` and `clippy`:\n\n```shell\nrustup component add rustfmt clippy\n```\n\nTo format the Rust code:\n\n```shell\ncargo fmt --all\n```\n\n> [!NOTE]\n> Trippy uses default settings for code formatting.\n\nTo lint the Rust code:\n\n```shell\ncargo clippy --workspace --all-features --tests -- -Dwarnings\n```\n\n> [!NOTE]\n> Clippy configuration is defined at the workspace level in the root `Cargo.toml` file.\n\n#### Cargo `deny`\n\nIf you add or update dependencies, you must run Cargo [`deny`](https://github.com/EmbarkStudios/cargo-deny) to ensure\nthat the licenses of the dependencies are acceptable.\n\n```shell\ncargo deny check --hide-inclusion-graph\n```\n\nThe allowed licenses are defined in the `deny.toml` file.\n\n#### Cargo `insta`\n\nIf you make changes that impact the command line interface arguments, manual pages or shell completions, you must update\nthe testing snapshots using Cargo [`insta`](https://insta.rs).\n\nAfter making your changes, run `cargo test` to generate the new snapshots followed by `cargo insta` to review and update\nthe snapshots.\n\n```shell\ncargo test && cargo insta review\n```\n\n#### Cargo `spelling`\n\nIf you make changes to code documentation, you must run Cargo [`spellcheck`](https://github.com/drahnr/cargo-spellcheck)\nto ensure they are free from misspellings and typos.\n\nTo check the spelling:\n\n```shell\ncargo spellcheck check\n```\n\nThe configuration for `spellcheck` is defined in the `.config/spellcheck.toml` file and the custom dictionary is defined\nin the `.config/trippy.dic` file.\n\n#### Cargo `msrv`\n\nIf you add or update dependencies, you should use the Cargo [msrv](https://github.com/foresterre/cargo-msrv) tool to\ncheck the Minimum Supported Rust Version (MSRV) to ensure that the new dependencies are compatible with the current\nMSRV.\n\nTo check the MSRV of the `trippy` crate:\n\n```shell\ncargo msrv verify --manifest-path crates/trippy/Cargo.toml-- cargo check\n```\n\n#### `dprint`\n\nThe [`dprint`](https://dprint.dev/) tool is needed to ensure consistent formatting of the non-Rust portions of the\ncodebase and docs.\n\nTo format the non-Rust code:\n\n```shell\ndprint fmt\n```\n\nThe configuration for `dprint` is defined in the `dprint.json` file.\n\n#### Docker\n\nIf you make changes to the `Dockerfile`, you should build the Docker image locally to ensure it builds correctly.\n\n```shell\ndocker build . -t trippy:dev\n```\n\n> [!NOTE]\n> If you add new files that are required at build time then you must update the `Dockerfile` to include them explicitly.\n\n### Development process\n\nThis section describes the development process for Trippy.\n\n#### Conventional commits\n\nAll commit messages should follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format.\n\nThe commit message should be structured as follows:\n\n```text\n<type>[optional scope]: <description>\n```\n\nWhere `type` is one of the following:\n\n- `feat`: A new feature\n- `fix`: A bug fix\n- `chore`: Build process, dependency and version updates\n- `docs`: Documentation only changes\n- `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)\n- `refactor`: A code change that neither fixes a bug nor adds a feature\n- `test`: Adding missing tests or correcting existing tests\n- `build`: Changes that affect the build system or external dependencies\n- `ci`: Changes to our CI configuration files and scripts\n- `revert`: Reverts a previous commit\n\nThe `scope` is optional and, if given, should be the name of the crate being modified, currently one of `core`,\n`packet`, `dns`, `privilege`, `tui` or `trippy`.\n\n> [!NOTE]\n> Small do-one-things commits are preferred over large do-many-things commits. This makes changes easier to review and\n> revert if necessary. For example, if you are adding a new feature and fixing a bug, it is better to create two\n> separate commits.\n\n## Releases\n\nInstructions for releasing a new `0.xx.0` version of Trippy.\n\nMany distribution packages are managed by external maintainers, however the following are managed by the Trippy\nmaintainers:\n\n- GitHub Releases\n- Crates.io\n- Docker\n- Snapcraft\n- WinGet\n- Ubuntu PPA\n\n### Prerequisites\n\n- Check the MSRV (Minimum Supported Rust Version) in the `trippy` crate `Cargo.toml` is still correct.\n\n> [!NOTE]\n> The MSRV should typically be the version from around 1 year before the current date to maximise compatibility.\n\n- Update all dependencies to the latest SemVer compatible versions\n\n> [!NOTE]\n> Some distributions may not support the latest versions of all dependencies, so be conservative with updates.\n\n- Record and add an `assets/0.xx.0/demo.gif` for the new version\n- Update the `README.md` with details of the features in the new version\n- Update the `CHANGELOG.md` for the new version\n- Update the `RELEASES.md` for the new version\n- Update the version to `0.xx.0` in `Cargo.toml`, `snap/snapcraft.yaml` & `ubuntu-ppa/release.sh`\n\n### Testing\n\nTrippy is tested extensively in CI on Linux, Windows and macOS for every pull request. However, it is recommended to\ntest the release binaries on all platforms before release.\n\n### GitHub Releases\n\n- Tag the release with the version number `0.xx.0` and push the tag to GitHub:\n\n```shell\ngit tag 0.xx.0\ngit push origin tag 0.xx.0\n```\n\nThis will trigger the GitHub Actions workflow to build the release binaries and publish them to the GitHub release page.\n\n- Edit GitHub release page and copy the relevant sections from `RELEASES.md` and `CHANGELOG.md`. Refer to previous\n  releases for the format.\n\n### Crates.io\n\n- Publish all crates to crates.io (in order):\n\n```shell\ncargo publish -p trippy-dns\ncargo publish -p trippy-packet\ncargo publish -p trippy-privilege\ncargo publish -p trippy-core\ncargo publish -p trippy-tui\ncargo publish -p trippy\n```\n\n### Docker\n\nFrom the repository root directory:\n\n```shell\ndocker build . -t fujiapple/trippy:0.xx.0 -t fujiapple/trippy:latest\ndocker push fujiapple/trippy:0.xx.0\ndocker push fujiapple/trippy:latest\n```\n\n### Snapcraft\n\n- Promote the first `0.xx.0` build to the `latest/stable` channel from the\n  Snapcraft [releases](https://snapcraft.io/trippy/releases) page\n\n### WinGet\n\n- Download the latest release Windows `zip` from\n  the [GitHub releases page](https://github.com/fujiapple852/trippy/releases/latest)\n- Determine the SHA256 checksum of the release:\n\n```shell\nshasum -a 256 trippy-0.xx.0-x86_64-pc-windows-msvc.zip\n```\n\n- Update the `winget` [fork](https://github.com/fujiapple852/winget-pkgs) to the latest upstream\n- Checkout the fork and create a branch called `fujiapple852-trippy-0.xx.0`\n- Go to the Trippy directory\n\n```shell\ncd winget-pkgs/manifests/f/FujiApple/Trippy\n```\n\n- Copy the previous `0.yy.0` directory to a new directory for the new `0.xx.0` version\n- Update the `PackageVersion`, `ReleaseDate` and update all paths to the new version\n- Update the `InstallerSha256` with the checksum from the previous step\n- Update the release notes from [CHANGELOG.md](https://github.com/fujiapple852/trippy/blob/master/CHANGELOG.md)\n- Commit the changes with message:\n\n```text\nupdate fujiapple852/trippy to 0.xx.0\n```\n\n- Push the branch to the fork and create a pull request against the upstream `winget-pkgs` repository\n\n### Ubuntu PPA\n\nSee the Ubuntu PPA [README.md](https://github.com/fujiapple852/trippy/blob/master/ubuntu-ppa/README.md)\n\n## Help wanted\n\nThere are several the issues tagged\nas [help wanted](https://github.com/fujiapple852/trippy/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) in the\nGitHub issue tracker for which I would be especially grateful for assistance.\n\n## License\n\nThis project is distributed under the terms of the Apache License (Version 2.0).\n\nUnless you explicitly state otherwise, any contribution intentionally submitted for inclusion in time by you, as defined\nin the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [\n  \"crates/trippy\",\n  \"crates/trippy-tui\",\n  \"crates/trippy-core\",\n  \"crates/trippy-packet\",\n  \"crates/trippy-privilege\",\n  \"crates/trippy-dns\",\n  \"examples/*\",\n]\n\n[workspace.package]\nversion = \"0.14.0-dev\"\nauthors = [\"FujiApple <fujiapple852@gmail.com>\"]\ndocumentation = \"https://github.com/fujiapple852/trippy\"\nhomepage = \"https://github.com/fujiapple852/trippy\"\nrepository = \"https://github.com/fujiapple852/trippy\"\nreadme = \"README.md\"\nlicense = \"Apache-2.0\"\nedition = \"2024\"\nrust-version = \"1.85\"\nkeywords = [\"cli\", \"tui\", \"traceroute\", \"ping\", \"icmp\"]\ncategories = [\"command-line-utilities\", \"network-programming\"]\n\n[workspace.dependencies]\ntrippy-tui = { version = \"0.14.0-dev\", path = \"crates/trippy-tui\" }\ntrippy-core = { version = \"0.14.0-dev\", path = \"crates/trippy-core\" }\ntrippy-privilege = { version = \"0.14.0-dev\", path = \"crates/trippy-privilege\" }\ntrippy-dns = { version = \"0.14.0-dev\", path = \"crates/trippy-dns\" }\ntrippy-packet = { version = \"0.14.0-dev\", path = \"crates/trippy-packet\" }\nanyhow = \"1.0.91\"\narrayvec = { version = \"0.7.6\", default-features = false }\nbitflags = \"2.11.0\"\ncaps = \"0.5.6\"\nchrono = { version = \"0.4.44\", default-features = false }\nchrono-tz = \"0.10.4\"\nclap = { version = \"4.5.60\", default-features = false }\nclap-cargo = \"0.15.2\"\nclap_complete = \"4.6.0\"\nclap_mangen = \"0.2.20\"\ncomfy-table = { version = \"7.1.4\", default-features = false }\ncrossbeam = \"0.8.4\"\ncrossterm = { version = \"0.28.1\", default-features = false }\ncsv = \"1.4.0\"\nderive_more = { version = \"2.1.1\", default-features = false }\ndns-lookup = \"3.0.1\"\nencoding_rs_io = \"0.1.7\"\netcetera = \"0.10.0\"\nfutures-concurrency = \"7.6.3\"\nhex-literal = \"1.1.0\"\nhickory-resolver = \"0.24.4\"\nhumantime = \"2.3.0\"\nindexmap = { version = \"2.13.0\", default-features = false }\ninsta = \"1.46.3\"\nitertools = \"0.14.0\"\nmaxminddb = \"0.27.3\"\nmockall = \"0.14.0\"\nnix = { version = \"0.31.2\", default-features = false }\nparking_lot = \"0.12.5\"\npaste = \"1.0.15\"\npetgraph = \"0.8.3\"\npretty_assertions = \"1.4.1\"\nrand = \"0.10.0\"\nratatui = \"0.29.0\"\nserde = { version = \"1.0.201\", default-features = false }\nserde_json = { version = \"1.0.117\", default-features = false }\nserde_with = { version = \"3.17.0\", default-features = false, features = [\"macros\"] }\nsocket2 = \"0.6.3\"\nstrum = { version = \"0.28.0\", default-features = false }\nsys-locale = \"0.3.2\"\ntest-case = \"3.3.1\"\nthiserror = \"2.0.3\"\ntokio = \"1.50.0\"\ntokio-util = \"0.7.18\"\ntoml = { version = \"1.0.7\", default-features = false, features = [\"serde\"] }\ntracing = \"0.1.44\"\ntracing-chrome = \"0.7.2\"\ntracing-subscriber = { version = \"0.3.23\", default-features = false }\ntun-rs = \"2.8.2\"\nunic-langid = \"0.9.6\"\nunicode-width = \"0.2.0\"\nwidestring = \"1.2.1\"\nwindows-sys = \"0.52.0\"\n\n[workspace.lints.rust]\nunsafe_code = \"deny\"\nrust_2018_idioms = { level = \"warn\", priority = -1 }\n\n[workspace.lints.clippy]\nall = { level = \"warn\", priority = -1 }\npedantic = { level = \"warn\", priority = -1 }\nnursery = { level = \"warn\", priority = -1 }\nmodule_name_repetitions = \"allow\"\noption_if_let_else = \"allow\"\ncast_possible_truncation = \"allow\"\nmissing_errors_doc = \"allow\"\ncast_precision_loss = \"allow\"\nbool_assert_comparison = \"allow\"\nmissing_const_for_fn = \"allow\"\nstruct_field_names = \"allow\"\ncognitive_complexity = \"allow\"\n\n[profile.release]\nlto = true\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM rust:1.85 AS build-env\nRUN rustup target add x86_64-unknown-linux-musl\nWORKDIR /app\nCOPY Cargo.toml /app\nCOPY Cargo.lock /app\nRUN mkdir -p /app/crates/trippy/src\nRUN mkdir -p /app/crates/trippy-tui/src\nRUN mkdir -p /app/crates/trippy-core/src\nRUN mkdir -p /app/crates/trippy-dns/src\nRUN mkdir -p /app/crates/trippy-packet/src\nRUN mkdir -p /app/crates/trippy-privilege/src\nCOPY crates/trippy/Cargo.toml /app/crates/trippy/Cargo.toml\nCOPY crates/trippy-tui/Cargo.toml /app/crates/trippy-tui/Cargo.toml\nCOPY crates/trippy-core/Cargo.toml /app/crates/trippy-core/Cargo.toml\nCOPY crates/trippy-dns/Cargo.toml /app/crates/trippy-dns/Cargo.toml\nCOPY crates/trippy-packet/Cargo.toml /app/crates/trippy-packet/Cargo.toml\nCOPY crates/trippy-privilege/Cargo.toml /app/crates/trippy-privilege/Cargo.toml\nCOPY examples/ /app/examples/\n\n# dummy build to cache dependencies\nRUN echo \"fn main() {}\" > /app/crates/trippy/src/main.rs\nRUN touch /app/crates/trippy-tui/src/lib.rs\nRUN touch /app/crates/trippy-core/src/lib.rs\nRUN touch /app/crates/trippy-dns/src/lib.rs\nRUN touch /app/crates/trippy-packet/src/lib.rs\nRUN touch /app/crates/trippy-privilege/src/lib.rs\nRUN cargo build --release --target=x86_64-unknown-linux-musl --package trippy\n\n# copy the actual application code and build\nCOPY crates/trippy/src /app/crates/trippy/src\nCOPY crates/trippy-tui/src /app/crates/trippy-tui/src\nCOPY crates/trippy-core/src /app/crates/trippy-core/src\nCOPY crates/trippy-dns/src /app/crates/trippy-dns/src\nCOPY crates/trippy-packet/src /app/crates/trippy-packet/src\nCOPY crates/trippy-privilege/src /app/crates/trippy-privilege/src\nCOPY crates/trippy-tui/build.rs /app/crates/trippy-tui\nCOPY crates/trippy-tui/locales.toml /app/crates/trippy-tui\nCOPY trippy-config-sample.toml /app\nCOPY trippy-config-sample.toml /app/crates/trippy-tui\nCOPY README.md /app\nCOPY README.md /app/crates/trippy\nRUN cargo clean --release --target=x86_64-unknown-linux-musl -p trippy-tui -p trippy-core -p trippy-dns -p trippy-packet -p trippy-privilege\nRUN cargo build --release --target=x86_64-unknown-linux-musl\n\nFROM alpine\nRUN apk update && apk add ncurses\nCOPY --from=build-env /app/target/x86_64-unknown-linux-musl/release/trip /\nENTRYPOINT [\"./trip\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/docs/src/assets/Trippy-Vertical-DarkMode.svg#gh-dark-mode-only\" width=\"300\">\n  <img src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/docs/src/assets/Trippy-Vertical.svg#gh-light-mode-only\" width=\"300\"><br>\n  <br>\n  <a href=\"https://github.com/fujiapple852/trippy/actions/workflows/ci.yml\">\n    <img src=\"https://github.com/fujiapple852/trippy/actions/workflows/ci.yml/badge.svg?branch=master\"></a>\n  <a href=\"https://crates.io/crates/trippy/0.13.0\">\n    <img src=\"https://img.shields.io/crates/v/trippy.svg\"></a>\n  <a href=\"https://repology.org/project/trippy/versions\">\n    <img src=\"https://repology.org/badge/tiny-repos/trippy.svg\"></a>\n  <a href=\"https://trippy.zulipchat.com\">\n    <img src=\"https://img.shields.io/badge/zulip-join_chat-brightgreen.svg\"></a>\n  <a href=\"https://matrix.to/#/#trippy-dev:matrix.org\">\n    <img src=\"https://img.shields.io/badge/matrix/trippy-dev:matrix.org-blue\"></a>\n  <br>\n  <br>\n  Trippy combines the functionality of traceroute and ping and is designed to assist with the analysis of networking\nissues.\n</p>\n\n<img src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.12.0/demo.gif\" alt=\"trippy\"/>\n\n## Quick Start\n\nSee the [getting started](https://trippy.rs/start/getting-started) guide.\n\n### Install\n\nTrippy runs on Linux, BSD, macOS, and Windows. It can be installed from most package managers, precompiled binaries, or\nsource.\n\nFor example, to install Trippy from `cargo`:\n\n```shell\ncargo install trippy --locked\n```\n\n<details>\n\n<summary>All package managers</summary>\n\n### Cargo\n\n[![Crates.io](https://img.shields.io/crates/v/trippy)](https://crates.io/crates/trippy/0.13.0)\n\n```shell\ncargo install trippy --locked\n```\n\n### APT (Debian)\n\n[![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy)\n\n```shell\napt install trippy\n```\n\n> ⓘ Note:\n>\n> Only available for Debian 13 (`trixie`) and later.\n\n### PPA (Ubuntu)\n\n[![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.13.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages)\n\n```shell\nadd-apt-repository ppa:fujiapple/trippy\napt update && apt install trippy\n```\n\n> ⓘ Note:\n>\n> Only available for Ubuntu 24.04 (`Noble`) and 22.04 (`Jammy`).\n\n### Snap (Linux)\n\n[![trippy](https://snapcraft.io/trippy/badge.svg)](https://snapcraft.io/trippy)\n\n```shell\nsnap install trippy\n```\n\n### Homebrew (macOS)\n\n[![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/trippy.svg)](https://formulae.brew.sh/formula/trippy)\n\n```shell\nbrew install trippy\n```\n\n### WinGet (Windows)\n\n[![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)\n\n```shell\nwinget install trippy\n```\n\n### Scoop (Windows)\n\n[![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)\n\n```shell\nscoop install trippy\n```\n\n### Chocolatey (Windows)\n\n[![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy)\n\n```shell\nchoco install trippy\n```\n\n### NetBSD\n\n[![pkgsrc current package](https://repology.org/badge/version-for-repo/pkgsrc_current/trippy.svg)](https://pkgsrc.se/net/trippy)\n\n```shell\npkgin install trippy\n```\n\n### FreeBSD\n\n[![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/trippy.svg)](https://www.freshports.org/net/trippy/)\n\n```shell\npkg install trippy\n```\n\n### OpenBSD\n\n[![OpenBSD port](https://repology.org/badge/version-for-repo/openbsd/trippy.svg)](https://openports.pl/path/net/trippy)\n\n```shell\npkg_add trippy\n```\n\n### Arch Linux\n\n[![Arch package](https://repology.org/badge/version-for-repo/arch/trippy.svg)](https://archlinux.org/packages/extra/x86_64/trippy)\n\n```shell\npacman -S trippy\n```\n\n### Gentoo Linux\n\n[![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/trippy.svg)](https://packages.gentoo.org/packages/net-analyzer/trippy)\n\n```shell\nemerge -av net-analyzer/trippy\n```\n\n### Void Linux\n\n[![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)\n\n```shell\nxbps-install -S trippy\n```\n\n### ALT Sisyphus\n\n[![ALT Sisyphus package](https://repology.org/badge/version-for-repo/altsisyphus/trippy.svg)](https://packages.altlinux.org/en/sisyphus/srpms/trippy/)\n\n```shell\napt-get install trippy\n```\n\n### Chimera Linux\n\n[![Chimera Linux package](https://repology.org/badge/version-for-repo/chimera/trippy.svg)](https://github.com/chimera-linux/cports/tree/master/user/trippy)\n\n```shell\napk add trippy\n```\n\n### Nix\n\n[![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)\n\n```shell\nnix-env -iA trippy\n```\n\n### Docker\n\n[![Docker Image Version (latest by date)](https://img.shields.io/docker/v/fujiapple/trippy)](https://hub.docker.com/r/fujiapple/trippy/)\n\n```shell\ndocker run -it fujiapple/trippy\n```\n\n### All Repositories\n\n[![Packaging status](https://repology.org/badge/vertical-allrepos/trippy.svg)](https://repology.org/project/trippy/versions)\n\n</details>\n\nSee the [installation](https://trippy.rs/start/installation) guide for details of how to install Trippy on your system.\n\n### Run\n\nTo run a basic trace to `example.com` with default settings, use the following command:\n\n```shell\nsudo trip example.com\n```\n\nSee the [usage examples](https://trippy.rs/guides/usage) and [CLI reference](https://trippy.rs/reference/cli) for\ndetails of how to use Trippy. To use Trippy without elevated privileges, see\nthe [privileges](https://trippy.rs/guides/privileges) guide.\n\n## Documentation\n\nFull documentation is available at [trippy.rs](https://trippy.rs).\n\n<details>\n\n<summary>documentation links</summary>\n\n## Getting Started\n\nSee the [Getting Started](https://trippy.rs/start/getting-started/) guide.\n\n## Features\n\nSee the [Features](https://trippy.rs/start/features/) list.\n\n## Distributions\n\nSee the [Distributions](https://trippy.rs/start/installation/) list.\n\n## Privileges\n\nSee the [Privileges](https://trippy.rs/guides/privileges/) guide.\n\n## Usage Examples\n\nSee the [Usage Examples](https://trippy.rs/guides/usage/).\n\n## Command Reference\n\nSee the [Command Reference](https://trippy.rs/reference/cli/).\n\n## Theme Reference\n\nSee the [Theme Reference](https://trippy.rs/reference/theme/).\n\n## Column Reference\n\nSee the [Column Reference](https://trippy.rs/reference/column/).\n\n## Configuration Reference\n\nSee the [Configuration Reference](https://trippy.rs/reference/configuration/).\n\n## Locale Reference\n\nSee the [Locale Reference](https://trippy.rs/reference/locale/).\n\n## Versions\n\nSee the [Version Reference](https://trippy.rs/reference/version/).\n\n## Frequently Asked Questions\n\n### Why does Trippy show \"Awaiting data...\"?\n\nSee the [Awaiting Data](https://trippy.rs/guides/faq/) guide.\n\n<a name=\"windows-defender\"></a>\n\n### How do I allow incoming ICMP traffic in the Windows Defender firewall?\n\nSee the [Windows Defender Firewall](https://trippy.rs/guides/windows_firewall/) guide.\n\n### What are the recommended settings for Trippy?\n\nSee the [Recommended Tracing Settings](https://trippy.rs/guides/recommendation/) guide.\n\n</details>\n\n## Acknowledgements\n\nTrippy is made possible by [ratatui](https://github.com/ratatui-org/ratatui) (\nformerly [tui-rs](https://github.com/fdehau/tui-rs)),\n[crossterm](https://github.com/crossterm-rs/crossterm) as well\nas [several](https://github.com/fujiapple852/trippy/blob/master/Cargo.toml) foundational Rust libraries.\n\nTrippy draws heavily from [mtr](https://github.com/traviscross/mtr) and also incorporates ideas\nfrom both [libparistraceroute](https://github.com/libparistraceroute/libparistraceroute)\n& [Dublin Traceroute](https://github.com/insomniacslk/dublin-traceroute).\n\nThe Trippy networking code is inspired by [pnet](https://github.com/libpnet/libpnet) and some elements of that codebase\nare incorporated in Trippy.\n\nThe [AS][autonomous_system] data is retrieved from\nthe [IP to ASN Mapping Service](https://team-cymru.com/community-services/ip-asn-mapping/#dns) provided\nby [Team Cymru](https://team-cymru.com).\n\nThe [trippy.cli.rs](https://trippy.cli.rs) CNAME hosting is provided by [cli.rs](https://cli.rs).\n\nThe Trippy chat room is sponsored by [Zulip](https://zulip.com).\n\nTrippy logo designed by [Harun Ocaksiz Design](https://www.instagram.com/harunocaksiz).\n\n## License\n\nThis project is distributed under the terms of the Apache License (Version 2.0).\n\nUnless you explicitly state otherwise, any contribution intentionally submitted for inclusion in time by you, as defined\nin the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.\n\nSee [LICENSE](LICENSE) for details.\n\nCopyright 2022 [Trippy Contributors](https://github.com/fujiapple852/trippy/graphs/contributors)\n\n[autonomous_system]: https://en.wikipedia.org/wiki/Autonomous_system_(Internet)\n"
  },
  {
    "path": "RELEASES.md",
    "content": "# Release Notes\n\nRelease notes for Trippy 0.6.0 onwards. See also the [CHANGELOG](CHANGELOG.md).\n\n# 0.13.0\n\n## Highlights\n\nThe 0.13.0 release of Trippy includes several enhancements related to Type of Service (`ToS`) and adds new `Dscp` and\n`Ecn` columns.\n\nIt also includes improvements to the TUI such as allowing a custom timezone to be set and adding the ability to read the\nconfiguration file from the XDG app config directory. The `json` report has been enriched with start and end timestamps.\n\nThis release includes a number of bug fixes. For Windows users in particular, this release includes several important\nstability improvements.\n\nThe release also introduces a new `system` address family option, which will become the new default in the next major\nrelease.\n\nFinally, Trippy now has a dedicated website and a logo!\n\n### Type of Service (DSCP/ECN) Improvements\n\nTrippy allows setting the Type of Service (`ToS`) for IPv4 via the `--tos` (`-Q`) command-line argument (or via the\nconfiguration file). The `ToS` value is the second byte of the `IPv4` header and encodes the Differentiated Services\nCode Point (`DSCP`) and Explicit Congestion Notification (`ECN`) fields.\n\nSetting the `ToS` on outgoing probe packets can influence the Quality of Service (`QoS`) used by the network devices\nalong the path.\n\nProbe responses received from the hops along the path include the `ToS` values in the Original Datagram (the `IPv4`/\n`IPv6` header of the probe packet nested inside the `ICMP` error). Examining the `ToS` value from the Original Datagram\ncan provide useful insight into the `QoS` treatment of the probe packets by network devices along the path.\n\nThis release of Trippy adds two new columns to display the `DSCP` & `ECN` values, which are derived from the `ToS` value\nfrom the Original Datagram for each hop. The new columns are:\n\n- `Dscp` (`K`): The Differentiated Services Code Point (`DSCP`) of the Original Datagram for a hop\n- `Ecn` (`M`): The Explicit Congestion Notification (`ECN`) of the Original Datagram for a hop\n\nThe `Dscp` and `Ecn` columns are decoded from the `ToS` field of the Original Datagram. If no `ToS` value is present,\nthen the columns will show `na`. Note that these columns show the most recent `ToS` value received from the hop and\nmay therefore change between rounds.\n\nWell-known `DSCP` values are displayed as follows:\n\n- Default Forwarding (`DF`) aka Best Effort aka Class Selector 0 (`CS0`)\n- Assured Forwarding (`AFn`)\n- Class Selector (`CSn`)\n- High Priority Expedited Forwarding (`EF`)\n- Voice Admit (`VA`)\n- Lower Effort (`LE`)\n\nUnknown `DSCP` values are displayed as a hexadecimal value.\n\nThe `ECN` value is displayed as follows:\n\n- Not ECN-Capable Transport (`NotECT`)\n- ECN Capable Transport(1) (`ECT1`)\n- ECN Capable Transport(0) (`ECT0`)\n- Congestion Experienced (`CE`)\n\nThese columns are hidden by default but can be enabled as needed. For more details, see\nthe [Column Reference](https://trippy.rs/reference/column).\n\nThe following example sets the `ToS` to be `224`, which is a `DSCP` value of `CS7` (0x38) and an `ECN` value of\n`NotECT` (0x0), and enables the new columns:\n\n```shell\ntrip example.com --tos 224 --tui-custom-columns holsravbwdtKM\n```\n\nThe following screenshot shows the example trace:\n\n<img width=\"60%\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.13.0/dscp_ecn.png\"/>\n\nThe `ToS` field of the Original Datagram has also been added to the `json` output format as a decimal value.\n\nSee [#1539](https://github.com/fujiapple852/trippy/issues/1539) for details.\n\nThis release also adds support for setting the `IPv6` Traffic Class (which encodes the `DSCP` & `ECN` values in the same\nway as `IPv4`) via the same `--tos` (`-Q`) command-line argument (or via the configuration file). Note that setting the\n`IPv6` Traffic Class is not currently supported on Windows.\nSee [#202](https://github.com/fujiapple852/trippy/issues/202) for details.\n\nFinally, a bug which caused the `--tos` (`-Q`) command-line argument to be ignored for `IPv4/UDP` tracing has been\nfixed in this release. See [#1540](https://github.com/fujiapple852/trippy/issues/1540) for details.\n\n### Custom TUI Timezone\n\nTrippy shows the wall-clock time in the header of the TUI. Currently, this is set to show the local timezone of the\nsystem running Trippy. This can be problematic for users who are running Trippy in a container or on a remote system\nthat uses a different timezone.\n\nThis release adds the ability to set a custom timezone for the TUI using the `--tui-timezone` command-line argument (or\nvia the configuration file). The timezone can be set to any valid IANA timezone identifier, such as `UTC`,\n`America/New_York`, or `Europe/London`.\n\nThe following example sets the timezone to `UTC`:\n\n```shell\ntrip example.com --tui-timezone UTC\n```\n\nThis can be made permanent by setting the `tui-timezone` value in the `tui` section of the configuration file:\n\n```toml\n[tui]\ntui-timezone = \"UTC\"\n```\n\nSee [#1513](https://github.com/fujiapple852/trippy/issues/1513) for details.\n\n### XDG App Config Directory\n\nTrippy will now attempt to locate a `trippy.toml` or `.trippy.toml` config file in the XDG app config directory\n(i.e. `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy`) in addition to existing locations.\n\nThis allows users to store their Trippy configuration files in a dedicated directory for Trippy, separate from other\napplications.\n\nThe full list of locations Trippy will check for a `trippy.toml` or `.trippy.toml` config file is as\nfollows:\n\n- the current directory\n- the user home directory\n- the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config`\n- the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy`\n- the Windows data directory (Windows only): `%APPDATA%`\n\nSee [#1528](https://github.com/fujiapple852/trippy/issues/1528) for details.\n\n### System Address Family\n\nTrippy supports tracing for both `IPv4` and `IPv6` address families. If the tracing target is supplied as a hostname,\nTrippy will attempt to resolve the hostname to a single `IPv4` or `IPv6` address. If the hostname resolves to both,\nTrippy will use the address family (`--addr-family`) configuration to determine which address family\nto use.\n\nThe possible values for `--addr-family` are:\n\n- `ipv4` - Lookup IPv4 only\n- `ipv6` - Lookup IPv6 only\n- `ipv6-then-ipv4` - Lookup IPv6 with a fallback to IPv4\n- `ipv4-then-ipv6` - Lookup IPv4 with a fallback to IPv6 [default]\n\nThe current default value for `--addr-family` is `ipv4-then-ipv6`, which means that if the hostname resolves to both\n`IPv4` and `IPv6` addresses, Trippy will prefer the `IPv4` address family.\n\nSome users find the default behavior undesirable, as it can lead to unexpected results when the hostname resolves to a\ndifferent address family than the one used by other applications on the system.\n\nThis release adds a new value for `--addr-family` called `system`. This value defers the choice of address family to the\nfirst address returned by OS resolver. This means that if the hostname resolves to both `IPv4` and `IPv6` addresses, the\nOS resolver will determine which address family to use based on the OS configuration.\n\nNote that if the `--addr-family` value is set to `system` and the `--dns-resolve-method` is set to any value _other_\nthan `system` (i.e. `resolv`, `cloudflare` or `google`), then the address family lookup will effectively default to\n`ipv6-then-ipv4`.\n\n> **Important**: The default value for `--addr-family` will change to become `system` in the next major release of\n> Trippy (0.14.0). This will be a breaking change for users who rely on the current default value of `ipv4-then-ipv6`.\n\nSee [#1469](https://github.com/fujiapple852/trippy/issues/1469) for details.\n\n### Remove Address Family \"downgrade\" for Dublin Strategy\n\nCurrently, the address families `ipv4-then-ipv6` and `ipv6-then-ipv4` are silently _downgraded_ to `ipv4` when the\n`dublin` ECMP strategy is used. This behaviour was previously necessary, as Trippy did not support the `dublin` ECMP\nstrategy for `IPv6`. However, Trippy has supported the `dublin` ECMP strategy for `IPv6` since version 0.10.0. As a\nresult, this release removes the address family _downgrade_ for the `dublin` ECMP strategy.\n\nSee [#1476](https://github.com/fujiapple852/trippy/issues/1476) for details.\n\n### Windows Stability Improvements\n\nThis release includes several stability improvements for Windows. It fixes several known or potential issues that\ncould cause crashes or memory corruption. It is recommended that all Windows users upgrade to this release.\n\nSee the following issues for details:\n\n- Memory corruption on Windows ([#1527](https://github.com/fujiapple852/trippy/issues/1527))\n- Socket being closed twice on Windows ([#1443](https://github.com/fujiapple852/trippy/issues/1443))\n- Potential crash on Windows for adapters without unicast\n  addresses ([#1547](https://github.com/fujiapple852/trippy/issues/1547))\n- Potential use-after-free when discovering source address on\n  Windows ([#1558](https://github.com/fujiapple852/trippy/issues/1558))\n\n### Start and End Timestamps in JSON report\n\nThe Trippy `json` report mode has been enhanced to show the start and end timestamps for the trace. These timestamps are\nshown in UTC using RFC 3339 format.\n\nThe following example runs a trace to `example.com` for a single round and outputs the results in `json` format:\n\n```shell\ntrip example.com -m json -C 1\n```\n\nThe `info` section of the output now includes the `start_timestamp` and `end_timestamp` fields:\n\n```json\n{\n  \"info\": {\n    \"target\": {\n      \"ip\": \"23.192.228.80\",\n      \"hostname\": \"example.com\"\n    },\n    \"start_timestamp\": \"2025-05-04T09:50:10.383221Z\",\n    \"end_timestamp\": \"2025-05-04T09:50:11.392039Z\"\n  }\n}\n```\n\nSee [#1510](https://github.com/fujiapple852/trippy/issues/1510) for details.\n\n### Reduce Tracing Verbosity\n\nThe `trippy-core` crate logging is overly verbose. This release reduces all `#[instrument]` annotations from the default\n`info` level to the `trace` level. It also removes some tracing annotations and, in some cases, adds new ones.\n\nThere are now no `info` level logs and only a handful of `debug` level logs:\n\n- Log the channel config when a channel is created (typically just once)\n- Log the strategy config when the tracer is started (typically just once)\n- Log each probe sent and received during a round (typically a handful per round)\n\nFor application users there is no change; however the default logging level (`--log-filter trippy=debug`) used when `-v`\nis passed will now show substantially fewer logs. Users can set `--log-filter trippy=trace` to see a logging level\nsimilar to the previous default.\n\nSee [#1482](https://github.com/fujiapple852/trippy/issues/1482) for details.\n\n### Bug Fixes\n\nThis release fixes a bug where ICMP packets larger than 256 bytes could cause a tracer panic.\nSee [#1561](https://github.com/fujiapple852/trippy/issues/1561) for details.\n\nIt also adds a handful of missing configuration options to the settings dialog.\nSee [#1541](https://github.com/fujiapple852/trippy/issues/1541) for details.\n\n### Trippy Website & Logo\n\nTrippy now has a dedicated website: https://trippy.rs\n\nThe website is now the primary source of documentation. My thanks to @orhun for building the https://binsider.dev\nwebsite which I ~~took inspiration from~~ shamelessly copied for Trippy.\n\nAlong with the new website, Trippy (finally!) has a logo:\n\n<img src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/docs/src/assets/Trippy-Vertical-DarkMode.svg#gh-dark-mode-only\" width=\"200\">\n<img src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/docs/src/assets/Trippy-Vertical.svg#gh-light-mode-only\" width=\"200\"><br>\n\nWith thanks to [Harun Ocaksiz Design](https://www.instagram.com/harunocaksiz).\n\nSee [#100](https://github.com/fujiapple852/trippy/issues/100) for details.\n\n### New Distribution Packages\n\nTrippy has been added to the Void Linux package repository (with thanks to @icp1994!):\n\n[![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)\n\n```shell\nxbps-install -S trippy\n```\n\nTrippy was also added to ALT Sisyphus package repository (with thanks\nto [Aleksandr Voyt](https://packages.altlinux.org/en/sisyphus/maintainers/sobue)!)\n\n[![ALT Sisyphus package](https://repology.org/badge/version-for-repo/altsisyphus/trippy.svg)](https://packages.altlinux.org/en/sisyphus/srpms/trippy/)\n\n```shell\napt-get install trippy\n```\n\nFinally, Trippy has been added to the Chimera Linux package repository (with thanks to @ttyyls!):\n\n[![Chimera Linux package](https://repology.org/badge/version-for-repo/chimera/trippy.svg)](https://github.com/chimera-linux/cports/tree/master/user/trippy)\n\n```shell\napk add trippy\n```\n\n### Thanks\n\nMy thanks to all Trippy contributors, package maintainers, translators and community members.\n\nFeel free to drop by the Trippy Zulip room for a chat:\n\n[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://trippy.zulipchat.com/)\n\nHappy Tracing!\n\n# 0.12.2\n\n## Highlights\n\nThis maintenance release of Trippy fixes a bug introduced in 0.12.0 which causes a tracer panic if `--first-ttl` is\nset to be greater than one. The release also addresses a longstanding bug which causes `--dns-resolve-method resolv` to\nignore any value provided for `--addr-family` and therefore always use the default value of `ipv4`. Finally the help\ntext for `--addr-family` has been corrected.\n\nSee the main [0.12.0](https://github.com/fujiapple852/trippy/releases/tag/0.12.0) release note.\n\n# 0.12.1\n\n## Highlights\n\nThis maintenance release of Trippy fixes a bug which prevented translations from working in Docker and also divests all\ninternal use of `yaml` dependencies which were problematic to maintain on some platforms (thanks to @nc7s).\n\nSee the main [0.12.0](https://github.com/fujiapple852/trippy/releases/tag/0.12.0) release note.\n\n# 0.12.0\n\n## Highlights\n\nThe latest release of Trippy brings both cosmetic and functional improvements to the TUI, new columns, new distribution\npackages, and a number of bug fixes.\n\nThe TUI has been updated to include a new _information bar_ at the bottom of the screen which allows for the header to\nbe shortened and simplified. The sample history chart has been enhanced to highlight missing probes and the presentation\nof source and target addresses has also been simplified.\n\nAs well as these cosmetic changes, the TUI has gained support for internationalization (i18n) and the ability to\nadjust the hop privacy setting dynamically.\n\nThis release introduces three new columns, which provide novel heuristics for measuring _forward loss_ and _backward\nloss_, that are designed to assist users in interpreting the status of the trace.\n\nFinally, this update includes new distribution packages for Debian and Ubuntu and addresses a number of bugs.\n\n### TUI Information Bar\n\nThe TUI now includes an _information bar_ at the bottom of the screen, replacing the previous `Config` line in the\nheader. This change shortens the header by one line, optimizing space usage while keeping the overall vertical space of\nthe TUI unchanged.\n\nThe main TUI screen now appears as shown below (120x40 terminal size):\n\n<img width=\"60%\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.12.0/main_screen.png\"/>\n\nThe left-hand side of the information bar displays a selection of static configuration items (in order):\n\n- The address family and tracing protocol, e.g., `IPv4/ICMP`\n- The privilege level, either `privileged` or `unprivileged`\n- The locale, e.g., English (`en`), French (`fr`), etc.\n\nThe right-hand side of the information bar displays a selection of adjustable configuration items (in order):\n\n- A toggle controlling whether `ASN` information is displayed (`□ ASN` for disabled, `■ ASN` for enabled)\n- A toggle controlling whether hop detail mode is enabled (`□ detail` for disabled, `■ detail` for enabled)\n- A toggle controlling whether hostnames, IP addresses, or both are displayed (`host`, `ip`, or `both`)\n- The maximum `ttl` value for hop privacy, shown as `-` (privacy disabled) or a number (0, 1, 2, etc.)\n- The maximum number of hosts displayed per hop, shown as `-` (automatic) or a number (1, 2, etc.)\n\nIn the above screenshot, the information bar indicates the trace is using `IPv4/ICMP`, is running in `privileged` mode,\nthe locale is English (`en`), `ASN` information is displayed, hop detail mode is disabled, hostnames are displayed, the\nhop privacy maximum `ttl` is 2, and the maximum number of hosts per hop is set to automatic.\n\n> **Note**: The information bar displays only a small number of important settings. All other settings can be viewed in\n> the settings dialog, which can be opened by pressing `s` (default key binding).\n\nThe theme colors of the information bar can be customized using the `info-bar-bg-color` and `info-bar-text-color` theme\nitems. Refer to the [Theme Reference](https://github.com/fujiapple852/trippy#theme-reference) for more details.\n\nThanks to @c-git for their valuable feedback in refining the design of the information bar.\n\nSee [#1349](https://github.com/fujiapple852/trippy/issues/1349) for details.\n\n### Sample History Missing Probes\n\nTrippy displays a history of samples for each hop as a chart at the bottom of the TUI display. Each vertical line in the\nchart corresponds to one sample, representing the value of the `Last` column.\n\nPreviously, if a probe was lost, the sample for that round would be shown as a blank vertical line. Starting with this\nrelease, Trippy now highlights lost probes using a full vertical line in red (default theme color), making them easier\nto identify.\n\n<img width=\"60%\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.12.0/lost_probes.png\"/>\n\nThe theme color for regular samples can be configured using the existing `samples-chart-color` configuration option.\nAdditionally, the theme color for lost probes can now be customized using the new `samples-chart-lost-color`\nconfiguration option. For more details, see\nthe [Theme Reference](https://github.com/fujiapple852/trippy#theme-reference).\n\nSee [#1247](https://github.com/fujiapple852/trippy/issues/1247) for further details.\n\n### Source and Target Address Display Improvements\n\nThis release simplifies the display of the source and target addresses in the `Target` line in the header of the TUI.\n\nThe `Target` line has been updated such that, for both the source and destination addresses, the hostname is only shown\nif it differs from the IP address.\n\nFor the destination address:\n\n- If the user supplies a target hostname, it is resolved to an IP address, and both the IP address and the _provided_\n  hostname are shown.\n- If the user supplies an IP address, a reverse DNS hostname lookup is attempted. If successful, both the IP address and\n  the _first resolved_ hostname are shown; otherwise, only the IP address is displayed.\n\nFor the source address:\n\n- A reverse DNS hostname lookup is attempted. If successful, both the IP address and the _first resolved_ hostname are\n  shown; otherwise, only the IP address is displayed.\n\nFor example, when the user supplies an IP address as the tracing target, the `Target` line in the header is now shown as\nfollows:\n\n```\nTarget: 192.168.1.21 -> 93.184.215.14 (example.com)\n```\n\nSee [#1363](https://github.com/fujiapple852/trippy/issues/1363) for details.\n\n### Adjustable Hop Privacy Mode Settings\n\nTrippy includes a privacy feature designed to hide sensitive information, such as IP addresses and GeoIP data, for all\nhops up to a configurable maximum `ttl` via the `tui-privacy-max-ttl` configuration option.\n\nPreviously, the privacy feature could only be toggled on or off within the TUI using the `toggle-privacy` command and\nonly if `tui-privacy-max-ttl` was configured _before_ Trippy was started.\n\nIn this release, the `toggle-privacy` command has been deprecated and replaced by two new TUI commands,\n`expand-privacy` (bound to the `p` key by default) and `contract-privacy` (bound to the `o` key by default).\n\nThe `expand-privacy` command increases the `tui-privacy-max-ttl` value up to the maximum number of hops in the current\ntrace and the `contract-privacy` command decreases the `tui-privacy-max-ttl` value to the minimum value, which disables\nprivacy mode.\n\nSee [#1347](https://github.com/fujiapple852/trippy/issues/1347) for more details.\n\nThis release also repurposes the meaning of `tui-privacy-max-ttl` when set to `0`. Previously, a value of `0` indicated\nthat no hops should be hidden. Starting from this release, a value of `0` will indicate that the source of the trace, as\nshown in the `Target` line of the header, should be hidden.\n\nValues of `1` or greater retain their existing behavior but will now also hide the source of the trace in addition to\nthe specified number of hops.\n\nAs a result of this change, the default value for `tui-privacy-max-ttl` has been updated:\n\n- If not explicitly set (via a command-line argument or the configuration file), nothing will be hidden by default.\n- If explicitly set to `0` (the previous default), the source of the trace will be hidden.\n\nSee [#1365](https://github.com/fujiapple852/trippy/issues/1365) for details.\n\n### Preserve Screen on Exit\n\nTrippy previously supported the `--tui-preserve-screen` command-line flag, which could be used to prevent the terminal\nscreen from being cleared when Trippy exits. This feature is useful for users who wish to review trace results after\nexiting the application. However, the flag had to be set before starting Trippy and could not be toggled during a trace.\n\nThis release introduces the `quit-preserve-screen` TUI command (bound to the `shift+q` key by default). This command\nallows users to quit Trippy without clearing the terminal screen, regardless of whether the `--tui-preserve-screen` flag\nis set.\n\nSee [#1382](https://github.com/fujiapple852/trippy/issues/1382) for details.\n\n### TUI Internationalization (i18n)\n\nThe Trippy TUI has been translated into multiple languages. This includes all text displayed in the TUI across all\nscreens and dialogs, as well as GeoIP location data shown on the world map.\n\nThe TUI will automatically detect the system locale and use the corresponding translations if available. The locale can\nbe overridden using the `--tui-locale` configuration option.\n\nLocales can be specified for a language or a combination of language and region. For example a general locale can be\ncreated for English (`en`) and specific regional locales can be created, such as United Kingdom English (`en-UK`) and\nUnited States English (`en-US`).\n\nIf the user's chosen full locale (`language-region`) is not available, Trippy will fall back to using the locale for the\nlanguage only, if it exists. For example if the user sets the locale to `en-AU`, which is not currently defined in\nTrippy, it will fall back to the `en` locale, which is defined.\n\nIf the user's chosen locale does not exist at all, Trippy will fall back to English (`en`).\n\nLocales are generally added for the language only unless there is a specific need for region-based translations.\n\nSome caveats to be aware of:\n\n- The configuration file, command-line options, and most error messages are not translated.\n- Many common abbreviated technical terms, such as `IPv4` and `ASN`, are not translated.\n\nThe following example sets the TUI locale to be Chinese (`zh`):\n\n```shell\ntrip example.com --tui-locale zh\n```\n\nThis can be made permanent by setting the `tui-locale` value in the `tui` section of the configuration file:\n\n```toml\n[tui]\ntui-locale = \"zh\"\n```\n\nThe following screenshot shows the TUI with the locale set to Chinese (`zh`):\n\n<img width=\"60%\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.12.0/help_screen_zh.png\"/>\n\nThe list of available locales can be printed using the `--print-locales` flag:\n\n```shell\ntrip --print-locales\n```\n\nAs of this release, the following locales are available:\n\n- Chinese (`zh`)\n- English (`en`)\n- French (`fr`)\n- German (`de`)\n- Italian (`it`)\n- Portuguese (`pt`)\n- Russian (`ru`)\n- Spanish (`es`)\n- Swedish (`sv`)\n- Turkish (`tr`)\n\nSee [#1319](https://github.com/fujiapple852/trippy/issues/1319), [#1357](https://github.com/fujiapple852/trippy/issues/1357), [#1336](https://github.com/fujiapple852/trippy/issues/1336)\nand the [Locale Reference](https://github.com/fujiapple852/trippy#locale-reference) for more details.\n\nCorrections to existing translations or the addition of new translations are always welcome. See\nthe [tracking issue](https://github.com/fujiapple852/trippy/issues/506) for the status of each translation and details\non how to contribute.\n\nAdding these translations has been a significant effort and I would like to express a huge _thank you_ (谢谢! Merci!\nDanke! Grazie! Obrigado! Спасибо! Gracias! Tack! Teşekkürler!) to @0323pin, @arda-guler, @histrio, @josueBarretogit,\n@one, @orhun, @peshay, @ricott1, @sxyazi, @ulissesf, and @zarkdav for all of their time and effort adding and reviewing\ntranslations for this release.\n\n### Forward and Backward Packet Loss Heuristics\n\nIn line with most classic traceroute tools, Trippy displays the number of probes sent (`Snd`), received (`Recv`), and a\nloss percentage (`Loss%`) for each hop. However, many routers are configured to rate-limit or even drop ICMP traffic.\nThis can lead to false positives for packet loss, particularly for intermediate hops, as the lack of a response from\nsuch hops does not typically indicate genuine packet loss. This is a common source of confusion for users interpreting\ntrace results.\n\nTrippy already provides a color-coded status column (`Sts`), that considers both packet loss percentage and whether the\nhop is the target of the trace, to try and assist users in interpreting the status of each hop. While this feature is\nhelpful, it does not make it clear _why_ a hop has a particular status nor help users interpret the overall status of\nthe trace.\n\nTo further assist users, this release of Trippy introduces a pair of novel heuristics to measure _forward loss_ and\n_backward loss_. Informally, _forward loss_ indicates whether the loss of a probe is the _cause_ of subsequent losses\nand _backward loss_ indicates whether the loss of a probe is the _result_ of a prior loss on the path.\n\nMore precisely:\n\n- _forward loss_ for probe `P` in round `R` occurs when probe `P` is lost in round `R` and _all_ subsequent probes\n  within round `R` are also lost.\n- _backward loss_ for probe `P` in round `R` occurs when probe `P` is lost in round `R` and _any_ prior probe within\n  round `R` has _forward loss_.\n\nThese heuristics are encoded in three new columns:\n\n- `Floss` (`F`): The number of probes with _forward loss_\n- `Bloss` (`B`): The number of probes with _backward loss_\n- `Floss%` (`D`): The percentage of probes with _forward loss_\n\nThese columns are hidden by default but can be enabled as needed. For more details, see\nthe [Column Reference](https://github.com/fujiapple852/trippy#column-reference).\n\nThe following screenshot shows an example trace with the new columns enabled:\n\n<img width=\"60%\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.12.0/floss_bloss.png\"/>\n\nIn the following (contrived) example, after initially discovering the target (`10.0.0.105`) during the first round,\ngenuine packet loss occurs in _all_ subsequent rounds at the third hop. This means that no probes on the common path are\nable to get beyond the third hop.\n\n```\n╭Hops───────────────────────────────────────────────────────────────╮\n│#    Host         Loss%    Snd     Recv    Floss   Bloss   Floss%  │\n│1    10.0.0.101   0.0%     96      96      0       0       0.0%    │\n│2    10.0.0.102   0.0%     96      96      0       0       0.0%    │\n│3    No response  100.0%   96      0       95      0       98.9%   │\n│4    No response  100.0%   96      0       0       95      0.0%    │\n│5    10.0.0.105   99.0%    96      1       0       95      0.0%    │\n```\n\nFrom this we can determine that the loss at the third hop is classified as _forward loss_ because all subsequent\nprobes (4th and 5th) in the same round are also lost. We can also conclude that the 4th and 5th hops have _backward\nloss_ starting from round two, as in those rounds a prior hop (the third hop) has _forward loss_.\n\nNote the difference between the traditional `Loss%` column and the new `Floss%` column. The `Loss%` column indicates\npacket loss at several hops (3rd, 4th, and 5th). In contrast, the `Floss%` column helps us determine that the true\npacket loss most likely occurs at the 3rd hop.\n\nIt is important to stress that this technique is a _heuristic_, and both _false positives_ and _false negatives_ are\npossible. Some specific caveats to be aware of include:\n\n- Every probe sent in every round is an _independent trial_, meaning there is no guarantee that all probes within a\n  given round will follow the same path (or \"flow\"). The concept of \"forward loss\" and \"backward loss\" assumes that all\n  probes followed a single path. This assumption is typically met (but not guaranteed) when using tracing strategies\n  such as ICMP, UDP/Dublin, or UDP/Paris.\n- Any given host on the path may drop packets for only a subset of probes sent within a round, either due to rate\n  limiting or genuine intermittent packet loss. This could result in a false positive for \"forward loss\" at a given hop\n  if all subsequent hops in the round exhibit packet loss that is not genuine. For example, in the scenario above, the\n  hop with `ttl=3` could be incorrectly deemed to have \"forward loss\" if observed loss from hops `ttl=4` and `ttl=5` is\n  not genuine (e.g., caused by rate-limiting).\n- A false positive for \"backward loss\" could occur at a hop experiencing genuine packet loss if a previous hop on the\n  path has \"forward loss\" that is not genuine. In the scenario above, if the hop with `ttl=4` has genuine packet loss,\n  it will still be marked with \"backward loss\" due to the \"forward loss\" at `ttl=3`.\n\nDespite these caveats, the addition of _forward loss_ and _backward loss_ heuristics aims to help users more accurately\ninterpret trace outputs. However, these heuristics should be considered experimental and may be subject to change in\nfuture releases.\n\nSee [#860](https://github.com/fujiapple852/trippy/issues/860) for details.\n\n### Bug Fixes\n\nThe previous release of Trippy introduced a bug ([#1290](https://github.com/fujiapple852/trippy/issues/1290)) that\ncaused reverse DNS lookups to be enqueued multiple times when the `dns-ttl` expired, potentially leading to the hostname\nbeing displayed as `Timeout: xxx` for a brief period.\n\nA long-standing bug ([#1398](https://github.com/fujiapple852/trippy/issues/1398)) which caused the TUI sample history\nand frequency charts to ignore sub-millisecond samples has been fixed.\n\nThis release fixes a bug ([#1287](https://github.com/fujiapple852/trippy/issues/1287)) that caused the tracer to panic\nwhen parsing certain ICMP extensions with malformed lengths.\n\nIt also resolves an issue ([#1289](https://github.com/fujiapple852/trippy/issues/1289)) where the ICMP extensions mode\nwas not being displayed in the TUI settings dialog.\n\nA bug ([#1375](https://github.com/fujiapple852/trippy/issues/1375)) that caused the cursor to not move to the bottom of\nthe screen when exiting while preserving the screen has also been fixed.\n\nFinally, this release fixes a bug ([#1327](https://github.com/fujiapple852/trippy/issues/1327)) that caused Trippy to\nincorrectly reject the value `ip` for the `tui-address-mode` configuration option (thanks to @c-git).\n\n### New Distribution Packages\n\nTrippy is now available in Debian 13 (`trixie`) and later (with thanks to @nc7s!).\n\n[![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy)\n\n```shell\napt install trippy\n```\n\nSee ([#1312](https://github.com/fujiapple852/trippy/issues/1312)) for details.\n\nThe official Trippy PPA for Ubuntu is now also available for the `noble` distribution.\n\n[![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.12.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages)\n\n```shell\nsudo add-apt-repository ppa:fujiapple/trippy\nsudo apt update && apt install trippy\n```\n\nSee ([#1308](https://github.com/fujiapple852/trippy/issues/1308)) for details.\n\nYou can find the full list of [distributions](https://github.com/fujiapple852/trippy/tree/master#distributions) in the\ndocumentation.\n\n### Thanks\n\nMy thanks to all Trippy contributors, package maintainers, translators and community members.\n\nFeel free to drop by the Trippy Zulip room for a chat:\n\n[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://trippy.zulipchat.com/)\n\nHappy Tracing!\n\n# 0.11.0\n\n## Highlights\n\nThis release of Trippy adds NAT detection for IPv4/UDP/Dublin tracing, a new public API, reverse DNS lookup cache\ntime-to-live, transient error handling for IPv4, a new ROFF manual page generator, several new columns, improved error\nmessages and a revamped help dialog with settings tab hotkeys.\n\nThere are two breaking changes, a new initial sequence number is used which impacts the default behavior of UDP tracing\nand two configuration fields have been renamed and moved.\n\nFinally, there are a handful of bug fixes and two new distribution packages, Chocolatey for Windows and an official PPA\nfor Ubuntu and Debian based distributions.\n\n### NAT Detection for IPv4/UDP/Dublin\n\nWhen tracing with the Dublin tracing strategy for IPv4/UDP, Trippy can now detect the presence of NAT (Network Address\nTranslation) devices on the path.\n\n[RFC 3022 section 4.3](https://datatracker.ietf.org/doc/html/rfc3022#section-4.3) requires that \"NAT to be completely\ntransparent to the host\" however in practice some fully compliant NAT devices leave behind a telltale sign that Trippy\ncan use.\n\nTrippy will indicate if a NAT device has been detected by adding `[NAT]` at the end of the hostname. There is also a\nnew (hidden by default) column, `Nat`, which can be enabled to show the NAT status per hop.\n\n<img width=\"60%\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.11.0/nat_detection.png\"/>\n\nNAT devices are detected by observing a difference in the _expected_ and _actual_ checksum of the UDP packet that is\nreturned as the part of the Original Datagram in the ICMP Time Exceeded message. If they differ then it indicates that a\nNAT device has modified the packet. This happens because the NAT device must recalculate the UDP checksum after\nmodifying the packet (i.e. translating the source port) and so the checksum in the UDP packet that is nested in the ICMP\nerror may not, depending on the device, match the original checksum.\n\nTo help illustrate the technique, consider sending the following IPv4/UDP packet (note the UDP `Checksum B` here):\n\n```\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │             \n|Version|  IHL  |Type of Service|          Total Length         | │             \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │             \n|         Identification        |Flags|     Fragment Offset     | │             \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │             \n|  Time to Live |    Protocol   |            Checksum A         | │ IPv4 Header \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │             \n|                         Source Address                        | │             \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │             \n|                      Destination Address                      | │             \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │             \n                                                                                \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │             \n|          Source Port          |        Destination Port       | │             \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ UDP Header  \n|             Length            |            Checksum B         | │             \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │\n```\n\nTrippy expect to receive an IPv4/ICMP `TimeExceeded` (or other) error which contains the Original Datagram (OD) IPv4/UDP\npacket that was sent above with `Checksum B'` in the Original Datagram (OD) IPv4/UDP packet:\n\n```\n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │                                    \n|Version|  IHL  |Type of Service|          Total Length         | │                                    \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │                                    \n|         Identification        |Flags|     Fragment Offset     | │                                    \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │                                    \n|  Time to Live |    Protocol   |            Checksum C         | │ IPv4 Header                        \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │                                    \n|                         Source Address                        | │                                    \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │                                    \n|                      Destination Address                      | │                                    \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │                                    \n                                                                                                       \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │                                    \n|      Type     |      Code     |            Checksum D         | │                                    \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ IPv4 Payload (ICMP TE Header)      \n|                             Unused                            | │                                    \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │                                    \n                                                                  │                                    \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │                                  \n|Version|  IHL  |Type of Service|          Total Length         | │ │                                  \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │                                  \n|         Identification        |Flags|     Fragment Offset     | │ │                                  \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │                                  \n|  Time to Live |    Protocol   |            Checksum A'        | │ │ ICMP TE Payload (OD IPv4 Header) \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │                                  \n|                         Source Address                        | │ │                                  \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │                                  \n|                      Destination Address                      | │ │                                  \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │                                  \n                                                                  │ │                                  \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ │                                \n|          Source Port          |        Destination Port       | │ │ │                                \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ │ OD IPv4 Payload (UDP header)   \n|             Length            |            Checksum B'        | │ │ │                                \n+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ │ │ │\n```\n\nIf `Checksum B'` in the UDP packet nested in the ICMP error does not match `Checksum B` in the UDP packet that was sent\nthen Trippy can infer that a NAT device is present.\n\nThis technique allows for the detection of NAT at the first hop. To detect multiple NAT devices along the path, Trippy\nmust also check for _changes_ in the observed checksum between consecutive hops, as changes to the UDP checksum will \"\ncarry forward\" to subsequent hops. This requires taking care to account for hops that do not respond. This is only\npossible when using the Dublin tracing strategy, as it does not modify the UDP header per probe; therefore, the\nchecksums are expected to remain constant, allowing changes in the checksum between hops to be detected.\n\nNote that this method cannot detect all types of NAT devices and so should be used in conjunction with other methods\nwhere possible.\n\nSee the [issue](https://github.com/fujiapple852/trippy/issues/1104) for more details.\n\n### Public API\n\nTrippy has been designed primarily as a standalone _tool_, however it is built on top of a number of useful libraries,\nsuch as the core tracer, DNS resolver and more. These libraries have always existed but were tightly integrated into the\ntool and were not designed for use by third party crates.\n\nThis release introduces the Trippy public API which can be used to build custom tools on top of the Trippy libraries.\n\nThe full set of libraries exposed is:\n\n| Crate                                                | Description                                          |\n| ---------------------------------------------------- | ---------------------------------------------------- |\n| [trippy](https://docs.rs/trippy)                     | Common entrypoint crate                              |\n| [trippy-core](https://docs.rs/trippy-core)           | The core Trippy tracing functionality                |\n| [trippy-packet](https://docs.rs/trippy-packet)       | Packet wire formats and packet parsing functionality |\n| [trippy-dns](https://docs.rs/trippy-dns)             | Perform forward and reverse lazy DNS resolution      |\n| [trippy-privilege](https://docs.rs/trippy-privilege) | Discover platform privileges                         |\n| [trippy-tui](https://docs.rs/trippy-tui)             | The Trippy terminal user interface                   |\n\nTo use the Trippy public API you should add the common entrypoint `trippy` crate to your `Cargo.toml` file and then\nenable the desired features. Note that the `trippy` crate includes `tui` as a default feature and so you should disable\ndefault features when using it as a library. Alternatively, it is also possible to add the crates individually.\n\nFor example, to use the core Trippy tracing functionality you would add the `trippy` crate, disable default features and\nenable the `core` feature:\n\n```toml\n[dependencies]\ntrippy = { version = \"0.11.0\", default-features = false, features = [\"core\"] }\n```\n\nThe `hello-world` example below demonstrates how to use the Trippy public API to perform a simple trace and print the\nresults of each round:\n\n```rust\nuse std::str::FromStr;\nuse trippy::core::Builder;\n\nfn main() -> anyhow::Result<()> {\n    let addr = std::net::IpAddr::from_str(\"1.1.1.1\")?;\n    Builder::new(addr)\n        .build()?\n        .run_with(|round| println!(\"{:?}\", round))?;\n    Ok(())\n}\n```\n\nWhilst Trippy adheres to [Semantic Versioning](https://semver.org/), the public API is not yet considered stable and may\nchange in future releases.\n\nSee [crates](crates/README.md) and the usage [examples](examples/README.md) for more information.\n\n### New Initial Sequence\n\nFor UDP tracing, by default, Trippy uses a fixed source port and a variable destination port which is set from the\nsequence number, starting from an initial sequence of 33000 and incremented for each probe, eventually wrapping around.\n\nBy [convention](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers), many devices on the internet allow UDP\nprobes to ports in the range 33434..=33534 and will return a `DestinationUnreachable` ICMP error, which can be used to\nconfirm that the target has been reached. Since Trippy does not use destination ports in this range for UDP probes by\ndefault, the target host will typically not respond with an ICMP error, and so Trippy cannot know that the target was\nreached, and must therefore show the hop as unknown.\n\nAnother issue with this default setup is that the sequence number will eventually enter the range 33434..=33534 at which\npoint the target will _begin_ to respond with the `DestinationUnreachable` ICMP error. However, there is no guarantee\nthat the probe sent for sequence 33434 (i.e., the first one for which the target host will be able to respond) will be\nfor the minimum time-to-live (ttl) required to reach the target. This leads to confusing output, which is hard for users\nto interpret. See [issue](https://github.com/fujiapple852/trippy/issues/1203) for more details.\n\nThese issues can be avoided today, either by changing the initial sequence number to be in the range 33434..=33534 by\nsetting the `--initial-sequence` flag or by using a fixed destination port (and therefore a variable source port) in the\nsame range by setting the `--target-port` flag.\n\nIn the following example, the initial sequence number is set to 33434:\n\n```shell\ntrip example.com --udp --initial-sequence 33434\n```\n\nThis can be made permanent by setting the `initial-sequence` value in the `strategy` section of the configuration file:\n\n```toml\n[strategy]\ninitial-sequence = 33434\n```\n\nIn the following example, the destination port is set to 33434:\n\n```shell\ntrip example.com --udp --target-port 33434\n```\n\nThis can be made permanent by setting the `target-port` value in the `strategy` section of the configuration file:\n\n```toml\n[strategy]\ntarget-port = 33434\n```\n\nAs the default behavior in Trippy leads to these confusing issues, this release modifies the default sequence number to\nbe 33434. This is a **breaking change** and will impact users who rely on the old default initial sequence number.\n\nThis change introduces a new problem, albeit a lesser one: UDP traces will now begin with a destination port of 33434\nand so `DestinationUnreachable` ICMP errors will typically be returned by the target immediately. However, eventually\nthe sequence number will move _beyond_ the range 33434..=33534 and so the target host will _stop_ responding\nwith `DestinationUnreachable` ICMP errors. This leads to the appearance that the target has started dropping packets.\nWhile this is technically correct, this is not desirable behavior as the target has not really disappeared.\n\nIt is therefore recommended to _always_ fix the `target-port` to be in the range 33434..=33534 for UDP tracing and allow\nthe source port to vary instead. This may become the default behavior for UDP tracing in a future release; that would\nrepresent a significant difference in default behavior compared to most traditional Unix traceroute tools, which vary\nthe destination port by default.\n\n### Reverse DNS Lookup Cache Time-to-live\n\nTrippy performs a reverse DNS lookup for each host encountered during the trace and the resulting hostnames are cached\nindefinitely. This can lead to stale hostnames being displayed in the TUI if they change after the trace has begun.\n\nNote that the DNS cache can be flushed manually by pressing `ctrl+k` (default key binding) in the TUI.\n\nStarting from this release, the reverse DNS cache can be configured to expire after a certain time to live. By default\nthis is set to be 5 minutes (300 seconds) and can be configured using the `--dns-ttl` flag or the `dns-ttl`\nconfiguration option.\n\nThe following example sets the DNS cache time-to-live to 30 seconds:\n\n```shell\ntrip example.com --dns-ttl 30s\n```\n\nThis can be made permanent by setting the `dns-ttl` value in the `dns` section of the configuration file:\n\n```toml\n[dns]\ndns-ttl = \"30s\"\n```\n\n### Transient Error Handling for IPv4\n\nTrippy records the number of probes sent and the number of probes received for each hop and uses this information to\ncalculate packet loss. Any probe that is _successfully_ sent for which no response is received is considered lost.\n\nCurrently, if a probe cannot be sent for any reason, then Trippy will crash and show a BSOD. This is not typically an\nissue, as such failures imply a local issue with the host network configuration rather than an issue with the target or\nany intermediate hops.\n\nHowever, it is possible that a probe may fail to send for a transient reason, such as a temporary local host issue, and\nso it would be useful to be able to handle such errors gracefully. A common example would be running Trippy on a host\nand during the trace disabling the network interface.\n\nStarting from this release, Trippy will continue the trace even if a probe fails to send and will instead show a warning\nto the user in the TUI about the number of probe failures. A new column (hidden by default), `Fail`, has also been added\nto the TUI to show the number of probes that failed to send for each hop.\n\n<img width=\"60%\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.11.0/fails.png\"/>\n\nThis has been implemented for macOS, Linux and Windows for IPv4 only. Support for IPv6 and other platforms will be added\nin future releases.\n\nSee the [tracking issue](https://github.com/fujiapple852/trippy/issues/1238) for more details.\n\n### Generate ROFF Man Page\n\nTrippy can now generate manual pages in ROFF format. This can be useful for users who wish to install Trippy on systems\nwhich do not have a package manager or for users who wish to install Trippy from source. It can also be used by package\nmaintainers to generate manual pages for their distribution.\n\nThe following command generates a ROFF manual page for Trippy:\n\n```shell\ntrip --generate-man > /path/to/man/pages/trip.1\n```\n\n### New Columns\n\nThis release introduced several new columns, all of which are hidden by default. These are:\n\n- `Type`: The ICMP packet type for the last probe for the hop\n- `Code`: The ICMP packet code for the last probe for the hop\n- `Nat`: The NAT detection status for the hop\n- `Fail`: The number of probes which failed to send for the hop\n\nThe following shows the `Type` and `Code` columns:\n\n<img width=\"60%\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.11.0/type_code_columns.png\"/>\n\nSee the [Column Reference](https://github.com/fujiapple852/trippy#column-reference) for a full list of all available\ncolumns.\n\n### Settings Dialog Tab Hotkeys\n\nThe settings dialog can be accessed by pressing `s` (default key binding) and users can navigate between the tabs using\nthe left and right arrow keys (default key bindings). This release introduces hotkeys to allow users to jump directly to\na specific tab by pressing `1`-`7` (default key bindings).\n\nSee the [Key Bindings Reference](https://github.com/fujiapple852/trippy#key-bindings-reference) for details.\n\n### Help Dialog Revamped\n\nThe existing Trippy help dialog shows a hardcoded list of key bindings which may not reflect the actual key bindings the\nuser has configured. Trippy shows the correct key bindings in the settings dialog which can be accessed by\npressing `s` (default key binding) and navigating to the Bindings tab. Therefore, the key bindings in the help dialog\nare both potentially incorrect and redundant.\n\nThis release revamps the help dialog and includes instructions on how to access the key bindings from the settings\ndialog as well as some other useful information.\n\n<img width=\"60%\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.11.0/new_help_dialog.png\"/>\n\n### Improved Error Messages\n\nError reporting has been improved for parameters such as `--min-round-duration` (`-i`). Previously, if an invalid\nduration was provided, the following error would be reported:\n\n```shell\n$ trip example.com -i 0.05\nError: invalid character at 1\n```\n\nStarting from this release, such error will instead be shown as:\n\n```shell\n$ trip example.com -i 0.05\nerror: invalid value '0.05' for '--min-round-duration <MIN_ROUND_DURATION>': expected time unit (i.e. 100ms, 2s, 1000us)\n\nFor more information, try '--help'.\n```\n\nThis covers all \"duration\" parameters, namely:\n\n- `min_round_duration`\n- `max_round_duration`\n- `grace_duration`\n- `read_timeout`\n- `dns_timeout`\n- `tui_refresh_rate`\n\n### Renamed Configuration\n\nThe following configuration fields have been renamed and moved from the `[tui]` to the `[strategy]` section in the\nconfiguration file:\n\n- `tui-max-samples` -> `max-samples`\n- `tui-max-flows` -> `max-flows`\n\nThis is a **breaking change**. Attempting to use the legacy field names will result in an error pointing to the new\nname.\n\nThe following example shows the error reported if the old names are used from the command line:\n\n```shell\nerror: unexpected argument '--tui-max-samples' found\n\n  tip: a similar argument exists: '--max-samples'\n```\n\nThe following examples shows the error reported if the ld names are used from the configuration file:\n\n```shell\nError: tui-max-samples in [tui] section is deprecated, use max-samples in [strategy] section instead\n```\n\n### Bug Fixes\n\nThis release fixes a bug where `DestinationUnreachable` ICMP errors were assumed to have been sent by the target host,\nwhereas they may also be sent by an intermediate hop.\n\nAnother fix addresses an issue where the TUI would calculate the maximum number of hops to display based on the maximum\nobserved across all rounds rather than for the latest round.\n\nFinally, a minor bug was fixed where `AddressInUse` and `AddrNotAvailable` errors were being conflated.\n\n### New Distribution Packages\n\nTrippy has been added to the Chocolatey community repository (with thanks to @Aurocosh!):\n\n[![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy)\n\n```shell\nchoco install trippy\n```\n\nTrippy also has an official PPA for Ubuntu and Debian based distributions (with thanks to @zarkdav!):\n\n[![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.11.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages)\n\n```shell\nsudo add-apt-repository ppa:fujiapple/trippy\nsudo apt update && apt install trippy\n```\n\nYou can find the full list of [distributions](https://github.com/fujiapple852/trippy/tree/master#distributions) in the\ndocumentation.\n\n### Thanks\n\nMy thanks to all Trippy contributors, package maintainers and community members.\n\nFeel free to drop by the new Trippy Zulip room for a chat:\n\n[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://trippy.zulipchat.com/)\n\nHappy Tracing!\n\n# 0.10.0\n\n## Highlights\n\nThe first release of 2024 is packed with new features, such as customizable columns, jitter calculations, Dublin tracing\nstrategy for IPv6/UDP, support for IPinfo GeoIp files, enhanced DNS resolution with IPv6/IPv4 fallback and CSS named\ncolors for the TUI as well as a number of bug fixes. Since the last release there has also been a significant\nimprovement in automated testing, notably the introduction of TUN based simulation testing for IPv4.\n\n### Customize Columns\n\n#### Customize Columns in TUI\n\nIt is now possible to customize which columns are shown in the TUI and to adjust the order in which they are displayed.\nThis customization can be made from within the TUI or via configuration.\n\nTo customize the columns from the TUI you must open the settings dialog (`s` key) and navigating to the new `Columns`\ntab (left and right arrow keys). From this tab you can select the desired column (up and down arrow keys) and toggle the\ncolumn visibility on and off (`c` key) or move it left (`,` key) or right (`.` key) in the list of columns.\n\n<img width=\"60%\" alt=\"columns\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.10.0/columns_settings.png\">\n\nYou can supply the full list of columns, in the desired order, using the new `--tui-custom-columns` command line\nargument. The following example specifies the standard list of columns in the default order:\n\n```shell\ntrip example.com --tui-custom-columns holsravbwdt\n```\n\nAlternatively, to make the changes permanent you may add the `tui-custom-columns` entry to the `tui` section of the\nTrippy configuration file:\n\n```toml\n[tui]\ntui-custom-columns = \"holsravbwdt\"\n```\n\nNote that the value of `tui-custom-columns` can be seen in the corresponding field of the `Tui` tab of the settings\ndialog and will reflect any changes made to the column order and visibility via the Tui. This can be useful as you may\ncopy this value and use it in the configuration file directly.\n\n<img width=\"60%\" alt=\"tui-custom-columns\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.10.0/tui_settings.png\">\n\n#### New Columns\n\nThis release also introduced several new columns, all of which are hidden by default. These are:\n\n- Last source port: The source port for last probe for the hop\n- Last destination port: The destination port for last probe for the hop\n- Last sequence number: The sequence number for the last probe for the hop\n- Jitter columns: see the \"Calculate and Display Jitter\" section below\n\nSee the [Column Reference](https://github.com/fujiapple852/trippy#column-reference) for a full list of all available\ncolumns.\n\n#### Column Layout Improvement\n\nThe column layout algorithm used in the hop table has been improved to allow the maximum possible space for the `Host`\ncolumn. The width of the `Host` column is now calculated dynamically based on the terminal width and the set of columns\ncurrently configured.\n\n### Calculate and Display Jitter\n\nTrippy can now calculate and display a variety of measurements related to _jitter_ for each hop. Jitter is a measurement\nof the difference in round trip time between consecutive probes. Specifically, the following new calculated values are\navailable in Trippy `0.10.0`:\n\n- Jitter: The round-trip-time (RTT) difference between consecutive rounds for the hop\n- Average Jitter: The average jitter of all probes for the hop\n- Maximum Jitter: The maximum jitter of all probes for the hop\n- Inter-arrival Jitter: The smoothed jitter value of all probes for the hop\n\nThese values are always calculated and are included in the `json` report. These may also be displayed as columns in the\nTUI, however they are not shown by default. To enabled these columns in the TUI, please see\nthe [Column Reference](https://github.com/fujiapple852/trippy#column-reference).\n\n<img width=\"60%\" alt=\"jitter\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.10.0/jitter_columns.png\">\n\n### Dublin Tracing Strategy for IPv6/UDP\n\nThe addition of support for the [dublin](https://github.com/insomniacslk/dublin-traceroute) tracing strategy for\nIPv6/UDP marks the completion of a multi-release journey to provide support for both Dublin\nand [paris](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum) tracing strategies for both IPv4/UDP\nand IPv6/UDP.\n\nAs a reminder, unlike classic traceroute and MTR, these alternative tracing strategies do not encode the probe sequence\nnumber in either the src or dest port of the UDP packet, but instead use other protocol and address family specific\ntechniques. Specifically, the Dublin tracing strategy for IPv6/UDP varies the length of the UDP payload for this\npurpose.\n\nBy doing so, these strategies are able to keep the src and dest ports fixed which makes it much more likely (though not\nguaranteed) that each round of tracing will follow the same path through the network (note that this is not true for the\nreturn path).\n\nThe following command runs an IPv6/UDP trace using the Dublin tracing strategy with fixed src and dest ports:\n\n```shell\ntrip example.com --udp -6 -R dublin -S 5000 -P 3500\n```\n\nNote that, for both Paris and Dublin tracing strategies, if you fix either the src or dest ports (but _not_ both) then\nTrippy will vary the unfixed port _per round_ rather than _per hop_. This has the effect that all probes _within_ a\nround will likely follow the same network path but probes _between_ round will follow different paths. This can be\nuseful in conjunction with flows (`f` key) to visualize the various paths packet flow through the network. See\nthis [issue](https://github.com/fujiapple852/trippy/issues/1007) for more details.\n\n<img width=\"60%\" alt=\"ipv6_dublin\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.10.0/dublin_ipv6_src_dest_seq_columns.png\">\n\nWith UDP support for the Paris and Dublin tracing strategies now complete, what remains is adding support for these for\nthe TCP protocol. Refer to the [ECMP tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details.\n\n### IPinfo GeoIp Provider\n\nTrippy currently supports the ability to lookup and display GeoIp information from MMDB files, but prior to `0.10.0`\nonly the [MaxMind](https://www.maxmind.com) \"GeoLite2 City\" (and lite) MMDB files were supported. This release\nintroduces support for the \"IP to Country + ASN Database\" and \"IP to Geolocation Extended Database\" MMDB files\nfrom [IPinfo](https://ipinfo.io).\n\nThe \"IP to Country + ASN Database\" MMDB file provided by IPinfo can be used as follows:\n\n```shell\ntrip example.com --geoip-mmdb-file /path/to/country_asn.mmdb --tui-geoip-mode short\n```\n\nThese settings can be made permanent by setting the following values in the `tui` section of the configuration file:\n\n```toml\n[tui]\ngeoip-mmdb-file = \"/path/to/country_asn.mmdb\"\ntui-geoip-mode = \"short\"\n```\n\n### Enhanced DNS Resolution with IPv4/IPv6 Fallback\n\nWhen provided with a DNS name such as `example.com` Trippy tries to resolve it to an IPv4 or an IPv6 address and fails\nif no such IP exists for the configured `addr-family` mode, which must be either IPv4 or IPv6.\n\nStarting from version `0.10.0`, Trippy can be configured to support `ipv4-then-ipv6` and `ipv6-then-ipv4` modes\nfor `addr-family`. In the new `ipv4-then-ipv6` mode Trippy will first attempt to resolve the given hostname to an IPv4\naddress and, if no such address exists, it will attempt to resolve to an IPv6 address and only fail if neither are\navailable (and the opposite for the new `ipv6-then-ipv4` mode). The `addr-family` mode may also be set to be `ipv4`\nor `ipv6` for IPv4 only and IPv6 only respectively.\n\nTo set the `addr-family` to be IPv6 with fallback to IPv4 you can set the `--addr-family` command line parameter:\n\n```shell\ntrip example.com --addr-family ipv6-then-ipv4\n```\n\nTo make the change permanent you can set the `addr-family` value in the `strategy` section of the configuration file:\n\n```toml\n[strategy]\naddr-family = \"ipv6-then-ipv4\"\n```\n\nNote that Trippy supports both the `addr-family` entry in the configuration file and also the `--ipv4` (`-4`)\nand `--ipv6` (`-6`) command line flags, all of which are optional. The command line flags (which are mutually exclusive)\ntake precedence over the config file entry and if neither are provided there it defaults to `ipv4-then-ipv6`.\n\n### Extended Colors in TUI\n\nTrippy allows the theme to be customized and supports the\nnamed [ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors):\n\nBlack, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, DarkGray, LightRed, LightGreen, LightYellow, LightBlue,\nLightMagenta, LightCyan, White\n\nThe `0.10.0` release adds support for CSS [named colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color) (\ne.g. SkyBlue). Note that these are only supported on some platforms and terminals and may not render correctly\nelsewhere.\n\nSee the [Theme Reference](https://github.com/fujiapple852/trippy#theme-reference)\n\n### Simulation Testing\n\nManually testing all Trippy features in all modes and on all supported platforms is an increasingly time consuming and\nerror prone activity. Since the last release a significant effort has been made to increase the testing coverage,\nincluding unit and integration testing.\n\nIn particular, the introduction of simulation testing allows for full end-to-end testing of all modes and features on\nLinux, macOS and Windows without the need to mock or stub any behaviour _within_ Trippy.\n\nThis is achieved by creating a [TUN](https://en.wikipedia.org/wiki/TUN/TAP) device to simulate the behavior of network\nnodes, responding to various pre-configured scenarios like packet loss and out-of-order arrivals.\n\nWhilst not a change that directly benefits end users, this new testing approach should reduce the effort needed to test\neach release of Trippy and help improve the overall reliability of the tool.\n\nNote that the simulation testing is currently only supported for IPv4. See\nthe [Integration Testing](https://github.com/fujiapple852/trippy/issues/759) tracking issue for more details.\n\n### Thanks\n\nMy thanks to all Trippy contributors, package maintainers and community members.\n\nFeel free to drop by the Trippy Matrix room for a chat:\n\n[![#trippy-dev:matrix.org](https://img.shields.io/badge/matrix/trippy-dev:matrix.org-blue)](https://matrix.to/#/#trippy-dev:matrix.org)\n\nHappy Tracing!\n\n# 0.9.0\n\n## Highlights\n\nTrippy `0.9.0` introduces many new features, including tracing flows and ICMP extensions, the expansion of support for\nthe Paris tracing strategy to encompass IPv6/UDP, an unprivileged execution mode for macOS, a hop privacy mode and many\nmore. Additionally, this release includes several important bug fixes along with a range of new distribution packages.\n\n### Tracing Flows\n\n#### Flow ID\n\nA tracing flow represents the sequence of hosts traversed from the source to the target. Trippy is now able to identify\nindividual flows within a trace and assign each a unique flow id. Trippy calculate a flow id for each round of tracing,\nbased on the sequence of hosts which responded during that round, taking care to account for rounds in which only a\nsubset of hosts responded. Tracing statistics, such as packet loss % and average RTT are recorded on a per-flow basis as\nwell as being aggregated across all flow.\n\nTracing flows adds to the existing capabilities provided by Trippy to assist\nwith [ECMP](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) (Equal-Cost Multi-Path Routing) when tracing\nwith UDP and TCP protocols. Some of these capabilities, such as\nthe [paris](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum)\nand [dublin](https://github.com/insomniacslk/dublin-traceroute) tracing strategies, are designed to _restrict_ tracing\nto a single flow, whilst others, such as the hop detail navigation mode (introduce in the last release) and tracing\nflows, are designed to help _visualize_ tracing data in the presence of multiple flows. See\nthe `0.8.0` [release note](https://github.com/fujiapple852/trippy/releases/tag/0.8.0) for other such capabilities.\n\n#### Tracing Flows in the TUI\n\nThe TUI has been enhanced with a new mode to help visualise flows. This can be toggled on and off with\nthe `toggle-flows` command (bound to the `f` key by default).\n\nWhen toggled on, this mode display flow information as a chart in a new panel above the hops table. Flows can be\nselected by using the left and right arrow keys (default key bindings). Flows are sorted by the number of rounds in\nwhich a given flow id was observed, with the most frequent flow ids shown on the left. When entering this mode flow id 1\nis selected automatically. The selected flow acts as a filter for the other parts of the TUI, including the hops table,\nchart and maps views which only show data relevant to that specific flow.\n\n<img width=\"60%\" alt=\"flows\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.9.0/flows.png\">\n\nWhen toggled off, Trippy behaves as it did in previous versions where aggregated statistics (across all flows) are\nshown. Note that per-flow data is always recorded, the toggle only influences how the data is displayed.\n\nThe number of flows visible in the TUI is limited and can be controlled by the `tui-max-flows` configuration items which\ncan be set via the command line or via the configuration file. By default up to 64 flows are shown.\n\nThe flows panel, as with all other parts of the TUI, can also be themed, see\nthe [theme reference](https://github.com/fujiapple852/trippy#theme-reference) for details.\n\n#### Flow Reports\n\nAs well as visualising flows in the TUI, Trippy `0.9.0` introduces two new reports which make use of the tracing flow\ndata.\n\nThe new `flows` report mode records and print all flows observed during tracing.\n\nThe following command will run a TCP trace for 10 round and report all of the flows observed:\n\n```shell\ntrip example.com --tcp -m flows -C 10\n```\n\nSample output (truncated) showing three unique flows:\n\n```text\nflow 1: 192.168.1.1, 10.193.232.245, 218.102.40.38, 10.195.41.9, 172.217.27.14\nflow 2: 192.168.1.1, 10.193.232.245, 218.102.40.22, 10.195.41.17, 172.217.27.14\nflow 3: 192.168.1.1, 10.193.232.245, 218.102.40.38, 10.195.41.1, 172.217.27.14\n```\n\nAnother new report, `dot`, outputs a [GraphViz](https://graphviz.org/) [`DOT`](https://graphviz.org/doc/info/lang.html)\nformat chart of all hosts observed during tracing.\n\nThe following command will run a TCP trace for 10 round and output a graph of flows in `DOT` format:\n\n```shell\ntrip example.com --tcp -m dot -C 10\n```\n\nIf you have a tool such as `dot` (Graphviz) installed you can use this to rendered the output in various formats, such\nas PNG:\n\n```shell\ntrip example.com --tcp -m dot -C 10 | dot -Tpng > path.png\n```\n\nSample output:\n\n<img width=\"60%\" alt=\"dot\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.9.0/dot.png\">\n\n### ICMP Extensions\n\n#### Parsing Extensions\n\nTrippy `0.9.0` adds the ability to parse and display ICMP Multi-Part Messages (aka extensions). It supports both\ncompliant and non-compliant ICMP extensions as defined\nin [section 5 of rfc4884](https://www.rfc-editor.org/rfc/rfc4884#section-5).\n\nTrippy is able to parse and render any generic Extension Object but is also able to parse some well known Object\nClasses, notably the MPLS class.\n\nSupport\nfor [additional classes](https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xml#icmp-parameters-ext-classes)\nwill be added to future versions of Trippy, see the ICMP\nExtensions [tracking issue](https://github.com/fujiapple852/trippy/issues/33).\n\nParsing of ICMP extensions can be enabled by setting the `--icmp-extensions` (`-e`) command line flag or by adding\nthe `icmp-extensions` entry in the `strategy` section of the configuration file:\n\n```toml\n[strategy]\nicmp-extensions = true\n```\n\n#### ICMP Extensions in the TUI\n\nThe TUI has been enhanced to display ICMP extensions in both the normal and hop detail navigation modes.\n\nIn normal mode, ICMP extensions are not shown by default but can be enabled by setting the `--tui-icmp-extension-mode`\ncommand line flag or by adding the `tui-icmp-extension-mode` entry in the `tui` section of the configuration file:\n\n```toml\n[tui]\ntui-icmp-extension-mode = \"full\"\n```\n\nThis can be set to `off` (do not show ICMP extension data), `mpls` (shows a list of MPLS label(s) per hop), `full` (\nshows all details of all extensions, such as `ttl`, `exp` and `bos` for MPLS) or `all` (the same as `full` but also\nshows `class`, `subtype` and `bytes` for unknown extension objects).\n\nThe following screenshot shows ICMP extensions in normal mode with `tui-icmp-extension-mode` set to be `mpls`:\n\n<img width=\"60%\" alt=\"extensions\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.9.0/extensions.png\">\n\nIn hop detail mode, the full details of all ICMP extension objects are always shown if parsing of ICMP extensions is\nenabled.\n\nThe following screenshot shows ICMP extensions in hop detail mode:\n\n<img width=\"60%\" alt=\"extensions_detail\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.9.0/extensions_detail.png\">\n\n#### ICMP Extensions in Reports\n\nICMP extension information is also included the `json` and `stream` report modes.\n\nSample output for a single hop from the `json` report:\n\n```json\n{\n  \"ttl\": 14,\n  \"hosts\": [\n    {\n      \"ip\": \"129.250.3.125\",\n      \"hostname\": \"ae-4.r25.sttlwa01.us.bb.gin.ntt.net\"\n    }\n  ],\n  \"extensions\": [\n    {\n      \"mpls\": {\n        \"members\": [\n          {\n            \"label\": 91106,\n            \"exp\": 0,\n            \"bos\": 1,\n            \"ttl\": 1\n          }\n        ]\n      }\n    }\n  ],\n  \"loss_pct\": \"0.00\",\n  \"sent\": 1,\n  \"last\": \"178.16\",\n  \"recv\": 1,\n  \"avg\": \"178.16\",\n  \"best\": \"178.16\",\n  \"worst\": \"178.16\",\n  \"stddev\": \"0.00\"\n}\n```\n\n### Paris Tracing Strategy for IPv6/UDP\n\nThe work to support the remaining [paris](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum)\nand [dublin](https://github.com/insomniacslk/dublin-traceroute) tracing modes continues in this release with the\naddition of support for the Paris tracing strategy for IPv6/UDP.\n\nAs a reminder, unlike classic traceroute and MTR, these alternative tracing strategies do not encode the probe sequence\nnumber in either the src or dest port of the UDP or TCP packet, but instead use other protocol and address family\nspecific techniques. Specifically, the Paris tracing strategy for IPv6/UDP utilizes the UDP checksum for this purposes\nand manipulates the UDP payload to ensure packets remind valid.\n\nBy doing so, these strategies are able to keep the src and dest ports fixed which makes it much more likely (though not\nguaranteed) that each round of tracing will follow the same path through the network (note that this is _not_ true for\nthe return path).\n\nThe following command runs a IPv6/UDP trace using the `paris` tracing strategy with fixed src and dest ports:\n\n```shell\ntrip example.com --udp -6 -R paris -S 5000 -P 3500\n```\n\nRefer to the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details of the work remaining to\nsupport all ECMP strategies for both UDP and TCP for IPv4 and IPv6.\n\n### Unprivileged Mode\n\nTrippy normally requires elevated privileges due to the use of raw sockets. Enabling the required privileges for a given\nplatform can be achieved in several ways as in described\nthe [privileges](https://github.com/fujiapple852/trippy#privileges) section of the documentation.\n\nThis release of Trippy adds the ability to run _without_ elevated privileged on a subset of platforms, but with some\nlimitations which are described below.\n\nThe unprivileged mode can be enabled by adding the `--unprivileged` (`-u`) command line flag or by adding\nthe `unprivileged` entry in the `trippy` section of the configuration file:\n\n```toml\n[trippy]\nunprivileged = true\n```\n\nThe following command runs a trace in unprivileged mode:\n\n```shell\ntrip example.com -u\n```\n\nUnprivileged mode is currently only supported on macOS. Linux support is possible and may be added in the future.\nUnprivileged mode is not supported on NetBSD, OpenBSD, FreeBSD or Windows as these platforms do not support\nthe `IPPROTO_ICMP` socket type.\n\nUnprivileged mode does not support the `paris` or `dublin` tracing strategies as these require raw sockets in order to\nmanipulate the UDP and IP header respectively.\n\nSee [#101](https://github.com/fujiapple852/trippy/issues/101) for further information.\n\n### Resolve All DNS\n\nTrippy can be provided with either an IP address or a hostname as the target for tracing. Trippy will resolve hostnames\nto IP addresses via DNS lookup (using the configured DNS resolver, see the existing `--dns-resolve-method` flag) and\npick an arbitrary IP address from those returned.\n\nTrippy also has the ability to trace to several targets simultaneously (for the ICMP protocol only) and can be provided\nwith a list of IP addresses and hostnames.\n\nTrippy `0.9.0` combined these features and introduces a convenience flag `--dns-resolve-all` which resolves a given\nhostname to all IP addresses and will begin to trace to all of them simultaneously.\n\n<img width=\"60%\" alt=\"dns_resolve_all\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.9.0/dns_resolve_all.png\">\n\n### Hop Privacy\n\nAt times it is desirable to share tracing information with others to help with diagnostics of a network problem. These\ntraces can contain sensitive information, such as IP addresses, hostnames and GeoIp details of the internet facing hops.\nUsers often wish to avoid exposing this data and are forced to redact the tracing output or screenshots.\n\nTrippy `0.9.0` adds a new privacy feature, which hides all sensitive information for a configurable number of hops in\nthe hops table, chart and GeoIP world map.\n\nThe following screenshot shows the world map view with the sensitive information of some hops hidden:\n\n<img width=\"60%\" alt=\"privacy\" src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.9.0/privacy.png\">\n\nThe following command will hide all sensitive information for the first 3 hops (ttl 1, 2 & 3) in the TUI:\n\n```shell\ntrip example.com --tui-privacy-max-ttl 3\n```\n\nThis can also be made the default behaviour by setting the value in the Trippy configuration file:\n\n```toml\n[tui]\ntui-privacy-max-ttl = 3\n```\n\nFrom within the TUI the privacy mode can be toggled on and off using the `toggle-privacy` TUI command (bound to the `p`\nkey by default).\n\nNote the toggle is only available if `tui-privacy-max-ttl` is configured to be non-zero. Privacy mode is entered\nautomatically on startup to avoid any accidental exposure of sensitive data, such as when sharing a screen.\n\n### Print Config Template\n\nThe `0.8.0` release of Trippy introduced\na [configuration file](https://github.com/fujiapple852/trippy#configuration-reference) and provided a sample\nconfiguration file you could download. This release adds a command which generates a configuration template appropriate\nfor the specific version of Trippy.\n\nThe following command generates a `trippy.toml` configuration file with all possible configuration options specified and\nset to their default values:\n\n```shell\ntrip --print-config-template > trippy.toml\n```\n\n### Alternative Help Key Binding\n\nCan't decide whether you want to use `h` or `?` to display help information? Well fear not, Trippy now supports\nan `toggle-help-alt` TUI command (bound to the `?` key by default) in additional to the existing `toggle-help` TUI\ncommand (bound to the `h` key by default).\n\n### Improvements to Reports\n\nThis release fixes a bug that prevented reverse DNS lookup from working in all reporting modes.\n\nThe list of IPs associated with a given hop have also been added to the `csv` and all tabular reports. ICMP extension\ndata has also been included in several reports.\n\nNote that these are breaking change as the output of the reports has changed.\n\n### New Binary Asset Downloads\n\nThe list of operating systems, CPU architectures and environments which have pre-build binary assets available for\ndownload has been greatly expanded for the `0.9.0` release.\n\nThis includes assets for Linux, macOS, Windows, NetBSD and FreeBSD. Assets are available for `x86_64`, `aarch64`\nand `arm7` and includes builds for various environments such as `gnu` and `musl` where appropriate. There are also\npre-build `RPM` and `deb` downloads available. See\nthe [Binary Asset Download](https://github.com/fujiapple852/trippy#binary-asset-download) section for a full list.\n\nNote that Trippy `0.9.0` has only been [tested](https://github.com/fujiapple852/trippy/issues/836) on a small subset of\nthese platforms.\n\n### New Distribution Packages\n\nSince the last release Trippy has been added as an official WinGet package (kudos to @mdanish-kh and\n@BrandonWanHuanSheng!) and can be installed as follows:\n\n```shell\nwinget install trippy\n```\n\nTrippy has also been added to the scoop `Main` bucket (thanks to @StarsbySea!) and can be installed as follows:\n\n```shell\nscoop install trippy\n```\n\nYou can find the full list of [distributions](https://github.com/fujiapple852/trippy/tree/master#distributions) in the\ndocumentation.\n\n### Thanks\n\nMy thanks to all Trippy contributors, package maintainers and community members.\n\nFeel free to drop by the Trippy Matrix room for a chat:\n\n[![#trippy-dev:matrix.org](https://img.shields.io/badge/matrix/trippy-dev:matrix.org-blue)](https://matrix.to/#/#trippy-dev:matrix.org)\n\nHappy Tracing!\n\n## New Contributors\n\n- @c-git made their first contribution in https://github.com/fujiapple852/trippy/pull/632\n- @trkelly23 made their first contribution in https://github.com/fujiapple852/trippy/pull/788\n\n# 0.8.0\n\n## Highlights\n\nThe `0.8.0` release of Trippy brings several new features, UX enhancements, and quality of life improvements, as well as\nvarious small fixes and other minor improvements.\n\n#### Hop Detail Navigation\n\nTrippy offers various mechanisms to visualize [ECMP](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) (\nEqual-Cost Multi-Path Routing) when tracing with UDP and TCP protocols. Features include displaying all hosts for a\ngiven hop in a scrollable table, limiting the number of hosts shown per hop (showing the % of traffic for each host),\nand greying out hops that are not part of a specific tracing round.\n\nDespite these helpful features, visualizing a complete trace can be challenging when there are numerous hosts for some\nhops, which is common in environments where ECMP is heavily utilized.\n\nThis release enhances ECMP visualization support by introducing a hop detail navigation mode, which can be toggled on\nand off by pressing `d` (default key binding). This mode displays multiline information for the selected hop only,\nincluding IP, hostname, AS, and GeoIP details about a single host for the hop. Users can navigate forward and backward\nbetween hosts in a given hop by pressing `,` and `.` (default key bindings), respectively.\n\n<img src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.8.0/hop_details.png\" width=\"60%\">\n\nIn addition to visualizing ECMP, Trippy also supports alternative tracing strategies to assist with ECMP routing, which\nare described below.\n\n#### Paris Tracing Strategy\n\nTrippy already supports both classic and [dublin](https://github.com/insomniacslk/dublin-traceroute) tracing strategies,\nand this release adds support for the [paris](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum)\ntracing strategy for the UDP protocol.\n\nUnlike classic traceroute and MTR, these alternative tracing strategies do not encode the probe sequence number in\neither the src or dest port of the UDP or TCP packet, but instead use other protocol and address family specific\ntechniques.\n\nThis means that every probe in a trace can share common values for the src & dest hosts and ports which, when combined\nwith the protocol, is typically what is used to making traffic route decisions in ECMP routing. This means that these\nalternative tracing strategies significantly increase the likelihood that the same path is followed for each probe in a\ntrace (but not the return path!) in the presence of ECMP routing.\n\nThe following command runs a UDP trace using the new `paris` tracing strategy with fixed src and dest ports (the src and\ndest hosts and the protocol are always fixed) and will therefore likely follow a common path for each probe in the\ntrace:\n\n```shell\ntrip www.example.com --udp -R paris -S 5000 -P 3500\n```\n\nFuture Trippy versions will build upon these strategies and further improve the ability to control and visualize ECMP\nrouting, refer to the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for further details.\n\n#### GeoIp Information & Interactive Map\n\nTrippy now supports the ability to look up and display GeoIP information from a user-provided\nMaxMind [GeoLite2 City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data). This information is\ndisplayed per host in the hop table (for both normal and new detail navigation modes) and can be shown in various\nformats. For example, short form like \"San Jose, CA, US\" or long form like \"San Jose, California, United States, North\nAmerica,\" or latitude, longitude, and accuracy radius like \"37.3512, -121.8846 (~20km)\".\n\nThe following command enables GeoIP lookup from the provided `GeoLite2-City.mmdb` file and will show long form locations\nin the hop table:\n\n```shell\ntrip example.com --geoip-mmdb-file GeoLite2-City.mmdb --tui-geoip-mode long\n```\n\nAdditionally, Trippy features a new interactive map screen that can be toggled on and off by pressing `m` (default key\nbinding). This screen displays a world map and plots the location of all hosts for all hops in the current trace, as\nwell as highlighting the location of the selected hop.\n\n<img src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.8.0/world_map.png\" width=\"60%\">\n\n#### Autonomous System Display Enhancements\n\nTrippy has long offered the ability to look up and display AS information. This release makes this feature more flexible\nby allowing different AS details to be shown in the hops table, including AS number, AS name, prefix CIDR, and registry\ndetails.\n\n<img src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.8.0/as_info.png\" width=\"60%\">\n\nThe following command enables AS lookup and will display the prefix CIDR for each host in the TUI:\n\n```shell\ntrip example.com -z true -r resolv --tui-as-mode prefix\n```\n\nThis release also fixes a limitation in earlier versions of Trippy that prevented the lookup of AS information for IP\naddresses without a corresponding `PTR` DNS record.\n\n#### UI Cleanup & Configuration Dialog\n\nThe number of configurable parameters in Trippy has grown significantly, surpassing the number that can be comfortably\ndisplayed in the TUI header section. Previous Trippy versions displayed an arbitrarily chosen subset of these\nparameters, many of which have limited value for users and consume valuable screen space.\n\nThis release introduces a new interactive settings dialog that can be toggled on and off with `s` (default key binding)\nto display all configured parameters. The TUI header has also been cleaned up to show only the most relevant\ninformation, specifically the protocol and address family, the AS info toggle, the hop details toggle, and the max-hosts\nsetting.\n\n<img src=\"https://raw.githubusercontent.com/fujiapple852/trippy/master/assets/0.8.0/settings.png\" width=\"60%\">\n\n#### Configuration File\n\nThe previous Trippy release introduced the ability to customize the TUI color theme and key bindings, both of which\ncould be specified by command-line arguments. While functional, this method is inconvenient when configuring a large\nnumber of colors or keys.\n\nThis release adds support for a Trippy configuration file, allowing for persistent storage of color themes, key\nbindings, and all other configuration items supported by Trippy.\n\nFor a sample configuration file showing all possible configurable items that are available, see\nthe [configuration reference](https://github.com/fujiapple852/trippy#configuration-reference) for details.\n\n#### Shell Completions\n\nThis release enables the generation of shell completions for various shells, including bash, zsh, PowerShell, and fish,\nusing the new `--generate` command-line flag.\n\nThe following command will generate and store shell completions for the fish shell:\n\n```shell\ntrip --generate fish > ~/.config/fish/completions/trip.fish\n```\n\n#### Improved Error Reporting & Debug Logging\n\nThis release adds a number of command-line flags to enable debug logging, enhancing the ability to diagnose failures.\nFor example, the following command can be used to run tracing with no output, except for debug output in a format\nsuitable to be displayed with `chrome://tracing` or similar tools:\n\n```shell\ntrip www.example.com -m silent -v --log-format chrome\n```\n\nSocket errors have also been augmented with contextual information, such as the socket address for a bind failure, to\nhelp with the diagnosis of issues.\n\n#### New Distribution Packages\n\nTrippy is now also available as a Nix package (@figsoda), a FreeBSD port (@ehaupt) and a Windows Scoop package. This\nrelease also re-enables support for a `musl` binary which was disabled in `0.7.0` due to a bug in a critical library\nused by Trippy.\n\nSee [distributions](https://github.com/fujiapple852/trippy#distributions) for the full list of available packages.\n\nMy thanks, as ever, to all Trippy contributors!\n\n## New Contributors\n\n- @utkarshgupta137 made their first contribution in https://github.com/fujiapple852/trippy/pull/537\n\n# 0.7.0\n\n## Highlights\n\nThe major highlight of the 0.7.0 release of Trippy is the addition of full support for Windows, for all tracing modes\nand protocols! 🎉. This has been many months in the making and is thanks to the hard work and perseverance of @zarkdav.\n\nThis release also sees the introduction of custom Tui themes and key bindings, `deb` and `rpm` package releases, as well\nas several important bug fixes.\n\nMy thanks to all the contributors!\n\n# 0.6.0\n\n## Highlights\n\nThe first official release of Trippy!\n"
  },
  {
    "path": "crates/README.md",
    "content": "## Crates\n\nThe following is a list of the crates defined by Trippy and their purposes:\n\n### `trippy`\n\nA binary crate for the Trippy application and a library crate. This is the crate you would use if you wish to install\nand run Trippy as a standalone tool.\n\n```shell\ncargo install --locked trippy\n```\n\nIt can also be used as library for crates that wish to use the Trippy tracing functionality.\n\n> [!NOTE]\n> The `trippy` crate has `tui` as a default feature and so you should disable default features when using it as a\n> library.\n\n```shell\ncargo add trippy --no-default-features --features core,dns\n```\n\n### `trippy-core`\n\nA library crate providing the core Trippy tracing functionality. This crate is used by the Trippy application and is\nthe crate you would use if you wish to provide the Trippy tracing functionality in your own application.\n\n```shell\ncargo add trippy-core\n```\n\n### `trippy-packet`\n\nA library crate which provides packet wire formats and packet parsing functionality. This crate is used by the Trippy\napplication and is the crate you would use if you wish to provide packet parsing functionality in your own application.\n\n```shell\ncargo add trippy-packet\n```\n\n### `trippy-dns`\n\nA library crate for performing forward and reverse lazy DNS resolution. This crate is designed to be used by the Trippy\napplication but may also be useful for other applications that need to perform forward and reverse lazy DNS resolution.\n\n```shell\ncargo add trippy-dns\n```\n\n### `trippy-privilege`\n\nA library crate for discovering platform privileges. This crate is designed to be used by the Trippy application but\nmay also be useful for other applications.\n\n```shell\ncargo add trippy-privilege\n```\n\n### `trippy-tui`\n\nA library crate for the Trippy terminal user interface.\n\n```shell\ncargo add trippy-tui\n```\n"
  },
  {
    "path": "crates/trippy/Cargo.toml",
    "content": "[package]\nname = \"trippy\"\ndescription = \"A network diagnostic tool\"\nversion.workspace = true\nauthors.workspace = true\ndocumentation.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\nreadme = \"README.md\"\nlicense.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nkeywords.workspace = true\ncategories.workspace = true\n\n[[bin]]\nname = \"trip\"\npath = \"src/main.rs\"\nrequired-features = [\"tui\"]\n\n[features]\ndefault = [\"tui\"]\ntui = [\"trippy-tui\", \"anyhow\"]\ncore = [\"trippy-core\"]\nprivilege = [\"trippy-privilege\"]\ndns = [\"trippy-dns\"]\npacket = [\"trippy-packet\"]\n\n[dependencies]\ntrippy-tui = { workspace = true, optional = true }\ntrippy-core = { workspace = true, optional = true }\ntrippy-privilege = { workspace = true, optional = true }\ntrippy-dns = { workspace = true, optional = true }\ntrippy-packet = { workspace = true, optional = true }\nanyhow = { workspace = true, optional = true }\n\n[lints]\nworkspace = true\n\n[package.metadata.generate-rpm]\nassets = [\n  { source = \"target/release/trip\", dest = \"/usr/bin/trip\", mode = \"755\" },\n]\n"
  },
  {
    "path": "crates/trippy/src/lib.rs",
    "content": "#![allow(\n    rustdoc::broken_intra_doc_links,\n    rustdoc::bare_urls,\n    clippy::doc_markdown,\n    clippy::doc_lazy_continuation\n)]\n#![doc = include_str!(\"../README.md\")]\n\n// Re-export the user facing libraries, so they may be used from trippy crate directly.\n\n#[cfg(feature = \"core\")]\n/// A network tracer.\npub mod core {\n    pub use trippy_core::*;\n}\n\n#[cfg(feature = \"dns\")]\n/// A lazy DNS resolver.\npub mod dns {\n    pub use trippy_dns::*;\n}\n\n#[cfg(feature = \"privilege\")]\n/// Discover platform privileges.\npub mod privilege {\n    pub use trippy_privilege::*;\n}\n\n#[cfg(feature = \"packet\")]\n/// Network packets.\npub mod packet {\n    pub use trippy_packet::*;\n}\n"
  },
  {
    "path": "crates/trippy/src/main.rs",
    "content": "fn main() -> anyhow::Result<()> {\n    trippy_tui::trippy()\n}\n"
  },
  {
    "path": "crates/trippy-core/Cargo.toml",
    "content": "[package]\nname = \"trippy-core\"\ndescription = \"A network tracing library\"\nversion.workspace = true\nauthors.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\nreadme.workspace = true\nlicense.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nkeywords.workspace = true\ncategories.workspace = true\n\n[dependencies]\ntrippy-packet.workspace = true\ntrippy-privilege.workspace = true\narrayvec.workspace = true\nbitflags.workspace = true\nderive_more = { workspace = true, default-features = false, features = [\"mul\", \"add\", \"add_assign\"] }\nindexmap = { workspace = true, default-features = false, features = [\"std\"] }\nitertools.workspace = true\nparking_lot.workspace = true\nsocket2 = { workspace = true, features = [\"all\"] }\nthiserror.workspace = true\ntracing.workspace = true\n\n[target.'cfg(unix)'.dependencies]\nnix = { workspace = true, default-features = false, features = [\"user\", \"poll\", \"net\"] }\n\n[target.'cfg(windows)'.dependencies]\npaste.workspace = true\nwidestring.workspace = true\nwindows-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\"] }\n\n[dev-dependencies]\nanyhow.workspace = true\nfutures-concurrency.workspace = true\nhex-literal.workspace = true\nmockall.workspace = true\nrand.workspace = true\nserde = { workspace = true, default-features = false, features = [\"derive\", \"std\"] }\ntest-case.workspace = true\ntokio-util.workspace = true\ntokio = { workspace = true, features = [\"full\"] }\ntoml = { workspace = true, default-features = false, features = [\"parse\"] }\ntracing-subscriber = { workspace = true, default-features = false, features = [\"env-filter\", \"fmt\"] }\n\n[target.'cfg(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\"))'.dev-dependencies]\ntun-rs = { workspace = true, features = [\"async\"] }\n\n[features]\n# Enable simulation integration tests\nsim-tests = []\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/trippy-core/src/builder.rs",
    "content": "use crate::config::{ChannelConfig, StateConfig, StrategyConfig};\nuse crate::constants::MAX_INITIAL_SEQUENCE;\nuse crate::error::Result;\nuse crate::{\n    Error, IcmpExtensionParseMode, MAX_TTL, MaxInflight, MaxRounds, MultipathStrategy, PacketSize,\n    PayloadPattern, PortDirection, PrivilegeMode, Protocol, Sequence, TimeToLive, TraceId, Tracer,\n    TypeOfService,\n};\nuse std::net::IpAddr;\nuse std::num::NonZeroUsize;\nuse std::time::Duration;\n\n/// Build a tracer.\n///\n/// This is a convenience builder to simplify the creation of execution of a\n/// tracer.\n///\n/// # Examples\n///\n/// ```no_run\n/// # fn main() -> anyhow::Result<()> {\n/// use trippy_core::{Builder, MultipathStrategy, Port, PortDirection, PrivilegeMode, Protocol};\n///\n/// let addr = std::net::IpAddr::from([1, 2, 3, 4]);\n/// let tracer = Builder::new(addr)\n///     .privilege_mode(PrivilegeMode::Unprivileged)\n///     .protocol(Protocol::Udp)\n///     .multipath_strategy(MultipathStrategy::Dublin)\n///     .port_direction(PortDirection::FixedBoth(Port(33434), Port(3500)))\n///     .build()?;\n/// # Ok(())\n/// # }\n/// ```\n///\n/// # See Also\n///\n/// - [`Tracer`] - A traceroute implementation.\n#[derive(Debug)]\npub struct Builder {\n    interface: Option<String>,\n    source_addr: Option<IpAddr>,\n    target_addr: IpAddr,\n    privilege_mode: PrivilegeMode,\n    protocol: Protocol,\n    packet_size: PacketSize,\n    payload_pattern: PayloadPattern,\n    tos: TypeOfService,\n    icmp_extension_parse_mode: IcmpExtensionParseMode,\n    read_timeout: Duration,\n    tcp_connect_timeout: Duration,\n    trace_identifier: TraceId,\n    max_rounds: Option<MaxRounds>,\n    first_ttl: TimeToLive,\n    max_ttl: TimeToLive,\n    grace_duration: Duration,\n    max_inflight: MaxInflight,\n    initial_sequence: Sequence,\n    multipath_strategy: MultipathStrategy,\n    port_direction: PortDirection,\n    min_round_duration: Duration,\n    max_round_duration: Duration,\n    max_samples: usize,\n    max_flows: usize,\n    drop_privileges: bool,\n}\n\nimpl Default for Builder {\n    fn default() -> Self {\n        Self {\n            interface: None,\n            source_addr: None,\n            target_addr: ChannelConfig::default().target_addr,\n            privilege_mode: ChannelConfig::default().privilege_mode,\n            protocol: ChannelConfig::default().protocol,\n            packet_size: ChannelConfig::default().packet_size,\n            payload_pattern: ChannelConfig::default().payload_pattern,\n            tos: ChannelConfig::default().tos,\n            icmp_extension_parse_mode: ChannelConfig::default().icmp_extension_parse_mode,\n            read_timeout: ChannelConfig::default().read_timeout,\n            tcp_connect_timeout: ChannelConfig::default().tcp_connect_timeout,\n            trace_identifier: StrategyConfig::default().trace_identifier,\n            max_rounds: StrategyConfig::default().max_rounds,\n            first_ttl: StrategyConfig::default().first_ttl,\n            max_ttl: StrategyConfig::default().max_ttl,\n            grace_duration: StrategyConfig::default().grace_duration,\n            max_inflight: StrategyConfig::default().max_inflight,\n            initial_sequence: StrategyConfig::default().initial_sequence,\n            multipath_strategy: StrategyConfig::default().multipath_strategy,\n            port_direction: StrategyConfig::default().port_direction,\n            min_round_duration: StrategyConfig::default().min_round_duration,\n            max_round_duration: StrategyConfig::default().max_round_duration,\n            max_samples: StateConfig::default().max_samples,\n            max_flows: StateConfig::default().max_flows,\n            drop_privileges: false,\n        }\n    }\n}\n\nimpl Builder {\n    /// Build a tracer builder for a given target.\n    ///\n    /// # Examples\n    ///\n    /// Basic usage:\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = std::net::IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn new(target_addr: IpAddr) -> Self {\n        Self {\n            target_addr,\n            ..Default::default()\n        }\n    }\n\n    /// Set the source address.\n    ///\n    /// If not set then the source address will be discovered based on the\n    /// target address and the interface.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let source_addr = IpAddr::from([192, 168, 1, 1]);\n    /// let tracer = Builder::new(addr).source_addr(Some(source_addr)).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn source_addr(self, source_addr: Option<IpAddr>) -> Self {\n        Self {\n            source_addr,\n            ..self\n        }\n    }\n\n    /// Set the source interface.\n    ///\n    /// If the source interface is provided it will be used to look up the IPv4\n    /// or IPv6 source address.\n    ///\n    /// If not provided the source address will be determined by OS based on\n    /// the target IPv4 or IPv6 address.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).interface(Some(\"eth0\")).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn interface<S: Into<String>>(self, interface: Option<S>) -> Self {\n        Self {\n            interface: interface.map(Into::into),\n            ..self\n        }\n    }\n\n    /// Set the protocol.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::{Builder, Protocol};\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).protocol(Protocol::Udp).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn protocol(self, protocol: Protocol) -> Self {\n        Self { protocol, ..self }\n    }\n\n    /// Set the trace identifier.\n    ///\n    /// If not set then 0 will be used as the trace identifier.\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).trace_identifier(12345).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn trace_identifier(self, trace_id: u16) -> Self {\n        Self {\n            trace_identifier: TraceId(trace_id),\n            ..self\n        }\n    }\n\n    /// Set the privilege mode.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::{Builder, PrivilegeMode};\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr)\n    ///     .privilege_mode(PrivilegeMode::Unprivileged)\n    ///     .build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn privilege_mode(self, privilege_mode: PrivilegeMode) -> Self {\n        Self {\n            privilege_mode,\n            ..self\n        }\n    }\n\n    /// Set the multipath strategy.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::{Builder, MultipathStrategy};\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr)\n    ///     .multipath_strategy(MultipathStrategy::Paris)\n    ///     .build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn multipath_strategy(self, multipath_strategy: MultipathStrategy) -> Self {\n        Self {\n            multipath_strategy,\n            ..self\n        }\n    }\n\n    /// Set the packet size.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).packet_size(128).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn packet_size(self, packet_size: u16) -> Self {\n        Self {\n            packet_size: PacketSize(packet_size),\n            ..self\n        }\n    }\n\n    /// Set the payload pattern.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).payload_pattern(0xff).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn payload_pattern(self, payload_pattern: u8) -> Self {\n        Self {\n            payload_pattern: PayloadPattern(payload_pattern),\n            ..self\n        }\n    }\n\n    /// Set the type of service.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).tos(0x1a).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn tos(self, tos: u8) -> Self {\n        Self {\n            tos: TypeOfService(tos),\n            ..self\n        }\n    }\n\n    /// Set the ICMP extensions mode.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::{Builder, IcmpExtensionParseMode};\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr)\n    ///     .icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled)\n    ///     .build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn icmp_extension_parse_mode(\n        self,\n        icmp_extension_parse_mode: IcmpExtensionParseMode,\n    ) -> Self {\n        Self {\n            icmp_extension_parse_mode,\n            ..self\n        }\n    }\n\n    /// Set the read timeout.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use std::time::Duration;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr)\n    ///     .read_timeout(Duration::from_millis(50))\n    ///     .build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn read_timeout(self, read_timeout: Duration) -> Self {\n        Self {\n            read_timeout,\n            ..self\n        }\n    }\n\n    /// Set the TCP connect timeout.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use std::time::Duration;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr)\n    ///     .tcp_connect_timeout(Duration::from_millis(100))\n    ///     .build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn tcp_connect_timeout(self, tcp_connect_timeout: Duration) -> Self {\n        Self {\n            tcp_connect_timeout,\n            ..self\n        }\n    }\n\n    /// Set the maximum number of rounds.\n    ///\n    /// If set to `None` then the tracer will run indefinitely, otherwise it\n    /// will stop after the given number of rounds.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).max_rounds(Some(10)).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn max_rounds(self, max_rounds: Option<usize>) -> Self {\n        Self {\n            max_rounds: max_rounds\n                .and_then(|max_rounds| NonZeroUsize::new(max_rounds).map(MaxRounds)),\n            ..self\n        }\n    }\n\n    /// Set the first ttl.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).first_ttl(2).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn first_ttl(self, first_ttl: u8) -> Self {\n        Self {\n            first_ttl: TimeToLive(first_ttl),\n            ..self\n        }\n    }\n\n    /// Set the maximum ttl.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).max_ttl(16).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn max_ttl(self, max_ttl: u8) -> Self {\n        Self {\n            max_ttl: TimeToLive(max_ttl),\n            ..self\n        }\n    }\n\n    /// Set the grace duration.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use std::time::Duration;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr)\n    ///     .grace_duration(Duration::from_millis(100))\n    ///     .build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn grace_duration(self, grace_duration: Duration) -> Self {\n        Self {\n            grace_duration,\n            ..self\n        }\n    }\n\n    /// Set the max number of probes in flight at any given time.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).max_inflight(22).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn max_inflight(self, max_inflight: u8) -> Self {\n        Self {\n            max_inflight: MaxInflight(max_inflight),\n            ..self\n        }\n    }\n\n    /// Set the initial sequence number.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).initial_sequence(35000).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn initial_sequence(self, initial_sequence: u16) -> Self {\n        Self {\n            initial_sequence: Sequence(initial_sequence),\n            ..self\n        }\n    }\n\n    /// Set the port direction.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::{Builder, Port, PortDirection};\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr)\n    ///     .port_direction(PortDirection::FixedDest(Port(8080)))\n    ///     .build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn port_direction(self, port_direction: PortDirection) -> Self {\n        Self {\n            port_direction,\n            ..self\n        }\n    }\n\n    /// Set the minimum round duration.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use std::time::Duration;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr)\n    ///     .min_round_duration(Duration::from_millis(500))\n    ///     .build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn min_round_duration(self, min_round_duration: Duration) -> Self {\n        Self {\n            min_round_duration,\n            ..self\n        }\n    }\n\n    /// Set the maximum round duration.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use std::time::Duration;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr)\n    ///     .max_round_duration(Duration::from_millis(1500))\n    ///     .build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn max_round_duration(self, max_round_duration: Duration) -> Self {\n        Self {\n            max_round_duration,\n            ..self\n        }\n    }\n\n    /// Set the maximum number of samples to record.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).max_samples(256).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn max_samples(self, max_samples: usize) -> Self {\n        Self {\n            max_samples,\n            ..self\n        }\n    }\n\n    /// Set the maximum number of flows to record.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).max_flows(64).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn max_flows(self, max_flows: usize) -> Self {\n        Self { max_flows, ..self }\n    }\n\n    /// Drop privileges after connection is established.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).drop_privileges(true).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    #[must_use]\n    pub fn drop_privileges(self, drop_privileges: bool) -> Self {\n        Self {\n            drop_privileges,\n            ..self\n        }\n    }\n\n    /// Build the `Tracer`.\n    ///\n    /// # Examples\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// use std::net::IpAddr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from([1, 1, 1, 1]);\n    /// let tracer = Builder::new(addr).build()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    ///\n    /// # Errors\n    ///\n    /// This function will return `Error::BadConfig` if the configuration is invalid.\n    pub fn build(self) -> Result<Tracer> {\n        match (self.protocol, self.port_direction) {\n            (Protocol::Udp, PortDirection::None) => {\n                return Err(Error::BadConfig(\n                    \"port_direction may not be None for udp protocol\".to_string(),\n                ));\n            }\n            (Protocol::Tcp, PortDirection::None) => {\n                return Err(Error::BadConfig(\n                    \"port_direction may not be None for tcp protocol\".to_string(),\n                ));\n            }\n            _ => (),\n        }\n        if self.first_ttl.0 > MAX_TTL {\n            return Err(Error::BadConfig(format!(\n                \"first_ttl {} > {MAX_TTL}\",\n                self.first_ttl.0\n            )));\n        }\n        if self.max_ttl.0 > MAX_TTL {\n            return Err(Error::BadConfig(format!(\n                \"max_ttl {} > {MAX_TTL}\",\n                self.max_ttl.0\n            )));\n        }\n        if self.initial_sequence.0 > MAX_INITIAL_SEQUENCE {\n            return Err(Error::BadConfig(format!(\n                \"initial_sequence {} > {MAX_INITIAL_SEQUENCE}\",\n                self.initial_sequence.0\n            )));\n        }\n        Ok(Tracer::new(\n            self.interface,\n            self.source_addr,\n            self.target_addr,\n            self.privilege_mode,\n            self.protocol,\n            self.packet_size,\n            self.payload_pattern,\n            self.tos,\n            self.icmp_extension_parse_mode,\n            self.read_timeout,\n            self.tcp_connect_timeout,\n            self.trace_identifier,\n            self.max_rounds,\n            self.first_ttl,\n            self.max_ttl,\n            self.grace_duration,\n            self.max_inflight,\n            self.initial_sequence,\n            self.multipath_strategy,\n            self.port_direction,\n            self.min_round_duration,\n            self.max_round_duration,\n            self.max_samples,\n            self.max_flows,\n            self.drop_privileges,\n        ))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::{Port, config};\n    use config::defaults;\n    use std::net::Ipv4Addr;\n    use std::num::NonZeroUsize;\n\n    const SOURCE_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::UNSPECIFIED);\n    const TARGET_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2));\n\n    #[test]\n    fn test_builder_minimal() {\n        let tracer = Builder::new(TARGET_ADDR).build().unwrap();\n        assert_eq!(TARGET_ADDR, tracer.target_addr());\n        assert_eq!(None, tracer.source_addr());\n        assert_eq!(None, tracer.interface());\n        assert_eq!(defaults::DEFAULT_MAX_SAMPLES, tracer.max_samples());\n        assert_eq!(defaults::DEFAULT_MAX_FLOWS, tracer.max_flows());\n        assert_eq!(defaults::DEFAULT_STRATEGY_PROTOCOL, tracer.protocol());\n        assert_eq!(TraceId::default(), tracer.trace_identifier());\n        assert_eq!(defaults::DEFAULT_PRIVILEGE_MODE, tracer.privilege_mode());\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_MULTIPATH,\n            tracer.multipath_strategy()\n        );\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_PACKET_SIZE,\n            tracer.packet_size().0\n        );\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN,\n            tracer.payload_pattern().0\n        );\n        assert_eq!(defaults::DEFAULT_STRATEGY_TOS, tracer.tos().0);\n        assert_eq!(\n            defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE,\n            tracer.icmp_extension_parse_mode()\n        );\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_READ_TIMEOUT,\n            tracer.read_timeout()\n        );\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_TCP_CONNECT_TIMEOUT,\n            tracer.tcp_connect_timeout()\n        );\n        assert_eq!(None, tracer.max_rounds());\n        assert_eq!(defaults::DEFAULT_STRATEGY_FIRST_TTL, tracer.first_ttl().0);\n        assert_eq!(defaults::DEFAULT_STRATEGY_MAX_TTL, tracer.max_ttl().0);\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_GRACE_DURATION,\n            tracer.grace_duration()\n        );\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_MAX_INFLIGHT,\n            tracer.max_inflight().0\n        );\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE,\n            tracer.initial_sequence().0\n        );\n        assert_eq!(PortDirection::None, tracer.port_direction());\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION,\n            tracer.min_round_duration()\n        );\n        assert_eq!(\n            defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION,\n            tracer.max_round_duration()\n        );\n    }\n\n    #[test]\n    fn test_builder_full() {\n        let tracer = Builder::new(TARGET_ADDR)\n            .source_addr(Some(SOURCE_ADDR))\n            .interface(Some(\"eth0\"))\n            .max_samples(10)\n            .max_flows(20)\n            .protocol(Protocol::Udp)\n            .trace_identifier(101)\n            .privilege_mode(PrivilegeMode::Unprivileged)\n            .multipath_strategy(MultipathStrategy::Paris)\n            .packet_size(128)\n            .payload_pattern(0xff)\n            .tos(0x1a)\n            .icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled)\n            .read_timeout(Duration::from_millis(50))\n            .tcp_connect_timeout(Duration::from_millis(100))\n            .max_rounds(Some(10))\n            .first_ttl(2)\n            .max_ttl(16)\n            .grace_duration(Duration::from_millis(100))\n            .max_inflight(22)\n            .initial_sequence(35000)\n            .port_direction(PortDirection::FixedSrc(Port(8080)))\n            .min_round_duration(Duration::from_millis(500))\n            .max_round_duration(Duration::from_millis(1500))\n            .build()\n            .unwrap();\n\n        assert_eq!(TARGET_ADDR, tracer.target_addr());\n        // note that `source_addr` is not set until the tracer is run\n        assert_eq!(None, tracer.source_addr());\n        assert_eq!(Some(\"eth0\"), tracer.interface());\n        assert_eq!(10, tracer.max_samples());\n        assert_eq!(20, tracer.max_flows());\n        assert_eq!(Protocol::Udp, tracer.protocol());\n        assert_eq!(TraceId(101), tracer.trace_identifier());\n        assert_eq!(PrivilegeMode::Unprivileged, tracer.privilege_mode());\n        assert_eq!(MultipathStrategy::Paris, tracer.multipath_strategy());\n        assert_eq!(PacketSize(128), tracer.packet_size());\n        assert_eq!(PayloadPattern(0xff), tracer.payload_pattern());\n        assert_eq!(TypeOfService(0x1a), tracer.tos());\n        assert_eq!(\n            IcmpExtensionParseMode::Enabled,\n            tracer.icmp_extension_parse_mode()\n        );\n        assert_eq!(Duration::from_millis(50), tracer.read_timeout());\n        assert_eq!(Duration::from_millis(100), tracer.tcp_connect_timeout());\n        assert_eq!(\n            Some(MaxRounds(NonZeroUsize::new(10).unwrap())),\n            tracer.max_rounds()\n        );\n        assert_eq!(TimeToLive(2), tracer.first_ttl());\n        assert_eq!(TimeToLive(16), tracer.max_ttl());\n        assert_eq!(Duration::from_millis(100), tracer.grace_duration());\n        assert_eq!(MaxInflight(22), tracer.max_inflight());\n        assert_eq!(Sequence(35000), tracer.initial_sequence());\n        assert_eq!(PortDirection::FixedSrc(Port(8080)), tracer.port_direction());\n        assert_eq!(Duration::from_millis(500), tracer.min_round_duration());\n        assert_eq!(Duration::from_millis(1500), tracer.max_round_duration());\n    }\n\n    #[test]\n    fn test_zero_max_rounds() {\n        let tracer = Builder::new(IpAddr::from([1, 2, 3, 4]))\n            .max_rounds(Some(0))\n            .build()\n            .unwrap();\n        assert_eq!(None, tracer.max_rounds());\n    }\n\n    #[test]\n    fn test_invalid_initial_sequence() {\n        let err = Builder::new(IpAddr::from([1, 2, 3, 4]))\n            .initial_sequence(u16::MAX)\n            .build()\n            .unwrap_err();\n        assert!(matches!(err, Error::BadConfig(s) if s == \"initial_sequence 65535 > 64511\"));\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/config.rs",
    "content": "use crate::types::Port;\nuse crate::{\n    MaxInflight, MaxRounds, PacketSize, PayloadPattern, Sequence, TimeToLive, TraceId,\n    TypeOfService,\n};\nuse std::fmt::{Display, Formatter};\nuse std::net::{IpAddr, Ipv4Addr};\nuse std::time::Duration;\n\n/// Default values for configuration.\npub mod defaults {\n    use crate::config::IcmpExtensionParseMode;\n    use crate::{MultipathStrategy, PrivilegeMode, Protocol};\n    use std::time::Duration;\n\n    /// The default value for `unprivileged`.\n    pub const DEFAULT_PRIVILEGE_MODE: PrivilegeMode = PrivilegeMode::Privileged;\n\n    /// The default value for `protocol`.\n    pub const DEFAULT_STRATEGY_PROTOCOL: Protocol = Protocol::Icmp;\n\n    /// The default value for `multipath-strategy`.\n    pub const DEFAULT_STRATEGY_MULTIPATH: MultipathStrategy = MultipathStrategy::Classic;\n\n    /// The default value for `icmp-extensions`.\n    pub const DEFAULT_ICMP_EXTENSION_PARSE_MODE: IcmpExtensionParseMode =\n        IcmpExtensionParseMode::Disabled;\n\n    /// The default value for `max-inflight`.\n    pub const DEFAULT_STRATEGY_MAX_INFLIGHT: u8 = 24;\n\n    /// The default value for `first-ttl`.\n    pub const DEFAULT_STRATEGY_FIRST_TTL: u8 = 1;\n\n    /// The default value for `max-ttl`.\n    pub const DEFAULT_STRATEGY_MAX_TTL: u8 = 64;\n\n    /// The default value for `packet-size`.\n    pub const DEFAULT_STRATEGY_PACKET_SIZE: u16 = 84;\n\n    /// The default value for `payload-pattern`.\n    pub const DEFAULT_STRATEGY_PAYLOAD_PATTERN: u8 = 0;\n\n    /// The default value for `min-round-duration`.\n    pub const DEFAULT_STRATEGY_MIN_ROUND_DURATION: Duration = Duration::from_millis(1000);\n\n    /// The default value for `max-round-duration`.\n    pub const DEFAULT_STRATEGY_MAX_ROUND_DURATION: Duration = Duration::from_millis(1000);\n\n    /// The default value for `initial-sequence`.\n    pub const DEFAULT_STRATEGY_INITIAL_SEQUENCE: u16 = 33434;\n\n    /// The default value for `tos`.\n    pub const DEFAULT_STRATEGY_TOS: u8 = 0;\n\n    /// The default value for `read-timeout`.\n    pub const DEFAULT_STRATEGY_READ_TIMEOUT: Duration = Duration::from_millis(10);\n\n    /// The default value for `grace-duration`.\n    pub const DEFAULT_STRATEGY_GRACE_DURATION: Duration = Duration::from_millis(100);\n\n    /// The default TCP connect timeout.\n    pub const DEFAULT_STRATEGY_TCP_CONNECT_TIMEOUT: Duration = Duration::from_millis(1000);\n\n    /// The default value for `max-samples`.\n    pub const DEFAULT_MAX_SAMPLES: usize = 256;\n\n    /// The default value for `max-flows`.\n    pub const DEFAULT_MAX_FLOWS: usize = 64;\n}\n\n/// The privilege mode.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum PrivilegeMode {\n    /// Privileged mode.\n    Privileged,\n    /// Unprivileged mode.\n    Unprivileged,\n}\n\nimpl PrivilegeMode {\n    #[must_use]\n    pub const fn is_unprivileged(self) -> bool {\n        match self {\n            Self::Privileged => false,\n            Self::Unprivileged => true,\n        }\n    }\n}\n\nimpl Display for PrivilegeMode {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Privileged => write!(f, \"privileged\"),\n            Self::Unprivileged => write!(f, \"unprivileged\"),\n        }\n    }\n}\n\n/// The ICMP extension parsing mode.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum IcmpExtensionParseMode {\n    /// Do not parse ICMP extensions.\n    Disabled,\n    /// Parse ICMP extensions.\n    Enabled,\n}\n\nimpl IcmpExtensionParseMode {\n    #[must_use]\n    pub const fn is_enabled(self) -> bool {\n        match self {\n            Self::Disabled => false,\n            Self::Enabled => true,\n        }\n    }\n}\n\nimpl Display for IcmpExtensionParseMode {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Disabled => write!(f, \"disabled\"),\n            Self::Enabled => write!(f, \"enabled\"),\n        }\n    }\n}\n\n/// The tracing protocol.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum Protocol {\n    /// Internet Control Message Protocol\n    Icmp,\n    /// User Datagram Protocol\n    Udp,\n    /// Transmission Control Protocol\n    Tcp,\n}\n\nimpl Display for Protocol {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Icmp => write!(f, \"icmp\"),\n            Self::Udp => write!(f, \"udp\"),\n            Self::Tcp => write!(f, \"tcp\"),\n        }\n    }\n}\n\n/// The [Equal-cost Multi-Path](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing) routing strategy.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum MultipathStrategy {\n    /// The src or dest port is used to store the sequence number.\n    ///\n    /// This does _not_ allow fixing both the src and dest port and so `PortDirection::Both` and\n    /// `SequenceField::Port` are mutually exclusive.\n    Classic,\n    /// The UDP `checksum` field is used to store the sequence number.\n    ///\n    /// a.k.a. [`paris`](https://github.com/libparistraceroute/libparistraceroute/wiki/Checksum) traceroute approach.\n    ///\n    /// This requires that the UDP payload contains a well-chosen value to ensure the UDP checksum\n    /// remains valid for the packet and therefore this cannot be used along with a custom\n    /// payload pattern.\n    Paris,\n    /// The IP `identifier` field is used to store the sequence number.\n    ///\n    /// a.k.a. [`dublin`](https://github.com/insomniacslk/dublin-traceroute) traceroute approach.\n    ///\n    /// The allow either the src or dest or both ports to be fixed.\n    ///\n    /// If either of the src or dest port may vary (i.e. `PortDirection::FixedSrc` or\n    /// `PortDirection::FixedDest`) then the port number is set to be the `initial_sequence`\n    /// plus the round number to ensure that there is a fixed `flowid` (protocol, src ip/port,\n    /// dest ip/port) for all packets in a given tracing round.  Each round may\n    /// therefore discover different paths.\n    ///\n    /// If both src and dest ports are fixed (i.e. `PortDirection::FixedBoth`) then every packet in\n    /// every round will share the same `flowid` and thus only a single path will be\n    /// discovered.\n    Dublin,\n}\n\nimpl Display for MultipathStrategy {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Classic => write!(f, \"classic\"),\n            Self::Paris => write!(f, \"paris\"),\n            Self::Dublin => write!(f, \"dublin\"),\n        }\n    }\n}\n\n/// Whether to fix the src, dest or both ports for a trace.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum PortDirection {\n    /// Trace without any source or destination port (i.e. for ICMP tracing).\n    None,\n    /// Trace from a fixed source port to a variable destination port (i.e. 5000 -> *).\n    ///\n    /// This is the default direction for UDP tracing.\n    FixedSrc(Port),\n    /// Trace from a variable source port to a fixed destination port (i.e. * -> 80).\n    ///\n    /// This is the default direction for TCP tracing.\n    FixedDest(Port),\n    /// Trace from a fixed source port to a fixed destination port (i.e. 5000 -> 80).\n    ///\n    /// When both ports are fixed another element of the IP header is required to vary per probe\n    /// such that probes can be identified.  Typically, this is only used for UDP, whereby the\n    /// checksum is manipulated by adjusting the payload and therefore used as the identifier.\n    ///\n    /// Note that this case is not currently implemented.\n    FixedBoth(Port, Port),\n}\n\nimpl PortDirection {\n    #[must_use]\n    pub const fn new_fixed_src(src: u16) -> Self {\n        Self::FixedSrc(Port(src))\n    }\n\n    #[must_use]\n    pub const fn new_fixed_dest(dest: u16) -> Self {\n        Self::FixedDest(Port(dest))\n    }\n\n    #[must_use]\n    pub const fn new_fixed_both(src: u16, dest: u16) -> Self {\n        Self::FixedBoth(Port(src), Port(dest))\n    }\n\n    #[must_use]\n    pub const fn src(&self) -> Option<Port> {\n        match *self {\n            Self::FixedSrc(src) | Self::FixedBoth(src, _) => Some(src),\n            _ => None,\n        }\n    }\n    #[must_use]\n    pub const fn dest(&self) -> Option<Port> {\n        match *self {\n            Self::FixedDest(dest) | Self::FixedBoth(_, dest) => Some(dest),\n            _ => None,\n        }\n    }\n}\n\n/// Tracer state configuration.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub struct StateConfig {\n    /// The maximum number of samples to record per hop.\n    ///\n    /// Once the maximum number of samples has been reached the oldest sample\n    /// is discarded (FIFO).\n    pub max_samples: usize,\n    /// The maximum number of flows to record.\n    ///\n    /// Once the maximum number of flows has been reached no new flows will be\n    /// created, existing flows are updated and are never removed.\n    pub max_flows: usize,\n}\n\nimpl Default for StateConfig {\n    fn default() -> Self {\n        Self {\n            max_samples: defaults::DEFAULT_MAX_SAMPLES,\n            max_flows: defaults::DEFAULT_MAX_FLOWS,\n        }\n    }\n}\n\n/// Tracer network channel configuration.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub struct ChannelConfig {\n    pub privilege_mode: PrivilegeMode,\n    pub protocol: Protocol,\n    pub source_addr: IpAddr,\n    pub target_addr: IpAddr,\n    pub packet_size: PacketSize,\n    pub payload_pattern: PayloadPattern,\n    pub initial_sequence: Sequence,\n    pub tos: TypeOfService,\n    pub icmp_extension_parse_mode: IcmpExtensionParseMode,\n    pub read_timeout: Duration,\n    pub tcp_connect_timeout: Duration,\n}\n\nimpl Default for ChannelConfig {\n    fn default() -> Self {\n        Self {\n            privilege_mode: defaults::DEFAULT_PRIVILEGE_MODE,\n            protocol: defaults::DEFAULT_STRATEGY_PROTOCOL,\n            source_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED),\n            target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED),\n            packet_size: PacketSize(defaults::DEFAULT_STRATEGY_PACKET_SIZE),\n            payload_pattern: PayloadPattern(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN),\n            initial_sequence: Sequence(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE),\n            tos: TypeOfService(defaults::DEFAULT_STRATEGY_TOS),\n            icmp_extension_parse_mode: defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE,\n            read_timeout: defaults::DEFAULT_STRATEGY_READ_TIMEOUT,\n            tcp_connect_timeout: defaults::DEFAULT_STRATEGY_TCP_CONNECT_TIMEOUT,\n        }\n    }\n}\n\n/// Tracing strategy configuration.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub struct StrategyConfig {\n    pub target_addr: IpAddr,\n    pub protocol: Protocol,\n    pub trace_identifier: TraceId,\n    pub max_rounds: Option<MaxRounds>,\n    pub first_ttl: TimeToLive,\n    pub max_ttl: TimeToLive,\n    pub grace_duration: Duration,\n    pub max_inflight: MaxInflight,\n    pub initial_sequence: Sequence,\n    pub multipath_strategy: MultipathStrategy,\n    pub port_direction: PortDirection,\n    pub min_round_duration: Duration,\n    pub max_round_duration: Duration,\n}\n\nimpl Default for StrategyConfig {\n    fn default() -> Self {\n        Self {\n            target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED),\n            protocol: defaults::DEFAULT_STRATEGY_PROTOCOL,\n            trace_identifier: TraceId::default(),\n            max_rounds: None,\n            first_ttl: TimeToLive(defaults::DEFAULT_STRATEGY_FIRST_TTL),\n            max_ttl: TimeToLive(defaults::DEFAULT_STRATEGY_MAX_TTL),\n            grace_duration: defaults::DEFAULT_STRATEGY_GRACE_DURATION,\n            max_inflight: MaxInflight(defaults::DEFAULT_STRATEGY_MAX_INFLIGHT),\n            initial_sequence: Sequence(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE),\n            multipath_strategy: defaults::DEFAULT_STRATEGY_MULTIPATH,\n            port_direction: PortDirection::None,\n            min_round_duration: defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION,\n            max_round_duration: defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/constants.rs",
    "content": "/// The maximum time-to-live value allowed.\n///\n/// The IP `ttl` is an u8 (0..255) but since a `ttl` of zero isn't useful we only allow 254 distinct\n/// hops (1..255).\npub const MAX_TTL: u8 = 254;\n\n/// The maximum number of sequence numbers allowed per round.\n///\n/// This is set to be far larger than the `MAX_TTL` to allow for the re-issue of probes (with the\n/// next sequence number, but the same ttl) which can occur for some protocols such as TCP when it\n/// cannot bind to a given port.\npub const MAX_SEQUENCE_PER_ROUND: u16 = 512;\n\n/// The maximum _starting_ sequence number allowed.\n///\n/// This ensures that there are sufficient sequence numbers available for at least _two_ rounds.  We\n/// require two rounds to ensure that delayed probe responses from the immediate prior round can be\n/// detected and excluded.\npub const MAX_INITIAL_SEQUENCE: u16 = u16::MAX - (MAX_SEQUENCE_PER_ROUND * 2);\n"
  },
  {
    "path": "crates/trippy-core/src/error.rs",
    "content": "use std::fmt::{Display, Formatter};\nuse std::io;\nuse std::net::{IpAddr, SocketAddr};\nuse thiserror::Error;\n\n/// A tracer error result.\npub type Result<T> = std::result::Result<T, Error>;\n\n/// A tracer error.\n#[derive(Error, Debug)]\npub enum Error {\n    #[error(\"invalid packet size: {0}\")]\n    InvalidPacketSize(usize),\n    #[error(\"invalid packet: {0}\")]\n    PacketError(#[from] trippy_packet::error::Error),\n    #[error(\"unknown interface: {0}\")]\n    UnknownInterface(String),\n    #[error(\"invalid config: {0}\")]\n    BadConfig(String),\n    #[error(\"IO error: {0}\")]\n    IoError(#[from] IoError),\n    #[error(\"Probe failed to send: {0}\")]\n    ProbeFailed(IoError),\n    #[error(\"insufficient buffer capacity\")]\n    InsufficientCapacity,\n    #[error(\"address {0} in use\")]\n    AddressInUse(SocketAddr),\n    #[error(\"source IP address {0} could not be bound\")]\n    InvalidSourceAddr(IpAddr),\n    #[error(\"missing address from socket call\")]\n    MissingAddr,\n    #[error(\"connect callback error: {0}\")]\n    PrivilegeError(#[from] trippy_privilege::Error),\n    #[error(\"tracer error: {0}\")]\n    Other(String),\n}\n\n/// Custom IO error result.\npub type IoResult<T> = std::result::Result<T, IoError>;\n\n/// Custom IO error.\n#[derive(Error, Debug)]\npub enum IoError {\n    #[error(\"Bind error for {1}: {0}\")]\n    Bind(io::Error, SocketAddr),\n    #[error(\"Connect error for {1}: {0}\")]\n    Connect(io::Error, SocketAddr),\n    #[error(\"Sendto error for {1}: {0}\")]\n    SendTo(io::Error, SocketAddr),\n    #[error(\"Failed to {0}: {1}\")]\n    Other(io::Error, IoOperation),\n}\n\nimpl IoError {\n    /// Get the custom error kind.\n    pub fn kind(&self) -> ErrorKind {\n        match self {\n            Self::Bind(e, _) | Self::Connect(e, _) | Self::SendTo(e, _) | Self::Other(e, _) => {\n                ErrorKind::from(e)\n            }\n        }\n    }\n}\n\n/// Custom error kind.\n///\n/// This includes additional error kinds that are not part of the standard [`io::ErrorKind`].\n#[derive(Debug, Eq, PartialEq)]\npub enum ErrorKind {\n    InProgress,\n    HostUnreachable,\n    NetUnreachable,\n    Std(io::ErrorKind),\n}\n\n/// Io operation.\n#[derive(Debug)]\npub enum IoOperation {\n    NewSocket,\n    SetNonBlocking,\n    Select,\n    RecvFrom,\n    Read,\n    Shutdown,\n    LocalAddr,\n    PeerAddr,\n    TakeError,\n    SetTos,\n    SetTclassV6,\n    SetTtl,\n    SetReusePort,\n    SetHeaderIncluded,\n    SetUnicastHopsV6,\n    WSACreateEvent,\n    WSARecvFrom,\n    WSAEventSelect,\n    WSAResetEvent,\n    WSAGetOverlappedResult,\n    WaitForSingleObject,\n    SetTcpFailConnectOnIcmpError,\n    TcpIcmpErrorInfo,\n    ConvertSocketAddress,\n    SioRoutingInterfaceQuery,\n    Startup,\n}\n\nimpl Display for IoOperation {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::NewSocket => write!(f, \"create new socket\"),\n            Self::SetNonBlocking => write!(f, \"set non-blocking\"),\n            Self::Select => write!(f, \"select\"),\n            Self::RecvFrom => write!(f, \"recv from\"),\n            Self::Read => write!(f, \"read\"),\n            Self::Shutdown => write!(f, \"shutdown\"),\n            Self::LocalAddr => write!(f, \"local addr\"),\n            Self::PeerAddr => write!(f, \"peer addr\"),\n            Self::TakeError => write!(f, \"take error\"),\n            Self::SetTos => write!(f, \"set TOS\"),\n            Self::SetTclassV6 => write!(f, \"set TCLASS v6\"),\n            Self::SetTtl => write!(f, \"set TTL\"),\n            Self::SetReusePort => write!(f, \"set reuse port\"),\n            Self::SetHeaderIncluded => write!(f, \"set header included\"),\n            Self::SetUnicastHopsV6 => write!(f, \"set unicast hops v6\"),\n            Self::WSACreateEvent => write!(f, \"WSA create event\"),\n            Self::WSARecvFrom => write!(f, \"WSA recv from\"),\n            Self::WSAEventSelect => write!(f, \"WSA event select\"),\n            Self::WSAResetEvent => write!(f, \"WSA reset event\"),\n            Self::WSAGetOverlappedResult => write!(f, \"WSA get overlapped result\"),\n            Self::WaitForSingleObject => write!(f, \"wait for single object\"),\n            Self::SetTcpFailConnectOnIcmpError => write!(f, \"set TCP failed connect on ICMP error\"),\n            Self::TcpIcmpErrorInfo => write!(f, \"get TCP ICMP error info\"),\n            Self::ConvertSocketAddress => write!(f, \"convert socket address\"),\n            Self::SioRoutingInterfaceQuery => write!(f, \"SIO routing interface query\"),\n            Self::Startup => write!(f, \"startup\"),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/flows.rs",
    "content": "use derive_more::{Add, AddAssign, Sub, SubAssign};\nuse itertools::{EitherOrBoth, Itertools};\nuse std::fmt::{Debug, Display, Formatter};\nuse std::net::IpAddr;\nuse tracing::instrument;\n\n/// Identifies a tracing `Flow`.\n#[derive(\n    Debug,\n    Clone,\n    Copy,\n    Default,\n    Ord,\n    PartialOrd,\n    Eq,\n    PartialEq,\n    Hash,\n    Add,\n    AddAssign,\n    Sub,\n    SubAssign,\n)]\npub struct FlowId(pub u64);\n\nimpl Display for FlowId {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.0)\n    }\n}\n\n/// A register of tracing `Flows`.\n#[derive(Debug, Clone, Default)]\npub struct FlowRegistry {\n    /// The id to assign to the next flow registered.\n    next_flow_id: FlowId,\n    /// The registry of flows observed.\n    flows: Vec<(Flow, FlowId)>,\n}\n\nimpl FlowRegistry {\n    /// Create a new `FlowRegistry`.\n    pub const fn new() -> Self {\n        Self {\n            flows: Vec::new(),\n            next_flow_id: FlowId(1),\n        }\n    }\n\n    /// Register a `Flow` with the `FlowRegistry`.\n    ///\n    /// If the flow matches a flow that has previously been observed by the registry then\n    /// the id of that flow is return.  Otherwise, a new flow id is created and\n    /// returned and the corresponding flow is stored in the registry.\n    ///\n    /// If the flow matches but also contains additional data not previously\n    /// observed for that flow then the existing flow will be updated to\n    /// merge the data.  In this case the existing flow id will be reused.\n    ///\n    /// If a flow matches more than one existing flow then only the first\n    /// matching flow will be updated.\n    #[instrument(skip(self), level = \"trace\")]\n    pub fn register(&mut self, flow: Flow) -> FlowId {\n        for (entry, id) in &mut self.flows {\n            let status = entry.check(&flow);\n            match status {\n                CheckStatus::Match => {\n                    return *id;\n                }\n                CheckStatus::NoMatch => {}\n                CheckStatus::MatchMerge => {\n                    entry.merge(&flow);\n                    return *id;\n                }\n            }\n        }\n        let flow_id = self.next_flow_id;\n        self.flows.push((flow, flow_id));\n        self.next_flow_id.0 += 1;\n        flow_id\n    }\n\n    /// All recorded flows.\n    pub fn flows(&self) -> &[(Flow, FlowId)] {\n        &self.flows\n    }\n}\n\n/// Represents a single tracing path over a number of (possibly unknown) hops.\n#[derive(Debug, Clone, Eq, PartialEq, Hash)]\npub struct Flow {\n    pub entries: Vec<FlowEntry>,\n}\n\nimpl Flow {\n    /// Create a new Flow from a slice of hops.\n    ///\n    /// Note that each entry is implicitly associated with a `ttl`.  For\n    /// example `hops[0]` would have a `ttl` of 1, `hops[1]` would have a\n    /// `ttl` of 2 and so on.\n    pub fn from_hops(hops: impl IntoIterator<Item = Option<IpAddr>>) -> Self {\n        let entries = hops\n            .into_iter()\n            .map(|addr| {\n                if let Some(addr) = addr {\n                    FlowEntry::Known(addr)\n                } else {\n                    FlowEntry::Unknown\n                }\n            })\n            .collect();\n        Self { entries }\n    }\n\n    /// Check if a given `Flow` matches this `Flow`.\n    ///\n    /// Two flows are said to match _unless_ they contain different IP\n    /// addresses for the _same_ position (i.e. the same `ttl`).\n    ///\n    /// This is true even for flows of differing lengths.\n    ///\n    /// In the even of a match, if the flow being checked contains\n    /// `FlowEntry::Known` entries which are `FlowEntry::Unknown` in the\n    /// current flow then `CheckStatus::MatchMerge` is returned to indicate\n    /// the two flows should be merged.\n    ///\n    /// This will also be the case if the flow being checked matches and is\n    /// longer than the existing flow.\n    #[instrument(skip(self), level = \"trace\")]\n    pub fn check(&self, flow: &Self) -> CheckStatus {\n        let mut additions = 0;\n        for (old, new) in self.entries.iter().zip(&flow.entries) {\n            match (old, new) {\n                (FlowEntry::Known(fst), FlowEntry::Known(snd)) if fst != snd => {\n                    return CheckStatus::NoMatch;\n                }\n                (FlowEntry::Unknown, FlowEntry::Known(_)) => additions += 1,\n                _ => {}\n            }\n        }\n        if flow.entries.len() > self.entries.len() || additions > 0 {\n            CheckStatus::MatchMerge\n        } else {\n            CheckStatus::Match\n        }\n    }\n\n    /// Marge the entries from the given `Flow` into our `Flow`.\n    #[instrument(skip(self), level = \"trace\")]\n    fn merge(&mut self, flow: &Self) {\n        self.entries = self\n            .entries\n            .iter()\n            .zip_longest(flow.entries.iter())\n            .map(|eob| match eob {\n                EitherOrBoth::Both(left, right) => match (left, right) {\n                    (FlowEntry::Unknown, FlowEntry::Known(_)) => *right,\n                    _ => *left,\n                },\n                EitherOrBoth::Left(left) => *left,\n                EitherOrBoth::Right(right) => *right,\n            })\n            .collect::<Vec<_>>();\n    }\n}\n\nimpl Display for Flow {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.entries.iter().format(\", \"))\n    }\n}\n\n/// The result of a `Flow` comparison check.\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum CheckStatus {\n    /// The flows match.\n    Match,\n    /// The flows do not match.\n    NoMatch,\n    /// The flows match but should be merged.\n    MatchMerge,\n}\n\n/// An entry in a `Flow`.\n#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]\npub enum FlowEntry {\n    /// An unknown flow entry.\n    Unknown,\n    /// A known flow entry with an `IpAddr`.\n    Known(IpAddr),\n}\n\nimpl Display for FlowEntry {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Unknown => f.write_str(\"*\"),\n            Self::Known(addr) => {\n                write!(f, \"{addr}\")\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::net::Ipv4Addr;\n    use std::str::FromStr;\n\n    #[test]\n    fn test_single_flow() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow_id = registry.register(flow1);\n        assert_eq!(FlowId(1), flow_id);\n        assert_eq!(\n            &[(Flow::from_hops([addr(\"1.1.1.1\")]), FlowId(1))],\n            registry.flows()\n        );\n    }\n\n    #[test]\n    fn test_two_different_flows() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow1_id = registry.register(flow1.clone());\n        let flow2 = Flow::from_hops([addr(\"2.2.2.2\")]);\n        let flow2_id = registry.register(flow2.clone());\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(2), flow2_id);\n        assert_eq!(&[(flow1, flow1_id), (flow2, flow2_id)], registry.flows());\n    }\n\n    #[test]\n    fn test_two_same_flows() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow1_id = registry.register(flow1.clone());\n        let flow2 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow2_id = registry.register(flow2);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(1), flow2_id);\n        assert_eq!(&[(flow1, flow1_id)], registry.flows());\n    }\n\n    #[test]\n    fn test_two_same_one_different_flows() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow1_id = registry.register(flow1.clone());\n        let flow2 = Flow::from_hops([addr(\"2.2.2.2\")]);\n        let flow2_id = registry.register(flow2.clone());\n        let flow3 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow3_id = registry.register(flow3);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(2), flow2_id);\n        assert_eq!(FlowId(1), flow3_id);\n        assert_eq!(&[(flow1, flow1_id), (flow2, flow2_id)], registry.flows());\n    }\n\n    #[test]\n    fn test_merge_flow1() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow1_id = registry.register(flow1);\n        let flow2 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\")]);\n        let flow2_id = registry.register(flow2);\n        let flow3 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\")]);\n        let flow3_id = registry.register(flow3);\n        let flow4 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"3.3.3.3\")]);\n        let flow4_id = registry.register(flow4);\n        let flow5 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow5_id = registry.register(flow5);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(1), flow2_id);\n        assert_eq!(FlowId(1), flow3_id);\n        assert_eq!(FlowId(2), flow4_id);\n        assert_eq!(FlowId(1), flow5_id);\n    }\n\n    #[test]\n    fn test_merge_flow2() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\"), addr(\"3.3.3.3\")]);\n        let flow1_id = registry.register(flow1);\n        let flow2 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\")]);\n        let flow2_id = registry.register(flow2);\n        let flow3 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\")]);\n        let flow3_id = registry.register(flow3);\n        let flow4 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\"), addr(\"3.3.3.3\")]);\n        let flow4_id = registry.register(flow4);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(1), flow2_id);\n        assert_eq!(FlowId(1), flow3_id);\n        assert_eq!(FlowId(1), flow4_id);\n    }\n\n    #[test]\n    fn test_merge_flow3() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\"), None, addr(\"3.3.3.3\")]);\n        let flow1_id = registry.register(flow1);\n        // doesn't match so new flow\n        let flow2 = Flow::from_hops([addr(\"2.2.2.2\")]);\n        let flow2_id = registry.register(flow2);\n        // matches and replaces flow 0\n        let flow3 = Flow::from_hops([\n            None,\n            addr(\"2.2.2.2\"),\n            None,\n            addr(\"4.4.4.4\"),\n            addr(\"5.5.5.5\"),\n        ]);\n        let flow3_id = registry.register(flow3);\n        // still matches flow 1\n        let flow4 = Flow::from_hops([addr(\"2.2.2.2\")]);\n        let flow4_id = registry.register(flow4);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(2), flow2_id);\n        assert_eq!(FlowId(1), flow3_id);\n        assert_eq!(FlowId(2), flow4_id);\n    }\n\n    #[test]\n    fn test_subset() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\")]);\n        let flow1_id = registry.register(flow1);\n        let flow2 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow2_id = registry.register(flow2);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(1), flow2_id);\n    }\n\n    #[test]\n    fn test_subset_any() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\")]);\n        let flow1_id = registry.register(flow1);\n        let flow2 = Flow::from_hops([addr(\"1.1.1.1\"), None]);\n        let flow2_id = registry.register(flow2);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(1), flow2_id);\n    }\n\n    #[test]\n    fn test_superset() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\")]);\n        let flow1_id = registry.register(flow1);\n        let flow2 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\")]);\n        let flow2_id = registry.register(flow2);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(1), flow2_id);\n    }\n\n    #[test]\n    fn test_superset_any() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([addr(\"1.1.1.1\"), None]);\n        let flow1_id = registry.register(flow1);\n        let flow2 = Flow::from_hops([addr(\"1.1.1.1\"), addr(\"2.2.2.2\")]);\n        let flow2_id = registry.register(flow2);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(1), flow2_id);\n    }\n\n    #[test]\n    fn test_start_any_then_same_flows() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([None, addr(\"1.1.1.1\")]);\n        let flow1_id = registry.register(flow1);\n        let flow2 = Flow::from_hops([None, addr(\"1.1.1.1\")]);\n        let flow2_id = registry.register(flow2);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(1), flow2_id);\n    }\n\n    #[test]\n    fn test_start_any_then_diff_flows() {\n        let mut registry = FlowRegistry::new();\n        let flow1 = Flow::from_hops([None, addr(\"1.1.1.1\")]);\n        let flow1_id = registry.register(flow1);\n        let flow2 = Flow::from_hops([None, addr(\"2.2.2.2\")]);\n        let flow2_id = registry.register(flow2);\n        assert_eq!(FlowId(1), flow1_id);\n        assert_eq!(FlowId(2), flow2_id);\n    }\n\n    #[expect(clippy::unnecessary_wraps)]\n    fn addr(addr: &str) -> Option<IpAddr> {\n        Some(IpAddr::V4(Ipv4Addr::from_str(addr).unwrap()))\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/lib.rs",
    "content": "//! Trippy - A network tracing library.\n//!\n//! This crate provides the core network tracing facility used by the\n//! standalone [Trippy](https://trippy.rs) application.\n//!\n//! Note: the public API is not stable and is highly likely to change\n//! in the future.\n//!\n//! # Example\n//!\n//! The following example builds and runs a tracer with default configuration\n//! and prints out the tracing data for each round:\n//!\n//! ```no_run\n//! # fn main() -> anyhow::Result<()> {\n//! # use std::net::IpAddr;\n//! # use std::str::FromStr;\n//! use trippy_core::Builder;\n//!\n//! let addr = IpAddr::from_str(\"1.1.1.1\")?;\n//! Builder::new(addr)\n//!     .build()?\n//!     .run_with(|round| println!(\"{:?}\", round))?;\n//! # Ok(())\n//! # }\n//! ```\n//!\n//! The following example traces using the UDP protocol with the Dublin ECMP\n//! strategy with fixed src and dest ports.  It also operates in unprivileged\n//! mode (only supported on some platforms):\n//!\n//! ```no_run\n//! # fn main() -> anyhow::Result<()> {\n//! # use std::net::IpAddr;\n//! # use std::str::FromStr;\n//! use trippy_core::{Builder, MultipathStrategy, Port, PortDirection, PrivilegeMode, Protocol};\n//!\n//! let addr = IpAddr::from_str(\"1.1.1.1\")?;\n//! Builder::new(addr)\n//!     .privilege_mode(PrivilegeMode::Unprivileged)\n//!     .protocol(Protocol::Udp)\n//!     .multipath_strategy(MultipathStrategy::Dublin)\n//!     .port_direction(PortDirection::FixedBoth(Port(33434), Port(3500)))\n//!     .build()?\n//!     .run_with(|round| println!(\"{:?}\", round))?;\n//! # Ok(())\n//! # }\n//! ```\n//!\n//! # See Also\n//!\n//! - [`Builder`] - Build a [`Tracer`].\n//! - [`Tracer::run`] - Run the tracer on the current thread.\n//! - [`Tracer::run_with`] - Run the tracer with a custom round handler.\n//! - [`Tracer::spawn`] - Run the tracer on a new thread.\n//! - [`Tracer::spawn_with`] - Run the tracer on a new thread with a custom round handler.\n\nmod builder;\nmod config;\nmod constants;\nmod error;\nmod flows;\nmod net;\nmod probe;\nmod state;\nmod strategy;\nmod tracer;\nmod types;\n\nuse net::channel::Channel;\nuse net::source::SourceAddr;\n\npub use builder::Builder;\npub use config::{\n    IcmpExtensionParseMode, MultipathStrategy, PortDirection, PrivilegeMode, Protocol, defaults,\n};\npub use constants::MAX_TTL;\npub use error::Error;\npub use flows::{FlowEntry, FlowId};\npub use probe::{\n    Extension, Extensions, IcmpPacketType, MplsLabelStack, MplsLabelStackMember, Probe,\n    ProbeComplete, ProbeStatus, UnknownExtension,\n};\npub use state::{Hop, NatStatus, State};\npub use strategy::{CompletionReason, Round, Strategy};\npub use tracer::Tracer;\npub use types::{\n    Dscp, Ecn, Flags, MaxInflight, MaxRounds, PacketSize, PayloadPattern, Port, RoundId, Sequence,\n    TimeToLive, TraceId, TypeOfService,\n};\n"
  },
  {
    "path": "crates/trippy-core/src/net/channel.rs",
    "content": "use crate::config::ChannelConfig;\nuse crate::error::{Error, Result};\nuse crate::net::socket::Socket;\nuse crate::net::{Network, ipv4::Ipv4, ipv6::Ipv6, platform};\nuse crate::probe::{Probe, Response};\nuse crate::{Port, PrivilegeMode, Protocol};\nuse arrayvec::ArrayVec;\nuse std::net::IpAddr;\nuse std::time::{Duration, SystemTime};\nuse tracing::instrument;\n\n/// The maximum size of the IP packet we allow.\npub const MAX_PACKET_SIZE: usize = 1024;\n\n/// The maximum number of TCP probes we allow.\nconst MAX_TCP_PROBES: usize = 256;\n\n/// A channel for sending and receiving `Probe` packets.\npub struct Channel<S: Socket> {\n    protocol: Protocol,\n    read_timeout: Duration,\n    tcp_connect_timeout: Duration,\n    send_socket: Option<S>,\n    recv_socket: S,\n    tcp_probes: ArrayVec<TcpProbe<S>, MAX_TCP_PROBES>,\n    family_config: FamilyConfig,\n}\n\n/// The IP family configuration for the channel.\nenum FamilyConfig {\n    V4(Ipv4),\n    V6(Ipv6),\n}\n\nimpl<S: Socket> Channel<S> {\n    /// Create an `IcmpChannel`.\n    ///\n    /// This operation requires the `CAP_NET_RAW` capability on Linux.\n    #[instrument(skip_all, level = \"trace\")]\n    pub fn connect(config: &ChannelConfig) -> Result<Self> {\n        tracing::debug!(?config);\n        if usize::from(config.packet_size.0) > MAX_PACKET_SIZE {\n            return Err(Error::InvalidPacketSize(usize::from(config.packet_size.0)));\n        }\n        let raw = config.privilege_mode == PrivilegeMode::Privileged;\n        platform::startup()?;\n        let ipv4_length_order = platform::Ipv4ByteOrder::for_address(config.source_addr)?;\n        let send_socket = match config.protocol {\n            Protocol::Icmp => Some(make_icmp_send_socket(config.source_addr, raw)?),\n            Protocol::Udp => Some(make_udp_send_socket(config.source_addr, raw)?),\n            Protocol::Tcp => None,\n        };\n        let recv_socket = make_recv_socket(config.source_addr, raw)?;\n        let family_config = match (config.source_addr, config.target_addr) {\n            (IpAddr::V4(src_addr), IpAddr::V4(dest_addr)) => FamilyConfig::V4(Ipv4 {\n                src_addr,\n                dest_addr,\n                byte_order: ipv4_length_order,\n                packet_size: config.packet_size,\n                payload_pattern: config.payload_pattern,\n                privilege_mode: config.privilege_mode,\n                tos: config.tos,\n                protocol: config.protocol,\n                icmp_extension_mode: config.icmp_extension_parse_mode,\n            }),\n            (IpAddr::V6(src_addr), IpAddr::V6(dest_addr)) => FamilyConfig::V6(Ipv6 {\n                src_addr,\n                dest_addr,\n                packet_size: config.packet_size,\n                payload_pattern: config.payload_pattern,\n                privilege_mode: config.privilege_mode,\n                tos: config.tos,\n                protocol: config.protocol,\n                icmp_extension_mode: config.icmp_extension_parse_mode,\n                initial_sequence: config.initial_sequence,\n            }),\n            _ => unreachable!(),\n        };\n        Ok(Self {\n            protocol: config.protocol,\n            read_timeout: config.read_timeout,\n            tcp_connect_timeout: config.tcp_connect_timeout,\n            send_socket,\n            recv_socket,\n            tcp_probes: ArrayVec::new(),\n            family_config,\n        })\n    }\n}\n\nimpl<S: Socket> Network for Channel<S> {\n    #[instrument(skip(self), level = \"trace\")]\n    fn send_probe(&mut self, probe: Probe) -> Result<()> {\n        tracing::debug!(?probe);\n        match self.protocol {\n            Protocol::Icmp => self.dispatch_icmp_probe(&probe),\n            Protocol::Udp => self.dispatch_udp_probe(&probe),\n            Protocol::Tcp => self.dispatch_tcp_probe(&probe),\n        }\n    }\n    #[instrument(skip_all, level = \"trace\")]\n    fn recv_probe(&mut self) -> Result<Option<Response>> {\n        let prob_response = match self.protocol {\n            Protocol::Icmp | Protocol::Udp => self.recv_icmp_probe(),\n            Protocol::Tcp => match self.recv_tcp_sockets()? {\n                None => self.recv_icmp_probe(),\n                resp => Ok(resp),\n            },\n        }?;\n        if let Some(resp) = &prob_response {\n            tracing::debug!(?resp);\n        }\n        Ok(prob_response)\n    }\n}\n\nimpl<S: Socket> Channel<S> {\n    /// Dispatch a ICMP probe.\n    #[instrument(skip_all, level = \"trace\")]\n    fn dispatch_icmp_probe(&mut self, probe: &Probe) -> Result<()> {\n        match (&self.family_config, self.send_socket.as_mut()) {\n            (FamilyConfig::V4(ipv4), Some(socket)) => ipv4.dispatch_icmp_probe(socket, probe),\n            (FamilyConfig::V6(ipv6), Some(socket)) => ipv6.dispatch_icmp_probe(socket, probe),\n            _ => unreachable!(),\n        }\n    }\n\n    /// Dispatch a UDP probe.\n    #[instrument(skip_all, level = \"trace\")]\n    fn dispatch_udp_probe(&mut self, probe: &Probe) -> Result<()> {\n        match (&self.family_config, self.send_socket.as_mut()) {\n            (FamilyConfig::V4(ipv4), Some(socket)) => ipv4.dispatch_udp_probe(socket, probe),\n            (FamilyConfig::V6(ipv6), Some(socket)) => ipv6.dispatch_udp_probe(socket, probe),\n            _ => unreachable!(),\n        }\n    }\n\n    /// Dispatch a TCP probe.\n    #[instrument(skip_all, level = \"trace\")]\n    fn dispatch_tcp_probe(&mut self, probe: &Probe) -> Result<()> {\n        let socket = match &self.family_config {\n            FamilyConfig::V4(ipv4) => ipv4.dispatch_tcp_probe(probe),\n            FamilyConfig::V6(ipv6) => ipv6.dispatch_tcp_probe(probe),\n        }?;\n        self.tcp_probes.push(TcpProbe::new(\n            socket,\n            probe.src_port,\n            probe.dest_port,\n            SystemTime::now(),\n        ));\n        Ok(())\n    }\n\n    /// Generate a `ProbeResponse` for the next available ICMP packet, if any\n    #[instrument(skip(self), level = \"trace\")]\n    fn recv_icmp_probe(&mut self) -> Result<Option<Response>> {\n        if self.recv_socket.is_readable(self.read_timeout)? {\n            match &self.family_config {\n                FamilyConfig::V4(ipv4) => ipv4.recv_icmp_probe(&mut self.recv_socket),\n                FamilyConfig::V6(ipv6) => ipv6.recv_icmp_probe(&mut self.recv_socket),\n            }\n        } else {\n            Ok(None)\n        }\n    }\n\n    /// Generate synthetic `ProbeResponse` if a TCP socket is connected or if the connection was\n    /// refused.\n    ///\n    /// Any TCP socket which has not connected or failed after a timeout will be removed.\n    #[instrument(skip(self), level = \"trace\")]\n    fn recv_tcp_sockets(&mut self) -> Result<Option<Response>> {\n        self.tcp_probes\n            .retain(|probe| probe.start.elapsed().unwrap_or_default() < self.tcp_connect_timeout);\n        let found_index = self\n            .tcp_probes\n            .iter_mut()\n            .enumerate()\n            .find_map(|(index, probe)| {\n                if probe.socket.is_writable().unwrap_or_default() {\n                    Some(index)\n                } else {\n                    None\n                }\n            });\n        if let Some(i) = found_index {\n            let mut probe = self.tcp_probes.remove(i);\n            match &self.family_config {\n                FamilyConfig::V4(ipv4) => {\n                    ipv4.recv_tcp_socket(&mut probe.socket, probe.src_port, probe.dest_port)\n                }\n                FamilyConfig::V6(ipv6) => {\n                    ipv6.recv_tcp_socket(&mut probe.socket, probe.src_port, probe.dest_port)\n                }\n            }\n        } else {\n            Ok(None)\n        }\n    }\n}\n\n/// An entry in the TCP probes array.\nstruct TcpProbe<S: Socket> {\n    socket: S,\n    src_port: Port,\n    dest_port: Port,\n    start: SystemTime,\n}\n\nimpl<S: Socket> TcpProbe<S> {\n    pub const fn new(socket: S, src_port: Port, dest_port: Port, start: SystemTime) -> Self {\n        Self {\n            socket,\n            src_port,\n            dest_port,\n            start,\n        }\n    }\n}\n\n/// Make a socket for sending raw `ICMP` packets.\n#[instrument(level = \"trace\")]\nfn make_icmp_send_socket<S: Socket>(addr: IpAddr, raw: bool) -> Result<S> {\n    Ok(match addr {\n        IpAddr::V4(_) => S::new_icmp_send_socket_ipv4(raw),\n        IpAddr::V6(_) => S::new_icmp_send_socket_ipv6(raw),\n    }?)\n}\n\n/// Make a socket for sending `UDP` packets.\n#[instrument(level = \"trace\")]\nfn make_udp_send_socket<S: Socket>(addr: IpAddr, raw: bool) -> Result<S> {\n    Ok(match addr {\n        IpAddr::V4(_) => S::new_udp_send_socket_ipv4(raw),\n        IpAddr::V6(_) => S::new_udp_send_socket_ipv6(raw),\n    }?)\n}\n\n/// Make a socket for receiving raw `ICMP` packets.\n#[instrument(level = \"trace\")]\nfn make_recv_socket<S: Socket>(addr: IpAddr, raw: bool) -> Result<S> {\n    Ok(match addr {\n        IpAddr::V4(ipv4addr) => S::new_recv_socket_ipv4(ipv4addr, raw),\n        IpAddr::V6(ipv6addr) => S::new_recv_socket_ipv6(ipv6addr, raw),\n    }?)\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net/common.rs",
    "content": "use crate::error::ErrorKind;\nuse crate::error::{Error, Result};\nuse std::net::SocketAddr;\n\n/// Utility methods to map errors.\npub struct ErrorMapper;\n\nimpl ErrorMapper {\n    /// Convert [`ErrorKind::InProgress`] to [`Ok`].\n    pub fn in_progress(err: Error) -> Result<()> {\n        match err {\n            Error::IoError(io_err) => match io_err.kind() {\n                ErrorKind::InProgress => Ok(()),\n                _ => Err(Error::IoError(io_err)),\n            },\n            err => Err(err),\n        }\n    }\n\n    /// Convert [`io::ErrorKind::AddrInUse`] to [`Error::AddressInUse`].\n    #[must_use]\n    pub fn addr_in_use(err: Error, addr: SocketAddr) -> Error {\n        match err {\n            Error::IoError(io_err) => match io_err.kind() {\n                ErrorKind::Std(std::io::ErrorKind::AddrInUse) => Error::AddressInUse(addr),\n                _ => Error::IoError(io_err),\n            },\n            err => err,\n        }\n    }\n\n    /// Convert a given [`ErrorKind`] to [`Error::ProbeFailed`].\n    #[expect(clippy::needless_pass_by_value)]\n    pub fn probe_failed(err: Error, kind: ErrorKind) -> Error {\n        match err {\n            Error::IoError(io_err) if io_err.kind() == kind => Error::ProbeFailed(io_err),\n            _ => err,\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::error::IoError;\n    use std::io;\n    use std::net::{Ipv4Addr, SocketAddrV4};\n\n    const ADDR: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0));\n\n    #[test]\n    fn test_in_progress() {\n        let io_err = io::Error::from(ErrorKind::InProgress);\n        let err = Error::IoError(IoError::Bind(io_err, ADDR));\n        assert!(ErrorMapper::in_progress(err).is_ok());\n    }\n\n    #[test]\n    fn test_not_in_progress() {\n        let io_err = io::Error::from(ErrorKind::Std(io::ErrorKind::Other));\n        let err = Error::IoError(IoError::Bind(io_err, ADDR));\n        assert!(ErrorMapper::in_progress(err).is_err());\n    }\n\n    #[test]\n    fn test_addr_in_use() {\n        let io_err = io::Error::from(ErrorKind::Std(io::ErrorKind::AddrInUse));\n        let err = Error::IoError(IoError::Bind(io_err, ADDR));\n        let addr_in_use_err = ErrorMapper::addr_in_use(err, ADDR);\n        assert!(matches!(addr_in_use_err, Error::AddressInUse(ADDR)));\n    }\n\n    #[test]\n    fn test_not_addr_in_use() {\n        let io_err = io::Error::from(ErrorKind::Std(io::ErrorKind::Other));\n        let err = Error::IoError(IoError::Bind(io_err, ADDR));\n        let addr_in_use_err = ErrorMapper::addr_in_use(err, ADDR);\n        assert!(matches!(addr_in_use_err, Error::IoError(_)));\n    }\n\n    #[test]\n    fn test_probe_failed() {\n        let io_err = io::Error::from(ErrorKind::HostUnreachable);\n        let err = Error::IoError(IoError::Bind(io_err, ADDR));\n        let probe_err = ErrorMapper::probe_failed(err, ErrorKind::HostUnreachable);\n        assert!(matches!(probe_err, Error::ProbeFailed(_)));\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net/extension.rs",
    "content": "use crate::error::Error;\nuse crate::probe::{Extension, Extensions, MplsLabelStack, MplsLabelStackMember, UnknownExtension};\nuse trippy_packet::icmp_extension::extension_header::ExtensionHeaderPacket;\nuse trippy_packet::icmp_extension::extension_object::{ClassNum, ExtensionObjectPacket};\nuse trippy_packet::icmp_extension::extension_structure::ExtensionsPacket;\nuse trippy_packet::icmp_extension::mpls_label_stack::MplsLabelStackPacket;\nuse trippy_packet::icmp_extension::mpls_label_stack_member::MplsLabelStackMemberPacket;\n\n/// The supported ICMP extension version number.\nconst ICMP_EXTENSION_VERSION: u8 = 2;\n\nimpl TryFrom<&[u8]> for Extensions {\n    type Error = Error;\n\n    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {\n        Self::try_from(ExtensionsPacket::new_view(value)?)\n    }\n}\n\nimpl TryFrom<ExtensionsPacket<'_>> for Extensions {\n    type Error = Error;\n\n    fn try_from(value: ExtensionsPacket<'_>) -> Result<Self, Self::Error> {\n        let header = ExtensionHeaderPacket::new_view(value.header())?;\n        if header.get_version() != ICMP_EXTENSION_VERSION {\n            return Ok(Self::default());\n        }\n        let extensions = value\n            .objects()\n            .flat_map(ExtensionObjectPacket::new_view)\n            .map(|obj| match obj.get_class_num() {\n                ClassNum::MultiProtocolLabelSwitchingLabelStack => {\n                    MplsLabelStackPacket::new_view(obj.payload())\n                        .map(|mpls| Extension::Mpls(MplsLabelStack::from(mpls)))\n                }\n                _ => Ok(Extension::Unknown(UnknownExtension::from(obj))),\n            })\n            .collect::<Result<_, _>>()?;\n        Ok(Self { extensions })\n    }\n}\n\nimpl From<MplsLabelStackPacket<'_>> for MplsLabelStack {\n    fn from(value: MplsLabelStackPacket<'_>) -> Self {\n        Self {\n            members: value\n                .members()\n                .flat_map(MplsLabelStackMemberPacket::new_view)\n                .map(MplsLabelStackMember::from)\n                .collect(),\n        }\n    }\n}\n\nimpl From<MplsLabelStackMemberPacket<'_>> for MplsLabelStackMember {\n    fn from(value: MplsLabelStackMemberPacket<'_>) -> Self {\n        Self {\n            label: value.get_label(),\n            exp: value.get_exp(),\n            bos: value.get_bos(),\n            ttl: value.get_ttl(),\n        }\n    }\n}\n\nimpl From<ExtensionObjectPacket<'_>> for UnknownExtension {\n    fn from(value: ExtensionObjectPacket<'_>) -> Self {\n        Self {\n            class_num: value.get_class_num().id(),\n            class_subtype: value.get_class_subtype().0,\n            bytes: value.payload().to_owned(),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    /// Convert a single MPLS extension which contains two labels.\n    #[test]\n    fn test_convert_mpls_extensions() {\n        let buf = hex_literal::hex!(\"20 00 96 53 00 0c 01 01 06 9f 18 01 00 00 29 ff\");\n        let exts = Extensions::try_from(buf.as_slice()).unwrap();\n        assert_eq!(1, exts.extensions.len());\n        match &exts.extensions[0] {\n            Extension::Mpls(mpls) => {\n                assert_eq!(2, mpls.members.len());\n                assert_eq!(27121, mpls.members[0].label);\n                assert_eq!(1, mpls.members[0].ttl);\n                assert_eq!(4, mpls.members[0].exp);\n                assert_eq!(0, mpls.members[0].bos);\n                assert_eq!(2, mpls.members[1].label);\n                assert_eq!(255, mpls.members[1].ttl);\n                assert_eq!(4, mpls.members[1].exp);\n                assert_eq!(1, mpls.members[1].bos);\n            }\n            Extension::Unknown(_) => panic!(\"expected Extension::Mpls\"),\n        }\n    }\n\n    /// Convert a single unknown extension.\n    #[test]\n    fn test_convert_unknown_extensions() {\n        let buf = hex_literal::hex!(\"20 00 96 53 00 0c 99 01 06 9f 18 01 00 00 29 ff\");\n        let exts = Extensions::try_from(buf.as_slice()).unwrap();\n        assert_eq!(1, exts.extensions.len());\n        match &exts.extensions[0] {\n            Extension::Unknown(unknown) => {\n                assert_eq!(0x99, unknown.class_num);\n                assert_eq!(0x01, unknown.class_subtype);\n                assert_eq!(\n                    hex_literal::hex!(\"06 9f 18 01 00 00 29 ff\"),\n                    unknown.bytes.as_slice()\n                );\n            }\n            Extension::Mpls(_) => panic!(\"expected Extension::Unknown\"),\n        }\n    }\n\n    /// Convert an extension with an unknown header version.\n    #[test]\n    fn test_convert_unknown_version() {\n        let buf = hex_literal::hex!(\"30 00 96 53 00 0c 99 01 06 9f 18 01 00 00 29 ff\");\n        let exts = Extensions::try_from(buf.as_slice()).unwrap();\n        assert_eq!(0, exts.extensions.len());\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net/ipv4.rs",
    "content": "use crate::config::IcmpExtensionParseMode;\nuse crate::error::{Error, ErrorKind, Result};\nuse crate::net::channel::MAX_PACKET_SIZE;\nuse crate::net::common::ErrorMapper;\nuse crate::net::platform;\nuse crate::net::socket::{Socket, SocketError};\nuse crate::probe::{\n    Extensions, IcmpPacketCode, IcmpProtocolResponse, Probe, ProtocolResponse, Response,\n    ResponseData, TcpProtocolResponse, UdpProtocolResponse,\n};\nuse crate::types::{PacketSize, PayloadPattern, Sequence, TraceId, TypeOfService};\nuse crate::{Flags, Port, PrivilegeMode, Protocol};\nuse std::io;\nuse std::net::{IpAddr, Ipv4Addr, SocketAddr};\nuse std::time::SystemTime;\nuse tracing::instrument;\nuse trippy_packet::IpProtocol;\nuse trippy_packet::checksum::{icmp_ipv4_checksum, udp_ipv4_checksum};\nuse trippy_packet::icmpv4::destination_unreachable::DestinationUnreachablePacket;\nuse trippy_packet::icmpv4::echo_reply::EchoReplyPacket;\nuse trippy_packet::icmpv4::echo_request::EchoRequestPacket;\nuse trippy_packet::icmpv4::time_exceeded::TimeExceededPacket;\nuse trippy_packet::icmpv4::{IcmpCode, IcmpPacket, IcmpTimeExceededCode, IcmpType};\nuse trippy_packet::ipv4::Ipv4Packet;\nuse trippy_packet::tcp::TcpPacket;\nuse trippy_packet::udp::UdpPacket;\n\n/// The maximum size of UDP packet we allow.\nconst MAX_UDP_PACKET_BUF: usize = MAX_PACKET_SIZE - Ipv4Packet::minimum_packet_size();\n\n/// The maximum size of UDP payload we allow.\nconst MAX_UDP_PAYLOAD_BUF: usize = MAX_UDP_PACKET_BUF - UdpPacket::minimum_packet_size();\n\n/// The maximum size of ICMP packet we allow.\nconst MAX_ICMP_PACKET_BUF: usize = MAX_PACKET_SIZE - Ipv4Packet::minimum_packet_size();\n\n/// The maximum size of ICMP payload we allow.\nconst MAX_ICMP_PAYLOAD_BUF: usize = MAX_ICMP_PACKET_BUF - IcmpPacket::minimum_packet_size();\n\n/// The minimum size of ICMP packets we allow.\nconst MIN_PACKET_SIZE_ICMP: usize =\n    Ipv4Packet::minimum_packet_size() + IcmpPacket::minimum_packet_size();\n\n/// The minimum size of UDP packets we allow.\nconst MIN_PACKET_SIZE_UDP: usize =\n    Ipv4Packet::minimum_packet_size() + UdpPacket::minimum_packet_size();\n\n/// The value for the IPv4 `flags_and_fragment_offset` field to set the `Don't fragment` bit.\n///\n/// 0100 0000 0000 0000\nconst DONT_FRAGMENT: u16 = 0x4000;\n\n/// IPv4 configuration.\n#[derive(Debug)]\npub struct Ipv4 {\n    pub src_addr: Ipv4Addr,\n    pub dest_addr: Ipv4Addr,\n    pub byte_order: platform::Ipv4ByteOrder,\n    pub packet_size: PacketSize,\n    pub payload_pattern: PayloadPattern,\n    pub privilege_mode: PrivilegeMode,\n    pub tos: TypeOfService,\n    pub protocol: Protocol,\n    pub icmp_extension_mode: IcmpExtensionParseMode,\n}\n\nimpl Default for Ipv4 {\n    fn default() -> Self {\n        Self {\n            src_addr: Ipv4Addr::UNSPECIFIED,\n            dest_addr: Ipv4Addr::UNSPECIFIED,\n            byte_order: platform::Ipv4ByteOrder::Network,\n            packet_size: PacketSize(0),\n            payload_pattern: PayloadPattern(0),\n            privilege_mode: PrivilegeMode::Privileged,\n            tos: TypeOfService(0),\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n        }\n    }\n}\n\nimpl Ipv4 {\n    /// Dispatch an ICMP probe.\n    #[instrument(skip(self, icmp_send_socket), level = \"trace\")]\n    pub fn dispatch_icmp_probe<S: Socket>(\n        &self,\n        icmp_send_socket: &mut S,\n        probe: &Probe,\n    ) -> Result<()> {\n        let mut ipv4_buf = [0_u8; MAX_PACKET_SIZE];\n        let mut icmp_buf = [0_u8; MAX_ICMP_PACKET_BUF];\n        let packet_size = usize::from(self.packet_size.0);\n        if !(MIN_PACKET_SIZE_ICMP..=MAX_PACKET_SIZE).contains(&packet_size) {\n            return Err(Error::InvalidPacketSize(packet_size));\n        }\n        let echo_request = self.make_echo_request_icmp_packet(\n            &mut icmp_buf,\n            probe.identifier,\n            probe.sequence,\n            icmp_payload_size(packet_size),\n        )?;\n        let ipv4 = self.make_ipv4_packet(\n            &mut ipv4_buf,\n            IpProtocol::Icmp,\n            probe.ttl.0,\n            0,\n            echo_request.packet(),\n        )?;\n        let remote_addr = SocketAddr::new(IpAddr::V4(self.dest_addr), 0);\n        icmp_send_socket\n            .send_to(ipv4.packet(), remote_addr)\n            .map_err(Error::IoError)\n            .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::HostUnreachable))\n            .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::NetUnreachable))\n            .map_err(|err| ErrorMapper::probe_failed(err, INVALID_INPUT_KIND))?;\n        Ok(())\n    }\n\n    /// Dispatch a UDP probe.\n    #[instrument(skip(self, raw_send_socket), level = \"trace\")]\n    pub fn dispatch_udp_probe<S: Socket>(\n        &self,\n        raw_send_socket: &mut S,\n        probe: &Probe,\n    ) -> Result<()> {\n        let packet_size = usize::from(self.packet_size.0);\n        if !(MIN_PACKET_SIZE_UDP..=MAX_PACKET_SIZE).contains(&packet_size) {\n            return Err(Error::InvalidPacketSize(packet_size));\n        }\n        let payload_size = udp_payload_size(packet_size);\n        let payload = &[self.payload_pattern.0; MAX_UDP_PAYLOAD_BUF][0..payload_size];\n        match self.privilege_mode {\n            PrivilegeMode::Privileged => {\n                self.dispatch_udp_probe_raw(raw_send_socket, probe, payload)\n            }\n            PrivilegeMode::Unprivileged => self.dispatch_udp_probe_non_raw::<S>(probe, payload),\n        }\n    }\n\n    /// Dispatch a UDP probe using a raw socket with `IP_HDRINCL` set.\n    ///\n    /// As `IP_HDRINCL` is set we must supply the IP and UDP headers which allows us to set custom\n    /// values for certain fields such as the checksum as required by the Paris tracing strategy.\n    #[instrument(skip(self, raw_send_socket), level = \"trace\")]\n    fn dispatch_udp_probe_raw<S: Socket>(\n        &self,\n        raw_send_socket: &mut S,\n        probe: &Probe,\n        payload: &[u8],\n    ) -> Result<()> {\n        let mut ipv4_buf = [0_u8; MAX_PACKET_SIZE];\n        let mut udp_buf = [0_u8; MAX_UDP_PACKET_BUF];\n        let payload_paris = probe.sequence.0.to_be_bytes();\n        let payload = if probe.flags.contains(Flags::PARIS_CHECKSUM) {\n            payload_paris.as_slice()\n        } else {\n            payload\n        };\n        let mut udp =\n            self.make_udp_packet(&mut udp_buf, probe.src_port.0, probe.dest_port.0, payload)?;\n        if probe.flags.contains(Flags::PARIS_CHECKSUM) {\n            let checksum = udp.get_checksum().to_be_bytes();\n            let payload = u16::from_be_bytes(core::array::from_fn(|i| udp.payload()[i]));\n            udp.set_checksum(payload);\n            udp.set_payload(&checksum);\n        }\n        let ipv4 = self.make_ipv4_packet(\n            &mut ipv4_buf,\n            IpProtocol::Udp,\n            probe.ttl.0,\n            probe.identifier.0,\n            udp.packet(),\n        )?;\n        let remote_addr = SocketAddr::new(IpAddr::V4(self.dest_addr), probe.dest_port.0);\n        raw_send_socket\n            .send_to(ipv4.packet(), remote_addr)\n            .map_err(Error::IoError)\n            .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::HostUnreachable))\n            .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::NetUnreachable))?;\n        Ok(())\n    }\n\n    /// Dispatch a UDP probe using a new UDP datagram socket.\n    #[instrument(skip(self), level = \"trace\")]\n    fn dispatch_udp_probe_non_raw<S: Socket>(&self, probe: &Probe, payload: &[u8]) -> Result<()> {\n        let local_addr = SocketAddr::new(IpAddr::V4(self.src_addr), probe.src_port.0);\n        let remote_addr = SocketAddr::new(IpAddr::V4(self.dest_addr), probe.dest_port.0);\n        let mut socket = S::new_udp_send_socket_ipv4(false)?;\n        socket\n            .bind(local_addr)\n            .map_err(Error::IoError)\n            .or_else(ErrorMapper::in_progress)\n            .map_err(|err| ErrorMapper::addr_in_use(err, local_addr))\n            .map_err(|err| ErrorMapper::probe_failed(err, ADDR_NOT_AVAILABLE_KIND))?;\n        socket.set_ttl(u32::from(probe.ttl.0))?;\n        socket.set_tos(u32::from(self.tos.0))?;\n        socket.send_to(payload, remote_addr)?;\n        Ok(())\n    }\n\n    /// Dispatch a TCP probe.\n    #[instrument(skip(self), level = \"trace\")]\n    pub fn dispatch_tcp_probe<S: Socket>(&self, probe: &Probe) -> Result<S> {\n        let mut socket = S::new_stream_socket_ipv4()?;\n        let local_addr = SocketAddr::new(IpAddr::V4(self.src_addr), probe.src_port.0);\n        socket\n            .bind(local_addr)\n            .map_err(Error::IoError)\n            .or_else(ErrorMapper::in_progress)\n            .map_err(|err| ErrorMapper::addr_in_use(err, local_addr))\n            .map_err(|err| ErrorMapper::probe_failed(err, ADDR_NOT_AVAILABLE_KIND))?;\n        socket.set_ttl(u32::from(probe.ttl.0))?;\n        socket.set_tos(u32::from(self.tos.0))?;\n        let remote_addr = SocketAddr::new(IpAddr::V4(self.dest_addr), probe.dest_port.0);\n        socket\n            .connect(remote_addr)\n            .map_err(Error::IoError)\n            .or_else(ErrorMapper::in_progress)\n            .map_err(|err| ErrorMapper::addr_in_use(err, remote_addr))\n            .map_err(|err| ErrorMapper::probe_failed(err, ErrorKind::NetUnreachable))?;\n        Ok(socket)\n    }\n\n    /// Receive an ICMP probe response.\n    #[instrument(skip(self, recv_socket), level = \"trace\")]\n    pub fn recv_icmp_probe<S: Socket>(&self, recv_socket: &mut S) -> Result<Option<Response>> {\n        let mut buf = [0_u8; MAX_PACKET_SIZE];\n        match recv_socket.read(&mut buf) {\n            Ok(bytes_read) => {\n                let ipv4 = Ipv4Packet::new_view(&buf[..bytes_read])?;\n                Ok(self.extract_probe_resp(&ipv4)?)\n            }\n            Err(err) => match err.kind() {\n                ErrorKind::Std(io::ErrorKind::WouldBlock) => Ok(None),\n                _ => Err(Error::IoError(err)),\n            },\n        }\n    }\n\n    /// Receive a TCP probe response.\n    #[instrument(skip(self, tcp_socket), level = \"trace\")]\n    pub fn recv_tcp_socket<S: Socket>(\n        &self,\n        tcp_socket: &mut S,\n        src_port: Port,\n        dest_port: Port,\n    ) -> Result<Option<Response>> {\n        let proto_resp = ProtocolResponse::Tcp(TcpProtocolResponse::new(\n            IpAddr::V4(self.dest_addr),\n            src_port.0,\n            dest_port.0,\n            None,\n        ));\n        match tcp_socket.take_error()? {\n            None => {\n                let addr = tcp_socket.peer_addr()?.ok_or(Error::MissingAddr)?.ip();\n                tcp_socket.shutdown()?;\n                return Ok(Some(Response::TcpReply(ResponseData::new(\n                    SystemTime::now(),\n                    addr,\n                    proto_resp,\n                ))));\n            }\n            Some(err) => match err {\n                SocketError::ConnectionRefused => {\n                    return Ok(Some(Response::TcpRefused(ResponseData::new(\n                        SystemTime::now(),\n                        IpAddr::V4(self.dest_addr),\n                        proto_resp,\n                    ))));\n                }\n                SocketError::HostUnreachable => {\n                    let error_addr = tcp_socket.icmp_error_info()?;\n                    return Ok(Some(Response::TimeExceeded(\n                        ResponseData::new(SystemTime::now(), error_addr, proto_resp),\n                        IcmpPacketCode(1),\n                        None,\n                    )));\n                }\n                SocketError::Other(_) => {}\n            },\n        }\n        Ok(None)\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn extract_probe_resp(&self, ipv4: &Ipv4Packet<'_>) -> Result<Option<Response>> {\n        let recv = SystemTime::now();\n        let src = IpAddr::V4(ipv4.get_source());\n        let icmp_v4 = IcmpPacket::new_view(ipv4.payload())?;\n        let icmp_type = icmp_v4.get_icmp_type();\n        let icmp_code = icmp_v4.get_icmp_code();\n        Ok(match icmp_type {\n            IcmpType::TimeExceeded => {\n                if IcmpTimeExceededCode::from(icmp_code) == IcmpTimeExceededCode::TtlExpired {\n                    let packet = TimeExceededPacket::new_view(icmp_v4.packet())?;\n                    let (nested_ipv4, extension) = match self.icmp_extension_mode {\n                        IcmpExtensionParseMode::Enabled => {\n                            let ipv4 = Ipv4Packet::new_view(packet.payload())?;\n                            let ext = packet.extension().map(Extensions::try_from).transpose()?;\n                            (ipv4, ext)\n                        }\n                        IcmpExtensionParseMode::Disabled => {\n                            let ipv4 = Ipv4Packet::new_view(packet.payload_raw())?;\n                            (ipv4, None)\n                        }\n                    };\n                    self.extract_probe_proto_resp(&nested_ipv4)?\n                        .map(|proto_resp| {\n                            Response::TimeExceeded(\n                                ResponseData::new(recv, src, proto_resp),\n                                IcmpPacketCode(icmp_code.0),\n                                extension,\n                            )\n                        })\n                } else {\n                    None\n                }\n            }\n            IcmpType::DestinationUnreachable => {\n                let packet = DestinationUnreachablePacket::new_view(icmp_v4.packet())?;\n                let nested_ipv4 = Ipv4Packet::new_view(packet.payload())?;\n                let extension = match self.icmp_extension_mode {\n                    IcmpExtensionParseMode::Enabled => {\n                        packet.extension().map(Extensions::try_from).transpose()?\n                    }\n                    IcmpExtensionParseMode::Disabled => None,\n                };\n                self.extract_probe_proto_resp(&nested_ipv4)?\n                    .map(|proto_resp| {\n                        Response::DestinationUnreachable(\n                            ResponseData::new(recv, src, proto_resp),\n                            IcmpPacketCode(icmp_code.0),\n                            extension,\n                        )\n                    })\n            }\n            IcmpType::EchoReply => match self.protocol {\n                Protocol::Icmp => {\n                    let packet = EchoReplyPacket::new_view(icmp_v4.packet())?;\n                    let id = packet.get_identifier();\n                    let seq = packet.get_sequence();\n                    let proto_resp =\n                        ProtocolResponse::Icmp(IcmpProtocolResponse::new(id, seq, None));\n                    Some(Response::EchoReply(\n                        ResponseData::new(recv, src, proto_resp),\n                        IcmpPacketCode(icmp_code.0),\n                    ))\n                }\n                Protocol::Udp | Protocol::Tcp => None,\n            },\n            _ => None,\n        })\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn extract_probe_proto_resp(&self, ipv4: &Ipv4Packet<'_>) -> Result<Option<ProtocolResponse>> {\n        Ok(match (self.protocol, ipv4.get_protocol()) {\n            (Protocol::Icmp, IpProtocol::Icmp) => {\n                let echo_request = extract_echo_request(ipv4)?;\n                let identifier = echo_request.get_identifier();\n                let sequence = echo_request.get_sequence();\n                Some(ProtocolResponse::Icmp(IcmpProtocolResponse::new(\n                    identifier,\n                    sequence,\n                    Some(TypeOfService(ipv4.get_tos())),\n                )))\n            }\n            (Protocol::Udp, IpProtocol::Udp) => {\n                let (src_port, dest_port, actual_checksum, identifier, payload_length) =\n                    extract_udp_packet(ipv4)?;\n                let expected_checksum =\n                    self.calc_udp_checksum(Port(src_port), Port(dest_port), payload_length)?;\n                Some(ProtocolResponse::Udp(UdpProtocolResponse::new(\n                    identifier,\n                    IpAddr::V4(ipv4.get_destination()),\n                    src_port,\n                    dest_port,\n                    Some(TypeOfService(ipv4.get_tos())),\n                    expected_checksum,\n                    actual_checksum,\n                    payload_length,\n                    false,\n                )))\n            }\n            (Protocol::Tcp, IpProtocol::Tcp) => {\n                let (src_port, dest_port) = extract_tcp_packet(ipv4)?;\n                Some(ProtocolResponse::Tcp(TcpProtocolResponse::new(\n                    IpAddr::V4(ipv4.get_destination()),\n                    src_port,\n                    dest_port,\n                    Some(TypeOfService(ipv4.get_tos())),\n                )))\n            }\n            _ => None,\n        })\n    }\n\n    /// Create an ICMP `EchoRequest` packet.\n    fn make_echo_request_icmp_packet<'a>(\n        &self,\n        icmp_buf: &'a mut [u8],\n        identifier: TraceId,\n        sequence: Sequence,\n        payload_size: usize,\n    ) -> Result<EchoRequestPacket<'a>> {\n        let payload_buf = [self.payload_pattern.0; MAX_ICMP_PAYLOAD_BUF];\n        let packet_size = IcmpPacket::minimum_packet_size() + payload_size;\n        let mut icmp = EchoRequestPacket::new(&mut icmp_buf[..packet_size])?;\n        icmp.set_icmp_type(IcmpType::EchoRequest);\n        icmp.set_icmp_code(IcmpCode(0));\n        icmp.set_identifier(identifier.0);\n        icmp.set_payload(&payload_buf[..payload_size]);\n        icmp.set_sequence(sequence.0);\n        icmp.set_checksum(icmp_ipv4_checksum(icmp.packet()));\n        Ok(icmp)\n    }\n\n    /// Create a `UdpPacket`\n    fn make_udp_packet<'a>(\n        &self,\n        udp_buf: &'a mut [u8],\n        src_port: u16,\n        dest_port: u16,\n        payload: &'_ [u8],\n    ) -> Result<UdpPacket<'a>> {\n        let udp_packet_size = UdpPacket::minimum_packet_size() + payload.len();\n        let mut udp = UdpPacket::new(&mut udp_buf[..udp_packet_size])?;\n        udp.set_source(src_port);\n        udp.set_destination(dest_port);\n        udp.set_length(udp_packet_size as u16);\n        udp.set_payload(payload);\n        udp.set_checksum(udp_ipv4_checksum(\n            udp.packet(),\n            self.src_addr,\n            self.dest_addr,\n        ));\n        Ok(udp)\n    }\n\n    /// Create an `Ipv4Packet`.\n    fn make_ipv4_packet<'a>(\n        &self,\n        ipv4_buf: &'a mut [u8],\n        protocol: IpProtocol,\n        ttl: u8,\n        identification: u16,\n        payload: &[u8],\n    ) -> Result<Ipv4Packet<'a>> {\n        let ipv4_total_length = (Ipv4Packet::minimum_packet_size() + payload.len()) as u16;\n        let ipv4_total_length_header = self.byte_order.adjust_length(ipv4_total_length);\n        let ipv4_flags_and_fragment_offset_header = self.byte_order.adjust_length(DONT_FRAGMENT);\n        let mut ipv4 = Ipv4Packet::new(&mut ipv4_buf[..ipv4_total_length as usize])?;\n        ipv4.set_version(4);\n        ipv4.set_header_length(5);\n        ipv4.set_total_length(ipv4_total_length_header);\n        ipv4.set_ttl(ttl);\n        ipv4.set_protocol(protocol);\n        ipv4.set_source(self.src_addr);\n        ipv4.set_destination(self.dest_addr);\n        ipv4.set_tos(self.tos.0);\n        ipv4.set_payload(payload);\n        ipv4.set_identification(identification);\n        ipv4.set_flags_and_fragment_offset(ipv4_flags_and_fragment_offset_header);\n        Ok(ipv4)\n    }\n\n    /// Calculate the expected checksum for a UDP packet.\n    ///\n    /// Note that this calculation takes place for incoming UDP packet before\n    /// packet validation and so this may not be a packet sent by us and so we\n    /// cannot assume the payload size is within the bounds of `MAX_UDP_PAYLOAD_BUF`.\n    pub fn calc_udp_checksum(\n        &self,\n        src_port: Port,\n        dest_port: Port,\n        payload_size: u16,\n    ) -> Result<u16> {\n        let mut udp_buf = [0_u8; MAX_UDP_PACKET_BUF];\n        let size = usize::from(payload_size).min(MAX_UDP_PAYLOAD_BUF);\n        let payload = &[self.payload_pattern.0; MAX_UDP_PAYLOAD_BUF][0..size];\n        let udp = self.make_udp_packet(&mut udp_buf, src_port.0, dest_port.0, payload)?;\n        Ok(udp.get_checksum())\n    }\n}\n\nconst ADDR_NOT_AVAILABLE_KIND: ErrorKind = ErrorKind::Std(io::ErrorKind::AddrNotAvailable);\nconst INVALID_INPUT_KIND: ErrorKind = ErrorKind::Std(io::ErrorKind::InvalidInput);\n\nconst fn icmp_payload_size(packet_size: usize) -> usize {\n    let ip_header_size = Ipv4Packet::minimum_packet_size();\n    let icmp_header_size = IcmpPacket::minimum_packet_size();\n    packet_size - icmp_header_size - ip_header_size\n}\n\nconst fn udp_payload_size(packet_size: usize) -> usize {\n    let ip_header_size = Ipv4Packet::minimum_packet_size();\n    let udp_header_size = UdpPacket::minimum_packet_size();\n    packet_size - udp_header_size - ip_header_size\n}\n\n#[instrument(level = \"trace\")]\nfn extract_echo_request<'a>(ipv4: &'a Ipv4Packet<'a>) -> Result<EchoRequestPacket<'a>> {\n    Ok(EchoRequestPacket::new_view(ipv4.payload())?)\n}\n\n/// Get the src and dest ports from the original `UdpPacket` packet embedded in the payload.\n#[instrument(level = \"trace\")]\nfn extract_udp_packet(ipv4: &Ipv4Packet<'_>) -> Result<(u16, u16, u16, u16, u16)> {\n    let nested = UdpPacket::new_view(ipv4.payload())?;\n    Ok((\n        nested.get_source(),\n        nested.get_destination(),\n        nested.get_checksum(),\n        ipv4.get_identification(),\n        nested.get_length() - UdpPacket::minimum_packet_size() as u16,\n    ))\n}\n\n/// Get the src and dest ports from the original `TcpPacket` packet embedded in the payload.\n///\n/// Unlike the embedded `ICMP` and `UDP` packets, which have a minimum header size of 8 bytes, the\n/// `TCP` packet header is a minimum of 20 bytes.\n///\n/// The `ICMP` packets we are extracting these from, such as `TimeExceeded`, only guarantee that 8\n/// bytes of the original packet (plus the IP header) be returned, and so we may not have a complete\n/// TCP packet.\n///\n/// We therefore have to detect this situation and ensure we provide buffer a large enough for a\n/// complete TCP packet header.\n#[instrument(level = \"trace\")]\nfn extract_tcp_packet(ipv4: &Ipv4Packet<'_>) -> Result<(u16, u16)> {\n    let nested_tcp = ipv4.payload();\n    if nested_tcp.len() < TcpPacket::minimum_packet_size() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        buf[..nested_tcp.len()].copy_from_slice(nested_tcp);\n        let tcp_packet = TcpPacket::new_view(&buf)?;\n        Ok((tcp_packet.get_source(), tcp_packet.get_destination()))\n    } else {\n        let tcp_packet = TcpPacket::new_view(nested_tcp)?;\n        Ok((tcp_packet.get_source(), tcp_packet.get_destination()))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::error::IoResult;\n    use crate::mocket_read;\n    use crate::net::socket::MockSocket;\n    use crate::{Flags, Port, RoundId, TimeToLive};\n    use mockall::predicate;\n    use std::str::FromStr;\n    use std::sync::Mutex;\n\n    static MTX: Mutex<()> = Mutex::new(());\n\n    // Test dispatching a IPv4/ICMP probe.\n    #[test]\n    fn test_dispatch_icmp_probe_no_payload() -> anyhow::Result<()> {\n        let probe = make_icmp_probe();\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let packet_size = PacketSize(28);\n        let payload_pattern = PayloadPattern(0x00);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            45 00 00 1c 00 00 40 00 0a 01 00 00 01 02 03 04\n            05 06 07 08 08 00 70 93 04 d2 82 9a\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 0);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            ..Default::default()\n        };\n        ipv4.dispatch_icmp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_icmp_probe_with_payload() -> anyhow::Result<()> {\n        let probe = make_icmp_probe();\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let packet_size = PacketSize(48);\n        let payload_pattern = PayloadPattern(0xff);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            45 00 00 30 00 00 40 00 0a 01 00 00 01 02 03 04\n            05 06 07 08 08 00 70 93 04 d2 82 9a ff ff ff ff\n            ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 0);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            ..Default::default()\n        };\n        ipv4.dispatch_icmp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_icmp_probe_invalid_packet_size_low() -> anyhow::Result<()> {\n        let probe = make_icmp_probe();\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let packet_size = PacketSize(27);\n        let payload_pattern = PayloadPattern(0x00);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let mut mocket = MockSocket::new();\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            ..Default::default()\n        };\n        let err = ipv4.dispatch_icmp_probe(&mut mocket, &probe).unwrap_err();\n        assert!(matches!(err, Error::InvalidPacketSize(_)));\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_icmp_probe_invalid_packet_size_high() -> anyhow::Result<()> {\n        let probe = make_icmp_probe();\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let packet_size = PacketSize(1025);\n        let payload_pattern = PayloadPattern(0x00);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let mut mocket = MockSocket::new();\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            ..Default::default()\n        };\n        let err = ipv4.dispatch_icmp_probe(&mut mocket, &probe).unwrap_err();\n        assert!(matches!(err, Error::InvalidPacketSize(_)));\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_icmp_probe_with_tos() -> anyhow::Result<()> {\n        let probe = make_icmp_probe();\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let packet_size = PacketSize(28);\n        let payload_pattern = PayloadPattern(0x00);\n        let tos = TypeOfService(0xE0);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            45 e0 00 1c 00 00 40 00 0a 01 00 00 01 02 03 04\n            05 06 07 08 08 00 70 93 04 d2 82 9a\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 0);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            tos,\n            ..Default::default()\n        };\n        ipv4.dispatch_icmp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_privileged_no_payload() -> anyhow::Result<()> {\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(28);\n        let payload_pattern = PayloadPattern(0x00);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            45 00 00 1c 04 d2 40 00 0a 11 00 00 01 02 03 04\n            05 06 07 08 00 7b 01 c8 00 08 ed 87\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            ..Default::default()\n        };\n        ipv4.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_privileged_with_payload() -> anyhow::Result<()> {\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(38);\n        let payload_pattern = PayloadPattern(0xaa);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            45 00 00 26 04 d2 40 00 0a 11 00 00 01 02 03 04\n            05 06 07 08 00 7b 01 c8 00 12 98 1e aa aa aa aa\n            aa aa aa aa aa aa\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            ..Default::default()\n        };\n        ipv4.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_paris_privileged() -> anyhow::Result<()> {\n        let probe = Probe {\n            flags: Flags::PARIS_CHECKSUM,\n            ..make_udp_probe(123, 456)\n        };\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        // packet size and payload pattern are ignored for paris mode as a\n        // fixed two byte payload is used to hold the sequence\n        let packet_size = PacketSize(300);\n        let payload_pattern = PayloadPattern(0xaa);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            45 00 00 1e 04 d2 40 00 0a 11 00 00 01 02 03 04\n            05 06 07 08 00 7b 01 c8 00 0a 82 9a 6a e9\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            ..Default::default()\n        };\n        ipv4.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_dublin_privileged() -> anyhow::Result<()> {\n        let probe = Probe {\n            // note: this is always set for UDP/Dublin but is a no-op for IPv4\n            flags: Flags::DUBLIN_IPV6_PAYLOAD_LENGTH,\n            identifier: TraceId(33434),\n            ..make_udp_probe(123, 456)\n        };\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(28);\n        let payload_pattern = PayloadPattern(0xaa);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            45 00 00 1c 82 9a 40 00 0a 11 00 00 01 02 03 04\n            05 06 07 08 00 7b 01 c8 00 08 ed 87\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            ..Default::default()\n        };\n        ipv4.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_unprivileged_no_payload() -> anyhow::Result<()> {\n        let _m = MTX.lock();\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Unprivileged;\n        let packet_size = PacketSize(28);\n        let payload_pattern = PayloadPattern(0x00);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let expected_send_to_buf = hex_literal::hex!(\"\");\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n        let expected_bind_addr = SocketAddr::new(IpAddr::V4(src_addr), 123);\n        let expected_set_ttl = 10;\n        let expected_set_tos = 0;\n\n        let mut mocket = MockSocket::new();\n\n        let ctx = MockSocket::new_udp_send_socket_ipv4_context();\n        ctx.expect().with(predicate::eq(false)).returning(move |_| {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_ttl()\n                .with(predicate::eq(expected_set_ttl))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_tos()\n                .with(predicate::eq(expected_set_tos))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_send_to()\n                .with(\n                    predicate::eq(expected_send_to_buf),\n                    predicate::eq(expected_send_to_addr),\n                )\n                .times(1)\n                .returning(|_, _| Ok(()));\n\n            Ok(mocket)\n        });\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            ..Default::default()\n        };\n        ipv4.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_unprivileged_with_payload() -> anyhow::Result<()> {\n        let _m = MTX.lock();\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Unprivileged;\n        let packet_size = PacketSize(36);\n        let payload_pattern = PayloadPattern(0x1f);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let expected_send_to_buf = hex_literal::hex!(\"1f 1f 1f 1f 1f 1f 1f 1f\");\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n        let expected_bind_addr = SocketAddr::new(IpAddr::V4(src_addr), 123);\n        let expected_set_ttl = 10;\n        let expected_set_tos = 0;\n\n        let mut mocket = MockSocket::new();\n\n        let ctx = MockSocket::new_udp_send_socket_ipv4_context();\n        ctx.expect().with(predicate::eq(false)).returning(move |_| {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_ttl()\n                .with(predicate::eq(expected_set_ttl))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_tos()\n                .with(predicate::eq(expected_set_tos))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_send_to()\n                .with(\n                    predicate::eq(expected_send_to_buf),\n                    predicate::eq(expected_send_to_addr),\n                )\n                .times(1)\n                .returning(|_, _| Ok(()));\n\n            Ok(mocket)\n        });\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            ..Default::default()\n        };\n        ipv4.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_privileged_with_tos() -> anyhow::Result<()> {\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(28);\n        let payload_pattern = PayloadPattern(0x00);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let tos = TypeOfService(0xE0);\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            45 e0 00 1c 04 d2 40 00 0a 11 00 00 01 02 03 04\n            05 06 07 08 00 7b 01 c8 00 08 ed 87\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            tos,\n            ..Default::default()\n        };\n        ipv4.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_unprivileged_with_tos() -> anyhow::Result<()> {\n        let _m = MTX.lock();\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Unprivileged;\n        let packet_size = PacketSize(28);\n        let payload_pattern = PayloadPattern(0x00);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let tos = TypeOfService(224);\n        let expected_send_to_buf = hex_literal::hex!(\"\");\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n        let expected_bind_addr = SocketAddr::new(IpAddr::V4(src_addr), 123);\n        let expected_set_ttl = 10;\n        let expected_set_tos = u32::from(tos.0);\n\n        let mut mocket = MockSocket::new();\n\n        let ctx = MockSocket::new_udp_send_socket_ipv4_context();\n        ctx.expect().with(predicate::eq(false)).returning(move |_| {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_ttl()\n                .with(predicate::eq(expected_set_ttl))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_tos()\n                .with(predicate::eq(expected_set_tos))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_send_to()\n                .with(\n                    predicate::eq(expected_send_to_buf),\n                    predicate::eq(expected_send_to_addr),\n                )\n                .times(1)\n                .returning(|_, _| Ok(()));\n\n            Ok(mocket)\n        });\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            tos,\n            ..Default::default()\n        };\n        ipv4.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_invalid_packet_size_low() -> anyhow::Result<()> {\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(27);\n        let payload_pattern = PayloadPattern(0x00);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let mut mocket = MockSocket::new();\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            ..Default::default()\n        };\n        let err = ipv4.dispatch_udp_probe(&mut mocket, &probe).unwrap_err();\n        assert!(matches!(err, Error::InvalidPacketSize(_)));\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_invalid_packet_size_high() -> anyhow::Result<()> {\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(1025);\n        let payload_pattern = PayloadPattern(0x00);\n        let byte_order = platform::Ipv4ByteOrder::Network;\n        let mut mocket = MockSocket::new();\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            byte_order,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            ..Default::default()\n        };\n        let err = ipv4.dispatch_udp_probe(&mut mocket, &probe).unwrap_err();\n        assert!(matches!(err, Error::InvalidPacketSize(_)));\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_tcp_probe() -> anyhow::Result<()> {\n        let _m = MTX.lock();\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let dest_addr = Ipv4Addr::from_str(\"5.6.7.8\")?;\n        let tos = TypeOfService(224);\n        let expected_bind_addr = SocketAddr::new(IpAddr::V4(src_addr), 123);\n        let expected_set_ttl = 10;\n        let expected_set_tos = u32::from(tos.0);\n        let expected_connect_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n\n        let ctx = MockSocket::new_stream_socket_ipv4_context();\n        ctx.expect().returning(move || {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_ttl()\n                .with(predicate::eq(expected_set_ttl))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_tos()\n                .with(predicate::eq(expected_set_tos))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_connect()\n                .with(predicate::eq(expected_connect_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            Ok(mocket)\n        });\n\n        let ipv4 = Ipv4 {\n            src_addr,\n            dest_addr,\n            tos,\n            ..Default::default()\n        };\n        ipv4.dispatch_tcp_probe::<MockSocket>(&probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_echo_reply() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n            45 20 00 54 00 00 00 00 3b 01 50 02 8e fb de ce\n            c0 a8 01 15 00 00 09 0f 75 d7 81 19 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(1)\n            .returning(mocket_read!(expected_read_buf));\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::EchoReply(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Icmp(IcmpProtocolResponse {\n                        identifier,\n                        sequence,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n        ) = resp\n        else {\n            panic!(\"expected EchoReply\")\n        };\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"142.251.222.206\")?), addr);\n        assert_eq!(30167, identifier);\n        assert_eq!(33049, sequence);\n        assert_eq!(None, tos);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_time_exceeded_icmp_no_extensions() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n             45 20 00 70 07 d7 00 00 3b 01 e9 5d 8e fa 3d 81\n             c0 a8 01 15 0b 00 f4 ff 00 00 00 00 45 60 00 54\n             65 b0 40 00 01 01 e4 11 c0 a8 01 15 8e fb de ce\n             08 00 01 11 75 d7 81 17 00 00 00 00 00 00 00 00\n             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(1)\n            .returning(mocket_read!(expected_read_buf));\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Icmp(IcmpProtocolResponse {\n                        identifier,\n                        sequence,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"142.250.61.129\")?), addr);\n        assert_eq!(30167, identifier);\n        assert_eq!(33047, sequence);\n        assert_eq!(Some(TypeOfService(96)), tos);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_destination_unreachable_icmp_no_extensions() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n            45 20 00 38 00 00 40 00 70 01 33 ea 14 00 00 fe\n            c0 a8 01 15 03 01 fc fe 00 00 00 00 45 00 00 54\n            00 00 40 00 80 01 23 ee c0 a8 01 15 14 00 00 fe\n            08 00 fb d9 7b 01 81 24\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(1)\n            .returning(mocket_read!(expected_read_buf));\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::DestinationUnreachable(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Icmp(IcmpProtocolResponse {\n                        identifier,\n                        sequence,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected DestinationUnreachable\")\n        };\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"20.0.0.254\")?), addr);\n        assert_eq!(31489, identifier);\n        assert_eq!(33060, sequence);\n        assert_eq!(Some(TypeOfService(0)), tos);\n        assert_eq!(IcmpPacketCode(1), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_time_exceeded_udp_no_extensions() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n            45 c0 00 70 0e c8 00 00 40 01 e7 9e c0 a8 01 01\n            c0 a8 01 15 0b 00 12 98 00 00 00 00 45 00 00 54\n            90 69 00 00 01 11 0b ea c0 a8 01 15 8e fa cc 8e\n            7c 55 81 06 00 40 e4 cb 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(1)\n            .returning(mocket_read!(expected_read_buf));\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Udp,\n            src_addr: Ipv4Addr::from_str(\"192.168.1.21\")?,\n            dest_addr: Ipv4Addr::from_str(\"142.250.204.142\")?,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Udp(UdpProtocolResponse {\n                        identifier,\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                        expected_udp_checksum,\n                        actual_udp_checksum,\n                        payload_len,\n                        has_magic,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"192.168.1.1\")?), addr);\n        assert_eq!(36969, identifier);\n        assert_eq!(\n            IpAddr::V4(Ipv4Addr::from_str(\"142.250.204.142\")?),\n            dest_addr\n        );\n        assert_eq!(31829, src_port);\n        assert_eq!(33030, dest_port);\n        assert_eq!(Some(TypeOfService(0)), tos);\n        assert_eq!(58571, expected_udp_checksum);\n        assert_eq!(58571, actual_udp_checksum);\n        assert_eq!(56, payload_len);\n        assert!(!has_magic);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_destination_unreachable_udp_no_extensions() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n            45 20 00 70 bc f6 00 00 39 01 f0 a7 09 09 09 09\n            c0 a8 01 15 03 0a d1 16 00 00 00 00 45 20 00 54\n            a2 09 00 00 01 11 43 a1 c0 a8 01 15 09 09 09 09\n            80 0b 80 f2 00 40 2a a1 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(1)\n            .returning(mocket_read!(expected_read_buf));\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Udp,\n            src_addr: Ipv4Addr::from_str(\"192.168.1.21\")?,\n            dest_addr: Ipv4Addr::from_str(\"9.9.9.9\")?,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::DestinationUnreachable(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Udp(UdpProtocolResponse {\n                        identifier,\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                        expected_udp_checksum,\n                        actual_udp_checksum,\n                        payload_len,\n                        has_magic,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected DestinationUnreachable\")\n        };\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"9.9.9.9\")?), addr);\n        assert_eq!(41481, identifier);\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"9.9.9.9\")?), dest_addr);\n        assert_eq!(32779, src_port);\n        assert_eq!(33010, dest_port);\n        assert_eq!(Some(TypeOfService(32)), tos);\n        assert_eq!(10913, expected_udp_checksum);\n        assert_eq!(10913, actual_udp_checksum);\n        assert_eq!(56, payload_len);\n        assert!(!has_magic);\n        assert_eq!(IcmpPacketCode(10), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_time_exceeded_tcp_no_extensions() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n            45 20 00 5c a6 9d 00 00 3b 01 54 e5 d1 55 f0 eb\n            c0 a8 01 15 0b 00 12 79 00 00 00 00 45 80 00 40\n            00 00 40 00 01 06 5b f2 c0 a8 01 15 8e fa cc 8e\n            80 fd 00 50 61 f2 4d 4a 00 00 00 00 b0 02 ff ff\n            14 05 00 00 02 04 05 b4 01 03 03 06 01 01 08 0a\n            55 59 7f cd 00 00 00 00 04 02 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(1)\n            .returning(mocket_read!(expected_read_buf));\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Tcp(TcpProtocolResponse {\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"209.85.240.235\")?), addr);\n        assert_eq!(\n            IpAddr::V4(Ipv4Addr::from_str(\"142.250.204.142\")?),\n            dest_addr\n        );\n        assert_eq!(33021, src_port);\n        assert_eq!(80, dest_port);\n        assert_eq!(Some(TypeOfService(128)), tos);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_destination_unreachable_tcp_no_extensions() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n            45 20 00 5c d6 e0 00 00 39 01 d6 d1 09 09 09 09\n            c0 a8 01 15 03 0a d0 f7 00 00 00 00 45 20 00 40\n            00 00 00 00 01 06 e5 c9 c0 a8 01 15 09 09 09 09\n            80 f2 27 1b 5e b1 fa c7 00 00 00 00 b0 02 ff ff\n            a4 53 00 00 02 04 05 b4 01 03 03 06 01 01 08 0a\n            1d 02 a0 50 00 00 00 00 04 02 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(1)\n            .returning(mocket_read!(expected_read_buf));\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::DestinationUnreachable(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Tcp(TcpProtocolResponse {\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected DestinationUnreachable\")\n        };\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"9.9.9.9\")?), addr);\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"9.9.9.9\")?), dest_addr);\n        assert_eq!(33010, src_port);\n        assert_eq!(10011, dest_port);\n        assert_eq!(Some(TypeOfService(32)), tos);\n        assert_eq!(IcmpPacketCode(10), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_wrong_icmp_original_datagram_type_ignored() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n             45 20 00 70 07 d7 00 00 3b 01 e9 5d 8e fa 3d 81\n             c0 a8 01 15 0b 00 f4 ff 00 00 00 00 45 60 00 54\n             65 b0 40 00 01 01 e4 11 c0 a8 01 15 8e fb de ce\n             08 00 01 11 75 d7 81 17 00 00 00 00 00 00 00 00\n             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n             00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(3)\n            .returning(mocket_read!(expected_read_buf));\n\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_some());\n\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_wrong_udp_original_datagram_type_ignored() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n            45 c0 00 70 0e c8 00 00 40 01 e7 9e c0 a8 01 01\n            c0 a8 01 15 0b 00 12 98 00 00 00 00 45 00 00 54\n            90 69 00 00 01 11 0b ea c0 a8 01 15 8e fa cc 8e\n            7c 55 81 06 00 40 e4 cb 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(3)\n            .returning(mocket_read!(expected_read_buf));\n\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_some());\n\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_wrong_tcp_original_datagram_type_ignored() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n            45 20 00 5c a6 9d 00 00 3b 01 54 e5 d1 55 f0 eb\n            c0 a8 01 15 0b 00 12 79 00 00 00 00 45 80 00 40\n            00 00 40 00 01 06 5b f2 c0 a8 01 15 8e fa cc 8e\n            80 fd 00 50 61 f2 4d 4a 00 00 00 00 b0 02 ff ff\n            14 05 00 00 02 04 05 b4 01 03 03 06 01 01 08 0a\n            55 59 7f cd 00 00 00 00 04 02 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(3)\n            .returning(mocket_read!(expected_read_buf));\n\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_some());\n\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_tcp_socket_tcp_reply() -> anyhow::Result<()> {\n        let dest_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n        let expected_peer_addr = SocketAddr::new(IpAddr::V4(dest_addr), 456);\n\n        let mut mocket = MockSocket::new();\n        mocket.expect_take_error().times(1).returning(|| Ok(None));\n        mocket\n            .expect_peer_addr()\n            .times(1)\n            .returning(move || Ok(Some(expected_peer_addr)));\n        mocket.expect_shutdown().times(1).returning(|| Ok(()));\n\n        let ipv4 = Ipv4 {\n            dest_addr,\n            ..Default::default()\n        };\n        let resp = ipv4\n            .recv_tcp_socket(&mut mocket, Port(33434), Port(456))?\n            .unwrap();\n\n        let Response::TcpReply(ResponseData {\n            addr,\n            proto_resp:\n                ProtocolResponse::Tcp(TcpProtocolResponse {\n                    dest_addr,\n                    src_port,\n                    dest_port,\n                    tos,\n                }),\n            ..\n        }) = resp\n        else {\n            panic!(\"expected TcpReply\")\n        };\n        assert_eq!(dest_addr, addr);\n        assert_eq!(33434, src_port);\n        assert_eq!(456, dest_port);\n        assert_eq!(None, tos);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_tcp_socket_tcp_refused() -> anyhow::Result<()> {\n        let dest_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_take_error()\n            .times(1)\n            .returning(|| Ok(Some(SocketError::ConnectionRefused)));\n\n        let ipv4 = Ipv4 {\n            dest_addr,\n            ..Default::default()\n        };\n        let resp = ipv4\n            .recv_tcp_socket(&mut mocket, Port(33434), Port(80))?\n            .unwrap();\n\n        let Response::TcpRefused(ResponseData {\n            addr,\n            proto_resp:\n                ProtocolResponse::Tcp(TcpProtocolResponse {\n                    dest_addr,\n                    src_port,\n                    dest_port,\n                    tos,\n                }),\n            ..\n        }) = resp\n        else {\n            panic!(\"expected TcpRefused\")\n        };\n        assert_eq!(dest_addr, addr);\n        assert_eq!(33434, src_port);\n        assert_eq!(80, dest_port);\n        assert_eq!(None, tos);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_tcp_socket_tcp_host_unreachable() -> anyhow::Result<()> {\n        let dest_addr = Ipv4Addr::from_str(\"1.2.3.4\")?;\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_take_error()\n            .times(1)\n            .returning(|| Ok(Some(SocketError::HostUnreachable)));\n        mocket\n            .expect_icmp_error_info()\n            .times(1)\n            .returning(move || Ok(IpAddr::V4(dest_addr)));\n\n        let ipv4 = Ipv4 {\n            dest_addr,\n            ..Default::default()\n        };\n        let resp = ipv4\n            .recv_tcp_socket(&mut mocket, Port(33434), Port(80))?\n            .unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Tcp(TcpProtocolResponse {\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(dest_addr, addr);\n        assert_eq!(33434, src_port);\n        assert_eq!(80, dest_port);\n        assert_eq!(None, tos);\n        assert_eq!(IcmpPacketCode(1), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    // This IPv4/ICMP `TimeExceeded` packet has code 1 (\"Fragment reassembly\n    // time exceeded\") and must be ignored.\n    //\n    // Note this is not real packet and so the length and checksum are not\n    // accurate.\n    #[test]\n    fn test_icmp_time_exceeded_fragment_reassembly_ignored() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n           45 20 2c 02 e4 5c 00 00 72 01 2e 04 67 4b 0b 34\n           c0 a8 01 15 0b 01 1c 38 00 00 00 00 45 00 8c 05\n           85 4e 20 00 30 11 ab d6 c0 a8 01 15 67 4b 0b 34\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(1)\n            .returning(mocket_read!(expected_read_buf));\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n        Ok(())\n    }\n\n    // This IPv4/ICMP `TimeExceeded` packet has an UDP Original Datagram\n    // with a bogus length (claimed 2040 vs actual 56).\n    //\n    // This is a test to ensure that the UDP checksum validation is working for\n    // packets which are larger than the maximum payload size.  This can occur\n    // as unrelated ICMP packets are delivered to our socket and the filtering\n    // occurs later on in the strategy module.\n    //\n    // The packet is not ignored and the UDP Original Datagram is parsed but\n    // notice the expected UDP checksum does not match the actual checksum as\n    // the calculation relies on the claimed payload length, which we restrict\n    // to the maximum packet size we can send.\n    #[test]\n    fn test_recv_icmp_probe_udp_wrong_payload_size() -> anyhow::Result<()> {\n        let expected_read_buf = hex_literal::hex!(\n            \"\n            45 c0 00 70 0e c8 00 00 40 01 e7 9e c0 a8 01 01\n            c0 a8 01 15 0b 00 12 98 00 00 00 00 45 00 00 54\n            90 69 00 00 01 11 0b ea c0 a8 01 15 8e fa cc 8e\n            7c 55 81 06 08 00 e4 cb 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_read()\n            .times(1)\n            .returning(mocket_read!(expected_read_buf));\n        let ipv4 = Ipv4 {\n            protocol: Protocol::Udp,\n            src_addr: Ipv4Addr::from_str(\"192.168.1.21\")?,\n            dest_addr: Ipv4Addr::from_str(\"9.9.9.9\")?,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv4.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Udp(UdpProtocolResponse {\n                        identifier,\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                        expected_udp_checksum,\n                        actual_udp_checksum,\n                        payload_len,\n                        has_magic,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(IpAddr::V4(Ipv4Addr::from_str(\"192.168.1.1\")?), addr);\n        assert_eq!(36969, identifier);\n        assert_eq!(\n            IpAddr::V4(Ipv4Addr::from_str(\"142.250.204.142\")?),\n            dest_addr\n        );\n        assert_eq!(31829, src_port);\n        assert_eq!(33030, dest_port);\n        assert_eq!(Some(TypeOfService(0)), tos);\n        assert_eq!(9963, expected_udp_checksum);\n        assert_eq!(58571, actual_udp_checksum);\n        assert_eq!(2040, payload_len);\n        assert!(!has_magic);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    fn make_icmp_probe() -> Probe {\n        Probe::new(\n            Sequence(33434),\n            TraceId(1234),\n            Port(0),\n            Port(0),\n            TimeToLive(10),\n            RoundId(0),\n            SystemTime::now(),\n            Flags::empty(),\n        )\n    }\n\n    fn make_udp_probe(src_port: u16, dest_port: u16) -> Probe {\n        Probe::new(\n            Sequence(33434),\n            TraceId(1234),\n            Port(src_port),\n            Port(dest_port),\n            TimeToLive(10),\n            RoundId(0),\n            SystemTime::now(),\n            Flags::empty(),\n        )\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net/ipv6.rs",
    "content": "use crate::config::IcmpExtensionParseMode;\nuse crate::error::{Error, ErrorKind, Result};\nuse crate::net::channel::MAX_PACKET_SIZE;\nuse crate::net::common::ErrorMapper;\nuse crate::net::socket::{Socket, SocketError};\nuse crate::probe::{\n    Extensions, IcmpPacketCode, IcmpProtocolResponse, Probe, ProtocolResponse, Response,\n    ResponseData, TcpProtocolResponse, UdpProtocolResponse,\n};\nuse crate::types::{PacketSize, PayloadPattern, Sequence, TraceId};\nuse crate::{Flags, Port, PrivilegeMode, Protocol, TypeOfService};\nuse std::io;\nuse std::net::{IpAddr, Ipv6Addr, SocketAddr};\nuse std::time::SystemTime;\nuse tracing::instrument;\nuse trippy_packet::IpProtocol;\nuse trippy_packet::checksum::{icmp_ipv6_checksum, udp_ipv6_checksum};\nuse trippy_packet::icmpv6::destination_unreachable::DestinationUnreachablePacket;\nuse trippy_packet::icmpv6::echo_reply::EchoReplyPacket;\nuse trippy_packet::icmpv6::echo_request::EchoRequestPacket;\nuse trippy_packet::icmpv6::time_exceeded::TimeExceededPacket;\nuse trippy_packet::icmpv6::{IcmpCode, IcmpPacket, IcmpTimeExceededCode, IcmpType};\nuse trippy_packet::ipv6::Ipv6Packet;\nuse trippy_packet::tcp::TcpPacket;\nuse trippy_packet::udp::UdpPacket;\n\n/// The maximum size of UDP packet we allow.\nconst MAX_UDP_PACKET_BUF: usize = MAX_PACKET_SIZE - Ipv6Packet::minimum_packet_size();\n\n/// The maximum size of UDP payload we allow.\nconst MAX_UDP_PAYLOAD_BUF: usize = MAX_UDP_PACKET_BUF - UdpPacket::minimum_packet_size();\n\n/// The maximum size of UDP packet we allow.\nconst MAX_ICMP_PACKET_BUF: usize = MAX_PACKET_SIZE - Ipv6Packet::minimum_packet_size();\n\n/// The maximum size of ICMP payload we allow.\nconst MAX_ICMP_PAYLOAD_BUF: usize = MAX_ICMP_PACKET_BUF - IcmpPacket::minimum_packet_size();\n\n/// The minimum size of ICMP packets we allow.\nconst MIN_PACKET_SIZE_ICMP: usize =\n    Ipv6Packet::minimum_packet_size() + IcmpPacket::minimum_packet_size();\n\n/// The minimum size of UDP packets we allow.\nconst MIN_PACKET_SIZE_UDP: usize =\n    Ipv6Packet::minimum_packet_size() + UdpPacket::minimum_packet_size();\n\n/// Magic prefix for IPv6/UDP/Dublin payloads.\nconst MAGIC: &[u8] = b\"trippy\";\n\n/// IPv6 configuration.\n#[derive(Debug)]\npub struct Ipv6 {\n    pub src_addr: Ipv6Addr,\n    pub dest_addr: Ipv6Addr,\n    pub packet_size: PacketSize,\n    pub payload_pattern: PayloadPattern,\n    pub privilege_mode: PrivilegeMode,\n    pub tos: TypeOfService,\n    pub protocol: Protocol,\n    pub icmp_extension_mode: IcmpExtensionParseMode,\n    pub initial_sequence: Sequence,\n}\n\nimpl Default for Ipv6 {\n    fn default() -> Self {\n        Self {\n            src_addr: Ipv6Addr::UNSPECIFIED,\n            dest_addr: Ipv6Addr::UNSPECIFIED,\n            packet_size: PacketSize(0),\n            payload_pattern: PayloadPattern(0),\n            privilege_mode: PrivilegeMode::Privileged,\n            tos: TypeOfService(0),\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            initial_sequence: Sequence(0),\n        }\n    }\n}\n\nimpl Ipv6 {\n    /// Dispatch an ICMP probe.\n    #[instrument(skip(self, icmp_send_socket), level = \"trace\")]\n    pub fn dispatch_icmp_probe<S: Socket>(\n        &self,\n        icmp_send_socket: &mut S,\n        probe: &Probe,\n    ) -> Result<()> {\n        let mut icmp_buf = [0_u8; MAX_ICMP_PACKET_BUF];\n        let packet_size = usize::from(self.packet_size.0);\n        if !(MIN_PACKET_SIZE_ICMP..=MAX_PACKET_SIZE).contains(&packet_size) {\n            return Err(Error::InvalidPacketSize(packet_size));\n        }\n        let echo_request = self.make_echo_request_icmp_packet(\n            &mut icmp_buf,\n            probe.identifier,\n            probe.sequence,\n            icmp_payload_size(packet_size),\n        )?;\n        icmp_send_socket.set_unicast_hops_v6(probe.ttl.0)?;\n        icmp_send_socket.set_tclass_v6(u32::from(self.tos.0))?;\n        let remote_addr = SocketAddr::new(IpAddr::V6(self.dest_addr), 0);\n        icmp_send_socket.send_to(echo_request.packet(), remote_addr)?;\n        Ok(())\n    }\n\n    /// Dispatch a UDP probe.\n    #[instrument(skip(self, raw_send_socket), level = \"trace\")]\n    pub fn dispatch_udp_probe<S: Socket>(\n        &self,\n        raw_send_socket: &mut S,\n        probe: &Probe,\n    ) -> Result<()> {\n        let packet_size = usize::from(self.packet_size.0);\n        if !(MIN_PACKET_SIZE_UDP..=MAX_PACKET_SIZE).contains(&packet_size) {\n            return Err(Error::InvalidPacketSize(packet_size));\n        }\n        let payload_size = udp_payload_size(packet_size);\n        let payload = &[self.payload_pattern.0; MAX_UDP_PAYLOAD_BUF][0..payload_size];\n        match self.privilege_mode {\n            PrivilegeMode::Privileged => {\n                self.dispatch_udp_probe_raw(raw_send_socket, probe, payload)\n            }\n            PrivilegeMode::Unprivileged => self.dispatch_udp_probe_non_raw::<S>(probe, payload),\n        }\n    }\n\n    #[instrument(skip(self, udp_send_socket), level = \"trace\")]\n    fn dispatch_udp_probe_raw<S: Socket>(\n        &self,\n        udp_send_socket: &mut S,\n        probe: &Probe,\n        payload: &[u8],\n    ) -> Result<()> {\n        let mut udp_buf = [0_u8; MAX_UDP_PACKET_BUF];\n        let mut dublin_payload = [self.payload_pattern.0; MAX_UDP_PAYLOAD_BUF];\n        let payload_paris = probe.sequence.0.to_be_bytes();\n        let payload = if probe.flags.contains(Flags::PARIS_CHECKSUM) {\n            payload_paris.as_slice()\n        } else if probe.flags.contains(Flags::DUBLIN_IPV6_PAYLOAD_LENGTH) {\n            let payload_len = probe.sequence.0 - self.initial_sequence.0;\n            dublin_payload[..MAGIC.len()].copy_from_slice(MAGIC);\n            &dublin_payload[..usize::from(payload_len) + MAGIC.len()]\n        } else {\n            payload\n        };\n        let mut udp =\n            self.make_udp_packet(&mut udp_buf, probe.src_port.0, probe.dest_port.0, payload)?;\n        if probe.flags.contains(Flags::PARIS_CHECKSUM) {\n            let checksum = udp.get_checksum().to_be_bytes();\n            let payload = u16::from_be_bytes(core::array::from_fn(|i| udp.payload()[i]));\n            udp.set_checksum(payload);\n            udp.set_payload(&checksum);\n        }\n        udp_send_socket.set_unicast_hops_v6(probe.ttl.0)?;\n        udp_send_socket.set_tclass_v6(u32::from(self.tos.0))?;\n        // Note that we set the port to be 0 in the remote `SocketAddr` as the target port is\n        // encoded in the `UDP` packet.  If we (redundantly) set the target port here then\n        // the `send_to` will fail with `EINVAL`.\n        let remote_addr = SocketAddr::new(IpAddr::V6(self.dest_addr), 0);\n        udp_send_socket.send_to(udp.packet(), remote_addr)?;\n        Ok(())\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn dispatch_udp_probe_non_raw<S: Socket>(&self, probe: &Probe, payload: &[u8]) -> Result<()> {\n        let local_addr = SocketAddr::new(IpAddr::V6(self.src_addr), probe.src_port.0);\n        let remote_addr = SocketAddr::new(IpAddr::V6(self.dest_addr), probe.dest_port.0);\n        let mut socket = S::new_udp_send_socket_ipv6(false)?;\n        socket\n            .bind(local_addr)\n            .map_err(Error::IoError)\n            .or_else(ErrorMapper::in_progress)\n            .map_err(|err| ErrorMapper::addr_in_use(err, local_addr))?;\n        socket.set_unicast_hops_v6(probe.ttl.0)?;\n        socket.set_tclass_v6(u32::from(self.tos.0))?;\n        socket.send_to(payload, remote_addr)?;\n        Ok(())\n    }\n\n    /// Dispatch a TCP probe.\n    #[instrument(skip(self), level = \"trace\")]\n    pub fn dispatch_tcp_probe<S: Socket>(&self, probe: &Probe) -> Result<S> {\n        let mut socket = S::new_stream_socket_ipv6()?;\n        let local_addr = SocketAddr::new(IpAddr::V6(self.src_addr), probe.src_port.0);\n        socket\n            .bind(local_addr)\n            .map_err(Error::IoError)\n            .or_else(ErrorMapper::in_progress)\n            .map_err(|err| ErrorMapper::addr_in_use(err, local_addr))?;\n        socket.set_unicast_hops_v6(probe.ttl.0)?;\n        socket.set_tclass_v6(u32::from(self.tos.0))?;\n        let remote_addr = SocketAddr::new(IpAddr::V6(self.dest_addr), probe.dest_port.0);\n        socket\n            .connect(remote_addr)\n            .map_err(Error::IoError)\n            .or_else(ErrorMapper::in_progress)\n            .map_err(|err| ErrorMapper::addr_in_use(err, remote_addr))?;\n        Ok(socket)\n    }\n\n    /// Receive an ICMP probe.\n    #[instrument(skip(self, recv_socket), level = \"trace\")]\n    pub fn recv_icmp_probe<S: Socket>(&self, recv_socket: &mut S) -> Result<Option<Response>> {\n        let mut buf = [0_u8; MAX_PACKET_SIZE];\n        match recv_socket.recv_from(&mut buf) {\n            Ok((bytes_read, addr)) => {\n                let icmp_v6 = IcmpPacket::new_view(&buf[..bytes_read])?;\n                let src_addr = match addr.as_ref().ok_or(Error::MissingAddr)? {\n                    SocketAddr::V6(addr) => addr.ip(),\n                    SocketAddr::V4(_) => panic!(),\n                };\n                Ok(self.extract_probe_resp(&icmp_v6, *src_addr)?)\n            }\n            Err(err) => match err.kind() {\n                ErrorKind::Std(io::ErrorKind::WouldBlock) => Ok(None),\n                _ => Err(Error::IoError(err)),\n            },\n        }\n    }\n\n    /// Receive a TCP probe.\n    #[instrument(skip(self, tcp_socket), level = \"trace\")]\n    pub fn recv_tcp_socket<S: Socket>(\n        &self,\n        tcp_socket: &mut S,\n        src_port: Port,\n        dest_port: Port,\n    ) -> Result<Option<Response>> {\n        let proto_resp = ProtocolResponse::Tcp(TcpProtocolResponse::new(\n            IpAddr::V6(self.dest_addr),\n            src_port.0,\n            dest_port.0,\n            None,\n        ));\n        match tcp_socket.take_error()? {\n            None => {\n                let addr = tcp_socket.peer_addr()?.ok_or(Error::MissingAddr)?.ip();\n                tcp_socket.shutdown()?;\n                return Ok(Some(Response::TcpReply(ResponseData::new(\n                    SystemTime::now(),\n                    addr,\n                    proto_resp,\n                ))));\n            }\n            Some(err) => match err {\n                SocketError::ConnectionRefused => {\n                    return Ok(Some(Response::TcpRefused(ResponseData::new(\n                        SystemTime::now(),\n                        IpAddr::V6(self.dest_addr),\n                        proto_resp,\n                    ))));\n                }\n                SocketError::HostUnreachable => {\n                    let error_addr = tcp_socket.icmp_error_info()?;\n                    return Ok(Some(Response::TimeExceeded(\n                        ResponseData::new(SystemTime::now(), error_addr, proto_resp),\n                        IcmpPacketCode(1),\n                        None,\n                    )));\n                }\n                SocketError::Other(_) => {}\n            },\n        }\n        Ok(None)\n    }\n\n    fn extract_probe_resp(\n        &self,\n        icmp_v6: &IcmpPacket<'_>,\n        src: Ipv6Addr,\n    ) -> Result<Option<Response>> {\n        let recv = SystemTime::now();\n        let ip = IpAddr::V6(src);\n        let icmp_type = icmp_v6.get_icmp_type();\n        let icmp_code = icmp_v6.get_icmp_code();\n        Ok(match icmp_type {\n            IcmpType::TimeExceeded => {\n                if IcmpTimeExceededCode::from(icmp_code) == IcmpTimeExceededCode::TtlExpired {\n                    let packet = TimeExceededPacket::new_view(icmp_v6.packet())?;\n                    let (nested_ipv6, extension) = match self.icmp_extension_mode {\n                        IcmpExtensionParseMode::Enabled => {\n                            let ipv6 = Ipv6Packet::new_view(packet.payload())?;\n                            let ext = packet.extension().map(Extensions::try_from).transpose()?;\n                            (ipv6, ext)\n                        }\n                        IcmpExtensionParseMode::Disabled => {\n                            let ipv6 = Ipv6Packet::new_view(packet.payload_raw())?;\n                            (ipv6, None)\n                        }\n                    };\n                    self.extract_probe_proto_resp(&nested_ipv6)?\n                        .map(|proto_resp| {\n                            Response::TimeExceeded(\n                                ResponseData::new(recv, ip, proto_resp),\n                                IcmpPacketCode(icmp_code.0),\n                                extension,\n                            )\n                        })\n                } else {\n                    None\n                }\n            }\n            IcmpType::DestinationUnreachable => {\n                let packet = DestinationUnreachablePacket::new_view(icmp_v6.packet())?;\n                let nested_ipv6 = Ipv6Packet::new_view(packet.payload())?;\n                let extension = match self.icmp_extension_mode {\n                    IcmpExtensionParseMode::Enabled => {\n                        packet.extension().map(Extensions::try_from).transpose()?\n                    }\n                    IcmpExtensionParseMode::Disabled => None,\n                };\n                self.extract_probe_proto_resp(&nested_ipv6)?\n                    .map(|proto_resp| {\n                        Response::DestinationUnreachable(\n                            ResponseData::new(recv, ip, proto_resp),\n                            IcmpPacketCode(icmp_code.0),\n                            extension,\n                        )\n                    })\n            }\n            IcmpType::EchoReply => match self.protocol {\n                Protocol::Icmp => {\n                    let packet = EchoReplyPacket::new_view(icmp_v6.packet())?;\n                    let id = packet.get_identifier();\n                    let seq = packet.get_sequence();\n                    let proto_resp =\n                        ProtocolResponse::Icmp(IcmpProtocolResponse::new(id, seq, None));\n                    Some(Response::EchoReply(\n                        ResponseData::new(recv, ip, proto_resp),\n                        IcmpPacketCode(icmp_code.0),\n                    ))\n                }\n                Protocol::Udp | Protocol::Tcp => None,\n            },\n            _ => None,\n        })\n    }\n\n    fn extract_probe_proto_resp(&self, ipv6: &Ipv6Packet<'_>) -> Result<Option<ProtocolResponse>> {\n        Ok(match (self.protocol, ipv6.get_next_header()) {\n            (Protocol::Icmp, IpProtocol::IcmpV6) => {\n                let (identifier, sequence) = extract_echo_request(ipv6)?;\n                Some(ProtocolResponse::Icmp(IcmpProtocolResponse::new(\n                    identifier,\n                    sequence,\n                    Some(TypeOfService(ipv6.get_traffic_class())),\n                )))\n            }\n            (Protocol::Udp, IpProtocol::Udp) => {\n                let (src_port, dest_port, actual_checksum, udp_payload_len) =\n                    extract_udp_packet(ipv6)?;\n                let has_magic = udp_payload_has_magic_prefix(ipv6)?;\n                let payload_len = if has_magic {\n                    udp_payload_len - MAGIC.len() as u16\n                } else {\n                    udp_payload_len\n                };\n                Some(ProtocolResponse::Udp(UdpProtocolResponse::new(\n                    0,\n                    IpAddr::V6(ipv6.get_destination_address()),\n                    src_port,\n                    dest_port,\n                    Some(TypeOfService(ipv6.get_traffic_class())),\n                    actual_checksum,\n                    actual_checksum,\n                    payload_len,\n                    has_magic,\n                )))\n            }\n            (Protocol::Tcp, IpProtocol::Tcp) => {\n                let (src_port, dest_port) = extract_tcp_packet(ipv6)?;\n                Some(ProtocolResponse::Tcp(TcpProtocolResponse::new(\n                    IpAddr::V6(ipv6.get_destination_address()),\n                    src_port,\n                    dest_port,\n                    Some(TypeOfService(ipv6.get_traffic_class())),\n                )))\n            }\n            _ => None,\n        })\n    }\n\n    /// Create a `UdpPacket`\n    fn make_udp_packet<'a>(\n        &self,\n        udp_buf: &'a mut [u8],\n        src_port: u16,\n        dest_port: u16,\n        payload: &'_ [u8],\n    ) -> Result<UdpPacket<'a>> {\n        let udp_packet_size = UdpPacket::minimum_packet_size() + payload.len();\n        let mut udp = UdpPacket::new(&mut udp_buf[..udp_packet_size])?;\n        udp.set_source(src_port);\n        udp.set_destination(dest_port);\n        udp.set_length(udp_packet_size as u16);\n        udp.set_payload(payload);\n        udp.set_checksum(udp_ipv6_checksum(\n            udp.packet(),\n            self.src_addr,\n            self.dest_addr,\n        ));\n        Ok(udp)\n    }\n\n    /// Create an ICMP `EchoRequest` packet.\n    fn make_echo_request_icmp_packet<'a>(\n        &self,\n        icmp_buf: &'a mut [u8],\n        identifier: TraceId,\n        sequence: Sequence,\n        payload_size: usize,\n    ) -> Result<EchoRequestPacket<'a>> {\n        let payload_buf = [self.payload_pattern.0; MAX_ICMP_PAYLOAD_BUF];\n        let packet_size = IcmpPacket::minimum_packet_size() + payload_size;\n        let mut icmp = EchoRequestPacket::new(&mut icmp_buf[..packet_size])?;\n        icmp.set_icmp_type(IcmpType::EchoRequest);\n        icmp.set_icmp_code(IcmpCode(0));\n        icmp.set_identifier(identifier.0);\n        icmp.set_payload(&payload_buf[..payload_size]);\n        icmp.set_sequence(sequence.0);\n        icmp.set_checksum(icmp_ipv6_checksum(\n            icmp.packet(),\n            self.src_addr,\n            self.dest_addr,\n        ));\n        Ok(icmp)\n    }\n}\n\nconst fn icmp_payload_size(packet_size: usize) -> usize {\n    let ip_header_size = Ipv6Packet::minimum_packet_size();\n    let icmp_header_size = IcmpPacket::minimum_packet_size();\n    packet_size - icmp_header_size - ip_header_size\n}\n\nconst fn udp_payload_size(packet_size: usize) -> usize {\n    let ip_header_size = Ipv6Packet::minimum_packet_size();\n    let udp_header_size = UdpPacket::minimum_packet_size();\n    packet_size - udp_header_size - ip_header_size\n}\n\nfn extract_echo_request(ipv6: &Ipv6Packet<'_>) -> Result<(u16, u16)> {\n    let echo_request_packet = EchoRequestPacket::new_view(ipv6.payload())?;\n    Ok((\n        echo_request_packet.get_identifier(),\n        echo_request_packet.get_sequence(),\n    ))\n}\n\nfn extract_udp_packet(ipv6: &Ipv6Packet<'_>) -> Result<(u16, u16, u16, u16)> {\n    let udp_packet = UdpPacket::new_view(ipv6.payload())?;\n    Ok((\n        udp_packet.get_source(),\n        udp_packet.get_destination(),\n        udp_packet.get_checksum(),\n        udp_packet.get_length() - UdpPacket::minimum_packet_size() as u16,\n    ))\n}\n\n/// From [rfc4443] (section 2.4, point c):\n///\n///    \"Every `ICMPv6` error message (type < 128) MUST include as much of\n///    the IPv6 offending (invoking) packet (the packet that caused the\n///    error) as possible without making the error message packet exceed\n///    the minimum IPv6 MTU\"\n///\n/// From [rfc2460] (section 5):\n///\n///    \"IPv6 requires that every link in the internet have an MTU of 1280\n///    octets or greater.  On any link that cannot convey a 1280-octet\n///    packet in one piece, link-specific fragmentation and reassembly must\n///    be provided at a layer below IPv6.\"\n///\n/// The maximum packet size we allow is 1024, and so we can safely assume that the originating IPv6\n/// packet being extracted will be at least as large as the minimum IPv6 packet size.\n///\n/// [rfc4443]: https://datatracker.ietf.org/doc/html/rfc4443#section-2.4\n/// [rfc2460]: https://datatracker.ietf.org/doc/html/rfc2460#section-5\nfn extract_tcp_packet(ipv6: &Ipv6Packet<'_>) -> Result<(u16, u16)> {\n    let tcp_packet = TcpPacket::new_view(ipv6.payload())?;\n    Ok((tcp_packet.get_source(), tcp_packet.get_destination()))\n}\n\nfn udp_payload_has_magic_prefix(ipv6: &Ipv6Packet<'_>) -> Result<bool> {\n    let udp_packet = UdpPacket::new_view(ipv6.payload())?;\n    Ok(udp_packet.payload().starts_with(MAGIC))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::error::IoResult;\n    use crate::mocket_recv_from;\n    use crate::net::socket::MockSocket;\n    use crate::{Flags, Port, RoundId, TimeToLive};\n    use mockall::predicate;\n    use std::str::FromStr;\n    use std::sync::Mutex;\n\n    static MTX: Mutex<()> = Mutex::new(());\n\n    // Test dispatching an IPv6/ICMP probe.\n    #[test]\n    fn test_dispatch_icmp_probe_no_payload() -> anyhow::Result<()> {\n        let probe = make_icmp_probe();\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let packet_size = PacketSize(48);\n        let payload_pattern = PayloadPattern(0x00);\n        let expected_send_to_buf = hex_literal::hex!(\"80 00 75 a2 04 d2 82 9a\");\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n        mocket\n            .expect_set_unicast_hops_v6()\n            .times(1)\n            .with(predicate::eq(10))\n            .returning(|_| Ok(()));\n        mocket\n            .expect_set_tclass_v6()\n            .times(1)\n            .with(predicate::eq(0))\n            .returning(|_| Ok(()));\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            ..Default::default()\n        };\n        ipv6.dispatch_icmp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_icmp_probe_with_payload() -> anyhow::Result<()> {\n        let probe = make_icmp_probe();\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let packet_size = PacketSize(68);\n        let payload_pattern = PayloadPattern(0xff);\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            80 00 75 8e 04 d2 82 9a ff ff ff ff ff ff ff ff\n            ff ff ff ff ff ff ff ff ff ff ff ff\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n        mocket\n            .expect_set_unicast_hops_v6()\n            .times(1)\n            .with(predicate::eq(10))\n            .returning(|_| Ok(()));\n        mocket\n            .expect_set_tclass_v6()\n            .times(1)\n            .with(predicate::eq(0))\n            .returning(|_| Ok(()));\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            ..Default::default()\n        };\n        ipv6.dispatch_icmp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_icmp_probe_invalid_packet_size_low() -> anyhow::Result<()> {\n        let probe = make_icmp_probe();\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let packet_size = PacketSize(47);\n        let payload_pattern = PayloadPattern(0x00);\n        let mut mocket = MockSocket::new();\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            ..Default::default()\n        };\n        let err = ipv6.dispatch_icmp_probe(&mut mocket, &probe).unwrap_err();\n        assert!(matches!(err, Error::InvalidPacketSize(_)));\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_icmp_probe_invalid_packet_size_high() -> anyhow::Result<()> {\n        let probe = make_icmp_probe();\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let packet_size = PacketSize(1025);\n        let payload_pattern = PayloadPattern(0x00);\n        let mut mocket = MockSocket::new();\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            ..Default::default()\n        };\n        let err = ipv6.dispatch_icmp_probe(&mut mocket, &probe).unwrap_err();\n        assert!(matches!(err, Error::InvalidPacketSize(_)));\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_privileged_no_payload() -> anyhow::Result<()> {\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(48);\n        let payload_pattern = PayloadPattern(0x00);\n        let initial_sequence = Sequence(33434);\n        let expected_send_to_buf = hex_literal::hex!(\"00 7b 01 c8 00 08 7a ed\");\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n        mocket\n            .expect_set_unicast_hops_v6()\n            .times(1)\n            .with(predicate::eq(10))\n            .returning(|_| Ok(()));\n        mocket\n            .expect_set_tclass_v6()\n            .times(1)\n            .with(predicate::eq(0))\n            .returning(|_| Ok(()));\n\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            initial_sequence,\n            ..Default::default()\n        };\n        ipv6.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_privileged_with_payload() -> anyhow::Result<()> {\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(56);\n        let payload_pattern = PayloadPattern(0xaa);\n        let initial_sequence = Sequence(33434);\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            00 7b 01 c8 00 10 d0 32 aa aa aa aa aa aa aa aa\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n        mocket\n            .expect_set_unicast_hops_v6()\n            .times(1)\n            .with(predicate::eq(10))\n            .returning(|_| Ok(()));\n        mocket\n            .expect_set_tclass_v6()\n            .times(1)\n            .with(predicate::eq(0))\n            .returning(|_| Ok(()));\n\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            initial_sequence,\n            ..Default::default()\n        };\n        ipv6.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_paris_privileged() -> anyhow::Result<()> {\n        let probe = Probe {\n            flags: Flags::PARIS_CHECKSUM,\n            ..make_udp_probe(123, 456)\n        };\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        // packet size and payload pattern are ignored for paris mode as a\n        // fixed two byte payload is used to hold the sequence\n        let packet_size = PacketSize(300);\n        let payload_pattern = PayloadPattern(0xaa);\n        let initial_sequence = Sequence(33434);\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            00 7b 01 c8 00 0a 82 9a f8 4e\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n        mocket\n            .expect_set_unicast_hops_v6()\n            .times(1)\n            .with(predicate::eq(10))\n            .returning(|_| Ok(()));\n        mocket\n            .expect_set_tclass_v6()\n            .times(1)\n            .with(predicate::eq(0))\n            .returning(|_| Ok(()));\n\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            initial_sequence,\n            ..Default::default()\n        };\n        ipv6.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    // Here we send probe 33007 (the 8th probe when starting from 33434) and\n    // so the payload will be 13 octets in length (7 + 6 for the magic prefix\n    // \"trippy\").\n    #[test]\n    fn test_dispatch_udp_probe_dublin_privileged() -> anyhow::Result<()> {\n        let probe = Probe {\n            flags: Flags::DUBLIN_IPV6_PAYLOAD_LENGTH,\n            sequence: Sequence(33441),\n            identifier: TraceId(33441),\n            ..make_udp_probe(123, 456)\n        };\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        // packet size and payload pattern are ignored for ipv6/udp/dublin mode.\n        let packet_size = PacketSize(300);\n        let payload_pattern = PayloadPattern(0xaa);\n        let initial_sequence = Sequence(33434);\n        let expected_send_to_buf = hex_literal::hex!(\n            \"\n            00 7b 01 c8 00 15 82 76\n            74 72 69 70 70 79 aa aa\n            aa aa aa aa aa\n            \"\n        );\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 0);\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_send_to()\n            .with(\n                predicate::eq(expected_send_to_buf),\n                predicate::eq(expected_send_to_addr),\n            )\n            .times(1)\n            .returning(|_, _| Ok(()));\n        mocket\n            .expect_set_unicast_hops_v6()\n            .times(1)\n            .with(predicate::eq(10))\n            .returning(|_| Ok(()));\n        mocket\n            .expect_set_tclass_v6()\n            .times(1)\n            .with(predicate::eq(0))\n            .returning(|_| Ok(()));\n\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            initial_sequence,\n            ..Default::default()\n        };\n        ipv6.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_unprivileged_no_payload() -> anyhow::Result<()> {\n        let _m = MTX.lock();\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let privilege_mode = PrivilegeMode::Unprivileged;\n        let packet_size = PacketSize(48);\n        let payload_pattern = PayloadPattern(0x00);\n        let initial_sequence = Sequence(33434);\n        let expected_send_to_buf = hex_literal::hex!(\"\");\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456);\n        let expected_bind_addr = SocketAddr::new(IpAddr::V6(src_addr), 123);\n        let expected_set_unicast_hops_v6 = 10;\n\n        let mut mocket = MockSocket::new();\n\n        let ctx = MockSocket::new_udp_send_socket_ipv6_context();\n        ctx.expect().with(predicate::eq(false)).returning(move |_| {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_unicast_hops_v6()\n                .times(1)\n                .with(predicate::eq(expected_set_unicast_hops_v6))\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_tclass_v6()\n                .times(1)\n                .with(predicate::eq(0))\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_send_to()\n                .with(\n                    predicate::eq(expected_send_to_buf),\n                    predicate::eq(expected_send_to_addr),\n                )\n                .times(1)\n                .returning(|_, _| Ok(()));\n\n            Ok(mocket)\n        });\n\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            initial_sequence,\n            ..Default::default()\n        };\n        ipv6.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_classic_unprivileged_with_payload() -> anyhow::Result<()> {\n        let _m = MTX.lock();\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let privilege_mode = PrivilegeMode::Unprivileged;\n        let packet_size = PacketSize(56);\n        let payload_pattern = PayloadPattern(0x1f);\n        let initial_sequence = Sequence(33434);\n        let expected_send_to_buf = hex_literal::hex!(\"1f 1f 1f 1f 1f 1f 1f 1f\");\n        let expected_send_to_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456);\n        let expected_bind_addr = SocketAddr::new(IpAddr::V6(src_addr), 123);\n        let expected_set_unicast_hops_v6 = 10;\n\n        let mut mocket = MockSocket::new();\n\n        let ctx = MockSocket::new_udp_send_socket_ipv6_context();\n        ctx.expect().with(predicate::eq(false)).returning(move |_| {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_unicast_hops_v6()\n                .times(1)\n                .with(predicate::eq(expected_set_unicast_hops_v6))\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_tclass_v6()\n                .times(1)\n                .with(predicate::eq(0))\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_send_to()\n                .with(\n                    predicate::eq(expected_send_to_buf),\n                    predicate::eq(expected_send_to_addr),\n                )\n                .times(1)\n                .returning(|_, _| Ok(()));\n\n            Ok(mocket)\n        });\n\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            initial_sequence,\n            ..Default::default()\n        };\n        ipv6.dispatch_udp_probe(&mut mocket, &probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_invalid_packet_size_low() -> anyhow::Result<()> {\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(47);\n        let payload_pattern = PayloadPattern(0x00);\n        let initial_sequence = Sequence(33434);\n        let mut mocket = MockSocket::new();\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            initial_sequence,\n            ..Default::default()\n        };\n        let err = ipv6.dispatch_udp_probe(&mut mocket, &probe).unwrap_err();\n        assert!(matches!(err, Error::InvalidPacketSize(_)));\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_udp_probe_invalid_packet_size_high() -> anyhow::Result<()> {\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let privilege_mode = PrivilegeMode::Privileged;\n        let packet_size = PacketSize(1025);\n        let payload_pattern = PayloadPattern(0x00);\n        let initial_sequence = Sequence(33434);\n        let mut mocket = MockSocket::new();\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            packet_size,\n            payload_pattern,\n            privilege_mode,\n            initial_sequence,\n            ..Default::default()\n        };\n        let err = ipv6.dispatch_udp_probe(&mut mocket, &probe).unwrap_err();\n        assert!(matches!(err, Error::InvalidPacketSize(_)));\n        Ok(())\n    }\n\n    #[test]\n    fn test_dispatch_tcp_probe() -> anyhow::Result<()> {\n        let _m = MTX.lock();\n        let probe = make_udp_probe(123, 456);\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\")?;\n        let dest_addr = Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?;\n        let expected_bind_addr = SocketAddr::new(IpAddr::V6(src_addr), 123);\n        let expected_set_unicast_hops_v6 = 10;\n        let expected_connect_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456);\n\n        let ctx = MockSocket::new_stream_socket_ipv6_context();\n        ctx.expect().returning(move || {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_unicast_hops_v6()\n                .times(1)\n                .with(predicate::eq(expected_set_unicast_hops_v6))\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_set_tclass_v6()\n                .times(1)\n                .with(predicate::eq(0))\n                .returning(|_| Ok(()));\n\n            mocket\n                .expect_connect()\n                .with(predicate::eq(expected_connect_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n\n            Ok(mocket)\n        });\n\n        let ipv6 = Ipv6 {\n            src_addr,\n            dest_addr,\n            ..Default::default()\n        };\n        ipv6.dispatch_tcp_probe::<MockSocket>(&probe)?;\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_echo_reply() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            81 00 52 c0 55 b9 81 26 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(1)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::EchoReply(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Icmp(IcmpProtocolResponse {\n                        identifier,\n                        sequence,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n        ) = resp\n        else {\n            panic!(\"expected EchoReply\")\n        };\n        assert_eq!(recv_from_addr, addr);\n        assert_eq!(21945, identifier);\n        assert_eq!(33062, sequence);\n        assert_eq!(None, tos);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_time_exceeded_icmp_no_extensions() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            03 00 4e c5 00 00 00 00 60 0f 08 00 00 2c 3a 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 04 4e 42 00 00 00 00 00 00 00 00 00 00 00 81\n            80 00 53 c6 55 b9 81 20 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(1)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Icmp(IcmpProtocolResponse {\n                        identifier,\n                        sequence,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(recv_from_addr, addr);\n        assert_eq!(21945, identifier);\n        assert_eq!(33056, sequence);\n        assert_eq!(Some(TypeOfService(0)), tos);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_destination_unreachable_icmp_no_extensions() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            01 00 ad ba 00 00 00 00 60 06 08 00 00 2c 3a 02\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            14 04 68 00 40 03 0c 02 00 00 00 00 00 00 00 69\n            80 00 02 62 57 a5 80 ed 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(1)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::DestinationUnreachable(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Icmp(IcmpProtocolResponse {\n                        identifier,\n                        sequence,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected DestinationUnreachable\")\n        };\n        assert_eq!(recv_from_addr, addr);\n        assert_eq!(22437, identifier);\n        assert_eq!(33005, sequence);\n        assert_eq!(Some(TypeOfService(0)), tos);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_time_exceeded_udp_no_extensions() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            03 00 7b a7 00 00 00 00 60 04 04 00 00 2c 11 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 04 4e 42 00 00 00 00 00 00 00 00 00 00 00 81\n            58 a6 81 05 00 2c d0 f1 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(1)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Udp(UdpProtocolResponse {\n                        identifier,\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                        expected_udp_checksum,\n                        actual_udp_checksum,\n                        payload_len,\n                        has_magic,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(recv_from_addr, addr);\n        assert_eq!(0, identifier);\n        assert_eq!(IpAddr::V6(Ipv6Addr::from_str(\"2a04:4e42::81\")?), dest_addr);\n        assert_eq!(22694, src_port);\n        assert_eq!(33029, dest_port);\n        assert_eq!(Some(TypeOfService(0)), tos);\n        assert_eq!(53489, expected_udp_checksum);\n        assert_eq!(53489, actual_udp_checksum);\n        assert_eq!(36, payload_len);\n        assert!(!has_magic);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_destination_unreachable_udp_no_extensions() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            01 00 a5 f5 00 00 00 00 60 03 08 00 00 2c 11 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 00 14 50 40 09 08 1f 00 00 00 00 00 00 20 0e\n            67 6d 81 5e 00 2c 94 12 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(1)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::DestinationUnreachable(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Udp(UdpProtocolResponse {\n                        identifier,\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                        expected_udp_checksum,\n                        actual_udp_checksum,\n                        payload_len,\n                        has_magic,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected DestinationUnreachable\")\n        };\n        assert_eq!(recv_from_addr, addr);\n        assert_eq!(0, identifier);\n        assert_eq!(\n            IpAddr::V6(Ipv6Addr::from_str(\"2a00:1450:4009:81f::200e\")?),\n            dest_addr\n        );\n        assert_eq!(26477, src_port);\n        assert_eq!(33118, dest_port);\n        assert_eq!(Some(TypeOfService(0)), tos);\n        assert_eq!(37906, expected_udp_checksum);\n        assert_eq!(37906, actual_udp_checksum);\n        assert_eq!(36, payload_len);\n        assert!(!has_magic);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    // Here we receive a `TimeExceeded` in UDP/Dublin mode and so extract the\n    // sequence from the length of the UDP payload, after subtracting the\n    // length of the magic prefix \"trippy\" (11 - 6 == 5).\n    //\n    // Note we do not know if we are in UDP/Dublin mode when decoding the\n    // packet and so the decision to reject the probe response is left to\n    // the `tracer::Tracer::validate(..)` function.\n    #[test]\n    fn test_recv_icmp_probe_time_exceeded_udp_dublin_with_magic() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            03 00 23 6f 00 00 00 00 60 0e 0e 00 00 13 11 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 00 14 50 40 09 08 20 00 00 00 00 00 00 20 0e\n            80 e8 13 88 00 13 9a 42 74 72 69 70 70 79 00 00\n            00 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(1)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Udp(UdpProtocolResponse {\n                        identifier,\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                        expected_udp_checksum,\n                        actual_udp_checksum,\n                        payload_len,\n                        has_magic,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(recv_from_addr, addr);\n        assert_eq!(0, identifier);\n        assert_eq!(\n            IpAddr::V6(Ipv6Addr::from_str(\"2a00:1450:4009:820::200e\")?),\n            dest_addr\n        );\n        assert_eq!(33000, src_port);\n        assert_eq!(5000, dest_port);\n        assert_eq!(Some(TypeOfService(0)), tos);\n        assert_eq!(39490, expected_udp_checksum);\n        assert_eq!(39490, actual_udp_checksum);\n        assert_eq!(5, payload_len);\n        assert!(has_magic);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_time_exceeded_tcp_no_extensions() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            03 00 f0 2d 00 00 00 00 68 0b 09 00 00 2c 06 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 00 14 50 40 09 08 15 00 00 00 00 00 00 20 0e\n            81 0e 00 50 aa c4 08 e6 00 00 00 00 b0 c2 ff ff\n            6d b4 00 00 02 04 04 c4 01 03 03 06 01 01 08 0a\n            cc f7 44 c9 00 00 00 00 04 02 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(1)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Tcp(TcpProtocolResponse {\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(recv_from_addr, addr);\n        assert_eq!(\n            IpAddr::V6(Ipv6Addr::from_str(\"2a00:1450:4009:815::200e\")?),\n            dest_addr\n        );\n        assert_eq!(33038, src_port);\n        assert_eq!(80, dest_port);\n        assert_eq!(Some(TypeOfService(128)), tos);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_destination_unreachable_tcp_no_extensions() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            01 00 b1 e9 00 00 00 00 60 04 07 00 00 2c 06 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 00 14 50 40 09 08 21 00 00 00 00 00 00 20 0e\n            81 24 00 7b 35 d2 32 c6 00 00 00 00 b0 c2 ff ff\n            71 b2 00 00 02 04 04 c4 01 03 03 06 01 01 08 0a\n            fa 0b 5e 7c 00 00 00 00 04 02 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(1)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Disabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?.unwrap();\n\n        let Response::DestinationUnreachable(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Tcp(TcpProtocolResponse {\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected DestinationUnreachable\")\n        };\n        assert_eq!(recv_from_addr, addr);\n        assert_eq!(\n            IpAddr::V6(Ipv6Addr::from_str(\"2a00:1450:4009:821::200e\")?),\n            dest_addr\n        );\n        assert_eq!(33060, src_port);\n        assert_eq!(123, dest_port);\n        assert_eq!(Some(TypeOfService(0)), tos);\n        assert_eq!(IcmpPacketCode(0), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_wrong_icmp_original_datagram_type_ignored() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            03 00 4e c5 00 00 00 00 60 0f 08 00 00 2c 3a 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 04 4e 42 00 00 00 00 00 00 00 00 00 00 00 81\n            80 00 53 c6 55 b9 81 20 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(3)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_some());\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_wrong_udp_original_datagram_type_ignored() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            03 00 7b a7 00 00 00 00 60 04 04 00 00 2c 11 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 04 4e 42 00 00 00 00 00 00 00 00 00 00 00 81\n            58 a6 81 05 00 2c d0 f1 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(3)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_some());\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_icmp_probe_wrong_tcp_original_datagram_type_ignored() -> anyhow::Result<()> {\n        let recv_from_addr = IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?);\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            03 00 f0 2d 00 00 00 00 68 0b 09 00 00 2c 06 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 00 14 50 40 09 08 15 00 00 00 00 00 00 20 0e\n            81 0e 00 50 aa c4 08 e6 00 00 00 00 b0 c2 ff ff\n            6d b4 00 00 02 04 04 c4 01 03 03 06 01 01 08 0a\n            cc f7 44 c9 00 00 00 00 04 02 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(recv_from_addr, 0);\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(3)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Tcp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_some());\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Icmp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_tcp_socket_tcp_reply() -> anyhow::Result<()> {\n        let dest_addr = Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?;\n        let expected_peer_addr = SocketAddr::new(IpAddr::V6(dest_addr), 456);\n\n        let mut mocket = MockSocket::new();\n        mocket.expect_take_error().times(1).returning(|| Ok(None));\n        mocket\n            .expect_peer_addr()\n            .times(1)\n            .returning(move || Ok(Some(expected_peer_addr)));\n        mocket.expect_shutdown().times(1).returning(|| Ok(()));\n\n        let ipv6 = Ipv6 {\n            dest_addr,\n            ..Default::default()\n        };\n        let resp = ipv6\n            .recv_tcp_socket(&mut mocket, Port(33434), Port(456))?\n            .unwrap();\n\n        let Response::TcpReply(ResponseData {\n            addr,\n            proto_resp:\n                ProtocolResponse::Tcp(TcpProtocolResponse {\n                    dest_addr,\n                    src_port,\n                    dest_port,\n                    tos,\n                }),\n            ..\n        }) = resp\n        else {\n            panic!(\"expected TcpReply\")\n        };\n        assert_eq!(dest_addr, addr);\n        assert_eq!(33434, src_port);\n        assert_eq!(456, dest_port);\n        assert_eq!(None, tos);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_tcp_socket_tcp_refused() -> anyhow::Result<()> {\n        let dest_addr = Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?;\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_take_error()\n            .times(1)\n            .returning(|| Ok(Some(SocketError::ConnectionRefused)));\n\n        let ipv6 = Ipv6 {\n            dest_addr,\n            ..Default::default()\n        };\n        let resp = ipv6\n            .recv_tcp_socket(&mut mocket, Port(33434), Port(80))?\n            .unwrap();\n\n        let Response::TcpRefused(ResponseData {\n            addr,\n            proto_resp:\n                ProtocolResponse::Tcp(TcpProtocolResponse {\n                    dest_addr,\n                    src_port,\n                    dest_port,\n                    tos,\n                }),\n            ..\n        }) = resp\n        else {\n            panic!(\"expected TcpRefused\")\n        };\n        assert_eq!(dest_addr, addr);\n        assert_eq!(33434, src_port);\n        assert_eq!(80, dest_port);\n        assert_eq!(None, tos);\n        Ok(())\n    }\n\n    #[test]\n    fn test_recv_tcp_socket_tcp_host_unreachable() -> anyhow::Result<()> {\n        let dest_addr = Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?;\n\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_take_error()\n            .times(1)\n            .returning(|| Ok(Some(SocketError::HostUnreachable)));\n        mocket\n            .expect_icmp_error_info()\n            .times(1)\n            .returning(move || Ok(IpAddr::V6(dest_addr)));\n\n        let ipv6 = Ipv6 {\n            dest_addr,\n            ..Default::default()\n        };\n        let resp = ipv6\n            .recv_tcp_socket(&mut mocket, Port(33434), Port(80))?\n            .unwrap();\n\n        let Response::TimeExceeded(\n            ResponseData {\n                addr,\n                proto_resp:\n                    ProtocolResponse::Tcp(TcpProtocolResponse {\n                        dest_addr,\n                        src_port,\n                        dest_port,\n                        tos,\n                    }),\n                ..\n            },\n            icmp_code,\n            extensions,\n        ) = resp\n        else {\n            panic!(\"expected TimeExceeded\")\n        };\n        assert_eq!(dest_addr, addr);\n        assert_eq!(33434, src_port);\n        assert_eq!(80, dest_port);\n        assert_eq!(None, tos);\n        assert_eq!(IcmpPacketCode(1), icmp_code);\n        assert_eq!(None, extensions);\n        Ok(())\n    }\n\n    // This ICMPv6 packet has code 1 (\"Fragment reassembly time exceeded\")\n    // and must be ignored.\n    //\n    // Note this is not real packet and so the length and checksum are not\n    // accurate.\n    #[test]\n    fn test_icmp_time_exceeded_fragment_reassembly_ignored() -> anyhow::Result<()> {\n        let expected_recv_from_buf = hex_literal::hex!(\n            \"\n            03 01 da 90 00 00 00 00 60 0f 02 00 00 2c 11 01\n            fd 7a 11 5c a1 e0 ab 12 48 43 cd 96 62 63 08 2a\n            2a 00 14 50 40 09 08 15 00 00 00 00 00 00 20 0e\n            95 ce 81 24 00 2c 65 f5 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n            00 00 00 00 00 00 00 00 00 00 00 00\n           \"\n        );\n        let expected_recv_from_addr = SocketAddr::new(\n            IpAddr::V6(Ipv6Addr::from_str(\"2604:a880:ffff:6:1::41c\")?),\n            0,\n        );\n        let mut mocket = MockSocket::new();\n        mocket\n            .expect_recv_from()\n            .times(1)\n            .returning(mocket_recv_from!(\n                expected_recv_from_buf,\n                expected_recv_from_addr\n            ));\n\n        let ipv6 = Ipv6 {\n            protocol: Protocol::Udp,\n            icmp_extension_mode: IcmpExtensionParseMode::Enabled,\n            ..Default::default()\n        };\n        let resp = ipv6.recv_icmp_probe(&mut mocket)?;\n        assert!(resp.is_none());\n        Ok(())\n    }\n\n    fn make_icmp_probe() -> Probe {\n        Probe::new(\n            Sequence(33434),\n            TraceId(1234),\n            Port(0),\n            Port(0),\n            TimeToLive(10),\n            RoundId(0),\n            SystemTime::now(),\n            Flags::empty(),\n        )\n    }\n\n    fn make_udp_probe(src_port: u16, dest_port: u16) -> Probe {\n        Probe::new(\n            Sequence(33434),\n            TraceId(1234),\n            Port(src_port),\n            Port(dest_port),\n            TimeToLive(10),\n            RoundId(0),\n            SystemTime::now(),\n            Flags::empty(),\n        )\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net/platform/byte_order.rs",
    "content": "use crate::error::Result;\nuse crate::net::platform::{Platform, PlatformImpl};\nuse std::net::IpAddr;\n\n/// The byte order to encode the `total_length`, `flags` and `fragment_offset` fields of the IPv4\n/// header.\n///\n/// To quote directly from the `mtr` source code (from `check_length_order` in `probe_unix.c`):\n///\n/// \"Nearly all fields in the IP header should be encoded in network byte\n/// order prior to passing to `send()`.  However, the required byte order of\n/// the length field of the IP header is inconsistent between operating\n/// systems and operating system versions.  FreeBSD 11 requires the length\n/// field in network byte order, but some older versions of FreeBSD\n/// require host byte order.  OS X requires the length field in host\n/// byte order.  Linux will accept either byte order.\"\n#[derive(Debug, Copy, Clone)]\npub enum Ipv4ByteOrder {\n    #[cfg(all(unix, not(target_os = \"linux\"), not(target_os = \"windows\")))]\n    Host,\n    Network,\n}\n\nimpl Ipv4ByteOrder {\n    /// Discover the required byte ordering for the IPv4 header fields `total_length`, `flags` and\n    /// `fragment_offset`.\n    ///\n    /// This is achieved by creating a raw socket and attempting to send an `IPv4` packet to\n    /// localhost with the `total_length` set in either host byte order or network byte order.\n    /// The OS will return an `InvalidInput` error if the buffer provided is smaller than the\n    /// `total_length` indicated, which will be the case when the byte order is set incorrectly.\n    ///\n    /// This is a little confusing as `Ipv4Packet::set_total_length` method will _always_ convert\n    /// from host byte order to network byte order (which will be a no-op on big-endian system)\n    /// and so to test the host byte order case we must try both the normal and the swapped byte\n    /// order.\n    ///\n    /// For example, for a packet of length 4660 bytes (dec):\n    ///\n    /// For a little-endian architecture:\n    ///\n    /// Try        Host (LE)    Wire (BE)   Order (if succeeds)\n    /// normal     34 12        12 34       `Ipv4ByteOrder::Network`\n    /// swapped    12 34        34 12       `Ipv4ByteOrder::Host`\n    ///\n    /// For a big-endian architecture:\n    ///\n    /// Try        Host (BE)    Wire (BE)   Order (if succeeds)\n    /// normal     12 34        12 34       `Ipv4ByteOrder::Host`\n    /// swapped    34 12        34 12       `Ipv4ByteOrder::Network`\n    pub fn for_address(addr: IpAddr) -> Result<Self> {\n        PlatformImpl::byte_order_for_address(addr)\n    }\n\n    /// Adjust the IPv4 `total_length` header.\n    #[must_use]\n    pub const fn adjust_length(self, ipv4_total_length: u16) -> u16 {\n        match self {\n            #[cfg(all(unix, not(target_os = \"linux\"), not(target_os = \"windows\")))]\n            Self::Host => ipv4_total_length.swap_bytes(),\n            Self::Network => ipv4_total_length,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net/platform/unix.rs",
    "content": "use crate::error::Result;\nuse crate::net::platform::{Ipv4ByteOrder, Platform};\nuse std::net::IpAddr;\n\npub struct PlatformImpl;\n\nimpl Platform for PlatformImpl {\n    fn byte_order_for_address(addr: IpAddr) -> Result<Ipv4ByteOrder> {\n        address::for_address(addr)\n    }\n    fn lookup_interface_addr(addr: IpAddr, name: &str) -> Result<IpAddr> {\n        address::lookup_interface_addr(addr, name)\n    }\n    fn discover_local_addr(target_addr: IpAddr, port: u16) -> Result<IpAddr> {\n        address::discover_local_addr(target_addr, port)\n    }\n}\n\nmod address {\n    use crate::error::{Error, Result};\n    use crate::net::SocketImpl;\n    use crate::net::platform::Ipv4ByteOrder;\n    use crate::net::socket::Socket;\n    use nix::sys::socket::{AddressFamily, SockaddrLike};\n    use std::net::{IpAddr, SocketAddr};\n    use tracing::instrument;\n\n    #[cfg(not(target_os = \"linux\"))]\n    use std::net::Ipv4Addr;\n\n    /// The size of the test packet to use for discovering the `total_length` byte order.\n    #[cfg(not(target_os = \"linux\"))]\n    const TEST_PACKET_LENGTH: u16 = 256;\n\n    /// Discover the required byte ordering for the IPv4 header fields `total_length`, `flags` and\n    /// `fragment_offset`.\n    ///\n    /// Linux accepts either network byte order or host byte order for the `total_length` field, and\n    /// so we skip the check and return network byte order unconditionally.\n    #[cfg(target_os = \"linux\")]\n    #[expect(clippy::unnecessary_wraps)]\n    pub const fn for_address(_src_addr: IpAddr) -> Result<Ipv4ByteOrder> {\n        Ok(Ipv4ByteOrder::Network)\n    }\n\n    #[cfg(not(target_os = \"linux\"))]\n    #[instrument(ret, level = \"trace\")]\n    pub fn for_address(addr: IpAddr) -> Result<Ipv4ByteOrder> {\n        let addr = match addr {\n            IpAddr::V4(addr) => addr,\n            IpAddr::V6(_) => return Ok(Ipv4ByteOrder::Network),\n        };\n        match test_send_local_ip4_packet(addr, TEST_PACKET_LENGTH) {\n            Ok(()) => Ok(Ipv4ByteOrder::Network),\n            Err(Error::IoError(io))\n                if io.kind() == crate::error::ErrorKind::Std(std::io::ErrorKind::InvalidInput) =>\n            {\n                match test_send_local_ip4_packet(addr, TEST_PACKET_LENGTH.swap_bytes()) {\n                    Ok(()) => Ok(Ipv4ByteOrder::Host),\n                    Err(err) => Err(err),\n                }\n            }\n            Err(err) => Err(err),\n        }\n    }\n\n    /// Attempt to send an `ICMP` packet to a local address.\n    ///\n    /// The packet is actually of length `256` bytes, but we set the `total_length` based on the\n    /// input provided to test if the OS rejects the attempt during the call to `send_to`.\n    ///\n    /// Note that this implementation will try to create an `IPPROTO_ICMP` socket and if that fails\n    /// it will fall back to creating an `IPPROTO_RAW` socket.\n    #[cfg(not(target_os = \"linux\"))]\n    #[instrument(ret, level = \"trace\")]\n    fn test_send_local_ip4_packet(src_addr: Ipv4Addr, total_length: u16) -> Result<()> {\n        use socket2::Protocol;\n        let mut icmp_buf = [0_u8; trippy_packet::icmpv4::IcmpPacket::minimum_packet_size()];\n        let mut icmp = trippy_packet::icmpv4::echo_request::EchoRequestPacket::new(&mut icmp_buf)?;\n        icmp.set_icmp_type(trippy_packet::icmpv4::IcmpType::EchoRequest);\n        icmp.set_icmp_code(trippy_packet::icmpv4::IcmpCode(0));\n        icmp.set_identifier(0);\n        icmp.set_sequence(0);\n        icmp.set_checksum(trippy_packet::checksum::icmp_ipv4_checksum(icmp.packet()));\n        let mut ipv4_buf = [0_u8; TEST_PACKET_LENGTH as usize];\n        let mut ipv4 = trippy_packet::ipv4::Ipv4Packet::new(&mut ipv4_buf)?;\n        ipv4.set_version(4);\n        ipv4.set_header_length(5);\n        ipv4.set_protocol(trippy_packet::IpProtocol::Icmp);\n        ipv4.set_ttl(255);\n        ipv4.set_source(src_addr);\n        ipv4.set_destination(Ipv4Addr::LOCALHOST);\n        ipv4.set_total_length(total_length);\n        ipv4.set_payload(icmp.packet());\n        let mut probe_socket = SocketImpl::new_dgram_ipv4(Protocol::ICMPV4)\n            .or_else(|_| SocketImpl::new_raw_ipv4(Protocol::from(nix::libc::IPPROTO_RAW)))?;\n        probe_socket.set_header_included(true)?;\n        let remote_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);\n        probe_socket.send_to(ipv4.packet(), remote_addr)?;\n        Ok(())\n    }\n\n    pub fn lookup_interface_addr(addr: IpAddr, name: &str) -> Result<IpAddr> {\n        match addr {\n            IpAddr::V4(_) => lookup_interface_addr_ipv4(name),\n            IpAddr::V6(_) => lookup_interface_addr_ipv6(name),\n        }\n    }\n\n    #[instrument(ret, level = \"trace\")]\n    fn lookup_interface_addr_ipv4(name: &str) -> Result<IpAddr> {\n        nix::ifaddrs::getifaddrs()\n            .map_err(|_| Error::UnknownInterface(name.to_string()))?\n            .find_map(|ia| {\n                ia.address.and_then(|addr| match addr.family() {\n                    Some(AddressFamily::Inet) if ia.interface_name == name => addr\n                        .as_sockaddr_in()\n                        .map(|sock_addr| IpAddr::V4(sock_addr.ip())),\n                    _ => None,\n                })\n            })\n            .ok_or_else(|| Error::UnknownInterface(name.to_string()))\n    }\n\n    #[instrument(ret, level = \"trace\")]\n    fn lookup_interface_addr_ipv6(name: &str) -> Result<IpAddr> {\n        nix::ifaddrs::getifaddrs()\n            .map_err(|_| Error::UnknownInterface(name.to_string()))?\n            .find_map(|ia| {\n                ia.address.and_then(|addr| match addr.family() {\n                    Some(AddressFamily::Inet6) if ia.interface_name == name => addr\n                        .as_sockaddr_in6()\n                        .map(|sock_addr| IpAddr::V6(sock_addr.ip())),\n                    _ => None,\n                })\n            })\n            .ok_or_else(|| Error::UnknownInterface(name.to_string()))\n    }\n\n    // Note that no packets are transmitted by this method.\n    #[instrument(ret, level = \"trace\")]\n    pub fn discover_local_addr(target_addr: IpAddr, port: u16) -> Result<IpAddr> {\n        let mut socket = match target_addr {\n            IpAddr::V4(_) => SocketImpl::new_udp_dgram_socket_ipv4(),\n            IpAddr::V6(_) => SocketImpl::new_udp_dgram_socket_ipv6(),\n        }?;\n        socket.connect(SocketAddr::new(target_addr, port))?;\n        Ok(socket.local_addr()?.ok_or(Error::MissingAddr)?.ip())\n    }\n}\n\nmod socket {\n    use crate::error::{ErrorKind, IoError, IoOperation};\n    use crate::error::{IoResult, Result};\n    use crate::net::socket::{Socket, SocketError};\n    use itertools::Itertools;\n    use nix::{\n        Error,\n        sys::select::FdSet,\n        sys::time::{TimeVal, TimeValLike},\n    };\n    use socket2::{Domain, Protocol, SockAddr, Type};\n    use std::io;\n    use std::io::Read;\n    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};\n    use std::net::{Shutdown, SocketAddr};\n    use std::os::fd::AsFd;\n    use std::time::Duration;\n    use tracing::instrument;\n\n    #[instrument(level = \"trace\")]\n    pub fn startup() -> Result<()> {\n        Ok(())\n    }\n\n    /// A network socket.\n    pub struct SocketImpl {\n        inner: socket2::Socket,\n    }\n\n    impl SocketImpl {\n        fn new(domain: Domain, ty: Type, protocol: Protocol) -> IoResult<Self> {\n            Ok(Self {\n                inner: socket2::Socket::new(domain, ty, Some(protocol))\n                    .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?,\n            })\n        }\n\n        pub(super) fn new_raw_ipv4(protocol: Protocol) -> IoResult<Self> {\n            Ok(Self {\n                inner: socket2::Socket::new(Domain::IPV4, Type::RAW, Some(protocol))\n                    .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?,\n            })\n        }\n\n        fn new_raw_ipv6(protocol: Protocol) -> IoResult<Self> {\n            Ok(Self {\n                inner: socket2::Socket::new(Domain::IPV6, Type::RAW, Some(protocol))\n                    .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?,\n            })\n        }\n\n        pub(super) fn new_dgram_ipv4(protocol: Protocol) -> IoResult<Self> {\n            Ok(Self {\n                inner: socket2::Socket::new(Domain::IPV4, Type::DGRAM, Some(protocol))\n                    .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?,\n            })\n        }\n\n        fn new_dgram_ipv6(protocol: Protocol) -> IoResult<Self> {\n            Ok(Self {\n                inner: socket2::Socket::new(Domain::IPV6, Type::DGRAM, Some(protocol))\n                    .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?,\n            })\n        }\n\n        fn set_nonblocking(&self, nonblocking: bool) -> IoResult<()> {\n            self.inner\n                .set_nonblocking(nonblocking)\n                .map_err(|err| IoError::Other(err, IoOperation::SetNonBlocking))\n        }\n\n        pub(super) fn local_addr(&self) -> IoResult<Option<SocketAddr>> {\n            Ok(self\n                .inner\n                .local_addr()\n                .map_err(|err| IoError::Other(err, IoOperation::LocalAddr))?\n                .as_socket())\n        }\n    }\n\n    impl Socket for SocketImpl {\n        #[instrument(level = \"trace\")]\n        fn new_icmp_send_socket_ipv4(raw: bool) -> IoResult<Self> {\n            if raw {\n                let mut socket = Self::new_raw_ipv4(Protocol::from(nix::libc::IPPROTO_RAW))?;\n                socket.set_nonblocking(true)?;\n                socket.set_header_included(true)?;\n                Ok(socket)\n            } else {\n                let mut socket = Self::new(Domain::IPV4, Type::DGRAM, Protocol::ICMPV4)?;\n                socket.set_nonblocking(true)?;\n                socket.set_header_included(true)?;\n                Ok(socket)\n            }\n        }\n        #[instrument(level = \"trace\")]\n        fn new_icmp_send_socket_ipv6(raw: bool) -> IoResult<Self> {\n            if raw {\n                let socket = Self::new_raw_ipv6(Protocol::ICMPV6)?;\n                socket.set_nonblocking(true)?;\n                Ok(socket)\n            } else {\n                let socket = Self::new_dgram_ipv6(Protocol::ICMPV6)?;\n                socket.set_nonblocking(true)?;\n                Ok(socket)\n            }\n        }\n        #[instrument(level = \"trace\")]\n        fn new_udp_send_socket_ipv4(raw: bool) -> IoResult<Self> {\n            if raw {\n                let mut socket = Self::new_raw_ipv4(Protocol::from(nix::libc::IPPROTO_RAW))?;\n                socket.set_nonblocking(true)?;\n                socket.set_header_included(true)?;\n                Ok(socket)\n            } else {\n                let socket = Self::new_dgram_ipv4(Protocol::UDP)?;\n                socket.set_nonblocking(true)?;\n                Ok(socket)\n            }\n        }\n        #[instrument(level = \"trace\")]\n        fn new_udp_send_socket_ipv6(raw: bool) -> IoResult<Self> {\n            if raw {\n                let socket = Self::new_raw_ipv6(Protocol::UDP)?;\n                socket.set_nonblocking(true)?;\n                Ok(socket)\n            } else {\n                let socket = Self::new_dgram_ipv6(Protocol::UDP)?;\n                socket.set_nonblocking(true)?;\n                Ok(socket)\n            }\n        }\n        #[instrument(level = \"trace\")]\n        fn new_recv_socket_ipv4(_: Ipv4Addr, raw: bool) -> IoResult<Self> {\n            if raw {\n                let mut socket = Self::new_raw_ipv4(Protocol::ICMPV4)?;\n                socket.set_nonblocking(true)?;\n                socket.set_header_included(true)?;\n                Ok(socket)\n            } else {\n                let socket = Self::new(Domain::IPV4, Type::DGRAM, Protocol::ICMPV4)?;\n                socket.set_nonblocking(true)?;\n                Ok(socket)\n            }\n        }\n        #[instrument(level = \"trace\")]\n        fn new_recv_socket_ipv6(_: Ipv6Addr, raw: bool) -> IoResult<Self> {\n            if raw {\n                let socket = Self::new_raw_ipv6(Protocol::ICMPV6)?;\n                socket.set_nonblocking(true)?;\n                Ok(socket)\n            } else {\n                let socket = Self::new_dgram_ipv6(Protocol::ICMPV6)?;\n                socket.set_nonblocking(true)?;\n                Ok(socket)\n            }\n        }\n        #[instrument(level = \"trace\")]\n        fn new_stream_socket_ipv4() -> IoResult<Self> {\n            let mut socket = Self::new(Domain::IPV4, Type::STREAM, Protocol::TCP)?;\n            socket.set_nonblocking(true)?;\n            socket.set_reuse_port(true)?;\n            Ok(socket)\n        }\n        #[instrument(level = \"trace\")]\n        fn new_stream_socket_ipv6() -> IoResult<Self> {\n            let mut socket = Self::new(Domain::IPV6, Type::STREAM, Protocol::TCP)?;\n            socket.set_nonblocking(true)?;\n            socket.set_reuse_port(true)?;\n            Ok(socket)\n        }\n        #[instrument(level = \"trace\")]\n        fn new_udp_dgram_socket_ipv4() -> IoResult<Self> {\n            Self::new_dgram_ipv4(Protocol::UDP)\n        }\n        #[instrument(level = \"trace\")]\n        fn new_udp_dgram_socket_ipv6() -> IoResult<Self> {\n            Self::new_dgram_ipv6(Protocol::UDP)\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn bind(&mut self, address: SocketAddr) -> IoResult<()> {\n            self.inner\n                .bind(&SockAddr::from(address))\n                .map_err(|err| IoError::Bind(err, address))\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn set_tos(&mut self, tos: u32) -> IoResult<()> {\n            self.inner\n                .set_tos_v4(tos)\n                .map_err(|err| IoError::Other(err, IoOperation::SetTos))\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn set_tclass_v6(&mut self, tclass: u32) -> IoResult<()> {\n            self.inner\n                .set_tclass_v6(tclass)\n                .map_err(|err| IoError::Other(err, IoOperation::SetTclassV6))\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn set_ttl(&mut self, ttl: u32) -> IoResult<()> {\n            self.inner\n                .set_ttl_v4(ttl)\n                .map_err(|err| IoError::Other(err, IoOperation::SetTtl))\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn set_reuse_port(&mut self, reuse: bool) -> IoResult<()> {\n            self.inner\n                .set_reuse_port(reuse)\n                .map_err(|err| IoError::Other(err, IoOperation::SetReusePort))\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn set_header_included(&mut self, included: bool) -> IoResult<()> {\n            self.inner\n                .set_header_included_v4(included)\n                .map_err(|err| IoError::Other(err, IoOperation::SetHeaderIncluded))\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn set_unicast_hops_v6(&mut self, hops: u8) -> IoResult<()> {\n            self.inner\n                .set_unicast_hops_v6(u32::from(hops))\n                .map_err(|err| IoError::Other(err, IoOperation::SetUnicastHopsV6))\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn connect(&mut self, address: SocketAddr) -> IoResult<()> {\n            tracing::trace!(?address);\n            self.inner\n                .connect(&SockAddr::from(address))\n                .map_err(|err| IoError::Connect(err, address))\n        }\n        #[instrument(skip(self, buf), level = \"trace\")]\n        fn send_to(&mut self, buf: &[u8], addr: SocketAddr) -> IoResult<()> {\n            tracing::trace!(buf = format!(\"{:02x?}\", buf.iter().format(\" \")), ?addr);\n            self.inner\n                .send_to(buf, &SockAddr::from(addr))\n                .map_err(|err| IoError::SendTo(err, addr))?;\n            Ok(())\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn is_readable(&mut self, timeout: Duration) -> IoResult<bool> {\n            let mut read = FdSet::new();\n            read.insert(self.inner.as_fd());\n            let readable = nix::sys::select::select(\n                None,\n                Some(&mut read),\n                None,\n                None,\n                Some(&mut TimeVal::milliseconds(timeout.as_millis() as i64)),\n            );\n            match readable {\n                Ok(readable) => Ok(readable == 1),\n                Err(Error::EINTR) => Ok(false),\n                Err(err) => Err(IoError::Other(\n                    std::io::Error::from(err),\n                    IoOperation::Select,\n                )),\n            }\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn is_writable(&mut self) -> IoResult<bool> {\n            let mut write = FdSet::new();\n            write.insert(self.inner.as_fd());\n            let writable = nix::sys::select::select(\n                None,\n                None,\n                Some(&mut write),\n                None,\n                Some(&mut TimeVal::zero()),\n            );\n            match writable {\n                Ok(writable) => Ok(writable == 1),\n                Err(Error::EINTR) => Ok(false),\n                Err(err) => Err(IoError::Other(\n                    std::io::Error::from(err),\n                    IoOperation::Select,\n                )),\n            }\n        }\n        #[instrument(skip(self, buf), level = \"trace\")]\n        fn recv_from(&mut self, buf: &mut [u8]) -> IoResult<(usize, Option<SocketAddr>)> {\n            let (bytes_read, addr) = self\n                .inner\n                .recv_from_into_buf(buf)\n                .map_err(|err| IoError::Other(err, IoOperation::RecvFrom))?;\n            tracing::trace!(\n                buf = format!(\"{:02x?}\", buf[..bytes_read].iter().format(\" \")),\n                bytes_read,\n                ?addr\n            );\n            Ok((bytes_read, addr))\n        }\n        #[instrument(skip(self, buf), level = \"trace\")]\n        fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {\n            let bytes_read = self\n                .inner\n                .read(buf)\n                .map_err(|err| IoError::Other(err, IoOperation::Read))?;\n            tracing::trace!(\n                buf = format!(\"{:02x?}\", buf[..bytes_read].iter().format(\" \")),\n                bytes_read\n            );\n            Ok(bytes_read)\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn shutdown(&mut self) -> IoResult<()> {\n            self.inner\n                .shutdown(Shutdown::Both)\n                .map_err(|err| IoError::Other(err, IoOperation::Shutdown))\n        }\n        #[instrument(skip(self), level = \"trace\")]\n        fn peer_addr(&mut self) -> IoResult<Option<SocketAddr>> {\n            let addr = self\n                .inner\n                .peer_addr()\n                .map_err(|err| IoError::Other(err, IoOperation::PeerAddr))?\n                .as_socket();\n            tracing::trace!(?addr);\n            Ok(addr)\n        }\n        #[instrument(skip(self), ret, level = \"trace\")]\n        fn take_error(&mut self) -> IoResult<Option<SocketError>> {\n            self.inner\n                .take_error()\n                .map(|err| {\n                    err.map(|e| match e.raw_os_error() {\n                        Some(errno) if Error::from_raw(errno) == Error::ECONNREFUSED => {\n                            SocketError::ConnectionRefused\n                        }\n                        _ => SocketError::Other(e),\n                    })\n                })\n                .map_err(|err| IoError::Other(err, IoOperation::TakeError))\n        }\n        #[instrument(skip(self), ret, level = \"trace\")]\n        fn icmp_error_info(&mut self) -> IoResult<IpAddr> {\n            Ok(IpAddr::V4(Ipv4Addr::UNSPECIFIED))\n        }\n    }\n\n    impl From<&io::Error> for ErrorKind {\n        fn from(value: &io::Error) -> Self {\n            if value.raw_os_error() == io::Error::from(Error::EINPROGRESS).raw_os_error() {\n                Self::InProgress\n            } else if value.raw_os_error() == io::Error::from(Error::EHOSTUNREACH).raw_os_error() {\n                Self::HostUnreachable\n            } else if value.raw_os_error() == io::Error::from(Error::ENETUNREACH).raw_os_error() {\n                Self::NetUnreachable\n            } else {\n                Self::Std(value.kind())\n            }\n        }\n    }\n\n    // only used for unit tests\n    impl From<ErrorKind> for io::Error {\n        fn from(value: ErrorKind) -> Self {\n            match value {\n                ErrorKind::InProgress => Self::from(Error::EINPROGRESS),\n                ErrorKind::HostUnreachable => Self::from(Error::EHOSTUNREACH),\n                ErrorKind::NetUnreachable => Self::from(Error::ENETUNREACH),\n                ErrorKind::Std(kind) => Self::from(kind),\n            }\n        }\n    }\n\n    impl Read for SocketImpl {\n        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {\n            self.inner.read(buf)\n        }\n    }\n\n    /// An extension trait to allow `recv_from` method which writes to a `&mut [u8]`.\n    ///\n    /// This is required for `socket2::Socket` which [does not currently provide] this method.\n    ///\n    /// [does not currently provide]: https://github.com/rust-lang/socket2/issues/223\n    trait RecvFrom {\n        fn recv_from_into_buf(&self, buf: &mut [u8]) -> io::Result<(usize, Option<SocketAddr>)>;\n    }\n\n    impl RecvFrom for socket2::Socket {\n        // Safety: the `recv` implementation promises not to write uninitialised\n        // bytes to the `buf`fer, so this casting is safe.\n        #![allow(unsafe_code)]\n        fn recv_from_into_buf(&self, buf: &mut [u8]) -> io::Result<(usize, Option<SocketAddr>)> {\n            let buf = unsafe {\n                &mut *(std::ptr::from_mut::<[u8]>(buf) as *mut [std::mem::MaybeUninit<u8>])\n            };\n            self.recv_from(buf)\n                .map(|(size, addr)| (size, addr.as_socket()))\n        }\n    }\n}\n\npub use socket::{SocketImpl, startup};\n"
  },
  {
    "path": "crates/trippy-core/src/net/platform/windows.rs",
    "content": "use super::byte_order::Ipv4ByteOrder;\nuse crate::error::{Error, ErrorKind, IoError, IoOperation, IoResult, Result};\nuse crate::net::channel::MAX_PACKET_SIZE;\nuse crate::net::platform::Platform;\nuse crate::net::platform::windows::adapter::Adapters;\nuse crate::net::socket::{Socket, SocketError};\nuse itertools::Itertools;\nuse socket2::{Domain, Protocol, SockAddr, Type};\nuse std::ffi::c_void;\nuse std::io::{Error as StdIoError, ErrorKind as StdErrorKind, Result as StdIoResult};\nuse std::mem::{size_of, zeroed};\nuse std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};\nuse std::os::windows::prelude::AsRawSocket;\nuse std::ptr::{addr_of, addr_of_mut, null_mut};\nuse std::time::Duration;\nuse tracing::instrument;\nuse windows_sys::Win32::Foundation::{WAIT_FAILED, WAIT_TIMEOUT};\nuse windows_sys::Win32::Networking::WinSock::{\n    AF_INET, AF_INET6, FD_CONNECT, FD_WRITE, ICMP_ERROR_INFO, IN_ADDR, IN_ADDR_0, IN6_ADDR,\n    IN6_ADDR_0, IPPROTO_RAW, IPPROTO_TCP, SIO_ROUTING_INTERFACE_QUERY, SO_ERROR,\n    SO_PORT_SCALABILITY, SO_REUSE_UNICASTPORT, SOCKADDR_IN, SOCKADDR_IN6, SOCKADDR_IN6_0,\n    SOCKADDR_STORAGE, SOCKET_ERROR, SOL_SOCKET, TCP_FAIL_CONNECT_ON_ICMP_ERROR,\n    TCP_ICMP_ERROR_INFO, WSA_IO_INCOMPLETE, WSA_IO_PENDING, WSABUF, WSADATA, WSAEADDRNOTAVAIL,\n    WSAECONNREFUSED, WSAEHOSTUNREACH, WSAEINPROGRESS, WSAENETUNREACH, WSAENOBUFS,\n};\nuse windows_sys::Win32::System::IO::OVERLAPPED;\n\n/// Execute a `Win32::Networking::WinSock` syscall.\n///\n/// The result of the syscall will be passed to the supplied boolean closure to determine if it\n/// represents an error and if so returns the last OS error, otherwise the result of the syscall is\n/// returned.\nmacro_rules! syscall {\n    ($fn: ident ( $($arg: expr),* $(,)* ), $err_fn: expr) => {{\n        #[expect(unsafe_code)]\n        let res = unsafe { windows_sys::Win32::Networking::WinSock::$fn($($arg, )*) };\n        if $err_fn(res) {\n            Err(StdIoError::last_os_error())\n        } else {\n            Ok(res)\n        }\n    }};\n}\n\n/// Execute a `Win32::NetworkManagement::IpHelper` syscall.\n///\n/// The raw result of the syscall is returned.\nmacro_rules! syscall_ip_helper {\n    ($fn: ident ( $($arg: expr),* $(,)* )) => {{\n        #[expect(unsafe_code)]\n        unsafe { windows_sys::Win32::NetworkManagement::IpHelper::$fn($($arg, )*) }\n    }};\n}\n\n/// Execute a `Win32::System::Threading` syscall.\n///\n/// The raw result of the syscall is returned.\nmacro_rules! syscall_threading {\n    ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{\n        #[expect(unsafe_code)]\n        unsafe { windows_sys::Win32::System::Threading::$fn($($arg, )*) }\n    }};\n}\n\npub struct PlatformImpl;\n\nimpl Platform for PlatformImpl {\n    fn byte_order_for_address(_addr: IpAddr) -> Result<Ipv4ByteOrder> {\n        Ok(Ipv4ByteOrder::Network)\n    }\n\n    fn lookup_interface_addr(addr: IpAddr, name: &str) -> Result<IpAddr> {\n        match addr {\n            IpAddr::V4(_) => lookup_interface_addr(&Adapters::ipv4()?, name),\n            IpAddr::V6(_) => lookup_interface_addr(&Adapters::ipv6()?, name),\n        }\n    }\n\n    fn discover_local_addr(target_addr: IpAddr, _port: u16) -> Result<IpAddr> {\n        routing_interface_query(target_addr)\n    }\n}\n\n#[instrument(level = \"trace\")]\npub fn startup() -> Result<()> {\n    SocketImpl::startup().map_err(Error::IoError)\n}\n\n/// `WinSock` version 2.2\nconst WINSOCK_VERSION: u16 = 0x202;\n\n/// A network socket.\npub struct SocketImpl {\n    inner: socket2::Socket,\n    ol: Box<OVERLAPPED>,\n    buf: Box<[u8]>,\n    from: Box<SOCKADDR_STORAGE>,\n    from_len: i32,\n    bytes_read: u32,\n}\n\n#[expect(clippy::cast_possible_wrap)]\nimpl SocketImpl {\n    fn startup() -> IoResult<()> {\n        let mut wsa_data = Self::new_wsa_data();\n        syscall!(WSAStartup(WINSOCK_VERSION, addr_of_mut!(wsa_data)), |res| {\n            res != 0\n        })\n        .map_err(|err| IoError::Other(err, IoOperation::Startup))\n        .map(|_| ())\n    }\n\n    fn new(domain: Domain, ty: Type, protocol: Option<Protocol>) -> IoResult<Self> {\n        let inner = socket2::Socket::new(domain, ty, protocol)\n            .map_err(|err| IoError::Other(err, IoOperation::NewSocket))?;\n        let from = Box::new(Self::new_sockaddr_storage());\n        let from_len = std::mem::size_of::<SOCKADDR_STORAGE>() as i32;\n        let ol = Box::new(Self::new_overlapped());\n        let buf = Box::new([0; MAX_PACKET_SIZE]);\n        Ok(Self {\n            inner,\n            ol,\n            buf,\n            from,\n            from_len,\n            bytes_read: 0,\n        })\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn create_event(&mut self) -> IoResult<()> {\n        self.ol.hEvent = syscall!(WSACreateEvent(), |res| { res == 0 || res == -1 })\n            .map_err(|err| IoError::Other(err, IoOperation::WSACreateEvent))?;\n        Ok(())\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn wait_for_event(&self, timeout: Duration) -> IoResult<bool> {\n        let millis = timeout.as_millis() as u32;\n        let rc = syscall_threading!(WaitForSingleObject(self.ol.hEvent, millis));\n        if rc == WAIT_TIMEOUT {\n            return Ok(false);\n        } else if rc == WAIT_FAILED {\n            return Err(IoError::Other(\n                StdIoError::last_os_error(),\n                IoOperation::WaitForSingleObject,\n            ));\n        }\n        Ok(true)\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn reset_event(&self) -> IoResult<()> {\n        syscall!(WSAResetEvent(self.ol.hEvent), |res| { res == 0 })\n            .map_err(|err| IoError::Other(err, IoOperation::WSAResetEvent))\n            .map(|_| ())\n    }\n\n    #[instrument(skip(self, optval), level = \"trace\")]\n    fn getsockopt<T>(&self, level: i32, optname: i32, mut optval: T) -> StdIoResult<T> {\n        let mut optlen = size_of::<T>() as i32;\n        syscall!(\n            getsockopt(\n                self.inner.as_raw_socket() as _,\n                level,\n                optname,\n                addr_of_mut!(optval).cast(),\n                &raw mut optlen,\n            ),\n            |res| res == SOCKET_ERROR\n        )?;\n        Ok(optval)\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn setsockopt_u32(&self, level: i32, optname: i32, optval: u32) -> StdIoResult<()> {\n        let bytes = optval.to_ne_bytes();\n        let optval = addr_of!(bytes).cast();\n        syscall!(\n            setsockopt(\n                self.inner.as_raw_socket() as _,\n                level,\n                optname,\n                optval,\n                size_of::<u32>() as i32,\n            ),\n            |res| res == SOCKET_ERROR\n        )\n        .map(|_| ())\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn setsockopt_bool(&self, level: i32, optname: i32, optval: bool) -> StdIoResult<()> {\n        self.setsockopt_u32(level, optname, u32::from(optval))\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn set_fail_connect_on_icmp_error(&self, enabled: bool) -> IoResult<()> {\n        self.setsockopt_bool(IPPROTO_TCP, TCP_FAIL_CONNECT_ON_ICMP_ERROR as _, enabled)\n            .map_err(|err| IoError::Other(err, IoOperation::SetTcpFailConnectOnIcmpError))\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn set_non_blocking(&self, is_non_blocking: bool) -> IoResult<()> {\n        self.inner\n            .set_nonblocking(is_non_blocking)\n            .map_err(|err| IoError::Other(err, IoOperation::SetNonBlocking))\n    }\n\n    // TODO handle case where `WSARecvFrom` succeeded immediately.\n    #[instrument(skip(self), level = \"trace\")]\n    fn post_recv_from(&mut self) -> IoResult<()> {\n        fn is_err(res: i32) -> bool {\n            res == SOCKET_ERROR\n                && StdIoError::last_os_error().raw_os_error() != Some(WSA_IO_PENDING)\n        }\n        let wbuf = WSABUF {\n            len: MAX_PACKET_SIZE as u32,\n            buf: self.buf.as_mut_ptr(),\n        };\n        syscall!(\n            WSARecvFrom(\n                self.inner.as_raw_socket() as usize,\n                addr_of!(wbuf),\n                1,\n                null_mut(),\n                &mut 0,\n                addr_of_mut!(*self.from).cast(),\n                addr_of_mut!(self.from_len),\n                addr_of_mut!(*self.ol),\n                None,\n            ),\n            is_err\n        )\n        .map_err(|err| IoError::Other(err, IoOperation::WSARecvFrom))?;\n        Ok(())\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn get_overlapped_result(&mut self) -> IoResult<()> {\n        let mut bytes_read = 0;\n        let mut flags = 0;\n        let ol = *self.ol;\n        syscall!(\n            WSAGetOverlappedResult(\n                self.inner.as_raw_socket() as _,\n                addr_of!(ol),\n                &raw mut bytes_read,\n                0,\n                &raw mut flags,\n            ),\n            |res| { res == 0 }\n        )\n        .map_err(|err| IoError::Other(err, IoOperation::WSAGetOverlappedResult))?;\n        self.bytes_read = bytes_read;\n        Ok(())\n    }\n\n    #[expect(unsafe_code)]\n    const fn new_wsa_data() -> WSADATA {\n        // Safety: an all-zero value is valid for `WSADATA`.\n        unsafe { zeroed::<WSADATA>() }\n    }\n\n    #[expect(unsafe_code)]\n    const fn new_sockaddr_storage() -> SOCKADDR_STORAGE {\n        // Safety: an all-zero value is valid for `SOCKADDR_STORAGE`.\n        unsafe { zeroed::<SOCKADDR_STORAGE>() }\n    }\n\n    #[expect(unsafe_code)]\n    const fn new_overlapped() -> OVERLAPPED {\n        // Safety: an all-zero value is valid for `OVERLAPPED.`\n        unsafe { zeroed::<OVERLAPPED>() }\n    }\n\n    #[expect(unsafe_code)]\n    const fn new_icmp_error_info() -> ICMP_ERROR_INFO {\n        // Safety: an all-zero value is valid for `ICMP_ERROR_INFO`.\n        unsafe { zeroed::<ICMP_ERROR_INFO>() }\n    }\n}\n\nimpl Drop for SocketImpl {\n    fn drop(&mut self) {\n        if self.ol.hEvent != -1 && self.ol.hEvent != 0 {\n            syscall!(WSACloseEvent(self.ol.hEvent), |res| { res == 0 }).unwrap_or_default();\n        }\n    }\n}\n\n#[expect(clippy::cast_possible_wrap)]\nimpl Socket for SocketImpl {\n    #[instrument(level = \"trace\")]\n    fn new_icmp_send_socket_ipv4(raw: bool) -> IoResult<Self> {\n        if raw {\n            let mut sock = Self::new(Domain::IPV4, Type::RAW, Some(Protocol::from(IPPROTO_RAW)))?;\n            sock.set_non_blocking(true)?;\n            sock.set_header_included(true)?;\n            Ok(sock)\n        } else {\n            unimplemented!(\"non-raw socket is not supported on Windows\")\n        }\n    }\n\n    #[instrument(level = \"trace\")]\n    fn new_icmp_send_socket_ipv6(raw: bool) -> IoResult<Self> {\n        if raw {\n            let sock = Self::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6))?;\n            sock.set_non_blocking(true)?;\n            Ok(sock)\n        } else {\n            unimplemented!(\"non-raw socket is not supported on Windows\")\n        }\n    }\n\n    #[instrument(level = \"trace\")]\n    fn new_udp_send_socket_ipv4(raw: bool) -> IoResult<Self> {\n        if raw {\n            let mut sock = Self::new(Domain::IPV4, Type::RAW, Some(Protocol::from(IPPROTO_RAW)))?;\n            sock.set_non_blocking(true)?;\n            sock.set_header_included(true)?;\n            Ok(sock)\n        } else {\n            unimplemented!(\"non-raw socket is not supported on Windows\")\n        }\n    }\n\n    #[instrument(level = \"trace\")]\n    fn new_udp_send_socket_ipv6(raw: bool) -> IoResult<Self> {\n        if raw {\n            let sock = Self::new(Domain::IPV6, Type::RAW, Some(Protocol::UDP))?;\n            sock.set_non_blocking(true)?;\n            Ok(sock)\n        } else {\n            unimplemented!(\"non-raw socket is not supported on Windows\")\n        }\n    }\n\n    #[instrument(level = \"trace\")]\n    fn new_recv_socket_ipv4(src_addr: Ipv4Addr, raw: bool) -> IoResult<Self> {\n        if raw {\n            let mut sock = Self::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?;\n            sock.bind(SocketAddr::new(IpAddr::V4(src_addr), 0))?;\n            sock.post_recv_from()?;\n            sock.set_non_blocking(true)?;\n            sock.set_header_included(true)?;\n            Ok(sock)\n        } else {\n            unimplemented!(\"non-raw socket is not supported on Windows\")\n        }\n    }\n\n    #[instrument(level = \"trace\")]\n    fn new_recv_socket_ipv6(src_addr: Ipv6Addr, raw: bool) -> IoResult<Self> {\n        if raw {\n            let mut sock = Self::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6))?;\n            sock.bind(SocketAddr::new(IpAddr::V6(src_addr), 0))?;\n            sock.post_recv_from()?;\n            sock.set_non_blocking(true)?;\n            Ok(sock)\n        } else {\n            unimplemented!(\"non-raw socket is not supported on Windows\")\n        }\n    }\n\n    #[instrument(level = \"trace\")]\n    fn new_stream_socket_ipv4() -> IoResult<Self> {\n        let mut sock = Self::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;\n        sock.set_non_blocking(true)?;\n        sock.set_reuse_port(true)?;\n        Ok(sock)\n    }\n\n    #[instrument(level = \"trace\")]\n    fn new_stream_socket_ipv6() -> IoResult<Self> {\n        let mut sock = Self::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))?;\n        sock.set_non_blocking(true)?;\n        sock.set_reuse_port(true)?;\n        Ok(sock)\n    }\n\n    #[instrument(level = \"trace\")]\n    fn new_udp_dgram_socket_ipv4() -> IoResult<Self> {\n        Self::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))\n    }\n\n    #[instrument(level = \"trace\")]\n    fn new_udp_dgram_socket_ipv6() -> IoResult<Self> {\n        Self::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn bind(&mut self, addr: SocketAddr) -> IoResult<()> {\n        self.inner\n            .bind(&SockAddr::from(addr))\n            .map_err(|e| {\n                if e.kind() == StdErrorKind::PermissionDenied {\n                    StdIoError::from_raw_os_error(WSAEADDRNOTAVAIL)\n                } else {\n                    e\n                }\n            })\n            .map_err(|err| IoError::Bind(err, addr))?;\n        self.create_event()?;\n        Ok(())\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn set_tos(&mut self, tos: u32) -> IoResult<()> {\n        self.inner\n            .set_tos_v4(tos)\n            .map_err(|err| IoError::Other(err, IoOperation::SetTos))\n    }\n\n    fn set_tclass_v6(&mut self, tclass: u32) -> IoResult<()> {\n        if tclass > 0 {\n            unimplemented!(\"setting tclass_v6 is not supported on Windows\")\n        }\n        Ok(())\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn set_ttl(&mut self, ttl: u32) -> IoResult<()> {\n        self.inner\n            .set_ttl_v4(ttl)\n            .map_err(|err| IoError::Other(err, IoOperation::SetTtl))\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn set_reuse_port(&mut self, is_reuse_port: bool) -> IoResult<()> {\n        self.setsockopt_bool(SOL_SOCKET as _, SO_REUSE_UNICASTPORT as _, is_reuse_port)\n            .or_else(|_| {\n                self.setsockopt_bool(SOL_SOCKET as _, SO_PORT_SCALABILITY as _, is_reuse_port)\n            })\n            .map_err(|err| IoError::Other(err, IoOperation::SetReusePort))\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn set_header_included(&mut self, is_header_included: bool) -> IoResult<()> {\n        self.inner\n            .set_header_included_v4(is_header_included)\n            .map_err(|err| IoError::Other(err, IoOperation::SetHeaderIncluded))\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn set_unicast_hops_v6(&mut self, max_hops: u8) -> IoResult<()> {\n        self.inner\n            .set_unicast_hops_v6(max_hops.into())\n            .map_err(|err| IoError::Other(err, IoOperation::SetUnicastHopsV6))\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn connect(&mut self, addr: SocketAddr) -> IoResult<()> {\n        self.set_fail_connect_on_icmp_error(true)?;\n        syscall!(\n            WSAEventSelect(\n                self.inner.as_raw_socket() as _,\n                self.ol.hEvent,\n                (FD_CONNECT | FD_WRITE) as _\n            ),\n            |res| res == SOCKET_ERROR\n        )\n        .map_err(|err| IoError::Other(err, IoOperation::WSAEventSelect))?;\n        let res = self.inner.connect(&SockAddr::from(addr));\n        match res {\n            Ok(()) => Ok(()),\n            Err(ref e) if e.kind() == StdErrorKind::WouldBlock => Ok(()),\n            Err(err) => Err(IoError::Connect(err, addr)),\n        }\n    }\n\n    #[instrument(skip(self, buf), level = \"trace\")]\n    fn send_to(&mut self, buf: &[u8], addr: SocketAddr) -> IoResult<()> {\n        tracing::trace!(buf = format!(\"{:02x?}\", buf.iter().format(\" \")), ?addr);\n        self.inner\n            .send_to(buf, &SockAddr::from(addr))\n            .map_err(|err| IoError::SendTo(err, addr))?;\n        Ok(())\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn is_readable(&mut self, timeout: Duration) -> IoResult<bool> {\n        if !self.wait_for_event(timeout)? {\n            return Ok(false);\n        }\n        while let Err(err) = self.get_overlapped_result() {\n            if err.kind() != ErrorKind::Std(StdIoError::from_raw_os_error(WSA_IO_INCOMPLETE).kind())\n            {\n                return Err(err);\n            }\n        }\n        self.reset_event()?;\n        Ok(true)\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn is_writable(&mut self) -> IoResult<bool> {\n        if !self.wait_for_event(Duration::ZERO)? {\n            return Ok(false);\n        }\n        while let Err(err) = self.get_overlapped_result() {\n            if err.kind() != ErrorKind::Std(StdIoError::from_raw_os_error(WSA_IO_INCOMPLETE).kind())\n            {\n                return Err(err);\n            }\n        }\n        self.reset_event()?;\n        Ok(true)\n    }\n\n    #[instrument(skip(self, buf), level = \"trace\")]\n    fn recv_from(&mut self, buf: &mut [u8]) -> IoResult<(usize, Option<SocketAddr>)> {\n        let addr = sockaddrptr_to_ipaddr(addr_of_mut!(*self.from))\n            .map_err(|err| IoError::Other(err, IoOperation::RecvFrom))?;\n        let len = self.read(buf)?;\n        tracing::trace!(\n            buf = format!(\"{:02x?}\", buf[..len].iter().format(\" \")),\n            len,\n            ?addr\n        );\n        Ok((len, Some(SocketAddr::new(addr, 0))))\n    }\n\n    #[instrument(skip(self, buf), ret, level = \"trace\")]\n    fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {\n        let bytes_read = std::cmp::min(self.bytes_read as usize, buf.len());\n        buf[..bytes_read].copy_from_slice(&self.buf[..bytes_read]);\n        tracing::trace!(buf = format!(\"{:02x?}\", buf[..bytes_read].iter().format(\" \")));\n        self.post_recv_from()?;\n        Ok(bytes_read)\n    }\n\n    #[instrument(skip(self), level = \"trace\")]\n    fn shutdown(&mut self) -> IoResult<()> {\n        self.inner\n            .shutdown(std::net::Shutdown::Both)\n            .map_err(|err| IoError::Other(err, IoOperation::Shutdown))\n    }\n\n    #[instrument(skip(self), ret, level = \"trace\")]\n    fn peer_addr(&mut self) -> IoResult<Option<SocketAddr>> {\n        Ok(self\n            .inner\n            .peer_addr()\n            .map_err(|err| IoError::Other(err, IoOperation::PeerAddr))?\n            .as_socket())\n    }\n\n    #[instrument(skip(self), ret, level = \"trace\")]\n    fn take_error(&mut self) -> IoResult<Option<SocketError>> {\n        match self.getsockopt(SOL_SOCKET as _, SO_ERROR as _, 0) {\n            Ok(0) => Ok(None),\n            Ok(errno) if errno == WSAEHOSTUNREACH => Ok(Some(SocketError::HostUnreachable)),\n            Ok(errno) if errno == WSAECONNREFUSED => Ok(Some(SocketError::ConnectionRefused)),\n            Ok(errno) => Ok(Some(SocketError::Other(StdIoError::from_raw_os_error(\n                errno,\n            )))),\n            Err(e) => Err(e),\n        }\n        .map_err(|err| IoError::Other(err, IoOperation::TakeError))\n    }\n\n    #[instrument(skip(self), ret, level = \"trace\")]\n    #[expect(unsafe_code)]\n    fn icmp_error_info(&mut self) -> IoResult<IpAddr> {\n        let icmp_error_info = self\n            .getsockopt::<ICMP_ERROR_INFO>(\n                IPPROTO_TCP as _,\n                TCP_ICMP_ERROR_INFO as _,\n                Self::new_icmp_error_info(),\n            )\n            .map_err(|err| IoError::Other(err, IoOperation::TcpIcmpErrorInfo))?;\n        let src_addr = icmp_error_info.srcaddress;\n        match unsafe { src_addr.si_family } {\n            AF_INET => Ok(IpAddr::V4(Ipv4Addr::from(unsafe {\n                src_addr.Ipv4.sin_addr.S_un.S_addr.to_ne_bytes()\n            }))),\n            AF_INET6 => Ok(IpAddr::V6(Ipv6Addr::from(unsafe {\n                src_addr.Ipv6.sin6_addr.u.Byte\n            }))),\n            _ => Err(IoError::Other(\n                StdIoError::from(StdErrorKind::AddrNotAvailable),\n                IoOperation::TcpIcmpErrorInfo,\n            )),\n        }\n    }\n}\n\n// Note that we handle `WSAENOBUFS`, which can occurs when calling `send_to()`\n// for ICMP and UDP.  We return it as `NetUnreachable` to piggyback on the\n// existing error handling.\nimpl From<&StdIoError> for ErrorKind {\n    fn from(value: &StdIoError) -> Self {\n        if let Some(raw) = value.raw_os_error() {\n            if raw == WSAEINPROGRESS {\n                Self::InProgress\n            } else if raw == WSAEHOSTUNREACH {\n                Self::HostUnreachable\n            } else if raw == WSAENETUNREACH || raw == WSAENOBUFS {\n                Self::NetUnreachable\n            } else {\n                Self::Std(value.kind())\n            }\n        } else {\n            Self::Std(value.kind())\n        }\n    }\n}\n\n// only used for unit tests\nimpl From<ErrorKind> for StdIoError {\n    fn from(value: ErrorKind) -> Self {\n        match value {\n            ErrorKind::InProgress => Self::from_raw_os_error(WSAEINPROGRESS),\n            ErrorKind::HostUnreachable => Self::from_raw_os_error(WSAEHOSTUNREACH),\n            ErrorKind::NetUnreachable => Self::from_raw_os_error(WSAENETUNREACH),\n            ErrorKind::Std(kind) => Self::from(kind),\n        }\n    }\n}\n\n/// NOTE under Windows, we cannot use a bind connect/getsockname as \"If the socket\n/// is using a connectionless protocol, the address may not be available until I/O\n/// occurs on the socket.\"  We use `SIO_ROUTING_INTERFACE_QUERY` instead.\n#[expect(clippy::cast_sign_loss)]\n#[instrument(level = \"trace\")]\nfn routing_interface_query(target: IpAddr) -> Result<IpAddr> {\n    let mut src_buf = [0; 1024];\n    let src: *mut c_void = src_buf.as_mut_ptr().cast();\n    let mut bytes = 0;\n    let socket = match target {\n        IpAddr::V4(_) => SocketImpl::new_udp_dgram_socket_ipv4(),\n        IpAddr::V6(_) => SocketImpl::new_udp_dgram_socket_ipv6(),\n    }?;\n    let (dest, destlen) = socketaddr_to_sockaddr(SocketAddr::new(target, 0));\n    syscall!(\n        WSAIoctl(\n            socket.inner.as_raw_socket() as _,\n            SIO_ROUTING_INTERFACE_QUERY,\n            addr_of!(dest).cast(),\n            destlen as u32,\n            src,\n            1024,\n            addr_of_mut!(bytes),\n            null_mut(),\n            None,\n        ),\n        |res| res == SOCKET_ERROR\n    )\n    .map_err(|err| IoError::Other(err, IoOperation::SioRoutingInterfaceQuery))?;\n    // Note that the `WSAIoctl` call potentially returns multiple results (see\n    // <https://www.winsocketdotnetworkprogramming.com/winsock2programming/winsock2advancedsocketoptionioctl7h.html>),\n    // TBD We choose the first one arbitrarily.\n    let sockaddr = src.cast::<SOCKADDR_STORAGE>();\n    sockaddrptr_to_ipaddr(sockaddr)\n        .map_err(|err| Error::IoError(IoError::Other(err, IoOperation::ConvertSocketAddress)))\n}\n\n#[expect(unsafe_code)]\nfn sockaddrptr_to_ipaddr(sockaddr: *mut SOCKADDR_STORAGE) -> StdIoResult<IpAddr> {\n    // Safety: TODO\n    match sockaddr_to_socketaddr(unsafe { sockaddr.as_ref().unwrap() }) {\n        Err(e) => Err(e),\n        Ok(socketaddr) => match socketaddr {\n            SocketAddr::V4(socketaddrv4) => Ok(IpAddr::V4(*socketaddrv4.ip())),\n            SocketAddr::V6(socketaddrv6) => Ok(IpAddr::V6(*socketaddrv6.ip())),\n        },\n    }\n}\n\n#[expect(unsafe_code)]\nfn sockaddr_to_socketaddr(sockaddr: &SOCKADDR_STORAGE) -> StdIoResult<SocketAddr> {\n    let ptr = std::ptr::from_ref(sockaddr);\n    let af = sockaddr.ss_family;\n    if af == AF_INET {\n        let sockaddr_in_ptr = ptr.cast::<SOCKADDR_IN>();\n        // Safety: TODO\n        let sockaddr_in = unsafe { *sockaddr_in_ptr };\n        let ipv4addr = u32::from_be(unsafe { sockaddr_in.sin_addr.S_un.S_addr });\n        let port = sockaddr_in.sin_port;\n        Ok(SocketAddr::V4(SocketAddrV4::new(\n            Ipv4Addr::from(ipv4addr),\n            port,\n        )))\n    } else if af == AF_INET6 {\n        let sockaddr_in6_ptr = ptr.cast::<SOCKADDR_IN6>();\n        // Safety: TODO\n        let sockaddr_in6 = unsafe { *sockaddr_in6_ptr };\n        // TODO: check endianness\n        // Safety: TODO\n        let ipv6addr = unsafe { sockaddr_in6.sin6_addr.u.Byte };\n        let port = sockaddr_in6.sin6_port;\n        // Safety: TODO\n        let scope_id = unsafe { sockaddr_in6.Anonymous.sin6_scope_id };\n        Ok(SocketAddr::V6(SocketAddrV6::new(\n            Ipv6Addr::from(ipv6addr),\n            port,\n            sockaddr_in6.sin6_flowinfo,\n            scope_id,\n        )))\n    } else {\n        Err(StdIoError::new(\n            StdErrorKind::Unsupported,\n            format!(\"Unsupported address family: {af:?}\"),\n        ))\n    }\n}\n\n#[expect(unsafe_code)]\n#[expect(clippy::cast_possible_wrap)]\n#[must_use]\nfn socketaddr_to_sockaddr(socketaddr: SocketAddr) -> (SOCKADDR_STORAGE, i32) {\n    #[repr(C)]\n    union SockAddr {\n        storage: SOCKADDR_STORAGE,\n        in4: SOCKADDR_IN,\n        in6: SOCKADDR_IN6,\n    }\n\n    let sockaddr = match socketaddr {\n        SocketAddr::V4(socketaddrv4) => SockAddr {\n            in4: SOCKADDR_IN {\n                sin_family: AF_INET,\n                sin_port: socketaddrv4.port().to_be(),\n                sin_addr: IN_ADDR {\n                    S_un: IN_ADDR_0 {\n                        S_addr: u32::from(*socketaddrv4.ip()).to_be(),\n                    },\n                },\n                sin_zero: [0; 8],\n            },\n        },\n        SocketAddr::V6(socketaddrv6) => SockAddr {\n            in6: SOCKADDR_IN6 {\n                sin6_family: AF_INET6,\n                sin6_port: socketaddrv6.port().to_be(),\n                sin6_flowinfo: socketaddrv6.flowinfo(),\n                sin6_addr: IN6_ADDR {\n                    u: IN6_ADDR_0 {\n                        Byte: socketaddrv6.ip().octets(),\n                    },\n                },\n                Anonymous: SOCKADDR_IN6_0 {\n                    sin6_scope_id: socketaddrv6.scope_id(),\n                },\n            },\n        },\n    };\n\n    (unsafe { sockaddr.storage }, size_of::<SockAddr>() as i32)\n}\n\n#[instrument(skip(adapters), ret, level = \"trace\")]\nfn lookup_interface_addr(adapters: &Adapters, name: &str) -> Result<IpAddr> {\n    adapters\n        .iter()\n        .find_map(|addr| {\n            if addr.name.eq_ignore_ascii_case(name) {\n                addr.addr\n            } else {\n                None\n            }\n        })\n        .ok_or_else(|| Error::UnknownInterface(name.to_string()))\n}\n\nmod adapter {\n    use crate::error::{Error, Result};\n    use crate::net::platform::windows::sockaddrptr_to_ipaddr;\n    use std::io::Error as StdIoError;\n    use std::marker::PhantomData;\n    use std::net::IpAddr;\n    use std::ptr::null_mut;\n    use widestring::WideCString;\n    use windows_sys::Win32::Foundation::{ERROR_BUFFER_OVERFLOW, NO_ERROR};\n    use windows_sys::Win32::NetworkManagement::IpHelper;\n    use windows_sys::Win32::NetworkManagement::IpHelper::{\n        GET_ADAPTERS_ADDRESSES_FLAGS, IP_ADAPTER_ADDRESSES_LH,\n    };\n    use windows_sys::Win32::Networking::WinSock::{ADDRESS_FAMILY, AF_INET, AF_INET6};\n\n    /// Retrieve adapter address information.\n    pub struct Adapters {\n        buf: Vec<u8>,\n    }\n\n    impl Adapters {\n        /// Retrieve IPv4 adapter details.\n        pub fn ipv4() -> Result<Self> {\n            Self::retrieve_addresses(AF_INET)\n        }\n\n        /// Retrieve IPv6 adapter details.\n        pub fn ipv6() -> Result<Self> {\n            Self::retrieve_addresses(AF_INET6)\n        }\n\n        /// Return an iterator of `AdapterAddress` in this `Adapters`.\n        pub fn iter(&self) -> AdaptersIter<'_> {\n            AdaptersIter::new(self)\n        }\n\n        // The maximum number of attempts to retrieve addresses.\n        const MAX_ATTEMPTS: usize = 3;\n\n        // The size of the buffer to use for the first retrieval attempt.\n        const INITIAL_BUFFER_SIZE: u32 = 15000;\n\n        // The flags to use when performing the adapter addresses retrieval.\n        const ADDRESS_FLAGS: GET_ADAPTERS_ADDRESSES_FLAGS = IpHelper::GAA_FLAG_SKIP_ANYCAST\n            | IpHelper::GAA_FLAG_SKIP_MULTICAST\n            | IpHelper::GAA_FLAG_SKIP_DNS_SERVER;\n\n        fn retrieve_addresses(family: ADDRESS_FAMILY) -> Result<Self> {\n            let mut buf_len = Self::INITIAL_BUFFER_SIZE;\n            let mut buf: Vec<u8>;\n            for _ in 0..Self::MAX_ATTEMPTS {\n                buf = vec![0_u8; buf_len as usize];\n                let res = syscall_ip_helper!(GetAdaptersAddresses(\n                    u32::from(family),\n                    Self::ADDRESS_FLAGS,\n                    null_mut(),\n                    buf.as_mut_ptr().cast(),\n                    &raw mut buf_len,\n                ));\n                if res == ERROR_BUFFER_OVERFLOW {\n                    continue;\n                }\n                if res != NO_ERROR {\n                    return Err(Error::UnknownInterface(format!(\n                        \"GetAdaptersAddresses returned error: {}\",\n                        StdIoError::from_raw_os_error(res.try_into().unwrap())\n                    )));\n                }\n                return Ok(Self { buf });\n            }\n            Err(Error::UnknownInterface(format!(\n                \"GetAdaptersAddresses did not succeed after {} attempts\",\n                Self::MAX_ATTEMPTS\n            )))\n        }\n    }\n\n    /// A named adapter address.\n    #[derive(Debug)]\n    pub struct AdapterAddress {\n        /// The adapter friendly name.\n        pub name: String,\n        /// The first adapter uni-cast `IpAddr`, if any.\n        pub addr: Option<IpAddr>,\n    }\n\n    /// An iterator for `Adapters` which yields `AdapterAddress`\n    pub struct AdaptersIter<'a> {\n        next: *const IP_ADAPTER_ADDRESSES_LH,\n        _data: PhantomData<&'a ()>,\n    }\n\n    impl<'a> AdaptersIter<'a> {\n        /// Create an iterator for an `Adapters`.\n        pub fn new(data: &'a Adapters) -> Self {\n            let next = data.buf.as_ptr().cast();\n            Self {\n                next,\n                // tie the lifetime of this iterator to the lifetime of the `Adapters`\n                _data: PhantomData,\n            }\n        }\n    }\n\n    impl Iterator for AdaptersIter<'_> {\n        type Item = AdapterAddress;\n\n        fn next(&mut self) -> Option<Self::Item> {\n            if self.next.is_null() {\n                None\n            } else {\n                // Safety: `next` is not null and points to a valid `IP_ADAPTER_ADDRESSES_LH`\n                #[expect(unsafe_code)]\n                unsafe {\n                    let friendly_name = WideCString::from_ptr_str((*self.next).FriendlyName)\n                        .to_string()\n                        .ok()?;\n                    let addr = {\n                        let first_unicast = (*self.next).FirstUnicastAddress;\n                        if first_unicast.is_null() {\n                            None\n                        } else {\n                            let socket_address = (*first_unicast).Address;\n                            let sockaddr = socket_address.lpSockaddr;\n                            Some(sockaddrptr_to_ipaddr(sockaddr.cast()).ok()?)\n                        }\n                    };\n                    self.next = (*self.next).Next;\n                    Some(AdapterAddress {\n                        name: friendly_name,\n                        addr,\n                    })\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net/platform.rs",
    "content": "pub mod byte_order;\n\npub use byte_order::Ipv4ByteOrder;\nuse std::net::IpAddr;\n\n#[cfg(unix)]\nmod unix;\n\nuse crate::error::Result;\n#[cfg(unix)]\npub use unix::*;\n\n#[cfg(windows)]\nmod windows;\n\n#[cfg(windows)]\npub use self::windows::*;\n\n/// Platform specific operations.\n#[cfg_attr(test, mockall::automock)]\npub trait Platform {\n    /// Determine the required byte ordering for IPv4 header fields.\n    fn byte_order_for_address(addr: IpAddr) -> Result<Ipv4ByteOrder>;\n\n    /// Lookup an `IpAddr` for an interface.\n    ///\n    /// If the interface has more than one address then an arbitrary address\n    /// is selected and returned.\n    fn lookup_interface_addr(addr: IpAddr, name: &str) -> Result<IpAddr>;\n\n    /// Discover a local `IpAddr` which can route to the target address.\n    fn discover_local_addr(target_addr: IpAddr, port: u16) -> Result<IpAddr>;\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net/socket.rs",
    "content": "use crate::error::IoResult as Result;\nuse std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};\nuse std::time::Duration;\n\n#[cfg_attr(test, mockall::automock)]\npub trait Socket\nwhere\n    Self: Sized,\n{\n    /// Create an IPv4 socket for sending ICMP probes.\n    fn new_icmp_send_socket_ipv4(raw: bool) -> Result<Self>;\n    /// Create an IPv6 socket for sending ICMP probes.\n    fn new_icmp_send_socket_ipv6(raw: bool) -> Result<Self>;\n    /// Create an IPv4 socket for sending UDP probes.\n    fn new_udp_send_socket_ipv4(raw: bool) -> Result<Self>;\n    /// Create an IPv6 socket for sending UDP probes.\n    fn new_udp_send_socket_ipv6(raw: bool) -> Result<Self>;\n    /// Create an IPv4 socket for receiving UDP probe responses.\n    fn new_recv_socket_ipv4(addr: Ipv4Addr, raw: bool) -> Result<Self>;\n    /// Create an IPv6 socket for receiving UDP probe responses.\n    fn new_recv_socket_ipv6(addr: Ipv6Addr, raw: bool) -> Result<Self>;\n    /// Create a IPv4/TCP socket for sending TCP probes.\n    fn new_stream_socket_ipv4() -> Result<Self>;\n    /// Create a IPv6/TCP socket for sending TCP probes.\n    fn new_stream_socket_ipv6() -> Result<Self>;\n    /// Create (non-raw) IPv4/UDP socket for local address validation.\n    fn new_udp_dgram_socket_ipv4() -> Result<Self>;\n    /// Create (non-raw) IPv6/UDP socket for local address validation.\n    fn new_udp_dgram_socket_ipv6() -> Result<Self>;\n    fn bind(&mut self, address: SocketAddr) -> Result<()>;\n    fn set_tos(&mut self, tos: u32) -> Result<()>;\n    fn set_tclass_v6(&mut self, tclass: u32) -> Result<()>;\n    fn set_ttl(&mut self, ttl: u32) -> Result<()>;\n    fn set_reuse_port(&mut self, reuse: bool) -> Result<()>;\n    fn set_header_included(&mut self, included: bool) -> Result<()>;\n    fn set_unicast_hops_v6(&mut self, hops: u8) -> Result<()>;\n    fn connect(&mut self, address: SocketAddr) -> Result<()>;\n    fn send_to(&mut self, buf: &[u8], addr: SocketAddr) -> Result<()>;\n    /// Returns true if the socket becomes readable before the timeout, false otherwise.\n    fn is_readable(&mut self, timeout: Duration) -> Result<bool>;\n    /// Returns true if the socket is currently writable, false otherwise.\n    fn is_writable(&mut self) -> Result<bool>;\n    fn recv_from(&mut self, buf: &mut [u8]) -> Result<(usize, Option<SocketAddr>)>;\n    fn read(&mut self, buf: &mut [u8]) -> Result<usize>;\n    fn shutdown(&mut self) -> Result<()>;\n    fn peer_addr(&mut self) -> Result<Option<SocketAddr>>;\n    fn take_error(&mut self) -> Result<Option<SocketError>>;\n    fn icmp_error_info(&mut self) -> Result<IpAddr>;\n}\n\n/// A socket error returned by `Socket::take_error`.\n#[derive(Debug)]\npub enum SocketError {\n    ConnectionRefused,\n    #[allow(dead_code)]\n    HostUnreachable,\n    Other(#[expect(dead_code)] std::io::Error),\n}\n\n#[cfg(test)]\npub mod tests {\n    #[macro_export]\n    macro_rules! mocket_read {\n        ($packet: expr) => {\n            move |buf: &mut [u8]| -> IoResult<usize> {\n                buf[..$packet.len()].copy_from_slice(&$packet);\n                Ok(buf.len())\n            }\n        };\n    }\n\n    #[macro_export]\n    macro_rules! mocket_recv_from {\n        ($packet: expr, $addr: expr) => {\n            move |buf: &mut [u8]| -> IoResult<(usize, Option<SocketAddr>)> {\n                buf[..$packet.len()].copy_from_slice(&$packet);\n                Ok((buf.len(), Some($addr)))\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net/source.rs",
    "content": "use crate::PortDirection;\nuse crate::error::Error::InvalidSourceAddr;\nuse crate::error::Result;\nuse crate::net::platform::Platform;\nuse crate::net::socket::Socket;\nuse crate::types::Port;\nuse std::net::{IpAddr, SocketAddr};\n\n/// The port used for local address discovery if not dest port is available.\nconst DISCOVERY_PORT: Port = Port(80);\n\n/// Discover or validate a source address.\npub struct SourceAddr;\n\nimpl SourceAddr {\n    /// Discover the source `IpAddr`.\n    pub fn discover<S: Socket, P: Platform>(\n        target_addr: IpAddr,\n        port_direction: PortDirection,\n        interface: Option<&str>,\n    ) -> Result<IpAddr> {\n        let port = port_direction.dest().unwrap_or(DISCOVERY_PORT).0;\n        match interface.as_ref() {\n            Some(interface) => P::lookup_interface_addr(target_addr, interface),\n            None => P::discover_local_addr(target_addr, port),\n        }\n    }\n\n    /// Validate that we can bind to the source `IpAddr`.\n    pub fn validate<S: Socket>(source_addr: IpAddr) -> Result<IpAddr> {\n        let mut socket = match source_addr {\n            IpAddr::V4(_) => S::new_udp_dgram_socket_ipv4(),\n            IpAddr::V6(_) => S::new_udp_dgram_socket_ipv6(),\n        }?;\n        let sock_addr = SocketAddr::new(source_addr, 0);\n        match socket.bind(sock_addr) {\n            Ok(()) => Ok(source_addr),\n            Err(_) => Err(InvalidSourceAddr(sock_addr.ip())),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::error::IoError;\n    use crate::net::platform::MockPlatform;\n    use crate::net::socket::MockSocket;\n    use mockall::predicate;\n    use std::str::FromStr;\n    use std::sync::Mutex;\n\n    static MTX: Mutex<()> = Mutex::new(());\n\n    #[test]\n    fn test_discover_local_addr_default_port() {\n        let _m = MTX.lock();\n\n        let direction = PortDirection::None;\n        let interface = None;\n        let expected_target = IpAddr::from_str(\"1.2.3.4\").unwrap();\n        let expected_port = DISCOVERY_PORT.0;\n        let expected_src = IpAddr::from_str(\"192.168.0.1\").unwrap();\n\n        let ctx = MockPlatform::discover_local_addr_context();\n        ctx.expect()\n            .with(predicate::eq(expected_target), predicate::eq(expected_port))\n            .times(1)\n            .returning(move |_, _| Ok(expected_src));\n\n        let src_addr =\n            SourceAddr::discover::<MockSocket, MockPlatform>(expected_target, direction, interface)\n                .unwrap();\n        assert_eq!(expected_src, src_addr);\n    }\n\n    #[test]\n    fn test_discover_local_addr_fixed_dest_port() {\n        let _m = MTX.lock();\n\n        let direction = PortDirection::FixedDest(Port(99));\n        let interface = None;\n        let expected_target = IpAddr::from_str(\"1.2.3.4\").unwrap();\n        let expected_port = 99;\n        let expected_src = IpAddr::from_str(\"192.168.0.1\").unwrap();\n\n        let ctx = MockPlatform::discover_local_addr_context();\n        ctx.expect()\n            .with(predicate::eq(expected_target), predicate::eq(expected_port))\n            .times(1)\n            .returning(move |_, _| Ok(expected_src));\n\n        let src_addr =\n            SourceAddr::discover::<MockSocket, MockPlatform>(expected_target, direction, interface)\n                .unwrap();\n        assert_eq!(expected_src, src_addr);\n    }\n\n    #[test]\n    fn test_discover_local_addr_fixed_both_port() {\n        let _m = MTX.lock();\n\n        let direction = PortDirection::FixedBoth(Port(1), Port(99));\n        let interface = None;\n        let expected_target = IpAddr::from_str(\"1.2.3.4\").unwrap();\n        let expected_port = 99;\n        let expected_src = IpAddr::from_str(\"192.168.0.1\").unwrap();\n\n        let ctx = MockPlatform::discover_local_addr_context();\n        ctx.expect()\n            .with(predicate::eq(expected_target), predicate::eq(expected_port))\n            .times(1)\n            .returning(move |_, _| Ok(expected_src));\n\n        let src_addr =\n            SourceAddr::discover::<MockSocket, MockPlatform>(expected_target, direction, interface)\n                .unwrap();\n        assert_eq!(expected_src, src_addr);\n    }\n\n    #[test]\n    fn test_discover_lookup_interface() {\n        let _m = MTX.lock();\n\n        let direction = PortDirection::None;\n        let interface = Some(\"en0\");\n        let expected_target = IpAddr::from_str(\"1.2.3.4\").unwrap();\n        let expected_src = IpAddr::from_str(\"192.168.0.1\").unwrap();\n        let expected_interface = \"en0\";\n\n        let ctx = MockPlatform::lookup_interface_addr_context();\n        ctx.expect()\n            .with(\n                predicate::eq(expected_target),\n                predicate::eq(expected_interface),\n            )\n            .times(1)\n            .returning(move |_, _| Ok(expected_src));\n\n        let src_addr =\n            SourceAddr::discover::<MockSocket, MockPlatform>(expected_target, direction, interface)\n                .unwrap();\n        assert_eq!(expected_src, src_addr);\n    }\n\n    #[test]\n    fn test_validate_ipv4() {\n        let _m = MTX.lock();\n\n        let addr = IpAddr::from_str(\"192.168.0.1\").unwrap();\n        let expected_bind_addr = SocketAddr::new(addr, 0);\n\n        let ctx = MockSocket::new_udp_dgram_socket_ipv4_context();\n        ctx.expect().times(1).returning(move || {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n            Ok(mocket)\n        });\n\n        let src_addr = SourceAddr::validate::<MockSocket>(addr).unwrap();\n        assert_eq!(addr, src_addr);\n    }\n\n    #[test]\n    fn test_validate_ipv6() {\n        let _m = MTX.lock();\n\n        let addr = IpAddr::from_str(\"2a00:1450:4009:815::200e\").unwrap();\n        let expected_bind_addr = SocketAddr::new(addr, 0);\n\n        let ctx = MockSocket::new_udp_dgram_socket_ipv6_context();\n        ctx.expect().times(1).returning(move || {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|_| Ok(()));\n            Ok(mocket)\n        });\n\n        let src_addr = SourceAddr::validate::<MockSocket>(addr).unwrap();\n        assert_eq!(addr, src_addr);\n    }\n\n    #[test]\n    fn test_validate_invalid() {\n        let _m = MTX.lock();\n\n        let addr = IpAddr::from_str(\"1.2.3.4\").unwrap();\n        let expected_bind_addr = SocketAddr::new(addr, 0);\n\n        let ctx = MockSocket::new_udp_dgram_socket_ipv4_context();\n        ctx.expect().times(1).returning(move || {\n            let mut mocket = MockSocket::new();\n            mocket\n                .expect_bind()\n                .with(predicate::eq(expected_bind_addr))\n                .times(1)\n                .returning(|addr| Err(IoError::Bind(std::io::Error::last_os_error(), addr)));\n            Ok(mocket)\n        });\n\n        let err = SourceAddr::validate::<MockSocket>(addr).unwrap_err();\n        assert!(matches!(err, InvalidSourceAddr(_)));\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/net.rs",
    "content": "use crate::error::Result;\nuse crate::probe::{Probe, Response};\n\n/// Common types and helper functions.\nmod common;\n\n/// IPv4 implementation.\nmod ipv4;\n\n/// IPv6 implementation.\nmod ipv6;\n\n/// ICMP extensions.\nmod extension;\n\n/// Platform specific network code.\nmod platform;\n\n/// A network socket.\nmod socket;\n\n/// A channel for sending and receiving probes.\npub mod channel;\n\n/// Determine the source address.\npub mod source;\n\n/// The platform specific socket type.\npub use platform::{PlatformImpl, SocketImpl};\n\n/// An abstraction over a network interface for tracing.\n#[cfg_attr(test, mockall::automock)]\npub trait Network {\n    /// Send a `Probe`.\n    fn send_probe(&mut self, probe: Probe) -> Result<()>;\n\n    /// Receive the next Icmp packet and return a `ProbeResponse`.\n    ///\n    /// Returns `None` if the read times out or the packet read is not one of the types expected.\n    fn recv_probe(&mut self) -> Result<Option<Response>>;\n}\n"
  },
  {
    "path": "crates/trippy-core/src/probe.rs",
    "content": "use crate::TypeOfService;\nuse crate::types::{Checksum, Flags, Port, RoundId, Sequence, TimeToLive, TraceId};\nuse std::net::IpAddr;\nuse std::time::SystemTime;\n\n/// A network tracing probe.\n///\n/// A `Probe` is a packet sent across the network to trace the path to a target host.\n/// It contains information such as sequence number, trace identifier, ports, and TTL.\n///\n/// A probe is always in one of the following states:\n///\n/// - `NotSent` - The probe has not been sent.\n/// - `Skipped` - The probe was skipped.\n/// - `Awaited` - The probe has been sent and is awaiting a response.\n/// - `Complete` - The probe has been sent and a response has been received.\n#[derive(Debug, Clone, PartialEq, Eq, Default)]\npub enum ProbeStatus {\n    /// The probe has not been sent.\n    #[default]\n    NotSent,\n    /// The probe was skipped.\n    ///\n    /// A probe may be skipped if, for TCP, it could not be bound to a local\n    /// port.  When a probe is skipped, it will be marked as `Skipped` and a\n    /// new probe will be sent with the same TTL next available sequence number.\n    Skipped,\n    /// The probe has failed.\n    ///\n    /// A probe is considered failed when an error occurs while sending or\n    /// receiving.\n    Failed(ProbeFailed),\n    /// The probe has been sent and is awaiting a response.\n    ///\n    /// If no response is received within the timeout, the probe will remain\n    /// in this state indefinitely.\n    Awaited(Probe),\n    /// The probe has been sent and a response has been received.\n    Complete(ProbeComplete),\n}\n\n/// An incomplete network tracing probe.\n///\n/// A `Probe` is a packet sent across the network to trace the path to a target host.\n/// It contains information such as sequence number, trace identifier, ports, and TTL.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct Probe {\n    /// The sequence of the probe.\n    pub sequence: Sequence,\n    /// The trace identifier.\n    pub identifier: TraceId,\n    /// The source port (UDP/TCP only).\n    pub src_port: Port,\n    /// The destination port (UDP/TCP only).\n    pub dest_port: Port,\n    /// The TTL of the probe.\n    pub ttl: TimeToLive,\n    /// Which round the probe belongs to.\n    pub round: RoundId,\n    /// Timestamp when the probe was sent.\n    pub sent: SystemTime,\n    /// Probe flags.\n    pub flags: Flags,\n}\n\nimpl Probe {\n    /// Create a new probe.\n    #[must_use]\n    #[expect(clippy::too_many_arguments)]\n    pub(crate) const fn new(\n        sequence: Sequence,\n        identifier: TraceId,\n        src_port: Port,\n        dest_port: Port,\n        ttl: TimeToLive,\n        round: RoundId,\n        sent: SystemTime,\n        flags: Flags,\n    ) -> Self {\n        Self {\n            sequence,\n            identifier,\n            src_port,\n            dest_port,\n            ttl,\n            round,\n            sent,\n            flags,\n        }\n    }\n\n    /// A response has been received and the probe is now complete.\n    #[expect(clippy::too_many_arguments)]\n    #[must_use]\n    pub(crate) const fn complete(\n        self,\n        host: IpAddr,\n        received: SystemTime,\n        icmp_packet_type: IcmpPacketType,\n        tos: Option<TypeOfService>,\n        expected_udp_checksum: Option<Checksum>,\n        actual_udp_checksum: Option<Checksum>,\n        extensions: Option<Extensions>,\n    ) -> ProbeComplete {\n        ProbeComplete {\n            sequence: self.sequence,\n            identifier: self.identifier,\n            src_port: self.src_port,\n            dest_port: self.dest_port,\n            ttl: self.ttl,\n            round: self.round,\n            sent: self.sent,\n            host,\n            received,\n            icmp_packet_type,\n            tos,\n            expected_udp_checksum,\n            actual_udp_checksum,\n            extensions,\n        }\n    }\n\n    /// The probe has failed to send.\n    #[must_use]\n    pub(crate) const fn failed(self) -> ProbeFailed {\n        ProbeFailed {\n            sequence: self.sequence,\n            identifier: self.identifier,\n            src_port: self.src_port,\n            dest_port: self.dest_port,\n            ttl: self.ttl,\n            round: self.round,\n            sent: self.sent,\n        }\n    }\n}\n\n/// A complete network tracing probe.\n///\n/// A probe is considered complete when one of the following responses has been\n/// received:\n///\n/// - `TimeExceeded` - an ICMP packet indicating the TTL has expired.\n/// - `EchoReply` - an ICMP packet indicating the probe has reached the target.\n/// - `DestinationUnreachable` - an ICMP packet indicating the probe could not reach the target.\n/// - `NotApplicable` - a non-ICMP response (i.e. for some `UDP` & `TCP` probes).\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ProbeComplete {\n    /// The sequence of the probe.\n    pub sequence: Sequence,\n    /// The trace identifier.\n    pub identifier: TraceId,\n    /// The source port (UDP/TCP only)\n    pub src_port: Port,\n    /// The destination port (UDP/TCP only)\n    pub dest_port: Port,\n    /// The TTL of the probe.\n    pub ttl: TimeToLive,\n    /// Which round the probe belongs to.\n    pub round: RoundId,\n    /// Timestamp when the probe was sent.\n    pub sent: SystemTime,\n    /// The host which responded to the probe.\n    pub host: IpAddr,\n    /// Timestamp when the response to the probe was received.\n    pub received: SystemTime,\n    /// The type of ICMP response packet received for the probe.\n    pub icmp_packet_type: IcmpPacketType,\n    /// The type of service (DSCP/ECN) of the original datagram.\n    pub tos: Option<TypeOfService>,\n    /// The expected UDP checksum of the original datagram.\n    pub expected_udp_checksum: Option<Checksum>,\n    /// The actual UDP checksum of the original datagram.\n    pub actual_udp_checksum: Option<Checksum>,\n    /// The ICMP response extensions.\n    pub extensions: Option<Extensions>,\n}\n\n/// A failed network tracing probe.\n///\n/// A probe is considered failed when an error occurs while sending or\n/// receiving.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct ProbeFailed {\n    /// The sequence of the probe.\n    pub sequence: Sequence,\n    /// The trace identifier.\n    pub identifier: TraceId,\n    /// The source port (UDP/TCP only)\n    pub src_port: Port,\n    /// The destination port (UDP/TCP only)\n    pub dest_port: Port,\n    /// The TTL of the probe.\n    pub ttl: TimeToLive,\n    /// Which round the probe belongs to.\n    pub round: RoundId,\n    /// Timestamp when the probe was sent.\n    pub sent: SystemTime,\n}\n\n/// The type of ICMP packet received.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum IcmpPacketType {\n    /// `TimeExceeded` packet.\n    TimeExceeded(IcmpPacketCode),\n    /// `EchoReply` packet.\n    EchoReply(IcmpPacketCode),\n    /// Unreachable packet.\n    Unreachable(IcmpPacketCode),\n    /// Non-ICMP response (i.e. for some `UDP` & `TCP` probes).\n    NotApplicable,\n}\n\n/// The code of `TimeExceeded`, `EchoReply` and `Unreachable` ICMP packets.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub struct IcmpPacketCode(pub u8);\n\n/// The response to a probe.\n#[derive(Debug, Clone)]\npub enum Response {\n    TimeExceeded(ResponseData, IcmpPacketCode, Option<Extensions>),\n    DestinationUnreachable(ResponseData, IcmpPacketCode, Option<Extensions>),\n    EchoReply(ResponseData, IcmpPacketCode),\n    TcpReply(ResponseData),\n    TcpRefused(ResponseData),\n}\n\nimpl Response {\n    /// The data in the probe response.\n    pub const fn data(&self) -> &ResponseData {\n        match self {\n            Self::TimeExceeded(data, _, _)\n            | Self::DestinationUnreachable(data, _, _)\n            | Self::EchoReply(data, _)\n            | Self::TcpReply(data)\n            | Self::TcpRefused(data) => data,\n        }\n    }\n}\n\n/// The ICMP extensions for a probe response.\n#[derive(Debug, Clone, PartialEq, Eq, Default)]\npub struct Extensions {\n    pub extensions: Vec<Extension>,\n}\n\n/// A probe response extension.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum Extension {\n    Unknown(UnknownExtension),\n    Mpls(MplsLabelStack),\n}\n\nimpl Default for Extension {\n    fn default() -> Self {\n        Self::Unknown(UnknownExtension::default())\n    }\n}\n\n/// The members of a MPLS probe response extension.\n#[derive(Debug, Clone, PartialEq, Eq, Default)]\npub struct MplsLabelStack {\n    pub members: Vec<MplsLabelStackMember>,\n}\n\n/// A member of a MPLS probe response extension.\n#[derive(Debug, Clone, PartialEq, Eq, Default)]\npub struct MplsLabelStackMember {\n    pub label: u32,\n    pub exp: u8,\n    pub bos: u8,\n    pub ttl: u8,\n}\n\n/// An unknown ICMP extension.\n#[derive(Debug, Clone, PartialEq, Eq, Default)]\npub struct UnknownExtension {\n    pub class_num: u8,\n    pub class_subtype: u8,\n    pub bytes: Vec<u8>,\n}\n\n/// The data in the probe response.\n#[derive(Debug, Clone)]\npub struct ResponseData {\n    /// Timestamp of the probe response.\n    pub recv: SystemTime,\n    /// The `IpAddr` that responded to the probe.\n    pub addr: IpAddr,\n    /// Protocol specific response information.\n    pub proto_resp: ProtocolResponse,\n}\n\nimpl ResponseData {\n    pub const fn new(recv: SystemTime, addr: IpAddr, proto_resp: ProtocolResponse) -> Self {\n        Self {\n            recv,\n            addr,\n            proto_resp,\n        }\n    }\n}\n\n/// Protocol specific response information.\n///\n/// This includes protocol specific information that is used to:\n///\n/// - determine the sequence number for matching the incoming probe response\n///   against the outgoing probe.\n/// - validate the probe response against the expected values and discard\n///   invalid responses.\n/// - record information from the probe Original Datagram such as the type of\n///   service (DSCP/ECN) and the expected UDP checksum.\n#[derive(Debug, Clone)]\npub enum ProtocolResponse {\n    Icmp(IcmpProtocolResponse),\n    Udp(UdpProtocolResponse),\n    Tcp(TcpProtocolResponse),\n}\n\n/// The data in the response to an ICMP probe.\n#[derive(Debug, Clone)]\npub struct IcmpProtocolResponse {\n    /// The ICMP identifier.\n    pub identifier: u16,\n    /// The ICMP sequence number.\n    pub sequence: u16,\n    /// The type of service (DSCP/ECN) of the original datagram.\n    pub tos: Option<TypeOfService>,\n}\n\nimpl IcmpProtocolResponse {\n    pub const fn new(identifier: u16, sequence: u16, tos: Option<TypeOfService>) -> Self {\n        Self {\n            identifier,\n            sequence,\n            tos,\n        }\n    }\n}\n\n/// The data in the response to a UDP probe.\n#[derive(Debug, Clone)]\npub struct UdpProtocolResponse {\n    /// The IPv4 identifier.\n    ///\n    /// This will be the sequence number for IPv4/Dublin.\n    pub identifier: u16,\n    /// The destination IP address.\n    ///\n    /// This is used to validate the probe response matches the expected values.\n    pub dest_addr: IpAddr,\n    /// The source port.\n    ///\n    /// This is used to validate the probe response matches the expected values.\n    pub src_port: u16,\n    /// The destination port.\n    ///\n    /// This is used to validate the probe response matches the expected values.\n    pub dest_port: u16,\n    /// The type of service (DSCP/ECN) of the original datagram.\n    pub tos: Option<TypeOfService>,\n    /// The expected UDP checksum.\n    ///\n    /// This is calculated based on the data from the probe response and should\n    /// match the checksum that in the probe that was sent.\n    pub expected_udp_checksum: u16,\n    /// The actual UDP checksum.\n    ///\n    /// This will contain the sequence number for IPv4 and IPv6 Paris.\n    pub actual_udp_checksum: u16,\n    /// The length of the UDP payload.\n    ///\n    /// This payload length will be the sequence number (offset from the\n    /// initial sequence number) for IPv6 Dublin.  Note that this length\n    /// does not include the length of the MAGIC payload prefix.\n    pub payload_len: u16,\n    /// Whether the response had the MAGIC payload prefix.\n    ///\n    /// This will be true for IPv6 Dublin for probe responses which\n    /// originated from the tracer and is used to validate the probe response.\n    pub has_magic: bool,\n}\n\nimpl UdpProtocolResponse {\n    #[expect(clippy::too_many_arguments)]\n    pub const fn new(\n        identifier: u16,\n        dest_addr: IpAddr,\n        src_port: u16,\n        dest_port: u16,\n        tos: Option<TypeOfService>,\n        expected_udp_checksum: u16,\n        actual_udp_checksum: u16,\n        payload_len: u16,\n        has_magic: bool,\n    ) -> Self {\n        Self {\n            identifier,\n            dest_addr,\n            src_port,\n            dest_port,\n            tos,\n            expected_udp_checksum,\n            actual_udp_checksum,\n            payload_len,\n            has_magic,\n        }\n    }\n}\n\n/// The data in the response to an TCP probe.\n#[derive(Debug, Clone)]\npub struct TcpProtocolResponse {\n    /// The destination IP address.\n    ///\n    /// This is used to validate the probe response matches the expected values.\n    pub dest_addr: IpAddr,\n    /// The source port.\n    ///\n    /// This is used to validate the probe response matches the expected values.\n    pub src_port: u16,\n    /// The destination port.\n    ///\n    /// This is used to validate the probe response matches the expected values.\n    pub dest_port: u16,\n    /// The type of service (DSCP/ECN) of the original datagram.\n    pub tos: Option<TypeOfService>,\n}\n\nimpl TcpProtocolResponse {\n    pub const fn new(\n        dest_addr: IpAddr,\n        src_port: u16,\n        dest_port: u16,\n        tos: Option<TypeOfService>,\n    ) -> Self {\n        Self {\n            dest_addr,\n            src_port,\n            dest_port,\n            tos,\n        }\n    }\n}\n\n#[cfg(test)]\nimpl ProbeStatus {\n    #[must_use]\n    pub fn try_into_awaited(self) -> Option<Probe> {\n        if let Self::Awaited(awaited) = self {\n            Some(awaited)\n        } else {\n            None\n        }\n    }\n\n    #[must_use]\n    pub fn try_into_complete(self) -> Option<ProbeComplete> {\n        if let Self::Complete(complete) = self {\n            Some(complete)\n        } else {\n            None\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/state.rs",
    "content": "use crate::config::StateConfig;\nuse crate::constants::MAX_TTL;\nuse crate::flows::{Flow, FlowId, FlowRegistry};\nuse crate::{\n    Dscp, Ecn, Extensions, IcmpPacketType, ProbeStatus, Round, RoundId, TimeToLive, TypeOfService,\n};\nuse indexmap::IndexMap;\nuse std::collections::HashMap;\nuse std::iter::once;\nuse std::net::IpAddr;\nuse std::time::Duration;\nuse tracing::instrument;\n\n/// The state of a trace.\n#[derive(Debug, Clone, Default)]\npub struct State {\n    /// The configuration for the state.\n    state_config: StateConfig,\n    /// The flow id for the current round.\n    round_flow_id: FlowId,\n    /// Tracing state per registered flow id.\n    state: HashMap<FlowId, FlowState>,\n    /// Flow registry.\n    registry: FlowRegistry,\n    /// Tracing error message.\n    error: Option<String>,\n}\n\nimpl State {\n    /// Create a new `State`.\n    #[must_use]\n    pub fn new(state_config: StateConfig) -> Self {\n        Self {\n            state: once((\n                Self::default_flow_id(),\n                FlowState::new(state_config.max_samples),\n            ))\n            .collect::<HashMap<FlowId, FlowState>>(),\n            round_flow_id: Self::default_flow_id(),\n            state_config,\n            registry: FlowRegistry::new(),\n            error: None,\n        }\n    }\n\n    /// Return the id of the default flow.\n    #[must_use]\n    pub const fn default_flow_id() -> FlowId {\n        FlowId(0)\n    }\n\n    /// Information about each hop for the combined default flow.\n    #[must_use]\n    pub fn hops(&self) -> &[Hop] {\n        self.state[&Self::default_flow_id()].hops()\n    }\n\n    /// Information about each hop for a given flow.\n    #[must_use]\n    pub fn hops_for_flow(&self, flow_id: FlowId) -> &[Hop] {\n        self.state[&flow_id].hops()\n    }\n\n    /// Is a given `Hop` the target hop for a given flow?\n    ///\n    /// A `Hop` is considered to be the target if it has the highest `ttl` value observed.\n    ///\n    /// Note that if the target host does not respond to probes then the highest `ttl` observed\n    /// will be one greater than the `ttl` of the last host which did respond.\n    #[must_use]\n    pub fn is_target(&self, hop: &Hop, flow_id: FlowId) -> bool {\n        self.state[&flow_id].is_target(hop)\n    }\n\n    /// Is a given `Hop` in the current round for a given flow?\n    #[must_use]\n    pub fn is_in_round(&self, hop: &Hop, flow_id: FlowId) -> bool {\n        self.state[&flow_id].is_in_round(hop)\n    }\n\n    /// Return the target `Hop` for a given flow.\n    #[must_use]\n    pub fn target_hop(&self, flow_id: FlowId) -> &Hop {\n        self.state[&flow_id].target_hop()\n    }\n\n    /// The current round of tracing for a given flow.\n    #[must_use]\n    pub fn round(&self, flow_id: FlowId) -> Option<usize> {\n        self.state[&flow_id].round()\n    }\n\n    /// The total rounds of tracing for a given flow.\n    #[must_use]\n    pub fn round_count(&self, flow_id: FlowId) -> usize {\n        self.state[&flow_id].round_count()\n    }\n\n    /// The `FlowId` for the current round.\n    #[must_use]\n    pub const fn round_flow_id(&self) -> FlowId {\n        self.round_flow_id\n    }\n\n    /// The registry of flows in the trace.\n    #[must_use]\n    pub fn flows(&self) -> &[(Flow, FlowId)] {\n        self.registry.flows()\n    }\n\n    /// The error message for the trace, if any.\n    #[must_use]\n    pub fn error(&self) -> Option<&str> {\n        self.error.as_deref()\n    }\n\n    pub fn set_error(&mut self, error: Option<String>) {\n        self.error = error;\n    }\n\n    /// The maximum number of samples to record per hop.\n    #[must_use]\n    pub const fn max_samples(&self) -> usize {\n        self.state_config.max_samples\n    }\n\n    /// The maximum number of flows to record.\n    #[must_use]\n    pub const fn max_flows(&self) -> usize {\n        self.state_config.max_flows\n    }\n\n    /// Update the tracing state from a `TracerRound`.\n    #[instrument(skip(self, round), level = \"trace\")]\n    pub fn update_from_round(&mut self, round: &Round<'_>) {\n        let flow = Flow::from_hops(\n            round\n                .probes\n                .iter()\n                .filter_map(|probe| match probe {\n                    ProbeStatus::Awaited(_) => Some(None),\n                    ProbeStatus::Complete(completed) => Some(Some(completed.host)),\n                    _ => None,\n                })\n                .take(usize::from(round.largest_ttl.0)),\n        );\n        self.update_trace_flow(Self::default_flow_id(), round);\n        if self.registry.flows().len() < self.state_config.max_flows {\n            let flow_id = self.registry.register(flow);\n            self.round_flow_id = flow_id;\n            self.update_trace_flow(flow_id, round);\n        }\n    }\n\n    #[instrument(skip(self, round), level = \"trace\")]\n    fn update_trace_flow(&mut self, flow_id: FlowId, round: &Round<'_>) {\n        let flow_trace = self\n            .state\n            .entry(flow_id)\n            .or_insert_with(|| FlowState::new(self.state_config.max_samples));\n        flow_trace.update_from_round(round);\n    }\n}\n\n/// Information about a single `Hop` within a `Trace`.\n#[derive(Debug, Clone)]\npub struct Hop {\n    /// The ttl of this hop.\n    ttl: u8,\n    /// The addrs of this hop and associated counts.\n    addrs: IndexMap<IpAddr, usize>,\n    /// The total probes sent for this hop.\n    total_sent: usize,\n    /// The total probes received for this hop.\n    total_recv: usize,\n    /// The total probes that failed for this hop.\n    total_failed: usize,\n    /// The total forward loss for this hop.\n    total_forward_lost: usize,\n    /// The total backward loss for this hop.\n    total_backward_lost: usize,\n    /// The total round trip time for this hop across all rounds.\n    total_time: Duration,\n    /// The round trip time for this hop in the current round.\n    last: Option<Duration>,\n    /// The best round trip time for this hop across all rounds.\n    best: Option<Duration>,\n    /// The worst round trip time for this hop across all rounds.\n    worst: Option<Duration>,\n    /// The current jitter i.e. round-trip difference with the last round-trip.\n    jitter: Option<Duration>,\n    /// The average jitter time for all probes at this hop.\n    javg: f64,\n    /// The worst round-trip jitter time for all probes at this hop.\n    jmax: Option<Duration>,\n    /// The smoothed jitter value for all probes at this hop.\n    jinta: f64,\n    /// The source port for last probe for this hop.\n    last_src_port: u16,\n    /// The destination port for last probe for this hop.\n    last_dest_port: u16,\n    /// The sequence number for the last probe for this hop.\n    last_sequence: u16,\n    /// The icmp packet type for the last probe for this hop.\n    last_icmp_packet_type: Option<IcmpPacketType>,\n    /// The NAT detection status for the last probe for this hop.\n    last_nat_status: NatStatus,\n    /// The history of round trip times across the last N rounds.\n    samples: Vec<Duration>,\n    /// The type of service (DSCP/ECN) for this hop.\n    tos: Option<TypeOfService>,\n    /// The ICMP extensions for this hop.\n    extensions: Option<Extensions>,\n    mean: f64,\n    m2: f64,\n}\n\nimpl Hop {\n    /// The time-to-live of this hop.\n    #[must_use]\n    pub const fn ttl(&self) -> u8 {\n        self.ttl\n    }\n\n    /// The set of addresses that have responded for this time-to-live.\n    pub fn addrs(&self) -> impl Iterator<Item = &IpAddr> {\n        self.addrs.keys()\n    }\n\n    pub fn addrs_with_counts(&self) -> impl Iterator<Item = (&IpAddr, &usize)> {\n        self.addrs.iter()\n    }\n\n    /// The number of unique address observed for this time-to-live.\n    #[must_use]\n    pub fn addr_count(&self) -> usize {\n        self.addrs.len()\n    }\n\n    /// The total number of probes sent.\n    #[must_use]\n    pub const fn total_sent(&self) -> usize {\n        self.total_sent\n    }\n\n    /// The total number of probes responses received.\n    #[must_use]\n    pub const fn total_recv(&self) -> usize {\n        self.total_recv\n    }\n\n    /// The total number of probes with forward loss.\n    #[must_use]\n    pub const fn total_forward_loss(&self) -> usize {\n        self.total_forward_lost\n    }\n\n    /// The total number of probes with backward loss.\n    #[must_use]\n    pub const fn total_backward_loss(&self) -> usize {\n        self.total_backward_lost\n    }\n\n    /// The total number of probes that failed.\n    #[must_use]\n    pub const fn total_failed(&self) -> usize {\n        self.total_failed\n    }\n\n    /// The % of packets that are lost.\n    #[must_use]\n    pub fn loss_pct(&self) -> f64 {\n        if self.total_sent > 0 {\n            let lost = self.total_sent - self.total_recv;\n            lost as f64 / self.total_sent as f64 * 100_f64\n        } else {\n            0_f64\n        }\n    }\n\n    /// The % of packets that are lost forward.\n    #[must_use]\n    pub fn forward_loss_pct(&self) -> f64 {\n        if self.total_sent > 0 {\n            let lost = self.total_forward_lost;\n            lost as f64 / self.total_sent as f64 * 100_f64\n        } else {\n            0_f64\n        }\n    }\n\n    /// The % of packets that are lost backward.\n    #[must_use]\n    pub fn backward_loss_pct(&self) -> f64 {\n        if self.total_sent > 0 {\n            let lost = self.total_backward_lost;\n            lost as f64 / self.total_sent as f64 * 100_f64\n        } else {\n            0_f64\n        }\n    }\n\n    /// The duration of the last probe.\n    #[must_use]\n    pub fn last_ms(&self) -> Option<f64> {\n        self.last.map(|last| last.as_secs_f64() * 1000_f64)\n    }\n\n    /// The duration of the best probe observed.\n    #[must_use]\n    pub fn best_ms(&self) -> Option<f64> {\n        self.best.map(|last| last.as_secs_f64() * 1000_f64)\n    }\n\n    /// The duration of the worst probe observed.\n    #[must_use]\n    pub fn worst_ms(&self) -> Option<f64> {\n        self.worst.map(|last| last.as_secs_f64() * 1000_f64)\n    }\n\n    /// The average duration of all probes.\n    #[must_use]\n    pub fn avg_ms(&self) -> f64 {\n        if self.total_recv() > 0 {\n            (self.total_time.as_secs_f64() * 1000_f64) / self.total_recv as f64\n        } else {\n            0_f64\n        }\n    }\n\n    /// The standard deviation of all probes.\n    #[must_use]\n    pub fn stddev_ms(&self) -> f64 {\n        if self.total_recv > 1 {\n            (self.m2 / (self.total_recv - 1) as f64).sqrt()\n        } else {\n            0_f64\n        }\n    }\n\n    /// The duration of the jitter probe observed.\n    #[must_use]\n    pub fn jitter_ms(&self) -> Option<f64> {\n        self.jitter.map(|j| j.as_secs_f64() * 1000_f64)\n    }\n\n    /// The duration of the worst probe observed.\n    #[must_use]\n    pub fn jmax_ms(&self) -> Option<f64> {\n        self.jmax.map(|x| x.as_secs_f64() * 1000_f64)\n    }\n\n    /// The jitter average duration of all probes.\n    #[must_use]\n    pub const fn javg_ms(&self) -> f64 {\n        self.javg\n    }\n\n    /// The jitter interval of all probes.\n    #[must_use]\n    pub const fn jinta(&self) -> f64 {\n        self.jinta\n    }\n\n    /// The source port for last probe for this hop.\n    #[must_use]\n    pub const fn last_src_port(&self) -> u16 {\n        self.last_src_port\n    }\n\n    /// The destination port for last probe for this hop.\n    #[must_use]\n    pub const fn last_dest_port(&self) -> u16 {\n        self.last_dest_port\n    }\n\n    /// The sequence number for the last probe for this hop.\n    #[must_use]\n    pub const fn last_sequence(&self) -> u16 {\n        self.last_sequence\n    }\n\n    /// The icmp packet type for the last probe for this hop.\n    #[must_use]\n    pub const fn last_icmp_packet_type(&self) -> Option<IcmpPacketType> {\n        self.last_icmp_packet_type\n    }\n\n    /// The NAT detection status for the last probe for this hop.\n    #[must_use]\n    pub const fn last_nat_status(&self) -> NatStatus {\n        self.last_nat_status\n    }\n\n    /// The type of service (DSCP/ECN) for this hop.\n    #[must_use]\n    pub fn tos(&self) -> Option<TypeOfService> {\n        self.tos\n    }\n\n    /// The `DSCP` for this hop.\n    #[must_use]\n    pub fn dscp(&self) -> Option<Dscp> {\n        self.tos.map(|tos| tos.dscp())\n    }\n\n    /// The `ECN` for this hop.\n    #[must_use]\n    pub fn ecn(&self) -> Option<Ecn> {\n        self.tos.map(|tos| tos.ecn())\n    }\n\n    /// The last N samples.\n    #[must_use]\n    pub fn samples(&self) -> &[Duration] {\n        &self.samples\n    }\n\n    #[must_use]\n    pub const fn extensions(&self) -> Option<&Extensions> {\n        self.extensions.as_ref()\n    }\n}\n\nimpl Default for Hop {\n    fn default() -> Self {\n        Self {\n            ttl: 0,\n            addrs: IndexMap::default(),\n            total_sent: 0,\n            total_recv: 0,\n            total_forward_lost: 0,\n            total_backward_lost: 0,\n            total_failed: 0,\n            total_time: Duration::default(),\n            last: None,\n            best: None,\n            worst: None,\n            jitter: None,\n            javg: 0f64,\n            jmax: None,\n            jinta: 0f64,\n            last_src_port: 0_u16,\n            last_dest_port: 0_u16,\n            last_sequence: 0_u16,\n            last_icmp_packet_type: None,\n            mean: 0f64,\n            m2: 0f64,\n            samples: Vec::default(),\n            tos: None,\n            extensions: None,\n            last_nat_status: NatStatus::NotApplicable,\n        }\n    }\n}\n\n/// The state of a NAT detection for a `Hop`.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum NatStatus {\n    /// NAT detection was not applicable.\n    NotApplicable,\n    /// NAT was not detected at this hop.\n    NotDetected,\n    /// NAT was detected at this hop.\n    Detected,\n}\n\n/// Data for a single trace flow.\n#[derive(Debug, Clone)]\nstruct FlowState {\n    /// The maximum number of samples to record.\n    max_samples: usize,\n    /// The lowest ttl observed across all rounds.\n    lowest_ttl: u8,\n    /// The highest ttl observed across all rounds.\n    highest_ttl: u8,\n    /// The highest ttl observed for the latest round.\n    highest_ttl_for_round: u8,\n    /// The latest round received.\n    round: Option<usize>,\n    /// The total number of rounds received.\n    round_count: usize,\n    /// The hops in this trace.\n    hops: Vec<Hop>,\n}\n\nimpl FlowState {\n    fn new(max_samples: usize) -> Self {\n        Self {\n            max_samples,\n            lowest_ttl: 0,\n            highest_ttl: 0,\n            highest_ttl_for_round: 0,\n            round: None,\n            round_count: 0,\n            hops: (0..MAX_TTL).map(|_| Hop::default()).collect(),\n        }\n    }\n\n    fn hops(&self) -> &[Hop] {\n        if self.lowest_ttl == 0 || self.highest_ttl == 0 {\n            &[]\n        } else {\n            let start = (self.lowest_ttl as usize) - 1;\n            let end = self.highest_ttl as usize;\n            &self.hops[start..end]\n        }\n    }\n\n    const fn is_target(&self, hop: &Hop) -> bool {\n        self.highest_ttl_for_round == hop.ttl\n    }\n\n    const fn is_in_round(&self, hop: &Hop) -> bool {\n        hop.ttl <= self.highest_ttl_for_round\n    }\n\n    fn target_hop(&self) -> &Hop {\n        if self.highest_ttl_for_round > 0 {\n            &self.hops[usize::from(self.highest_ttl_for_round) - 1]\n        } else {\n            &self.hops[0]\n        }\n    }\n\n    const fn round(&self) -> Option<usize> {\n        self.round\n    }\n\n    const fn round_count(&self) -> usize {\n        self.round_count\n    }\n\n    fn update_from_round(&mut self, round: &Round<'_>) {\n        state_updater::StateUpdater::new(self, round).apply();\n    }\n\n    fn update_round(&mut self, round: RoundId) {\n        self.round = match self.round {\n            None => Some(round.0),\n            Some(r) => Some(r.max(round.0)),\n        }\n    }\n\n    fn update_lowest_ttl(&mut self, ttl: TimeToLive) {\n        if self.lowest_ttl == 0 {\n            self.lowest_ttl = ttl.0;\n        } else {\n            self.lowest_ttl = self.lowest_ttl.min(ttl.0);\n        }\n    }\n}\n\nmod state_updater {\n    use crate::state::FlowState;\n    use crate::types::Checksum;\n    use crate::{NatStatus, ProbeStatus, Round, TimeToLive};\n    use std::time::Duration;\n    use tracing::instrument;\n\n    /// Update the state of a `FlowState` from a `Round`.\n    pub(super) struct StateUpdater<'a> {\n        /// The state to update.\n        state: &'a mut FlowState,\n        /// The `Round` being processed.\n        round: &'a Round<'a>,\n        /// The checksum of the previous hop, if any.\n        prev_hop_checksum: Option<u16>,\n        /// Whether any previous hop in the round had forward loss.\n        forward_loss: bool,\n    }\n    impl<'a> StateUpdater<'a> {\n        pub(super) fn new(state: &'a mut FlowState, round: &'a Round<'_>) -> Self {\n            Self {\n                state,\n                round,\n                prev_hop_checksum: None,\n                forward_loss: false,\n            }\n        }\n\n        #[instrument(skip(self), level = \"trace\")]\n        pub(super) fn apply(&mut self) {\n            self.state.round_count += 1;\n            self.state.highest_ttl =\n                std::cmp::max(self.state.highest_ttl, self.round.largest_ttl.0);\n            self.state.highest_ttl_for_round = self.round.largest_ttl.0;\n            for probe in self.round.probes {\n                self.update_for_probe(probe);\n            }\n        }\n\n        #[instrument(skip(self), level = \"trace\")]\n        fn update_for_probe(&mut self, probe: &ProbeStatus) {\n            let state = &mut *self.state;\n            match probe {\n                ProbeStatus::Complete(complete) => {\n                    state.update_lowest_ttl(complete.ttl);\n                    state.update_round(complete.round);\n                    let index = usize::from(complete.ttl.0) - 1;\n                    let hop = &mut state.hops[index];\n                    hop.ttl = complete.ttl.0;\n                    hop.total_sent += 1;\n                    hop.total_recv += 1;\n                    let dur = complete\n                        .received\n                        .duration_since(complete.sent)\n                        .unwrap_or_default();\n                    let dur_ms = dur.as_secs_f64() * 1000_f64;\n                    hop.total_time += dur;\n                    // Before last is set use it to calc jitter\n                    let last_ms = hop.last_ms().unwrap_or_default();\n                    let jitter_ms = (dur_ms - last_ms).abs();\n                    let jitter_dur = Duration::from_secs_f64(jitter_ms / 1000_f64);\n                    hop.jitter = hop.last.and(Some(jitter_dur));\n                    hop.javg += (jitter_ms - hop.javg) / hop.total_recv as f64;\n                    // algorithm is from rfc1889, A.8 or rfc3550\n                    hop.jinta += jitter_ms.max(0.5) - ((hop.jinta + 8.0) / 16.0);\n                    hop.jmax = hop\n                        .jmax\n                        .map_or(Some(jitter_dur), |d| Some(d.max(jitter_dur)));\n                    hop.last = Some(dur);\n                    hop.samples.insert(0, dur);\n                    hop.best = hop.best.map_or(Some(dur), |d| Some(d.min(dur)));\n                    hop.worst = hop.worst.map_or(Some(dur), |d| Some(d.max(dur)));\n                    hop.mean += (dur_ms - hop.mean) / hop.total_recv as f64;\n                    hop.m2 += (dur_ms - hop.mean) * (dur_ms - hop.mean);\n                    if hop.samples.len() > state.max_samples {\n                        hop.samples.pop();\n                    }\n                    let host = complete.host;\n                    *hop.addrs.entry(host).or_default() += 1;\n                    hop.extensions.clone_from(&complete.extensions);\n                    hop.last_src_port = complete.src_port.0;\n                    hop.last_dest_port = complete.dest_port.0;\n                    hop.last_sequence = complete.sequence.0;\n                    hop.last_icmp_packet_type = Some(complete.icmp_packet_type);\n                    hop.tos = complete.tos;\n                    if let (Some(expected), Some(actual)) =\n                        (complete.expected_udp_checksum, complete.actual_udp_checksum)\n                    {\n                        let (nat_status, checksum) =\n                            nat_status(expected, actual, self.prev_hop_checksum);\n                        hop.last_nat_status = nat_status;\n                        self.prev_hop_checksum = Some(checksum);\n                    }\n                }\n                ProbeStatus::Awaited(awaited) => {\n                    state.update_lowest_ttl(awaited.ttl);\n                    state.update_round(awaited.round);\n                    let index = usize::from(awaited.ttl.0) - 1;\n                    let hop = &mut state.hops[index];\n                    hop.total_sent += 1;\n                    hop.ttl = awaited.ttl.0;\n                    hop.samples.insert(0, Duration::default());\n                    if hop.samples.len() > state.max_samples {\n                        hop.samples.pop();\n                    }\n                    hop.last_src_port = awaited.src_port.0;\n                    hop.last_dest_port = awaited.dest_port.0;\n                    hop.last_sequence = awaited.sequence.0;\n                    if self.forward_loss {\n                        hop.total_backward_lost += 1;\n                    } else if is_forward_loss(self.round.probes, awaited.ttl) {\n                        hop.total_forward_lost += 1;\n                        self.forward_loss = true;\n                    }\n                }\n                ProbeStatus::Failed(failed) => {\n                    state.update_lowest_ttl(failed.ttl);\n                    state.update_round(failed.round);\n                    let index = usize::from(failed.ttl.0) - 1;\n                    let hop = &mut state.hops[index];\n                    hop.total_sent += 1;\n                    hop.total_failed += 1;\n                    hop.ttl = failed.ttl.0;\n                    hop.samples.insert(0, Duration::default());\n                    if hop.samples.len() > state.max_samples {\n                        hop.samples.pop();\n                    }\n                    hop.last_src_port = failed.src_port.0;\n                    hop.last_dest_port = failed.dest_port.0;\n                    hop.last_sequence = failed.sequence.0;\n                }\n                ProbeStatus::NotSent | ProbeStatus::Skipped => {}\n            }\n        }\n    }\n\n    /// Determine if forward loss has occurred at a given time-to-live.\n    ///\n    /// This is determined by checking if all probes after the awaited probe are all also awaited.\n    fn is_forward_loss(probes: &[ProbeStatus], awaited_ttl: TimeToLive) -> bool {\n        // Skip all probes that have a ttl less than or equal to the awaited ttl. What remains\n        // are the probes we are interested in.\n        let mut remaining = probes\n            .iter()\n            .skip_while(|p| match p {\n                ProbeStatus::Awaited(a) => a.ttl <= awaited_ttl,\n                ProbeStatus::Complete(c) => c.ttl <= awaited_ttl,\n                ProbeStatus::Failed(f) => f.ttl <= awaited_ttl,\n                ProbeStatus::NotSent | ProbeStatus::Skipped => true,\n            })\n            .peekable();\n        let is_empty = remaining.peek().is_none();\n        let all_awaited =\n            remaining.all(|p| matches!(p, ProbeStatus::Awaited(_) | ProbeStatus::Skipped));\n        // If there is at least one probe remaining and all are awaited then we have forward loss.\n        !is_empty && all_awaited\n    }\n\n    /// Determine the NAT detection status.\n    ///\n    /// Returns a tuple of the NAT detection status and the checksum to use for the next hop.\n    const fn nat_status(\n        expected: Checksum,\n        actual: Checksum,\n        prev_hop_checksum: Option<u16>,\n    ) -> (NatStatus, u16) {\n        if let Some(prev_hop_checksum) = prev_hop_checksum {\n            // If the actual checksum matches the checksum of the previous probe\n            // then we can assume NAT has not occurred.  Note that it is perfectly\n            // valid for the expected checksum to differ from the actual checksum\n            // in this case as the NAT'ed checksum \"carries forward\" throughout the\n            // remainder of the hops on the path.\n            if prev_hop_checksum == actual.0 {\n                (NatStatus::NotDetected, prev_hop_checksum)\n            } else {\n                (NatStatus::Detected, actual.0)\n            }\n        } else {\n            // If we have no prior checksum (i.e. this is the first probe that\n            // responded) and the expected and actual checksums do not match then\n            // we can assume NAT has occurred.\n            if expected.0 == actual.0 {\n                (NatStatus::NotDetected, actual.0)\n            } else {\n                (NatStatus::Detected, actual.0)\n            }\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n        use crate::probe::ProbeFailed;\n        use crate::{\n            Flags, IcmpPacketType, Port, Probe, ProbeComplete, RoundId, Sequence, TimeToLive,\n            TraceId,\n        };\n        use std::net::{IpAddr, Ipv4Addr};\n        use std::time::SystemTime;\n        use test_case::test_case;\n\n        #[test_case(false, &[], 1; \"no forward loss no probes ttl 1\")]\n        #[test_case(true, &[('a', 1), ('a', 2)], 1; \"forward loss AA ttl 1\")]\n        #[test_case(false, &[('a', 1), ('c', 2)], 1; \"no forward loss AC ttl 1\")]\n        #[test_case(false, &[('a', 1), ('f', 2)], 1; \"no forward loss AF ttl 1\")]\n        #[test_case(false, &[('a', 1), ('n', 2)], 1; \"no forward loss AN ttl 1\")]\n        #[test_case(false, &[('a', 1), ('c', 2), ('a', 3), ('a', 4)], 1; \"no forward loss ACAA ttl 1\")]\n        #[test_case(true, &[('a', 1), ('c', 2), ('a', 3), ('a', 4)], 3; \"forward loss ACAA ttl 3\")]\n        #[test_case(false, &[('a', 1), ('c', 2), ('a', 3), ('a', 4)], 4; \"no forward loss ACAA ttl 4\")]\n        #[test_case(false, &[('a', 1), ('f', 2), ('n', 3), ('a', 4)], 4; \"no forward loss AFAN ttl 1\")]\n        #[test_case(true, &[('a', 4), ('a', 5)], 4; \"forward loss AA non-default minimum ttl 4\")]\n        #[test_case(false, &[('a', 4), ('c', 5)], 4; \"no forward loss AC non-default minimum ttl 4\")]\n        #[test_case(false, &[('a', 4), ('c', 5), ('a', 6), ('a', 7)], 4; \"no forward loss ACAA non-default minimum ttl 4\")]\n        #[test_case(true, &[('a', 4), ('c', 5), ('a', 6), ('a', 7)], 6; \"forward loss ACAA non-default minimum ttl 6\")]\n        fn test_is_forward_loss(expected: bool, probes: &[(char, u8)], awaited_ttl: u8) {\n            assert!(awaited_ttl > 0);\n            let probes = probes\n                .iter()\n                .map(|(typ, ttl)| {\n                    assert!(matches!(typ, 'n' | 's' | 'f' | 'a' | 'c'));\n                    if *ttl == awaited_ttl {\n                        assert!(matches!(typ, 'a'));\n                    }\n                    match typ {\n                        'n' => ProbeStatus::NotSent,\n                        's' => ProbeStatus::Skipped,\n                        'f' => ProbeStatus::Failed(ProbeFailed {\n                            sequence: Sequence::default(),\n                            identifier: TraceId::default(),\n                            src_port: Port::default(),\n                            dest_port: Port::default(),\n                            ttl: TimeToLive(*ttl),\n                            round: RoundId::default(),\n                            sent: SystemTime::now(),\n                        }),\n                        'a' => ProbeStatus::Awaited(Probe {\n                            sequence: Sequence::default(),\n                            identifier: TraceId::default(),\n                            src_port: Port::default(),\n                            dest_port: Port::default(),\n                            ttl: TimeToLive(*ttl),\n                            round: RoundId(0),\n                            sent: SystemTime::now(),\n                            flags: Flags::empty(),\n                        }),\n                        'c' => ProbeStatus::Complete(ProbeComplete {\n                            sequence: Sequence::default(),\n                            identifier: TraceId::default(),\n                            src_port: Port::default(),\n                            dest_port: Port::default(),\n                            ttl: TimeToLive(*ttl),\n                            round: RoundId::default(),\n                            sent: SystemTime::now(),\n                            host: IpAddr::V4(Ipv4Addr::UNSPECIFIED),\n                            received: SystemTime::now(),\n                            icmp_packet_type: IcmpPacketType::NotApplicable,\n                            tos: None,\n                            expected_udp_checksum: None,\n                            actual_udp_checksum: None,\n                            extensions: None,\n                        }),\n                        _ => unreachable!(),\n                    }\n                })\n                .collect::<Vec<_>>();\n            assert_eq!(is_forward_loss(&probes, TimeToLive(awaited_ttl)), expected);\n        }\n\n        #[test_case(123, 123, None => (NatStatus::NotDetected, 123); \"first hop matching checksum\")]\n        #[test_case(123, 321, None => (NatStatus::Detected, 321); \"first hop non-matching checksum\")]\n        #[test_case(123, 123, Some(123) => (NatStatus::NotDetected, 123); \"non-first hop matching checksum match previous\")]\n        #[test_case(999, 999, Some(321) => (NatStatus::Detected, 999); \"non-first hop matching checksum not match previous\")]\n        #[test_case(777, 888, Some(321) => (NatStatus::Detected, 888); \"non-first hop non-matching checksum not match previous\")]\n        const fn test_nat(expected: u16, actual: u16, prev: Option<u16>) -> (NatStatus, u16) {\n            nat_status(Checksum(expected), Checksum(actual), prev)\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::types::Checksum;\n    use crate::{\n        CompletionReason, Flags, IcmpPacketType, Port, Probe, ProbeComplete, ProbeStatus, Sequence,\n        TimeToLive, TraceId, TypeOfService,\n    };\n    use anyhow::anyhow;\n    use serde::Deserialize;\n    use std::collections::HashSet;\n    use std::fmt::Debug;\n    use std::ops::Add;\n    use std::str::FromStr;\n    use std::time::SystemTime;\n    use test_case::test_case;\n\n    /// A test scenario.\n    #[derive(Deserialize, Debug)]\n    #[serde(deny_unknown_fields)]\n    struct Scenario {\n        /// the biggest ttl expected in this scenario\n        largest_ttl: u8,\n        /// The rounds of probe tracing data in this scenario.\n        rounds: Vec<RoundData>,\n        /// The expected outcome from running this scenario.\n        expected: Expected,\n    }\n\n    /// A single round of tracing probe data.\n    #[derive(Deserialize, Debug)]\n    #[serde(deny_unknown_fields)]\n    struct RoundData {\n        /// The probes in this round.\n        probes: Vec<ProbeData>,\n    }\n\n    /// A single probe from a single round.\n    #[derive(Deserialize, Debug)]\n    #[serde(deny_unknown_fields)]\n    #[serde(try_from = \"String\")]\n    struct ProbeData(ProbeStatus);\n\n    impl TryFrom<String> for ProbeData {\n        type Error = anyhow::Error;\n\n        fn try_from(value: String) -> Result<Self, Self::Error> {\n            // format: `{ttl} {status} {duration} {host} {sequence} {src_port} {dest_port} {checksum} {tos}`\n            let values = value.split_ascii_whitespace().collect::<Vec<_>>();\n            if values.len() == 10 {\n                let ttl = TimeToLive(u8::from_str(values[0])?);\n                let state = values[1].to_ascii_lowercase();\n                let sequence = Sequence(u16::from_str(values[4])?);\n                let src_port = Port(u16::from_str(values[5])?);\n                let dest_port = Port(u16::from_str(values[6])?);\n                let round = RoundId(0); // note we inject this later, see `ProbeRound`\n                let sent = SystemTime::now();\n                let flags = Flags::empty();\n                let state = match state.as_str() {\n                    \"n\" => Ok(ProbeStatus::NotSent),\n                    \"s\" => Ok(ProbeStatus::Skipped),\n                    \"a\" => Ok(ProbeStatus::Awaited(Probe::new(\n                        sequence,\n                        TraceId(0),\n                        src_port,\n                        dest_port,\n                        ttl,\n                        round,\n                        sent,\n                        flags,\n                    ))),\n                    \"c\" => {\n                        let host = IpAddr::from_str(values[3])?;\n                        let duration = Duration::from_millis(u64::from_str(values[2])?);\n                        let received = sent.add(duration);\n                        let expected_udp_checksum = Some(Checksum(u16::from_str(values[7])?));\n                        let actual_udp_checksum = Some(Checksum(u16::from_str(values[8])?));\n                        let icmp_packet_type = IcmpPacketType::NotApplicable;\n                        let tos = Some(TypeOfService(u8::from_str(values[9])?));\n                        Ok(ProbeStatus::Complete(\n                            Probe::new(\n                                sequence,\n                                TraceId(0),\n                                src_port,\n                                dest_port,\n                                ttl,\n                                round,\n                                sent,\n                                flags,\n                            )\n                            .complete(\n                                host,\n                                received,\n                                icmp_packet_type,\n                                tos,\n                                expected_udp_checksum,\n                                actual_udp_checksum,\n                                None,\n                            ),\n                        ))\n                    }\n                    _ => Err(anyhow!(\"unknown probe state\")),\n                }?;\n                Ok(Self(state))\n            } else {\n                Err(anyhow!(\"failed to parse {value}\"))\n            }\n        }\n    }\n\n    /// A helper struct so we may inject the round into the probes.\n    struct ProbeRound(ProbeData, RoundId);\n\n    impl From<ProbeRound> for ProbeStatus {\n        fn from(value: ProbeRound) -> Self {\n            let probe_data = value.0;\n            let round = value.1;\n            match probe_data.0 {\n                Self::NotSent => Self::NotSent,\n                Self::Skipped => Self::Skipped,\n                Self::Awaited(awaited) => Self::Awaited(Probe { round, ..awaited }),\n                Self::Complete(completed) => Self::Complete(ProbeComplete { round, ..completed }),\n                Self::Failed(failed) => Self::Failed(failed),\n            }\n        }\n    }\n\n    /// The expected outcome.\n    #[derive(Deserialize, Debug, Clone)]\n    #[serde(deny_unknown_fields)]\n    struct Expected {\n        /// The expected outcome per hop.\n        hops: Vec<HopData>,\n    }\n\n    /// The expected outcome for a single hop.\n    #[derive(Deserialize, Debug, Clone)]\n    #[serde(deny_unknown_fields)]\n    struct HopData {\n        ttl: Option<u8>,\n        total_sent: Option<usize>,\n        total_recv: Option<usize>,\n        total_forward_loss: Option<usize>,\n        total_backward_loss: Option<usize>,\n        loss_pct: Option<f64>,\n        last_ms: Option<f64>,\n        best_ms: Option<f64>,\n        worst_ms: Option<f64>,\n        avg_ms: Option<f64>,\n        jitter: Option<f64>,\n        javg: Option<f64>,\n        jmax: Option<f64>,\n        jinta: Option<f64>,\n        addrs: Option<HashMap<IpAddr, usize>>,\n        samples: Option<Vec<f64>>,\n        last_src: Option<u16>,\n        last_dest: Option<u16>,\n        last_sequence: Option<u16>,\n        last_nat_status: Option<NatStatusWrapper>,\n        tos: Option<u8>,\n    }\n\n    /// A wrapper struct over `NatStatus` to allow deserialization.\n    #[derive(Deserialize, Debug, Clone)]\n    #[serde(try_from = \"String\")]\n    struct NatStatusWrapper(NatStatus);\n\n    impl TryFrom<String> for NatStatusWrapper {\n        type Error = anyhow::Error;\n\n        fn try_from(value: String) -> Result<Self, Self::Error> {\n            match value.to_ascii_lowercase().as_str() {\n                \"none\" => Ok(Self(NatStatus::NotApplicable)),\n                \"nat\" => Ok(Self(NatStatus::Detected)),\n                \"no_nat\" => Ok(Self(NatStatus::NotDetected)),\n                _ => Err(anyhow!(\"unknown nat status\")),\n            }\n        }\n    }\n\n    macro_rules! file {\n        ($path:expr) => {{\n            let data = include_str!(concat!(\"../tests/resources/state/\", $path));\n            toml::from_str(data).unwrap()\n        }};\n    }\n\n    #[test_case(file!(\"full_mixed.toml\"))]\n    #[test_case(file!(\"full_completed.toml\"))]\n    #[test_case(file!(\"all_status.toml\"))]\n    #[test_case(file!(\"no_latency.toml\"))]\n    #[test_case(file!(\"nat.toml\"))]\n    #[test_case(file!(\"minimal.toml\"))]\n    #[test_case(file!(\"floss_bloss.toml\"))]\n    #[test_case(file!(\"non_default_minimum_ttl.toml\"))]\n    #[test_case(file!(\"tos.toml\"))]\n    fn test_scenario(scenario: Scenario) {\n        let mut trace = State::new(StateConfig {\n            max_flows: 1,\n            ..StateConfig::default()\n        });\n        for (i, round) in scenario.rounds.into_iter().enumerate() {\n            let probes = round\n                .probes\n                .into_iter()\n                .map(|p| ProbeRound(p, RoundId(i)))\n                .map(Into::into)\n                .collect::<Vec<_>>();\n            let largest_ttl = TimeToLive(scenario.largest_ttl);\n            let tracer_round = Round::new(&probes, largest_ttl, CompletionReason::TargetFound);\n            trace.update_from_round(&tracer_round);\n        }\n        let actual_hops = trace.hops();\n        let expected_hops = scenario.expected.hops;\n        for (actual, expected) in actual_hops.iter().zip(expected_hops) {\n            assert_eq_opt(Some(&actual.ttl()), expected.ttl.as_ref());\n            assert_eq_opt(\n                Some(actual.addrs().collect::<HashSet<_>>()),\n                expected\n                    .addrs\n                    .as_ref()\n                    .map(|addrs| addrs.keys().collect::<HashSet<_>>()),\n            );\n            assert_eq_opt(\n                Some(actual.addr_count()),\n                expected.addrs.as_ref().map(HashMap::len),\n            );\n            assert_eq_opt(Some(&actual.total_sent()), expected.total_sent.as_ref());\n            assert_eq_opt(Some(&actual.total_recv()), expected.total_recv.as_ref());\n            assert_eq_opt_f64(Some(&actual.loss_pct()), expected.loss_pct.as_ref());\n            assert_eq_opt(\n                Some(&actual.total_forward_loss()),\n                expected.total_forward_loss.as_ref(),\n            );\n            assert_eq_opt(\n                Some(&actual.total_backward_loss()),\n                expected.total_backward_loss.as_ref(),\n            );\n            assert_eq_opt_f64(actual.last_ms().as_ref(), expected.last_ms.as_ref());\n            assert_eq_opt_f64(actual.best_ms().as_ref(), expected.best_ms.as_ref());\n            assert_eq_opt_f64(actual.worst_ms().as_ref(), expected.worst_ms.as_ref());\n            assert_eq_opt_f64(Some(&actual.avg_ms()), expected.avg_ms.as_ref());\n            assert_eq_opt_f64(actual.jitter_ms().as_ref(), expected.jitter.as_ref());\n            assert_eq_opt_f64(Some(&actual.javg_ms()), expected.javg.as_ref());\n            assert_eq_opt_f64(actual.jmax_ms().as_ref(), expected.jmax.as_ref());\n            assert_eq_opt_f64(Some(&actual.jinta()), expected.jinta.as_ref());\n            assert_eq_opt(Some(&actual.last_src_port()), expected.last_src.as_ref());\n            assert_eq_opt(Some(&actual.last_dest_port()), expected.last_dest.as_ref());\n            assert_eq_opt(\n                Some(&actual.last_sequence()),\n                expected.last_sequence.as_ref(),\n            );\n            assert_eq_opt(\n                Some(&actual.last_nat_status()),\n                expected.last_nat_status.as_ref().map(|nat| &nat.0),\n            );\n            assert_eq_vec_f64(\n                Some(\n                    &actual\n                        .samples()\n                        .iter()\n                        .map(|s| s.as_secs_f64() * 1000_f64)\n                        .collect(),\n                ),\n                expected.samples.as_ref(),\n            );\n            assert_eq_opt(actual.tos().map(|tos| tos.0), expected.tos);\n        }\n    }\n\n    #[expect(clippy::needless_pass_by_value)]\n    fn assert_eq_opt<T: Eq + Debug>(actual: Option<T>, expected: Option<T>) {\n        assert_eq_inner(actual.as_ref(), expected.as_ref(), |a, e| a == e);\n    }\n\n    fn assert_eq_opt_f64(actual: Option<&f64>, expected: Option<&f64>) {\n        assert_eq_inner(actual, expected, |a, e| (e - a).abs() < f64::EPSILON);\n    }\n\n    fn assert_eq_vec_f64(actual: Option<&Vec<f64>>, expected: Option<&Vec<f64>>) {\n        assert_eq_inner(actual, expected, |a, e| {\n            if a.len() != e.len() {\n                return false;\n            }\n            a.iter()\n                .zip(e.iter())\n                .all(|(a, e)| (e - a).abs() < f64::EPSILON)\n        });\n    }\n\n    fn assert_eq_inner<T: Debug>(\n        actual: Option<&T>,\n        expected: Option<&T>,\n        eq: impl Fn(&T, &T) -> bool,\n    ) {\n        match (actual, expected) {\n            (Some(actual), Some(expected)) if eq(actual, expected) => {}\n            (Some(actual), Some(expected)) => {\n                panic!(\"expected {expected:?} did not match actual {actual:?}\")\n            }\n            (None, Some(_)) => panic!(\"expected {expected:?} but no actual\"),\n            (_, None) => {}\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/strategy.rs",
    "content": "use self::state::TracerState;\nuse crate::config::StrategyConfig;\nuse crate::error::{Error, Result};\nuse crate::net::Network;\nuse crate::probe::{\n    IcmpProtocolResponse, ProbeStatus, ProtocolResponse, Response, ResponseData,\n    TcpProtocolResponse, UdpProtocolResponse,\n};\nuse crate::types::{Checksum, Sequence, TimeToLive, TraceId};\nuse crate::{\n    Extensions, IcmpPacketType, MultipathStrategy, PortDirection, Probe, Protocol, TypeOfService,\n};\nuse std::net::IpAddr;\nuse std::time::{Duration, SystemTime};\nuse tracing::instrument;\n\n/// The output from a round of tracing.\n#[derive(Debug, Clone)]\npub struct Round<'a> {\n    /// The state of all `ProbeStatus` that were sent in the round.\n    pub probes: &'a [ProbeStatus],\n    /// The largest time-to-live (ttl) for which we received a reply in the round.\n    pub largest_ttl: TimeToLive,\n    /// Indicates what triggered the completion of the tracing round.\n    pub reason: CompletionReason,\n}\n\nimpl<'a> Round<'a> {\n    #[must_use]\n    pub const fn new(\n        probes: &'a [ProbeStatus],\n        largest_ttl: TimeToLive,\n        reason: CompletionReason,\n    ) -> Self {\n        Self {\n            probes,\n            largest_ttl,\n            reason,\n        }\n    }\n}\n\n/// Indicates what triggered the completion of the tracing round.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum CompletionReason {\n    /// The round ended because the target was found.\n    TargetFound,\n    /// The round ended because the time exceeded the configured maximum round time.\n    RoundTimeLimitExceeded,\n}\n\n/// Trace a path to a target.\n#[derive(Debug, Clone)]\npub struct Strategy<F> {\n    config: StrategyConfig,\n    publish: F,\n}\n\nimpl<F: Fn(&Round<'_>)> Strategy<F> {\n    #[instrument(skip_all, level = \"trace\")]\n    pub fn new(config: &StrategyConfig, publish: F) -> Self {\n        tracing::debug!(?config);\n        Self {\n            config: *config,\n            publish,\n        }\n    }\n\n    /// Run a continuous trace and publish results.\n    #[instrument(skip(self, network), level = \"trace\")]\n    pub fn run<N: Network>(self, mut network: N) -> Result<()> {\n        let mut state = TracerState::new(self.config);\n        while !state.finished(self.config.max_rounds) {\n            self.send_request(&mut network, &mut state)?;\n            self.recv_response(&mut network, &mut state)?;\n            self.update_round(&mut state);\n        }\n        Ok(())\n    }\n\n    /// Send the next probe if required.\n    ///\n    /// Send a `ProbeStatus` for the next time-to-live (ttl) if all the following are true:\n    ///\n    /// 1 - the target host has not been found\n    /// 2 - the next ttl is not greater than the maximum allowed ttl\n    /// 3 - if the target ttl of the target is known:\n    ///       - the next ttl is not greater than the ttl of the target host observed from the prior\n    ///         round\n    ///     otherwise:\n    ///       - the number of unknown-in-flight probes is lower than the maximum allowed\n    fn send_request<N: Network>(&self, network: &mut N, st: &mut TracerState) -> Result<()> {\n        let can_send_ttl = if let Some(target_ttl) = st.target_ttl() {\n            st.ttl() <= target_ttl\n        } else {\n            st.ttl() - st.max_received_ttl().unwrap_or_default()\n                < TimeToLive(self.config.max_inflight.0)\n        };\n        if !st.target_found() && st.ttl() <= self.config.max_ttl && can_send_ttl {\n            let sent = SystemTime::now();\n            match self.config.protocol {\n                Protocol::Icmp | Protocol::Udp => {\n                    let probe = st.next_probe(sent);\n                    Self::do_send(network, st, probe)?;\n                }\n                Protocol::Tcp => {\n                    let mut probe = if st.round_has_capacity() {\n                        st.next_probe(sent)\n                    } else {\n                        return Err(Error::InsufficientCapacity);\n                    };\n                    while let Err(err) = Self::do_send(network, st, probe) {\n                        match err {\n                            Error::AddressInUse(_) => {\n                                if st.round_has_capacity() {\n                                    probe = st.reissue_probe(SystemTime::now());\n                                } else {\n                                    return Err(Error::InsufficientCapacity);\n                                }\n                            }\n                            other => return Err(other),\n                        }\n                    }\n                }\n            }\n        }\n        Ok(())\n    }\n\n    /// Send the probe and handle errors.\n    ///\n    /// Some errors are transient and should not be considered fatal.  In these cases we mark the\n    /// probe as failed and continue.\n    #[instrument(skip(network, st), level = \"trace\")]\n    fn do_send<N: Network>(network: &mut N, st: &mut TracerState, probe: Probe) -> Result<()> {\n        match network.send_probe(probe) {\n            Ok(()) => Ok(()),\n            Err(Error::ProbeFailed(_)) => {\n                st.fail_probe();\n                Ok(())\n            }\n            Err(err) => Err(err),\n        }\n    }\n\n    /// Read and process the next incoming `ICMP` packet.\n    ///\n    /// We allow multiple probes to be in-flight at any time, and we cannot guarantee that responses\n    /// will be received in-order.  We therefore maintain a buffer which holds details of each\n    /// `ProbeStatus` which is indexed by the offset of the sequence number from the sequence number\n    /// at the beginning of the round.  The sequence number is set in the outgoing `ICMP`\n    /// `EchoRequest` (or `UDP` / `TCP`) packet and returned in both the `TimeExceeded` and\n    /// `EchoReply` responses.\n    ///\n    /// Each incoming `ICMP` packet contains the original `ICMP` `EchoRequest` packet from which we\n    /// can read the `identifier` that we set which we can now validate to ensure we only\n    /// process responses which correspond to packets sent from this process.  For The `UDP` and\n    /// `TCP` protocols, only packets destined for our src port will be delivered to us by the\n    /// OS and so no other `identifier` is needed, and so we allow the special case value of 0.\n    ///\n    /// When we process an `EchoReply` from the target host we extract the time-to-live from the\n    /// corresponding original `EchoRequest`.  Note that this may not be the greatest\n    /// time-to-live that was sent in the round as the algorithm will send `EchoRequest` with\n    /// larger time-to-live values before the `EchoReply` is received.\n    fn recv_response<N: Network>(&self, network: &mut N, st: &mut TracerState) -> Result<()> {\n        let next = network.recv_probe()?;\n        if let Some(resp) = next {\n            if self.validate(resp.data()) {\n                let resp = StrategyResponse::from((resp, &self.config));\n                if self.check_trace_id(resp.trace_id) && st.in_round(resp.sequence) {\n                    st.complete_probe(resp);\n                }\n            }\n        }\n        Ok(())\n    }\n\n    /// Check if the round is complete and publish the results.\n    ///\n    /// A round is considered to be complete when:\n    ///\n    /// 1 - the round has exceeded the minimum round duration AND\n    /// 2 - the duration since the last packet was received exceeds the grace period AND\n    /// 3 - either:\n    ///     A - the target has been found OR\n    ///     B - the target has not been found and the round has exceeded the maximum round duration\n    fn update_round(&self, st: &mut TracerState) {\n        let now = SystemTime::now();\n        let round_duration = now.duration_since(st.round_start()).unwrap_or_default();\n        let round_min = round_duration > self.config.min_round_duration;\n        let grace_exceeded = exceeds(st.received_time(), now, self.config.grace_duration);\n        let round_max = round_duration > self.config.max_round_duration;\n        let target_found = st.target_found();\n        if round_min && grace_exceeded && target_found || round_max {\n            self.publish_trace(st);\n            st.advance_round(self.config.first_ttl);\n        }\n    }\n\n    /// Publish details of all `ProbeStatus` in the completed round.\n    ///\n    /// If the round completed without receiving an `EchoReply` from the target host then we also\n    /// publish the next `ProbeStatus` which is assumed to represent the TTL of the target host.\n    #[instrument(skip(self, state), level = \"trace\")]\n    fn publish_trace(&self, state: &TracerState) {\n        let max_received_ttl = if let Some(target_ttl) = state.target_ttl() {\n            target_ttl\n        } else {\n            state\n                .max_received_ttl()\n                .map_or(TimeToLive(0), |max_received_ttl| {\n                    let max_sent_ttl = state.ttl() - TimeToLive(1);\n                    max_sent_ttl.min(max_received_ttl + TimeToLive(1))\n                })\n        };\n        let probes = state.probes();\n        let largest_ttl = max_received_ttl;\n        let reason = if state.target_found() {\n            CompletionReason::TargetFound\n        } else {\n            CompletionReason::RoundTimeLimitExceeded\n        };\n        (self.publish)(&Round::new(probes, largest_ttl, reason));\n    }\n\n    /// Check if the `TraceId` matches the expected value for this tracer.\n    ///\n    /// A special value of `0` is accepted for `udp` and `tcp` which do not have an identifier.\n    #[instrument(skip(self), level = \"trace\")]\n    fn check_trace_id(&self, trace_id: TraceId) -> bool {\n        self.config.trace_identifier == trace_id || trace_id == TraceId(0)\n    }\n\n    /// Validate the probe response data.\n    ///\n    /// Carries out specific check for UDP/TCP probe responses.  This is\n    /// required as the network layer may receive incoming ICMP\n    /// `DestinationUnreachable` (and other types) packets with a UDP/TCP\n    /// original datagram which does not correspond to a probe sent by the\n    /// tracer and must therefore be ignored.\n    ///\n    /// For UDP and TCP probe responses, check that the src/dest ports and\n    /// dest address match the expected values.\n    ///\n    /// For ICMP probe responses no additional checks are required.\n    #[instrument(skip(self), level = \"trace\")]\n    fn validate(&self, resp: &ResponseData) -> bool {\n        const fn validate_ports(\n            port_direction: PortDirection,\n            src_port: u16,\n            dest_port: u16,\n        ) -> bool {\n            match port_direction {\n                PortDirection::FixedSrc(src) if src.0 == src_port => true,\n                PortDirection::FixedDest(dest) if dest.0 == dest_port => true,\n                PortDirection::FixedBoth(src, dest) if src.0 == src_port && dest.0 == dest_port => {\n                    true\n                }\n                _ => false,\n            }\n        }\n        match resp.proto_resp {\n            ProtocolResponse::Icmp(_) => true,\n            ProtocolResponse::Udp(UdpProtocolResponse {\n                dest_addr,\n                src_port,\n                dest_port,\n                has_magic,\n                ..\n            }) => {\n                let check_ports = validate_ports(self.config.port_direction, src_port, dest_port);\n                let check_dest_addr = self.config.target_addr == dest_addr;\n                let check_magic = match (self.config.multipath_strategy, self.config.target_addr) {\n                    (MultipathStrategy::Dublin, IpAddr::V6(_)) => has_magic,\n                    _ => true,\n                };\n                check_dest_addr && check_ports && check_magic\n            }\n            ProtocolResponse::Tcp(TcpProtocolResponse {\n                dest_addr,\n                src_port,\n                dest_port,\n                ..\n            }) => {\n                let check_ports = validate_ports(self.config.port_direction, src_port, dest_port);\n                let check_dest_addr = self.config.target_addr == dest_addr;\n                check_dest_addr && check_ports\n            }\n        }\n    }\n}\n\n/// Derived response based on strategy config.\n#[derive(Debug)]\nstruct StrategyResponse {\n    icmp_packet_type: IcmpPacketType,\n    trace_id: TraceId,\n    sequence: Sequence,\n    tos: Option<TypeOfService>,\n    expected_udp_checksum: Option<Checksum>,\n    actual_udp_checksum: Option<Checksum>,\n    received: SystemTime,\n    addr: IpAddr,\n    is_target: bool,\n    exts: Option<Extensions>,\n}\n\nimpl From<(Response, &StrategyConfig)> for StrategyResponse {\n    fn from((resp, config): (Response, &StrategyConfig)) -> Self {\n        match resp {\n            Response::TimeExceeded(data, code, exts) => {\n                let proto_resp = ProtocolStrategyResponse::from((data.proto_resp, config));\n                let is_target = data.addr == config.target_addr;\n                Self {\n                    icmp_packet_type: IcmpPacketType::TimeExceeded(code),\n                    trace_id: proto_resp.trace_id,\n                    sequence: proto_resp.sequence,\n                    tos: proto_resp.tos,\n                    expected_udp_checksum: proto_resp.expected_udp_checksum,\n                    actual_udp_checksum: proto_resp.actual_udp_checksum,\n                    received: data.recv,\n                    addr: data.addr,\n                    is_target,\n                    exts,\n                }\n            }\n            Response::DestinationUnreachable(data, code, exts) => {\n                let proto_resp = ProtocolStrategyResponse::from((data.proto_resp, config));\n                let is_target = data.addr == config.target_addr;\n                Self {\n                    icmp_packet_type: IcmpPacketType::Unreachable(code),\n                    trace_id: proto_resp.trace_id,\n                    sequence: proto_resp.sequence,\n                    tos: proto_resp.tos,\n                    expected_udp_checksum: proto_resp.expected_udp_checksum,\n                    actual_udp_checksum: proto_resp.actual_udp_checksum,\n                    received: data.recv,\n                    addr: data.addr,\n                    is_target,\n                    exts,\n                }\n            }\n            Response::EchoReply(data, code) => {\n                let proto_resp = ProtocolStrategyResponse::from((data.proto_resp, config));\n                Self {\n                    icmp_packet_type: IcmpPacketType::EchoReply(code),\n                    trace_id: proto_resp.trace_id,\n                    sequence: proto_resp.sequence,\n                    tos: proto_resp.tos,\n                    expected_udp_checksum: proto_resp.expected_udp_checksum,\n                    actual_udp_checksum: proto_resp.actual_udp_checksum,\n                    received: data.recv,\n                    addr: data.addr,\n                    is_target: true,\n                    exts: None,\n                }\n            }\n            Response::TcpReply(data) | Response::TcpRefused(data) => {\n                let proto_resp = ProtocolStrategyResponse::from((data.proto_resp, config));\n                Self {\n                    icmp_packet_type: IcmpPacketType::NotApplicable,\n                    trace_id: proto_resp.trace_id,\n                    sequence: proto_resp.sequence,\n                    tos: proto_resp.tos,\n                    expected_udp_checksum: proto_resp.expected_udp_checksum,\n                    actual_udp_checksum: proto_resp.actual_udp_checksum,\n                    received: data.recv,\n                    addr: data.addr,\n                    is_target: true,\n                    exts: None,\n                }\n            }\n        }\n    }\n}\n\n/// Derived response sequence based on strategy config.\n#[derive(Debug)]\nstruct ProtocolStrategyResponse {\n    trace_id: TraceId,\n    sequence: Sequence,\n    tos: Option<TypeOfService>,\n    expected_udp_checksum: Option<Checksum>,\n    actual_udp_checksum: Option<Checksum>,\n}\n\nimpl From<(ProtocolResponse, &StrategyConfig)> for ProtocolStrategyResponse {\n    fn from((proto_resp, config): (ProtocolResponse, &StrategyConfig)) -> Self {\n        match proto_resp {\n            ProtocolResponse::Icmp(IcmpProtocolResponse {\n                identifier,\n                sequence,\n                tos,\n            }) => Self {\n                trace_id: TraceId(identifier),\n                sequence: Sequence(sequence),\n                tos,\n                expected_udp_checksum: None,\n                actual_udp_checksum: None,\n            },\n            ProtocolResponse::Udp(UdpProtocolResponse {\n                identifier,\n                src_port,\n                dest_port,\n                tos,\n                expected_udp_checksum,\n                actual_udp_checksum,\n                payload_len,\n                ..\n            }) => {\n                let sequence = match (\n                    config.multipath_strategy,\n                    config.port_direction,\n                    config.target_addr,\n                ) {\n                    (MultipathStrategy::Classic, PortDirection::FixedDest(_), _) => src_port,\n                    (MultipathStrategy::Classic, _, _) => dest_port,\n                    (MultipathStrategy::Paris, _, _) => actual_udp_checksum,\n                    (MultipathStrategy::Dublin, _, IpAddr::V4(_)) => identifier,\n                    (MultipathStrategy::Dublin, _, IpAddr::V6(_)) => {\n                        config.initial_sequence.0 + payload_len\n                    }\n                };\n\n                let (expected_udp_checksum, actual_udp_checksum) =\n                    match (config.multipath_strategy, config.target_addr) {\n                        (MultipathStrategy::Dublin, IpAddr::V4(_)) => (\n                            Some(Checksum(expected_udp_checksum)),\n                            Some(Checksum(actual_udp_checksum)),\n                        ),\n                        _ => (None, None),\n                    };\n\n                Self {\n                    trace_id: TraceId(0),\n                    sequence: Sequence(sequence),\n                    tos,\n                    expected_udp_checksum,\n                    actual_udp_checksum,\n                }\n            }\n            ProtocolResponse::Tcp(TcpProtocolResponse {\n                src_port,\n                dest_port,\n                tos,\n                ..\n            }) => {\n                let sequence = match config.port_direction {\n                    PortDirection::FixedSrc(_) => dest_port,\n                    _ => src_port,\n                };\n                Self {\n                    trace_id: TraceId(0),\n                    sequence: Sequence(sequence),\n                    tos,\n                    expected_udp_checksum: None,\n                    actual_udp_checksum: None,\n                }\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::net::MockNetwork;\n    use crate::probe::IcmpPacketCode;\n    use crate::{MaxRounds, Port};\n    use std::net::Ipv4Addr;\n    use std::num::NonZeroUsize;\n\n    #[test]\n    fn test_time_exceeded_target_response() {\n        let config = StrategyConfig {\n            target_addr: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)),\n            ..Default::default()\n        };\n        let now = SystemTime::now();\n        let resp_data = Response::TimeExceeded(response_data(now), IcmpPacketCode(1), None);\n        let resp = StrategyResponse::from((resp_data, &config));\n        assert_eq!(\n            resp.icmp_packet_type,\n            IcmpPacketType::TimeExceeded(IcmpPacketCode(1))\n        );\n        assert_eq!(resp.trace_id, TraceId(0));\n        assert_eq!(resp.sequence, Sequence(33434));\n        assert_eq!(resp.received, now);\n        assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));\n        assert_eq!(resp.is_target, true);\n        assert!(resp.exts.is_none());\n    }\n\n    #[test]\n    fn test_time_exceeded_not_target_response() {\n        let config = StrategyConfig {\n            target_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),\n            ..Default::default()\n        };\n        let now = SystemTime::now();\n        let resp_data = Response::TimeExceeded(response_data(now), IcmpPacketCode(1), None);\n        let resp = StrategyResponse::from((resp_data, &config));\n        assert_eq!(\n            resp.icmp_packet_type,\n            IcmpPacketType::TimeExceeded(IcmpPacketCode(1))\n        );\n        assert_eq!(resp.trace_id, TraceId(0));\n        assert_eq!(resp.sequence, Sequence(33434));\n        assert_eq!(resp.received, now);\n        assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));\n        assert_eq!(resp.is_target, false);\n        assert!(resp.exts.is_none());\n    }\n\n    #[test]\n    fn test_destination_unreachable_target_response() {\n        let config = StrategyConfig {\n            target_addr: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)),\n            ..Default::default()\n        };\n        let now = SystemTime::now();\n        let resp_data =\n            Response::DestinationUnreachable(response_data(now), IcmpPacketCode(10), None);\n        let resp = StrategyResponse::from((resp_data, &config));\n        assert_eq!(\n            resp.icmp_packet_type,\n            IcmpPacketType::Unreachable(IcmpPacketCode(10))\n        );\n        assert_eq!(resp.trace_id, TraceId(0));\n        assert_eq!(resp.sequence, Sequence(33434));\n        assert_eq!(resp.received, now);\n        assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));\n        assert_eq!(resp.is_target, true);\n        assert!(resp.exts.is_none());\n    }\n\n    #[test]\n    fn test_destination_unreachable_not_target_response() {\n        let config = StrategyConfig::default();\n        let now = SystemTime::now();\n        let resp_data =\n            Response::DestinationUnreachable(response_data(now), IcmpPacketCode(10), None);\n        let resp = StrategyResponse::from((resp_data, &config));\n        assert_eq!(\n            resp.icmp_packet_type,\n            IcmpPacketType::Unreachable(IcmpPacketCode(10))\n        );\n        assert_eq!(resp.trace_id, TraceId(0));\n        assert_eq!(resp.sequence, Sequence(33434));\n        assert_eq!(resp.received, now);\n        assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));\n        assert_eq!(resp.is_target, false);\n        assert!(resp.exts.is_none());\n    }\n\n    #[test]\n    fn test_echo_reply_response() {\n        let config = StrategyConfig::default();\n        let now = SystemTime::now();\n        let resp_data = Response::EchoReply(response_data(now), IcmpPacketCode(99));\n        let resp = StrategyResponse::from((resp_data, &config));\n        assert_eq!(\n            resp.icmp_packet_type,\n            IcmpPacketType::EchoReply(IcmpPacketCode(99))\n        );\n        assert_eq!(resp.trace_id, TraceId(0));\n        assert_eq!(resp.sequence, Sequence(33434));\n        assert_eq!(resp.received, now);\n        assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));\n        assert_eq!(resp.is_target, true);\n        assert!(resp.exts.is_none());\n    }\n\n    #[test]\n    fn test_tcp_reply_response() {\n        let config = StrategyConfig::default();\n        let now = SystemTime::now();\n        let resp_data = Response::TcpReply(response_data(now));\n        let resp = StrategyResponse::from((resp_data, &config));\n        assert_eq!(resp.icmp_packet_type, IcmpPacketType::NotApplicable);\n        assert_eq!(resp.trace_id, TraceId(0));\n        assert_eq!(resp.sequence, Sequence(33434));\n        assert_eq!(resp.received, now);\n        assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));\n        assert_eq!(resp.is_target, true);\n        assert!(resp.exts.is_none());\n    }\n\n    #[test]\n    fn test_tcp_refused_response() {\n        let config = StrategyConfig::default();\n        let now = SystemTime::now();\n        let resp_data = Response::TcpRefused(response_data(now));\n        let resp = StrategyResponse::from((resp_data, &config));\n        assert_eq!(resp.icmp_packet_type, IcmpPacketType::NotApplicable);\n        assert_eq!(resp.trace_id, TraceId(0));\n        assert_eq!(resp.sequence, Sequence(33434));\n        assert_eq!(resp.received, now);\n        assert_eq!(resp.addr, IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));\n        assert_eq!(resp.is_target, true);\n        assert!(resp.exts.is_none());\n    }\n\n    #[test]\n    fn test_icmp_response() {\n        let config = StrategyConfig::default();\n        let proto_resp = ProtocolResponse::Icmp(IcmpProtocolResponse {\n            identifier: 1234,\n            sequence: 33434,\n            tos: Some(TypeOfService(0)),\n        });\n        let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config));\n        assert_eq!(strategy_resp.trace_id, TraceId(1234));\n        assert_eq!(strategy_resp.sequence, Sequence(33434));\n    }\n\n    #[test]\n    fn test_udp_classic_fixed_src_response() {\n        let config = StrategyConfig {\n            protocol: Protocol::Udp,\n            port_direction: PortDirection::FixedSrc(Port(5000)),\n            ..Default::default()\n        };\n        let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse {\n            identifier: 0,\n            dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),\n            src_port: 5000,\n            dest_port: 33434,\n            tos: Some(TypeOfService(0)),\n            expected_udp_checksum: 0,\n            actual_udp_checksum: 0,\n            payload_len: 0,\n            has_magic: false,\n        });\n        let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config));\n        assert_eq!(strategy_resp.trace_id, TraceId(0));\n        assert_eq!(strategy_resp.sequence, Sequence(33434));\n    }\n\n    #[test]\n    fn test_udp_classic_fixed_dest_response() {\n        let config = StrategyConfig {\n            protocol: Protocol::Udp,\n            port_direction: PortDirection::FixedDest(Port(5000)),\n            ..Default::default()\n        };\n        let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse {\n            identifier: 0,\n            dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),\n            src_port: 33434,\n            dest_port: 5000,\n            tos: Some(TypeOfService(0)),\n            expected_udp_checksum: 0,\n            actual_udp_checksum: 0,\n            payload_len: 0,\n            has_magic: false,\n        });\n        let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config));\n        assert_eq!(strategy_resp.trace_id, TraceId(0));\n        assert_eq!(strategy_resp.sequence, Sequence(33434));\n    }\n\n    #[test]\n    fn test_udp_paris_response() {\n        let config = StrategyConfig {\n            protocol: Protocol::Udp,\n            multipath_strategy: MultipathStrategy::Paris,\n            port_direction: PortDirection::FixedSrc(Port(5000)),\n            ..Default::default()\n        };\n        let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse {\n            identifier: 33434,\n            dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),\n            src_port: 5000,\n            dest_port: 35000,\n            tos: Some(TypeOfService(0)),\n            expected_udp_checksum: 33434,\n            actual_udp_checksum: 33434,\n            payload_len: 0,\n            has_magic: false,\n        });\n        let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config));\n        assert_eq!(strategy_resp.trace_id, TraceId(0));\n        assert_eq!(strategy_resp.sequence, Sequence(33434));\n    }\n\n    #[test]\n    fn test_udp_dublin_ipv4_response() {\n        let config = StrategyConfig {\n            protocol: Protocol::Udp,\n            multipath_strategy: MultipathStrategy::Dublin,\n            port_direction: PortDirection::FixedSrc(Port(5000)),\n            ..Default::default()\n        };\n        let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse {\n            identifier: 33434,\n            dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),\n            src_port: 5000,\n            dest_port: 35000,\n            tos: Some(TypeOfService(0)),\n            expected_udp_checksum: 0,\n            actual_udp_checksum: 0,\n            payload_len: 0,\n            has_magic: false,\n        });\n        let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config));\n        assert_eq!(strategy_resp.trace_id, TraceId(0));\n        assert_eq!(strategy_resp.sequence, Sequence(33434));\n    }\n\n    #[test]\n    fn test_udp_dublin_ipv6_response() {\n        let config = StrategyConfig {\n            protocol: Protocol::Udp,\n            target_addr: IpAddr::V6(\"::1\".parse().unwrap()),\n            multipath_strategy: MultipathStrategy::Dublin,\n            port_direction: PortDirection::FixedSrc(Port(5000)),\n            ..Default::default()\n        };\n        let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse {\n            identifier: 0,\n            dest_addr: IpAddr::V6(\"::1\".parse().unwrap()),\n            src_port: 5000,\n            dest_port: 35000,\n            tos: Some(TypeOfService(0)),\n            expected_udp_checksum: 0,\n            actual_udp_checksum: 0,\n            payload_len: 55,\n            has_magic: true,\n        });\n        let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config));\n        assert_eq!(strategy_resp.trace_id, TraceId(0));\n        assert_eq!(strategy_resp.sequence, Sequence(33489));\n    }\n\n    #[test]\n    fn test_tcp_fixed_dest_response() {\n        let config = StrategyConfig {\n            protocol: Protocol::Tcp,\n            port_direction: PortDirection::FixedDest(Port(80)),\n            ..Default::default()\n        };\n        let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse {\n            identifier: 0,\n            dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),\n            src_port: 33434,\n            dest_port: 80,\n            tos: Some(TypeOfService(0)),\n            expected_udp_checksum: 0,\n            actual_udp_checksum: 0,\n            payload_len: 0,\n            has_magic: false,\n        });\n        let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config));\n        assert_eq!(strategy_resp.trace_id, TraceId(0));\n        assert_eq!(strategy_resp.sequence, Sequence(33434));\n    }\n\n    #[test]\n    fn test_tcp_fixed_src_response() {\n        let config = StrategyConfig {\n            protocol: Protocol::Tcp,\n            port_direction: PortDirection::FixedSrc(Port(5000)),\n            ..Default::default()\n        };\n        let proto_resp = ProtocolResponse::Udp(UdpProtocolResponse {\n            identifier: 0,\n            dest_addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),\n            src_port: 5000,\n            dest_port: 33434,\n            tos: Some(TypeOfService(0)),\n            expected_udp_checksum: 0,\n            actual_udp_checksum: 0,\n            payload_len: 0,\n            has_magic: false,\n        });\n        let strategy_resp = ProtocolStrategyResponse::from((proto_resp, &config));\n        assert_eq!(strategy_resp.trace_id, TraceId(0));\n        assert_eq!(strategy_resp.sequence, Sequence(33434));\n    }\n\n    // The network can return both `DestinationUnreachable` and `TcpRefused`\n    // for the same sequence number.  This can occur for the target hop for\n    // TCP protocol as the network layer check for ICMP responses such as\n    // `DestinationUnreachable` and also synthesizes a `TcpRefused` response.\n    //\n    // This test simulates sending 1 TCP probe (seq=33434) and receiving two\n    // responses for that probe, a `DestinationUnreachable` followed by a\n    // `TcpRefused`.\n    #[test]\n    fn test_tcp_dest_unreachable_and_refused() -> anyhow::Result<()> {\n        let sequence = 33434;\n        let target_addr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));\n\n        let mut network = MockNetwork::new();\n        let mut seq = mockall::Sequence::new();\n        network.expect_send_probe().times(1).returning(|_| Ok(()));\n        network\n            .expect_recv_probe()\n            .times(1)\n            .in_sequence(&mut seq)\n            .returning(move || {\n                Ok(Some(Response::DestinationUnreachable(\n                    ResponseData::new(\n                        SystemTime::now(),\n                        target_addr,\n                        ProtocolResponse::Tcp(TcpProtocolResponse::new(\n                            target_addr,\n                            sequence,\n                            80,\n                            None,\n                        )),\n                    ),\n                    IcmpPacketCode(1),\n                    None,\n                )))\n            });\n        network\n            .expect_recv_probe()\n            .times(1)\n            .in_sequence(&mut seq)\n            .returning(move || {\n                Ok(Some(Response::TcpRefused(ResponseData::new(\n                    SystemTime::now(),\n                    target_addr,\n                    ProtocolResponse::Tcp(TcpProtocolResponse::new(\n                        target_addr,\n                        sequence,\n                        80,\n                        None,\n                    )),\n                ))))\n            });\n\n        let config = StrategyConfig {\n            target_addr,\n            max_rounds: Some(MaxRounds(NonZeroUsize::MIN)),\n            initial_sequence: Sequence(sequence),\n            port_direction: PortDirection::FixedDest(Port(80)),\n            protocol: Protocol::Tcp,\n            ..Default::default()\n        };\n        let tracer = Strategy::new(&config, |_| {});\n        let mut state = TracerState::new(config);\n        tracer.send_request(&mut network, &mut state)?;\n        tracer.recv_response(&mut network, &mut state)?;\n        tracer.recv_response(&mut network, &mut state)?;\n        Ok(())\n    }\n\n    const fn response_data(now: SystemTime) -> ResponseData {\n        ResponseData::new(\n            now,\n            IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)),\n            ProtocolResponse::Icmp(IcmpProtocolResponse {\n                identifier: 0,\n                sequence: 33434,\n                tos: Some(TypeOfService(0)),\n            }),\n        )\n    }\n}\n\n/// Mutable state needed for the tracing algorithm.\n///\n/// This is contained within a submodule to ensure that mutations are only performed via methods on\n/// the `TracerState` struct.\nmod state {\n    use crate::constants::MAX_SEQUENCE_PER_ROUND;\n    use crate::probe::{Probe, ProbeStatus};\n    use crate::strategy::{StrategyConfig, StrategyResponse};\n    use crate::types::{MaxRounds, Port, RoundId, Sequence, TimeToLive, TraceId};\n    use crate::{Flags, MultipathStrategy, PortDirection, Protocol};\n    use std::array::from_fn;\n    use std::net::IpAddr;\n    use std::time::SystemTime;\n    use tracing::instrument;\n\n    /// The maximum number of `ProbeStatus` entries in the buffer.\n    ///\n    /// This is larger than maximum number of time-to-live (TTL) we can support to allow for skipped\n    /// sequences.\n    const BUFFER_SIZE: u16 = MAX_SEQUENCE_PER_ROUND;\n\n    /// The maximum sequence number.\n    ///\n    /// The sequence number is only ever wrapped between rounds, and so we need to ensure that there\n    /// are enough sequence numbers for a complete round.\n    ///\n    /// A sequence number can be skipped if, for example, the port for that sequence number cannot\n    /// be bound as it is already in use.\n    ///\n    /// To ensure each `ProbeStatus` is in the correct place in the buffer (i.e. the index into the\n    /// buffer is always `Probe.sequence - round_sequence`), when we skip a sequence we leave\n    /// the skipped `ProbeStatus` in-place and use the next slot for the next sequence.\n    ///\n    /// We cap the number of sequences that can potentially be skipped in a round to ensure that\n    /// sequence number does not even need to wrap around during a round.\n    ///\n    /// We only ever send `ttl` in the range 1..255, and so we may use all buffer capacity, except\n    /// the minimum needed to send up to a max `ttl` of 255 (a `ttl` of 0 is never sent).\n    const MAX_SEQUENCE: Sequence = Sequence(u16::MAX - BUFFER_SIZE);\n\n    /// Mutable state needed for the tracing algorithm.\n    #[derive(Debug)]\n    pub struct TracerState {\n        /// Tracer configuration.\n        config: StrategyConfig,\n        /// The state of all `ProbeStatus` requests and responses.\n        buffer: [ProbeStatus; BUFFER_SIZE as usize],\n        /// An increasing sequence number for every `EchoRequest`.\n        sequence: Sequence,\n        /// The starting sequence number of the current round.\n        round_sequence: Sequence,\n        /// The time-to-live for the _next_ `EchoRequest` packet to be sent.\n        ttl: TimeToLive,\n        /// The current round.\n        round: RoundId,\n        /// The timestamp of when the current round started.\n        round_start: SystemTime,\n        /// Did we receive an `EchoReply` from the target host in this round?\n        target_found: bool,\n        /// The maximum time-to-live echo response packet we have received.\n        max_received_ttl: Option<TimeToLive>,\n        /// The observed time-to-live of the `EchoReply` from the target host.\n        ///\n        /// Note that this is _not_ reset each round and that it can also _change_ over time,\n        /// including going _down_ as responses can be received out-of-order.\n        target_ttl: Option<TimeToLive>,\n        /// The timestamp of the echo response packet.\n        received_time: Option<SystemTime>,\n    }\n\n    impl TracerState {\n        pub fn new(config: StrategyConfig) -> Self {\n            Self {\n                config,\n                buffer: from_fn(|_| ProbeStatus::default()),\n                sequence: config.initial_sequence,\n                round_sequence: config.initial_sequence,\n                ttl: config.first_ttl,\n                round: RoundId(0),\n                round_start: SystemTime::now(),\n                target_found: false,\n                max_received_ttl: None,\n                target_ttl: None,\n                received_time: None,\n            }\n        }\n\n        /// Get a slice of `ProbeStatus` for the current round.\n        pub fn probes(&self) -> &[ProbeStatus] {\n            let round_size = self.sequence - self.round_sequence;\n            &self.buffer[..round_size.0 as usize]\n        }\n\n        /// Get the `ProbeStatus` for `sequence`\n        pub fn probe_at(&self, sequence: Sequence) -> ProbeStatus {\n            self.buffer[usize::from(sequence - self.round_sequence)].clone()\n        }\n\n        pub const fn ttl(&self) -> TimeToLive {\n            self.ttl\n        }\n\n        pub const fn round_start(&self) -> SystemTime {\n            self.round_start\n        }\n\n        pub const fn target_found(&self) -> bool {\n            self.target_found\n        }\n\n        pub const fn max_received_ttl(&self) -> Option<TimeToLive> {\n            self.max_received_ttl\n        }\n\n        pub const fn target_ttl(&self) -> Option<TimeToLive> {\n            self.target_ttl\n        }\n\n        pub const fn received_time(&self) -> Option<SystemTime> {\n            self.received_time\n        }\n\n        /// Is `sequence` in the current round?\n        pub fn in_round(&self, sequence: Sequence) -> bool {\n            sequence >= self.round_sequence && sequence.0 - self.round_sequence.0 < BUFFER_SIZE\n        }\n\n        /// Do we have capacity in the current round for another sequence?\n        pub fn round_has_capacity(&self) -> bool {\n            let round_size = self.sequence - self.round_sequence;\n            round_size.0 < BUFFER_SIZE\n        }\n\n        /// Are all rounds complete?\n        pub const fn finished(&self, max_rounds: Option<MaxRounds>) -> bool {\n            match max_rounds {\n                None => false,\n                Some(max_rounds) => self.round.0 > max_rounds.0.get() - 1,\n            }\n        }\n\n        /// Create and return the next `Probe` at the current `sequence` and `ttl`.\n        ///\n        /// We post-increment `ttl` here and so in practice we only allow `ttl` values in the range\n        /// `1..254` to allow us to use a `u8`.\n        #[instrument(skip(self), level = \"trace\")]\n        pub fn next_probe(&mut self, sent: SystemTime) -> Probe {\n            let (src_port, dest_port, identifier, flags) = self.probe_data();\n            let probe = Probe::new(\n                self.sequence,\n                identifier,\n                src_port,\n                dest_port,\n                self.ttl,\n                self.round,\n                sent,\n                flags,\n            );\n            let probe_index = usize::from(self.sequence - self.round_sequence);\n            self.buffer[probe_index] = ProbeStatus::Awaited(probe.clone());\n            debug_assert!(self.ttl < TimeToLive(u8::MAX));\n            self.ttl += TimeToLive(1);\n            debug_assert!(self.sequence < Sequence(u16::MAX));\n            self.sequence += Sequence(1);\n            probe\n        }\n\n        /// Re-issue the `Probe` with the next sequence number.\n        ///\n        /// This will mark the `ProbeStatus` at the previous `sequence` as skipped and re-create it\n        /// with the previous `ttl` and the current `sequence`.\n        ///\n        /// For example, if the sequence is `4` and the `ttl` is `5` prior to calling this method\n        /// then afterward:\n        /// - The `ProbeStatus` at sequence `3` will be set to `Skipped` state\n        /// - A new `ProbeStatus` will be created at sequence `4` with a `ttl` of `5`\n        #[instrument(skip(self), level = \"trace\")]\n        pub fn reissue_probe(&mut self, sent: SystemTime) -> Probe {\n            let probe_index = usize::from(self.sequence - self.round_sequence);\n            self.buffer[probe_index - 1] = ProbeStatus::Skipped;\n            let (src_port, dest_port, identifier, flags) = self.probe_data();\n            let probe = Probe::new(\n                self.sequence,\n                identifier,\n                src_port,\n                dest_port,\n                self.ttl - TimeToLive(1),\n                self.round,\n                sent,\n                flags,\n            );\n            self.buffer[probe_index] = ProbeStatus::Awaited(probe.clone());\n            debug_assert!(self.sequence < Sequence(u16::MAX));\n            self.sequence += Sequence(1);\n            probe\n        }\n\n        /// Mark the `ProbeStatus` at the current `sequence` as failed.\n        #[instrument(skip(self), level = \"trace\")]\n        pub fn fail_probe(&mut self) {\n            let probe_index = usize::from(self.sequence - self.round_sequence);\n            let probe = self.buffer[probe_index - 1].clone();\n            match probe {\n                ProbeStatus::Awaited(awaited) => {\n                    self.buffer[probe_index - 1] = ProbeStatus::Failed(awaited.failed());\n                }\n                _ => unreachable!(\"expected ProbeStatus::Awaited\"),\n            }\n        }\n\n        /// Determine the `src_port`, `dest_port` and `identifier` for the current probe.\n        ///\n        /// This will differ depending on the `TracerProtocol`, `MultipathStrategy` &\n        /// `PortDirection`.\n        fn probe_data(&self) -> (Port, Port, TraceId, Flags) {\n            match self.config.protocol {\n                Protocol::Icmp => self.probe_icmp_data(),\n                Protocol::Udp => self.probe_udp_data(),\n                Protocol::Tcp => self.probe_tcp_data(),\n            }\n        }\n\n        /// Determine the `src_port`, `dest_port` and `identifier` for the current ICMP probe.\n        const fn probe_icmp_data(&self) -> (Port, Port, TraceId, Flags) {\n            (\n                Port(0),\n                Port(0),\n                self.config.trace_identifier,\n                Flags::empty(),\n            )\n        }\n\n        /// Determine the `src_port`, `dest_port` and `identifier` for the current UDP probe.\n        fn probe_udp_data(&self) -> (Port, Port, TraceId, Flags) {\n            match self.config.multipath_strategy {\n                MultipathStrategy::Classic => match self.config.port_direction {\n                    PortDirection::FixedSrc(src_port) => (\n                        Port(src_port.0),\n                        Port(self.sequence.0),\n                        TraceId(0),\n                        Flags::empty(),\n                    ),\n                    PortDirection::FixedDest(dest_port) => (\n                        Port(self.sequence.0),\n                        Port(dest_port.0),\n                        TraceId(0),\n                        Flags::empty(),\n                    ),\n                    PortDirection::FixedBoth(_, _) | PortDirection::None => {\n                        unimplemented!()\n                    }\n                },\n                MultipathStrategy::Paris => {\n                    let round_port = ((self.config.initial_sequence.0 as usize + self.round.0)\n                        % usize::from(u16::MAX)) as u16;\n                    match self.config.port_direction {\n                        PortDirection::FixedSrc(src_port) => (\n                            Port(src_port.0),\n                            Port(round_port),\n                            TraceId(0),\n                            Flags::PARIS_CHECKSUM,\n                        ),\n                        PortDirection::FixedDest(dest_port) => (\n                            Port(round_port),\n                            Port(dest_port.0),\n                            TraceId(0),\n                            Flags::PARIS_CHECKSUM,\n                        ),\n                        PortDirection::FixedBoth(src_port, dest_port) => (\n                            Port(src_port.0),\n                            Port(dest_port.0),\n                            TraceId(0),\n                            Flags::PARIS_CHECKSUM,\n                        ),\n                        PortDirection::None => unimplemented!(),\n                    }\n                }\n                MultipathStrategy::Dublin => {\n                    let round_port = ((self.config.initial_sequence.0 as usize + self.round.0)\n                        % usize::from(u16::MAX)) as u16;\n                    match self.config.port_direction {\n                        PortDirection::FixedSrc(src_port) => (\n                            Port(src_port.0),\n                            Port(round_port),\n                            TraceId(self.sequence.0),\n                            Flags::DUBLIN_IPV6_PAYLOAD_LENGTH,\n                        ),\n                        PortDirection::FixedDest(dest_port) => (\n                            Port(round_port),\n                            Port(dest_port.0),\n                            TraceId(self.sequence.0),\n                            Flags::DUBLIN_IPV6_PAYLOAD_LENGTH,\n                        ),\n                        PortDirection::FixedBoth(src_port, dest_port) => (\n                            Port(src_port.0),\n                            Port(dest_port.0),\n                            TraceId(self.sequence.0),\n                            Flags::DUBLIN_IPV6_PAYLOAD_LENGTH,\n                        ),\n                        PortDirection::None => unimplemented!(),\n                    }\n                }\n            }\n        }\n\n        /// Determine the `src_port`, `dest_port` and `identifier` for the current TCP probe.\n        fn probe_tcp_data(&self) -> (Port, Port, TraceId, Flags) {\n            let (src_port, dest_port) = match self.config.port_direction {\n                PortDirection::FixedSrc(src_port) => (src_port.0, self.sequence.0),\n                PortDirection::FixedDest(dest_port) => (self.sequence.0, dest_port.0),\n                PortDirection::FixedBoth(_, _) | PortDirection::None => unimplemented!(),\n            };\n            (Port(src_port), Port(dest_port), TraceId(0), Flags::empty())\n        }\n\n        /// Update the state of a `ProbeStatus` and the trace.\n        ///\n        /// We want to update:\n        ///\n        /// - the `target_ttl` to be the time-to-live of the `ProbeStatus` request from the target\n        /// - the `max_received_ttl` we have observed this round\n        /// - the latest packet `received_time` in this round\n        /// - whether the target has been found in this round\n        ///\n        /// The ICMP replies may arrive out-of-order, and so we must be careful here to avoid\n        /// overwriting the state with stale values.  We may also receive multiple replies\n        /// from the target host with differing time-to-live values and so must ensure we\n        /// use the time-to-live with the lowest sequence number.\n        #[instrument(skip(self), level = \"trace\")]\n        pub fn complete_probe(&mut self, resp: StrategyResponse) {\n            // Retrieve and update the `ProbeStatus` at `sequence`.\n            let probe = self.probe_at(resp.sequence);\n            let awaited = match probe {\n                ProbeStatus::Awaited(awaited) => awaited,\n                // there is a valid scenario for TCP where a probe is already\n                // `Complete`, see `test_tcp_dest_unreachable_and_refused`.\n                ProbeStatus::Complete(_) => {\n                    return;\n                }\n                _ => {\n                    debug_assert!(\n                        false,\n                        \"completed probe was not in Awaited state (probe={probe:#?})\"\n                    );\n                    return;\n                }\n            };\n            let completed = awaited.complete(\n                resp.addr,\n                resp.received,\n                resp.icmp_packet_type,\n                resp.tos,\n                resp.expected_udp_checksum,\n                resp.actual_udp_checksum,\n                resp.exts,\n            );\n            let ttl = completed.ttl;\n            self.buffer[usize::from(resp.sequence - self.round_sequence)] =\n                ProbeStatus::Complete(completed);\n\n            // If this `ProbeStatus` found the target then we set the `target_ttl` if not already\n            // set, being careful to account for `Probes` being received out-of-order.\n            //\n            // If this `ProbeStatus` did not find the target but has a ttl that is greater or equal\n            // to the target ttl (if known) then we reset the target ttl to None.  This\n            // is to support Equal Cost Multi-path Routing (ECMP) cases where the number\n            // of hops to the target will vary over the lifetime of the trace.\n            self.target_ttl = if resp.is_target {\n                match self.target_ttl {\n                    None => Some(ttl),\n                    Some(target_ttl) if ttl < target_ttl => Some(ttl),\n                    Some(target_ttl) => Some(target_ttl),\n                }\n            } else {\n                match self.target_ttl {\n                    Some(target_ttl) if ttl >= target_ttl => None,\n                    Some(target_ttl) => Some(target_ttl),\n                    None => None,\n                }\n            };\n\n            self.max_received_ttl = match self.max_received_ttl {\n                None => Some(ttl),\n                Some(max_received_ttl) => Some(max_received_ttl.max(ttl)),\n            };\n\n            self.received_time = Some(resp.received);\n            self.target_found |= resp.is_target;\n        }\n\n        /// Advance to the next round.\n        ///\n        /// If, during the round which just completed, we went above the max sequence number then we\n        /// reset it here. We do this here to avoid having to deal with the sequence number\n        /// wrapping during a round, which is more problematic.\n        #[instrument(skip(self), level = \"trace\")]\n        pub fn advance_round(&mut self, first_ttl: TimeToLive) {\n            if self.sequence >= self.max_sequence() {\n                self.sequence = self.config.initial_sequence;\n            }\n            self.target_found = false;\n            self.round_sequence = self.sequence;\n            self.received_time = None;\n            self.round_start = SystemTime::now();\n            self.max_received_ttl = None;\n            self.round += RoundId(1);\n            self.ttl = first_ttl;\n        }\n\n        /// The maximum sequence number allowed.\n        ///\n        /// The Dublin multipath strategy for IPv6/udp encodes the sequence\n        /// number as the payload length and consequently the maximum sequence\n        /// number must be no larger than the maximum IPv6/udp payload size.\n        ///\n        /// It is also required that the range of possible sequence numbers is\n        /// _at least_ `BUFFER_SIZE` to ensure delayed responses from a prior\n        /// round are not incorrectly associated with later rounds (see\n        /// `in_round` function).\n        fn max_sequence(&self) -> Sequence {\n            match (self.config.multipath_strategy, self.config.target_addr) {\n                (MultipathStrategy::Dublin, IpAddr::V6(_)) => {\n                    self.config.initial_sequence + Sequence(BUFFER_SIZE)\n                }\n                _ => MAX_SEQUENCE,\n            }\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n        use crate::TypeOfService;\n        use crate::probe::{IcmpPacketCode, IcmpPacketType};\n        use crate::types::MaxInflight;\n        use rand::RngExt;\n        use std::net::{IpAddr, Ipv4Addr};\n        use std::time::Duration;\n\n        #[expect(clippy::too_many_lines, clippy::bool_assert_comparison)]\n        #[test]\n        fn test_state() {\n            let mut state = TracerState::new(cfg(Sequence(33434)));\n\n            // Validate the initial `TracerState`\n            assert_eq!(state.round, RoundId(0));\n            assert_eq!(state.sequence, Sequence(33434));\n            assert_eq!(state.round_sequence, Sequence(33434));\n            assert_eq!(state.ttl, TimeToLive(1));\n            assert_eq!(state.max_received_ttl, None);\n            assert_eq!(state.received_time, None);\n            assert_eq!(state.target_ttl, None);\n            assert_eq!(state.target_found, false);\n\n            // The initial state of the probe before sending\n            let prob_init = state.probe_at(Sequence(33434));\n            assert_eq!(ProbeStatus::NotSent, prob_init);\n\n            // Prepare probe 1 (round 0, sequence 33434, ttl 1) for sending\n            let sent_1 = SystemTime::now();\n            let probe_1 = state.next_probe(sent_1);\n            assert_eq!(probe_1.sequence, Sequence(33434));\n            assert_eq!(probe_1.ttl, TimeToLive(1));\n            assert_eq!(probe_1.round, RoundId(0));\n            assert_eq!(probe_1.sent, sent_1);\n\n            // Update the state of the probe 1 after receiving a `TimeExceeded`\n            let received_1 = SystemTime::now();\n            let host = IpAddr::V4(Ipv4Addr::LOCALHOST);\n            state.complete_probe(StrategyResponse {\n                icmp_packet_type: IcmpPacketType::TimeExceeded(IcmpPacketCode(1)),\n                trace_id: TraceId(0),\n                sequence: Sequence(33434),\n                tos: Some(TypeOfService(0)),\n                expected_udp_checksum: None,\n                actual_udp_checksum: None,\n                received: received_1,\n                addr: host,\n                is_target: false,\n                exts: None,\n            });\n\n            // Validate the state of the probe 1 after the update\n            let probe_1_fetch = state.probe_at(Sequence(33434)).try_into_complete().unwrap();\n            assert_eq!(probe_1_fetch.sequence, Sequence(33434));\n            assert_eq!(probe_1_fetch.ttl, TimeToLive(1));\n            assert_eq!(probe_1_fetch.round, RoundId(0));\n            assert_eq!(probe_1_fetch.received, received_1);\n            assert_eq!(probe_1_fetch.host, host);\n            assert_eq!(probe_1_fetch.sent, sent_1);\n            assert_eq!(\n                probe_1_fetch.icmp_packet_type,\n                IcmpPacketType::TimeExceeded(IcmpPacketCode(1))\n            );\n\n            // Validate the `TracerState` after the update\n            assert_eq!(state.round, RoundId(0));\n            assert_eq!(state.sequence, Sequence(33435));\n            assert_eq!(state.round_sequence, Sequence(33434));\n            assert_eq!(state.ttl, TimeToLive(2));\n            assert_eq!(state.max_received_ttl, Some(TimeToLive(1)));\n            assert_eq!(state.received_time, Some(received_1));\n            assert_eq!(state.target_ttl, None);\n            assert_eq!(state.target_found, false);\n\n            // Validate the probes() iterator returns only a single probe\n            {\n                let mut probe_iter = state.probes().iter();\n                let probe_next1 = probe_iter.next().unwrap();\n                assert_eq!(ProbeStatus::Complete(probe_1_fetch), probe_next1.clone());\n                assert_eq!(None, probe_iter.next());\n            }\n\n            // Advance to the next round\n            state.advance_round(TimeToLive(1));\n\n            // Validate the `TracerState` after the round update\n            assert_eq!(state.round, RoundId(1));\n            assert_eq!(state.sequence, Sequence(33435));\n            assert_eq!(state.round_sequence, Sequence(33435));\n            assert_eq!(state.ttl, TimeToLive(1));\n            assert_eq!(state.max_received_ttl, None);\n            assert_eq!(state.received_time, None);\n            assert_eq!(state.target_ttl, None);\n            assert_eq!(state.target_found, false);\n\n            // Prepare probe 2 (round 1, sequence 33001, ttl 1) for sending\n            let sent_2 = SystemTime::now();\n            let probe_2 = state.next_probe(sent_2);\n            assert_eq!(probe_2.sequence, Sequence(33435));\n            assert_eq!(probe_2.ttl, TimeToLive(1));\n            assert_eq!(probe_2.round, RoundId(1));\n            assert_eq!(probe_2.sent, sent_2);\n\n            // Prepare probe 3 (round 1, sequence 33002, ttl 2) for sending\n            let sent_3 = SystemTime::now();\n            let probe_3 = state.next_probe(sent_3);\n            assert_eq!(probe_3.sequence, Sequence(33436));\n            assert_eq!(probe_3.ttl, TimeToLive(2));\n            assert_eq!(probe_3.round, RoundId(1));\n            assert_eq!(probe_3.sent, sent_3);\n\n            // Update the state of probe 2 after receiving a `TimeExceeded`\n            let received_2 = SystemTime::now();\n            let host = IpAddr::V4(Ipv4Addr::LOCALHOST);\n            state.complete_probe(StrategyResponse {\n                icmp_packet_type: IcmpPacketType::TimeExceeded(IcmpPacketCode(1)),\n                trace_id: TraceId(0),\n                sequence: Sequence(33435),\n                tos: Some(TypeOfService(0)),\n                expected_udp_checksum: None,\n                actual_udp_checksum: None,\n                received: received_2,\n                addr: host,\n                is_target: false,\n                exts: None,\n            });\n            let probe_2_recv = state.probe_at(Sequence(33435));\n\n            // Validate the `TracerState` after the update to probe 2\n            assert_eq!(state.round, RoundId(1));\n            assert_eq!(state.sequence, Sequence(33437));\n            assert_eq!(state.round_sequence, Sequence(33435));\n            assert_eq!(state.ttl, TimeToLive(3));\n            assert_eq!(state.max_received_ttl, Some(TimeToLive(1)));\n            assert_eq!(state.received_time, Some(received_2));\n            assert_eq!(state.target_ttl, None);\n            assert_eq!(state.target_found, false);\n\n            // Validate the probes() iterator returns the two probes in the states we expect\n            {\n                let mut probe_iter = state.probes().iter();\n                let probe_next1 = probe_iter.next().unwrap();\n                assert_eq!(&probe_2_recv, probe_next1);\n                let probe_next2 = probe_iter.next().unwrap();\n                assert_eq!(ProbeStatus::Awaited(probe_3), probe_next2.clone());\n            }\n\n            // Update the state of probe 3 after receiving a `EchoReply`\n            let received_3 = SystemTime::now();\n            let host = IpAddr::V4(Ipv4Addr::LOCALHOST);\n            state.complete_probe(StrategyResponse {\n                icmp_packet_type: IcmpPacketType::EchoReply(IcmpPacketCode(0)),\n                trace_id: TraceId(0),\n                sequence: Sequence(33436),\n                tos: Some(TypeOfService(0)),\n                expected_udp_checksum: None,\n                actual_udp_checksum: None,\n                received: received_3,\n                addr: host,\n                is_target: true,\n                exts: None,\n            });\n            let probe_3_recv = state.probe_at(Sequence(33436));\n\n            // Validate the `TracerState` after the update to probe 3\n            assert_eq!(state.round, RoundId(1));\n            assert_eq!(state.sequence, Sequence(33437));\n            assert_eq!(state.round_sequence, Sequence(33435));\n            assert_eq!(state.ttl, TimeToLive(3));\n            assert_eq!(state.max_received_ttl, Some(TimeToLive(2)));\n            assert_eq!(state.received_time, Some(received_3));\n            assert_eq!(state.target_ttl, Some(TimeToLive(2)));\n            assert_eq!(state.target_found, true);\n\n            // Validate the probes() iterator returns the two probes in the states we expect\n            {\n                let mut probe_iter = state.probes().iter();\n                let probe_next1 = probe_iter.next().unwrap();\n                assert_eq!(&probe_2_recv, probe_next1);\n                let probe_next2 = probe_iter.next().unwrap();\n                assert_eq!(&probe_3_recv, probe_next2);\n            }\n        }\n\n        #[test]\n        fn test_sequence_wrap1() {\n            // Start from `MAX_SEQUENCE` - 1 which is (65279 - 1) == 65278\n            let initial_sequence = Sequence(65278);\n            let mut state = TracerState::new(cfg(initial_sequence));\n            assert_eq!(state.round, RoundId(0));\n            assert_eq!(state.sequence, initial_sequence);\n            assert_eq!(state.round_sequence, initial_sequence);\n\n            // Create a probe at seq 65278\n            assert_eq!(\n                state.next_probe(SystemTime::now()).sequence,\n                Sequence(65278)\n            );\n            assert_eq!(state.sequence, Sequence(65279));\n\n            // Validate the probes()\n            {\n                let mut iter = state.probes().iter();\n                assert_eq!(\n                    iter.next()\n                        .unwrap()\n                        .clone()\n                        .try_into_awaited()\n                        .unwrap()\n                        .sequence,\n                    Sequence(65278)\n                );\n                iter.take(BUFFER_SIZE as usize - 1)\n                    .for_each(|p| assert!(matches!(p, ProbeStatus::NotSent)));\n            }\n\n            // Advance the round, which will wrap the sequence back to `initial_sequence`\n            state.advance_round(TimeToLive(1));\n            assert_eq!(state.round, RoundId(1));\n            assert_eq!(state.sequence, initial_sequence);\n            assert_eq!(state.round_sequence, initial_sequence);\n\n            // Create a probe at seq 65278\n            assert_eq!(\n                state.next_probe(SystemTime::now()).sequence,\n                Sequence(65278)\n            );\n            assert_eq!(state.sequence, Sequence(65279));\n\n            // Validate the probes() again\n            {\n                let mut iter = state.probes().iter();\n                assert_eq!(\n                    iter.next()\n                        .unwrap()\n                        .clone()\n                        .try_into_awaited()\n                        .unwrap()\n                        .sequence,\n                    Sequence(65278)\n                );\n                iter.take(BUFFER_SIZE as usize - 1)\n                    .for_each(|p| assert!(matches!(p, ProbeStatus::NotSent)));\n            }\n        }\n\n        #[test]\n        fn test_sequence_wrap2() {\n            let total_rounds = 2000;\n            let max_probe_per_round = 254;\n            let mut state = TracerState::new(cfg(Sequence(33434)));\n            for _ in 0..total_rounds {\n                for _ in 0..max_probe_per_round {\n                    let _probe = state.next_probe(SystemTime::now());\n                }\n                state.advance_round(TimeToLive(1));\n            }\n            assert_eq!(state.round, RoundId(2000));\n            assert_eq!(state.round_sequence, Sequence(33434));\n            assert_eq!(state.sequence, Sequence(33434));\n        }\n\n        #[test]\n        fn test_sequence_wrap3() {\n            let total_rounds = 2000;\n            let max_probe_per_round = 20;\n            let mut state = TracerState::new(cfg(Sequence(33434)));\n            let mut rng = rand::rng();\n            for _ in 0..total_rounds {\n                for _ in 0..rng.random_range(0..max_probe_per_round) {\n                    state.next_probe(SystemTime::now());\n                }\n                state.advance_round(TimeToLive(1));\n            }\n        }\n\n        #[test]\n        fn test_sequence_wrap_with_skip() {\n            let total_rounds = 2000;\n            let max_probe_per_round = 254;\n            let mut state = TracerState::new(cfg(Sequence(33434)));\n            for _ in 0..total_rounds {\n                for _ in 0..max_probe_per_round {\n                    _ = state.next_probe(SystemTime::now());\n                    _ = state.reissue_probe(SystemTime::now());\n                }\n                state.advance_round(TimeToLive(1));\n            }\n            assert_eq!(state.round, RoundId(2000));\n            assert_eq!(state.round_sequence, Sequence(57310));\n            assert_eq!(state.sequence, Sequence(57310));\n        }\n\n        #[test]\n        fn test_in_round() {\n            let state = TracerState::new(cfg(Sequence(33434)));\n            assert!(state.in_round(Sequence(33434)));\n            assert!(state.in_round(Sequence(33945)));\n            assert!(!state.in_round(Sequence(33946)));\n        }\n\n        #[test]\n        #[should_panic(expected = \"assertion failed: !state.in_round(Sequence(64491))\")]\n        fn test_in_delayed_probe_not_in_round() {\n            let mut state = TracerState::new(cfg(Sequence(64000)));\n            for _ in 0..55 {\n                _ = state.next_probe(SystemTime::now());\n            }\n            state.advance_round(TimeToLive(1));\n            assert!(!state.in_round(Sequence(64491)));\n        }\n\n        fn cfg(initial_sequence: Sequence) -> StrategyConfig {\n            StrategyConfig {\n                target_addr: IpAddr::V4(Ipv4Addr::UNSPECIFIED),\n                protocol: Protocol::Icmp,\n                trace_identifier: TraceId::default(),\n                max_rounds: None,\n                first_ttl: TimeToLive(1),\n                max_ttl: TimeToLive(24),\n                grace_duration: Duration::default(),\n                max_inflight: MaxInflight::default(),\n                initial_sequence,\n                multipath_strategy: MultipathStrategy::Classic,\n                port_direction: PortDirection::None,\n                min_round_duration: Duration::default(),\n                max_round_duration: Duration::default(),\n            }\n        }\n    }\n}\n\n/// Returns true if the duration between start and end is grater than a duration, false otherwise.\nfn exceeds(start: Option<SystemTime>, end: SystemTime, dur: Duration) -> bool {\n    start.is_some_and(|start| end.duration_since(start).unwrap_or_default() > dur)\n}\n"
  },
  {
    "path": "crates/trippy-core/src/tracer.rs",
    "content": "use crate::error::Result;\nuse crate::{\n    Error, IcmpExtensionParseMode, MaxInflight, MaxRounds, MultipathStrategy, PacketSize,\n    PayloadPattern, PortDirection, PrivilegeMode, Protocol, Round, Sequence, State, TimeToLive,\n    TraceId, TypeOfService,\n};\nuse std::fmt::Debug;\nuse std::net::IpAddr;\nuse std::sync::Arc;\nuse std::thread;\nuse std::thread::JoinHandle;\nuse std::time::Duration;\n\n/// A traceroute implementation.\n///\n/// See the [`crate`] documentation for more information.\n///\n/// Note that this is type cheaply cloneable.\n#[derive(Debug, Clone)]\npub struct Tracer {\n    inner: Arc<inner::TracerInner>,\n}\n\nimpl Tracer {\n    /// Create a `Tracer`.\n    ///\n    /// Use the [`crate::Builder`] type to create a [`Tracer`].\n    #[expect(clippy::too_many_arguments)]\n    #[must_use]\n    pub(crate) fn new(\n        interface: Option<String>,\n        source_addr: Option<IpAddr>,\n        target_addr: IpAddr,\n        privilege_mode: PrivilegeMode,\n        protocol: Protocol,\n        packet_size: PacketSize,\n        payload_pattern: PayloadPattern,\n        tos: TypeOfService,\n        icmp_extension_parse_mode: IcmpExtensionParseMode,\n        read_timeout: Duration,\n        tcp_connect_timeout: Duration,\n        trace_identifier: TraceId,\n        max_rounds: Option<MaxRounds>,\n        first_ttl: TimeToLive,\n        max_ttl: TimeToLive,\n        grace_duration: Duration,\n        max_inflight: MaxInflight,\n        initial_sequence: Sequence,\n        multipath_strategy: MultipathStrategy,\n        port_direction: PortDirection,\n        min_round_duration: Duration,\n        max_round_duration: Duration,\n        max_samples: usize,\n        max_flows: usize,\n        drop_privileges: bool,\n    ) -> Self {\n        Self {\n            inner: Arc::new(inner::TracerInner::new(\n                interface,\n                source_addr,\n                target_addr,\n                privilege_mode,\n                protocol,\n                packet_size,\n                payload_pattern,\n                tos,\n                icmp_extension_parse_mode,\n                read_timeout,\n                tcp_connect_timeout,\n                trace_identifier,\n                max_rounds,\n                first_ttl,\n                max_ttl,\n                grace_duration,\n                max_inflight,\n                initial_sequence,\n                multipath_strategy,\n                port_direction,\n                min_round_duration,\n                max_round_duration,\n                max_samples,\n                max_flows,\n                drop_privileges,\n            )),\n        }\n    }\n\n    /// Run the [`Tracer`].\n    ///\n    /// This method will block until either the trace completes all rounds (if\n    /// [`crate::Builder::max_rounds`] has been called to set to a non-zero\n    /// value) or until the trace fails.\n    ///\n    /// At the completion of the trace, the state of the tracer can be\n    /// retrieved using the [`Tracer::snapshot`] method.\n    ///\n    /// If you want to run the tracer indefinitely (by not setting\n    /// [`crate::Builder::max_rounds`]), you can either clone and run the\n    /// tracer on a separate thread by using the [`Tracer::spawn`] method or\n    /// by use the [`Tracer::run_with`] method in the current thread to gather\n    /// pee round state manually.\n    ///\n    /// # Example\n    ///\n    /// The following will run the tracer for a fixed number (3) of rounds and\n    /// then retrieve the final state snapshot:\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// # use std::net::IpAddr;\n    /// # use std::str::FromStr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from_str(\"1.1.1.1\")?;\n    /// let tracer = Builder::new(addr).max_rounds(Some(3)).build()?;\n    /// tracer.run()?;\n    /// let _state = tracer.snapshot();\n    /// # Ok(())\n    /// # }\n    /// ```\n    ///\n    /// # See Also\n    ///\n    /// - [`Tracer::run_with`] - Run the tracer with a custom round handler.\n    /// - [`Tracer::spawn`] - Spawn the tracer on a new thread without a custom round handler.\n    pub fn run(&self) -> Result<()> {\n        self.inner.run()\n    }\n\n    /// Run the [`Tracer`] with a custom round handler.\n    ///\n    /// This method will block until either the trace completes all rounds (if\n    /// [`crate::Builder::max_rounds`] has been called to set to a non-zero\n    /// value) or until the trace fails.\n    ///\n    /// At the completion of the trace, the state of the tracer can be\n    /// retrieved using the [`Tracer::snapshot`] method.\n    ///\n    /// This method will additionally call the provided function for each round\n    /// that is completed.  This can be useful if you want to gather round state\n    /// manually if the tracer is run indefinitely (by not setting\n    /// [`crate::Builder::max_rounds`])\n    ///\n    /// # Example\n    ///\n    /// The following will run the tracer indefinitely and print the data from\n    /// each round of tracing:\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// # use std::net::IpAddr;\n    /// # use std::str::FromStr;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from_str(\"1.1.1.1\")?;\n    /// let tracer = Builder::new(addr).build()?;\n    /// tracer.run_with(|round| println!(\"{:?}\", round))?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    ///\n    /// # See Also\n    ///\n    /// - [`Tracer::run`] - Run the tracer without a custom round handler.\n    pub fn run_with<F: Fn(&Round<'_>)>(&self, func: F) -> Result<()> {\n        self.inner.run_with(func)\n    }\n\n    /// Spawn the tracer on a new thread.\n    ///\n    /// This method will spawn a new thread to run the tracer and immediately\n    /// return the [`Tracer`] and a handle to the thread, so it may be joined\n    /// with [`JoinHandle::join`].\n    ///\n    /// If you want to run the tracer indefinitely (by not setting\n    /// [`crate::Builder::max_rounds`]) you can use this method to spawn the\n    /// tracer on a new thread and return the [`Tracer`] such that a\n    /// [`Tracer::snapshot`] of the state can be taken at any time.\n    ///\n    /// # Example\n    ///\n    /// The following will spawn a tracer on a new thread and take a snapshot\n    /// of the state every 5 seconds:\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// # use std::net::IpAddr;\n    /// # use std::str::FromStr;\n    /// # use std::thread;\n    /// # use std::time::Duration;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from_str(\"1.1.1.1\")?;\n    /// let (tracer, _) = Builder::new(addr).build()?.spawn()?;\n    /// loop {\n    ///     thread::sleep(Duration::from_secs(5));\n    ///     // get the latest state.\n    ///     let _state = tracer.snapshot();\n    /// }\n    /// # Ok(())\n    /// # }\n    /// ```\n    ///\n    /// # See Also\n    ///\n    /// - [`Tracer::run`] - Run the tracer on the current thread.\n    pub fn spawn(self) -> Result<(Self, JoinHandle<Result<()>>)> {\n        let tracer = self.clone();\n        let handle = thread::Builder::new()\n            .name(format!(\"tracer-{}\", self.trace_identifier().0))\n            .spawn(move || tracer.run())\n            .map_err(|err| Error::Other(err.to_string()))?;\n        Ok((self, handle))\n    }\n\n    /// Spawn the tracer with a custom round handler on a new thread.\n    ///\n    /// This method will spawn a new thread to run the tracer with a custom\n    /// round handler and immediately return the [`Tracer`] and a handle to the\n    /// thread, so it may be joined with [`JoinHandle::join`].\n    ///\n    /// # Example\n    ///\n    /// The following will spawn a tracer on a new thread with a custom round\n    /// handler to print the data from each round of tracing and also take a\n    /// snapshot of the state every 5 seconds until the tracer completes all\n    /// rounds:\n    ///\n    /// ```no_run\n    /// # fn main() -> anyhow::Result<()> {\n    /// # use std::net::IpAddr;\n    /// # use std::str::FromStr;\n    /// # use std::thread;\n    /// # use std::time::Duration;\n    /// use trippy_core::Builder;\n    ///\n    /// let addr = IpAddr::from_str(\"1.1.1.1\")?;\n    /// let (tracer, handle) = Builder::new(addr)\n    ///     .max_rounds(Some(3))\n    ///     .build()?\n    ///     .spawn_with(|round| println!(\"{:?}\", round))?;\n    /// for i in 0..3 {\n    ///     thread::sleep(Duration::from_secs(5));\n    ///     // get the latest state.\n    ///     let _state = tracer.snapshot();\n    /// }\n    /// handle.join().unwrap()?;\n    /// # Ok(())\n    /// # }\n    /// ```\n    ///\n    /// # See Also\n    ///\n    /// - [`Tracer::spawn`] - Spawn the tracer on a new thread without a custom round handler.\n    pub fn spawn_with<F: Fn(&Round<'_>) + Send + 'static>(\n        self,\n        func: F,\n    ) -> Result<(Self, JoinHandle<Result<()>>)> {\n        let tracer = self.clone();\n        let handle = thread::Builder::new()\n            .name(format!(\"tracer-{}\", self.trace_identifier().0))\n            .spawn(move || tracer.run_with(func))\n            .map_err(|err| Error::Other(err.to_string()))?;\n        Ok((self, handle))\n    }\n\n    /// Take a snapshot of the tracer state.\n    #[must_use]\n    pub fn snapshot(&self) -> State {\n        self.inner.snapshot()\n    }\n\n    /// Clear the tracer state.\n    pub fn clear(&self) {\n        self.inner.clear();\n    }\n\n    /// The maximum number of flows to record.\n    #[must_use]\n    pub fn max_flows(&self) -> usize {\n        self.inner.max_flows()\n    }\n\n    /// The maximum number of samples to record.\n    #[must_use]\n    pub fn max_samples(&self) -> usize {\n        self.inner.max_samples()\n    }\n\n    /// The privilege mode of the tracer.\n    #[must_use]\n    pub fn privilege_mode(&self) -> PrivilegeMode {\n        self.inner.privilege_mode()\n    }\n\n    /// The protocol of the tracer.\n    #[must_use]\n    pub fn protocol(&self) -> Protocol {\n        self.inner.protocol()\n    }\n\n    /// The interface to use for the tracer.\n    #[must_use]\n    pub fn interface(&self) -> Option<&str> {\n        self.inner.interface()\n    }\n\n    /// The source address of the tracer.\n    #[must_use]\n    pub fn source_addr(&self) -> Option<IpAddr> {\n        self.inner.source_addr()\n    }\n\n    /// The target address of the tracer.\n    #[must_use]\n    pub fn target_addr(&self) -> IpAddr {\n        self.inner.target_addr()\n    }\n\n    /// The packet size of the tracer.\n    #[must_use]\n    pub fn packet_size(&self) -> PacketSize {\n        self.inner.packet_size()\n    }\n\n    /// The payload pattern of the tracer.\n    #[must_use]\n    pub fn payload_pattern(&self) -> PayloadPattern {\n        self.inner.payload_pattern()\n    }\n\n    /// The initial sequence number of the tracer.\n    #[must_use]\n    pub fn initial_sequence(&self) -> Sequence {\n        self.inner.initial_sequence()\n    }\n\n    /// The type of service of the tracer.\n    #[must_use]\n    pub fn tos(&self) -> TypeOfService {\n        self.inner.tos()\n    }\n\n    /// The ICMP extension parse mode of the tracer.\n    #[must_use]\n    pub fn icmp_extension_parse_mode(&self) -> IcmpExtensionParseMode {\n        self.inner.icmp_extension_parse_mode()\n    }\n\n    /// The read timeout of the tracer.\n    #[must_use]\n    pub fn read_timeout(&self) -> Duration {\n        self.inner.read_timeout()\n    }\n\n    /// The TCP connect timeout of the tracer.\n    #[must_use]\n    pub fn tcp_connect_timeout(&self) -> Duration {\n        self.inner.tcp_connect_timeout()\n    }\n\n    /// The trace identifier of the tracer.\n    #[must_use]\n    pub fn trace_identifier(&self) -> TraceId {\n        self.inner.trace_identifier()\n    }\n\n    /// The maximum number of rounds of the tracer.\n    #[must_use]\n    pub fn max_rounds(&self) -> Option<MaxRounds> {\n        self.inner.max_rounds()\n    }\n\n    /// The first time-to-live value of the tracer.\n    #[must_use]\n    pub fn first_ttl(&self) -> TimeToLive {\n        self.inner.first_ttl()\n    }\n\n    /// The maximum time-to-live value of the tracer.\n    #[must_use]\n    pub fn max_ttl(&self) -> TimeToLive {\n        self.inner.max_ttl()\n    }\n\n    /// The grace duration of the tracer.\n    #[must_use]\n    pub fn grace_duration(&self) -> Duration {\n        self.inner.grace_duration()\n    }\n\n    /// The maximum number of in-flight probes of the tracer.\n    #[must_use]\n    pub fn max_inflight(&self) -> MaxInflight {\n        self.inner.max_inflight()\n    }\n\n    /// The multipath strategy of the tracer.\n    #[must_use]\n    pub fn multipath_strategy(&self) -> MultipathStrategy {\n        self.inner.multipath_strategy()\n    }\n\n    /// The port direction of the tracer.\n    #[must_use]\n    pub fn port_direction(&self) -> PortDirection {\n        self.inner.port_direction()\n    }\n\n    /// The minimum round duration of the tracer.\n    #[must_use]\n    pub fn min_round_duration(&self) -> Duration {\n        self.inner.min_round_duration()\n    }\n\n    /// The maximum round duration of the tracer.\n    #[must_use]\n    pub fn max_round_duration(&self) -> Duration {\n        self.inner.max_round_duration()\n    }\n}\n\nmod inner {\n    use crate::config::{ChannelConfig, StateConfig, StrategyConfig};\n    use crate::error::Result;\n    use crate::net::{PlatformImpl, SocketImpl};\n    use crate::{\n        Channel, Error, IcmpExtensionParseMode, MaxInflight, MaxRounds, MultipathStrategy,\n        PacketSize, PayloadPattern, PortDirection, PrivilegeMode, Protocol, Round, Sequence,\n        SourceAddr, State, Strategy, TimeToLive, TraceId, TypeOfService,\n    };\n    use parking_lot::RwLock;\n    use std::fmt::Debug;\n    use std::net::IpAddr;\n    use std::sync::OnceLock;\n    use std::time::Duration;\n    use tracing::instrument;\n    use trippy_privilege::Privilege;\n\n    #[derive(Debug)]\n    pub(super) struct TracerInner {\n        source_addr: Option<IpAddr>,\n        interface: Option<String>,\n        target_addr: IpAddr,\n        privilege_mode: PrivilegeMode,\n        protocol: Protocol,\n        packet_size: PacketSize,\n        payload_pattern: PayloadPattern,\n        tos: TypeOfService,\n        icmp_extension_parse_mode: IcmpExtensionParseMode,\n        read_timeout: Duration,\n        tcp_connect_timeout: Duration,\n        trace_identifier: TraceId,\n        max_rounds: Option<MaxRounds>,\n        first_ttl: TimeToLive,\n        max_ttl: TimeToLive,\n        grace_duration: Duration,\n        max_inflight: MaxInflight,\n        initial_sequence: Sequence,\n        multipath_strategy: MultipathStrategy,\n        port_direction: PortDirection,\n        min_round_duration: Duration,\n        max_round_duration: Duration,\n        max_samples: usize,\n        max_flows: usize,\n        drop_privileges: bool,\n        state: RwLock<State>,\n        src: OnceLock<IpAddr>,\n    }\n\n    impl TracerInner {\n        #[expect(clippy::too_many_arguments)]\n        pub(super) fn new(\n            interface: Option<String>,\n            source_addr: Option<IpAddr>,\n            target_addr: IpAddr,\n            privilege_mode: PrivilegeMode,\n            protocol: Protocol,\n            packet_size: PacketSize,\n            payload_pattern: PayloadPattern,\n            tos: TypeOfService,\n            icmp_extension_parse_mode: IcmpExtensionParseMode,\n            read_timeout: Duration,\n            tcp_connect_timeout: Duration,\n            trace_identifier: TraceId,\n            max_rounds: Option<MaxRounds>,\n            first_ttl: TimeToLive,\n            max_ttl: TimeToLive,\n            grace_duration: Duration,\n            max_inflight: MaxInflight,\n            initial_sequence: Sequence,\n            multipath_strategy: MultipathStrategy,\n            port_direction: PortDirection,\n            min_round_duration: Duration,\n            max_round_duration: Duration,\n            max_samples: usize,\n            max_flows: usize,\n            drop_privileges: bool,\n        ) -> Self {\n            Self {\n                source_addr,\n                interface,\n                target_addr,\n                privilege_mode,\n                protocol,\n                packet_size,\n                payload_pattern,\n                tos,\n                icmp_extension_parse_mode,\n                read_timeout,\n                tcp_connect_timeout,\n                trace_identifier,\n                max_rounds,\n                first_ttl,\n                max_ttl,\n                grace_duration,\n                max_inflight,\n                initial_sequence,\n                multipath_strategy,\n                port_direction,\n                min_round_duration,\n                max_round_duration,\n                max_samples,\n                max_flows,\n                drop_privileges,\n                state: RwLock::new(State::new(Self::make_state_config(max_flows, max_samples))),\n                src: OnceLock::new(),\n            }\n        }\n\n        #[instrument(skip_all, level = \"trace\")]\n        pub(super) fn run(&self) -> Result<()> {\n            self.run_internal(|_| ())\n                .map_err(|err| self.handle_error(err))\n        }\n\n        #[instrument(skip_all, level = \"trace\")]\n        pub(super) fn run_with<F: Fn(&Round<'_>)>(&self, func: F) -> Result<()> {\n            self.run_internal(func)\n                .map_err(|err| self.handle_error(err))\n        }\n\n        pub(super) fn snapshot(&self) -> State {\n            self.state.read().clone()\n        }\n\n        pub(super) fn clear(&self) {\n            *self.state.write() =\n                State::new(Self::make_state_config(self.max_flows, self.max_samples));\n        }\n\n        pub(super) const fn max_flows(&self) -> usize {\n            self.max_flows\n        }\n\n        pub(super) const fn max_samples(&self) -> usize {\n            self.max_samples\n        }\n\n        pub(super) const fn privilege_mode(&self) -> PrivilegeMode {\n            self.privilege_mode\n        }\n\n        pub(super) const fn protocol(&self) -> Protocol {\n            self.protocol\n        }\n\n        pub(super) fn interface(&self) -> Option<&str> {\n            self.interface.as_deref()\n        }\n\n        pub(super) fn source_addr(&self) -> Option<IpAddr> {\n            self.src.get().copied()\n        }\n\n        pub(super) const fn target_addr(&self) -> IpAddr {\n            self.target_addr\n        }\n\n        pub(super) const fn packet_size(&self) -> PacketSize {\n            self.packet_size\n        }\n\n        pub(super) const fn payload_pattern(&self) -> PayloadPattern {\n            self.payload_pattern\n        }\n\n        pub(super) const fn initial_sequence(&self) -> Sequence {\n            self.initial_sequence\n        }\n\n        pub(super) const fn tos(&self) -> TypeOfService {\n            self.tos\n        }\n\n        pub(super) const fn icmp_extension_parse_mode(&self) -> IcmpExtensionParseMode {\n            self.icmp_extension_parse_mode\n        }\n\n        pub(super) const fn read_timeout(&self) -> Duration {\n            self.read_timeout\n        }\n\n        pub(super) const fn tcp_connect_timeout(&self) -> Duration {\n            self.tcp_connect_timeout\n        }\n\n        pub(super) const fn trace_identifier(&self) -> TraceId {\n            self.trace_identifier\n        }\n\n        pub(super) const fn max_rounds(&self) -> Option<MaxRounds> {\n            self.max_rounds\n        }\n\n        pub(super) const fn first_ttl(&self) -> TimeToLive {\n            self.first_ttl\n        }\n\n        pub(super) const fn max_ttl(&self) -> TimeToLive {\n            self.max_ttl\n        }\n\n        pub(super) const fn grace_duration(&self) -> Duration {\n            self.grace_duration\n        }\n\n        pub(super) const fn max_inflight(&self) -> MaxInflight {\n            self.max_inflight\n        }\n\n        pub(super) const fn multipath_strategy(&self) -> MultipathStrategy {\n            self.multipath_strategy\n        }\n\n        pub(super) const fn port_direction(&self) -> PortDirection {\n            self.port_direction\n        }\n\n        pub(super) const fn min_round_duration(&self) -> Duration {\n            self.min_round_duration\n        }\n\n        pub(super) const fn max_round_duration(&self) -> Duration {\n            self.max_round_duration\n        }\n\n        #[instrument(skip_all, level = \"trace\")]\n        fn run_internal<F: Fn(&Round<'_>)>(&self, func: F) -> Result<()> {\n            // if we are given a source address, validate it otherwise\n            // discover it based on the target address and interface.\n            let source_addr = match self.source_addr {\n                None => SourceAddr::discover::<SocketImpl, PlatformImpl>(\n                    self.target_addr,\n                    self.port_direction,\n                    self.interface.as_deref(),\n                )?,\n                Some(addr) => SourceAddr::validate::<SocketImpl>(addr)?,\n            };\n            self.src\n                .set(source_addr)\n                .map_err(|_| Error::Other(String::from(\"failed to set source_addr\")))?;\n            let channel_config = self.make_channel_config(source_addr);\n            let channel = Channel::<SocketImpl>::connect(&channel_config)?;\n            if self.drop_privileges {\n                Privilege::drop_privileges()?;\n            }\n            let strategy_config = self.make_strategy_config();\n            let strategy = Strategy::new(&strategy_config, |round| {\n                self.handler(round);\n                func(round);\n            });\n            strategy.run(channel)?;\n            Ok(())\n        }\n\n        fn handler(&self, round: &Round<'_>) {\n            self.state.write().update_from_round(round);\n        }\n\n        fn handle_error(&self, err: Error) -> Error {\n            self.state.write().set_error(Some(err.to_string()));\n            err\n        }\n\n        const fn make_state_config(max_flows: usize, max_samples: usize) -> StateConfig {\n            StateConfig {\n                max_samples,\n                max_flows,\n            }\n        }\n\n        const fn make_channel_config(&self, source_addr: IpAddr) -> ChannelConfig {\n            ChannelConfig {\n                privilege_mode: self.privilege_mode,\n                protocol: self.protocol,\n                source_addr,\n                target_addr: self.target_addr,\n                packet_size: self.packet_size,\n                payload_pattern: self.payload_pattern,\n                initial_sequence: self.initial_sequence,\n                tos: self.tos,\n                icmp_extension_parse_mode: self.icmp_extension_parse_mode,\n                read_timeout: self.read_timeout,\n                tcp_connect_timeout: self.tcp_connect_timeout,\n            }\n        }\n\n        const fn make_strategy_config(&self) -> StrategyConfig {\n            StrategyConfig {\n                target_addr: self.target_addr,\n                protocol: self.protocol,\n                trace_identifier: self.trace_identifier,\n                max_rounds: self.max_rounds,\n                first_ttl: self.first_ttl,\n                max_ttl: self.max_ttl,\n                grace_duration: self.grace_duration,\n                max_inflight: self.max_inflight,\n                initial_sequence: self.initial_sequence,\n                multipath_strategy: self.multipath_strategy,\n                port_direction: self.port_direction,\n                min_round_duration: self.min_round_duration,\n                max_round_duration: self.max_round_duration,\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/src/types.rs",
    "content": "use bitflags::bitflags;\nuse derive_more::{Add, AddAssign, Rem, Sub};\nuse std::num::NonZeroUsize;\n\n/// `Round` newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd, AddAssign)]\npub struct RoundId(pub usize);\n\n/// `MaxRound` newtype.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]\npub struct MaxRounds(pub NonZeroUsize);\n\n/// `TimeToLive` (ttl) newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd, Add, Sub, AddAssign)]\npub struct TimeToLive(pub u8);\n\n/// `Sequence` number newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd, Add, Sub, AddAssign, Rem)]\npub struct Sequence(pub u16);\n\n/// `TraceId` newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)]\npub struct TraceId(pub u16);\n\n/// `MaxInflight` newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)]\npub struct MaxInflight(pub u8);\n\n/// `PacketSize` newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)]\npub struct PacketSize(pub u16);\n\n/// `PayloadPattern` newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)]\npub struct PayloadPattern(pub u8);\n\n/// `TypeOfService` (aka `DSCP` & `ECN`) newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)]\npub struct TypeOfService(pub u8);\n\n/// Port newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)]\npub struct Port(pub u16);\n\n/// Checksum newtype.\n#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Ord, PartialOrd)]\npub struct Checksum(pub u16);\n\nbitflags! {\n    /// Probe flags.\n    #[derive(Debug, Clone, PartialEq, Eq)]\n    pub struct Flags: u32 {\n        /// Swap the checksum and payload (UDP only).\n        const PARIS_CHECKSUM = 1;\n        /// Encode the sequence number as the payload length (IPv6/UDP only)\n        const DUBLIN_IPV6_PAYLOAD_LENGTH = 2;\n    }\n}\n\nimpl From<Sequence> for usize {\n    fn from(sequence: Sequence) -> Self {\n        sequence.0 as Self\n    }\n}\n\n/// Explicit Congestion Notification (`ECN`).\n///\n/// This is used in the `ECN` field of the `IP` header.\n///\n/// - See [rfc3246](https://datatracker.ietf.org/doc/html/rfc3246) for more details.\n#[derive(Debug, Copy, Clone, PartialEq, Eq)]\npub enum Ecn {\n    /// Not ECN-Capable Transport (00, 0 dec).\n    NotECT,\n    /// ECN Capable Transport(1) (01, 1 dec).\n    ECT1,\n    /// ECN Capable Transport(0) (10, 2 dec).\n    ECT0,\n    /// Congestion Experienced (11, 3 dec).\n    CE,\n}\n\n/// Differentiated Services Code Point (`DSCP`).\n///\n/// This is used in the `DSCP` field of the `IP` header.\n///\n/// - See [rfc2474](https://datatracker.ietf.org/doc/html/rfc2474) for more details on `AFnn`.\n/// - See [rfc2475](https://datatracker.ietf.org/doc/html/rfc2475) and\n///   [rfc2476](https://datatracker.ietf.org/doc/html/rfc2476) for more details on `CSn`.\n/// - See [rfc3168](https://datatracker.ietf.org/doc/html/rfc3168) for more details on `CE`.\n/// - See [rfc5865](https://datatracker.ietf.org/doc/html/rfc5865) for more details on `VA`.\n/// - See [rfc8622](https://datatracker.ietf.org/doc/html/rfc8622) for more details on `LE`.\n#[derive(Debug, Copy, Clone, PartialEq, Eq)]\npub enum Dscp {\n    /// Default Forwarding (000 000, 0 dec).\n    ///\n    /// aka Best Effort (`BE`) aka Class Selector 0 (`CS0`).\n    ///\n    /// See rfc2474 and 2475.\n    DF,\n    /// Assured Forwarding 11 (001 010, 10 dec).\n    AF11,\n    /// Assured Forwarding 12 (001 100, 12 dec).\n    AF12,\n    /// Assured Forwarding 13 (001 110, 14 dec).\n    AF13,\n    /// Assured Forwarding 21 (010 010, 18 dec).\n    AF21,\n    /// Assured Forwarding 22 (010 100, 20 dec).\n    AF22,\n    /// Assured Forwarding 23 (010 110, 22 dec).\n    AF23,\n    /// Assured Forwarding 31 (011 010, 26 dec).\n    AF31,\n    /// Assured Forwarding 32 (011 100, 28 dec).\n    AF32,\n    /// Assured Forwarding 33 (011 110, 30 dec).\n    AF33,\n    /// Assured Forwarding 41 (100 010, 34 dec).\n    AF41,\n    /// Assured Forwarding 42 (100 100, 36 dec).\n    AF42,\n    /// Assured Forwarding 43 (100 110, 38 dec).\n    AF43,\n    /// Class Selector 1 (001 000, 8 dec).\n    CS1,\n    /// Class Selector 2 (010 000, 16 dec).\n    CS2,\n    /// Class Selector 3 (011 000, 24 dec).\n    CS3,\n    /// Class Selector 4 (100 000, 32 dec).\n    CS4,\n    /// Class Selector 5 (101 000, 40 dec).\n    CS5,\n    /// Class Selector 6 (110 000, 48 dec).\n    CS6,\n    /// Class Selector 7 (111 000, 56 dec).\n    CS7,\n    /// High Priority Expedited Forwarding (101 110, 46 dec).\n    EF,\n    /// Voice Admit (101 100, 44 dec).\n    VA,\n    /// Lower Effort (000 001, 1 dec).\n    LE,\n    /// Other DSCP value (not defined in the standard).\n    Other(u8),\n}\n\nimpl TypeOfService {\n    #[must_use]\n    pub fn dscp(&self) -> Dscp {\n        self.split().0\n    }\n    #[must_use]\n    pub fn ecn(&self) -> Ecn {\n        self.split().1\n    }\n    fn split(self) -> (Dscp, Ecn) {\n        let dscp = match (self.0 & 0xfc) >> 2 {\n            0 => Dscp::DF,\n            10 => Dscp::AF11,\n            12 => Dscp::AF12,\n            14 => Dscp::AF13,\n            18 => Dscp::AF21,\n            20 => Dscp::AF22,\n            22 => Dscp::AF23,\n            26 => Dscp::AF31,\n            28 => Dscp::AF32,\n            30 => Dscp::AF33,\n            34 => Dscp::AF41,\n            36 => Dscp::AF42,\n            38 => Dscp::AF43,\n            8 => Dscp::CS1,\n            16 => Dscp::CS2,\n            24 => Dscp::CS3,\n            32 => Dscp::CS4,\n            40 => Dscp::CS5,\n            48 => Dscp::CS6,\n            56 => Dscp::CS7,\n            46 => Dscp::EF,\n            44 => Dscp::VA,\n            1 => Dscp::LE,\n            n => Dscp::Other(n),\n        };\n        let ecn = match self.0 & 0x3 {\n            0 => Ecn::NotECT,\n            1 => Ecn::ECT1,\n            2 => Ecn::ECT0,\n            3 => Ecn::CE,\n            _ => unreachable!(),\n        };\n        (dscp, ecn)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::TypeOfService;\n    use test_case::test_case;\n\n    #[test_case(TypeOfService(0x0), Dscp::DF, Ecn::NotECT; \"BE, Not-ECT\")]\n    #[test_case(TypeOfService(0xe0), Dscp::CS7, Ecn::NotECT; \"CS7, Not-ECT\")]\n    #[test_case(TypeOfService(0xa), Dscp::Other(2), Ecn::ECT0; \"Other, ECT0\")]\n    #[test_case(TypeOfService(0x8b), Dscp::AF41, Ecn::CE; \"AF41, CE\")]\n    fn test_dscp_ecn(tos: TypeOfService, dscp: Dscp, ecn: Ecn) {\n        assert_eq!(tos.dscp(), dscp);\n        assert_eq!(tos.ecn(), ecn);\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_icmp.toml",
    "content": "name = \"IPv4/ICMP\"\ntarget = \"10.0.0.107\"\nprotocol = \"Icmp\"\nicmp_identifier = 1\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 30 }\n\n[[hops]]\nttl = 4\nresp = { tag = \"SingleHost\", addr = \"10.0.0.104\", rtt_ms = 40 }\n\n[[hops]]\nttl = 5\nresp = { tag = \"SingleHost\", addr = \"10.0.0.105\", rtt_ms = 50 }\n\n[[hops]]\nttl = 6\nresp = { tag = \"SingleHost\", addr = \"10.0.0.106\", rtt_ms = 60 }\n\n[[hops]]\nttl = 7\nresp = { tag = \"SingleHost\", addr = \"10.0.0.107\", rtt_ms = 70 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_icmp_gaps.toml",
    "content": "name = \"IPv4/ICMP with 9 hops, 2 of which do not respond\"\ntarget = \"10.0.0.109\"\nprotocol = \"Icmp\"\nicmp_identifier = 3\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 20 }\n\n[[hops]]\nttl = 2\nresp.tag = \"NoResponse\"\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 20 }\n\n[[hops]]\nttl = 4\nresp = { tag = \"SingleHost\", addr = \"10.0.0.104\", rtt_ms = 20 }\n\n[[hops]]\nttl = 5\nresp = { tag = \"SingleHost\", addr = \"10.0.0.105\", rtt_ms = 20 }\n\n[[hops]]\nttl = 6\nresp = { tag = \"SingleHost\", addr = \"10.0.0.106\", rtt_ms = 20 }\n\n[[hops]]\nttl = 7\nresp = { tag = \"SingleHost\", addr = \"10.0.0.107\", rtt_ms = 20 }\n\n[[hops]]\nttl = 8\nresp.tag = \"NoResponse\"\n\n[[hops]]\nttl = 9\nresp = { tag = \"SingleHost\", addr = \"10.0.0.109\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_icmp_min.toml",
    "content": "name = \"IPv4/ICMP with a minimum packet size\"\ntarget = \"10.0.0.103\"\nprotocol = \"Icmp\"\nicmp_identifier = 5\npacket_size = 28\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 30 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_icmp_ooo.toml",
    "content": "name = \"IPv4/ICMP with out of order responses\"\ntarget = \"10.0.0.105\"\nprotocol = \"Icmp\"\nicmp_identifier = 4\ngrace_duration = 300\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 20 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 15 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 10 }\n\n[[hops]]\nttl = 4\nresp = { tag = \"SingleHost\", addr = \"10.0.0.104\", rtt_ms = 5 }\n\n[[hops]]\nttl = 5\nresp = { tag = \"SingleHost\", addr = \"10.0.0.105\", rtt_ms = 0 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_icmp_pattern.toml",
    "content": "name = \"IPv4/ICMP with an alternative payload pattern (0xFF)\"\ntarget = \"10.0.0.103\"\nprotocol = \"Icmp\"\nicmp_identifier = 6\npayload_pattern = 255\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 30 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_icmp_quick.toml",
    "content": "name = \"IPv4/ICMP with min/max round duration 250ms and grace 50ms\"\ntarget = \"10.0.0.110\"\nprotocol = \"Icmp\"\nicmp_identifier = 7\nmin_round_duration = 250\nmax_round_duration = 250\ngrace_duration = 50\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 1 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 2 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 3 }\n\n[[hops]]\nttl = 4\nresp = { tag = \"SingleHost\", addr = \"10.0.0.104\", rtt_ms = 4 }\n\n[[hops]]\nttl = 5\nresp = { tag = \"SingleHost\", addr = \"10.0.0.105\", rtt_ms = 5 }\n\n[[hops]]\nttl = 6\nresp = { tag = \"SingleHost\", addr = \"10.0.0.106\", rtt_ms = 6 }\n\n[[hops]]\nttl = 7\nresp = { tag = \"SingleHost\", addr = \"10.0.0.107\", rtt_ms = 7 }\n\n[[hops]]\nttl = 8\nresp = { tag = \"SingleHost\", addr = \"10.0.0.108\", rtt_ms = 8 }\n\n[[hops]]\nttl = 9\nresp = { tag = \"SingleHost\", addr = \"10.0.0.109\", rtt_ms = 9 }\n\n[[hops]]\nttl = 10\nresp = { tag = \"SingleHost\", addr = \"10.0.0.110\", rtt_ms = 10 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_icmp_tos.toml",
    "content": "name = \"IPv4/ICMP with a TOS\"\ntarget = \"10.0.0.103\"\nprotocol = \"Icmp\"\nicmp_identifier = 9\ntos = 224\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 30 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_icmp_wrap.toml",
    "content": "name = \"IPv4/ICMP wrap sequence\"\ntarget = \"10.0.0.130\"\nrounds = 20\nprotocol = \"Icmp\"\nicmp_identifier = 8\ninitial_sequence = 64511\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 0 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 0 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 0 }\n\n[[hops]]\nttl = 4\nresp = { tag = \"SingleHost\", addr = \"10.0.0.104\", rtt_ms = 0 }\n\n[[hops]]\nttl = 5\nresp = { tag = \"SingleHost\", addr = \"10.0.0.105\", rtt_ms = 0 }\n\n[[hops]]\nttl = 6\nresp = { tag = \"SingleHost\", addr = \"10.0.0.106\", rtt_ms = 0 }\n\n[[hops]]\nttl = 7\nresp = { tag = \"SingleHost\", addr = \"10.0.0.107\", rtt_ms = 0 }\n\n[[hops]]\nttl = 8\nresp = { tag = \"SingleHost\", addr = \"10.0.0.108\", rtt_ms = 0 }\n\n[[hops]]\nttl = 9\nresp = { tag = \"SingleHost\", addr = \"10.0.0.109\", rtt_ms = 0 }\n\n[[hops]]\nttl = 10\nresp = { tag = \"SingleHost\", addr = \"10.0.0.110\", rtt_ms = 0 }\n\n[[hops]]\nttl = 11\nresp = { tag = \"SingleHost\", addr = \"10.0.0.111\", rtt_ms = 0 }\n\n[[hops]]\nttl = 12\nresp = { tag = \"SingleHost\", addr = \"10.0.0.112\", rtt_ms = 0 }\n\n[[hops]]\nttl = 13\nresp = { tag = \"SingleHost\", addr = \"10.0.0.113\", rtt_ms = 0 }\n\n[[hops]]\nttl = 14\nresp = { tag = \"SingleHost\", addr = \"10.0.0.114\", rtt_ms = 0 }\n\n[[hops]]\nttl = 15\nresp = { tag = \"SingleHost\", addr = \"10.0.0.115\", rtt_ms = 0 }\n\n[[hops]]\nttl = 16\nresp = { tag = \"SingleHost\", addr = \"10.0.0.116\", rtt_ms = 0 }\n\n[[hops]]\nttl = 17\nresp = { tag = \"SingleHost\", addr = \"10.0.0.117\", rtt_ms = 0 }\n\n[[hops]]\nttl = 18\nresp = { tag = \"SingleHost\", addr = \"10.0.0.118\", rtt_ms = 0 }\n\n[[hops]]\nttl = 19\nresp = { tag = \"SingleHost\", addr = \"10.0.0.119\", rtt_ms = 0 }\n\n[[hops]]\nttl = 20\nresp = { tag = \"SingleHost\", addr = \"10.0.0.120\", rtt_ms = 0 }\n\n[[hops]]\nttl = 21\nresp = { tag = \"SingleHost\", addr = \"10.0.0.121\", rtt_ms = 0 }\n\n[[hops]]\nttl = 22\nresp = { tag = \"SingleHost\", addr = \"10.0.0.122\", rtt_ms = 0 }\n\n[[hops]]\nttl = 23\nresp = { tag = \"SingleHost\", addr = \"10.0.0.123\", rtt_ms = 0 }\n\n[[hops]]\nttl = 24\nresp = { tag = \"SingleHost\", addr = \"10.0.0.124\", rtt_ms = 0 }\n\n[[hops]]\nttl = 25\nresp = { tag = \"SingleHost\", addr = \"10.0.0.125\", rtt_ms = 0 }\n\n[[hops]]\nttl = 26\nresp = { tag = \"SingleHost\", addr = \"10.0.0.126\", rtt_ms = 0 }\n\n[[hops]]\nttl = 27\nresp = { tag = \"SingleHost\", addr = \"10.0.0.127\", rtt_ms = 0 }\n\n[[hops]]\nttl = 28\nresp = { tag = \"SingleHost\", addr = \"10.0.0.128\", rtt_ms = 0 }\n\n[[hops]]\nttl = 29\nresp = { tag = \"SingleHost\", addr = \"10.0.0.129\", rtt_ms = 0 }\n\n[[hops]]\nttl = 30\nresp = { tag = \"SingleHost\", addr = \"10.0.0.130\", rtt_ms = 0 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_tcp_fixed_dest.toml",
    "content": "name = \"IPv4/TCP with a fixed dest port\"\ntarget = \"10.0.0.103\"\nprotocol = \"Tcp\"\nport_direction = { tag = \"FixedDest\", value = 80 }\ntos = 224\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_fixed_dest.toml",
    "content": "name = \"IPv4/UDP classic with a fixed dest port\"\ntarget = \"10.0.0.103\"\nprotocol = \"Udp\"\nport_direction = { tag = \"FixedDest\", value = 33434 }\nmultipath_strategy = \"Classic\"\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_fixed_src.toml",
    "content": "name = \"IPv4/UDP classic with a fixed src port\"\ntarget = \"10.0.0.103\"\nprotocol = \"Udp\"\nport_direction = { tag = \"FixedSrc\", value = 5000 }\nmultipath_strategy = \"Classic\"\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_privileged_tos.toml",
    "content": "name = \"IPv4/UDP classic privileged with a TOS\"\ntarget = \"10.0.0.103\"\nprotocol = \"Udp\"\nport_direction = { tag = \"FixedDest\", value = 33434 }\nmultipath_strategy = \"Classic\"\ntos = 224\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_unprivileged.toml",
    "content": "name = \"IPv4/UDP classic unprivileged\"\nprivilege_mode = \"Unprivileged\"\ntarget = \"10.0.0.103\"\nprotocol = \"Udp\"\nport_direction = { tag = \"FixedDest\", value = 33434 }\nmultipath_strategy = \"Classic\"\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_udp_classic_unprivileged_tos.toml",
    "content": "name = \"IPv4/UDP classic unprivileged with a TOS\"\nprivilege_mode = \"Unprivileged\"\ntarget = \"10.0.0.103\"\nprotocol = \"Udp\"\nport_direction = { tag = \"FixedDest\", value = 33434 }\nmultipath_strategy = \"Classic\"\ntos = 224\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_udp_dublin_fixed_both.toml",
    "content": "name = \"IPv4/UDP Dublin with a fixed src and dest port\"\ntarget = \"10.0.0.103\"\nprotocol = \"Udp\"\nmultipath_strategy = \"Dublin\"\nport_direction = { tag = \"FixedBoth\", value = { src = 5000, dest = 33434 } }\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv4_udp_paris_fixed_both.toml",
    "content": "name = \"IPv4/UDP Paris with a fixed src and dest port\"\ntarget = \"10.0.0.103\"\nprotocol = \"Udp\"\nmultipath_strategy = \"Paris\"\nport_direction = { tag = \"FixedBoth\", value = { src = 5000, dest = 33434 } }\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"10.0.0.101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"10.0.0.102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"10.0.0.103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_icmp.toml",
    "content": "name = \"IPv6/ICMP\"\ntarget = \"fd00:10::107\"\nprotocol = \"Icmp\"\nicmp_identifier = 1\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 30 }\n\n[[hops]]\nttl = 4\nresp = { tag = \"SingleHost\", addr = \"fd00:10::104\", rtt_ms = 40 }\n\n[[hops]]\nttl = 5\nresp = { tag = \"SingleHost\", addr = \"fd00:10::105\", rtt_ms = 50 }\n\n[[hops]]\nttl = 6\nresp = { tag = \"SingleHost\", addr = \"fd00:10::106\", rtt_ms = 60 }\n\n[[hops]]\nttl = 7\nresp = { tag = \"SingleHost\", addr = \"fd00:10::107\", rtt_ms = 70 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_icmp_min.toml",
    "content": "name = \"IPv6/ICMP with a minimum packet size\"\ntarget = \"fd00:10::103\"\nprotocol = \"Icmp\"\nicmp_identifier = 5\npacket_size = 48\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 30 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_icmp_pattern.toml",
    "content": "name = \"IPv6/ICMP with an alternative payload pattern (0xFF)\"\ntarget = \"fd00:10::103\"\nprotocol = \"Icmp\"\nicmp_identifier = 6\npayload_pattern = 255\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 30 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_tcp_fixed_dest.toml",
    "content": "name = \"IPv6/TCP with a fixed dest port\"\ntarget = \"fd00:10::103\"\nprotocol = \"Tcp\"\nport_direction = { tag = \"FixedDest\", value = 80 }\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_udp_classic_fixed_dest.toml",
    "content": "name = \"IPv6/UDP classic with a fixed dest port\"\ntarget = \"fd00:10::103\"\nprotocol = \"Udp\"\nport_direction = { tag = \"FixedDest\", value = 33434 }\nmultipath_strategy = \"Classic\"\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_udp_classic_fixed_src.toml",
    "content": "name = \"IPv6/UDP classic with a fixed src port\"\ntarget = \"fd00:10::103\"\nprotocol = \"Udp\"\nport_direction = { tag = \"FixedSrc\", value = 5000 }\nmultipath_strategy = \"Classic\"\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_udp_classic_unprivileged.toml",
    "content": "name = \"IPv6/UDP classic unprivileged\"\nprivilege_mode = \"Unprivileged\"\ntarget = \"fd00:10::103\"\nprotocol = \"Udp\"\nport_direction = { tag = \"FixedDest\", value = 33434 }\nmultipath_strategy = \"Classic\"\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_udp_classic_unprivileged_tos.toml",
    "content": "name = \"IPv6/UDP classic unprivileged with a TOS\"\nprivilege_mode = \"Unprivileged\"\ntarget = \"fd00:10::103\"\nprotocol = \"Udp\"\nport_direction = { tag = \"FixedDest\", value = 33434 }\nmultipath_strategy = \"Classic\"\ntos = 224\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_udp_dublin_fixed_both.toml",
    "content": "name = \"IPv6/UDP Dublin with a fixed src and dest port\"\ntarget = \"fd00:10::103\"\nprotocol = \"Udp\"\nmultipath_strategy = \"Dublin\"\nport_direction = { tag = \"FixedBoth\", value = { src = 5000, dest = 33434 } }\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/simulation/ipv6_udp_paris_fixed_both.toml",
    "content": "name = \"IPv6/UDP Paris with a fixed src and dest port\"\ntarget = \"fd00:10::103\"\nprotocol = \"Udp\"\nmultipath_strategy = \"Paris\"\nport_direction = { tag = \"FixedBoth\", value = { src = 5000, dest = 33434 } }\n\n[[hops]]\nttl = 1\nresp = { tag = \"SingleHost\", addr = \"fd00:10::101\", rtt_ms = 10 }\n\n[[hops]]\nttl = 2\nresp = { tag = \"SingleHost\", addr = \"fd00:10::102\", rtt_ms = 20 }\n\n[[hops]]\nttl = 3\nresp = { tag = \"SingleHost\", addr = \"fd00:10::103\", rtt_ms = 20 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/state/all_status.toml",
    "content": "largest_ttl = 1\n\n[[rounds]]\nprobes = [\n  \"1 A 300 10.1.0.2 0 12340 80 0 0 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 C 700 10.1.0.2 0 12340 80 0 0 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 N 300 10.1.0.2 0 12340 80 0 0 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 S 300 10.1.0.2 0 12340 80 0 0 0\",\n]\n\n[[expected.hops]]\nttl = 1\ntotal_sent = 2\ntotal_recv = 1\nloss_pct = 50\nbest_ms = 700\nworst_ms = 700\navg_ms = 700\nsamples = [700.0, 0.0]\nlast_ms = 700\nlast_sequence = 0\nlast_src = 12340\nlast_dest = 80\nlast_nat_status = \"no_nat\"\ntos = 0\naddrs = { \"10.1.0.2\" = 1 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/state/floss_bloss.toml",
    "content": "largest_ttl = 3\n\n[[rounds]]\nprobes = [\n  \"1 C 333 10.1.0.1 0 12340 80 0 0 0\",\n  \"2 C 777 10.1.0.2 1 12340 80 0 0 0\",\n  \"3 C 778 10.1.0.3 2 12340 80 0 0 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 C 333 10.1.0.1 3 12340 80 0 0 0\",\n  \"2 A 777 10.1.0.2 4 12340 80 0 0 0\",\n  \"3 A 778 10.1.0.3 5 12340 80 0 0 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 C 333 10.1.0.1 6 12340 80 0 0 0\",\n  \"2 C 777 10.1.0.2 7 12340 80 0 0 0\",\n  \"3 C 778 10.1.0.3 8 12340 80 0 0 0\",\n]\n\n[[expected.hops]]\nttl = 1\ntotal_sent = 3\ntotal_recv = 3\ntotal_forward_loss = 0\ntotal_backward_loss = 0\n\n[[expected.hops]]\nttl = 2\ntotal_sent = 3\ntotal_recv = 2\ntotal_forward_loss = 1\ntotal_backward_loss = 0\n\n[[expected.hops]]\nttl = 3\ntotal_sent = 3\ntotal_recv = 2\ntotal_forward_loss = 0\ntotal_backward_loss = 1\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/state/full_completed.toml",
    "content": "largest_ttl = 3\n\n[[rounds]]\nprobes = [\n  \"1 C 333 10.1.0.1 0 12340 80 0 0 0\",\n  \"2 C 777 10.1.0.2 1 12340 80 0 0 0\",\n  \"3 C 778 10.1.0.3 2 12340 80 0 0 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 C 123 10.1.0.1 3 12340 80 0 0 0\",\n  \"2 C 788 10.1.0.2 4 12340 80 0 0 0\",\n  \"3 C 789 10.1.0.3 5 12340 80 0 0 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 C 123 10.1.0.1 6 12340 80 0 0 0\",\n  \"2 C 780 10.1.0.2 7 12340 80 0 0 0\",\n  \"3 C 781 10.1.0.3 8 12340 80 0 0 0\",\n]\n\n[[expected.hops]]\nttl = 1\ntotal_sent = 3\ntotal_recv = 3\nloss_pct = 0\nbest_ms = 123\nworst_ms = 333\navg_ms = 193\njitter = 0\njavg = 181.0\njinta = 488.642578125\njmax = 333\nlast_nat_status = \"no_nat\"\nsamples = [123, 123, 333]\nlast_ms = 123\nlast_sequence = 6\nlast_src = 12340\nlast_dest = 80\ntos = 0\naddrs = { \"10.1.0.1\" = 3 }\n\n[[expected.hops]]\nttl = 2\ntotal_sent = 3\ntotal_recv = 3\nloss_pct = 0\nbest_ms = 777\nworst_ms = 788\navg_ms = 781.6666666666665\njitter = 8\njavg = 265.33333333333337\njinta = 699.814453125\njmax = 777.0\nlast_nat_status = \"no_nat\"\nsamples = [780, 788, 777]\nlast_ms = 780\nlast_sequence = 7\nlast_src = 12340\nlast_dest = 80\ntos = 0\naddrs = { \"10.1.0.2\" = 3 }\n\n[[expected.hops]]\nttl = 3\ntotal_sent = 3\ntotal_recv = 3\nloss_pct = 0\nbest_ms = 778\nworst_ms = 789\navg_ms = 782.6666666666666\njitter = 8\njavg = 265.66666666666663\njinta = 700.693359375\njmax = 778.0\nlast_nat_status = \"no_nat\"\nsamples = [781, 789, 778]\nlast_ms = 781\nlast_sequence = 8\nlast_src = 12340\nlast_dest = 80\ntos = 0\naddrs = { \"10.1.0.3\" = 3 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/state/full_mixed.toml",
    "content": "largest_ttl = 3\n\n[[rounds]]\nprobes = [\n  \"1 C 10 10.0.0.1 0 12345 80 0 0 0\",\n  \"2 A 12 10.0.0.2 1 12345 80 0 0 0\",\n  \"3 C 11 10.0.0.3 2 12345 80 0 0 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 C 101 10.0.0.1 3 12345 80 0 0 0\",\n  \"2 A 121 10.0.0.2 4 12345 80 0 0 0\",\n  \"3 C 111 10.0.0.4 5 12345 80 0 0 0\",\n]\n\n[[expected.hops]]\nttl = 1\ntotal_sent = 2\ntotal_recv = 2\nloss_pct = 0\nbest_ms = 10\nworst_ms = 101\navg_ms = 55.5\njitter = 91\njavg = 50.5\njinta = 99.40625\njmax = 91\nlast_nat_status = \"no_nat\"\nsamples = [101, 10]\nlast_ms = 101\nlast_src = 12345\nlast_dest = 80\nlast_sequence = 3\ntos = 0\naddrs = { \"10.0.0.1\" = 2 }\n\n[[expected.hops]]\nttl = 2\ntotal_sent = 2\ntotal_recv = 0\nloss_pct = 100\navg_ms = 0\njavg = 0\njinta = 0\nlast_nat_status = \"none\"\nsamples = [0, 0]\nlast_src = 12345\nlast_dest = 80\nlast_sequence = 4\n\n[[expected.hops]]\nttl = 3\ntotal_sent = 2\ntotal_recv = 2\nloss_pct = 0\nbest_ms = 11\nworst_ms = 111\navg_ms = 61\njitter = 100\njavg = 55.5\njinta = 109.34375\njmax = 100\nlast_nat_status = \"no_nat\"\nsamples = [111, 11]\nlast_ms = 111\nlast_src = 12345\nlast_dest = 80\nlast_sequence = 5\ntos = 0\naddrs = { \"10.0.0.3\" = 1, \"10.0.0.4\" = 1 }\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/state/minimal.toml",
    "content": "largest_ttl = 3\n\n[[rounds]]\nprobes = [\n  \"1 C 333 10.1.0.1 0 12340 80 0 0 0\",\n  \"2 C 777 10.1.0.2 1 12340 80 0 0 0\",\n  \"3 C 778 10.1.0.3 2 12340 80 0 0 0\",\n]\n\n[[expected.hops]]\nttl = 1\n\n[[expected.hops]]\nttl = 2\n\n[[expected.hops]]\nttl = 3\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/state/nat.toml",
    "content": "largest_ttl = 3\n\n[[rounds]]\nprobes = [\n  \"1 C 333 10.1.0.1 0 12340 80 43012 43012 0\",\n  \"2 C 777 10.1.0.2 1 12340 80 20544 20544 0\",\n  \"3 C 778 10.1.0.3 2 12340 80 20544 20544 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 C 123 10.1.0.1 3 12340 80 43012 43012 0\",\n  \"2 C 788 10.1.0.2 4 12340 80 20544 20544 0\",\n  \"3 C 789 10.1.0.3 5 12340 80 20544 20544 0\",\n]\n\n[[rounds]]\nprobes = [\n  \"1 C 123 10.1.0.1 6 12340 80 43012 43012 0\",\n  \"2 C 780 10.1.0.2 7 12340 80 20544 20544 0\",\n  \"3 C 781 10.1.0.3 8 12340 80 20544 20544 0\",\n]\n\n[[expected.hops]]\nttl = 1\nlast_nat_status = \"no_nat\"\n\n[[expected.hops]]\nttl = 2\nlast_nat_status = \"nat\"\n\n[[expected.hops]]\nttl = 3\nlast_nat_status = \"no_nat\"\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/state/no_latency.toml",
    "content": "largest_ttl = 1\n\n[[rounds]]\nprobes = [\"1 C 0 127.0.0.1 0 80 80 0 0 0\"]\n\n[[rounds]]\nprobes = [\"1 C 0 127.0.0.1 0 80 80 0 0 0\"]\n\n[[rounds]]\nprobes = [\"1 C 0 127.0.0.1 0 80 80 0 0 0\"]\n\n[[rounds]]\nprobes = [\"1 C 0 127.0.0.1 0 80 80 0 0 0\"]\n\n[[expected.hops]]\nttl = 1\nloss_pct = 0\nlast_ms = 0.0\nbest_ms = 0\nworst_ms = 0\navg_ms = 0\nsamples = [0, 0, 0, 0]\njitter = 0\njavg = 0.0\njmax = 0.0\njinta = 0.0\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/state/non_default_minimum_ttl.toml",
    "content": "largest_ttl = 5\n\n[[rounds]]\nprobes = [\n  \"4 A 333 10.1.0.1 0 12340 80 0 0 0\",\n  \"5 A 777 10.1.0.2 1 12340 80 0 0 0\",\n]\n\n[[expected.hops]]\nttl = 4\ntotal_sent = 1\ntotal_recv = 0\ntotal_forward_loss = 1\ntotal_backward_loss = 0\n\n[[expected.hops]]\nttl = 5\ntotal_sent = 1\ntotal_recv = 0\ntotal_forward_loss = 0\ntotal_backward_loss = 1\n"
  },
  {
    "path": "crates/trippy-core/tests/resources/state/tos.toml",
    "content": "largest_ttl = 3\n\n[[rounds]]\nprobes = [\n  \"1 C 333 10.1.0.1 0 12340 80 0 0 224\",\n  \"2 A 777 10.1.0.2 1 12340 80 0 0 0\",\n]\n\n[[expected.hops]]\nttl = 1\ntos = 224\n\n[[expected.hops]]\nttl = 2\n"
  },
  {
    "path": "crates/trippy-core/tests/sim/main.rs",
    "content": "#![cfg(all(\n    feature = \"sim-tests\",\n    any(target_os = \"linux\", target_os = \"macos\", target_os = \"windows\")\n))]\nmod network;\nmod simulation;\nmod tests;\nmod tracer;\nmod tun_device;\n"
  },
  {
    "path": "crates/trippy-core/tests/sim/network/ipv4.rs",
    "content": "use crate::simulation::{Protocol, Response, Simulation, SingleHost};\nuse std::net::{IpAddr, Ipv4Addr};\nuse tracing::{debug, info};\nuse trippy_packet::IpProtocol;\nuse trippy_packet::checksum::{icmp_ipv4_checksum, ipv4_header_checksum, tcp_ipv4_checksum};\nuse trippy_packet::icmpv4::destination_unreachable::DestinationUnreachablePacket;\nuse trippy_packet::icmpv4::echo_reply::EchoReplyPacket;\nuse trippy_packet::icmpv4::echo_request::EchoRequestPacket;\nuse trippy_packet::icmpv4::time_exceeded::TimeExceededPacket;\nuse trippy_packet::icmpv4::{IcmpCode, IcmpType};\nuse trippy_packet::ipv4::Ipv4Packet;\nuse trippy_packet::tcp::TcpPacket;\nuse trippy_packet::udp::UdpPacket;\n\n#[expect(clippy::too_many_lines)]\npub fn process(sim: &Simulation, packet_buf: &[u8]) -> anyhow::Result<Option<(u16, Vec<u8>)>> {\n    let ipv4 = Ipv4Packet::new_view(packet_buf)?;\n    debug!(\"read: {:?}\", ipv4);\n    let orig_datagram_length = usize::from(ipv4.get_header_length() * 4) + 8;\n    match (ipv4.get_protocol(), sim.protocol) {\n        (IpProtocol::Icmp, Protocol::Icmp) => {\n            let echo_request = EchoRequestPacket::new_view(ipv4.payload())?;\n            if echo_request.get_identifier() != sim.icmp_identifier {\n                debug!(\n                    \"skipping EchoRequest with unexpected id (exp={} act={}))\",\n                    echo_request.get_identifier(),\n                    sim.icmp_identifier\n                );\n                return Ok(None);\n            }\n            debug!(\"payload in: {:?}\", echo_request);\n            info!(\n                \"received EchoRequest with ttl={} id={} seq={}\",\n                ipv4.get_ttl(),\n                echo_request.get_identifier(),\n                echo_request.get_sequence()\n            );\n        }\n        (IpProtocol::Udp, Protocol::Udp) => {\n            let udp = UdpPacket::new_view(ipv4.payload())?;\n            debug!(\"payload in: {:?}\", udp);\n            info!(\n                \"received UdpPacket with ttl={} src={} dest={}\",\n                ipv4.get_ttl(),\n                udp.get_source(),\n                udp.get_destination()\n            );\n        }\n        (IpProtocol::Tcp, Protocol::Tcp) => {\n            let tcp = TcpPacket::new_view(ipv4.payload())?;\n            debug!(\"payload in: {:?}\", tcp);\n            info!(\n                \"received TcpPacket with ttl={} src={} dest={}\",\n                ipv4.get_ttl(),\n                tcp.get_source(),\n                tcp.get_destination()\n            );\n        }\n        _ => {\n            return Ok(None);\n        }\n    }\n\n    // if the ttl is greater than the largest ttl in our sim we will reply as the last node in\n    // the sim\n    let index = std::cmp::min(usize::from(ipv4.get_ttl()) - 1, sim.hops.len() - 1);\n    let (reply_addr, reply_delay_ms) = match sim.hops[index].resp {\n        Response::NoResponse => {\n            return Ok(None);\n        }\n        Response::SingleHost(SingleHost {\n            addr: IpAddr::V4(addr),\n            rtt_ms,\n        }) => (addr, rtt_ms),\n        Response::SingleHost(SingleHost { addr, .. }) => anyhow::bail!(\n            \"invalid simulation hop {}: expected IPv4 responder, got {}\",\n            index + 1,\n            addr\n        ),\n    };\n\n    // decide what response to send\n    let (protocol, payload) = if IpAddr::V4(reply_addr) == sim.target {\n        match sim.protocol {\n            Protocol::Icmp => {\n                info!(\n                    \"sending ICMP EchoReply from {} to {} for ttl {} after {}ms delay\",\n                    reply_addr,\n                    ipv4.get_source(),\n                    ipv4.get_ttl(),\n                    reply_delay_ms,\n                );\n                let echo_request = EchoRequestPacket::new_view(ipv4.payload())?;\n                let mut packet_buf = vec![0_u8; EchoReplyPacket::minimum_packet_size()];\n                let packet = make_echo_reply(\n                    &mut packet_buf,\n                    sim.icmp_identifier,\n                    echo_request.get_sequence(),\n                )?;\n                debug!(\"payload out: {:?}\", packet);\n                (IpProtocol::Icmp, packet_buf)\n            }\n            Protocol::Udp => {\n                info!(\n                    \"sending ICMP DestinationUnreachable from {} to {} for ttl {} after {}ms delay\",\n                    reply_addr,\n                    ipv4.get_source(),\n                    ipv4.get_ttl(),\n                    reply_delay_ms,\n                );\n                let length =\n                    DestinationUnreachablePacket::minimum_packet_size() + orig_datagram_length;\n                let mut packet_buf = vec![0_u8; length];\n                let packet = make_destination_unreachable(\n                    &mut packet_buf,\n                    &ipv4.packet()[..orig_datagram_length],\n                )?;\n                debug!(\"payload out: {:?}\", packet);\n                (IpProtocol::Icmp, packet_buf)\n            }\n            Protocol::Tcp => {\n                info!(\n                    \"sending TCP syn+ack from {} to {} for ttl {} after {}ms delay\",\n                    reply_addr,\n                    ipv4.get_source(),\n                    ipv4.get_ttl(),\n                    reply_delay_ms,\n                );\n                let tcp_in = TcpPacket::new_view(ipv4.payload())?;\n                let mut packet_buf = vec![0_u8; TcpPacket::minimum_packet_size()];\n                let packet = make_tcp_syn_ack(&mut packet_buf, &ipv4, &tcp_in)?;\n                debug!(\"payload out: {:?}\", packet);\n                (IpProtocol::Tcp, packet_buf)\n            }\n        }\n    } else {\n        info!(\n            \"sending ICMP TimeExceeded from {} to {} for ttl {} after {}ms delay\",\n            reply_addr,\n            ipv4.get_source(),\n            ipv4.get_ttl(),\n            reply_delay_ms,\n        );\n        let length = TimeExceededPacket::minimum_packet_size() + orig_datagram_length;\n        let mut packet_buf = vec![0_u8; length];\n        let packet = make_time_exceeded(&mut packet_buf, &ipv4.packet()[..orig_datagram_length])?;\n        debug!(\"payload out: {:?}\", packet);\n        (IpProtocol::Icmp, packet_buf)\n    };\n\n    let ipv4_length = Ipv4Packet::minimum_packet_size() + payload.len();\n    let mut ipv4_buf = vec![0_u8; ipv4_length];\n    make_ip(\n        &mut ipv4_buf,\n        reply_addr,\n        ipv4.get_source(),\n        protocol,\n        &payload,\n    )?;\n    Ok(Some((reply_delay_ms, ipv4_buf)))\n}\n\nfn make_time_exceeded<'a>(\n    buf: &'a mut [u8],\n    payload: &[u8],\n) -> anyhow::Result<TimeExceededPacket<'a>> {\n    let mut packet = TimeExceededPacket::new(buf)?;\n    packet.set_icmp_type(IcmpType::TimeExceeded);\n    packet.set_icmp_code(IcmpCode(0));\n    packet.set_payload(payload);\n    packet.set_checksum(icmp_ipv4_checksum(packet.packet()));\n    Ok(packet)\n}\n\nfn make_echo_reply(\n    buf: &mut [u8],\n    icmp_identifier: u16,\n    sequence: u16,\n) -> anyhow::Result<EchoReplyPacket<'_>> {\n    let mut packet = EchoReplyPacket::new(buf)?;\n    packet.set_icmp_type(IcmpType::EchoReply);\n    packet.set_icmp_code(IcmpCode(0));\n    packet.set_identifier(icmp_identifier);\n    packet.set_sequence(sequence);\n    packet.set_checksum(icmp_ipv4_checksum(packet.packet()));\n    Ok(packet)\n}\n\nfn make_destination_unreachable<'a>(\n    buf: &'a mut [u8],\n    payload: &[u8],\n) -> anyhow::Result<DestinationUnreachablePacket<'a>> {\n    let mut packet = DestinationUnreachablePacket::new(buf)?;\n    packet.set_icmp_type(IcmpType::DestinationUnreachable);\n    packet.set_icmp_code(IcmpCode(3));\n    packet.set_payload(payload);\n    packet.set_checksum(icmp_ipv4_checksum(packet.packet()));\n    Ok(packet)\n}\n\nfn make_tcp_syn_ack<'a>(\n    buf: &'a mut [u8],\n    ipv4: &Ipv4Packet<'_>,\n    tcp_in: &TcpPacket<'_>,\n) -> anyhow::Result<TcpPacket<'a>> {\n    let mut packet = TcpPacket::new(buf)?;\n    packet.set_data_offset(5);\n    packet.set_source(tcp_in.get_destination());\n    packet.set_destination(tcp_in.get_source());\n    packet.set_sequence(0);\n    packet.set_acknowledgement(tcp_in.get_sequence() + 1);\n    packet.set_flags(0b0001_0010);\n    packet.set_window_size(0xFFFF);\n    packet.set_checksum(tcp_ipv4_checksum(\n        packet.packet(),\n        ipv4.get_destination(),\n        ipv4.get_source(),\n    ));\n    Ok(packet)\n}\n\nfn make_ip<'a>(\n    buf: &'a mut [u8],\n    source: Ipv4Addr,\n    destination: Ipv4Addr,\n    protocol: IpProtocol,\n    payload: &[u8],\n) -> anyhow::Result<Ipv4Packet<'a>> {\n    let ipv4_total_length = buf.len();\n    let mut packet = Ipv4Packet::new(buf)?;\n    packet.set_version(4);\n    packet.set_header_length(5);\n    packet.set_protocol(protocol);\n    packet.set_ttl(64);\n    packet.set_source(source);\n    packet.set_destination(destination);\n    packet.set_total_length(u16::try_from(ipv4_total_length)?);\n    packet.set_checksum(ipv4_header_checksum(\n        &packet.packet()[..Ipv4Packet::minimum_packet_size()],\n    ));\n    packet.set_payload(payload);\n    Ok(packet)\n}\n"
  },
  {
    "path": "crates/trippy-core/tests/sim/network/ipv6.rs",
    "content": "use crate::simulation::{Protocol, Response, Simulation, SingleHost};\nuse std::net::{IpAddr, Ipv6Addr};\nuse tracing::{debug, info};\nuse trippy_packet::IpProtocol;\nuse trippy_packet::checksum::{icmp_ipv6_checksum, tcp_ipv6_checksum};\nuse trippy_packet::icmpv6::destination_unreachable::DestinationUnreachablePacket;\nuse trippy_packet::icmpv6::echo_reply::EchoReplyPacket;\nuse trippy_packet::icmpv6::echo_request::EchoRequestPacket;\nuse trippy_packet::icmpv6::time_exceeded::TimeExceededPacket;\nuse trippy_packet::icmpv6::{IcmpCode, IcmpType};\nuse trippy_packet::ipv6::Ipv6Packet;\nuse trippy_packet::tcp::TcpPacket;\nuse trippy_packet::udp::UdpPacket;\n\n#[expect(clippy::too_many_lines)]\npub fn process(sim: &Simulation, packet_buf: &[u8]) -> anyhow::Result<Option<(u16, Vec<u8>)>> {\n    let ipv6 = Ipv6Packet::new_view(packet_buf)?;\n    debug!(\"read: {:?}\", ipv6);\n    let orig_datagram_length = Ipv6Packet::minimum_packet_size() + ipv6.payload().len();\n    match (ipv6.get_next_header(), sim.protocol) {\n        (IpProtocol::IcmpV6, Protocol::Icmp) => {\n            let echo_request = EchoRequestPacket::new_view(ipv6.payload())?;\n            if echo_request.get_identifier() != sim.icmp_identifier {\n                debug!(\n                    \"skipping EchoRequest with unexpected id (exp={} act={}))\",\n                    echo_request.get_identifier(),\n                    sim.icmp_identifier\n                );\n                return Ok(None);\n            }\n            debug!(\"payload in: {:?}\", echo_request);\n            info!(\n                \"received EchoRequest with hop_limit={} id={} seq={}\",\n                ipv6.get_hop_limit(),\n                echo_request.get_identifier(),\n                echo_request.get_sequence()\n            );\n        }\n        (IpProtocol::Udp, Protocol::Udp) => {\n            let udp = UdpPacket::new_view(ipv6.payload())?;\n            debug!(\"payload in: {:?}\", udp);\n            info!(\n                \"received UdpPacket with hop_limit={} src={} dest={}\",\n                ipv6.get_hop_limit(),\n                udp.get_source(),\n                udp.get_destination()\n            );\n        }\n        (IpProtocol::Tcp, Protocol::Tcp) => {\n            let tcp = TcpPacket::new_view(ipv6.payload())?;\n            debug!(\"payload in: {:?}\", tcp);\n            info!(\n                \"received TcpPacket with hop_limit={} src={} dest={}\",\n                ipv6.get_hop_limit(),\n                tcp.get_source(),\n                tcp.get_destination()\n            );\n        }\n        _ => {\n            return Ok(None);\n        }\n    }\n\n    // if the hop limit is greater than the largest ttl in our sim we will reply as the last node in\n    // the sim\n    let index = std::cmp::min(usize::from(ipv6.get_hop_limit()) - 1, sim.hops.len() - 1);\n    let (reply_addr, reply_delay_ms) = match sim.hops[index].resp {\n        Response::NoResponse => {\n            return Ok(None);\n        }\n        Response::SingleHost(SingleHost {\n            addr: IpAddr::V6(addr),\n            rtt_ms,\n        }) => (addr, rtt_ms),\n        Response::SingleHost(SingleHost { addr, .. }) => anyhow::bail!(\n            \"invalid simulation hop {}: expected IPv6 responder, got {}\",\n            index + 1,\n            addr\n        ),\n    };\n\n    // decide what response to send\n    let (next_header, payload) = if IpAddr::V6(reply_addr) == sim.target {\n        match sim.protocol {\n            Protocol::Icmp => {\n                info!(\n                    \"sending ICMPv6 EchoReply from {} to {} for hop_limit {} after {}ms delay\",\n                    reply_addr,\n                    ipv6.get_source_address(),\n                    ipv6.get_hop_limit(),\n                    reply_delay_ms,\n                );\n                let echo_request = EchoRequestPacket::new_view(ipv6.payload())?;\n                let mut packet_buf = vec![0_u8; EchoReplyPacket::minimum_packet_size()];\n                let packet = make_echo_reply(\n                    &mut packet_buf,\n                    reply_addr,\n                    ipv6.get_source_address(),\n                    sim.icmp_identifier,\n                    echo_request.get_sequence(),\n                )?;\n                debug!(\"payload out: {:?}\", packet);\n                (IpProtocol::IcmpV6, packet_buf)\n            }\n            Protocol::Udp => {\n                info!(\n                    \"sending ICMPv6 DestinationUnreachable from {} to {} for hop_limit {} after {}ms delay\",\n                    reply_addr,\n                    ipv6.get_source_address(),\n                    ipv6.get_hop_limit(),\n                    reply_delay_ms,\n                );\n                let length =\n                    DestinationUnreachablePacket::minimum_packet_size() + orig_datagram_length;\n                let mut packet_buf = vec![0_u8; length];\n                let packet = make_destination_unreachable(\n                    &mut packet_buf,\n                    reply_addr,\n                    ipv6.get_source_address(),\n                    &ipv6.packet()[..orig_datagram_length],\n                )?;\n                debug!(\"payload out: {:?}\", packet);\n                (IpProtocol::IcmpV6, packet_buf)\n            }\n            Protocol::Tcp => {\n                info!(\n                    \"sending TCP syn+ack from {} to {} for hop_limit {} after {}ms delay\",\n                    reply_addr,\n                    ipv6.get_source_address(),\n                    ipv6.get_hop_limit(),\n                    reply_delay_ms,\n                );\n                let tcp_in = TcpPacket::new_view(ipv6.payload())?;\n                let mut packet_buf = vec![0_u8; TcpPacket::minimum_packet_size()];\n                let packet = make_tcp_syn_ack(&mut packet_buf, &ipv6, &tcp_in)?;\n                debug!(\"payload out: {:?}\", packet);\n                (IpProtocol::Tcp, packet_buf)\n            }\n        }\n    } else {\n        info!(\n            \"sending ICMPv6 TimeExceeded from {} to {} for hop_limit {} after {}ms delay\",\n            reply_addr,\n            ipv6.get_source_address(),\n            ipv6.get_hop_limit(),\n            reply_delay_ms,\n        );\n        let length = TimeExceededPacket::minimum_packet_size() + orig_datagram_length;\n        let mut packet_buf = vec![0_u8; length];\n        let packet = make_time_exceeded(\n            &mut packet_buf,\n            reply_addr,\n            ipv6.get_source_address(),\n            &ipv6.packet()[..orig_datagram_length],\n        )?;\n        debug!(\"payload out: {:?}\", packet);\n        (IpProtocol::IcmpV6, packet_buf)\n    };\n\n    let ipv6_length = Ipv6Packet::minimum_packet_size() + payload.len();\n    let mut ipv6_buf = vec![0_u8; ipv6_length];\n    make_ip(\n        &mut ipv6_buf,\n        reply_addr,\n        ipv6.get_source_address(),\n        next_header,\n        &payload,\n    )?;\n    Ok(Some((reply_delay_ms, ipv6_buf)))\n}\n\nfn make_time_exceeded<'a>(\n    buf: &'a mut [u8],\n    source: Ipv6Addr,\n    destination: Ipv6Addr,\n    payload: &[u8],\n) -> anyhow::Result<TimeExceededPacket<'a>> {\n    let mut packet = TimeExceededPacket::new(buf)?;\n    packet.set_icmp_type(IcmpType::TimeExceeded);\n    packet.set_icmp_code(IcmpCode(0));\n    packet.set_payload(payload);\n    packet.set_checksum(icmp_ipv6_checksum(packet.packet(), source, destination));\n    Ok(packet)\n}\n\nfn make_echo_reply(\n    buf: &mut [u8],\n    source: Ipv6Addr,\n    destination: Ipv6Addr,\n    icmp_identifier: u16,\n    sequence: u16,\n) -> anyhow::Result<EchoReplyPacket<'_>> {\n    let mut packet = EchoReplyPacket::new(buf)?;\n    packet.set_icmp_type(IcmpType::EchoReply);\n    packet.set_icmp_code(IcmpCode(0));\n    packet.set_identifier(icmp_identifier);\n    packet.set_sequence(sequence);\n    packet.set_checksum(icmp_ipv6_checksum(packet.packet(), source, destination));\n    Ok(packet)\n}\n\nfn make_destination_unreachable<'a>(\n    buf: &'a mut [u8],\n    source: Ipv6Addr,\n    destination: Ipv6Addr,\n    payload: &[u8],\n) -> anyhow::Result<DestinationUnreachablePacket<'a>> {\n    let mut packet = DestinationUnreachablePacket::new(buf)?;\n    packet.set_icmp_type(IcmpType::DestinationUnreachable);\n    packet.set_icmp_code(IcmpCode(4));\n    packet.set_payload(payload);\n    packet.set_checksum(icmp_ipv6_checksum(packet.packet(), source, destination));\n    Ok(packet)\n}\n\nfn make_tcp_syn_ack<'a>(\n    buf: &'a mut [u8],\n    ipv6: &Ipv6Packet<'_>,\n    tcp_in: &TcpPacket<'_>,\n) -> anyhow::Result<TcpPacket<'a>> {\n    let mut packet = TcpPacket::new(buf)?;\n    packet.set_data_offset(5);\n    packet.set_source(tcp_in.get_destination());\n    packet.set_destination(tcp_in.get_source());\n    packet.set_sequence(0);\n    packet.set_acknowledgement(tcp_in.get_sequence() + 1);\n    packet.set_flags(0b0001_0010);\n    packet.set_window_size(0xFFFF);\n    packet.set_checksum(tcp_ipv6_checksum(\n        packet.packet(),\n        ipv6.get_destination_address(),\n        ipv6.get_source_address(),\n    ));\n    Ok(packet)\n}\n\nfn make_ip<'a>(\n    buf: &'a mut [u8],\n    source: Ipv6Addr,\n    destination: Ipv6Addr,\n    next_header: IpProtocol,\n    payload: &[u8],\n) -> anyhow::Result<Ipv6Packet<'a>> {\n    let mut packet = Ipv6Packet::new(buf)?;\n    packet.set_version(6);\n    packet.set_traffic_class(0);\n    packet.set_flow_label(0);\n    packet.set_payload_length(u16::try_from(payload.len())?);\n    packet.set_next_header(next_header);\n    packet.set_hop_limit(64);\n    packet.set_source_address(source);\n    packet.set_destination_address(destination);\n    packet.set_payload(payload);\n    Ok(packet)\n}\n"
  },
  {
    "path": "crates/trippy-core/tests/sim/network.rs",
    "content": "mod ipv4;\nmod ipv6;\n\nuse crate::simulation::Simulation;\nuse crate::tun_device::TunDevice;\nuse futures_concurrency::future::Race;\nuse std::io;\nuse std::net::IpAddr;\nuse std::sync::Arc;\nuse std::time::Duration;\nuse tokio::sync::Mutex;\nuse tokio::task::JoinHandle;\nuse tokio_util::sync::CancellationToken;\nuse tracing::debug;\nuse trippy_packet::ip::{IpPacket, IpVersion};\nuse trippy_packet::ipv4::Ipv4Packet;\nuse trippy_packet::ipv6::Ipv6Packet;\n\nconst READ_TIMEOUT: Duration = Duration::from_millis(10);\n\npub async fn run(\n    tun: Arc<Mutex<TunDevice>>,\n    sim: Arc<Simulation>,\n    token: CancellationToken,\n) -> anyhow::Result<()> {\n    let mut handles: Vec<JoinHandle<()>> = vec![];\n    let expected_version = match sim.target {\n        IpAddr::V4(_) => IpVersion::Ipv4,\n        IpAddr::V6(_) => IpVersion::Ipv6,\n    };\n    loop {\n        let mut buf = [0_u8; 4096];\n        let Some(bytes_read) = {\n            let tun = tun.clone();\n            (\n                async {\n                    token.cancelled().await;\n                    Ok::<Option<usize>, io::Error>(None)\n                },\n                async { read_with_timeout(&mut buf, tun.clone()).await.map(Some) },\n            )\n                .race()\n                .await\n        }?\n        else {\n            for h in handles {\n                h.abort();\n            }\n            return Ok(());\n        };\n        if bytes_read == 0 {\n            continue;\n        }\n        let ip = IpPacket::new_view(&buf[..bytes_read]).expect(\"valid IP packet\");\n        match ip.get_version() {\n            IpVersion::Ipv4 => {\n                if expected_version != IpVersion::Ipv4 {\n                    debug!(\n                        \"skipping IPv4 packet while expecting {:?} packets\",\n                        expected_version\n                    );\n                    continue;\n                }\n                if let Some((reply_delay_ms, packet_buf)) =\n                    ipv4::process(sim.as_ref(), ip.packet())?\n                {\n                    handles.push(tokio::spawn(write_packet(\n                        tun.clone(),\n                        reply_delay_ms,\n                        packet_buf,\n                        IpVersion::Ipv4,\n                    )));\n                }\n            }\n            IpVersion::Ipv6 => {\n                if expected_version != IpVersion::Ipv6 {\n                    debug!(\n                        \"skipping IPv6 packet while expecting {:?} packets\",\n                        expected_version\n                    );\n                    continue;\n                }\n                if let Some((reply_delay_ms, packet_buf)) =\n                    ipv6::process(sim.as_ref(), ip.packet())?\n                {\n                    handles.push(tokio::spawn(write_packet(\n                        tun.clone(),\n                        reply_delay_ms,\n                        packet_buf,\n                        IpVersion::Ipv6,\n                    )));\n                }\n            }\n            IpVersion::Other(version) => {\n                debug!(\"skipping unknown IP version packet: {}\", version);\n            }\n        }\n    }\n}\n\n/// Read from the tun device with a timeout.\n///\n/// Note that the tun device is only locked for the timeout period\nasync fn read_with_timeout(buf: &mut [u8], tun: Arc<Mutex<TunDevice>>) -> io::Result<usize> {\n    tokio::time::timeout(READ_TIMEOUT, tun.lock().await.read(buf))\n        .await\n        .unwrap_or(Ok(0))\n}\n\nasync fn write_packet(\n    tun: Arc<Mutex<TunDevice>>,\n    reply_delay_ms: u16,\n    packet_buf: Vec<u8>,\n    version: IpVersion,\n) {\n    tokio::time::sleep(Duration::from_millis(u64::from(reply_delay_ms))).await;\n    match version {\n        IpVersion::Ipv4 => {\n            let packet = Ipv4Packet::new_view(&packet_buf).expect(\"valid ipv4 packet\");\n            debug!(\"write: {:?}\", packet);\n            tun.lock().await.write(packet.packet()).await.expect(\"send\");\n        }\n        IpVersion::Ipv6 => {\n            let packet = Ipv6Packet::new_view(&packet_buf).expect(\"valid ipv6 packet\");\n            debug!(\"write: {:?}\", packet);\n            tun.lock().await.write(packet.packet()).await.expect(\"send\");\n        }\n        IpVersion::Other(version) => {\n            panic!(\"unexpected packet version: {version}\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/tests/sim/simulation.rs",
    "content": "use serde::Deserialize;\nuse std::net::IpAddr;\nuse trippy_core::Port;\n\n/// A simulated trace.\n#[derive(Debug, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct Simulation {\n    pub name: String,\n    pub rounds: Option<usize>,\n    #[serde(default)]\n    pub privilege_mode: PrivilegeMode,\n    pub target: IpAddr,\n    pub protocol: Protocol,\n    #[serde(default)]\n    pub port_direction: PortDirection,\n    #[serde(default)]\n    pub multipath_strategy: MultipathStrategy,\n    #[serde(default)]\n    pub icmp_identifier: u16,\n    pub initial_sequence: Option<u16>,\n    pub packet_size: Option<u16>,\n    pub payload_pattern: Option<u8>,\n    pub tos: Option<u8>,\n    pub min_round_duration: Option<u64>,\n    pub max_round_duration: Option<u64>,\n    pub grace_duration: Option<u64>,\n    pub hops: Vec<Hop>,\n}\n\nimpl Simulation {\n    pub fn latest_ttl(&self) -> u8 {\n        if self.hops.is_empty() {\n            0\n        } else {\n            self.hops[self.hops.len() - 1].ttl\n        }\n    }\n}\n\n/// A simulated hop.\n#[derive(Debug, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct Hop {\n    /// The simulated time-to-live (TTL).\n    pub ttl: u8,\n    /// The simulated probe response.\n    pub resp: Response,\n}\n\n/// A simulated probe response.\n#[derive(Debug, Deserialize)]\n#[serde(tag = \"tag\")]\npub enum Response {\n    /// Simulate a hop which does not response to probes.\n    NoResponse,\n    /// Simulate a hop which responds to probes from a single host.\n    SingleHost(SingleHost),\n}\n\n/// A simulated probe response with a single addr and fixed ttl.\n#[derive(Debug, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct SingleHost {\n    /// The simulated host responding to the probe.\n    pub addr: IpAddr,\n    /// The simulated round trim time (RTT) in ms.\n    pub rtt_ms: u16,\n}\n\n#[derive(Copy, Clone, Debug, Default, Deserialize)]\npub enum PrivilegeMode {\n    #[default]\n    Privileged,\n    Unprivileged,\n}\n\nimpl From<PrivilegeMode> for trippy_core::PrivilegeMode {\n    fn from(value: PrivilegeMode) -> Self {\n        match value {\n            PrivilegeMode::Privileged => Self::Privileged,\n            PrivilegeMode::Unprivileged => Self::Unprivileged,\n        }\n    }\n}\n\n#[derive(Copy, Clone, Debug, Deserialize)]\npub enum Protocol {\n    Icmp,\n    Udp,\n    Tcp,\n}\n\nimpl From<Protocol> for trippy_core::Protocol {\n    fn from(value: Protocol) -> Self {\n        match value {\n            Protocol::Icmp => Self::Icmp,\n            Protocol::Udp => Self::Udp,\n            Protocol::Tcp => Self::Tcp,\n        }\n    }\n}\n\n#[derive(Copy, Clone, Debug, Default, Deserialize)]\n#[serde(tag = \"tag\", content = \"value\")]\npub enum PortDirection {\n    #[default]\n    None,\n    FixedSrc(u16),\n    FixedDest(u16),\n    FixedBoth(FixedBoth),\n}\n\n#[derive(Copy, Clone, Debug, Default, Deserialize)]\n#[serde(deny_unknown_fields)]\npub struct FixedBoth {\n    pub src: u16,\n    pub dest: u16,\n}\n\nimpl From<PortDirection> for trippy_core::PortDirection {\n    fn from(value: PortDirection) -> Self {\n        match value {\n            PortDirection::None => Self::None,\n            PortDirection::FixedSrc(src) => Self::FixedSrc(Port(src)),\n            PortDirection::FixedDest(dest) => Self::FixedDest(Port(dest)),\n            PortDirection::FixedBoth(FixedBoth { src, dest }) => {\n                Self::FixedBoth(Port(src), Port(dest))\n            }\n        }\n    }\n}\n\n#[derive(Copy, Clone, Debug, Default, Deserialize)]\npub enum MultipathStrategy {\n    #[default]\n    Classic,\n    Paris,\n    Dublin,\n}\n\nimpl From<MultipathStrategy> for trippy_core::MultipathStrategy {\n    fn from(value: MultipathStrategy) -> Self {\n        match value {\n            MultipathStrategy::Classic => Self::Classic,\n            MultipathStrategy::Paris => Self::Paris,\n            MultipathStrategy::Dublin => Self::Dublin,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/tests/sim/tests.rs",
    "content": "use crate::simulation::Simulation;\nuse crate::tun_device::tun;\nuse crate::{network, tracer};\nuse std::sync::{Arc, Mutex, OnceLock};\nuse test_case::test_case;\nuse tokio::runtime::Runtime;\nuse tokio_util::sync::CancellationToken;\nuse tracing::{error, info, warn};\nuse tracing_subscriber::fmt::format::FmtSpan;\n\n/// The maximum number of attempts for each test.\nconst MAX_ATTEMPTS: usize = 5;\n\nstatic RUNTIME: OnceLock<Arc<Mutex<Runtime>>> = OnceLock::new();\n\npub fn runtime() -> &'static Arc<Mutex<Runtime>> {\n    RUNTIME.get_or_init(|| {\n        tracing_subscriber::fmt()\n            .with_span_events(FmtSpan::NONE)\n            .with_env_filter(\"trippy=off,sim=debug\")\n            .init();\n\n        let runtime = tokio::runtime::Builder::new_multi_thread()\n            .enable_all()\n            .build()\n            .unwrap();\n        Arc::new(Mutex::new(runtime))\n    })\n}\n\nmacro_rules! sim {\n    ($path:expr) => {{\n        let data = include_str!(concat!(\"../resources/simulation/\", $path));\n        toml::from_str(data)?\n    }};\n}\n\n#[test_case(sim!(\"ipv4_icmp.toml\"))]\n#[test_case(sim!(\"ipv6_icmp.toml\"))]\n#[test_case(sim!(\"ipv4_icmp_gaps.toml\"))]\n#[test_case(sim!(\"ipv4_icmp_ooo.toml\"))]\n#[test_case(sim!(\"ipv4_icmp_min.toml\"))]\n#[test_case(sim!(\"ipv6_icmp_min.toml\"))]\n#[test_case(sim!(\"ipv4_icmp_pattern.toml\"))]\n#[test_case(sim!(\"ipv6_icmp_pattern.toml\"))]\n#[test_case(sim!(\"ipv4_icmp_quick.toml\"))]\n#[test_case(sim!(\"ipv4_icmp_wrap.toml\"))]\n#[test_case(sim!(\"ipv4_icmp_tos.toml\"))]\n#[test_case(sim!(\"ipv4_udp_classic_fixed_src.toml\"))]\n#[test_case(sim!(\"ipv6_udp_classic_fixed_src.toml\"))]\n#[test_case(sim!(\"ipv4_udp_classic_fixed_dest.toml\"))]\n#[test_case(sim!(\"ipv6_udp_classic_fixed_dest.toml\"))]\n#[test_case(sim!(\"ipv4_udp_paris_fixed_both.toml\"))]\n#[test_case(sim!(\"ipv6_udp_paris_fixed_both.toml\"))]\n#[test_case(sim!(\"ipv4_udp_dublin_fixed_both.toml\"))]\n#[test_case(sim!(\"ipv6_udp_dublin_fixed_both.toml\"))]\n#[test_case(sim!(\"ipv4_udp_classic_privileged_tos.toml\"))]\n#[test_case(sim!(\"ipv4_tcp_fixed_dest.toml\"))]\n#[test_case(sim!(\"ipv6_tcp_fixed_dest.toml\"))]\nfn test_simulation(simulation: Simulation) -> anyhow::Result<()> {\n    run_simulation_with_retry(simulation)\n}\n\n// unprivileged mode is only supported on macOS\n#[cfg(target_os = \"macos\")]\n#[test_case(sim!(\"ipv4_udp_classic_unprivileged.toml\"))]\n#[test_case(sim!(\"ipv4_udp_classic_unprivileged_tos.toml\"))]\n#[test_case(sim!(\"ipv6_udp_classic_unprivileged.toml\"))]\n#[test_case(sim!(\"ipv6_udp_classic_unprivileged_tos.toml\"))]\nfn test_simulation_macos(simulation: Simulation) -> anyhow::Result<()> {\n    run_simulation_with_retry(simulation)\n}\n\nfn run_simulation_with_retry(simulation: Simulation) -> anyhow::Result<()> {\n    let runtime = runtime().lock().unwrap();\n    let simulation = Arc::new(simulation);\n    let name = simulation.name.clone();\n    if !trippy_privilege::Privilege::discover()?.has_privileges() {\n        // Skip if the current test as the user cannot create a tun device.\n        warn!(\"skipping test {}: insufficient privileges\", name);\n        return Ok(());\n    }\n    for attempt in 1..=MAX_ATTEMPTS {\n        info!(\"start simulating {} [attempt #{}]\", name, attempt);\n        if let Err(err) = runtime.block_on(run_simulation(simulation.clone())) {\n            error!(\"failed simulating {} {} [attempt #{}]\", name, err, attempt);\n        } else {\n            info!(\"end simulating {} [attempt #{}]\", name, attempt);\n            return Ok(());\n        }\n    }\n    anyhow::bail!(\"failed simulating {name} after {MAX_ATTEMPTS} attempts\")\n}\n\nasync fn run_simulation(sim: Arc<Simulation>) -> anyhow::Result<()> {\n    let tun = tun();\n    let token = CancellationToken::new();\n    let handle = tokio::spawn(network::run(tun.clone(), sim.clone(), token.clone()));\n    tokio::task::spawn_blocking(move || tracer::Tracer::new(sim, token).trace()).await??;\n    handle.await?\n}\n"
  },
  {
    "path": "crates/trippy-core/tests/sim/tracer.rs",
    "content": "use crate::simulation::{Response, Simulation, SingleHost};\nuse crate::tun_device::{TUN_NETWORK_ADDR_V4, TUN_NETWORK_ADDR_V6};\nuse std::cell::RefCell;\nuse std::net::IpAddr;\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::Duration;\nuse tokio_util::sync::CancellationToken;\nuse tracing::info;\nuse trippy_core::{\n    Builder, CompletionReason, MultipathStrategy, PortDirection, PrivilegeMode, ProbeStatus,\n    Protocol, Round, TimeToLive, defaults,\n};\n\n// The length of time to wait after the completion of the tracing before\n// cancelling the network simulator.  This is needed to ensure that all\n// in-flight packets for the current test are sent or received prior to\n// ending the round so that they are not incorrectly used in a subsequent\n// test.\nconst CLEANUP_DELAY: Duration = Duration::from_millis(1000);\n\nmacro_rules! assert_eq_result {\n    ($res:ident, $exp1:expr, $exp2:expr) => {{\n        fn ensure_match<T: PartialEq>(fst: T, snd: T) -> anyhow::Result<()> {\n            anyhow::ensure!(fst == snd);\n            Ok(())\n        }\n        if let err @ Err(_) = ensure_match($exp1, $exp2) {\n            *$res.borrow_mut() = err;\n            return;\n        }\n    }};\n}\n\nmacro_rules! error_result {\n    ($res:ident, $err:expr) => {{\n        *$res.borrow_mut() = Err($err);\n        return;\n    }};\n}\n\npub struct Tracer {\n    sim: Arc<Simulation>,\n    token: CancellationToken,\n}\n\nimpl Tracer {\n    pub const fn new(sim: Arc<Simulation>, token: CancellationToken) -> Self {\n        Self { sim, token }\n    }\n\n    pub fn trace(&self) -> anyhow::Result<()> {\n        let result = RefCell::new(Ok(()));\n        let source_addr = Some(match self.sim.target {\n            IpAddr::V4(_) => IpAddr::V4(TUN_NETWORK_ADDR_V4),\n            IpAddr::V6(_) => IpAddr::V6(TUN_NETWORK_ADDR_V6),\n        });\n        let tracer = Builder::new(self.sim.target)\n            .source_addr(source_addr)\n            .privilege_mode(PrivilegeMode::from(self.sim.privilege_mode))\n            .trace_identifier(self.sim.icmp_identifier)\n            .initial_sequence(\n                self.sim\n                    .initial_sequence\n                    .unwrap_or(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE),\n            )\n            .protocol(Protocol::from(self.sim.protocol))\n            .port_direction(PortDirection::from(self.sim.port_direction))\n            .multipath_strategy(MultipathStrategy::from(self.sim.multipath_strategy))\n            .packet_size(\n                self.sim\n                    .packet_size\n                    .unwrap_or(defaults::DEFAULT_STRATEGY_PACKET_SIZE),\n            )\n            .payload_pattern(\n                self.sim\n                    .payload_pattern\n                    .unwrap_or(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN),\n            )\n            .tos(self.sim.tos.unwrap_or(defaults::DEFAULT_STRATEGY_TOS))\n            .min_round_duration(self.sim.min_round_duration.map_or(\n                defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION,\n                Duration::from_millis,\n            ))\n            .max_round_duration(self.sim.max_round_duration.map_or(\n                defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION,\n                Duration::from_millis,\n            ))\n            .grace_duration(self.sim.grace_duration.map_or(\n                defaults::DEFAULT_STRATEGY_GRACE_DURATION,\n                Duration::from_millis,\n            ))\n            .max_rounds(self.sim.rounds.or(Some(1)))\n            .build()?;\n        let tracer_res = tracer\n            .run_with(|round| self.validate_round(round, &result))\n            .map_err(anyhow::Error::from);\n        thread::sleep(CLEANUP_DELAY);\n        self.token.cancel();\n        // ensure both the tracer and the validator were successful.\n        tracer_res.and(result.replace(Ok(())))\n    }\n\n    fn validate_round(&self, round: &Round<'_>, result: &RefCell<anyhow::Result<()>>) {\n        assert_eq_result!(result, round.reason, CompletionReason::TargetFound);\n        assert_eq_result!(result, TimeToLive(self.sim.latest_ttl()), round.largest_ttl);\n        for hop in round\n            .probes\n            .iter()\n            .filter(|p| matches!(p, ProbeStatus::Awaited(_) | ProbeStatus::Complete(_)))\n            .take(round.largest_ttl.0 as usize)\n        {\n            match hop {\n                ProbeStatus::Complete(complete) => {\n                    info!(\n                        \"{} {} {}\",\n                        complete.round.0,\n                        complete.ttl.0,\n                        complete.host.to_string(),\n                    );\n\n                    let hop_index = usize::from(complete.ttl.0 - 1);\n                    let sim_hop = &self.sim.hops[hop_index];\n                    if matches!(sim_hop.resp, Response::NoResponse) {\n                        error_result!(result, anyhow::anyhow!(\"expected Response::SingleHost\"));\n                    }\n                    let expected_host = match sim_hop.resp {\n                        Response::NoResponse => None,\n                        Response::SingleHost(SingleHost { addr, .. }) => Some(addr),\n                    };\n                    assert_eq_result!(result, expected_host, Some(complete.host));\n                    let expected_ttl = TimeToLive(self.sim.hops[hop_index].ttl);\n                    assert_eq_result!(result, expected_ttl, complete.ttl);\n                }\n                ProbeStatus::Awaited(awaited) => {\n                    info!(\"{} {} * * *\", awaited.round.0, awaited.ttl.0);\n\n                    let hop_index = usize::from(awaited.ttl.0 - 1);\n                    let sim_hop = &self.sim.hops[hop_index];\n                    if let Response::SingleHost(_) = sim_hop.resp {\n                        error_result!(result, anyhow::anyhow!(\"expected Response::NoResponse\"));\n                    }\n                    let expected_host = match sim_hop.resp {\n                        Response::NoResponse => None,\n                        Response::SingleHost(SingleHost { addr, .. }) => Some(addr),\n                    };\n                    assert_eq_result!(result, expected_host, None);\n                    let expected_ttl = TimeToLive(self.sim.hops[hop_index].ttl);\n                    assert_eq_result!(result, expected_ttl, awaited.ttl);\n                }\n                _ => {}\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-core/tests/sim/tun_device.rs",
    "content": "use std::net::{Ipv4Addr, Ipv6Addr};\nuse std::sync::{Arc, OnceLock};\nuse tokio::sync::Mutex;\n\nstatic TUN: OnceLock<Arc<Mutex<TunDevice>>> = OnceLock::new();\n\n/// Get a reference to the singleton `tun` device, initializing as necessary.\npub fn tun() -> &'static Arc<Mutex<TunDevice>> {\n    TUN.get_or_init(|| {\n        let tun = TunDevice::start().expect(\"tun\");\n        Arc::new(Mutex::new(tun))\n    })\n}\n\n/// IPv4 address and CIDR prefix configured on the `tun` device.\n///\n/// For example, if this is set to `10.0.0.1` with a prefix length of 24 then\n/// the `tun` device will be assigned the IP `10.0.0.1` and packets sent to\n/// the network range `10.0.0.0/24` will typically be routed via the `tun`\n/// device and therefore have the source address `10.0.0.1`.\npub const TUN_NETWORK_ADDR_V4: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);\nconst TUN_NETWORK_PREFIX_V4: u8 = 24;\n\n/// IPv6 address and CIDR prefix configured on the `tun` device.\npub const TUN_NETWORK_ADDR_V6: Ipv6Addr = Ipv6Addr::new(0xfd00, 0x0010, 0, 0, 0, 0, 0, 1);\nconst TUN_NETWORK_PREFIX_V6: u8 = 64;\n\n/// A `tun` device.\npub struct TunDevice {\n    dev: tun_rs::AsyncDevice,\n}\n\nimpl TunDevice {\n    pub fn start() -> anyhow::Result<Self> {\n        let dev = tun_rs::DeviceBuilder::new()\n            .ipv4(TUN_NETWORK_ADDR_V4, TUN_NETWORK_PREFIX_V4, None)\n            .ipv6(TUN_NETWORK_ADDR_V6, TUN_NETWORK_PREFIX_V6)\n            .build_async()?;\n        #[cfg(target_os = \"windows\")]\n        std::thread::sleep(std::time::Duration::from_millis(10000));\n        Ok(Self { dev })\n    }\n\n    pub async fn read(&self, buf: &mut [u8]) -> std::io::Result<usize> {\n        let bytes_read = self.dev.recv(buf).await?;\n        Ok(bytes_read)\n    }\n\n    pub async fn write(&self, buf: &[u8]) -> std::io::Result<usize> {\n        self.dev.send(buf).await\n    }\n}\n"
  },
  {
    "path": "crates/trippy-dns/Cargo.toml",
    "content": "[package]\nname = \"trippy-dns\"\ndescription = \"A lazy DNS resolver for Trippy\"\nversion.workspace = true\nauthors.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\nreadme.workspace = true\nlicense.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nkeywords.workspace = true\ncategories.workspace = true\n\n[dependencies]\ncrossbeam.workspace = true\ndns-lookup.workspace = true\nhickory-resolver.workspace = true\nitertools.workspace = true\nparking_lot.workspace = true\nthiserror.workspace = true\n\n[dev-dependencies]\nanyhow.workspace = true\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/trippy-dns/src/config.rs",
    "content": "use crate::{IpAddrFamily, ResolveMethod};\nuse std::time::Duration;\n\n/// A builder for DNS `Config`.\n///\n/// # Example\n///\n/// Build a DNS `Config` for the `Ipv4Only` address family.\n///\n/// ```no_run\n/// use trippy_dns::{Builder, IpAddrFamily};\n///\n/// let config = Builder::new().addr_family(IpAddrFamily::Ipv4Only).build();\npub struct Builder {\n    resolve_method: ResolveMethod,\n    addr_family: IpAddrFamily,\n    timeout: Duration,\n    ttl: Duration,\n}\n\nimpl Builder {\n    /// Create a new `Builder`.\n    #[must_use]\n    pub fn new() -> Self {\n        Self {\n            resolve_method: Config::default().resolve_method,\n            addr_family: Config::default().addr_family,\n            timeout: Config::default().timeout,\n            ttl: Config::default().ttl,\n        }\n    }\n\n    /// Set the method to use for DNS resolution.\n    #[must_use]\n    pub const fn resolve_method(self, resolve_method: ResolveMethod) -> Self {\n        Self {\n            resolve_method,\n            ..self\n        }\n    }\n\n    /// Set the address family.\n    #[must_use]\n    pub const fn addr_family(self, addr_family: IpAddrFamily) -> Self {\n        Self {\n            addr_family,\n            ..self\n        }\n    }\n\n    /// Set the timeout for DNS resolution.\n    #[must_use]\n    pub const fn timeout(self, timeout: Duration) -> Self {\n        Self { timeout, ..self }\n    }\n\n    /// Set the time-to-live (TTL) for DNS cache entries.\n    #[must_use]\n    pub const fn ttl(self, ttl: Duration) -> Self {\n        Self { ttl, ..self }\n    }\n\n    /// Build the DNS `Config`.\n    #[must_use]\n    pub const fn build(self) -> Config {\n        Config {\n            resolve_method: self.resolve_method,\n            addr_family: self.addr_family,\n            timeout: self.timeout,\n            ttl: self.ttl,\n        }\n    }\n}\n\nimpl Default for Builder {\n    fn default() -> Self {\n        Self::new()\n    }\n}\n\n/// Configuration for the `DnsResolver`.\n#[derive(Debug, Copy, Clone)]\npub struct Config {\n    /// The method to use for DNS resolution.\n    pub resolve_method: ResolveMethod,\n    /// The IP address resolution family.\n    pub addr_family: IpAddrFamily,\n    /// The timeout for DNS resolution.\n    pub timeout: Duration,\n    /// The time-to-live (TTL) for DNS cache entries.\n    pub ttl: Duration,\n}\n\nimpl Config {\n    /// Create a `Config`.\n    #[must_use]\n    pub const fn new(\n        resolve_method: ResolveMethod,\n        addr_family: IpAddrFamily,\n        timeout: Duration,\n        ttl: Duration,\n    ) -> Self {\n        Self {\n            resolve_method,\n            addr_family,\n            timeout,\n            ttl,\n        }\n    }\n}\n\nimpl Default for Config {\n    fn default() -> Self {\n        Self {\n            resolve_method: ResolveMethod::System,\n            addr_family: IpAddrFamily::Ipv4thenIpv6,\n            timeout: Duration::from_millis(5000),\n            ttl: Duration::from_secs(300),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-dns/src/lazy_resolver.rs",
    "content": "use crate::config::Config;\nuse crate::resolver::{DnsEntry, ResolvedIpAddrs, Resolver, Result};\nuse std::fmt::{Display, Formatter};\nuse std::net::IpAddr;\nuse std::rc::Rc;\n\n/// How DNS queries will be resolved.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum ResolveMethod {\n    /// Resolve using the OS resolver.\n    System,\n    /// Resolve using the `/etc/resolv.conf` DNS configuration.\n    Resolv,\n    /// Resolve using the Google `8.8.8.8` DNS service.\n    Google,\n    /// Resolve using the Cloudflare `1.1.1.1` DNS service.\n    Cloudflare,\n}\n\n/// How to resolve IP addresses.\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum IpAddrFamily {\n    /// Lookup IPv4 only.\n    Ipv4Only,\n    /// Lookup IPv6 only.\n    Ipv6Only,\n    /// Lookup IPv6 with a fallback to IPv4.\n    Ipv6thenIpv4,\n    /// Lookup IPv4 with a fallback to IPv6.\n    Ipv4thenIpv6,\n    /// Use the first IP address returned by the OS resolver when using `ResolveMethod::System`,\n    /// otherwise lookup IPv4 with a fallback to IPv6.\n    System,\n}\n\nimpl Display for IpAddrFamily {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Ipv4Only => write!(f, \"Ipv4Only\"),\n            Self::Ipv6Only => write!(f, \"Ipv6Only\"),\n            Self::Ipv6thenIpv4 => write!(f, \"Ipv6thenIpv4\"),\n            Self::Ipv4thenIpv6 => write!(f, \"Ipv4thenIpv6\"),\n            Self::System => write!(f, \"System\"),\n        }\n    }\n}\n\n/// A cheaply cloneable, non-blocking, caching, forward and reverse DNS resolver.\n#[derive(Clone)]\npub struct DnsResolver {\n    inner: Rc<inner::DnsResolver>,\n}\n\nimpl DnsResolver {\n    /// Create and start a new `DnsResolver`.\n    pub fn start(config: Config) -> std::io::Result<Self> {\n        Ok(Self {\n            inner: Rc::new(inner::DnsResolver::start(config)?),\n        })\n    }\n\n    /// Get the `Config`.\n    #[must_use]\n    pub fn config(&self) -> &Config {\n        self.inner.config()\n    }\n\n    /// Flush the cache of responses.\n    pub fn flush(&self) {\n        self.inner.flush();\n    }\n}\n\nimpl Resolver for DnsResolver {\n    fn lookup(&self, hostname: impl AsRef<str>) -> Result<ResolvedIpAddrs> {\n        self.inner.lookup(hostname.as_ref())\n    }\n    fn reverse_lookup(&self, addr: impl Into<IpAddr>) -> DnsEntry {\n        self.inner.reverse_lookup(addr.into(), false, false)\n    }\n    fn reverse_lookup_with_asinfo(&self, addr: impl Into<IpAddr>) -> DnsEntry {\n        self.inner.reverse_lookup(addr.into(), true, false)\n    }\n    fn lazy_reverse_lookup(&self, addr: impl Into<IpAddr>) -> DnsEntry {\n        self.inner.reverse_lookup(addr.into(), false, true)\n    }\n    fn lazy_reverse_lookup_with_asinfo(&self, addr: impl Into<IpAddr>) -> DnsEntry {\n        self.inner.reverse_lookup(addr.into(), true, true)\n    }\n}\n\n/// Private impl of resolver.\nmod inner {\n    use super::{Config, IpAddrFamily, ResolveMethod};\n    use crate::resolver::{AsInfo, DnsEntry, Error, Resolved, ResolvedIpAddrs, Result, Unresolved};\n    use crossbeam::channel::{Receiver, Sender, bounded};\n    use hickory_resolver::config::{LookupIpStrategy, ResolverConfig, ResolverOpts};\n    use hickory_resolver::error::{ResolveError, ResolveErrorKind};\n    use hickory_resolver::proto::error::ProtoError;\n    use hickory_resolver::proto::rr::RecordType;\n    use hickory_resolver::system_conf::read_system_conf;\n    use hickory_resolver::{Name, Resolver};\n    use itertools::{Either, Itertools};\n    use parking_lot::RwLock;\n    use std::collections::HashMap;\n    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};\n    use std::str::FromStr;\n    use std::sync::Arc;\n    use std::thread;\n    use std::time::{Duration, SystemTime};\n\n    /// The maximum number of in-flight reverse DNS resolutions that may be\n    const RESOLVER_MAX_QUEUE_SIZE: usize = 100;\n\n    /// The duration wait to enqueue a `DnsEntry::Pending` to the resolver before returning\n    /// `DnsEntry::Timeout`.\n    const RESOLVER_QUEUE_TIMEOUT: Duration = Duration::from_millis(10);\n\n    /// Alias for a cache of reverse DNS lookup entries.\n    type Cache = Arc<RwLock<HashMap<IpAddr, CacheEntry>>>;\n\n    /// A cache entry for a reverse DNS lookup.\n    #[derive(Debug, Clone)]\n    struct CacheEntry {\n        /// The DNS entry to cache.\n        entry: DnsEntry,\n        /// The timestamp of the entry.\n        timestamp: SystemTime,\n    }\n\n    impl CacheEntry {\n        const fn new(entry: DnsEntry, timestamp: SystemTime) -> Self {\n            Self { entry, timestamp }\n        }\n\n        fn set_timestamp(&mut self, timestamp: SystemTime) {\n            self.timestamp = timestamp;\n        }\n    }\n\n    #[derive(Clone)]\n    enum DnsProvider {\n        TrustDns(Arc<Resolver>),\n        DnsLookup,\n    }\n\n    #[derive(Debug, Clone)]\n    struct DnsResolveRequest {\n        addr: IpAddr,\n        with_asinfo: bool,\n    }\n\n    /// Resolver implementation.\n    pub(super) struct DnsResolver {\n        config: Config,\n        provider: DnsProvider,\n        tx: Sender<DnsResolveRequest>,\n        addr_cache: Cache,\n    }\n\n    impl DnsResolver {\n        pub(super) fn start(config: Config) -> std::io::Result<Self> {\n            let (tx, rx) = bounded(RESOLVER_MAX_QUEUE_SIZE);\n            let addr_cache = Arc::new(RwLock::new(HashMap::new()));\n\n            let provider = if matches!(config.resolve_method, ResolveMethod::System) {\n                DnsProvider::DnsLookup\n            } else {\n                let mut options = ResolverOpts::default();\n                #[expect(clippy::match_same_arms)]\n                let ip_strategy = match config.addr_family {\n                    IpAddrFamily::Ipv4Only => LookupIpStrategy::Ipv4Only,\n                    IpAddrFamily::Ipv6Only => LookupIpStrategy::Ipv6Only,\n                    IpAddrFamily::Ipv6thenIpv4 => LookupIpStrategy::Ipv6thenIpv4,\n                    IpAddrFamily::Ipv4thenIpv6 => LookupIpStrategy::Ipv4thenIpv6,\n                    // see issue #1469\n                    IpAddrFamily::System => LookupIpStrategy::Ipv4thenIpv6,\n                };\n                options.timeout = config.timeout;\n                options.ip_strategy = ip_strategy;\n                let res = match config.resolve_method {\n                    ResolveMethod::Resolv => {\n                        let (resolver_cfg, mut options) = read_system_conf()?;\n                        options.timeout = config.timeout;\n                        options.ip_strategy = ip_strategy;\n                        Resolver::new(resolver_cfg, options)\n                    }\n                    ResolveMethod::Google => Resolver::new(ResolverConfig::google(), options),\n                    ResolveMethod::Cloudflare => {\n                        Resolver::new(ResolverConfig::cloudflare(), options)\n                    }\n                    ResolveMethod::System => unreachable!(),\n                }?;\n                let resolver = Arc::new(res);\n                DnsProvider::TrustDns(resolver)\n            };\n\n            // spawn a thread to process the resolve queue\n            {\n                let cache = addr_cache.clone();\n                let provider = provider.clone();\n                thread::spawn(move || resolver_queue_processor(rx, &provider, &cache));\n            }\n            Ok(Self {\n                config,\n                provider,\n                tx,\n                addr_cache,\n            })\n        }\n\n        pub(super) const fn config(&self) -> &Config {\n            &self.config\n        }\n\n        pub(super) fn lookup(&self, hostname: &str) -> Result<ResolvedIpAddrs> {\n            fn partition<I: Iterator<Item = IpAddr>>(all: I) -> (Vec<IpAddr>, Vec<IpAddr>) {\n                all.partition_map(|ip| match ip {\n                    IpAddr::V4(_) => Either::Left(ip),\n                    IpAddr::V6(_) => Either::Right(ip),\n                })\n            }\n            match &self.provider {\n                DnsProvider::TrustDns(resolver) => Ok(resolver\n                    .lookup_ip(hostname)\n                    .map_err(|err| Error::LookupFailed(Box::new(err)))?\n                    .iter()\n                    .collect::<Vec<_>>()),\n                DnsProvider::DnsLookup => {\n                    let all = dns_lookup::lookup_host(hostname)\n                        .map_err(|err| Error::LookupFailed(Box::new(err)))?;\n                    Ok(match self.config.addr_family {\n                        IpAddrFamily::Ipv4Only => {\n                            let (ipv4, _): (Vec<_>, Vec<_>) = partition(all);\n                            if ipv4.is_empty() { vec![] } else { ipv4 }\n                        }\n                        IpAddrFamily::Ipv6Only => {\n                            let (_, ipv6): (Vec<_>, Vec<_>) = partition(all);\n                            if ipv6.is_empty() { vec![] } else { ipv6 }\n                        }\n                        IpAddrFamily::Ipv6thenIpv4 => {\n                            let (ipv4, ipv6): (Vec<_>, Vec<_>) = partition(all);\n                            if ipv6.is_empty() { ipv4 } else { ipv6 }\n                        }\n                        IpAddrFamily::Ipv4thenIpv6 => {\n                            let (ipv4, ipv6): (Vec<_>, Vec<_>) = partition(all);\n                            if ipv4.is_empty() { ipv6 } else { ipv4 }\n                        }\n                        IpAddrFamily::System => all.collect(),\n                    })\n                }\n            }\n            .map(ResolvedIpAddrs)\n        }\n\n        pub(super) fn reverse_lookup(\n            &self,\n            addr: IpAddr,\n            with_asinfo: bool,\n            lazy: bool,\n        ) -> DnsEntry {\n            if lazy {\n                self.lazy_reverse_lookup(addr, with_asinfo).entry\n            } else {\n                reverse_lookup(&self.provider, addr, with_asinfo).entry\n            }\n        }\n\n        fn lazy_reverse_lookup(&self, addr: IpAddr, with_asinfo: bool) -> CacheEntry {\n            let mut enqueue = false;\n            let now = SystemTime::now();\n\n            // Check if we have already attempted to resolve this `IpAddr` and return the current\n            // `DnsEntry` if so, otherwise add it in a state of `DnsEntry::Pending`.\n            let mut dns_entry = self\n                .addr_cache\n                .write()\n                .entry(addr)\n                .or_insert_with(|| {\n                    enqueue = true;\n                    CacheEntry::new(DnsEntry::Pending(addr), now)\n                })\n                .clone();\n\n            // If the entry exists but is stale then enqueue it again.  The existing entry will\n            // be returned until it is refreshed but with an updated timestamp to prevent it from\n            // being enqueued multiple times.\n            match &dns_entry.entry {\n                DnsEntry::Resolved(_) | DnsEntry::NotFound(_) | DnsEntry::Failed(_) => {\n                    if now.duration_since(dns_entry.timestamp).unwrap_or_default() > self.config.ttl\n                    {\n                        self.addr_cache\n                            .write()\n                            .get_mut(&addr)\n                            .expect(\"addr must be in cache\")\n                            .set_timestamp(now);\n                        enqueue = true;\n                    }\n                }\n                _ => {}\n            }\n\n            // If the entry exists but has timed out, then set it as `DnsEntry::Pending` and enqueue\n            // it again.\n            if let DnsEntry::Timeout(addr) = dns_entry.entry {\n                *self\n                    .addr_cache\n                    .write()\n                    .get_mut(&addr)\n                    .expect(\"addr must be in cache\") =\n                    CacheEntry::new(DnsEntry::Pending(addr), now);\n                dns_entry = CacheEntry::new(DnsEntry::Pending(addr), now);\n                enqueue = true;\n            }\n\n            // If this is a newly added `DnsEntry` then send it to the channel to be resolved in the\n            // background.  We do this after the above to ensure we aren't holding the\n            // lock on the cache, which is used by the resolver and so would deadlock.\n            if enqueue {\n                if self\n                    .tx\n                    .send_timeout(\n                        DnsResolveRequest { addr, with_asinfo },\n                        RESOLVER_QUEUE_TIMEOUT,\n                    )\n                    .is_ok()\n                {\n                    dns_entry\n                } else {\n                    *self\n                        .addr_cache\n                        .write()\n                        .get_mut(&addr)\n                        .expect(\"addr must be in cache\") =\n                        CacheEntry::new(DnsEntry::Timeout(addr), now);\n                    CacheEntry::new(DnsEntry::Timeout(addr), now)\n                }\n            } else {\n                dns_entry\n            }\n        }\n\n        pub fn flush(&self) {\n            self.addr_cache.write().clear();\n        }\n    }\n\n    /// Process each `IpAddr` from the resolver queue and perform the reverse DNS lookup.\n    ///\n    /// For each `IpAddr`, perform the reverse DNS lookup and update the cache with the result\n    /// (`Resolved`, `NotFound`, `Timeout` or `Failed`) for that addr.\n    fn resolver_queue_processor(\n        rx: Receiver<DnsResolveRequest>,\n        provider: &DnsProvider,\n        cache: &Cache,\n    ) {\n        for DnsResolveRequest { addr, with_asinfo } in rx {\n            let dns_entry = reverse_lookup(provider, addr, with_asinfo);\n            cache.write().insert(addr, dns_entry);\n        }\n    }\n\n    fn reverse_lookup(provider: &DnsProvider, addr: IpAddr, with_asinfo: bool) -> CacheEntry {\n        let now = SystemTime::now();\n        match &provider {\n            DnsProvider::DnsLookup => {\n                // we can't distinguish between a failed lookup or a genuine error, and so we just\n                // assume all failures are `DnsEntry::NotFound`.\n                match dns_lookup::lookup_addr(&addr) {\n                    Ok(dns) => {\n                        CacheEntry::new(DnsEntry::Resolved(Resolved::Normal(addr, vec![dns])), now)\n                    }\n                    Err(_) => CacheEntry::new(DnsEntry::NotFound(Unresolved::Normal(addr)), now),\n                }\n            }\n            DnsProvider::TrustDns(resolver) => match resolver.reverse_lookup(addr) {\n                Ok(name) => {\n                    let hostnames = name\n                        .into_iter()\n                        .map(|mut s| {\n                            s.0.set_fqdn(false);\n                            s\n                        })\n                        .map(|s| s.to_string())\n                        .collect();\n                    if with_asinfo {\n                        let as_info = lookup_asinfo(resolver, addr).unwrap_or_default();\n                        CacheEntry::new(\n                            DnsEntry::Resolved(Resolved::WithAsInfo(addr, hostnames, as_info)),\n                            now,\n                        )\n                    } else {\n                        CacheEntry::new(DnsEntry::Resolved(Resolved::Normal(addr, hostnames)), now)\n                    }\n                }\n                Err(err) => match err.kind() {\n                    ResolveErrorKind::NoRecordsFound { .. } => {\n                        if with_asinfo {\n                            let as_info = lookup_asinfo(resolver, addr).unwrap_or_default();\n                            CacheEntry::new(\n                                DnsEntry::NotFound(Unresolved::WithAsInfo(addr, as_info)),\n                                now,\n                            )\n                        } else {\n                            CacheEntry::new(DnsEntry::NotFound(Unresolved::Normal(addr)), now)\n                        }\n                    }\n                    ResolveErrorKind::Timeout => CacheEntry::new(DnsEntry::Timeout(addr), now),\n                    _ => CacheEntry::new(DnsEntry::Failed(addr), now),\n                },\n            },\n        }\n    }\n\n    /// Lookup up `AsInfo` for an `IpAddr` address.\n    fn lookup_asinfo(resolver: &Arc<Resolver>, addr: IpAddr) -> Result<AsInfo> {\n        let origin_query_txt = match addr {\n            IpAddr::V4(addr) => query_asn_ipv4(resolver, addr)?,\n            IpAddr::V6(addr) => query_asn_ipv6(resolver, addr)?,\n        };\n        let asinfo = parse_origin_query_txt(&origin_query_txt)?;\n        let asn_query_txt = query_asn_name(resolver, &asinfo.asn)?;\n        let as_name = parse_asn_query_txt(&asn_query_txt)?;\n        Ok(AsInfo {\n            asn: asinfo.asn,\n            prefix: asinfo.prefix,\n            cc: asinfo.cc,\n            registry: asinfo.registry,\n            allocated: asinfo.allocated,\n            name: as_name,\n        })\n    }\n\n    /// Perform the `origin` query.\n    fn query_asn_ipv4(resolver: &Arc<Resolver>, addr: Ipv4Addr) -> Result<String> {\n        let query = format!(\n            \"{}.origin.asn.cymru.com.\",\n            addr.octets().iter().rev().join(\".\")\n        );\n        let name = Name::from_str(query.as_str()).map_err(proto_error)?;\n        let response = resolver\n            .lookup(name, RecordType::TXT)\n            .map_err(resolve_error)?;\n        let data = response\n            .iter()\n            .next()\n            .ok_or_else(|| Error::QueryAsnOriginFailed)?;\n        let bytes = data.as_txt().ok_or_else(|| Error::QueryAsnOriginFailed)?;\n        Ok(bytes.to_string())\n    }\n\n    /// Perform the `origin` query.\n    fn query_asn_ipv6(resolver: &Arc<Resolver>, addr: Ipv6Addr) -> Result<String> {\n        let query = format!(\n            \"{:x}.origin6.asn.cymru.com.\",\n            addr.octets()\n                .iter()\n                .rev()\n                .flat_map(|o| [o & 0x0F, (o & 0xF0) >> 4])\n                .format(\".\")\n        );\n        let name = Name::from_str(query.as_str()).map_err(proto_error)?;\n        let response = resolver\n            .lookup(name, RecordType::TXT)\n            .map_err(resolve_error)?;\n        let data = response\n            .iter()\n            .next()\n            .ok_or_else(|| Error::QueryAsnOriginFailed)?;\n        let bytes = data.as_txt().ok_or_else(|| Error::QueryAsnOriginFailed)?;\n        Ok(bytes.to_string())\n    }\n\n    /// Perform the `asn` query.\n    fn query_asn_name(resolver: &Arc<Resolver>, asn: &str) -> Result<String> {\n        let query = format!(\"AS{asn}.asn.cymru.com.\");\n        let name = Name::from_str(query.as_str()).map_err(proto_error)?;\n        let response = resolver\n            .lookup(name, RecordType::TXT)\n            .map_err(resolve_error)?;\n        let data = response\n            .iter()\n            .next()\n            .ok_or_else(|| Error::QueryAsnFailed)?;\n        let bytes = data.as_txt().ok_or_else(|| Error::QueryAsnFailed)?;\n        Ok(bytes.to_string())\n    }\n\n    /// The `origin` DNS query returns a TXT record in the formal:\n    ///      `asn | prefix | cc | registry | allocated`\n    ///\n    /// For example:\n    ///      `12301 | 81.0.100.0/22 | HU | ripencc | 2001-12-06`\n    ///\n    /// From this we extract all fields.\n    fn parse_origin_query_txt(origin_query_txt: &str) -> Result<AsInfo> {\n        if origin_query_txt.chars().filter(|c| *c == '|').count() != 4 {\n            return Err(Error::ParseOriginQueryFailed(String::from(\n                origin_query_txt,\n            )));\n        }\n        let mut split = origin_query_txt.split('|');\n        let asn = split.next().unwrap_or_default().trim().to_string();\n        let prefix = split.next().unwrap_or_default().trim().to_string();\n        let cc = split.next().unwrap_or_default().trim().to_string();\n        let registry = split.next().unwrap_or_default().trim().to_string();\n        let allocated = split.next().unwrap_or_default().trim().to_string();\n        Ok(AsInfo {\n            asn,\n            prefix,\n            cc,\n            registry,\n            allocated,\n            name: String::default(),\n        })\n    }\n\n    /// The `asn` DNS query returns a TXT record in the formal:\n    ///      `asn | cc | registry | allocated | name`\n    ///\n    /// For example:\n    ///      `12301 | HU | ripencc | 1999-02-25 | INVITECH, HU`\n    ///\n    /// From this we extract the 4th field (name, `INVITECH, HU` in this example)\n    fn parse_asn_query_txt(asn_query_txt: &str) -> Result<String> {\n        if asn_query_txt.chars().filter(|c| *c == '|').count() != 4 {\n            return Err(Error::ParseAsnQueryFailed(String::from(asn_query_txt)));\n        }\n        let mut split = asn_query_txt.split('|');\n        Ok(split.nth(4).unwrap_or_default().trim().to_string())\n    }\n\n    /// Convert a `ResolveError` to an `Error::LookupFailed`.\n    fn resolve_error(err: ResolveError) -> Error {\n        Error::LookupFailed(Box::new(err))\n    }\n\n    /// Convert a `ProtoError` to an `Error::LookupFailed`.\n    fn proto_error(err: ProtoError) -> Error {\n        Error::LookupFailed(Box::new(err))\n    }\n}\n"
  },
  {
    "path": "crates/trippy-dns/src/lib.rs",
    "content": "//! This crate provides a cheaply cloneable, non-blocking, caching, forward\n//! and reverse DNS resolver which support the ability to lookup Autonomous\n//! System (AS) information.\n//!\n//! Only a single reverse DNS lookup is performed (lazily) regardless of how\n//! often the lookup is performed unless:\n//! - the previous lookup failed with `DnsEntry::Timeout(_)`\n//! - the previous lookup is older than the configured time-to-live (TTL)\n//!\n//! # Example\n//!\n//! The following example perform a reverse DNS lookup and loop until it is\n//! resolved or fails.  The lookup uses the Cloudflare 1.1.1.1 public DNS\n//! service.\n//!\n//! ```no_run\n//! # fn main() -> anyhow::Result<()> {\n//! # use std::net::IpAddr;\n//! # use std::str::FromStr;\n//! # use std::thread::sleep;\n//! # use std::time::Duration;\n//! use trippy_dns::{\n//!     Config, DnsEntry, DnsResolver, IpAddrFamily, ResolveMethod, Resolved, Resolver, Unresolved,\n//! };\n//!\n//! let config = Config::new(\n//!     ResolveMethod::Cloudflare,\n//!     IpAddrFamily::Ipv4Only,\n//!     Duration::from_secs(5),\n//!     Duration::from_secs(300),\n//! );\n//! let resolver = DnsResolver::start(config)?;\n//! let addr = IpAddr::from_str(\"1.1.1.1\")?;\n//! loop {\n//!     let entry = resolver.lazy_reverse_lookup_with_asinfo(addr);\n//!     match entry {\n//!         DnsEntry::Pending(ip) => {\n//!             println!(\"lookup of {ip} is pending, sleeping for 1 sec\");\n//!             sleep(Duration::from_secs(1));\n//!         }\n//!         DnsEntry::Resolved(Resolved::Normal(ip, addrs)) => {\n//!             println!(\"lookup of {ip} resolved to {addrs:?}\");\n//!             return Ok(());\n//!         }\n//!         DnsEntry::Resolved(Resolved::WithAsInfo(ip, addrs, as_info)) => {\n//!             println!(\"lookup of {ip} resolved to {addrs:?} with AS information {as_info:?}\");\n//!             return Ok(());\n//!         }\n//!         DnsEntry::NotFound(Unresolved::Normal(ip)) => {\n//!             println!(\"lookup of {ip} did not match any records\");\n//!             return Ok(());\n//!         }\n//!         DnsEntry::NotFound(Unresolved::WithAsInfo(ip, as_info)) => {\n//!             println!(\n//!                 \"lookup of {ip} did not match any records with AS information {as_info:?}\"\n//!             );\n//!             return Ok(());\n//!         }\n//!         DnsEntry::Timeout(ip) => {\n//!             println!(\"lookup of {ip} timed out\");\n//!             return Ok(());\n//!         }\n//!         DnsEntry::Failed(ip) => {\n//!             println!(\"lookup of {ip} failed\");\n//!             return Ok(());\n//!         }\n//!     }\n//! }\n//! # Ok(())\n//! # }\n//! ```\n#![forbid(unsafe_code)]\n\nmod config;\nmod lazy_resolver;\nmod resolver;\n\npub use config::{Builder, Config};\npub use lazy_resolver::{DnsResolver, IpAddrFamily, ResolveMethod};\npub use resolver::{AsInfo, DnsEntry, Error, Resolved, Resolver, Result, Unresolved};\n"
  },
  {
    "path": "crates/trippy-dns/src/resolver.rs",
    "content": "use std::fmt::{Display, Formatter};\nuse std::net::IpAddr;\nuse thiserror::Error;\n\n/// A DNS resolver.\npub trait Resolver {\n    /// Perform a blocking DNS hostname lookup and return the resolved IPv4 or IPv6 addresses.\n    fn lookup(&self, hostname: impl AsRef<str>) -> Result<ResolvedIpAddrs>;\n\n    /// Perform a blocking reverse DNS lookup of `IpAddr` and return a `DnsEntry`.\n    ///\n    /// As this method is blocking it will never return a `DnsEntry::Pending`.\n    #[must_use]\n    fn reverse_lookup(&self, addr: impl Into<IpAddr>) -> DnsEntry;\n\n    /// Perform a blocking reverse DNS lookup of `IpAddr` and return a `DnsEntry` with `AS`\n    /// information.\n    ///\n    /// See [`Resolver::reverse_lookup`]\n    #[must_use]\n    fn reverse_lookup_with_asinfo(&self, addr: impl Into<IpAddr>) -> DnsEntry;\n\n    /// Perform a lazy reverse DNS lookup of `IpAddr` and return a `DnsEntry`.\n    ///\n    /// If the `IpAddr` has already been resolved then `DnsEntry::Resolved` is returned immediately.\n    ///\n    /// Otherwise, the `IpAddr` is enqueued to be resolved in the background and a\n    /// `DnsEntry::Pending` is returned.\n    ///\n    /// If the entry exists but is `DnsEntry::Timeout` then it is changed to be `DnsEntry::Pending`\n    /// and enqueued.\n    ///\n    /// If enqueuing times out then the entry is changed to be `DnsEntry::Timeout` and returned.\n    #[must_use]\n    fn lazy_reverse_lookup(&self, addr: impl Into<IpAddr>) -> DnsEntry;\n\n    /// Perform a lazy reverse DNS lookup of `IpAddr` and return a `DnsEntry` with `AS` information.\n    ///\n    /// See [`Resolver::lazy_reverse_lookup`]\n    #[must_use]\n    fn lazy_reverse_lookup_with_asinfo(&self, addr: impl Into<IpAddr>) -> DnsEntry;\n}\n\n/// A DNS resolver error result.\npub type Result<T> = std::result::Result<T, Error>;\n\n/// A DNS resolver error.\n#[derive(Error, Debug)]\npub enum Error {\n    #[error(\"DNS lookup failed\")]\n    LookupFailed(Box<dyn std::error::Error + Send + Sync + 'static>),\n    #[error(\"ASN origin query failed\")]\n    QueryAsnOriginFailed,\n    #[error(\"ASN query failed\")]\n    QueryAsnFailed,\n    #[error(\"origin query txt parse failed: {0}\")]\n    ParseOriginQueryFailed(String),\n    #[error(\"asn query txt parse failed: {0}\")]\n    ParseAsnQueryFailed(String),\n}\n\n/// The output of a successful DNS lookup.\n#[derive(Debug, Clone)]\npub struct ResolvedIpAddrs(pub(super) Vec<IpAddr>);\n\nimpl ResolvedIpAddrs {\n    pub fn iter(&self) -> impl Iterator<Item = &'_ IpAddr> {\n        self.0.iter()\n    }\n}\n\nimpl IntoIterator for ResolvedIpAddrs {\n    type Item = IpAddr;\n    type IntoIter = std::vec::IntoIter<Self::Item>;\n\n    fn into_iter(self) -> Self::IntoIter {\n        self.0.into_iter()\n    }\n}\n\n/// The state of reverse DNS resolution.\n#[derive(Debug, Clone)]\npub enum DnsEntry {\n    /// The reverse DNS resolution of `IpAddr` is pending.\n    Pending(IpAddr),\n    /// The reverse DNS resolution of `IpAddr` has resolved.\n    Resolved(Resolved),\n    /// The `IpAddr` could not be resolved.\n    NotFound(Unresolved),\n    /// The reverse DNS resolution of `IpAddr` failed.\n    Failed(IpAddr),\n    /// The reverse DNS resolution of `IpAddr` timed out.\n    Timeout(IpAddr),\n}\n\n/// The resolved hostnames of a `DnsEntry`.\n#[derive(Debug, Clone)]\npub struct ResolvedHostnames<'a>(pub(super) std::slice::Iter<'a, String>);\n\nimpl<'a> Iterator for ResolvedHostnames<'a> {\n    type Item = &'a str;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.0.next().map(String::as_str)\n    }\n}\n\nimpl DnsEntry {\n    /// The resolved hostnames.\n    #[must_use]\n    pub fn hostnames(&self) -> ResolvedHostnames<'_> {\n        match self {\n            Self::Resolved(Resolved::WithAsInfo(_, hosts, _) | Resolved::Normal(_, hosts)) => {\n                ResolvedHostnames(hosts.iter())\n            }\n            Self::Pending(_) | Self::Timeout(_) | Self::NotFound(_) | Self::Failed(_) => {\n                ResolvedHostnames([].iter())\n            }\n        }\n    }\n}\n\n/// Information about a resolved `IpAddr`.\n#[derive(Debug, Clone)]\npub enum Resolved {\n    /// Resolved without `AsInfo`.\n    Normal(IpAddr, Vec<String>),\n    /// Resolved with `AsInfo`.\n    WithAsInfo(IpAddr, Vec<String>, AsInfo),\n}\n\n/// Information about an unresolved `IpAddr`.\n#[derive(Debug, Clone)]\npub enum Unresolved {\n    /// Unresolved without `AsInfo`.\n    Normal(IpAddr),\n    /// Unresolved with `AsInfo`.\n    WithAsInfo(IpAddr, AsInfo),\n}\n\n/// Information about an autonomous System (AS).\n#[derive(Debug, Clone, Default)]\npub struct AsInfo {\n    /// The autonomous system Number.\n    ///\n    /// This is returned without the AS prefix i.e. `12301`.\n    pub asn: String,\n    /// The AS prefix.\n    ///\n    /// Given in CIDR notation i.e. `81.0.100.0/22`.\n    pub prefix: String,\n    /// The country code.\n    ///\n    /// Given as a ISO format i.e. `HU`.\n    pub cc: String,\n    /// AS registry name.\n    ///\n    /// Given as a string i.e. `ripencc`.\n    pub registry: String,\n    /// Allocation date.\n    ///\n    /// Given as an ISO date i.e. `1999-02-25`.\n    pub allocated: String,\n    /// The autonomous system (AS) Name.\n    ///\n    /// Given as a string i.e. `INVITECH, HU`.\n    pub name: String,\n}\n\nimpl Display for DnsEntry {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        #[expect(clippy::match_same_arms)]\n        match self {\n            Self::Resolved(Resolved::Normal(_, hosts)) => write!(f, \"{}\", hosts.join(\" \")),\n            Self::Resolved(Resolved::WithAsInfo(_, hosts, asinfo)) => {\n                write!(f, \"AS{} {}\", asinfo.asn, hosts.join(\" \"))\n            }\n            Self::Pending(ip) => write!(f, \"{ip}\"),\n            Self::Timeout(ip) => write!(f, \"Timeout: {ip}\"),\n            Self::NotFound(Unresolved::Normal(ip)) => write!(f, \"{ip}\"),\n            Self::NotFound(Unresolved::WithAsInfo(ip, asinfo)) => {\n                write!(f, \"AS{} {}\", asinfo.asn, ip)\n            }\n            Self::Failed(ip) => write!(f, \"Failed: {ip}\"),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::net::IpAddr;\n    use std::str::FromStr;\n\n    #[test]\n    fn test_iterator_returns_each_hostname_once() {\n        let entry = DnsEntry::Resolved(Resolved::Normal(\n            IpAddr::from_str(\"1.1.1.1\").unwrap(),\n            vec![\"one\".to_string(), \"two\".to_string(), \"three\".to_string()],\n        ));\n\n        let mut iter = entry.hostnames();\n        assert_eq!(iter.next(), Some(\"one\"));\n        assert_eq!(iter.next(), Some(\"two\"));\n        assert_eq!(iter.next(), Some(\"three\"));\n        assert_eq!(iter.next(), None);\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/Cargo.toml",
    "content": "[package]\nname = \"trippy-packet\"\ndescription = \"Network packets for Trippy\"\nversion.workspace = true\nauthors.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\nreadme.workspace = true\nlicense.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nkeywords.workspace = true\ncategories.workspace = true\n\n[dependencies]\nitertools.workspace = true\nthiserror.workspace = true\n\n[dev-dependencies]\nanyhow.workspace = true\nhex-literal.workspace = true\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/trippy-packet/src/buffer.rs",
    "content": "/// A byte buffer that holds a mutable or immutable byte slice.\n#[derive(Debug)]\npub enum Buffer<'a> {\n    Immutable(&'a [u8]),\n    Mutable(&'a mut [u8]),\n}\n\nimpl Buffer<'_> {\n    /// access the buffer as an immutable slice of bytes.\n    pub fn as_slice(&self) -> &[u8] {\n        match &self {\n            Buffer::Immutable(packet) => packet,\n            Buffer::Mutable(packet) => packet,\n        }\n    }\n\n    /// Get N bytes from the packet at a given byte offset.\n    pub fn get_bytes<const N: usize>(&self, offset: usize) -> [u8; N] {\n        core::array::from_fn(|i| self.read(offset + i))\n    }\n\n    /// Set N bytes in the packet at a given offset.\n    pub fn set_bytes<const N: usize>(&mut self, offset: usize, bytes: [u8; N]) {\n        self.as_slice_mut()[offset..offset + N].copy_from_slice(&bytes);\n    }\n\n    /// Get the value at a given offset.\n    pub fn read(&self, offset: usize) -> u8 {\n        match &self {\n            Buffer::Immutable(packet) => packet[offset],\n            Buffer::Mutable(packet) => packet[offset],\n        }\n    }\n\n    /// Set the value at a given offset.\n    pub fn write(&mut self, offset: usize) -> &mut u8 {\n        match self {\n            Buffer::Immutable(_) => panic!(\"write operation called on readonly buffer\"),\n            Buffer::Mutable(packet) => &mut packet[offset],\n        }\n    }\n\n    /// access the buffer as a mutable slice of bytes.\n    pub fn as_slice_mut(&mut self) -> &mut [u8] {\n        match self {\n            Buffer::Immutable(_) => panic!(\"write operation called on readonly buffer\"),\n            Buffer::Mutable(packet) => packet,\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_immutable_buffer() {\n        let buf = [0_u8; 5];\n        let buffer = Buffer::Immutable(&buf);\n        assert_eq!(buf.as_slice(), buffer.as_slice());\n        assert_eq!(buf, buffer.get_bytes(0));\n        assert_eq!(0_u8, buffer.read(0));\n    }\n\n    #[test]\n    fn test_mutable_buffer() {\n        let mut buf = [0_u8; 5];\n        let mut buffer = Buffer::Mutable(&mut buf);\n        assert_eq!(&[0_u8; 5], buffer.as_slice());\n        assert_eq!([0_u8; 5], buffer.get_bytes(0));\n        assert_eq!(0_u8, buffer.read(0));\n        buffer.set_bytes(1, [1_u8; 4]);\n        assert_eq!([1_u8; 4], buffer.get_bytes(1));\n        *buffer.write(0) = 2;\n        assert_eq!(2_u8, buffer.read(0));\n        buffer.as_slice_mut().copy_from_slice(&[3_u8; 5]);\n        assert_eq!(&[3_u8; 5], buffer.as_slice());\n    }\n\n    #[test]\n    fn test_debug() {\n        let buf = [0_u8; 5];\n        let buffer = Buffer::Immutable(&buf);\n        assert_eq!(\n            String::from(\"Immutable([0, 0, 0, 0, 0])\"),\n            format!(\"{buffer:?}\")\n        );\n        let mut buf = [0_u8; 5];\n        let buffer = Buffer::Mutable(&mut buf);\n        assert_eq!(\n            String::from(\"Mutable([0, 0, 0, 0, 0])\"),\n            format!(\"{buffer:?}\")\n        );\n    }\n\n    #[test]\n    #[should_panic(expected = \"write operation called on readonly buffer\")]\n    fn test_immutable_buffer_cannot_write() {\n        let buf = [0_u8; 5];\n        let mut buffer = Buffer::Immutable(&buf);\n        buffer.set_bytes(0, [1_u8; 5]);\n    }\n\n    #[test]\n    #[should_panic(expected = \"write operation called on readonly buffer\")]\n    fn test_immutable_buffer_cannot_mut_slice() {\n        let buf = [0_u8; 5];\n        let mut buffer = Buffer::Immutable(&buf);\n        buffer.as_slice_mut();\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/checksum.rs",
    "content": "//! Checksum implementations for ICMP & UDP over IPv4 and IPV6.\n//!\n//! This code is derived from [`libpnet`] which is available under the Apache 2.0 license.\n//!\n//! [`libpnet`]: https://github.com/libpnet/libpnet\n\nuse crate::IpProtocol;\nuse std::net::{Ipv4Addr, Ipv6Addr};\n\n/// Calculate the checksum for an `Ipv4` header.\n#[must_use]\npub fn ipv4_header_checksum(data: &[u8]) -> u16 {\n    checksum(data, 5)\n}\n\n/// Calculate the checksum for an `Ipv4` `ICMP` packet.\n#[must_use]\npub fn icmp_ipv4_checksum(data: &[u8]) -> u16 {\n    checksum(data, 1)\n}\n\n/// Calculate the checksum for an `Ipv4` `ICMP` packet.\n#[must_use]\npub fn icmp_ipv6_checksum(data: &[u8], src_addr: Ipv6Addr, dest_addr: Ipv6Addr) -> u16 {\n    ipv6_checksum(data, 1, src_addr, dest_addr, IpProtocol::IcmpV6)\n}\n\n/// Calculate the checksum for an `IPv4` `UDP` packet.\n#[must_use]\npub fn udp_ipv4_checksum(data: &[u8], src_addr: Ipv4Addr, dest_addr: Ipv4Addr) -> u16 {\n    ipv4_checksum(data, 3, src_addr, dest_addr, IpProtocol::Udp)\n}\n\n/// Calculate the checksum for an `IPv4` `TCP` packet.\n#[must_use]\npub fn tcp_ipv4_checksum(data: &[u8], src_addr: Ipv4Addr, dest_addr: Ipv4Addr) -> u16 {\n    ipv4_checksum(data, 8, src_addr, dest_addr, IpProtocol::Tcp)\n}\n\n/// Calculate the checksum for an `IPv6` `UDP` packet.\n#[must_use]\npub fn udp_ipv6_checksum(data: &[u8], src_addr: Ipv6Addr, dest_addr: Ipv6Addr) -> u16 {\n    ipv6_checksum(data, 3, src_addr, dest_addr, IpProtocol::Udp)\n}\n\n/// Calculate the checksum for an `IPv6` `TCP` packet.\n#[must_use]\npub fn tcp_ipv6_checksum(data: &[u8], src_addr: Ipv6Addr, dest_addr: Ipv6Addr) -> u16 {\n    ipv6_checksum(data, 8, src_addr, dest_addr, IpProtocol::Tcp)\n}\n\nfn checksum(data: &[u8], ignore_word: usize) -> u16 {\n    if data.is_empty() {\n        return 0;\n    }\n    let sum = sum_be_words(data, ignore_word);\n    finalize_checksum(sum)\n}\n\nfn ipv4_checksum(\n    data: &[u8],\n    ignore_word: usize,\n    source: Ipv4Addr,\n    destination: Ipv4Addr,\n    next_level_protocol: IpProtocol,\n) -> u16 {\n    let mut sum = 0u32;\n    sum += ipv4_word_sum(source);\n    sum += ipv4_word_sum(destination);\n    sum += u32::from(next_level_protocol.id());\n    sum += data.len() as u32;\n    sum += sum_be_words(data, ignore_word);\n    finalize_checksum(sum)\n}\n\nfn ipv4_word_sum(ip: Ipv4Addr) -> u32 {\n    let octets = ip.octets();\n    (((u32::from(octets[0])) << 8) | u32::from(octets[1]))\n        + (((u32::from(octets[2])) << 8) | u32::from(octets[3]))\n}\n\n/// Calculate the checksum for a packet built on IPv6.\nfn ipv6_checksum(\n    data: &[u8],\n    ignore_word: usize,\n    source: Ipv6Addr,\n    destination: Ipv6Addr,\n    next_level_protocol: IpProtocol,\n) -> u16 {\n    let mut sum = 0u32;\n    sum += ipv6_word_sum(source);\n    sum += ipv6_word_sum(destination);\n    sum += u32::from(next_level_protocol.id());\n    sum += data.len() as u32;\n    sum += sum_be_words(data, ignore_word);\n    finalize_checksum(sum)\n}\n\nfn ipv6_word_sum(ip: Ipv6Addr) -> u32 {\n    ip.segments().iter().map(|x| u32::from(*x)).sum()\n}\n\nfn sum_be_words(data: &[u8], ignore_word: usize) -> u32 {\n    if data.is_empty() {\n        return 0;\n    }\n    let len = data.len();\n    let mut cur_data = data;\n    let mut sum = 0u32;\n    let mut i = 0;\n    while cur_data.len() >= 2 {\n        if i != ignore_word {\n            sum += u32::from(u16::from_be_bytes(cur_data[0..2].try_into().unwrap()));\n        }\n        cur_data = &cur_data[2..];\n        i += 1;\n    }\n    if i != ignore_word && len & 1 != 0 {\n        sum += u32::from(data[len - 1]) << 8;\n    }\n    sum\n}\n\nconst fn finalize_checksum(mut sum: u32) -> u16 {\n    while sum >> 16 != 0 {\n        sum = (sum >> 16) + (sum & 0xFFFF);\n    }\n    !sum as u16\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use hex_literal::hex;\n    use std::str::FromStr;\n\n    #[test]\n    fn test_empty_ipv4_checksum() {\n        let src_addr = Ipv4Addr::from_str(\"192.168.1.201\").unwrap();\n        let dest_addr = Ipv4Addr::from_str(\"142.250.66.46\").unwrap();\n        assert_eq!(0, ipv4_header_checksum(&[]));\n        assert_eq!(0, icmp_ipv4_checksum(&[]));\n        assert_eq!(27732, udp_ipv4_checksum(&[], src_addr, dest_addr));\n        assert_eq!(27743, tcp_ipv4_checksum(&[], src_addr, dest_addr));\n    }\n\n    #[test]\n    fn test_empty_ipv6_checksum() {\n        let src_addr = Ipv6Addr::from_str(\"fe80::811:3f6:7601:6c3f\").unwrap();\n        let dest_addr = Ipv6Addr::from_str(\"fe80::1c8d:7d69:d0b6:8182\").unwrap();\n        assert_eq!(10316, icmp_ipv6_checksum(&[], src_addr, dest_addr));\n        assert_eq!(10357, udp_ipv6_checksum(&[], src_addr, dest_addr));\n    }\n\n    #[test]\n    fn test_odd_length() {\n        assert_eq!(65535, ipv4_header_checksum(&[0x00]));\n    }\n\n    #[test]\n    fn test_icmp_ipv4_checksum() {\n        let bytes = [\n            0x0b, 0x00, 0x88, 0xeb, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x54, 0xb0, 0xde,\n            0x00, 0x00, 0x01, 0x11, 0x75, 0x21, 0xc0, 0xa8, 0x01, 0xc9, 0x8e, 0xfa, 0x42, 0x2e,\n            0x62, 0x57, 0x81, 0x95, 0x00, 0x40, 0x87, 0xe7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n        ];\n        assert_eq!(35051, icmp_ipv4_checksum(&bytes));\n    }\n\n    #[test]\n    fn test_icmp_ipv6_checksum() {\n        let src_addr = Ipv6Addr::from_str(\"fe80::811:3f6:7601:6c3f\").unwrap();\n        let dest_addr = Ipv6Addr::from_str(\"fe80::1c8d:7d69:d0b6:8182\").unwrap();\n        let bytes = [\n            0x88, 0x00, 0x73, 0x6a, 0x40, 0x00, 0x00, 0x00, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x08, 0x11, 0x03, 0xf6, 0x76, 0x01, 0x6c, 0x3f,\n        ];\n        assert_eq!(29546, icmp_ipv6_checksum(&bytes, src_addr, dest_addr));\n    }\n\n    #[test]\n    fn test_udp_ipv4_checksum() {\n        let src_addr = Ipv4Addr::from_str(\"192.168.1.201\").unwrap();\n        let dest_addr = Ipv4Addr::from_str(\"142.250.66.46\").unwrap();\n        let bytes = [\n            0x62, 0x57, 0x81, 0xa8, 0x00, 0x40, 0x87, 0xd4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n        ];\n        assert_eq!(34772, udp_ipv4_checksum(&bytes, src_addr, dest_addr));\n    }\n\n    #[test]\n    fn test_udp_ipv6_checksum() {\n        let src_addr = Ipv6Addr::from_str(\"2406:da18:599:2d01:fa25:98be:5ab1:87a5\").unwrap();\n        let dest_addr = Ipv6Addr::from_str(\"2404:6800:4003:c02::8b\").unwrap();\n        let bytes = [\n            0x10, 0x13, 0x80, 0xeb, 0x00, 0x2c, 0xf0, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00,\n        ];\n        assert_eq!(61454, udp_ipv6_checksum(&bytes, src_addr, dest_addr));\n    }\n\n    #[test]\n    fn test_tcp_ipv6_checksum() {\n        let src_addr = Ipv6Addr::from_str(\"fd7a:115c:a1e0:ab12:4843:cd96:6263:82a\").unwrap();\n        let dest_addr = Ipv6Addr::from_str(\"2404:6800:4003:c03::bc\").unwrap();\n        let bytes = hex!(\"fa 54 14 6c 96 16 44 89 08 f0 39 2b 50 10 08 00 c7 60 00 00\");\n        assert_eq!(0xc760, tcp_ipv6_checksum(&bytes, src_addr, dest_addr));\n    }\n\n    #[test]\n    fn test_ipv4_header_checksum() {\n        let bytes = hex!(\"45 00 0f fc 38 c0 00 00 40 01 2e 3b 0a 00 00 02 0a 00 00 01\");\n        assert_eq!(0x1e3f, ipv4_header_checksum(&bytes));\n    }\n\n    #[test]\n    fn test_tcp_ipv4_checksum() {\n        let bytes = hex!(\"00 50 80 ea 00 00 00 00 95 9d 2e c7 50 12 ff ff 55 cc 00 00\");\n        assert_eq!(\n            0x55cc,\n            tcp_ipv4_checksum(\n                &bytes,\n                Ipv4Addr::new(10, 0, 0, 103),\n                Ipv4Addr::new(10, 0, 0, 1)\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/error.rs",
    "content": "use thiserror::Error;\n\n/// A packet error result.\npub type Result<T> = std::result::Result<T, Error>;\n\n/// A packet error.\n#[derive(Error, Debug, Eq, PartialEq)]\npub enum Error {\n    /// Attempting to create a packet with an insufficient buffer size.\n    #[error(\"insufficient buffer for {0} packet, minimum={1}, provided={2}\")]\n    InsufficientPacketBuffer(String, usize, usize),\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/icmp_extension.rs",
    "content": "pub mod extension_structure {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::icmp_extension::extension_object::ExtensionObjectPacket;\n\n    /// Represents an ICMP `ExtensionsPacket` pseudo object.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct ExtensionsPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> ExtensionsPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"ExtensionsPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"ExtensionsPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            4\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn header(&self) -> &[u8] {\n            &self.buf.as_slice()[..Self::minimum_packet_size()]\n        }\n\n        /// An iterator of Extension Objects contained within this `ExtensionsPacket`.\n        #[must_use]\n        pub const fn objects(&self) -> ExtensionObjectIter<'_> {\n            ExtensionObjectIter::new(&self.buf)\n        }\n    }\n\n    pub struct ExtensionObjectIter<'a> {\n        buf: &'a Buffer<'a>,\n        offset: usize,\n    }\n\n    impl<'a> ExtensionObjectIter<'a> {\n        #[must_use]\n        pub const fn new(buf: &'a Buffer<'_>) -> Self {\n            Self {\n                buf,\n                offset: ExtensionsPacket::minimum_packet_size(),\n            }\n        }\n    }\n\n    impl<'a> Iterator for ExtensionObjectIter<'a> {\n        type Item = &'a [u8];\n\n        fn next(&mut self) -> Option<Self::Item> {\n            let buf_slice = self.buf.as_slice();\n            if self.offset > buf_slice.len() {\n                None\n            } else {\n                let object_bytes = &buf_slice[self.offset..];\n                if let Ok(object) = ExtensionObjectPacket::new_view(object_bytes) {\n                    let length = usize::from(object.get_length());\n                    // If a malformed extension object has a length that is less than the minimum\n                    // size or extends beyond the end of available bytes, then we discard it.\n                    if length < ExtensionObjectPacket::minimum_packet_size()\n                        || length > object_bytes.len()\n                    {\n                        return None;\n                    }\n                    self.offset += length;\n                    Some(object_bytes)\n                } else {\n                    None\n                }\n            }\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n        use crate::icmp_extension::extension_header::ExtensionHeaderPacket;\n        use crate::icmp_extension::extension_object::{\n            ClassNum, ClassSubType, ExtensionObjectPacket,\n        };\n\n        #[test]\n        fn test_header() {\n            let buf = [\n                0x20, 0x00, 0x99, 0x3a, 0x00, 0x08, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01,\n            ];\n            let extensions = ExtensionsPacket::new_view(&buf).unwrap();\n            let header = ExtensionHeaderPacket::new_view(extensions.header()).unwrap();\n            assert_eq!(2, header.get_version());\n            assert_eq!(0x993A, header.get_checksum());\n        }\n\n        #[test]\n        fn test_object_iterator() {\n            let buf = [\n                0x20, 0x00, 0x99, 0x3a, 0x00, 0x08, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01,\n            ];\n            let extensions = ExtensionsPacket::new_view(&buf).unwrap();\n            let mut object_iter = extensions.objects();\n            let object_bytes = object_iter.next().unwrap();\n            let object = ExtensionObjectPacket::new_view(object_bytes).unwrap();\n            assert_eq!(8, object.get_length());\n            assert_eq!(\n                ClassNum::MultiProtocolLabelSwitchingLabelStack,\n                object.get_class_num()\n            );\n            assert_eq!(ClassSubType(1), object.get_class_subtype());\n            assert_eq!([0x04, 0xbb, 0x41, 0x01], object.payload());\n            assert!(object_iter.next().is_none());\n        }\n\n        #[test]\n        fn test_object_iterator_zero_length() {\n            let buf = [\n                0x20, 0x00, 0x99, 0x3a, 0x00, 0x00, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01,\n            ];\n            let extensions = ExtensionsPacket::new_view(&buf).unwrap();\n            let mut object_iter = extensions.objects();\n            assert!(object_iter.next().is_none());\n        }\n\n        #[test]\n        fn test_object_iterator_minimum_length() {\n            let buf = [\n                0x20, 0x00, 0x99, 0x3a, 0x00, 0x04, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01,\n            ];\n            let extensions = ExtensionsPacket::new_view(&buf).unwrap();\n            let mut object_iter = extensions.objects();\n            let object_bytes = object_iter.next().unwrap();\n            let object = ExtensionObjectPacket::new_view(object_bytes).unwrap();\n            assert_eq!(4, object.get_length());\n            assert_eq!(0, object.payload().len());\n        }\n\n        #[test]\n        fn test_object_iterator_length_to_short() {\n            let buf = [\n                0x20, 0x00, 0x99, 0x3a, 0x00, 0x03, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01,\n            ];\n            let extensions = ExtensionsPacket::new_view(&buf).unwrap();\n            let mut object_iter = extensions.objects();\n            assert!(object_iter.next().is_none());\n        }\n\n        #[test]\n        fn test_object_iterator_length_to_long() {\n            let buf = [\n                0x20, 0x00, 0x99, 0x3a, 0xa7, 0xdd, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01,\n            ];\n            let extensions = ExtensionsPacket::new_view(&buf).unwrap();\n            let mut object_iter = extensions.objects();\n            assert!(object_iter.next().is_none());\n        }\n    }\n}\n\npub mod extension_header {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use std::fmt::{Debug, Formatter};\n\n    const VERSION_OFFSET: usize = 0;\n    const CHECKSUM_OFFSET: usize = 2;\n\n    /// Represents an ICMP `ExtensionHeaderPacket`.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct ExtensionHeaderPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> ExtensionHeaderPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"ExtensionHeaderPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"ExtensionHeaderPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            4\n        }\n\n        #[must_use]\n        pub fn get_version(&self) -> u8 {\n            (self.buf.read(VERSION_OFFSET) & 0xf0) >> 4\n        }\n\n        #[must_use]\n        pub fn get_checksum(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n        }\n\n        pub fn set_version(&mut self, val: u8) {\n            *self.buf.write(VERSION_OFFSET) =\n                (self.buf.read(VERSION_OFFSET) & 0xf) | ((val & 0xf) << 4);\n        }\n\n        pub fn set_checksum(&mut self, val: u16) {\n            self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n    }\n\n    impl Debug for ExtensionHeaderPacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"ExtensionHeader\")\n                .field(\"version\", &self.get_version())\n                .field(\"checksum\", &self.get_checksum())\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_version() {\n            let mut buf = [0_u8; ExtensionHeaderPacket::minimum_packet_size()];\n            let mut extension = ExtensionHeaderPacket::new(&mut buf).unwrap();\n            extension.set_version(0);\n            assert_eq!(0, extension.get_version());\n            assert_eq!([0x00], extension.packet()[0..1]);\n            extension.set_version(2);\n            assert_eq!(2, extension.get_version());\n            assert_eq!([0x20], extension.packet()[0..1]);\n            extension.set_version(15);\n            assert_eq!(15, extension.get_version());\n            assert_eq!([0xF0], extension.packet()[0..1]);\n        }\n\n        #[test]\n        fn test_checksum() {\n            let mut buf = [0_u8; ExtensionHeaderPacket::minimum_packet_size()];\n            let mut extension = ExtensionHeaderPacket::new(&mut buf).unwrap();\n            extension.set_checksum(0);\n            assert_eq!(0, extension.get_checksum());\n            assert_eq!([0x00, 0x00], extension.packet()[2..=3]);\n            extension.set_checksum(1999);\n            assert_eq!(1999, extension.get_checksum());\n            assert_eq!([0x07, 0xCF], extension.packet()[2..=3]);\n            extension.set_checksum(39226);\n            assert_eq!(39226, extension.get_checksum());\n            assert_eq!([0x99, 0x3A], extension.packet()[2..=3]);\n            extension.set_checksum(u16::MAX);\n            assert_eq!(u16::MAX, extension.get_checksum());\n            assert_eq!([0xFF, 0xFF], extension.packet()[2..=3]);\n        }\n\n        #[test]\n        fn test_extension_header_view() {\n            let buf = [\n                0x20, 0x00, 0x99, 0x3a, 0x00, 0x08, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01,\n            ];\n            let extension = ExtensionHeaderPacket::new_view(&buf).unwrap();\n            assert_eq!(2, extension.get_version());\n            assert_eq!(0x993A, extension.get_checksum());\n        }\n    }\n}\n\npub mod extension_object {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::fmt_payload;\n    use std::fmt::{Debug, Formatter};\n\n    /// The ICMP Extension Object Class Num.\n    #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]\n    pub enum ClassNum {\n        MultiProtocolLabelSwitchingLabelStack,\n        InterfaceInformationObject,\n        InterfaceIdentificationObject,\n        ExtendedInformation,\n        Other(u8),\n    }\n\n    impl ClassNum {\n        #[must_use]\n        pub const fn id(&self) -> u8 {\n            match self {\n                Self::MultiProtocolLabelSwitchingLabelStack => 1,\n                Self::InterfaceInformationObject => 2,\n                Self::InterfaceIdentificationObject => 3,\n                Self::ExtendedInformation => 4,\n                Self::Other(id) => *id,\n            }\n        }\n    }\n\n    impl From<u8> for ClassNum {\n        fn from(val: u8) -> Self {\n            match val {\n                1 => Self::MultiProtocolLabelSwitchingLabelStack,\n                2 => Self::InterfaceInformationObject,\n                3 => Self::InterfaceIdentificationObject,\n                4 => Self::ExtendedInformation,\n                id => Self::Other(id),\n            }\n        }\n    }\n\n    /// The ICMP Extension Object Class Sub-type.\n    #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]\n    pub struct ClassSubType(pub u8);\n\n    impl From<u8> for ClassSubType {\n        fn from(val: u8) -> Self {\n            Self(val)\n        }\n    }\n\n    const LENGTH_OFFSET: usize = 0;\n    const CLASS_NUM_OFFSET: usize = 2;\n    const CLASS_SUBTYPE_OFFSET: usize = 3;\n\n    /// Represents an ICMP `ExtensionObjectPacket`.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct ExtensionObjectPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> ExtensionObjectPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"ExtensionObjectPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"ExtensionObjectPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            4\n        }\n\n        pub fn set_length(&mut self, val: u16) {\n            self.buf.set_bytes(LENGTH_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_class_num(&mut self, val: ClassNum) {\n            *self.buf.write(CLASS_NUM_OFFSET) = val.id();\n        }\n\n        pub fn set_class_subtype(&mut self, val: ClassSubType) {\n            *self.buf.write(CLASS_SUBTYPE_OFFSET) = val.0;\n        }\n\n        pub fn set_payload(&mut self, vals: &[u8]) {\n            let current_offset = Self::minimum_packet_size();\n            self.buf.as_slice_mut()[current_offset..current_offset + vals.len()]\n                .copy_from_slice(vals);\n        }\n\n        #[must_use]\n        pub fn get_length(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(LENGTH_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_class_num(&self) -> ClassNum {\n            ClassNum::from(self.buf.read(CLASS_NUM_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_class_subtype(&self) -> ClassSubType {\n            ClassSubType::from(self.buf.read(CLASS_SUBTYPE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn payload(&self) -> &[u8] {\n            &self.buf.as_slice()[Self::minimum_packet_size()..usize::from(self.get_length())]\n        }\n    }\n\n    impl Debug for ExtensionObjectPacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"ExtensionObject\")\n                .field(\"length\", &self.get_length())\n                .field(\"class_num\", &self.get_class_num())\n                .field(\"class_subtype\", &self.get_class_subtype())\n                .field(\"payload\", &fmt_payload(self.payload()))\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_length() {\n            let mut buf = [0_u8; ExtensionObjectPacket::minimum_packet_size()];\n            let mut extension = ExtensionObjectPacket::new(&mut buf).unwrap();\n            extension.set_length(0);\n            assert_eq!(0, extension.get_length());\n            assert_eq!([0x00, 0x00], extension.packet()[0..=1]);\n            extension.set_length(8);\n            assert_eq!(8, extension.get_length());\n            assert_eq!([0x00, 0x08], extension.packet()[0..=1]);\n            extension.set_length(u16::MAX);\n            assert_eq!(u16::MAX, extension.get_length());\n            assert_eq!([0xFF, 0xFF], extension.packet()[0..=1]);\n        }\n\n        #[test]\n        fn test_class_num() {\n            let mut buf = [0_u8; ExtensionObjectPacket::minimum_packet_size()];\n            let mut extension = ExtensionObjectPacket::new(&mut buf).unwrap();\n            extension.set_class_num(ClassNum::MultiProtocolLabelSwitchingLabelStack);\n            assert_eq!(\n                ClassNum::MultiProtocolLabelSwitchingLabelStack,\n                extension.get_class_num()\n            );\n            assert_eq!([0x01], extension.packet()[2..3]);\n            extension.set_class_num(ClassNum::InterfaceInformationObject);\n            assert_eq!(\n                ClassNum::InterfaceInformationObject,\n                extension.get_class_num()\n            );\n            assert_eq!([0x02], extension.packet()[2..3]);\n            extension.set_class_num(ClassNum::InterfaceIdentificationObject);\n            assert_eq!(\n                ClassNum::InterfaceIdentificationObject,\n                extension.get_class_num()\n            );\n            assert_eq!([0x03], extension.packet()[2..3]);\n            extension.set_class_num(ClassNum::ExtendedInformation);\n            assert_eq!(ClassNum::ExtendedInformation, extension.get_class_num());\n            assert_eq!([0x04], extension.packet()[2..3]);\n            extension.set_class_num(ClassNum::Other(255));\n            assert_eq!(ClassNum::Other(255), extension.get_class_num());\n            assert_eq!([0xFF], extension.packet()[2..3]);\n        }\n\n        #[test]\n        fn test_class_subtype() {\n            let mut buf = [0_u8; ExtensionObjectPacket::minimum_packet_size()];\n            let mut extension = ExtensionObjectPacket::new(&mut buf).unwrap();\n            extension.set_class_subtype(ClassSubType(0));\n            assert_eq!(ClassSubType(0), extension.get_class_subtype());\n            assert_eq!([0x00], extension.packet()[3..4]);\n            extension.set_class_subtype(ClassSubType(1));\n            assert_eq!(ClassSubType(1), extension.get_class_subtype());\n            assert_eq!([0x01], extension.packet()[3..4]);\n            extension.set_class_subtype(ClassSubType(255));\n            assert_eq!(ClassSubType(255), extension.get_class_subtype());\n            assert_eq!([0xff], extension.packet()[3..4]);\n        }\n\n        #[test]\n        fn test_extension_header_view() {\n            let buf = [0x00, 0x08, 0x01, 0x01, 0x04, 0xbb, 0x41, 0x01];\n            let object = ExtensionObjectPacket::new_view(&buf).unwrap();\n            assert_eq!(8, object.get_length());\n            assert_eq!(\n                ClassNum::MultiProtocolLabelSwitchingLabelStack,\n                object.get_class_num()\n            );\n            assert_eq!(ClassSubType(1), object.get_class_subtype());\n            assert_eq!([0x04, 0xbb, 0x41, 0x01], object.payload());\n        }\n    }\n}\n\npub mod mpls_label_stack {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::icmp_extension::mpls_label_stack_member::MplsLabelStackMemberPacket;\n\n    /// Represents an ICMP `MplsLabelStackPacket`.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct MplsLabelStackPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> MplsLabelStackPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"MplsLabelStackPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"MplsLabelStackPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            4\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub const fn members(&self) -> MplsLabelStackIter<'_> {\n            MplsLabelStackIter::new(&self.buf)\n        }\n    }\n\n    pub struct MplsLabelStackIter<'a> {\n        buf: &'a Buffer<'a>,\n        offset: usize,\n        bos: u8,\n    }\n\n    impl<'a> MplsLabelStackIter<'a> {\n        #[must_use]\n        pub const fn new(buf: &'a Buffer<'_>) -> Self {\n            Self {\n                buf,\n                offset: 0,\n                bos: 0,\n            }\n        }\n    }\n\n    impl<'a> Iterator for MplsLabelStackIter<'a> {\n        type Item = &'a [u8];\n\n        fn next(&mut self) -> Option<Self::Item> {\n            if self.bos > 0 || self.offset >= self.buf.as_slice().len() {\n                None\n            } else {\n                let member_bytes = &self.buf.as_slice()[self.offset..];\n                if let Ok(member) = MplsLabelStackMemberPacket::new_view(member_bytes) {\n                    self.bos = member.get_bos();\n                    self.offset += MplsLabelStackMemberPacket::minimum_packet_size();\n                    Some(member_bytes)\n                } else {\n                    None\n                }\n            }\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_stack_member_iterator() {\n            let buf = [0x04, 0xbb, 0x41, 0x01];\n            let stack = MplsLabelStackPacket::new_view(&buf).unwrap();\n            let mut member_iter = stack.members();\n            let member_bytes = member_iter.next().unwrap();\n            let member = MplsLabelStackMemberPacket::new_view(member_bytes).unwrap();\n            assert_eq!(19380, member.get_label());\n            assert_eq!(0, member.get_exp());\n            assert_eq!(1, member.get_bos());\n            assert_eq!(1, member.get_ttl());\n            assert!(member_iter.next().is_none());\n        }\n    }\n}\n\npub mod mpls_label_stack_member {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use std::fmt::{Debug, Formatter};\n\n    const LABEL_OFFSET: usize = 0;\n    const EXP_OFFSET: usize = 2;\n    const BOS_OFFSET: usize = 2;\n    const TTL_OFFSET: usize = 3;\n\n    /// Represents an ICMP `MplsLabelStackMemberPacket`.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct MplsLabelStackMemberPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> MplsLabelStackMemberPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"MplsLabelStackMemberPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"MplsLabelStackMemberPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            4\n        }\n\n        #[must_use]\n        pub fn get_label(&self) -> u32 {\n            u32::from_be_bytes([\n                0x0,\n                self.buf.read(LABEL_OFFSET),\n                self.buf.read(LABEL_OFFSET + 1),\n                self.buf.read(LABEL_OFFSET + 2),\n            ]) >> 4\n        }\n\n        #[must_use]\n        pub fn get_exp(&self) -> u8 {\n            (self.buf.read(EXP_OFFSET) & 0x0e) >> 1\n        }\n\n        #[must_use]\n        pub fn get_bos(&self) -> u8 {\n            self.buf.read(BOS_OFFSET) & 0x01\n        }\n\n        #[must_use]\n        pub fn get_ttl(&self) -> u8 {\n            self.buf.read(TTL_OFFSET)\n        }\n\n        pub fn set_label(&mut self, val: u32) {\n            let bytes = (val << 4).to_be_bytes();\n            *self.buf.write(LABEL_OFFSET) = bytes[1];\n            *self.buf.write(LABEL_OFFSET + 1) = bytes[2];\n            *self.buf.write(LABEL_OFFSET + 2) =\n                (self.buf.read(LABEL_OFFSET + 2) & 0x0f) | (bytes[3] & 0xf0);\n        }\n\n        pub fn set_exp(&mut self, exp: u8) {\n            *self.buf.write(EXP_OFFSET) = (self.buf.read(EXP_OFFSET) & 0xf1) | ((exp << 1) & 0x0e);\n        }\n\n        pub fn set_bos(&mut self, bos: u8) {\n            *self.buf.write(BOS_OFFSET) = (self.buf.read(BOS_OFFSET) & 0xfe) | (bos & 0x01);\n        }\n\n        pub fn set_ttl(&mut self, ttl: u8) {\n            *self.buf.write(TTL_OFFSET) = ttl;\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n    }\n\n    impl Debug for MplsLabelStackMemberPacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"MplsLabelStackMember\")\n                .field(\"label\", &self.get_label())\n                .field(\"exp\", &self.get_exp())\n                .field(\"bos\", &self.get_bos())\n                .field(\"ttl\", &self.get_ttl())\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_label() {\n            let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()];\n            let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap();\n            mpls_extension.set_label(0);\n            assert_eq!(0, mpls_extension.get_label());\n            assert_eq!([0x00, 0x00, 0x00], mpls_extension.packet()[0..3]);\n            mpls_extension.set_label(19380);\n            assert_eq!(19380, mpls_extension.get_label());\n            assert_eq!([0x04, 0xbb, 0x40], mpls_extension.packet()[0..3]);\n            mpls_extension.set_label(1_048_575);\n            assert_eq!(1_048_575, mpls_extension.get_label());\n            assert_eq!([0xff, 0xff, 0xf0], mpls_extension.packet()[0..3]);\n        }\n\n        #[test]\n        fn test_exp() {\n            let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()];\n            let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap();\n            mpls_extension.set_exp(0);\n            assert_eq!(0, mpls_extension.get_exp());\n            assert_eq!([0x00], mpls_extension.packet()[2..3]);\n            mpls_extension.set_exp(7);\n            assert_eq!(7, mpls_extension.get_exp());\n            assert_eq!([0x0e], mpls_extension.packet()[2..3]);\n        }\n\n        #[test]\n        fn test_bos() {\n            let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()];\n            let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap();\n            mpls_extension.set_bos(0);\n            assert_eq!(0, mpls_extension.get_bos());\n            assert_eq!([0x00], mpls_extension.packet()[2..3]);\n            mpls_extension.set_bos(1);\n            assert_eq!(1, mpls_extension.get_bos());\n            assert_eq!([0x01], mpls_extension.packet()[2..3]);\n        }\n\n        #[test]\n        fn test_ttl() {\n            let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()];\n            let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap();\n            mpls_extension.set_ttl(0);\n            assert_eq!(0, mpls_extension.get_ttl());\n            assert_eq!([0x00], mpls_extension.packet()[3..4]);\n            mpls_extension.set_ttl(1);\n            assert_eq!(1, mpls_extension.get_ttl());\n            assert_eq!([0x01], mpls_extension.packet()[3..4]);\n            mpls_extension.set_ttl(255);\n            assert_eq!(255, mpls_extension.get_ttl());\n            assert_eq!([0xff], mpls_extension.packet()[3..4]);\n        }\n\n        #[test]\n        fn test_combined() {\n            let mut buf = [0_u8; MplsLabelStackMemberPacket::minimum_packet_size()];\n            let mut mpls_extension = MplsLabelStackMemberPacket::new(&mut buf).unwrap();\n            mpls_extension.set_label(19380);\n            mpls_extension.set_exp(0);\n            mpls_extension.set_bos(1);\n            mpls_extension.set_ttl(1);\n            assert_eq!(19380, mpls_extension.get_label());\n            assert_eq!(0, mpls_extension.get_exp());\n            assert_eq!(1, mpls_extension.get_bos());\n            assert_eq!(1, mpls_extension.get_ttl());\n            assert_eq!([0x04, 0xbb, 0x41, 0x01], mpls_extension.packet()[0..4]);\n            mpls_extension.set_label(1_048_575);\n            mpls_extension.set_exp(7);\n            mpls_extension.set_bos(1);\n            mpls_extension.set_ttl(255);\n            assert_eq!(1_048_575, mpls_extension.get_label());\n            assert_eq!(7, mpls_extension.get_exp());\n            assert_eq!(1, mpls_extension.get_bos());\n            assert_eq!(255, mpls_extension.get_ttl());\n            assert_eq!([0xff, 0xff, 0xff, 0xff], mpls_extension.packet()[0..4]);\n        }\n\n        #[test]\n        fn test_view() {\n            let buf = [0x04, 0xbb, 0x41, 0x01];\n            let object = MplsLabelStackMemberPacket::new_view(&buf).unwrap();\n            assert_eq!(19380, object.get_label());\n            assert_eq!(0, object.get_exp());\n            assert_eq!(1, object.get_bos());\n            assert_eq!(1, object.get_ttl());\n        }\n    }\n}\n\npub mod extension_splitter {\n    use crate::icmp_extension::extension_header::ExtensionHeaderPacket;\n    const MIN_HEADER: usize = ExtensionHeaderPacket::minimum_packet_size();\n\n    /// From rfc4884 (section 3) entitled \"Summary of Changes to ICMP\":\n    ///\n    /// \"When the ICMP Extension Structure is appended to an ICMP message\n    /// and that ICMP message contains an \"original datagram\" field, the\n    /// \"original datagram\" field MUST contain at least 128 octets.\"\n    const ICMP_ORIG_DATAGRAM_MIN_LENGTH: usize = 128;\n\n    /// Separate an ICMP payload from ICMP extensions as defined in rfc4884.\n    ///\n    /// Applies to `TimeExceeded` and `DestinationUnreachable` ICMP messages only.\n    #[must_use]\n    pub fn split(length: usize, icmp_payload: &[u8]) -> (&[u8], Option<&[u8]>) {\n        // If the rfc4884 length field provided is larger than the payload length then\n        // the full payload is returned without any extension.\n        if length > icmp_payload.len() {\n            return (icmp_payload, None);\n        }\n        if icmp_payload.len() > ICMP_ORIG_DATAGRAM_MIN_LENGTH {\n            if length > ICMP_ORIG_DATAGRAM_MIN_LENGTH {\n                // a 'compliant' ICMP extension longer than 128 octets.\n                match icmp_payload.split_at(length) {\n                    (payload, extension) if extension.len() >= MIN_HEADER => {\n                        (payload, Some(extension))\n                    }\n                    _ => (icmp_payload, None),\n                }\n            } else if length > 0 {\n                // a 'compliant' ICMP extension padded to at least 128 octets,\n                // so we trim the original datagram to rfc4884 length.\n                match icmp_payload.split_at(ICMP_ORIG_DATAGRAM_MIN_LENGTH) {\n                    (payload, extension) if extension.len() >= MIN_HEADER => {\n                        (&payload[..length], Some(extension))\n                    }\n                    _ => (icmp_payload, None),\n                }\n            } else {\n                // a 'non-compliant' ICMP extension padded to 128 octets.\n                match icmp_payload.split_at(ICMP_ORIG_DATAGRAM_MIN_LENGTH) {\n                    (payload, extension) if extension.len() >= MIN_HEADER => {\n                        (payload, Some(extension))\n                    }\n                    _ => (icmp_payload, None),\n                }\n            }\n        } else {\n            // no extension present\n            (icmp_payload, None)\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n        use crate::icmp_extension::extension_header::ExtensionHeaderPacket;\n        use crate::icmp_extension::extension_object::{\n            ClassNum, ClassSubType, ExtensionObjectPacket,\n        };\n        use crate::icmp_extension::extension_structure::ExtensionsPacket;\n        use crate::icmp_extension::mpls_label_stack::MplsLabelStackPacket;\n        use crate::icmp_extension::mpls_label_stack_member::MplsLabelStackMemberPacket;\n\n        #[test]\n        fn test_split_empty_payload() {\n            let icmp_payload: [u8; 0] = [];\n            let (payload, extension) = split(0, &icmp_payload);\n            assert!(payload.is_empty() && extension.is_none());\n        }\n\n        // Test ICMP payload which is 12 bytes and has rfc4884 length of 3 (12\n        // bytes) so payload is 12 bytes and there is no extension.\n        #[test]\n        fn test_split_payload_with_compliant_empty_extension() {\n            let icmp_payload: [u8; 12] = [0; 12];\n            let (payload, extension) = split(3 * 4, &icmp_payload);\n            assert_eq!(payload, &[0; 12]);\n            assert_eq!(extension, None);\n        }\n\n        // Test ICMP payload with a minimal compliant extension.\n        #[test]\n        fn test_split_payload_with_compliant_minimal_extension() {\n            let icmp_payload: [u8; 132] = [0; 132];\n            let (payload, extension) = split(32 * 4, &icmp_payload);\n            assert_eq!(payload, &[0; 128]);\n            assert_eq!(extension, Some([0; 4].as_slice()));\n        }\n\n        // Test handling of an ICMP payload which has a rfc4884 length that\n        // is longer than the original datagram.\n        //\n        // For such invalid packets we assume there is no extension.\n        #[test]\n        fn test_split_payload_with_invalid_rfc4884_length() {\n            let icmp_payload: [u8; 128] = [0; 128];\n            let (payload, extension) = split(33 * 4, &icmp_payload);\n            assert_eq!(payload, &[0; 128]);\n            assert!(extension.is_none());\n        }\n\n        // Test handling of an ICMP payload which has a compliant extension\n        // which is not as long as the minimum size for an ICMP extension\n        // header (4 bytes).\n        //\n        // For such invalid packets we assume there is no extension.\n        #[test]\n        fn test_split_payload_with_compliant_invalid_extension() {\n            let icmp_payload: [u8; 129] = [0; 129];\n            let (payload, extension) = split(32 * 4, &icmp_payload);\n            assert_eq!(payload, &[0; 129]);\n            assert!(extension.is_none());\n        }\n\n        mod ipv4 {\n            use super::*;\n            use crate::icmpv4::echo_request::EchoRequestPacket;\n            use crate::icmpv4::time_exceeded::TimeExceededPacket;\n            use crate::icmpv4::{IcmpCode, IcmpType};\n            use crate::ipv4::Ipv4Packet;\n            use std::net::Ipv4Addr;\n\n            // This ICMP `TimeExceeded` packet which contains single `MPLS` extension\n            // object with a single member.  The packet does not have a `length`\n            // field and is therefore rfc4884 non-complaint.\n            #[test]\n            fn test_split_extension_ipv4_time_exceeded_non_compliant_mpls() {\n                let buf = hex_literal::hex!(\n                    \"\n                   0b 00 f4 ff 00 00 00 00 45 00 00 54 cc 1c 40 00\n                   01 01 b5 f4 c0 a8 01 15 5d b8 d8 22 08 00 0f e3\n                   65 da 82 42 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 20 00 99 3a 00 08 01 01\n                   04 bb 41 01\n                   \"\n                );\n                let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap();\n                assert_eq!(IcmpType::TimeExceeded, time_exceeded_packet.get_icmp_type());\n                assert_eq!(IcmpCode(0), time_exceeded_packet.get_icmp_code());\n                assert_eq!(62719, time_exceeded_packet.get_checksum());\n                assert_eq!(0, time_exceeded_packet.get_length());\n                assert_eq!(&buf[8..136], time_exceeded_packet.payload());\n                assert_eq!(Some(&buf[136..]), time_exceeded_packet.extension());\n\n                let nested_ipv4 = Ipv4Packet::new_view(time_exceeded_packet.payload()).unwrap();\n                assert_eq!(Ipv4Addr::from([192, 168, 1, 21]), nested_ipv4.get_source());\n                assert_eq!(\n                    Ipv4Addr::from([93, 184, 216, 34]),\n                    nested_ipv4.get_destination()\n                );\n                assert_eq!(&buf[28..136], nested_ipv4.payload());\n\n                let nested_echo = EchoRequestPacket::new_view(nested_ipv4.payload()).unwrap();\n                assert_eq!(IcmpCode(0), nested_echo.get_icmp_code());\n                assert_eq!(IcmpType::EchoRequest, nested_echo.get_icmp_type());\n                assert_eq!(0x0FE3, nested_echo.get_checksum());\n                assert_eq!(26074, nested_echo.get_identifier());\n                assert_eq!(33346, nested_echo.get_sequence());\n                assert_eq!(&buf[36..136], nested_echo.payload());\n\n                let extensions =\n                    ExtensionsPacket::new_view(time_exceeded_packet.extension().unwrap()).unwrap();\n\n                let extension_header =\n                    ExtensionHeaderPacket::new_view(extensions.header()).unwrap();\n                assert_eq!(2, extension_header.get_version());\n                assert_eq!(0x993A, extension_header.get_checksum());\n\n                let object_bytes = extensions.objects().next().unwrap();\n                let extension_object = ExtensionObjectPacket::new_view(object_bytes).unwrap();\n\n                assert_eq!(8, extension_object.get_length());\n                assert_eq!(\n                    ClassNum::MultiProtocolLabelSwitchingLabelStack,\n                    extension_object.get_class_num()\n                );\n                assert_eq!(ClassSubType(1), extension_object.get_class_subtype());\n                assert_eq!([0x04, 0xbb, 0x41, 0x01], extension_object.payload());\n\n                let mpls_stack =\n                    MplsLabelStackPacket::new_view(extension_object.payload()).unwrap();\n                let mpls_stack_member_bytes = mpls_stack.members().next().unwrap();\n                let mpls_stack_member =\n                    MplsLabelStackMemberPacket::new_view(mpls_stack_member_bytes).unwrap();\n                assert_eq!(19380, mpls_stack_member.get_label());\n                assert_eq!(0, mpls_stack_member.get_exp());\n                assert_eq!(1, mpls_stack_member.get_bos());\n                assert_eq!(1, mpls_stack_member.get_ttl());\n            }\n\n            // This ICMP `TimeExceeded` packet does not have any ICMP extensions.\n            // It has a rfc4884 complaint `length` field.\n            #[test]\n            fn test_split_extension_ipv4_time_exceeded_compliant_no_extension() {\n                let buf = hex_literal::hex!(\n                    \"\n                   0b 00 f4 ee 00 11 00 00 45 00 00 54 a2 ee 40 00\n                   01 01 df 22 c0 a8 01 15 5d b8 d8 22 08 00 0f e1\n                   65 da 82 44 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00\n                   \"\n                );\n                let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap();\n                assert_eq!(IcmpType::TimeExceeded, time_exceeded_packet.get_icmp_type());\n                assert_eq!(IcmpCode(0), time_exceeded_packet.get_icmp_code());\n                assert_eq!(62702, time_exceeded_packet.get_checksum());\n                assert_eq!(17, time_exceeded_packet.get_length());\n                assert_eq!(&buf[8..76], time_exceeded_packet.payload());\n                assert_eq!(None, time_exceeded_packet.extension());\n\n                let nested_ipv4 = Ipv4Packet::new_view(&buf[8..76]).unwrap();\n                assert_eq!(Ipv4Addr::from([192, 168, 1, 21]), nested_ipv4.get_source());\n                assert_eq!(\n                    Ipv4Addr::from([93, 184, 216, 34]),\n                    nested_ipv4.get_destination()\n                );\n                assert_eq!(&buf[28..76], nested_ipv4.payload());\n\n                let nested_echo = EchoRequestPacket::new_view(nested_ipv4.payload()).unwrap();\n                assert_eq!(IcmpCode(0), nested_echo.get_icmp_code());\n                assert_eq!(IcmpType::EchoRequest, nested_echo.get_icmp_type());\n                assert_eq!(0x0FE1, nested_echo.get_checksum());\n                assert_eq!(26074, nested_echo.get_identifier());\n                assert_eq!(33348, nested_echo.get_sequence());\n                assert_eq!(&buf[36..76], nested_echo.payload());\n            }\n\n            // This is a real example that was observed in the wild whilst testing.\n            //\n            // It has a rfc4884 complaint `length` field set to be 17 and so has\n            // an original datagram if length 68 octet (17 * 4 = 68) but is padded\n            // to be 128 octets.\n            //\n            // See `https://github.com/fujiapple852/trippy/issues/804` for further\n            // discussion and analysis of this case.\n            #[test]\n            fn test_split_extension_ipv4_time_exceeded_compliant_extension() {\n                let buf = hex_literal::hex!(\n                    \"\n                   0b 00 f4 ee 00 11 00 00 45 00 00 54 20 c3 40 00\n                   02 01 b5 7e 64 63 08 2a 5d b8 d8 22 08 00 11 8d\n                   65 83 80 ef 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                   00 00 00 00 00 00 00 00 20 00 78 56 00 08 01 01\n                   65 9f 01 01\n                   \"\n                );\n                let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap();\n                assert_eq!(68, time_exceeded_packet.payload().len());\n                assert_eq!(12, time_exceeded_packet.extension().unwrap().len());\n                let extensions =\n                    ExtensionsPacket::new_view(time_exceeded_packet.extension().unwrap()).unwrap();\n\n                let extension_header =\n                    ExtensionHeaderPacket::new_view(extensions.header()).unwrap();\n                assert_eq!(2, extension_header.get_version());\n                assert_eq!(0x7856, extension_header.get_checksum());\n\n                let object_bytes = extensions.objects().next().unwrap();\n                let extension_object = ExtensionObjectPacket::new_view(object_bytes).unwrap();\n\n                assert_eq!(8, extension_object.get_length());\n                assert_eq!(\n                    ClassNum::MultiProtocolLabelSwitchingLabelStack,\n                    extension_object.get_class_num()\n                );\n                assert_eq!(ClassSubType(1), extension_object.get_class_subtype());\n                assert_eq!([0x65, 0x9f, 0x01, 0x01], extension_object.payload());\n\n                let mpls_stack =\n                    MplsLabelStackPacket::new_view(extension_object.payload()).unwrap();\n                let mpls_stack_member_bytes = mpls_stack.members().next().unwrap();\n                let mpls_stack_member =\n                    MplsLabelStackMemberPacket::new_view(mpls_stack_member_bytes).unwrap();\n                assert_eq!(416_240, mpls_stack_member.get_label());\n                assert_eq!(0, mpls_stack_member.get_exp());\n                assert_eq!(1, mpls_stack_member.get_bos());\n                assert_eq!(1, mpls_stack_member.get_ttl());\n            }\n        }\n\n        mod ipv6 {\n            use crate::icmp_extension::extension_header::ExtensionHeaderPacket;\n            use crate::icmp_extension::extension_object::{\n                ClassNum, ClassSubType, ExtensionObjectPacket,\n            };\n            use crate::icmp_extension::extension_structure::ExtensionsPacket;\n            use crate::icmp_extension::mpls_label_stack::MplsLabelStackPacket;\n            use crate::icmp_extension::mpls_label_stack_member::MplsLabelStackMemberPacket;\n            use crate::icmpv6::echo_request::EchoRequestPacket;\n            use crate::icmpv6::time_exceeded::TimeExceededPacket;\n            use crate::icmpv6::{IcmpCode, IcmpType};\n            use crate::ipv6::Ipv6Packet;\n\n            // Real IPv6 example with a rfc4884 length of 10 (10 * 8 = 80\n            // octets).\n            //\n            // This example contain an MPLS extension stack which contains\n            // two member (i.e. labels)\n            #[test]\n            fn test_ipv6() {\n                let buf = hex_literal::hex!(\n                    \"\n                    03 00 be a8 0a 00 00 00 68 04 83 fe 00 2c 3a 01\n                    24 00 61 80 00 00 00 d0 00 00 00 00 12 65 b0 01\n                    24 04 68 00 40 03 0c 1c 00 00 00 00 00 00 00 8a\n                    80 00 b2 e1 2a 60 80 f2 00 00 00 00 00 00 00 00\n                    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                    00 00 00 00 00 00 00 00 20 00 96 53 00 0c 01 01\n                    06 9f 18 01 00 00 29 ff\n                    \"\n                );\n                let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap();\n                assert_eq!(IcmpType::TimeExceeded, time_exceeded_packet.get_icmp_type());\n                assert_eq!(IcmpCode(0), time_exceeded_packet.get_icmp_code());\n                assert_eq!(48808, time_exceeded_packet.get_checksum());\n                assert_eq!(10, time_exceeded_packet.get_length());\n                assert_eq!(&buf[8..88], time_exceeded_packet.payload());\n                assert_eq!(Some(&buf[136..]), time_exceeded_packet.extension());\n                assert_eq!(80, time_exceeded_packet.payload().len());\n                assert_eq!(16, time_exceeded_packet.extension().unwrap().len());\n\n                let nested_ipv6 = Ipv6Packet::new_view(time_exceeded_packet.payload()).unwrap();\n                let nested_echo = EchoRequestPacket::new_view(nested_ipv6.payload()).unwrap();\n                assert_eq!(IcmpCode(0), nested_echo.get_icmp_code());\n                assert_eq!(IcmpType::EchoRequest, nested_echo.get_icmp_type());\n                assert_eq!(0xB2E1, nested_echo.get_checksum());\n                assert_eq!(10848, nested_echo.get_identifier());\n                assert_eq!(33010, nested_echo.get_sequence());\n\n                let extensions =\n                    ExtensionsPacket::new_view(time_exceeded_packet.extension().unwrap()).unwrap();\n\n                let extension_header =\n                    ExtensionHeaderPacket::new_view(extensions.header()).unwrap();\n                assert_eq!(2, extension_header.get_version());\n                assert_eq!(0x9653, extension_header.get_checksum());\n\n                let object_bytes = extensions.objects().next().unwrap();\n                let extension_object = ExtensionObjectPacket::new_view(object_bytes).unwrap();\n                assert_eq!(12, extension_object.get_length());\n                assert_eq!(\n                    ClassNum::MultiProtocolLabelSwitchingLabelStack,\n                    extension_object.get_class_num()\n                );\n                assert_eq!(ClassSubType(1), extension_object.get_class_subtype());\n                assert_eq!(\n                    [0x06, 0x9f, 0x18, 0x01, 0x00, 0x00, 0x29, 0xff],\n                    extension_object.payload()\n                );\n\n                let mpls_stack =\n                    MplsLabelStackPacket::new_view(extension_object.payload()).unwrap();\n                let mut mpls_stack_member_iter = mpls_stack.members();\n\n                // 1st stack member\n                let mpls_stack_member_bytes = mpls_stack_member_iter.next().unwrap();\n                let mpls_stack_member =\n                    MplsLabelStackMemberPacket::new_view(mpls_stack_member_bytes).unwrap();\n                assert_eq!(27121, mpls_stack_member.get_label());\n                assert_eq!(4, mpls_stack_member.get_exp());\n                assert_eq!(0, mpls_stack_member.get_bos());\n                assert_eq!(1, mpls_stack_member.get_ttl());\n\n                // 2nd stack member\n                let mpls_stack_member_bytes = mpls_stack_member_iter.next().unwrap();\n                let mpls_stack_member =\n                    MplsLabelStackMemberPacket::new_view(mpls_stack_member_bytes).unwrap();\n                assert_eq!(2, mpls_stack_member.get_label());\n                assert_eq!(4, mpls_stack_member.get_exp());\n                assert_eq!(1, mpls_stack_member.get_bos());\n                assert_eq!(255, mpls_stack_member.get_ttl());\n                assert!(mpls_stack_member_iter.next().is_none());\n            }\n\n            // Real IPv6 example with a rfc4884 length of 16 (16 * 8 = 128\n            // octets for) but the total payload is only 84 octets and\n            // therefore this is a malformed packet.\n            //\n            // For such packets Trippy assumes there are no extensions.\n            #[test]\n            fn test_ipv6_2() {\n                let buf = hex_literal::hex!(\n                    \"\n                    03 00 5a b4 10 00 00 00 68 0e 0d 91 00 2c 3a 01\n                    24 00 61 80 00 00 00 d0 00 00 00 00 12 65 b0 01\n                    24 04 68 00 40 03 0c 05 00 00 00 00 00 00 00 71\n                    80 00 a8 e7 34 88 80 f4 00 00 00 00 00 00 00 00\n                    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n                    00 00 00 00 00 00 00 00 00 00 00 00\n                    \"\n                );\n                let time_exceeded_packet = TimeExceededPacket::new_view(&buf).unwrap();\n                assert_eq!(84, time_exceeded_packet.payload().len());\n                assert_eq!(None, time_exceeded_packet.extension());\n\n                let nested_ipv6 = Ipv6Packet::new_view(time_exceeded_packet.payload()).unwrap();\n                let nested_echo = EchoRequestPacket::new_view(nested_ipv6.payload()).unwrap();\n                assert_eq!(IcmpCode(0), nested_echo.get_icmp_code());\n                assert_eq!(IcmpType::EchoRequest, nested_echo.get_icmp_type());\n                assert_eq!(0xA8E7, nested_echo.get_checksum());\n                assert_eq!(13448, nested_echo.get_identifier());\n                assert_eq!(33012, nested_echo.get_sequence());\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/icmpv4.rs",
    "content": "use crate::buffer::Buffer;\nuse crate::error::{Error, Result};\nuse std::fmt::{Debug, Formatter};\n\n/// The type of ICMP packet.\n#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]\npub enum IcmpType {\n    EchoRequest,\n    EchoReply,\n    DestinationUnreachable,\n    TimeExceeded,\n    Other(u8),\n}\n\nimpl IcmpType {\n    #[must_use]\n    pub const fn id(&self) -> u8 {\n        match self {\n            Self::EchoRequest => 8,\n            Self::EchoReply => 0,\n            Self::DestinationUnreachable => 3,\n            Self::TimeExceeded => 11,\n            Self::Other(id) => *id,\n        }\n    }\n}\n\nimpl From<u8> for IcmpType {\n    fn from(val: u8) -> Self {\n        match val {\n            8 => Self::EchoRequest,\n            0 => Self::EchoReply,\n            3 => Self::DestinationUnreachable,\n            11 => Self::TimeExceeded,\n            id => Self::Other(id),\n        }\n    }\n}\n\n/// The ICMP code.\n#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]\npub struct IcmpCode(pub u8);\n\nimpl From<u8> for IcmpCode {\n    fn from(val: u8) -> Self {\n        Self(val)\n    }\n}\n\n/// The code for `TimeExceeded` ICMP packet type.\n#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]\npub enum IcmpTimeExceededCode {\n    /// TTL expired in transit.\n    TtlExpired,\n    /// Fragment reassembly time exceeded.\n    FragmentReassembly,\n    /// An unknown code.\n    Unknown(u8),\n}\n\nimpl From<IcmpCode> for IcmpTimeExceededCode {\n    fn from(val: IcmpCode) -> Self {\n        match val {\n            IcmpCode(0) => Self::TtlExpired,\n            IcmpCode(1) => Self::FragmentReassembly,\n            IcmpCode(id) => Self::Unknown(id),\n        }\n    }\n}\n\nconst TYPE_OFFSET: usize = 0;\nconst CODE_OFFSET: usize = 1;\nconst CHECKSUM_OFFSET: usize = 2;\n\n/// Represents an ICMP packet.\n///\n/// The internal representation is held in network byte order (big-endian) and all accessor methods\n/// take and return data in host byte order, converting as necessary for the given architecture.\npub struct IcmpPacket<'a> {\n    buf: Buffer<'a>,\n}\n\nimpl<'a> IcmpPacket<'a> {\n    pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Mutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"IcmpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Immutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"IcmpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    #[must_use]\n    pub const fn minimum_packet_size() -> usize {\n        8\n    }\n\n    #[must_use]\n    pub fn get_icmp_type(&self) -> IcmpType {\n        IcmpType::from(self.buf.read(TYPE_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_icmp_code(&self) -> IcmpCode {\n        IcmpCode::from(self.buf.read(CODE_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_checksum(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n    }\n\n    pub fn set_icmp_type(&mut self, val: IcmpType) {\n        *self.buf.write(TYPE_OFFSET) = val.id();\n    }\n\n    pub fn set_icmp_code(&mut self, val: IcmpCode) {\n        *self.buf.write(CODE_OFFSET) = val.0;\n    }\n\n    pub fn set_checksum(&mut self, val: u16) {\n        self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n    }\n\n    #[must_use]\n    pub fn packet(&self) -> &[u8] {\n        self.buf.as_slice()\n    }\n}\n\nimpl Debug for IcmpPacket<'_> {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"IcmpPacket\")\n            .field(\"icmp_type\", &self.get_icmp_type())\n            .field(\"icmp_code\", &self.get_icmp_code())\n            .field(\"checksum\", &self.get_checksum())\n            .finish()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_icmp_type() {\n        let mut buf = [0_u8; IcmpPacket::minimum_packet_size()];\n        let mut packet = IcmpPacket::new(&mut buf).unwrap();\n        packet.set_icmp_type(IcmpType::EchoRequest);\n        assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n        assert_eq!([0x08], packet.packet()[0..1]);\n        packet.set_icmp_type(IcmpType::EchoReply);\n        assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n        assert_eq!([0x00], packet.packet()[0..1]);\n        packet.set_icmp_type(IcmpType::DestinationUnreachable);\n        assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n        assert_eq!([0x03], packet.packet()[0..1]);\n        packet.set_icmp_type(IcmpType::TimeExceeded);\n        assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n        assert_eq!([0x0B], packet.packet()[0..1]);\n        packet.set_icmp_type(IcmpType::Other(255));\n        assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n        assert_eq!([0xFF], packet.packet()[0..1]);\n    }\n\n    #[test]\n    fn test_icmp_code() {\n        let mut buf = [0_u8; IcmpPacket::minimum_packet_size()];\n        let mut packet = IcmpPacket::new(&mut buf).unwrap();\n        packet.set_icmp_code(IcmpCode(0));\n        assert_eq!(IcmpCode(0), packet.get_icmp_code());\n        assert_eq!([0x00], packet.packet()[1..2]);\n        packet.set_icmp_code(IcmpCode(5));\n        assert_eq!(IcmpCode(5), packet.get_icmp_code());\n        assert_eq!([0x05], packet.packet()[1..2]);\n        packet.set_icmp_code(IcmpCode(255));\n        assert_eq!(IcmpCode(255), packet.get_icmp_code());\n        assert_eq!([0xFF], packet.packet()[1..2]);\n    }\n\n    #[test]\n    fn test_checksum() {\n        let mut buf = [0_u8; IcmpPacket::minimum_packet_size()];\n        let mut packet = IcmpPacket::new(&mut buf).unwrap();\n        packet.set_checksum(0);\n        assert_eq!(0, packet.get_checksum());\n        assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n        packet.set_checksum(1999);\n        assert_eq!(1999, packet.get_checksum());\n        assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n        packet.set_checksum(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_checksum());\n        assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n    }\n\n    #[test]\n    fn test_new_insufficient_buffer() {\n        const SIZE: usize = IcmpPacket::minimum_packet_size();\n        let mut buf = [0_u8; SIZE - 1];\n        let err = IcmpPacket::new(&mut buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"IcmpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n\n    #[test]\n    fn test_new_view_insufficient_buffer() {\n        const SIZE: usize = IcmpPacket::minimum_packet_size();\n        let buf = [0_u8; SIZE - 1];\n        let err = IcmpPacket::new_view(&buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"IcmpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n}\n\npub mod echo_request {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::fmt_payload;\n    use crate::icmpv4::{IcmpCode, IcmpType};\n    use std::fmt::{Debug, Formatter};\n\n    const TYPE_OFFSET: usize = 0;\n    const CODE_OFFSET: usize = 1;\n    const CHECKSUM_OFFSET: usize = 2;\n    const IDENTIFIER_OFFSET: usize = 4;\n    const SEQUENCE_OFFSET: usize = 6;\n\n    /// Represents an ICMP `EchoRequest` packet.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct EchoRequestPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> EchoRequestPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"EchoRequestPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"EchoRequestPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            8\n        }\n\n        #[must_use]\n        pub fn get_icmp_type(&self) -> IcmpType {\n            IcmpType::from(self.buf.read(TYPE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_icmp_code(&self) -> IcmpCode {\n            IcmpCode::from(self.buf.read(CODE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_checksum(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_identifier(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(IDENTIFIER_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_sequence(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET))\n        }\n\n        pub fn set_icmp_type(&mut self, val: IcmpType) {\n            *self.buf.write(TYPE_OFFSET) = val.id();\n        }\n\n        pub fn set_icmp_code(&mut self, val: IcmpCode) {\n            *self.buf.write(CODE_OFFSET) = val.0;\n        }\n\n        pub fn set_checksum(&mut self, val: u16) {\n            self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_identifier(&mut self, val: u16) {\n            self.buf.set_bytes(IDENTIFIER_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_sequence(&mut self, val: u16) {\n            self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_payload(&mut self, vals: &[u8]) {\n            let current_offset = Self::minimum_packet_size();\n            self.buf.as_slice_mut()[current_offset..current_offset + vals.len()]\n                .copy_from_slice(vals);\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn payload(&self) -> &[u8] {\n            &self.buf.as_slice()[Self::minimum_packet_size()..]\n        }\n    }\n\n    impl Debug for EchoRequestPacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"EchoRequestPacket\")\n                .field(\"icmp_type\", &self.get_icmp_type())\n                .field(\"icmp_code\", &self.get_icmp_code())\n                .field(\"checksum\", &self.get_checksum())\n                .field(\"identifier\", &self.get_identifier())\n                .field(\"sequence\", &self.get_sequence())\n                .field(\"payload\", &fmt_payload(self.payload()))\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_icmp_type() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_icmp_type(IcmpType::EchoRequest);\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!([0x08], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::EchoReply);\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!([0x00], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::DestinationUnreachable);\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!([0x03], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::TimeExceeded);\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!([0x0B], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::Other(255));\n            assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n            assert_eq!([0xFF], packet.packet()[0..1]);\n        }\n\n        #[test]\n        fn test_icmp_code() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_icmp_code(IcmpCode(0));\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!([0x00], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(5));\n            assert_eq!(IcmpCode(5), packet.get_icmp_code());\n            assert_eq!([0x05], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(255));\n            assert_eq!(IcmpCode(255), packet.get_icmp_code());\n            assert_eq!([0xFF], packet.packet()[1..2]);\n        }\n\n        #[test]\n        fn test_checksum() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_checksum(0);\n            assert_eq!(0, packet.get_checksum());\n            assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n            packet.set_checksum(1999);\n            assert_eq!(1999, packet.get_checksum());\n            assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n            packet.set_checksum(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_checksum());\n            assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n        }\n\n        #[test]\n        fn test_identifier() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_identifier(0);\n            assert_eq!(0, packet.get_identifier());\n            assert_eq!([0x00, 0x00], packet.packet()[4..=5]);\n            packet.set_identifier(1999);\n            assert_eq!(1999, packet.get_identifier());\n            assert_eq!([0x07, 0xCF], packet.packet()[4..=5]);\n            packet.set_identifier(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_identifier());\n            assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]);\n        }\n\n        #[test]\n        fn test_sequence() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_sequence(0);\n            assert_eq!(0, packet.get_sequence());\n            assert_eq!([0x00, 0x00], packet.packet()[6..=7]);\n            packet.set_sequence(1999);\n            assert_eq!(1999, packet.get_sequence());\n            assert_eq!([0x07, 0xCF], packet.packet()[6..=7]);\n            packet.set_sequence(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_sequence());\n            assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]);\n        }\n\n        #[test]\n        fn test_view() {\n            let buf = [0x08, 0x00, 0x16, 0x7c, 0x60, 0x9b, 0x82, 0x9a];\n            let packet = EchoRequestPacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!(5756, packet.get_checksum());\n            assert_eq!(24731, packet.get_identifier());\n            assert_eq!(33434, packet.get_sequence());\n            assert!(packet.payload().is_empty());\n        }\n\n        #[test]\n        fn test_new_insufficient_buffer() {\n            const SIZE: usize = EchoRequestPacket::minimum_packet_size();\n            let mut buf = [0_u8; SIZE - 1];\n            let err = EchoRequestPacket::new(&mut buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"EchoRequestPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n\n        #[test]\n        fn test_new_view_insufficient_buffer() {\n            const SIZE: usize = EchoRequestPacket::minimum_packet_size();\n            let buf = [0_u8; SIZE - 1];\n            let err = EchoRequestPacket::new_view(&buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"EchoRequestPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n    }\n}\n\npub mod echo_reply {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::fmt_payload;\n    use crate::icmpv4::{IcmpCode, IcmpType};\n    use std::fmt::{Debug, Formatter};\n\n    const TYPE_OFFSET: usize = 0;\n    const CODE_OFFSET: usize = 1;\n    const CHECKSUM_OFFSET: usize = 2;\n    const IDENTIFIER_OFFSET: usize = 4;\n    const SEQUENCE_OFFSET: usize = 6;\n\n    /// Represents an ICMP `EchoReply` packet.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct EchoReplyPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> EchoReplyPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"EchoReplyPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"EchoReplyPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            8\n        }\n\n        #[must_use]\n        pub fn get_icmp_type(&self) -> IcmpType {\n            IcmpType::from(self.buf.read(TYPE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_icmp_code(&self) -> IcmpCode {\n            IcmpCode::from(self.buf.read(CODE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_checksum(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_identifier(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(IDENTIFIER_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_sequence(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET))\n        }\n\n        pub fn set_icmp_type(&mut self, val: IcmpType) {\n            *self.buf.write(TYPE_OFFSET) = val.id();\n        }\n\n        pub fn set_icmp_code(&mut self, val: IcmpCode) {\n            *self.buf.write(CODE_OFFSET) = val.0;\n        }\n\n        pub fn set_checksum(&mut self, val: u16) {\n            self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_identifier(&mut self, val: u16) {\n            self.buf.set_bytes(IDENTIFIER_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_sequence(&mut self, val: u16) {\n            self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_payload(&mut self, vals: &[u8]) {\n            let current_offset = Self::minimum_packet_size();\n            self.buf.as_slice_mut()[current_offset..current_offset + vals.len()]\n                .copy_from_slice(vals);\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn payload(&self) -> &[u8] {\n            &self.buf.as_slice()[Self::minimum_packet_size()..]\n        }\n    }\n\n    impl Debug for EchoReplyPacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"EchoReplyPacket\")\n                .field(\"icmp_type\", &self.get_icmp_type())\n                .field(\"icmp_code\", &self.get_icmp_code())\n                .field(\"checksum\", &self.get_checksum())\n                .field(\"identifier\", &self.get_identifier())\n                .field(\"sequence\", &self.get_sequence())\n                .field(\"payload\", &fmt_payload(self.payload()))\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_icmp_type() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_icmp_type(IcmpType::EchoRequest);\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!([0x08], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::EchoReply);\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!([0x00], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::DestinationUnreachable);\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!([0x03], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::TimeExceeded);\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!([0x0B], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::Other(255));\n            assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n            assert_eq!([0xFF], packet.packet()[0..1]);\n        }\n\n        #[test]\n        fn test_icmp_code() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_icmp_code(IcmpCode(0));\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!([0x00], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(5));\n            assert_eq!(IcmpCode(5), packet.get_icmp_code());\n            assert_eq!([0x05], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(255));\n            assert_eq!(IcmpCode(255), packet.get_icmp_code());\n            assert_eq!([0xFF], packet.packet()[1..2]);\n        }\n\n        #[test]\n        fn test_checksum() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_checksum(0);\n            assert_eq!(0, packet.get_checksum());\n            assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n            packet.set_checksum(1999);\n            assert_eq!(1999, packet.get_checksum());\n            assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n            packet.set_checksum(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_checksum());\n            assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n        }\n\n        #[test]\n        fn test_identifier() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_identifier(0);\n            assert_eq!(0, packet.get_identifier());\n            assert_eq!([0x00, 0x00], packet.packet()[4..=5]);\n            packet.set_identifier(1999);\n            assert_eq!(1999, packet.get_identifier());\n            assert_eq!([0x07, 0xCF], packet.packet()[4..=5]);\n            packet.set_identifier(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_identifier());\n            assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]);\n        }\n\n        #[test]\n        fn test_sequence() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_sequence(0);\n            assert_eq!(0, packet.get_sequence());\n            assert_eq!([0x00, 0x00], packet.packet()[6..=7]);\n            packet.set_sequence(1999);\n            assert_eq!(1999, packet.get_sequence());\n            assert_eq!([0x07, 0xCF], packet.packet()[6..=7]);\n            packet.set_sequence(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_sequence());\n            assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]);\n        }\n\n        #[test]\n        fn test_view() {\n            let buf = [0x00, 0x00, 0x1e, 0x70, 0x60, 0x9b, 0x80, 0xf4];\n            let packet = EchoReplyPacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!(7792, packet.get_checksum());\n            assert_eq!(24731, packet.get_identifier());\n            assert_eq!(33012, packet.get_sequence());\n            assert!(packet.payload().is_empty());\n        }\n\n        #[test]\n        fn test_new_insufficient_buffer() {\n            const SIZE: usize = EchoReplyPacket::minimum_packet_size();\n            let mut buf = [0_u8; SIZE - 1];\n            let err = EchoReplyPacket::new(&mut buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"EchoReplyPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n\n        #[test]\n        fn test_new_view_insufficient_buffer() {\n            const SIZE: usize = EchoReplyPacket::minimum_packet_size();\n            let buf = [0_u8; SIZE - 1];\n            let err = EchoReplyPacket::new_view(&buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"EchoReplyPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n    }\n}\n\npub mod time_exceeded {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::fmt_payload;\n    use crate::icmp_extension::extension_splitter::split;\n    use crate::icmpv4::{IcmpCode, IcmpType};\n    use std::fmt::{Debug, Formatter};\n\n    const TYPE_OFFSET: usize = 0;\n    const CODE_OFFSET: usize = 1;\n    const CHECKSUM_OFFSET: usize = 2;\n    const LENGTH_OFFSET: usize = 5;\n\n    /// Represents an ICMP `TimeExceeded` packet.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct TimeExceededPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> TimeExceededPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"TimeExceededPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"TimeExceededPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            8\n        }\n\n        #[must_use]\n        pub fn get_icmp_type(&self) -> IcmpType {\n            IcmpType::from(self.buf.read(TYPE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_icmp_code(&self) -> IcmpCode {\n            IcmpCode::from(self.buf.read(CODE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_checksum(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_length(&self) -> u8 {\n            self.buf.read(LENGTH_OFFSET)\n        }\n\n        pub fn set_icmp_type(&mut self, val: IcmpType) {\n            *self.buf.write(TYPE_OFFSET) = val.id();\n        }\n\n        pub fn set_icmp_code(&mut self, val: IcmpCode) {\n            *self.buf.write(CODE_OFFSET) = val.0;\n        }\n\n        pub fn set_checksum(&mut self, val: u16) {\n            self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_length(&mut self, val: u8) {\n            *self.buf.write(LENGTH_OFFSET) = val;\n        }\n\n        pub fn set_payload(&mut self, vals: &[u8]) {\n            let current_offset = Self::minimum_packet_size();\n            self.buf.as_slice_mut()[current_offset..current_offset + vals.len()]\n                .copy_from_slice(vals);\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn payload(&self) -> &[u8] {\n            let (payload, _) = self.split_payload_extension();\n            payload\n        }\n\n        #[must_use]\n        pub fn payload_raw(&self) -> &[u8] {\n            &self.buf.as_slice()[Self::minimum_packet_size()..]\n        }\n\n        #[must_use]\n        pub fn extension(&self) -> Option<&[u8]> {\n            let (_, extension) = self.split_payload_extension();\n            extension\n        }\n\n        fn split_payload_extension(&self) -> (&[u8], Option<&[u8]>) {\n            // From rfc4884:\n            //\n            // \"For ICMPv4 messages, the length attribute represents 32-bit words\n            let length = usize::from(self.get_length()) * 4;\n            let icmp_payload = &self.buf.as_slice()[Self::minimum_packet_size()..];\n            split(length, icmp_payload)\n        }\n    }\n\n    impl Debug for TimeExceededPacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"TimeExceededPacket\")\n                .field(\"icmp_type\", &self.get_icmp_type())\n                .field(\"icmp_code\", &self.get_icmp_code())\n                .field(\"checksum\", &self.get_checksum())\n                .field(\"length\", &self.get_length())\n                .field(\"payload\", &fmt_payload(self.payload()))\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_icmp_type() {\n            let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()];\n            let mut packet = TimeExceededPacket::new(&mut buf).unwrap();\n            packet.set_icmp_type(IcmpType::EchoRequest);\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!([0x08], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::EchoReply);\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!([0x00], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::DestinationUnreachable);\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!([0x03], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::TimeExceeded);\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!([0x0B], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::Other(255));\n            assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n            assert_eq!([0xFF], packet.packet()[0..1]);\n        }\n\n        #[test]\n        fn test_icmp_code() {\n            let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()];\n            let mut packet = TimeExceededPacket::new(&mut buf).unwrap();\n            packet.set_icmp_code(IcmpCode(0));\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!([0x00], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(5));\n            assert_eq!(IcmpCode(5), packet.get_icmp_code());\n            assert_eq!([0x05], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(255));\n            assert_eq!(IcmpCode(255), packet.get_icmp_code());\n            assert_eq!([0xFF], packet.packet()[1..2]);\n        }\n\n        #[test]\n        fn test_checksum() {\n            let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()];\n            let mut packet = TimeExceededPacket::new(&mut buf).unwrap();\n            packet.set_checksum(0);\n            assert_eq!(0, packet.get_checksum());\n            assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n            packet.set_checksum(1999);\n            assert_eq!(1999, packet.get_checksum());\n            assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n            packet.set_checksum(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_checksum());\n            assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n        }\n\n        #[test]\n        fn test_length() {\n            let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()];\n            let mut packet = TimeExceededPacket::new(&mut buf).unwrap();\n            packet.set_length(0);\n            assert_eq!(0, packet.get_length());\n            assert_eq!([0x00], packet.packet()[5..6]);\n            packet.set_length(8);\n            assert_eq!(8, packet.get_length());\n            assert_eq!([0x08], packet.packet()[5..6]);\n            packet.set_length(u8::MAX);\n            assert_eq!(u8::MAX, packet.get_length());\n            assert_eq!([0xFF], packet.packet()[5..6]);\n        }\n\n        #[test]\n        fn test_view() {\n            let buf = [0x0b, 0x00, 0xf4, 0xee, 0x00, 0x11, 0x00, 0x00];\n            let packet = TimeExceededPacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!(62702, packet.get_checksum());\n            assert_eq!(17, packet.get_length());\n            assert!(packet.payload().is_empty());\n        }\n\n        #[test]\n        fn test_view_large() {\n            let mut buf = [0x0_u8; 256];\n            buf[..8].copy_from_slice(&[0x0b, 0x00, 0xf4, 0xee, 0x00, 0x40, 0x00, 0x00]);\n            let packet = TimeExceededPacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!(62702, packet.get_checksum());\n            assert_eq!(64, packet.get_length());\n            assert_eq!(&[0x0_u8; 248], packet.payload());\n            assert_eq!(None, packet.extension());\n        }\n\n        #[test]\n        fn test_new_insufficient_buffer() {\n            const SIZE: usize = TimeExceededPacket::minimum_packet_size();\n            let mut buf = [0_u8; SIZE - 1];\n            let err = TimeExceededPacket::new(&mut buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"TimeExceededPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n\n        #[test]\n        fn test_new_view_insufficient_buffer() {\n            const SIZE: usize = TimeExceededPacket::minimum_packet_size();\n            let buf = [0_u8; SIZE - 1];\n            let err = TimeExceededPacket::new_view(&buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"TimeExceededPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n    }\n}\n\npub mod destination_unreachable {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::fmt_payload;\n    use crate::icmp_extension::extension_splitter::split;\n    use crate::icmpv4::{IcmpCode, IcmpType};\n    use std::fmt::{Debug, Formatter};\n\n    const TYPE_OFFSET: usize = 0;\n    const CODE_OFFSET: usize = 1;\n    const CHECKSUM_OFFSET: usize = 2;\n    const LENGTH_OFFSET: usize = 5;\n    const NEXT_HOP_MTU_OFFSET: usize = 6;\n\n    /// Represents an ICMP `DestinationUnreachable` packet.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct DestinationUnreachablePacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> DestinationUnreachablePacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"DestinationUnreachablePacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"DestinationUnreachablePacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            8\n        }\n\n        #[must_use]\n        pub fn get_icmp_type(&self) -> IcmpType {\n            IcmpType::from(self.buf.read(TYPE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_icmp_code(&self) -> IcmpCode {\n            IcmpCode::from(self.buf.read(CODE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_checksum(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_length(&self) -> u8 {\n            self.buf.read(LENGTH_OFFSET)\n        }\n\n        #[must_use]\n        pub fn get_next_hop_mtu(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(NEXT_HOP_MTU_OFFSET))\n        }\n\n        pub fn set_icmp_type(&mut self, val: IcmpType) {\n            *self.buf.write(TYPE_OFFSET) = val.id();\n        }\n\n        pub fn set_icmp_code(&mut self, val: IcmpCode) {\n            *self.buf.write(CODE_OFFSET) = val.0;\n        }\n\n        pub fn set_checksum(&mut self, val: u16) {\n            self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_length(&mut self, val: u8) {\n            *self.buf.write(LENGTH_OFFSET) = val;\n        }\n\n        pub fn set_next_hop_mtu(&mut self, val: u16) {\n            self.buf.set_bytes(NEXT_HOP_MTU_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_payload(&mut self, vals: &[u8]) {\n            let current_offset = Self::minimum_packet_size();\n            self.buf.as_slice_mut()[current_offset..current_offset + vals.len()]\n                .copy_from_slice(vals);\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn payload(&self) -> &[u8] {\n            let (payload, _) = self.split_payload_extension();\n            payload\n        }\n\n        #[must_use]\n        pub fn payload_raw(&self) -> &[u8] {\n            &self.buf.as_slice()[Self::minimum_packet_size()..]\n        }\n\n        #[must_use]\n        pub fn extension(&self) -> Option<&[u8]> {\n            let (_, extension) = self.split_payload_extension();\n            extension\n        }\n\n        fn split_payload_extension(&self) -> (&[u8], Option<&[u8]>) {\n            let length = usize::from(self.get_length()) * 4;\n            let icmp_payload = &self.buf.as_slice()[Self::minimum_packet_size()..];\n            split(length, icmp_payload)\n        }\n    }\n\n    impl Debug for DestinationUnreachablePacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"DestinationUnreachablePacket\")\n                .field(\"icmp_type\", &self.get_icmp_type())\n                .field(\"icmp_code\", &self.get_icmp_code())\n                .field(\"checksum\", &self.get_checksum())\n                .field(\"length\", &self.get_length())\n                .field(\"next_hop_mtu\", &self.get_next_hop_mtu())\n                .field(\"payload\", &fmt_payload(self.payload()))\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_icmp_type() {\n            let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()];\n            let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap();\n            packet.set_icmp_type(IcmpType::EchoRequest);\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!([0x08], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::EchoReply);\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!([0x00], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::DestinationUnreachable);\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!([0x03], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::TimeExceeded);\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!([0x0B], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::Other(255));\n            assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n            assert_eq!([0xFF], packet.packet()[0..1]);\n        }\n\n        #[test]\n        fn test_icmp_code() {\n            let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()];\n            let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap();\n            packet.set_icmp_code(IcmpCode(0));\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!([0x00], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(5));\n            assert_eq!(IcmpCode(5), packet.get_icmp_code());\n            assert_eq!([0x05], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(255));\n            assert_eq!(IcmpCode(255), packet.get_icmp_code());\n            assert_eq!([0xFF], packet.packet()[1..2]);\n        }\n\n        #[test]\n        fn test_checksum() {\n            let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()];\n            let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap();\n            packet.set_checksum(0);\n            assert_eq!(0, packet.get_checksum());\n            assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n            packet.set_checksum(1999);\n            assert_eq!(1999, packet.get_checksum());\n            assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n            packet.set_checksum(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_checksum());\n            assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n        }\n\n        #[test]\n        fn test_length() {\n            let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()];\n            let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap();\n            packet.set_length(0);\n            assert_eq!(0, packet.get_length());\n            assert_eq!([0x00], packet.packet()[5..6]);\n            packet.set_length(8);\n            assert_eq!(8, packet.get_length());\n            assert_eq!([0x08], packet.packet()[5..6]);\n            packet.set_length(u8::MAX);\n            assert_eq!(u8::MAX, packet.get_length());\n            assert_eq!([0xFF], packet.packet()[5..6]);\n        }\n\n        #[test]\n        fn test_view() {\n            let buf = [0x03, 0x03, 0xdf, 0xdc, 0x00, 0x00, 0x00, 0x00];\n            let packet = DestinationUnreachablePacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!(IcmpCode(3), packet.get_icmp_code());\n            assert_eq!(57308, packet.get_checksum());\n            assert_eq!(0, packet.get_length());\n            assert!(packet.payload().is_empty());\n        }\n\n        #[test]\n        fn test_view_large() {\n            let mut buf = [0x0_u8; 256];\n            buf[..8].copy_from_slice(&[0x03, 0x03, 0xdf, 0xdc, 0x00, 0x40, 0x00, 0x00]);\n            let packet = DestinationUnreachablePacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!(IcmpCode(3), packet.get_icmp_code());\n            assert_eq!(57308, packet.get_checksum());\n            assert_eq!(64, packet.get_length());\n            assert_eq!(&[0x0_u8; 248], packet.payload());\n            assert_eq!(None, packet.extension());\n        }\n\n        #[test]\n        fn test_new_insufficient_buffer() {\n            const SIZE: usize = DestinationUnreachablePacket::minimum_packet_size();\n            let mut buf = [0_u8; SIZE - 1];\n            let err = DestinationUnreachablePacket::new(&mut buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(\n                    String::from(\"DestinationUnreachablePacket\"),\n                    SIZE,\n                    SIZE - 1\n                ),\n                err\n            );\n        }\n\n        #[test]\n        fn test_new_view_insufficient_buffer() {\n            const SIZE: usize = DestinationUnreachablePacket::minimum_packet_size();\n            let buf = [0_u8; SIZE - 1];\n            let err = DestinationUnreachablePacket::new_view(&buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(\n                    String::from(\"DestinationUnreachablePacket\"),\n                    SIZE,\n                    SIZE - 1\n                ),\n                err\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/icmpv6.rs",
    "content": "use crate::buffer::Buffer;\nuse crate::error::{Error, Result};\nuse std::fmt::{Debug, Formatter};\n\n/// The type of `ICMPv6` packet.\n#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]\npub enum IcmpType {\n    EchoRequest,\n    EchoReply,\n    DestinationUnreachable,\n    TimeExceeded,\n    Other(u8),\n}\n\nimpl IcmpType {\n    #[must_use]\n    pub const fn id(&self) -> u8 {\n        match self {\n            Self::EchoRequest => 128,\n            Self::EchoReply => 129,\n            Self::DestinationUnreachable => 1,\n            Self::TimeExceeded => 3,\n            Self::Other(id) => *id,\n        }\n    }\n}\n\nimpl From<u8> for IcmpType {\n    fn from(val: u8) -> Self {\n        match val {\n            128 => Self::EchoRequest,\n            129 => Self::EchoReply,\n            1 => Self::DestinationUnreachable,\n            3 => Self::TimeExceeded,\n            id => Self::Other(id),\n        }\n    }\n}\n\n/// The `ICMPv6` code.\n#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]\npub struct IcmpCode(pub u8);\n\nimpl From<u8> for IcmpCode {\n    fn from(val: u8) -> Self {\n        Self(val)\n    }\n}\n\n/// The code for `TimeExceeded` `ICMPv6` packet type.\n#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]\npub enum IcmpTimeExceededCode {\n    /// Hop limit exceeded in transit.\n    TtlExpired,\n    /// Fragment reassembly time exceeded.\n    FragmentReassembly,\n    /// An unknown code.\n    Unknown(u8),\n}\n\nimpl From<IcmpCode> for IcmpTimeExceededCode {\n    fn from(val: IcmpCode) -> Self {\n        match val {\n            IcmpCode(0) => Self::TtlExpired,\n            IcmpCode(1) => Self::FragmentReassembly,\n            IcmpCode(id) => Self::Unknown(id),\n        }\n    }\n}\n\nconst TYPE_OFFSET: usize = 0;\nconst CODE_OFFSET: usize = 1;\nconst CHECKSUM_OFFSET: usize = 2;\n\n/// Represents an ICMP packet.\n///\n/// The internal representation is held in network byte order (big-endian) and all accessor methods\n/// take and return data in host byte order, converting as necessary for the given architecture.\npub struct IcmpPacket<'a> {\n    buf: Buffer<'a>,\n}\n\nimpl<'a> IcmpPacket<'a> {\n    pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Mutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"IcmpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Immutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"IcmpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    #[must_use]\n    pub const fn minimum_packet_size() -> usize {\n        8\n    }\n\n    #[must_use]\n    pub fn get_icmp_type(&self) -> IcmpType {\n        IcmpType::from(self.buf.read(TYPE_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_icmp_code(&self) -> IcmpCode {\n        IcmpCode::from(self.buf.read(CODE_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_checksum(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n    }\n\n    pub fn set_icmp_type(&mut self, val: IcmpType) {\n        *self.buf.write(TYPE_OFFSET) = val.id();\n    }\n\n    pub fn set_icmp_code(&mut self, val: IcmpCode) {\n        *self.buf.write(CODE_OFFSET) = val.0;\n    }\n\n    pub fn set_checksum(&mut self, val: u16) {\n        self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n    }\n\n    #[must_use]\n    pub fn packet(&self) -> &[u8] {\n        self.buf.as_slice()\n    }\n}\n\nimpl Debug for IcmpPacket<'_> {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"IcmpPacket\")\n            .field(\"icmp_type\", &self.get_icmp_type())\n            .field(\"icmp_code\", &self.get_icmp_code())\n            .field(\"checksum\", &self.get_checksum())\n            .finish()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_icmp_type() {\n        let mut buf = [0_u8; IcmpPacket::minimum_packet_size()];\n        let mut packet = IcmpPacket::new(&mut buf).unwrap();\n        packet.set_icmp_type(IcmpType::EchoRequest);\n        assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n        assert_eq!([0x80], packet.packet()[0..1]);\n        packet.set_icmp_type(IcmpType::EchoReply);\n        assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n        assert_eq!([0x81], packet.packet()[0..1]);\n        packet.set_icmp_type(IcmpType::DestinationUnreachable);\n        assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n        assert_eq!([0x01], packet.packet()[0..1]);\n        packet.set_icmp_type(IcmpType::TimeExceeded);\n        assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n        assert_eq!([0x03], packet.packet()[0..1]);\n        packet.set_icmp_type(IcmpType::Other(255));\n        assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n        assert_eq!([0xFF], packet.packet()[0..1]);\n    }\n\n    #[test]\n    fn test_icmp_code() {\n        let mut buf = [0_u8; IcmpPacket::minimum_packet_size()];\n        let mut packet = IcmpPacket::new(&mut buf).unwrap();\n        packet.set_icmp_code(IcmpCode(0));\n        assert_eq!(IcmpCode(0), packet.get_icmp_code());\n        assert_eq!([0x00], packet.packet()[1..2]);\n        packet.set_icmp_code(IcmpCode(5));\n        assert_eq!(IcmpCode(5), packet.get_icmp_code());\n        assert_eq!([0x05], packet.packet()[1..2]);\n        packet.set_icmp_code(IcmpCode(255));\n        assert_eq!(IcmpCode(255), packet.get_icmp_code());\n        assert_eq!([0xFF], packet.packet()[1..2]);\n    }\n\n    #[test]\n    fn test_checksum() {\n        let mut buf = [0_u8; IcmpPacket::minimum_packet_size()];\n        let mut packet = IcmpPacket::new(&mut buf).unwrap();\n        packet.set_checksum(0);\n        assert_eq!(0, packet.get_checksum());\n        assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n        packet.set_checksum(1999);\n        assert_eq!(1999, packet.get_checksum());\n        assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n        packet.set_checksum(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_checksum());\n        assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n    }\n\n    #[test]\n    fn test_new_insufficient_buffer() {\n        const SIZE: usize = IcmpPacket::minimum_packet_size();\n        let mut buf = [0_u8; SIZE - 1];\n        let err = IcmpPacket::new(&mut buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"IcmpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n\n    #[test]\n    fn test_new_view_insufficient_buffer() {\n        const SIZE: usize = IcmpPacket::minimum_packet_size();\n        let buf = [0_u8; SIZE - 1];\n        let err = IcmpPacket::new_view(&buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"IcmpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n}\n\npub mod echo_request {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::fmt_payload;\n    use crate::icmpv6::{IcmpCode, IcmpType};\n    use std::fmt::{Debug, Formatter};\n\n    const TYPE_OFFSET: usize = 0;\n    const CODE_OFFSET: usize = 1;\n    const CHECKSUM_OFFSET: usize = 2;\n    const IDENTIFIER_OFFSET: usize = 4;\n    const SEQUENCE_OFFSET: usize = 6;\n\n    /// Represents an `ICMPv6` `EchoRequest` packet.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct EchoRequestPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> EchoRequestPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"EchoRequestPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"EchoRequestPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            8\n        }\n\n        #[must_use]\n        pub fn get_icmp_type(&self) -> IcmpType {\n            IcmpType::from(self.buf.read(TYPE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_icmp_code(&self) -> IcmpCode {\n            IcmpCode::from(self.buf.read(CODE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_checksum(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_identifier(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(IDENTIFIER_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_sequence(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET))\n        }\n\n        pub fn set_icmp_type(&mut self, val: IcmpType) {\n            *self.buf.write(TYPE_OFFSET) = val.id();\n        }\n\n        pub fn set_icmp_code(&mut self, val: IcmpCode) {\n            *self.buf.write(CODE_OFFSET) = val.0;\n        }\n\n        pub fn set_checksum(&mut self, val: u16) {\n            self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_identifier(&mut self, val: u16) {\n            self.buf.set_bytes(IDENTIFIER_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_sequence(&mut self, val: u16) {\n            self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_payload(&mut self, vals: &[u8]) {\n            let current_offset = Self::minimum_packet_size();\n            self.buf.as_slice_mut()[current_offset..current_offset + vals.len()]\n                .copy_from_slice(vals);\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn payload(&self) -> &[u8] {\n            &self.buf.as_slice()[Self::minimum_packet_size()..]\n        }\n    }\n\n    impl Debug for EchoRequestPacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"EchoRequestPacket\")\n                .field(\"icmp_type\", &self.get_icmp_type())\n                .field(\"icmp_code\", &self.get_icmp_code())\n                .field(\"checksum\", &self.get_checksum())\n                .field(\"identifier\", &self.get_identifier())\n                .field(\"sequence\", &self.get_sequence())\n                .field(\"payload\", &fmt_payload(self.payload()))\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_icmp_type() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_icmp_type(IcmpType::EchoRequest);\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!([0x80], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::EchoReply);\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!([0x81], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::DestinationUnreachable);\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!([0x01], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::TimeExceeded);\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!([0x03], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::Other(255));\n            assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n            assert_eq!([0xFF], packet.packet()[0..1]);\n        }\n\n        #[test]\n        fn test_icmp_code() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_icmp_code(IcmpCode(0));\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!([0x00], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(5));\n            assert_eq!(IcmpCode(5), packet.get_icmp_code());\n            assert_eq!([0x05], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(255));\n            assert_eq!(IcmpCode(255), packet.get_icmp_code());\n            assert_eq!([0xFF], packet.packet()[1..2]);\n        }\n\n        #[test]\n        fn test_checksum() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_checksum(0);\n            assert_eq!(0, packet.get_checksum());\n            assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n            packet.set_checksum(1999);\n            assert_eq!(1999, packet.get_checksum());\n            assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n            packet.set_checksum(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_checksum());\n            assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n        }\n\n        #[test]\n        fn test_identifier() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_identifier(0);\n            assert_eq!(0, packet.get_identifier());\n            assert_eq!([0x00, 0x00], packet.packet()[4..=5]);\n            packet.set_identifier(1999);\n            assert_eq!(1999, packet.get_identifier());\n            assert_eq!([0x07, 0xCF], packet.packet()[4..=5]);\n            packet.set_identifier(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_identifier());\n            assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]);\n        }\n\n        #[test]\n        fn test_sequence() {\n            let mut buf = [0_u8; EchoRequestPacket::minimum_packet_size()];\n            let mut packet = EchoRequestPacket::new(&mut buf).unwrap();\n            packet.set_sequence(0);\n            assert_eq!(0, packet.get_sequence());\n            assert_eq!([0x00, 0x00], packet.packet()[6..=7]);\n            packet.set_sequence(1999);\n            assert_eq!(1999, packet.get_sequence());\n            assert_eq!([0x07, 0xCF], packet.packet()[6..=7]);\n            packet.set_sequence(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_sequence());\n            assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]);\n        }\n\n        #[test]\n        fn test_view() {\n            let buf = [0x80, 0x00, 0x16, 0x7c, 0x60, 0x9b, 0x82, 0x9a];\n            let packet = EchoRequestPacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!(5756, packet.get_checksum());\n            assert_eq!(24731, packet.get_identifier());\n            assert_eq!(33434, packet.get_sequence());\n            assert!(packet.payload().is_empty());\n        }\n\n        #[test]\n        fn test_new_insufficient_buffer() {\n            const SIZE: usize = EchoRequestPacket::minimum_packet_size();\n            let mut buf = [0_u8; SIZE - 1];\n            let err = EchoRequestPacket::new(&mut buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"EchoRequestPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n\n        #[test]\n        fn test_new_view_insufficient_buffer() {\n            const SIZE: usize = EchoRequestPacket::minimum_packet_size();\n            let buf = [0_u8; SIZE - 1];\n            let err = EchoRequestPacket::new_view(&buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"EchoRequestPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n    }\n}\n\npub mod echo_reply {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::fmt_payload;\n    use crate::icmpv6::{IcmpCode, IcmpType};\n    use std::fmt::{Debug, Formatter};\n\n    const TYPE_OFFSET: usize = 0;\n    const CODE_OFFSET: usize = 1;\n    const CHECKSUM_OFFSET: usize = 2;\n    const IDENTIFIER_OFFSET: usize = 4;\n    const SEQUENCE_OFFSET: usize = 6;\n\n    /// Represents an ICMP `EchoReply` packet.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct EchoReplyPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> EchoReplyPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"EchoReplyPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"EchoReplyPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            8\n        }\n\n        #[must_use]\n        pub fn get_icmp_type(&self) -> IcmpType {\n            IcmpType::from(self.buf.read(TYPE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_icmp_code(&self) -> IcmpCode {\n            IcmpCode::from(self.buf.read(CODE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_checksum(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_identifier(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(IDENTIFIER_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_sequence(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET))\n        }\n\n        pub fn set_icmp_type(&mut self, val: IcmpType) {\n            *self.buf.write(TYPE_OFFSET) = val.id();\n        }\n\n        pub fn set_icmp_code(&mut self, val: IcmpCode) {\n            *self.buf.write(CODE_OFFSET) = val.0;\n        }\n\n        pub fn set_checksum(&mut self, val: u16) {\n            self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_identifier(&mut self, val: u16) {\n            self.buf.set_bytes(IDENTIFIER_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_sequence(&mut self, val: u16) {\n            self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_payload(&mut self, vals: &[u8]) {\n            let current_offset = Self::minimum_packet_size();\n            self.buf.as_slice_mut()[current_offset..current_offset + vals.len()]\n                .copy_from_slice(vals);\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn payload(&self) -> &[u8] {\n            &self.buf.as_slice()[Self::minimum_packet_size()..]\n        }\n    }\n\n    impl Debug for EchoReplyPacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"EchoReplyPacket\")\n                .field(\"icmp_type\", &self.get_icmp_type())\n                .field(\"icmp_code\", &self.get_icmp_code())\n                .field(\"checksum\", &self.get_checksum())\n                .field(\"identifier\", &self.get_identifier())\n                .field(\"sequence\", &self.get_sequence())\n                .field(\"payload\", &fmt_payload(self.payload()))\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_icmp_type() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_icmp_type(IcmpType::EchoRequest);\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!([0x80], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::EchoReply);\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!([0x81], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::DestinationUnreachable);\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!([0x01], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::TimeExceeded);\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!([0x03], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::Other(255));\n            assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n            assert_eq!([0xFF], packet.packet()[0..1]);\n        }\n\n        #[test]\n        fn test_icmp_code() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_icmp_code(IcmpCode(0));\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!([0x00], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(5));\n            assert_eq!(IcmpCode(5), packet.get_icmp_code());\n            assert_eq!([0x05], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(255));\n            assert_eq!(IcmpCode(255), packet.get_icmp_code());\n            assert_eq!([0xFF], packet.packet()[1..2]);\n        }\n\n        #[test]\n        fn test_checksum() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_checksum(0);\n            assert_eq!(0, packet.get_checksum());\n            assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n            packet.set_checksum(1999);\n            assert_eq!(1999, packet.get_checksum());\n            assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n            packet.set_checksum(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_checksum());\n            assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n        }\n\n        #[test]\n        fn test_identifier() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_identifier(0);\n            assert_eq!(0, packet.get_identifier());\n            assert_eq!([0x00, 0x00], packet.packet()[4..=5]);\n            packet.set_identifier(1999);\n            assert_eq!(1999, packet.get_identifier());\n            assert_eq!([0x07, 0xCF], packet.packet()[4..=5]);\n            packet.set_identifier(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_identifier());\n            assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]);\n        }\n\n        #[test]\n        fn test_sequence() {\n            let mut buf = [0_u8; EchoReplyPacket::minimum_packet_size()];\n            let mut packet = EchoReplyPacket::new(&mut buf).unwrap();\n            packet.set_sequence(0);\n            assert_eq!(0, packet.get_sequence());\n            assert_eq!([0x00, 0x00], packet.packet()[6..=7]);\n            packet.set_sequence(1999);\n            assert_eq!(1999, packet.get_sequence());\n            assert_eq!([0x07, 0xCF], packet.packet()[6..=7]);\n            packet.set_sequence(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_sequence());\n            assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]);\n        }\n\n        #[test]\n        fn test_view() {\n            let buf = [0x81, 0x00, 0x1e, 0x70, 0x60, 0x9b, 0x80, 0xf4];\n            let packet = EchoReplyPacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!(7792, packet.get_checksum());\n            assert_eq!(24731, packet.get_identifier());\n            assert_eq!(33012, packet.get_sequence());\n            assert!(packet.payload().is_empty());\n        }\n\n        #[test]\n        fn test_new_insufficient_buffer() {\n            const SIZE: usize = EchoReplyPacket::minimum_packet_size();\n            let mut buf = [0_u8; SIZE - 1];\n            let err = EchoReplyPacket::new(&mut buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"EchoReplyPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n\n        #[test]\n        fn test_new_view_insufficient_buffer() {\n            const SIZE: usize = EchoReplyPacket::minimum_packet_size();\n            let buf = [0_u8; SIZE - 1];\n            let err = EchoReplyPacket::new_view(&buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"EchoReplyPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n    }\n}\n\npub mod time_exceeded {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::fmt_payload;\n    use crate::icmp_extension::extension_splitter::split;\n    use crate::icmpv6::{IcmpCode, IcmpType};\n    use std::fmt::{Debug, Formatter};\n\n    const TYPE_OFFSET: usize = 0;\n    const CODE_OFFSET: usize = 1;\n    const CHECKSUM_OFFSET: usize = 2;\n    const LENGTH_OFFSET: usize = 4;\n\n    /// Represents an ICMP `TimeExceeded` packet.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct TimeExceededPacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> TimeExceededPacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"TimeExceededPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"TimeExceededPacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            8\n        }\n\n        #[must_use]\n        pub fn get_icmp_type(&self) -> IcmpType {\n            IcmpType::from(self.buf.read(TYPE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_icmp_code(&self) -> IcmpCode {\n            IcmpCode::from(self.buf.read(CODE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_checksum(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_length(&self) -> u8 {\n            self.buf.read(LENGTH_OFFSET)\n        }\n\n        pub fn set_icmp_type(&mut self, val: IcmpType) {\n            *self.buf.write(TYPE_OFFSET) = val.id();\n        }\n\n        pub fn set_icmp_code(&mut self, val: IcmpCode) {\n            *self.buf.write(CODE_OFFSET) = val.0;\n        }\n\n        pub fn set_checksum(&mut self, val: u16) {\n            self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_length(&mut self, val: u8) {\n            *self.buf.write(LENGTH_OFFSET) = val;\n        }\n\n        pub fn set_payload(&mut self, vals: &[u8]) {\n            let current_offset = Self::minimum_packet_size();\n            self.buf.as_slice_mut()[current_offset..current_offset + vals.len()]\n                .copy_from_slice(vals);\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn payload(&self) -> &[u8] {\n            let (payload, _) = self.split_payload_extension();\n            payload\n        }\n\n        #[must_use]\n        pub fn payload_raw(&self) -> &[u8] {\n            &self.buf.as_slice()[Self::minimum_packet_size()..]\n        }\n\n        #[must_use]\n        pub fn extension(&self) -> Option<&[u8]> {\n            let (_, extension) = self.split_payload_extension();\n            extension\n        }\n\n        fn split_payload_extension(&self) -> (&[u8], Option<&[u8]>) {\n            let length = usize::from(self.get_length()) * 8;\n            let icmp_payload = &self.buf.as_slice()[Self::minimum_packet_size()..];\n            split(length, icmp_payload)\n        }\n    }\n\n    impl Debug for TimeExceededPacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"TimeExceededPacket\")\n                .field(\"icmp_type\", &self.get_icmp_type())\n                .field(\"icmp_code\", &self.get_icmp_code())\n                .field(\"checksum\", &self.get_checksum())\n                .field(\"length\", &self.get_length())\n                .field(\"payload\", &fmt_payload(self.payload()))\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_icmp_type() {\n            let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()];\n            let mut packet = TimeExceededPacket::new(&mut buf).unwrap();\n            packet.set_icmp_type(IcmpType::EchoRequest);\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!([0x80], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::EchoReply);\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!([0x81], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::DestinationUnreachable);\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!([0x01], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::TimeExceeded);\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!([0x03], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::Other(255));\n            assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n            assert_eq!([0xFF], packet.packet()[0..1]);\n        }\n\n        #[test]\n        fn test_icmp_code() {\n            let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()];\n            let mut packet = TimeExceededPacket::new(&mut buf).unwrap();\n            packet.set_icmp_code(IcmpCode(0));\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!([0x00], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(5));\n            assert_eq!(IcmpCode(5), packet.get_icmp_code());\n            assert_eq!([0x05], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(255));\n            assert_eq!(IcmpCode(255), packet.get_icmp_code());\n            assert_eq!([0xFF], packet.packet()[1..2]);\n        }\n\n        #[test]\n        fn test_checksum() {\n            let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()];\n            let mut packet = TimeExceededPacket::new(&mut buf).unwrap();\n            packet.set_checksum(0);\n            assert_eq!(0, packet.get_checksum());\n            assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n            packet.set_checksum(1999);\n            assert_eq!(1999, packet.get_checksum());\n            assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n            packet.set_checksum(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_checksum());\n            assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n        }\n\n        #[test]\n        fn test_length() {\n            let mut buf = [0_u8; TimeExceededPacket::minimum_packet_size()];\n            let mut packet = TimeExceededPacket::new(&mut buf).unwrap();\n            packet.set_length(0);\n            assert_eq!(0, packet.get_length());\n            assert_eq!([0x00], packet.packet()[4..5]);\n            packet.set_length(8);\n            assert_eq!(8, packet.get_length());\n            assert_eq!([0x08], packet.packet()[4..5]);\n            packet.set_length(u8::MAX);\n            assert_eq!(u8::MAX, packet.get_length());\n            assert_eq!([0xFF], packet.packet()[4..5]);\n        }\n\n        #[test]\n        fn test_view() {\n            let buf = [0x03, 0x00, 0xf4, 0xee, 0x11, 0x00, 0x00, 0x00];\n            let packet = TimeExceededPacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!(62702, packet.get_checksum());\n            assert_eq!(17, packet.get_length());\n            assert!(packet.payload().is_empty());\n        }\n\n        #[test]\n        fn test_view_large() {\n            let mut buf = [0x0_u8; 128];\n            buf[..8].copy_from_slice(&[0x03, 0x00, 0xf4, 0xee, 0x20, 0x00, 0x00, 0x00]);\n            let packet = TimeExceededPacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!(62702, packet.get_checksum());\n            assert_eq!(32, packet.get_length());\n            assert_eq!(&[0x0_u8; 120], packet.payload());\n            assert_eq!(None, packet.extension());\n        }\n\n        #[test]\n        fn test_new_insufficient_buffer() {\n            const SIZE: usize = TimeExceededPacket::minimum_packet_size();\n            let mut buf = [0_u8; SIZE - 1];\n            let err = TimeExceededPacket::new(&mut buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"TimeExceededPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n\n        #[test]\n        fn test_new_view_insufficient_buffer() {\n            const SIZE: usize = TimeExceededPacket::minimum_packet_size();\n            let buf = [0_u8; SIZE - 1];\n            let err = TimeExceededPacket::new_view(&buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(String::from(\"TimeExceededPacket\"), SIZE, SIZE - 1),\n                err\n            );\n        }\n    }\n}\n\npub mod destination_unreachable {\n    use crate::buffer::Buffer;\n    use crate::error::{Error, Result};\n    use crate::fmt_payload;\n    use crate::icmp_extension::extension_splitter::split;\n    use crate::icmpv6::{IcmpCode, IcmpType};\n    use std::fmt::{Debug, Formatter};\n\n    const TYPE_OFFSET: usize = 0;\n    const CODE_OFFSET: usize = 1;\n    const CHECKSUM_OFFSET: usize = 2;\n    const LENGTH_OFFSET: usize = 4;\n    const NEXT_HOP_MTU_OFFSET: usize = 6;\n\n    /// Represents an ICMP `DestinationUnreachable` packet.\n    ///\n    /// The internal representation is held in network byte order (big-endian) and all accessor\n    /// methods take and return data in host byte order, converting as necessary for the given\n    /// architecture.\n    pub struct DestinationUnreachablePacket<'a> {\n        buf: Buffer<'a>,\n    }\n\n    impl<'a> DestinationUnreachablePacket<'a> {\n        pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Mutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"DestinationUnreachablePacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n            if packet.len() >= Self::minimum_packet_size() {\n                Ok(Self {\n                    buf: Buffer::Immutable(packet),\n                })\n            } else {\n                Err(Error::InsufficientPacketBuffer(\n                    String::from(\"DestinationUnreachablePacket\"),\n                    Self::minimum_packet_size(),\n                    packet.len(),\n                ))\n            }\n        }\n\n        #[must_use]\n        pub const fn minimum_packet_size() -> usize {\n            8\n        }\n\n        #[must_use]\n        pub fn get_icmp_type(&self) -> IcmpType {\n            IcmpType::from(self.buf.read(TYPE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_icmp_code(&self) -> IcmpCode {\n            IcmpCode::from(self.buf.read(CODE_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_checksum(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n        }\n\n        #[must_use]\n        pub fn get_length(&self) -> u8 {\n            self.buf.read(LENGTH_OFFSET)\n        }\n\n        #[must_use]\n        pub fn get_next_hop_mtu(&self) -> u16 {\n            u16::from_be_bytes(self.buf.get_bytes(NEXT_HOP_MTU_OFFSET))\n        }\n\n        pub fn set_icmp_type(&mut self, val: IcmpType) {\n            *self.buf.write(TYPE_OFFSET) = val.id();\n        }\n\n        pub fn set_icmp_code(&mut self, val: IcmpCode) {\n            *self.buf.write(CODE_OFFSET) = val.0;\n        }\n\n        pub fn set_checksum(&mut self, val: u16) {\n            self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_length(&mut self, val: u8) {\n            *self.buf.write(LENGTH_OFFSET) = val;\n        }\n\n        pub fn set_next_hop_mtu(&mut self, val: u16) {\n            self.buf.set_bytes(NEXT_HOP_MTU_OFFSET, val.to_be_bytes());\n        }\n\n        pub fn set_payload(&mut self, vals: &[u8]) {\n            let current_offset = Self::minimum_packet_size();\n            self.buf.as_slice_mut()[current_offset..current_offset + vals.len()]\n                .copy_from_slice(vals);\n        }\n\n        #[must_use]\n        pub fn packet(&self) -> &[u8] {\n            self.buf.as_slice()\n        }\n\n        #[must_use]\n        pub fn payload(&self) -> &[u8] {\n            let (payload, _) = self.split_payload_extension();\n            payload\n        }\n\n        #[must_use]\n        pub fn payload_raw(&self) -> &[u8] {\n            &self.buf.as_slice()[Self::minimum_packet_size()..]\n        }\n\n        #[must_use]\n        pub fn extension(&self) -> Option<&[u8]> {\n            let (_, extension) = self.split_payload_extension();\n            extension\n        }\n\n        fn split_payload_extension(&self) -> (&[u8], Option<&[u8]>) {\n            // From rfc4884:\n            //\n            // \"For ICMPv6 messages, the length attribute represents 64-bit words\"\n            let length = usize::from(self.get_length()) * 8;\n            let icmp_payload = &self.buf.as_slice()[Self::minimum_packet_size()..];\n            split(length, icmp_payload)\n        }\n    }\n\n    impl Debug for DestinationUnreachablePacket<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            f.debug_struct(\"DestinationUnreachablePacket\")\n                .field(\"icmp_type\", &self.get_icmp_type())\n                .field(\"icmp_code\", &self.get_icmp_code())\n                .field(\"checksum\", &self.get_checksum())\n                .field(\"length\", &self.get_length())\n                .field(\"next_hop_mtu\", &self.get_next_hop_mtu())\n                .field(\"payload\", &fmt_payload(self.payload()))\n                .finish()\n        }\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_icmp_type() {\n            let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()];\n            let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap();\n            packet.set_icmp_type(IcmpType::EchoRequest);\n            assert_eq!(IcmpType::EchoRequest, packet.get_icmp_type());\n            assert_eq!([0x80], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::EchoReply);\n            assert_eq!(IcmpType::EchoReply, packet.get_icmp_type());\n            assert_eq!([0x81], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::DestinationUnreachable);\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!([0x01], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::TimeExceeded);\n            assert_eq!(IcmpType::TimeExceeded, packet.get_icmp_type());\n            assert_eq!([0x03], packet.packet()[0..1]);\n            packet.set_icmp_type(IcmpType::Other(255));\n            assert_eq!(IcmpType::Other(255), packet.get_icmp_type());\n            assert_eq!([0xFF], packet.packet()[0..1]);\n        }\n\n        #[test]\n        fn test_icmp_code() {\n            let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()];\n            let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap();\n            packet.set_icmp_code(IcmpCode(0));\n            assert_eq!(IcmpCode(0), packet.get_icmp_code());\n            assert_eq!([0x00], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(5));\n            assert_eq!(IcmpCode(5), packet.get_icmp_code());\n            assert_eq!([0x05], packet.packet()[1..2]);\n            packet.set_icmp_code(IcmpCode(255));\n            assert_eq!(IcmpCode(255), packet.get_icmp_code());\n            assert_eq!([0xFF], packet.packet()[1..2]);\n        }\n\n        #[test]\n        fn test_checksum() {\n            let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()];\n            let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap();\n            packet.set_checksum(0);\n            assert_eq!(0, packet.get_checksum());\n            assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n            packet.set_checksum(1999);\n            assert_eq!(1999, packet.get_checksum());\n            assert_eq!([0x07, 0xCF], packet.packet()[2..=3]);\n            packet.set_checksum(u16::MAX);\n            assert_eq!(u16::MAX, packet.get_checksum());\n            assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n        }\n\n        #[test]\n        fn test_length() {\n            let mut buf = [0_u8; DestinationUnreachablePacket::minimum_packet_size()];\n            let mut packet = DestinationUnreachablePacket::new(&mut buf).unwrap();\n            packet.set_length(0);\n            assert_eq!(0, packet.get_length());\n            assert_eq!([0x00], packet.packet()[4..5]);\n            packet.set_length(8);\n            assert_eq!(8, packet.get_length());\n            assert_eq!([0x08], packet.packet()[4..5]);\n            packet.set_length(u8::MAX);\n            assert_eq!(u8::MAX, packet.get_length());\n            assert_eq!([0xFF], packet.packet()[4..5]);\n        }\n\n        #[test]\n        fn test_view() {\n            let buf = [0x01, 0x03, 0xdf, 0xdc, 0x00, 0x00, 0x00, 0x00];\n            let packet = DestinationUnreachablePacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!(IcmpCode(3), packet.get_icmp_code());\n            assert_eq!(57308, packet.get_checksum());\n            assert_eq!(0, packet.get_length());\n            assert!(packet.payload().is_empty());\n        }\n\n        #[test]\n        fn test_view_large() {\n            let mut buf = [0x0_u8; 128];\n            buf[..8].copy_from_slice(&[0x01, 0x03, 0xdf, 0xdc, 0x20, 0x00, 0x00, 0x00]);\n            let packet = DestinationUnreachablePacket::new_view(&buf).unwrap();\n            assert_eq!(IcmpType::DestinationUnreachable, packet.get_icmp_type());\n            assert_eq!(IcmpCode(3), packet.get_icmp_code());\n            assert_eq!(57308, packet.get_checksum());\n            assert_eq!(32, packet.get_length());\n            assert_eq!(&[0x0_u8; 120], packet.payload());\n            assert_eq!(None, packet.extension());\n        }\n\n        #[test]\n        fn test_new_insufficient_buffer() {\n            const SIZE: usize = DestinationUnreachablePacket::minimum_packet_size();\n            let mut buf = [0_u8; SIZE - 1];\n            let err = DestinationUnreachablePacket::new(&mut buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(\n                    String::from(\"DestinationUnreachablePacket\"),\n                    SIZE,\n                    SIZE - 1\n                ),\n                err\n            );\n        }\n\n        #[test]\n        fn test_new_view_insufficient_buffer() {\n            const SIZE: usize = DestinationUnreachablePacket::minimum_packet_size();\n            let buf = [0_u8; SIZE - 1];\n            let err = DestinationUnreachablePacket::new_view(&buf).unwrap_err();\n            assert_eq!(\n                Error::InsufficientPacketBuffer(\n                    String::from(\"DestinationUnreachablePacket\"),\n                    SIZE,\n                    SIZE - 1\n                ),\n                err\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/ip.rs",
    "content": "use crate::buffer::Buffer;\nuse crate::error::{Error, Result};\nuse std::fmt::{Debug, Formatter};\n\n/// The IP packet version.\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum IpVersion {\n    Ipv4,\n    Ipv6,\n    Other(u8),\n}\n\nimpl IpVersion {\n    #[must_use]\n    pub const fn id(self) -> u8 {\n        match self {\n            Self::Ipv4 => 4,\n            Self::Ipv6 => 6,\n            Self::Other(id) => id,\n        }\n    }\n\n    #[must_use]\n    pub const fn new(value: u8) -> Self {\n        Self::Other(value)\n    }\n}\n\nimpl From<u8> for IpVersion {\n    fn from(id: u8) -> Self {\n        match id {\n            4 => Self::Ipv4,\n            6 => Self::Ipv6,\n            p => Self::Other(p),\n        }\n    }\n}\n\nconst VERSION_OFFSET: usize = 0;\n\n/// Represents a generic IP packet.\n///\n/// The internal representation is held in network byte order (big-endian) and all accessor methods\n/// take and return data in host byte order, converting as necessary for the given architecture.\npub struct IpPacket<'a> {\n    buf: Buffer<'a>,\n}\n\nimpl<'a> IpPacket<'a> {\n    pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Mutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"IpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Immutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"IpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    #[must_use]\n    pub const fn minimum_packet_size() -> usize {\n        20\n    }\n\n    #[must_use]\n    pub fn get_version(&self) -> IpVersion {\n        IpVersion::from((self.buf.read(VERSION_OFFSET) & 0xF0) >> 4)\n    }\n\n    pub fn set_version(&mut self, val: IpVersion) {\n        *self.buf.write(VERSION_OFFSET) =\n            (self.buf.read(VERSION_OFFSET) & 0x0F) | ((val.id() & 0x0F) << 4);\n    }\n\n    #[must_use]\n    pub fn packet(&self) -> &[u8] {\n        self.buf.as_slice()\n    }\n}\n\nimpl Debug for IpPacket<'_> {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"IpPacket\")\n            .field(\"version\", &self.get_version())\n            .finish()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_version() {\n        let mut buf = [0_u8; IpPacket::minimum_packet_size()];\n        let mut packet = IpPacket::new(&mut buf).unwrap();\n        packet.set_version(IpVersion::Ipv4);\n        assert_eq!(IpVersion::Ipv4, packet.get_version());\n        assert_eq!([0x40], packet.packet()[..1]);\n        packet.set_version(IpVersion::Ipv6);\n        assert_eq!(IpVersion::Ipv6, packet.get_version());\n        assert_eq!([0x60], packet.packet()[..1]);\n        packet.set_version(IpVersion::Other(15));\n        assert_eq!(IpVersion::Other(15), packet.get_version());\n        assert_eq!([0xF0], packet.packet()[..1]);\n    }\n\n    #[test]\n    fn test_view_ipv4_packet() {\n        let buf = hex_literal::hex!(\n            \"\n           45 00 00 54 a2 71 00 00 15 11 9a ee 7f 00 00 01\n           de 9a 56 12\n           \"\n        );\n        let packet = IpPacket::new_view(&buf).unwrap();\n        assert_eq!(IpVersion::Ipv4, packet.get_version());\n    }\n\n    #[test]\n    fn test_view_ipv6_packet() {\n        let buf = hex_literal::hex!(\n            \"\n           60 06 05 00 00 20 06 40 fe 80 00 00 00 00 00 00\n           1c 8d 7d 69 d0 b6 81 82 fe 80 00 00 00 00 00 00\n           08 11 03 f6 76 01 6c 3f\n           \"\n        );\n        let packet = IpPacket::new_view(&buf).unwrap();\n        assert_eq!(IpVersion::Ipv6, packet.get_version());\n    }\n\n    #[test]\n    fn test_new_insufficient_buffer() {\n        const SIZE: usize = IpPacket::minimum_packet_size();\n        let mut buf = [0_u8; SIZE - 1];\n        let err = IpPacket::new(&mut buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"IpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n\n    #[test]\n    fn test_new_view_insufficient_buffer() {\n        const SIZE: usize = IpPacket::minimum_packet_size();\n        let buf = [0_u8; SIZE - 1];\n        let err = IpPacket::new_view(&buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"IpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/ipv4.rs",
    "content": "use crate::buffer::Buffer;\nuse crate::error::{Error, Result};\nuse crate::{IpProtocol, fmt_payload};\nuse std::fmt::{Debug, Formatter};\nuse std::net::Ipv4Addr;\n\nconst VERSION_OFFSET: usize = 0;\nconst IHL_OFFSET: usize = 0;\nconst DSCP_OFFSET: usize = 1;\nconst ECN_OFFSET: usize = 1;\nconst TOTAL_LENGTH_OFFSET: usize = 2;\nconst IDENTIFICATION_OFFSET: usize = 4;\nconst FLAGS_AND_FRAGMENT_OFFSET_OFFSET: usize = 6;\nconst TIME_TO_LIVE_OFFSET: usize = 8;\nconst PROTOCOL_OFFSET: usize = 9;\nconst CHECKSUM_OFFSET: usize = 10;\nconst SOURCE_OFFSET: usize = 12;\nconst DESTINATION_OFFSET: usize = 16;\n\n/// Represents an IPv4 Packet.\n///\n/// The internal representation is held in network byte order (big-endian) and all accessor methods\n/// take and return data in host byte order, converting as necessary for the given architecture.\npub struct Ipv4Packet<'a> {\n    buf: Buffer<'a>,\n}\n\nimpl<'a> Ipv4Packet<'a> {\n    pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Mutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"Ipv4Packet\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Immutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"Ipv4Packet\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    #[must_use]\n    pub const fn minimum_packet_size() -> usize {\n        20\n    }\n\n    #[must_use]\n    pub fn get_version(&self) -> u8 {\n        (self.buf.read(VERSION_OFFSET) & 0xf0) >> 4\n    }\n\n    #[must_use]\n    pub fn get_header_length(&self) -> u8 {\n        self.buf.read(IHL_OFFSET) & 0xf\n    }\n\n    #[must_use]\n    pub fn get_dscp(&self) -> u8 {\n        (self.buf.read(DSCP_OFFSET) & 0xfc) >> 2\n    }\n\n    #[must_use]\n    pub fn get_ecn(&self) -> u8 {\n        self.buf.read(ECN_OFFSET) & 0x3\n    }\n\n    #[must_use]\n    pub fn get_tos(&self) -> u8 {\n        (self.get_dscp() << 2) | self.get_ecn()\n    }\n\n    #[must_use]\n    pub fn get_total_length(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(TOTAL_LENGTH_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_identification(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(IDENTIFICATION_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_flags_and_fragment_offset(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(FLAGS_AND_FRAGMENT_OFFSET_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_ttl(&self) -> u8 {\n        self.buf.read(TIME_TO_LIVE_OFFSET)\n    }\n\n    #[must_use]\n    pub fn get_protocol(&self) -> IpProtocol {\n        IpProtocol::from(self.buf.read(PROTOCOL_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_checksum(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_source(&self) -> Ipv4Addr {\n        Ipv4Addr::from(self.buf.get_bytes(SOURCE_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_destination(&self) -> Ipv4Addr {\n        Ipv4Addr::from(self.buf.get_bytes(DESTINATION_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_options_raw(&self) -> &[u8] {\n        let current_offset = Self::minimum_packet_size();\n        let end = std::cmp::min(\n            current_offset + ipv4_options_length(self),\n            self.buf.as_slice().len(),\n        );\n        &self.buf.as_slice()[current_offset..end]\n    }\n\n    pub fn set_version(&mut self, val: u8) {\n        *self.buf.write(VERSION_OFFSET) =\n            (self.buf.read(VERSION_OFFSET) & 0xf) | ((val & 0xf) << 4);\n    }\n\n    pub fn set_header_length(&mut self, val: u8) {\n        *self.buf.write(IHL_OFFSET) = (self.buf.read(IHL_OFFSET) & 0xf0) | (val & 0xf);\n    }\n\n    pub fn set_dscp(&mut self, val: u8) {\n        *self.buf.write(DSCP_OFFSET) = (self.buf.read(DSCP_OFFSET) & 0x3) | ((val & 0x3f) << 2);\n    }\n\n    pub fn set_ecn(&mut self, val: u8) {\n        *self.buf.write(ECN_OFFSET) = (self.buf.read(ECN_OFFSET) & 0xfc) | (val & 0x3);\n    }\n\n    pub fn set_tos(&mut self, val: u8) {\n        self.set_dscp((val & 0xfc) >> 2);\n        self.set_ecn(val & 0x3);\n    }\n\n    pub fn set_total_length(&mut self, val: u16) {\n        self.buf.set_bytes(TOTAL_LENGTH_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_identification(&mut self, val: u16) {\n        self.buf.set_bytes(IDENTIFICATION_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_flags_and_fragment_offset(&mut self, val: u16) {\n        self.buf\n            .set_bytes(FLAGS_AND_FRAGMENT_OFFSET_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_ttl(&mut self, val: u8) {\n        *self.buf.write(TIME_TO_LIVE_OFFSET) = val;\n    }\n\n    pub fn set_protocol(&mut self, val: IpProtocol) {\n        *self.buf.write(PROTOCOL_OFFSET) = val.id();\n    }\n\n    pub fn set_checksum(&mut self, val: u16) {\n        self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_source(&mut self, val: Ipv4Addr) {\n        self.buf.set_bytes(SOURCE_OFFSET, val.octets());\n    }\n\n    pub fn set_destination(&mut self, val: Ipv4Addr) {\n        self.buf.set_bytes(DESTINATION_OFFSET, val.octets());\n    }\n\n    pub fn get_options_raw_mut(&mut self) -> &mut [u8] {\n        use std::cmp::min;\n        let current_offset = Self::minimum_packet_size();\n        let end = min(\n            current_offset + ipv4_options_length(self),\n            self.buf.as_slice().len(),\n        );\n        &mut self.buf.as_slice_mut()[current_offset..end]\n    }\n\n    pub fn set_payload(&mut self, vals: &[u8]) {\n        let current_offset = Self::minimum_packet_size() + ipv4_options_length(self);\n        self.buf.as_slice_mut()[current_offset..current_offset + vals.len()].copy_from_slice(vals);\n    }\n\n    #[must_use]\n    pub fn packet(&self) -> &[u8] {\n        self.buf.as_slice()\n    }\n\n    #[must_use]\n    pub fn payload(&self) -> &[u8] {\n        let start = Ipv4Packet::minimum_packet_size() + ipv4_options_length(self);\n        &self.buf.as_slice()[start..]\n    }\n}\n\nfn ipv4_options_length(ipv4: &Ipv4Packet<'_>) -> usize {\n    (ipv4.get_header_length() as usize * 4).saturating_sub(Ipv4Packet::minimum_packet_size())\n}\n\nimpl Debug for Ipv4Packet<'_> {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"Ipv4Packet\")\n            .field(\"version\", &self.get_version())\n            .field(\"header_length\", &self.get_header_length())\n            .field(\"dscp\", &self.get_dscp())\n            .field(\"ecn\", &self.get_ecn())\n            .field(\"total_length\", &self.get_total_length())\n            .field(\"identification\", &self.get_identification())\n            .field(\n                \"flags_and_fragment_offset\",\n                &self.get_flags_and_fragment_offset(),\n            )\n            .field(\"ttl\", &self.get_ttl())\n            .field(\"protocol\", &self.get_protocol())\n            .field(\"checksum\", &self.get_checksum())\n            .field(\"source\", &self.get_source())\n            .field(\"destination\", &self.get_destination())\n            .field(\"options_raw\", &self.get_options_raw())\n            .field(\"payload\", &fmt_payload(self.payload()))\n            .finish()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_version() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_version(4);\n        assert_eq!(4, packet.get_version());\n        assert_eq!([0x40], packet.packet()[..1]);\n        packet.set_version(15);\n        assert_eq!(15, packet.get_version());\n        assert_eq!([0xF0], packet.packet()[..1]);\n    }\n\n    #[test]\n    fn test_header_length() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_header_length(5);\n        assert_eq!(5, packet.get_header_length());\n        assert_eq!([0x05], packet.packet()[..1]);\n        packet.set_header_length(15);\n        assert_eq!(15, packet.get_header_length());\n        assert_eq!([0x0F], packet.packet()[..1]);\n    }\n\n    #[test]\n    fn test_version_and_header_length() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_version(4);\n        packet.set_header_length(5);\n        assert_eq!(4, packet.get_version());\n        assert_eq!(5, packet.get_header_length());\n        assert_eq!([0x45], packet.packet()[..1]);\n        packet.set_version(15);\n        packet.set_header_length(15);\n        assert_eq!(15, packet.get_version());\n        assert_eq!(15, packet.get_header_length());\n        assert_eq!([0xFF], packet.packet()[..1]);\n    }\n\n    #[test]\n    fn test_dscp() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_dscp(63);\n        assert_eq!(63, packet.get_dscp());\n        assert_eq!([0xFC], packet.packet()[1..2]);\n    }\n\n    #[test]\n    fn test_ecn() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_ecn(3);\n        assert_eq!(3, packet.get_ecn());\n        assert_eq!([0x03], packet.packet()[1..2]);\n    }\n\n    #[test]\n    fn test_dscp_and_ecn() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_dscp(63);\n        packet.set_ecn(3);\n        assert_eq!(63, packet.get_dscp());\n        assert_eq!(3, packet.get_ecn());\n        assert_eq!([0xFF], packet.packet()[1..2]);\n    }\n\n    #[test]\n    fn test_tos() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_tos(224);\n        assert_eq!(224, packet.get_tos());\n        assert_eq!(56, packet.get_dscp());\n        assert_eq!(0, packet.get_ecn());\n        assert_eq!([0xE0], packet.packet()[1..2]);\n        packet.set_tos(255);\n        assert_eq!(255, packet.get_tos());\n        assert_eq!(63, packet.get_dscp());\n        assert_eq!(3, packet.get_ecn());\n        assert_eq!([0xFF], packet.packet()[1..2]);\n    }\n\n    #[test]\n    fn test_total_length() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_total_length(84);\n        assert_eq!(84, packet.get_total_length());\n        assert_eq!([0x00, 0x54], packet.packet()[2..=3]);\n        packet.set_total_length(65535);\n        assert_eq!(65535, packet.get_total_length());\n        assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n    }\n\n    #[test]\n    fn test_identification() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_identification(32);\n        assert_eq!(32, packet.get_identification());\n        assert_eq!([0x00, 0x20], packet.packet()[4..=5]);\n        packet.set_identification(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_identification());\n        assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]);\n    }\n\n    #[test]\n    fn test_flags() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_flags_and_fragment_offset(0);\n        assert_eq!(0, packet.get_flags_and_fragment_offset());\n        assert_eq!([0x00, 0x00], packet.packet()[6..=7]);\n        // The Don't Fragment (DF) bit set:\n        packet.set_flags_and_fragment_offset(0x4000);\n        assert_eq!(0x4000, packet.get_flags_and_fragment_offset());\n        assert_eq!([0x40, 0x00], packet.packet()[6..=7]);\n    }\n\n    #[test]\n    fn test_time_to_live() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_ttl(16);\n        assert_eq!(16, packet.get_ttl());\n        assert_eq!([0x10], packet.packet()[8..9]);\n        packet.set_ttl(u8::MAX);\n        assert_eq!(u8::MAX, packet.get_ttl());\n        assert_eq!([0xFF], packet.packet()[8..9]);\n    }\n\n    #[test]\n    fn test_protocol() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_protocol(IpProtocol::Icmp);\n        assert_eq!(IpProtocol::Icmp, packet.get_protocol());\n        assert_eq!([0x01], packet.packet()[9..10]);\n        packet.set_protocol(IpProtocol::IcmpV6);\n        assert_eq!(IpProtocol::IcmpV6, packet.get_protocol());\n        assert_eq!([0x3A], packet.packet()[9..10]);\n        packet.set_protocol(IpProtocol::Udp);\n        assert_eq!(IpProtocol::Udp, packet.get_protocol());\n        assert_eq!([0x11], packet.packet()[9..10]);\n        packet.set_protocol(IpProtocol::Tcp);\n        assert_eq!(IpProtocol::Tcp, packet.get_protocol());\n        assert_eq!([0x06], packet.packet()[9..10]);\n        packet.set_protocol(IpProtocol::Other(123));\n        assert_eq!(IpProtocol::Other(123), packet.get_protocol());\n        assert_eq!([0x7B], packet.packet()[9..10]);\n        packet.set_protocol(IpProtocol::Other(255));\n        assert_eq!(IpProtocol::Other(255), packet.get_protocol());\n        assert_eq!([0xFF], packet.packet()[9..10]);\n    }\n\n    #[test]\n    fn test_header_checksum() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_checksum(0);\n        assert_eq!(0, packet.get_checksum());\n        assert_eq!([0x00, 0x00], packet.packet()[10..=11]);\n        packet.set_checksum(12345);\n        assert_eq!(12345, packet.get_checksum());\n        assert_eq!([0x30, 0x39], packet.packet()[10..=11]);\n        packet.set_checksum(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_checksum());\n        assert_eq!([0x0FF, 0xFF], packet.packet()[10..=11]);\n    }\n\n    #[test]\n    fn test_source() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_source(Ipv4Addr::LOCALHOST);\n        assert_eq!(Ipv4Addr::LOCALHOST, packet.get_source());\n        assert_eq!([0x07F, 0x00, 0x00, 0x01], packet.packet()[12..=15]);\n        packet.set_source(Ipv4Addr::UNSPECIFIED);\n        assert_eq!(Ipv4Addr::UNSPECIFIED, packet.get_source());\n        assert_eq!([0x00, 0x00, 0x00, 0x00], packet.packet()[12..=15]);\n        packet.set_source(Ipv4Addr::BROADCAST);\n        assert_eq!(Ipv4Addr::BROADCAST, packet.get_source());\n        assert_eq!([0xFF, 0xFF, 0xFF, 0xFF], packet.packet()[12..=15]);\n        packet.set_source(Ipv4Addr::new(0xDE, 0x9A, 0x56, 0x12));\n        assert_eq!(Ipv4Addr::new(0xDE, 0x9A, 0x56, 0x12), packet.get_source());\n        assert_eq!([0xDE, 0x9A, 0x56, 0x12], packet.packet()[12..=15]);\n    }\n\n    #[test]\n    fn test_destination() {\n        let mut buf = [0_u8; Ipv4Packet::minimum_packet_size()];\n        let mut packet = Ipv4Packet::new(&mut buf).unwrap();\n        packet.set_destination(Ipv4Addr::LOCALHOST);\n        assert_eq!(Ipv4Addr::LOCALHOST, packet.get_destination());\n        assert_eq!([0x07F, 0x00, 0x00, 0x01], packet.packet()[16..=19]);\n        packet.set_destination(Ipv4Addr::UNSPECIFIED);\n        assert_eq!(Ipv4Addr::UNSPECIFIED, packet.get_destination());\n        assert_eq!([0x00, 0x00, 0x00, 0x00], packet.packet()[16..=19]);\n        packet.set_destination(Ipv4Addr::BROADCAST);\n        assert_eq!(Ipv4Addr::BROADCAST, packet.get_destination());\n        assert_eq!([0xFF, 0xFF, 0xFF, 0xFF], packet.packet()[16..=19]);\n        packet.set_destination(Ipv4Addr::new(0xDE, 0x9A, 0x56, 0x12));\n        assert_eq!(\n            Ipv4Addr::new(0xDE, 0x9A, 0x56, 0x12),\n            packet.get_destination()\n        );\n        assert_eq!([0xDE, 0x9A, 0x56, 0x12], packet.packet()[16..=19]);\n    }\n\n    #[test]\n    fn test_view() {\n        let buf = [\n            0x45, 0x00, 0x00, 0x54, 0xa2, 0x71, 0x00, 0x00, 0x15, 0x11, 0x9a, 0xee, 0x7f, 0x00,\n            0x00, 0x01, 0xde, 0x9a, 0x56, 0x12,\n        ];\n        let packet = Ipv4Packet::new_view(&buf).unwrap();\n        assert_eq!(4, packet.get_version());\n        assert_eq!(5, packet.get_header_length());\n        assert_eq!(0, packet.get_dscp());\n        assert_eq!(0, packet.get_ecn());\n        assert_eq!(84, packet.get_total_length());\n        assert_eq!(41585, packet.get_identification());\n        assert_eq!(0, packet.get_flags_and_fragment_offset());\n        assert_eq!(21, packet.get_ttl());\n        assert_eq!(IpProtocol::Udp, packet.get_protocol());\n        assert_eq!(39662, packet.get_checksum());\n        assert_eq!(Ipv4Addr::LOCALHOST, packet.get_source());\n        assert_eq!(\n            Ipv4Addr::new(0xde, 0x9a, 0x56, 0x12),\n            packet.get_destination()\n        );\n        assert!(packet.payload().is_empty());\n    }\n\n    #[test]\n    fn test_new_insufficient_buffer() {\n        const SIZE: usize = Ipv4Packet::minimum_packet_size();\n        let mut buf = [0_u8; SIZE - 1];\n        let err = Ipv4Packet::new(&mut buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"Ipv4Packet\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n\n    #[test]\n    fn test_new_view_insufficient_buffer() {\n        const SIZE: usize = Ipv4Packet::minimum_packet_size();\n        let buf = [0_u8; SIZE - 1];\n        let err = Ipv4Packet::new_view(&buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"Ipv4Packet\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/ipv6.rs",
    "content": "use crate::buffer::Buffer;\nuse crate::error::{Error, Result};\nuse crate::{IpProtocol, fmt_payload};\nuse std::fmt::{Debug, Formatter};\nuse std::net::Ipv6Addr;\n\nconst VERSION_OFFSET: usize = 0;\nconst TRAFFIC_CLASS_OFFSET: usize = 0;\nconst FLOW_LABEL_OFFSET: usize = 1;\nconst PAYLOAD_LENGTH_OFFSET: usize = 4;\nconst NEXT_HEADER_OFFSET: usize = 6;\nconst HOP_LIMIT_OFFSET: usize = 7;\nconst SOURCE_ADDRESS_OFFSET: usize = 8;\nconst DESTINATION_ADDRESS_OFFSET: usize = 24;\n\n/// Represents an IPv6 Packet.\n///\n/// The internal representation is held in network byte order (big-endian) and all accessor methods\n/// take and return data in host byte order, converting as necessary for the given architecture.\npub struct Ipv6Packet<'a> {\n    buf: Buffer<'a>,\n}\n\nimpl<'a> Ipv6Packet<'a> {\n    pub fn new(packet: &'a mut [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Mutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"Ipv6Packet\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    pub fn new_view(packet: &'a [u8]) -> Result<Self> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(Self {\n                buf: Buffer::Immutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"Ipv6Packet\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    #[must_use]\n    pub const fn minimum_packet_size() -> usize {\n        40\n    }\n\n    #[must_use]\n    pub fn get_version(&self) -> u8 {\n        (self.buf.read(VERSION_OFFSET) & 0xf0) >> 4\n    }\n\n    #[must_use]\n    pub fn get_traffic_class(&self) -> u8 {\n        let b0 = ((self.buf.read(TRAFFIC_CLASS_OFFSET)) & 0xf) << 4;\n        let b1 = ((self.buf.read(TRAFFIC_CLASS_OFFSET + 1)) & 0xf0) >> 4;\n        b0 | b1\n    }\n\n    #[must_use]\n    pub fn get_flow_label(&self) -> u32 {\n        let b1 = (self.buf.read(FLOW_LABEL_OFFSET)) & 0xf;\n        let b2 = self.buf.read(FLOW_LABEL_OFFSET + 1);\n        let b3 = self.buf.read(FLOW_LABEL_OFFSET + 2);\n        u32::from_be_bytes([0, b1, b2, b3])\n    }\n\n    #[must_use]\n    pub fn get_payload_length(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(PAYLOAD_LENGTH_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_next_header(&self) -> IpProtocol {\n        IpProtocol::from(self.buf.read(NEXT_HEADER_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_hop_limit(&self) -> u8 {\n        self.buf.read(HOP_LIMIT_OFFSET)\n    }\n\n    #[must_use]\n    pub fn get_source_address(&self) -> Ipv6Addr {\n        Ipv6Addr::from(self.buf.get_bytes(SOURCE_ADDRESS_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_destination_address(&self) -> Ipv6Addr {\n        Ipv6Addr::from(self.buf.get_bytes(DESTINATION_ADDRESS_OFFSET))\n    }\n\n    pub fn set_version(&mut self, val: u8) {\n        *self.buf.write(VERSION_OFFSET) =\n            (self.buf.read(VERSION_OFFSET) & 0xf) | ((val & 0xf) << 4);\n    }\n\n    pub fn set_traffic_class(&mut self, val: u8) {\n        *self.buf.write(TRAFFIC_CLASS_OFFSET) =\n            (self.buf.read(TRAFFIC_CLASS_OFFSET) & 0xf0) | ((val & 0xf0) >> 4);\n        *self.buf.write(TRAFFIC_CLASS_OFFSET + 1) =\n            (self.buf.read(TRAFFIC_CLASS_OFFSET + 1) & 0xf) | ((val & 0xf) << 4);\n    }\n\n    pub fn set_flow_label(&mut self, val: u32) {\n        let bytes = val.to_be_bytes();\n        *self.buf.write(FLOW_LABEL_OFFSET) = (self.buf.read(FLOW_LABEL_OFFSET) & 0xf0) | bytes[1];\n        *self.buf.write(FLOW_LABEL_OFFSET + 1) = bytes[2];\n        *self.buf.write(FLOW_LABEL_OFFSET + 2) = bytes[3];\n    }\n\n    pub fn set_payload_length(&mut self, val: u16) {\n        self.buf.set_bytes(PAYLOAD_LENGTH_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_next_header(&mut self, val: IpProtocol) {\n        *self.buf.write(NEXT_HEADER_OFFSET) = val.id();\n    }\n\n    pub fn set_hop_limit(&mut self, val: u8) {\n        *self.buf.write(HOP_LIMIT_OFFSET) = val;\n    }\n\n    pub fn set_source_address(&mut self, val: Ipv6Addr) {\n        self.buf.set_bytes(SOURCE_ADDRESS_OFFSET, val.octets());\n    }\n\n    pub fn set_destination_address(&mut self, val: Ipv6Addr) {\n        self.buf.set_bytes(DESTINATION_ADDRESS_OFFSET, val.octets());\n    }\n\n    pub fn set_payload(&mut self, vals: &[u8]) {\n        let current_offset = Self::minimum_packet_size();\n        debug_assert!(\n            vals.len() <= self.get_payload_length() as usize,\n            \"vals.len() <= len\"\n        );\n        self.buf.as_slice_mut()[current_offset..current_offset + vals.len()].copy_from_slice(vals);\n    }\n\n    #[must_use]\n    pub fn packet(&self) -> &[u8] {\n        self.buf.as_slice()\n    }\n\n    #[must_use]\n    pub fn payload(&self) -> &[u8] {\n        let start = Self::minimum_packet_size();\n        let end = std::cmp::min(\n            Self::minimum_packet_size() + self.get_payload_length() as usize,\n            self.buf.as_slice().len(),\n        );\n        if self.buf.as_slice().len() <= start {\n            return &[];\n        }\n        &self.buf.as_slice()[start..end]\n    }\n}\n\nimpl Debug for Ipv6Packet<'_> {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"Ipv6Packet\")\n            .field(\"version\", &self.get_version())\n            .field(\"traffic_class\", &self.get_traffic_class())\n            .field(\"flow_label\", &self.get_flow_label())\n            .field(\"payload_length\", &self.get_payload_length())\n            .field(\"next_header\", &self.get_next_header())\n            .field(\"hop_limit\", &self.get_hop_limit())\n            .field(\"source_address\", &self.get_source_address())\n            .field(\"destination_address\", &self.get_destination_address())\n            .field(\"payload\", &fmt_payload(self.payload()))\n            .finish()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::str::FromStr;\n\n    #[test]\n    fn test_version() {\n        let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()];\n        let mut packet = Ipv6Packet::new(&mut buf).unwrap();\n        packet.set_version(5);\n        assert_eq!(5, packet.get_version());\n        assert_eq!([0x50], packet.packet()[..1]);\n        packet.set_version(15);\n        assert_eq!(15, packet.get_version());\n        assert_eq!([0xF0], packet.packet()[..1]);\n    }\n\n    #[test]\n    fn test_traffic_class() {\n        let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()];\n        let mut packet = Ipv6Packet::new(&mut buf).unwrap();\n        packet.set_traffic_class(0);\n        assert_eq!(0, packet.get_traffic_class());\n        assert_eq!([0x00, 0x00], packet.packet()[..2]);\n        packet.set_traffic_class(63);\n        assert_eq!(63, packet.get_traffic_class());\n        assert_eq!([0x03, 0xF0], packet.packet()[..2]);\n    }\n\n    #[test]\n    fn test_version_and_traffic_class() {\n        let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()];\n        let mut packet = Ipv6Packet::new(&mut buf).unwrap();\n        packet.set_version(15);\n        packet.set_traffic_class(63);\n        assert_eq!(15, packet.get_version());\n        assert_eq!(63, packet.get_traffic_class());\n        assert_eq!([0xF3, 0xF0], packet.packet()[..2]);\n    }\n\n    #[test]\n    fn test_flow_label() {\n        let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()];\n        let mut packet = Ipv6Packet::new(&mut buf).unwrap();\n        packet.set_flow_label(0);\n        assert_eq!(0, packet.get_flow_label());\n        assert_eq!([0x00, 0x00, 0x00], packet.packet()[1..=3]);\n        packet.set_flow_label(500_000);\n        assert_eq!(500_000, packet.get_flow_label());\n        assert_eq!([0x07, 0xA1, 0x20], packet.packet()[1..=3]);\n        packet.set_flow_label(1_048_575);\n        assert_eq!(1_048_575, packet.get_flow_label());\n        assert_eq!([0x0F, 0xFF, 0xFF], packet.packet()[1..=3]);\n    }\n\n    #[test]\n    fn test_payload_length() {\n        let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()];\n        let mut packet = Ipv6Packet::new(&mut buf).unwrap();\n        packet.set_payload_length(0);\n        assert_eq!(0, packet.get_payload_length());\n        assert_eq!([0x00, 0x00], packet.packet()[4..=5]);\n        packet.set_payload_length(120);\n        assert_eq!(120, packet.get_payload_length());\n        assert_eq!([0x00, 0x78], packet.packet()[4..=5]);\n        packet.set_payload_length(65535);\n        assert_eq!(65535, packet.get_payload_length());\n        assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]);\n    }\n\n    #[test]\n    fn test_next_header() {\n        let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()];\n        let mut packet = Ipv6Packet::new(&mut buf).unwrap();\n        packet.set_next_header(IpProtocol::Icmp);\n        assert_eq!(IpProtocol::Icmp, packet.get_next_header());\n        assert_eq!([0x01], packet.packet()[6..7]);\n        packet.set_next_header(IpProtocol::IcmpV6);\n        assert_eq!(IpProtocol::IcmpV6, packet.get_next_header());\n        assert_eq!([0x3A], packet.packet()[6..7]);\n        packet.set_next_header(IpProtocol::Udp);\n        assert_eq!(IpProtocol::Udp, packet.get_next_header());\n        assert_eq!([0x11], packet.packet()[6..7]);\n        packet.set_next_header(IpProtocol::Tcp);\n        assert_eq!(IpProtocol::Tcp, packet.get_next_header());\n        assert_eq!([0x06], packet.packet()[6..7]);\n        packet.set_next_header(IpProtocol::Other(123));\n        assert_eq!(IpProtocol::Other(123), packet.get_next_header());\n        assert_eq!([0x7B], packet.packet()[6..7]);\n        packet.set_next_header(IpProtocol::Other(255));\n        assert_eq!(IpProtocol::Other(255), packet.get_next_header());\n        assert_eq!([0xFF], packet.packet()[6..7]);\n    }\n\n    #[test]\n    fn test_hop_limit() {\n        let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()];\n        let mut packet = Ipv6Packet::new(&mut buf).unwrap();\n        packet.set_hop_limit(0);\n        assert_eq!(0, packet.get_hop_limit());\n        assert_eq!([0x00], packet.packet()[7..8]);\n        packet.set_hop_limit(120);\n        assert_eq!(120, packet.get_hop_limit());\n        assert_eq!([0x78], packet.packet()[7..8]);\n        packet.set_hop_limit(255);\n        assert_eq!(255, packet.get_hop_limit());\n        assert_eq!([0xFF], packet.packet()[7..8]);\n    }\n\n    #[test]\n    fn test_source_address() {\n        let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()];\n        let mut packet = Ipv6Packet::new(&mut buf).unwrap();\n        packet.set_source_address(Ipv6Addr::LOCALHOST);\n        assert_eq!(Ipv6Addr::LOCALHOST, packet.get_source_address());\n        assert_eq!(\n            [\n                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                0x00, 0x01\n            ],\n            packet.packet()[8..=23]\n        );\n        packet.set_source_address(Ipv6Addr::from_str(\"2404:6800:4005:812::200e\").unwrap());\n        assert_eq!(\n            Ipv6Addr::from_str(\"2404:6800:4005:812::200e\").unwrap(),\n            packet.get_source_address()\n        );\n        assert_eq!(\n            [\n                0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                0x20, 0x0E\n            ],\n            packet.packet()[8..=23]\n        );\n    }\n\n    #[test]\n    fn test_destination_address() {\n        let mut buf = [0_u8; Ipv6Packet::minimum_packet_size()];\n        let mut packet = Ipv6Packet::new(&mut buf).unwrap();\n        packet.set_destination_address(Ipv6Addr::LOCALHOST);\n        assert_eq!(Ipv6Addr::LOCALHOST, packet.get_destination_address());\n        assert_eq!(\n            [\n                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                0x00, 0x01\n            ],\n            packet.packet()[24..=39]\n        );\n        packet.set_destination_address(Ipv6Addr::from_str(\"2404:6800:4005:812::200e\").unwrap());\n        assert_eq!(\n            Ipv6Addr::from_str(\"2404:6800:4005:812::200e\").unwrap(),\n            packet.get_destination_address()\n        );\n        assert_eq!(\n            [\n                0x24, 0x04, 0x68, 0x00, 0x40, 0x05, 0x08, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n                0x20, 0x0E\n            ],\n            packet.packet()[24..=39]\n        );\n    }\n\n    #[test]\n    fn test_view() {\n        let buf = [\n            0x60, 0x06, 0x05, 0x00, 0x00, 0x20, 0x06, 0x40, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x1c, 0x8d, 0x7d, 0x69, 0xd0, 0xb6, 0x81, 0x82, 0xfe, 0x80, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x08, 0x11, 0x03, 0xf6, 0x76, 0x01, 0x6c, 0x3f,\n        ];\n        let packet = Ipv6Packet::new_view(&buf).unwrap();\n        assert_eq!(6, packet.get_version());\n        assert_eq!(0, packet.get_traffic_class());\n        assert_eq!(394_496, packet.get_flow_label());\n        assert_eq!(32, packet.get_payload_length());\n        assert_eq!(IpProtocol::Tcp, packet.get_next_header());\n        assert_eq!(64, packet.get_hop_limit());\n        assert_eq!(\n            Ipv6Addr::from_str(\"fe80::1c8d:7d69:d0b6:8182\").unwrap(),\n            packet.get_source_address()\n        );\n        assert_eq!(\n            Ipv6Addr::from_str(\"fe80::811:3f6:7601:6c3f\").unwrap(),\n            packet.get_destination_address()\n        );\n        assert!(packet.payload().is_empty());\n    }\n\n    #[test]\n    fn test_new_insufficient_buffer() {\n        const SIZE: usize = Ipv6Packet::minimum_packet_size();\n        let mut buf = [0_u8; SIZE - 1];\n        let err = Ipv6Packet::new(&mut buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"Ipv6Packet\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n\n    #[test]\n    fn test_new_view_insufficient_buffer() {\n        const SIZE: usize = Ipv6Packet::minimum_packet_size();\n        let buf = [0_u8; SIZE - 1];\n        let err = Ipv6Packet::new_view(&buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"Ipv6Packet\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/lib.rs",
    "content": "//! Packet wire format parsing and building.\n//!\n//! The following packet are supported:\n//! - `IP`\n//! - `ICMPv4`\n//! - `ICMPv6`\n//! - `IPv4`\n//! - `IPv6`\n//! - `UDP`\n//! - `TCP`\n//! - `ICMP` extensions\n//!\n//! # Endianness\n//!\n//! The internal representation is held in network byte order (big-endian) and\n//! all accessor methods take and return data in host byte order, converting as\n//! necessary for the given architecture.\n//!\n//! # Example\n//!\n//! The following example parses an `UDP` packet and asserts its fields:\n//!\n//! ```rust\n//! # fn main() -> anyhow::Result<()> {\n//! use trippy_packet::udp::UdpPacket;\n//!\n//! let buf = hex_literal::hex!(\"68 bf 81 b6 00 40 ac be\");\n//! let packet = UdpPacket::new_view(&buf)?;\n//! assert_eq!(26815, packet.get_source());\n//! assert_eq!(33206, packet.get_destination());\n//! assert_eq!(64, packet.get_length());\n//! assert_eq!(44222, packet.get_checksum());\n//! assert!(packet.payload().is_empty());\n//! # Ok(())\n//! # }\n//! ```\n//!\n//! The following example builds an `ICMPv4` echo request packet:\n//!\n//! ```rust\n//! # fn main() -> anyhow::Result<()> {\n//! use trippy_packet::checksum::icmp_ipv4_checksum;\n//! use trippy_packet::icmpv4::echo_request::EchoRequestPacket;\n//! use trippy_packet::icmpv4::{IcmpCode, IcmpPacket, IcmpType};\n//!\n//! let mut buf = [0; IcmpPacket::minimum_packet_size()];\n//! let mut icmp = EchoRequestPacket::new(&mut buf)?;\n//! icmp.set_icmp_type(IcmpType::EchoRequest);\n//! icmp.set_icmp_code(IcmpCode(0));\n//! icmp.set_identifier(1234);\n//! icmp.set_sequence(10);\n//! icmp.set_checksum(icmp_ipv4_checksum(icmp.packet()));\n//! assert_eq!(icmp.packet(), &hex_literal::hex!(\"08 00 f3 23 04 d2 00 0a\"));\n//! # Ok(())\n//! # }\n//! ```\n#![forbid(unsafe_code)]\n\nmod buffer;\n\n/// Packet errors.\npub mod error;\n\n/// Functions for calculating network checksums.\npub mod checksum;\n\n/// `ICMPv4` packets.\npub mod icmpv4;\n\n/// `ICMPv6` packets.\npub mod icmpv6;\n\n/// `ICMP` extensions.\npub mod icmp_extension;\n\n/// `IP` packets.\npub mod ip;\n\n/// `IPv4` packets.\npub mod ipv4;\n\n/// `IPv6` packets.\npub mod ipv6;\n\n/// `UDP` packets.\npub mod udp;\n\n/// `TCP` packets.\npub mod tcp;\n\n/// The IP packet next layer protocol.\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub enum IpProtocol {\n    Icmp,\n    IcmpV6,\n    Udp,\n    Tcp,\n    Other(u8),\n}\n\nimpl IpProtocol {\n    #[must_use]\n    pub const fn id(self) -> u8 {\n        match self {\n            Self::Icmp => 1,\n            Self::IcmpV6 => 58,\n            Self::Udp => 17,\n            Self::Tcp => 6,\n            Self::Other(id) => id,\n        }\n    }\n\n    #[must_use]\n    pub const fn new(value: u8) -> Self {\n        Self::Other(value)\n    }\n}\n\nimpl From<u8> for IpProtocol {\n    fn from(id: u8) -> Self {\n        match id {\n            1 => Self::Icmp,\n            58 => Self::IcmpV6,\n            17 => Self::Udp,\n            6 => Self::Tcp,\n            p => Self::Other(p),\n        }\n    }\n}\n\n/// Format a payload as a hexadecimal string.\n#[must_use]\npub fn fmt_payload(bytes: &[u8]) -> String {\n    use itertools::Itertools as _;\n    format!(\"{:02x}\", bytes.iter().format(\" \"))\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/tcp.rs",
    "content": "use crate::buffer::Buffer;\nuse crate::error::{Error, Result};\nuse crate::fmt_payload;\nuse std::fmt::{Debug, Formatter};\n\nconst SOURCE_PORT_OFFSET: usize = 0;\nconst DESTINATION_PORT_OFFSET: usize = 2;\nconst SEQUENCE_OFFSET: usize = 4;\nconst ACKNOWLEDGEMENT_OFFSET: usize = 8;\nconst DATA_OFFSET_OFFSET: usize = 12;\nconst RESERVED_OFFSET: usize = 12;\nconst FLAGS_OFFSET: usize = 12;\nconst WINDOW_SIZE_OFFSET: usize = 14;\nconst CHECKSUM_OFFSET: usize = 16;\nconst URGENT_POINTER_OFFSET: usize = 18;\n\n/// Represents an TCP Packet.\n///\n/// The internal representation is held in network byte order (big-endian) and all accessor methods\n/// take and return data in host byte order, converting as necessary for the given architecture.\npub struct TcpPacket<'a> {\n    buf: Buffer<'a>,\n}\n\nimpl TcpPacket<'_> {\n    pub fn new(packet: &mut [u8]) -> Result<TcpPacket<'_>> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(TcpPacket {\n                buf: Buffer::Mutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"TcpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    pub fn new_view(packet: &[u8]) -> Result<TcpPacket<'_>> {\n        if packet.len() >= Self::minimum_packet_size() {\n            Ok(TcpPacket {\n                buf: Buffer::Immutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"TcpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    #[must_use]\n    pub const fn minimum_packet_size() -> usize {\n        20\n    }\n\n    #[must_use]\n    pub fn get_source(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(SOURCE_PORT_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_destination(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(DESTINATION_PORT_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_sequence(&self) -> u32 {\n        u32::from_be_bytes(self.buf.get_bytes(SEQUENCE_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_acknowledgement(&self) -> u32 {\n        u32::from_be_bytes(self.buf.get_bytes(ACKNOWLEDGEMENT_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_data_offset(&self) -> u8 {\n        (self.buf.read(DATA_OFFSET_OFFSET) & 0xf0) >> 4\n    }\n\n    #[must_use]\n    pub fn get_reserved(&self) -> u8 {\n        (self.buf.read(RESERVED_OFFSET) & 0xe) >> 1\n    }\n\n    #[must_use]\n    pub fn get_flags(&self) -> u16 {\n        u16::from_be_bytes([\n            self.buf.read(FLAGS_OFFSET) & 0x1,\n            self.buf.read(FLAGS_OFFSET + 1),\n        ])\n    }\n\n    #[must_use]\n    pub fn get_window_size(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(WINDOW_SIZE_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_checksum(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_urgent_pointer(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(URGENT_POINTER_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_options_raw(&self) -> &[u8] {\n        let current_offset = Self::minimum_packet_size();\n        let end = std::cmp::min(\n            current_offset + self.tcp_options_length(),\n            self.buf.as_slice().len(),\n        );\n        &self.buf.as_slice()[current_offset..end]\n    }\n\n    pub fn set_source(&mut self, val: u16) {\n        self.buf.set_bytes(SOURCE_PORT_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_destination(&mut self, val: u16) {\n        self.buf\n            .set_bytes(DESTINATION_PORT_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_sequence(&mut self, val: u32) {\n        self.buf.set_bytes(SEQUENCE_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_acknowledgement(&mut self, val: u32) {\n        self.buf\n            .set_bytes(ACKNOWLEDGEMENT_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_data_offset(&mut self, val: u8) {\n        *self.buf.write(DATA_OFFSET_OFFSET) =\n            (self.buf.read(DATA_OFFSET_OFFSET) & 0xf) | ((val & 0xf) << 4);\n    }\n\n    pub fn set_reserved(&mut self, val: u8) {\n        *self.buf.write(RESERVED_OFFSET) =\n            (self.buf.read(RESERVED_OFFSET) & 0xf1) | ((val & 0x7) << 1);\n    }\n\n    pub fn set_flags(&mut self, val: u16) {\n        let bytes = val.to_be_bytes();\n        *self.buf.write(FLAGS_OFFSET) = (self.buf.read(FLAGS_OFFSET) & 0xfe) | (bytes[0] & 0x1);\n        *self.buf.write(FLAGS_OFFSET + 1) = bytes[1];\n    }\n\n    pub fn set_window_size(&mut self, val: u16) {\n        self.buf.set_bytes(WINDOW_SIZE_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_checksum(&mut self, val: u16) {\n        self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_urgent_pointer(&mut self, val: u16) {\n        self.buf.set_bytes(URGENT_POINTER_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_payload(&mut self, vals: &[u8]) {\n        let current_offset = Self::minimum_packet_size() + self.tcp_options_length();\n        self.buf.as_slice_mut()[current_offset..current_offset + vals.len()].copy_from_slice(vals);\n    }\n\n    #[must_use]\n    pub fn packet(&self) -> &[u8] {\n        self.buf.as_slice()\n    }\n\n    #[must_use]\n    pub fn payload(&self) -> &[u8] {\n        let start = Self::minimum_packet_size() + self.tcp_options_length();\n        if self.buf.as_slice().len() <= start {\n            return &[];\n        }\n        &self.buf.as_slice()[start..]\n    }\n\n    fn tcp_options_length(&self) -> usize {\n        let data_offset = self.get_data_offset();\n        if data_offset > 5 {\n            data_offset as usize * 4 - 20\n        } else {\n            0\n        }\n    }\n}\n\nimpl Debug for TcpPacket<'_> {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"TcpPacket\")\n            .field(\"source\", &self.get_source())\n            .field(\"destination\", &self.get_destination())\n            .field(\"sequence\", &self.get_sequence())\n            .field(\"acknowledgement\", &self.get_acknowledgement())\n            .field(\"data_offset\", &self.get_data_offset())\n            .field(\"reserved\", &self.get_reserved())\n            .field(\"flags\", &self.get_flags())\n            .field(\"window_size\", &self.get_window_size())\n            .field(\"checksum\", &self.get_checksum())\n            .field(\"urgent_pointer\", &self.get_urgent_pointer())\n            .field(\"options\", &self.get_options_raw())\n            .field(\"payload\", &fmt_payload(self.payload()))\n            .finish()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_source() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_source(0);\n        assert_eq!(0, packet.get_source());\n        assert_eq!([0x00, 0x00], packet.packet()[..=1]);\n        packet.set_source(80);\n        assert_eq!(80, packet.get_source());\n        assert_eq!([0x00, 0x50], packet.packet()[..=1]);\n        packet.set_source(443);\n        assert_eq!(443, packet.get_source());\n        assert_eq!([0x01, 0xBB], packet.packet()[..=1]);\n        packet.set_source(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_source());\n        assert_eq!([0xFF, 0xFF], packet.packet()[..=1]);\n    }\n\n    #[test]\n    fn test_destination() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_destination(0);\n        assert_eq!(0, packet.get_destination());\n        assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n        packet.set_destination(80);\n        assert_eq!(80, packet.get_destination());\n        assert_eq!([0x00, 0x50], packet.packet()[2..=3]);\n        packet.set_destination(443);\n        assert_eq!(443, packet.get_destination());\n        assert_eq!([0x01, 0xBB], packet.packet()[2..=3]);\n        packet.set_destination(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_destination());\n        assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n    }\n\n    #[test]\n    fn test_sequence() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_sequence(0);\n        assert_eq!(0, packet.get_sequence());\n        assert_eq!([0x00, 0x00, 0x00, 0x00], packet.packet()[4..=7]);\n        packet.set_sequence(123_456);\n        assert_eq!(123_456, packet.get_sequence());\n        assert_eq!([0x00, 0x01, 0xE2, 0x40], packet.packet()[4..=7]);\n        packet.set_sequence(u32::MAX);\n        assert_eq!(u32::MAX, packet.get_sequence());\n        assert_eq!([0xFF, 0xFF, 0xFF, 0xFF], packet.packet()[4..=7]);\n    }\n\n    #[test]\n    fn test_acknowledgement() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_acknowledgement(0);\n        assert_eq!(0, packet.get_acknowledgement());\n        assert_eq!([0x00, 0x00, 0x00, 0x00], packet.packet()[8..=11]);\n        packet.set_acknowledgement(123_456);\n        assert_eq!(123_456, packet.get_acknowledgement());\n        assert_eq!([0x00, 0x01, 0xE2, 0x40], packet.packet()[8..=11]);\n        packet.set_acknowledgement(u32::MAX);\n        assert_eq!(u32::MAX, packet.get_acknowledgement());\n        assert_eq!([0xFF, 0xFF, 0xFF, 0xFF], packet.packet()[8..=11]);\n    }\n\n    #[test]\n    fn test_data_offset() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_data_offset(0);\n        assert_eq!(0, packet.get_data_offset());\n        assert_eq!([0x00], packet.packet()[12..13]);\n        packet.set_data_offset(15);\n        assert_eq!(15, packet.get_data_offset());\n        assert_eq!([0xf0], packet.packet()[12..13]);\n    }\n\n    #[test]\n    fn test_reserved() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_reserved(0);\n        assert_eq!(0, packet.get_reserved());\n        assert_eq!([0x00], packet.packet()[12..13]);\n        packet.set_reserved(7);\n        assert_eq!(7, packet.get_reserved());\n        assert_eq!([0x0e], packet.packet()[12..13]);\n    }\n\n    #[test]\n    fn test_flags() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_flags(0);\n        assert_eq!(0, packet.get_flags());\n        assert_eq!([0x00, 0x00], packet.packet()[12..=13]);\n        packet.set_flags(511);\n        assert_eq!(511, packet.get_flags());\n        assert_eq!([0x01, 0xff], packet.packet()[12..=13]);\n    }\n\n    #[test]\n    fn test_data_offset_and_reserved() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_data_offset(0);\n        packet.set_reserved(0);\n        assert_eq!(0, packet.get_data_offset());\n        assert_eq!(0, packet.get_reserved());\n        assert_eq!([0x00], packet.packet()[12..13]);\n        packet.set_data_offset(15);\n        packet.set_reserved(7);\n        assert_eq!(15, packet.get_data_offset());\n        assert_eq!(7, packet.get_reserved());\n        assert_eq!([0xfe], packet.packet()[12..13]);\n    }\n\n    #[test]\n    fn test_reserved_and_flags() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_reserved(0);\n        packet.set_flags(0);\n        assert_eq!(0, packet.get_flags());\n        assert_eq!([0x00, 0x00], packet.packet()[12..=13]);\n        packet.set_reserved(7);\n        packet.set_flags(511);\n        assert_eq!(511, packet.get_flags());\n        assert_eq!([0x0f, 0xff], packet.packet()[12..=13]);\n    }\n\n    #[test]\n    fn test_data_offset_and_reserved_and_flags() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_data_offset(0);\n        packet.set_reserved(0);\n        packet.set_flags(0);\n        assert_eq!(0, packet.get_flags());\n        assert_eq!([0x00, 0x00], packet.packet()[12..=13]);\n        packet.set_data_offset(15);\n        packet.set_reserved(7);\n        packet.set_flags(511);\n        assert_eq!(511, packet.get_flags());\n        assert_eq!([0xff, 0xff], packet.packet()[12..=13]);\n    }\n\n    #[test]\n    fn test_window_size() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_window_size(0);\n        assert_eq!(0, packet.get_window_size());\n        assert_eq!([0x00, 0x00], packet.packet()[14..=15]);\n        packet.set_window_size(80);\n        assert_eq!(80, packet.get_window_size());\n        assert_eq!([0x00, 0x50], packet.packet()[14..=15]);\n        packet.set_window_size(443);\n        assert_eq!(443, packet.get_window_size());\n        assert_eq!([0x01, 0xBB], packet.packet()[14..=15]);\n        packet.set_window_size(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_window_size());\n        assert_eq!([0xFF, 0xFF], packet.packet()[14..=15]);\n    }\n\n    #[test]\n    fn test_checksum() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_checksum(0);\n        assert_eq!(0, packet.get_checksum());\n        assert_eq!([0x00, 0x00], packet.packet()[16..=17]);\n        packet.set_checksum(80);\n        assert_eq!(80, packet.get_checksum());\n        assert_eq!([0x00, 0x50], packet.packet()[16..=17]);\n        packet.set_checksum(443);\n        assert_eq!(443, packet.get_checksum());\n        assert_eq!([0x01, 0xBB], packet.packet()[16..=17]);\n        packet.set_checksum(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_checksum());\n        assert_eq!([0xFF, 0xFF], packet.packet()[16..=17]);\n    }\n\n    #[test]\n    fn test_urgent_pointer() {\n        let mut buf = [0_u8; TcpPacket::minimum_packet_size()];\n        let mut packet = TcpPacket::new(&mut buf).unwrap();\n        packet.set_urgent_pointer(0);\n        assert_eq!(0, packet.get_urgent_pointer());\n        assert_eq!([0x00, 0x00], packet.packet()[18..=19]);\n        packet.set_urgent_pointer(80);\n        assert_eq!(80, packet.get_urgent_pointer());\n        assert_eq!([0x00, 0x50], packet.packet()[18..=19]);\n        packet.set_urgent_pointer(443);\n        assert_eq!(443, packet.get_urgent_pointer());\n        assert_eq!([0x01, 0xBB], packet.packet()[18..=19]);\n        packet.set_urgent_pointer(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_urgent_pointer());\n        assert_eq!([0xFF, 0xFF], packet.packet()[18..=19]);\n    }\n\n    #[test]\n    fn test_view() {\n        let buf = [\n            0x01, 0xbb, 0xe5, 0xd7, 0x60, 0xb0, 0x76, 0x50, 0x8e, 0x03, 0x46, 0xa2, 0x80, 0x10,\n            0x00, 0x80, 0x3e, 0xdc, 0x00, 0x00, 0x01, 0x01, 0x08, 0x0a, 0x10, 0x52, 0xf6, 0xd4,\n            0xea, 0x3a, 0x2a, 0x51,\n        ];\n        let packet = TcpPacket::new_view(&buf).unwrap();\n        assert_eq!(443, packet.get_source());\n        assert_eq!(58839, packet.get_destination());\n        assert_eq!(1_622_177_360, packet.get_sequence());\n        assert_eq!(2_382_579_362, packet.get_acknowledgement());\n        assert_eq!(8, packet.get_data_offset());\n        assert_eq!(0, packet.get_reserved());\n        assert_eq!(0x10, packet.get_flags());\n        assert_eq!(128, packet.get_window_size());\n        assert_eq!(0x3edc, packet.get_checksum());\n        assert_eq!(0, packet.get_urgent_pointer());\n        assert_eq!(12, packet.tcp_options_length());\n        assert_eq!(\n            &[\n                0x01, 0x01, 0x08, 0x0a, 0x10, 0x52, 0xf6, 0xd4, 0xea, 0x3a, 0x2a, 0x51\n            ],\n            packet.get_options_raw()\n        );\n        assert!(packet.payload().is_empty());\n    }\n\n    #[test]\n    fn test_new_insufficient_buffer() {\n        const SIZE: usize = TcpPacket::minimum_packet_size();\n        let mut buf = [0_u8; SIZE - 1];\n        let err = TcpPacket::new(&mut buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"TcpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n\n    #[test]\n    fn test_new_view_insufficient_buffer() {\n        const SIZE: usize = TcpPacket::minimum_packet_size();\n        let buf = [0_u8; SIZE - 1];\n        let err = TcpPacket::new_view(&buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"TcpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n}\n"
  },
  {
    "path": "crates/trippy-packet/src/udp.rs",
    "content": "use crate::buffer::Buffer;\nuse crate::error::{Error, Result};\nuse crate::fmt_payload;\nuse std::fmt::{Debug, Formatter};\n\nconst SOURCE_PORT_OFFSET: usize = 0;\nconst DESTINATION_PORT_OFFSET: usize = 2;\nconst LENGTH_OFFSET: usize = 4;\nconst CHECKSUM_OFFSET: usize = 6;\n\n/// Represents a UDP Packet.\n///\n/// The internal representation is held in network byte order (big-endian) and all accessor methods\n/// take and return data in host byte order, converting as necessary for the given architecture.\npub struct UdpPacket<'a> {\n    buf: Buffer<'a>,\n}\n\nimpl UdpPacket<'_> {\n    pub fn new(packet: &mut [u8]) -> Result<UdpPacket<'_>> {\n        if packet.len() >= UdpPacket::minimum_packet_size() {\n            Ok(UdpPacket {\n                buf: Buffer::Mutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"UdpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    pub fn new_view(packet: &[u8]) -> Result<UdpPacket<'_>> {\n        if packet.len() >= UdpPacket::minimum_packet_size() {\n            Ok(UdpPacket {\n                buf: Buffer::Immutable(packet),\n            })\n        } else {\n            Err(Error::InsufficientPacketBuffer(\n                String::from(\"UdpPacket\"),\n                Self::minimum_packet_size(),\n                packet.len(),\n            ))\n        }\n    }\n\n    #[must_use]\n    pub const fn minimum_packet_size() -> usize {\n        8\n    }\n\n    #[must_use]\n    pub fn get_source(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(SOURCE_PORT_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_destination(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(DESTINATION_PORT_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_length(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(LENGTH_OFFSET))\n    }\n\n    #[must_use]\n    pub fn get_checksum(&self) -> u16 {\n        u16::from_be_bytes(self.buf.get_bytes(CHECKSUM_OFFSET))\n    }\n\n    pub fn set_source(&mut self, val: u16) {\n        self.buf.set_bytes(SOURCE_PORT_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_destination(&mut self, val: u16) {\n        self.buf\n            .set_bytes(DESTINATION_PORT_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_length(&mut self, val: u16) {\n        self.buf.set_bytes(LENGTH_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_checksum(&mut self, val: u16) {\n        self.buf.set_bytes(CHECKSUM_OFFSET, val.to_be_bytes());\n    }\n\n    pub fn set_payload(&mut self, vals: &[u8]) {\n        let current_offset = Self::minimum_packet_size();\n        self.buf.as_slice_mut()[current_offset..current_offset + vals.len()].copy_from_slice(vals);\n    }\n\n    #[must_use]\n    pub fn packet(&self) -> &[u8] {\n        self.buf.as_slice()\n    }\n\n    #[must_use]\n    pub fn payload(&self) -> &[u8] {\n        &self.buf.as_slice()[Self::minimum_packet_size()..]\n    }\n}\n\nimpl Debug for UdpPacket<'_> {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"UdpPacket\")\n            .field(\"source\", &self.get_source())\n            .field(\"destination\", &self.get_destination())\n            .field(\"length\", &self.get_length())\n            .field(\"checksum\", &self.get_checksum())\n            .field(\"payload\", &fmt_payload(self.payload()))\n            .finish()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_source() {\n        let mut buf = [0_u8; UdpPacket::minimum_packet_size()];\n        let mut packet = UdpPacket::new(&mut buf).unwrap();\n        packet.set_source(0);\n        assert_eq!(0, packet.get_source());\n        assert_eq!([0x00, 0x00], packet.packet()[..=1]);\n        packet.set_source(80);\n        assert_eq!(80, packet.get_source());\n        assert_eq!([0x00, 0x50], packet.packet()[..=1]);\n        packet.set_source(443);\n        assert_eq!(443, packet.get_source());\n        assert_eq!([0x01, 0xBB], packet.packet()[..=1]);\n        packet.set_source(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_source());\n        assert_eq!([0xFF, 0xFF], packet.packet()[..=1]);\n    }\n\n    #[test]\n    fn test_destination() {\n        let mut buf = [0_u8; UdpPacket::minimum_packet_size()];\n        let mut packet = UdpPacket::new(&mut buf).unwrap();\n        packet.set_destination(0);\n        assert_eq!(0, packet.get_destination());\n        assert_eq!([0x00, 0x00], packet.packet()[2..=3]);\n        packet.set_destination(80);\n        assert_eq!(80, packet.get_destination());\n        assert_eq!([0x00, 0x50], packet.packet()[2..=3]);\n        packet.set_destination(443);\n        assert_eq!(443, packet.get_destination());\n        assert_eq!([0x01, 0xBB], packet.packet()[2..=3]);\n        packet.set_destination(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_destination());\n        assert_eq!([0xFF, 0xFF], packet.packet()[2..=3]);\n    }\n\n    #[test]\n    fn test_length() {\n        let mut buf = [0_u8; UdpPacket::minimum_packet_size()];\n        let mut packet = UdpPacket::new(&mut buf).unwrap();\n        packet.set_length(0);\n        assert_eq!(0, packet.get_length());\n        assert_eq!([0x00, 0x00], packet.packet()[4..=5]);\n        packet.set_length(202);\n        assert_eq!(202, packet.get_length());\n        assert_eq!([0x00, 0xCA], packet.packet()[4..=5]);\n        packet.set_length(1025);\n        assert_eq!(1025, packet.get_length());\n        assert_eq!([0x04, 0x01], packet.packet()[4..=5]);\n        packet.set_length(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_length());\n        assert_eq!([0xFF, 0xFF], packet.packet()[4..=5]);\n    }\n\n    #[test]\n    fn test_checksum() {\n        let mut buf = [0_u8; UdpPacket::minimum_packet_size()];\n        let mut packet = UdpPacket::new(&mut buf).unwrap();\n        packet.set_checksum(0);\n        assert_eq!(0, packet.get_checksum());\n        assert_eq!([0x00, 0x00], packet.packet()[6..=7]);\n        packet.set_checksum(202);\n        assert_eq!(202, packet.get_checksum());\n        assert_eq!([0x00, 0xCA], packet.packet()[6..=7]);\n        packet.set_checksum(1025);\n        assert_eq!(1025, packet.get_checksum());\n        assert_eq!([0x04, 0x01], packet.packet()[6..=7]);\n        packet.set_checksum(u16::MAX);\n        assert_eq!(u16::MAX, packet.get_checksum());\n        assert_eq!([0xFF, 0xFF], packet.packet()[6..=7]);\n    }\n\n    #[test]\n    fn test_view() {\n        let buf = [0x68, 0xbf, 0x81, 0xb6, 0x00, 0x40, 0xac, 0xbe];\n        let packet = UdpPacket::new_view(&buf).unwrap();\n        assert_eq!(26815, packet.get_source());\n        assert_eq!(33206, packet.get_destination());\n        assert_eq!(64, packet.get_length());\n        assert_eq!(44222, packet.get_checksum());\n        assert!(packet.payload().is_empty());\n    }\n\n    #[test]\n    fn test_new_insufficient_buffer() {\n        const SIZE: usize = UdpPacket::minimum_packet_size();\n        let mut buf = [0_u8; SIZE - 1];\n        let err = UdpPacket::new(&mut buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"UdpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n\n    #[test]\n    fn test_new_view_insufficient_buffer() {\n        const SIZE: usize = UdpPacket::minimum_packet_size();\n        let buf = [0_u8; SIZE - 1];\n        let err = UdpPacket::new_view(&buf).unwrap_err();\n        assert_eq!(\n            Error::InsufficientPacketBuffer(String::from(\"UdpPacket\"), SIZE, SIZE - 1),\n            err\n        );\n    }\n}\n"
  },
  {
    "path": "crates/trippy-privilege/Cargo.toml",
    "content": "[package]\nname = \"trippy-privilege\"\ndescription = \"Discover platform privileges\"\nversion.workspace = true\nauthors.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\nreadme.workspace = true\nlicense.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nkeywords.workspace = true\ncategories.workspace = true\n\n[dependencies]\nthiserror.workspace = true\n\n[target.'cfg(target_os = \"linux\")'.dependencies]\ncaps.workspace = true\n\n[target.'cfg(unix)'.dependencies]\nnix = { workspace = true, default-features = false, features = [\"user\"] }\n\n[target.'cfg(windows)'.dependencies]\nwindows-sys = { workspace = true, features = [\"Win32_Foundation\", \"Win32_System_Threading\"] }\npaste.workspace = true\n\n[dev-dependencies]\nanyhow.workspace = true\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/trippy-privilege/src/lib.rs",
    "content": "//! Discover platform privileges.\n//!\n//! A cross-platform library to discover and manage platform privileges needed\n//! for sending ICMP packets via RAW and `IPPROTO_ICMP` sockets.\n//!\n//! [`Privilege::acquire_privileges`]:\n//!\n//! - On Linux we check if `CAP_NET_RAW` is in the permitted set and if so raise it to the effective\n//!   set\n//! - On other Unix platforms this is a no-op\n//! - On Windows this is a no-op\n//!\n//! [`Privilege::has_privileges`] (obtained via [`Privilege::discover`]):\n//!\n//! - On Linux we check if `CAP_NET_RAW` is in the effective set\n//! - On other Unix platforms we check that the effective user is root\n//! - On Windows we check if the current process has an elevated token\n//!\n//! [`Privilege::needs_privileges`] (obtained via [`Privilege::discover`]):\n//!\n//! - On macOS we do not always need privileges to send ICMP packets as we can use `IPPROTO_ICMP`\n//!   sockets with the `IP_HDRINCL` socket option.\n//! - On Linux we always need privileges to send ICMP packets even though it supports the\n//!   `IPPROTO_ICMP` socket type but not the `IP_HDRINCL` socket option\n//! - On Windows we always need privileges to send ICMP packets\n//!\n//! [`Privilege::drop_privileges`]:\n//!\n//! - On Linux we clear the effective set\n//! - On other Unix platforms this is a no-op\n//! - On Windows this is a no-op\n//!\n//! # Examples\n//!\n//! Acquire the required privileges if we can:\n//!\n//! ```rust\n//! # fn main() -> anyhow::Result<()> {\n//! # use trippy_privilege::Privilege;\n//! let privilege = Privilege::acquire_privileges()?;\n//! if privilege.has_privileges() {\n//!     println!(\"You have the required privileges for raw sockets\");\n//! } else {\n//!     println!(\"You do not have the required privileges for raw sockets\");\n//! }\n//! if privilege.needs_privileges() {\n//!     println!(\"You always need privileges to send ICMP packets.\");\n//! } else {\n//!     println!(\"You do not always need privileges to send ICMP packets.\");\n//! }\n//! # Ok(())\n//! # }\n//! ```\n//!\n//! Discover the current privileges:\n//!\n//! ```rust\n//! # fn main() -> anyhow::Result<()> {\n//! # use trippy_privilege::Privilege;\n//! let privilege = Privilege::discover()?;\n//! if privilege.has_privileges() {\n//!     println!(\"You have the required privileges for raw sockets\");\n//! } else {\n//!     println!(\"You do not have the required privileges for raw sockets\");\n//! }\n//! if privilege.needs_privileges() {\n//!     println!(\"You always need privileges to send ICMP packets.\");\n//! } else {\n//!     println!(\"You do not always need privileges to send ICMP packets.\");\n//! }\n//! # Ok(())\n//! # }\n//! ```\n//!\n//! Drop all privileges:\n//!\n//! ```rust\n//! # fn main() -> anyhow::Result<()> {\n//! # use trippy_privilege::Privilege;\n//! Privilege::drop_privileges()?;\n//! # Ok(())\n//! # }\n//! ```\n\n/// A privilege error result.\npub type Result<T> = std::result::Result<T, Error>;\n\n/// A privilege error.\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    #[cfg(target_os = \"linux\")]\n    #[error(\"caps error: {0}\")]\n    CapsError(#[from] caps::errors::CapsError),\n    #[cfg(windows)]\n    #[error(\"OpenProcessToken failed\")]\n    OpenProcessTokenError,\n    #[cfg(windows)]\n    #[error(\"GetTokenInformation failed\")]\n    GetTokenInformationError,\n}\n\n/// Run-time platform privilege information.\n#[derive(Debug)]\npub struct Privilege {\n    has_privileges: bool,\n    needs_privileges: bool,\n}\n\nimpl Privilege {\n    /// Discover information about the platform privileges.\n    pub fn discover() -> Result<Self> {\n        let has_privileges = Self::check_has_privileges()?;\n        let needs_privileges = Self::check_needs_privileges();\n        Ok(Self {\n            has_privileges,\n            needs_privileges,\n        })\n    }\n\n    /// Create a new Privilege instance.\n    #[must_use]\n    pub const fn new(has_privileges: bool, needs_privileges: bool) -> Self {\n        Self {\n            has_privileges,\n            needs_privileges,\n        }\n    }\n\n    /// Are we running with the privileges required for raw sockets?\n    #[must_use]\n    pub const fn has_privileges(&self) -> bool {\n        self.has_privileges\n    }\n\n    /// Does our platform always need privileges for `ICMP`?\n    ///\n    /// Specifically, each platform requires privileges unless it supports the `IPPROTO_ICMP` socket\n    /// type which _also_ allows the `IP_HDRINCL` socket option to be set.\n    #[must_use]\n    pub const fn needs_privileges(&self) -> bool {\n        self.needs_privileges\n    }\n\n    // Linux\n\n    #[cfg(target_os = \"linux\")]\n    /// Acquire privileges, if possible.\n    ///\n    /// Check if `CAP_NET_RAW` is in the permitted set and if so raise it to the effective set.\n    pub fn acquire_privileges() -> Result<Self> {\n        if caps::has_cap(None, caps::CapSet::Permitted, caps::Capability::CAP_NET_RAW)? {\n            caps::raise(None, caps::CapSet::Effective, caps::Capability::CAP_NET_RAW)?;\n        }\n        Self::discover()\n    }\n\n    #[cfg(target_os = \"linux\")]\n    /// Do we have the required privileges?\n    ///\n    /// Check if `CAP_NET_RAW` is in the effective set.\n    fn check_has_privileges() -> Result<bool> {\n        Ok(caps::has_cap(\n            None,\n            caps::CapSet::Effective,\n            caps::Capability::CAP_NET_RAW,\n        )?)\n    }\n\n    #[cfg(target_os = \"linux\")]\n    /// Drop all privileges.\n    ///\n    /// Clears the effective set.\n    pub fn drop_privileges() -> Result<()> {\n        caps::clear(None, caps::CapSet::Effective)?;\n        Ok(())\n    }\n\n    // Unix (excl. Linux)\n\n    #[cfg(all(unix, not(target_os = \"linux\")))]\n    /// Acquire privileges, if possible.\n    ///\n    /// This is a no-op on non-Linux unix systems.\n    pub fn acquire_privileges() -> Result<Self> {\n        Self::discover()\n    }\n\n    #[cfg(all(unix, not(target_os = \"linux\")))]\n    #[expect(clippy::unnecessary_wraps)]\n    /// Do we have the required privileges?\n    ///\n    /// Checks if the effective user is root.\n    fn check_has_privileges() -> Result<bool> {\n        Ok(nix::unistd::Uid::effective().is_root())\n    }\n\n    #[cfg(all(unix, not(target_os = \"linux\")))]\n    /// Drop all privileges.\n    ///\n    /// This is a no-op on non-Linux unix systems.\n    pub const fn drop_privileges() -> Result<()> {\n        Ok(())\n    }\n\n    // Unix (excl. macOS)\n\n    #[cfg(all(unix, not(target_os = \"macos\")))]\n    /// Does the platform always require privileges?\n    ///\n    /// Whilst Linux supports the `IPPROTO_ICMP` socket type, it does not allow using it with the\n    /// `IP_HDRINCL` socket option and is therefore not supported.  This may be supported in the\n    /// future.\n    ///\n    /// `NetBSD`, `OpenBSD` and `FreeBSD` do not support `IPPROTO_ICMP`.\n    const fn check_needs_privileges() -> bool {\n        true\n    }\n\n    // macOS\n\n    #[cfg(target_os = \"macos\")]\n    /// Does the platform always require privileges?\n    ///\n    /// `macOS` supports both privileged and unprivileged modes.\n    const fn check_needs_privileges() -> bool {\n        false\n    }\n\n    // Windows\n\n    #[cfg(windows)]\n    /// Acquire privileges, if possible.\n    ///\n    /// This is a no-op on `Windows`.\n    pub fn acquire_privileges() -> Result<Self> {\n        Self::discover()\n    }\n\n    #[cfg(windows)]\n    /// Do we have the required privileges?\n    ///\n    /// Check if the current process has an elevated token.\n    fn check_has_privileges() -> Result<bool> {\n        macro_rules! syscall {\n            ($p: path, $fn: ident ( $($arg: expr),* $(,)* ) ) => {{\n                #[expect(unsafe_code)]\n                unsafe { paste::paste!(windows_sys::Win32::$p::$fn) ($($arg, )*) }\n            }};\n        }\n\n        /// Window elevated privilege checker.\n        pub struct Privileged {\n            handle: windows_sys::Win32::Foundation::HANDLE,\n        }\n\n        impl Privileged {\n            /// Create a new `ElevationChecker` for the current process.\n            pub fn current_process() -> Result<Self> {\n                use windows_sys::Win32::Security::TOKEN_QUERY;\n                let mut handle: windows_sys::Win32::Foundation::HANDLE = 0;\n                let current_process = syscall!(System::Threading, GetCurrentProcess());\n                let res = syscall!(\n                    System::Threading,\n                    OpenProcessToken(current_process, TOKEN_QUERY, std::ptr::addr_of_mut!(handle))\n                );\n                if res == 0 {\n                    Err(Error::OpenProcessTokenError)\n                } else {\n                    Ok(Self { handle })\n                }\n            }\n\n            /// Check if the current process has elevated privileged.\n            pub fn is_elevated(&self) -> Result<bool> {\n                use windows_sys::Win32::Security::TOKEN_ELEVATION;\n                use windows_sys::Win32::Security::TokenElevation;\n                let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };\n                #[expect(clippy::cast_possible_truncation)]\n                let size = std::mem::size_of::<TOKEN_ELEVATION>() as u32;\n                let mut ret_size = 0u32;\n                let ret = syscall!(\n                    Security,\n                    GetTokenInformation(\n                        self.handle,\n                        TokenElevation,\n                        std::ptr::addr_of_mut!(elevation).cast(),\n                        size,\n                        std::ptr::addr_of_mut!(ret_size),\n                    )\n                );\n                if ret == 0 {\n                    Err(Error::GetTokenInformationError)\n                } else {\n                    Ok(elevation.TokenIsElevated != 0)\n                }\n            }\n        }\n\n        impl Drop for Privileged {\n            fn drop(&mut self) {\n                if self.handle != 0 {\n                    syscall!(Foundation, CloseHandle(self.handle));\n                }\n            }\n        }\n        Privileged::current_process()?.is_elevated()\n    }\n\n    #[cfg(windows)]\n    /// Drop all capabilities.\n    ///\n    /// This is a no-op on `Windows`.\n    pub const fn drop_privileges() -> Result<()> {\n        Ok(())\n    }\n\n    #[cfg(target_os = \"windows\")]\n    /// Does the platform always require privileges?\n    ///\n    /// Privileges are always required on `Windows`.\n    const fn check_needs_privileges() -> bool {\n        true\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/Cargo.toml",
    "content": "[package]\nname = \"trippy-tui\"\ndescription = \"A network diagnostic tool\"\nversion.workspace = true\nauthors.workspace = true\ndocumentation.workspace = true\nhomepage.workspace = true\nrepository.workspace = true\nreadme.workspace = true\nlicense.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nkeywords.workspace = true\ncategories.workspace = true\n\n[dependencies]\ntrippy-core.workspace = true\ntrippy-privilege.workspace = true\ntrippy-dns.workspace = true\nanyhow.workspace = true\nchrono = { workspace = true, default-features = false, features = [\"clock\", \"serde\"] }\nchrono-tz.workspace = true\nclap = { workspace = true, default-features = false, features = [\"cargo\", \"derive\", \"wrap_help\", \"usage\", \"unstable-styles\", \"color\", \"suggestions\", \"error-context\", \"env\"] }\nclap_complete.workspace = true\nclap_mangen.workspace = true\ncomfy-table.workspace = true\ncrossterm = { workspace = true, default-features = false, features = [\"events\", \"windows\"] }\ncsv.workspace = true\nencoding_rs_io.workspace = true\netcetera.workspace = true\nhumantime.workspace = true\nitertools.workspace = true\nmaxminddb.workspace = true\npetgraph.workspace = true\nratatui.workspace = true\nserde = { workspace = true, default-features = false, features = [\"derive\"] }\nserde_json.workspace = true\nserde_with.workspace = true\nstrum = { workspace = true, default-features = false, features = [\"std\", \"derive\"] }\nsys-locale.workspace = true\ntoml = { workspace = true, default-features = false, features = [\"parse\"] }\ntracing-chrome.workspace = true\ntracing-subscriber = { workspace = true, default-features = false, features = [\"env-filter\", \"json\"] }\ntracing.workspace = true\nunic-langid.workspace = true\nunicode-width.workspace = true\nclap-cargo.workspace = true\n\n[dev-dependencies]\ninsta = { workspace = true, features = [\"serde\"] }\npretty_assertions.workspace = true\ntest-case.workspace = true\n\n[lints]\nworkspace = true\n"
  },
  {
    "path": "crates/trippy-tui/build.rs",
    "content": "pub fn main() {\n    println!(\"cargo:rerun-if-changed=locales.toml\");\n}\n"
  },
  {
    "path": "crates/trippy-tui/locales.toml",
    "content": "[trippy]\nen = \"trippy\"\nfr = \"trippy\"\ntr = \"trippy\"\nit = \"trippy\"\npt = \"trippy\"\nzh = \"trippy\"\nzh-TW = \"trippy\"\nsv = \"trippy\"\nru = \"trippy\"\nes = \"trippy\"\nde = \"trippy\"\n\n[auto]\nen = \"auto\"\nfr = \"automatique\"\ntr = \"otomatik\"\nit = \"auto\"\npt = \"automático\"\nzh = \"自动\"\nzh-TW = \"自動\"\nsv = \"automatisk\"\nru = \"авто\"\nes = \"automático\"\nde = \"auto\"\n\n[on]\nen = \"on\"\nfr = \"activé\"\ntr = \"açık\"\nit = \"on\"\npt = \"ligado\"\nzh = \"开\"\nzh-TW = \"開\"\nsv = \"på\"\nru = \"вкл\"\nes = \"activo\"\nde = \"an\"\n\n[off]\nen = \"off\"\nfr = \"désactivé\"\ntr = \"kapalı\"\nit = \"off\"\npt = \"desligado\"\nzh = \"关\"\nzh-TW = \"關\"\nsv = \"av\"\nru = \"выкл\"\nes = \"inactivo\"\nde = \"aus\"\n\n[yes]\nen = \"Yes\"\nfr = \"Oui\"\ntr = \"Evet\"\nit = \"Sì\"\npt = \"Sim\"\nzh = \"是\"\nzh-TW = \"是\"\nsv = \"Ja\"\nru = \"Да\"\nes = \"Sí\"\nde = \"Ja\"\n\n[no]\nen = \"No\"\nfr = \"Non\"\ntr = \"Hayır\"\nit = \"No\"\npt = \"Não\"\nzh = \"否\"\nzh-TW = \"否\"\nsv = \"Nej\"\nru = \"Нет\"\nes = \"No\"\nde = \"Nein\"\n\n[none]\nen = \"none\"\nfr = \"aucun\"\ntr = \"hiçbiri\"\nit = \"nessuno\"\npt = \"nenhum\"\nzh = \"无\"\nzh-TW = \"無\"\nsv = \"ingen\"\nru = \"нет\"\nes = \"ninguno\"\nde = \"keiner\"\n\n[hidden]\nen = \"Hidden\"\nfr = \"Caché\"\ntr = \"Gizli\"\nit = \"Nascosto\"\npt = \"Oculto\"\nzh = \"隐藏\"\nzh-TW = \"隱藏\"\nsv = \"Dold\"\nru = \"Скрыто\"\nes = \"Oculto\"\nde = \"Versteckt\"\n\n[flow]\nen = \"flow\"\nfr = \"flux\"\ntr = \"akış\"\nit = \"flusso\"\npt = \"fluxo\"\nzh = \"流量\"\nzh-TW = \"流量\"\nsv = \"flöde\"\nru = \"поток\"\nes = \"flujo\"\nde = \"fluss\"\n\n[flows]\nen = \"flows\"\nfr = \"flux\"\ntr = \"akışlar\"\nit = \"flussi\"\npt = \"fluxos\"\nzh = \"流量\"\nzh-TW = \"流量\"\nsv = \"flöden\"\nru = \"потоки\"\nes = \"flujos\"\nde = \"flüsse\"\n\n[target]\nen = \"Target\"\nfr = \"Cible\"\ntr = \"Hedef\"\nit = \"Target\"\npt = \"Alvo\"\nzh = \"目标\"\nzh-TW = \"目標\"\nsv = \"Mål\"\nru = \"Цель\"\nes = \"Objetivo\"\nde = \"Ziel\"\n\n[status]\nen = \"Status\"\nfr = \"Statut\"\ntr = \"Durum\"\nit = \"Stato\"\npt = \"Estado\"\nzh = \"状态\"\nzh-TW = \"狀態\"\nsv = \"Status\"\nru = \"Статус\"\nes = \"Estado\"\nde = \"Status\"\n\n[details]\nen = \"detail\"\nfr = \"détail\"\ntr = \"ayrıntılar\"\nit = \"dettagli\"\npt = \"detalhe\"\nzh = \"详情\"\nzh-TW = \"詳情\"\nsv = \"detaljer\"\nru = \"детали\"\nes = \"detalles\"\nde = \"detail\"\n\n[privileged]\nen = \"privileged\"\nfr = \"privilégié\"\ntr = \"ayrıcalıklı\"\nit = \"privilegiato\"\npt = \"privilegiado\"\nzh = \"特权\"\nzh-TW = \"特權\"\nsv = \"privilegierad\"\nru = \"привилегированный\"\nes = \"privilegiado\"\nde = \"privilegiert\"\n\n[unprivileged]\nen = \"unprivileged\"\nfr = \"non privilégié\"\ntr = \"ayrıcalıksız\"\nit = \"non privilegiato\"\npt = \"não privilegiado\"\nzh = \"非特权\"\nzh-TW = \"非特權\"\nsv = \"oprivilegierad\"\nru = \"непривилегированный\"\nes = \"no privilegiado\"\nde = \"unprivilegiert\"\n\n[privacy]\nen = \"privacy\"\nfr = \"confidentialité\"\ntr = \"gizlilik\"\nit = \"privacy\"\npt = \"privacidade\"\nzh = \"隐私\"\nzh-TW = \"隱私\"\nsv = \"integritet\"\nru = \"конфиденциальность\"\nes = \"privacidad\"\nde = \"datenschutz\"\n\n[na]\nen = \"n/a\"\nfr = \"non disponible\"\ntr = \"yok\"\nit = \"n/d\"\npt = \"n/d\"\nzh = \"无\"\nzh-TW = \"無\"\nsv = \"ej tillgänglig\"\nru = \"н/д\"\nes = \"n/d\"\nde = \"n/a\"\n\n[discovered]\nen = \"discovered %{hop_count} hops\"\nfr = \"%{hop_count} sauts découverts\"\ntr = \"%{hop_count} atlanan keşfedildi\"\nit = \"%{hop_count} salti trovati\"\npt = \"descobriu %{hop_count} saltos\"\nzh = \"发现 %{hop_count} 跳\"\nzh-TW = \"發現 %{hop_count} 跳\"\nsv = \"upptäckte %{hop_count} hopp\"\nru = \"обнаружено %{hop_count} прыжков\"\nes = \"se descubrieron %{hop_count} saltos\"\nde = \"%{hop_count} gefundene hops\"\n\n[discovered_flows]\nen = \"discovered %{hop_count} hops and %{flow_count} unique %{plural_flows}\"\nfr = \"%{hop_count} sauts et %{flow_count} %{plural_flows} uniques découverts\"\ntr = \"%{hop_count} atlanan ve %{flow_count} benzersiz %{plural_flows} keşfedildi\"\nit = \"scoperti %{hop_count} salti e %{flow_count} %{plural_flows} unici\"\npt = \"descobriu %{hop_count} saltos e %{flow_count} %{plural_flows} únicos\"\nzh = \"发现 %{hop_count} 跳，%{flow_count} 个唯一%{plural_flow}\"\nzh-TW = \"發現 %{hop_count} 跳，%{flow_count} 個唯一%{plural_flow}\"\nsv = \"%{hop_count} hopp och unika %{flow_count} %{plural_flows} upptäckta\"\nru = \"обнаружено %{hop_count} прыжков и %{flow_count} уникальных %{plural_flows}\"\nes = \"se descubrieron %{hop_count} saltos y %{flow_count} únicos %{plural_flows}\"\nde = \"%{hop_count} gefundene hops und %{flow_count} eindeutige %{plural_flows}\"\n\n[unknown]\nen = \"unknown\"\nfr = \"inconnu\"\ntr = \"bilinmeyen\"\nit = \"sconosciuto\"\npt = \"desconhecido\"\nzh = \"未知\"\nzh-TW = \"未知\"\nsv = \"okänd\"\nru = \"неизвестно\"\nes = \"desconocido\"\nde = \"unbekannt\"\n\n[icmp]\nen = \"icmp\"\nfr = \"icmp\"\ntr = \"icmp\"\nit = \"icmp\"\npt = \"icmp\"\nzh = \"icmp\"\nzh-TW = \"icmp\"\nsv = \"icmp\"\nru = \"icmp\"\nes = \"icmp\"\nde = \"icmp\"\n\n[udp]\nen = \"udp\"\nfr = \"udp\"\ntr = \"udp\"\nit = \"udp\"\npt = \"udp\"\nzh = \"udp\"\nzh-TW = \"udp\"\nsv = \"udp\"\nru = \"udp\"\nes = \"udp\"\nde = \"udp\"\n\n[tcp]\nen = \"tcp\"\nfr = \"tcp\"\ntr = \"tcp\"\nit = \"tcp\"\npt = \"tcp\"\nzh = \"tcp\"\nzh-TW = \"tcp\"\nsv = \"tcp\"\nru = \"tcp\"\nes = \"tcp\"\nde = \"tcp\"\n\n[status_failures]\nen = \"%{failure_count} of %{total_probes} (%{failure_rate}%) probes failed\"\nfr = \"%{failure_count} sur %{total_probes} (%{failure_rate}%) sondes ont échoué\"\ntr = \"%{failure_count} / %{total_probes} (%{failure_rate}%) sondajın başarısız olması\"\nit = \"%{failure_count} di %{total_probes} (%{failure_rate}%) prove fallite\"\npt = \"%{failure_count} de %{total_probes} (%{failure_rate}%) sondas falharam\"\nzh = \"%{failure_count} 个失败，共探测到 %{total_probes} 个（%{failure_rate}%）\"\nzh-TW = \"%{failure_count} 個失敗，共探測到 %{total_probes} 個（%{failure_rate}%）\"\nsv = \"%{failure_count} av %{total_probes} (%{failure_rate}%) misslyckade prober\"\nru = \"%{failure_count} из %{total_probes} (%{failure_rate}%) зондов не удалось\"\nes = \"%{failure_count} de %{total_probes} (%{failure_rate}%) sondas fallaron\"\nde = \"%{failure_count} von %{total_probes} (%{failure_rate}%) sonden sind fehlgeschlagen\"\n\n[status_failed]\nen = \"Failed\"\nfr = \"Échec\"\ntr = \"Başarısız\"\nit = \"Fallito\"\npt = \"Falhou\"\nzh = \"失败\"\nzh-TW = \"失敗\"\nsv = \"Misslyckades\"\nru = \"Не удалось\"\nes = \"Fallido\"\nde = \"Fehlgeschlagen\"\n\n[status_running]\nen = \"Running\"\nfr = \"En cours\"\ntr = \"Çalışıyor\"\nit = \"In esecuzione\"\npt = \"Executando\"\nzh = \"运行中\"\nzh-TW = \"執行中\"\nsv = \"Kör\"\nru = \"Запущен\"\nes = \"En ejecución\"\nde = \"Läuft\"\n\n[status_frozen]\nen = \"Frozen\"\nfr = \"Gelé\"\ntr = \"Dondurulmuş\"\nit = \"Congelato\"\npt = \"Congelado\"\nzh = \"冻结\"\nzh-TW = \"凍結\"\nsv = \"Frusen\"\nru = \"Заморожен\"\nes = \"Congelado\"\nde = \"Eingefroren\"\n\n[awaiting_data]\nen = \"Awaiting data...\"\nfr = \"En attente de données...\"\ntr = \"Veri bekleniyor...\"\nit = \"In attesa di dati...\"\npt = \"Aguardando dados...\"\nzh = \"等待数据……\"\nzh-TW = \"等待資料……\"\nsv = \"Väntar på data...\"\nru = \"Ожидание данных...\"\nes = \"Esperando datos...\"\nde = \"Warten auf Daten...\"\n\n[header_help]\nen = \"help\"\nfr = \"aide\"\ntr = \"yardım\"\nit = \"aiuto\"\npt = \"ajuda\"\nzh = \"帮助\"\nzh-TW = \"幫助\"\nsv = \"hjälp\"\nru = \"помощь\"\nes = \"ayuda\"\nde = \"hilfe\"\n\n[header_settings]\nen = \"settings\"\nfr = \"paramètres\"\ntr = \"ayarlar\"\nit = \"impostazioni\"\npt = \"configurações\"\nzh = \"设置\"\nzh-TW = \"設定\"\nsv = \"inställningar\"\nru = \"настройки\"\nes = \"configuraciones\"\nde = \"einstellungen\"\n\n[header_quit]\nen = \"quit\"\nfr = \"quitter\"\ntr = \"cıkış\"\nit = \"uscita\"\npt = \"sair\"\nzh = \"退出\"\nzh-TW = \"退出\"\nsv = \"avsluta\"\nru = \"Выход\"\nes = \"salir\"\nde = \"beenden\"\n\n[title_hops]\nen = \"Hops\"\nfr = \"Sauts\"\ntr = \"Atlananlar\"\nit = \"Salti\"\npt = \"Saltos\"\nzh = \"跳\"\nzh-TW = \"跳\"\nsv = \"Hopp\"\nru = \"Прыжки\"\nes = \"Saltos\"\nde = \"Hops\"\n\n[title_frequency]\nen = \"Frequency\"\nfr = \"Fréquence\"\ntr = \"Sıklık\"\nit = \"Frequenza\"\npt = \"Frequência\"\nzh = \"频率\"\nzh-TW = \"頻率\"\nsv = \"Frekvens\"\nru = \"Частота\"\nes = \"Frecuencia\"\nde = \"Frequenz\"\n\n[title_samples]\nen = \"Samples\"\nfr = \"Échantillons\"\ntr = \"Örnekler\"\nit = \"Campioni\"\npt = \"Amostras\"\nzh = \"样本\"\nzh-TW = \"樣本\"\nsv = \"Prover\"\nru = \"Образцы\"\nes = \"Muestras\"\nde = \"Proben\"\n\n[title_traces]\nen = \"Traces\"\nfr = \"Traces\"\ntr = \"İzler\"\nit = \"Tracce\"\npt = \"Rastreios\"\nzh = \"跟踪\"\nzh-TW = \"追蹤\"\nsv = \"Spår\"\nru = \"Следы\"\nes = \"Trazas\"\nde = \"Spuren\"\n\n[title_flows]\nen = \"Flows\"\nfr = \"Flux\"\ntr = \"Akışlar\"\nit = \"Flussi\"\npt = \"Fluxos\"\nzh = \"流量\"\nzh-TW = \"流量\"\nsv = \"Flöden\"\nru = \"Потоки\"\nes = \"Flujos\"\nde = \"Flüsse\"\n\n[title_map]\nen = \"Map\"\nfr = \"Carte\"\ntr = \"Harita\"\nit = \"Mappa\"\npt = \"Mapa\"\nzh = \"地图\"\nzh-TW = \"地圖\"\nsv = \"Karta\"\nru = \"Карта\"\nes = \"Mapa\"\nde = \"Karte\"\n\n[title_help]\nen = \"Help\"\nfr = \"Aide\"\ntr = \"Yardım\"\nit = \"Aiuto\"\npt = \"Ajuda\"\nzh = \"帮助\"\nzh-TW = \"幫助\"\nsv = \"Hjälp\"\nru = \"Помощь\"\nes = \"Ayuda\"\nde = \"Hilfe\"\n\n[title_settings]\nen = \"Settings\"\nfr = \"Paramètres\"\ntr = \"Ayarlar\"\nit = \"Impostazioni\"\npt = \"Configurações\"\nzh = \"设置\"\nzh-TW = \"設定\"\nsv = \"Inställningar\"\nru = \"Настройки\"\nes = \"Configuraciones\"\nde = \"Einstellungen\"\n\n[bsod_failed]\nen = \"Trippy Failed :(\"\nfr = \"Trippy a échoué :(\"\ntr = \"Trippy Başarısız :(\"\nit = \"Trippy ha avuto un problema :(\"\npt = \"Trippy falhou :(\"\nzh = \"Trippy 失败 :(\"\nzh-TW = \"Trippy 失敗 :(\"\nsv = \"Trippy misslyckades :(\"\nru = \"Trippy не удалось :(\"\nes = \"Trippy tuvo un problema :(\"\nde = \"Trippy ist fehlgeschlagen :(\"\n\n[bsod_quit]\nen = \"Press q to quit\"\nfr = \"Appuyez sur q pour quitter\"\ntr = \"Çıkmak için q tuşuna basın\"\nit = \"Premi q per uscire\"\npt = \"Pressione q para sair\"\nzh = \"按 q 退出\"\nzh-TW = \"按 q 退出\"\nsv = \"Tryck på q för att avsluta\"\nru = \"Нажмите q для выхода\"\nes = \"Presiona q para salir\"\nde = \"Drücken Sie q, um zu beenden\"\n\n[hop]\nen = \"Hop\"\nfr = \"Saut\"\ntr = \"Atla\"\nit = \"Salto\"\npt = \"Salto\"\nzh = \"跳\"\nzh-TW = \"跳\"\nsv = \"Hopp\"\nru = \"Прыжок\"\nes = \"Salto\"\nde = \"Hop\"\n\n[rtt]\nen = \"RTT\"\nfr = \"RTT\"\ntr = \"RTT\"\nit = \"RTT\"\npt = \"RTT\"\nzh = \"往返时间\"\nzh-TW = \"往返時間\"\nsv = \"RTT\"\nru = \"RTT\"\nes = \"RTT\"\nde = \"RTT\"\n\n[title_chart]\nen = \"Chart\"\nfr = \"Graphique\"\ntr = \"Grafik\"\nit = \"Grafico\"\npt = \"Gráfico\"\nzh = \"图表\"\nzh-TW = \"圖表\"\nsv = \"Diagram\"\nru = \"Диаграмма\"\nes = \"Gráfico\"\nde = \"Diagramm\"\n\n[samples]\nen = \"Samples\"\nfr = \"Échantillons\"\ntr = \"Örnekler\"\nit = \"Campioni\"\npt = \"Amostras\"\nzh = \"样本\"\nzh-TW = \"樣本\"\nsv = \"Prover\"\nru = \"Образцы\"\nes = \"Muestras\"\nde = \"Proben\"\n\n[host]\nen = \"Host\"\nfr = \"Hôte\"\ntr = \"Ana bilgisayar\"\nit = \"Host\"\npt = \"Host\"\nzh = \"主机\"\nzh-TW = \"主機\"\nsv = \"Värd\"\nru = \"Хост\"\nes = \"Host\"\nde = \"Host\"\n\n[no_response]\nen = \"No response\"\nfr = \"Pas de réponse\"\ntr = \"Yanıt yok\"\nit = \"Nessuna risposta\"\npt = \"Sem resposta\"\nzh = \"无响应\"\nzh-TW = \"無回應\"\nsv = \"Inget svar\"\nru = \"Нет ответа\"\nes = \"Sin respuesta\"\nde = \"Keine Antwort\"\n\n[dns_failed]\nen = \"Failed\"\nfr = \"Échec\"\ntr = \"Başarısız\"\nit = \"Fallito\"\npt = \"Falhou\"\nzh = \"失败\"\nzh-TW = \"失敗\"\nsv = \"Misslyckades\"\nru = \"Не удалось\"\nes = \"Fallido\"\nde = \"Fehlgeschlagen\"\n\n[dns_timeout]\nen = \"Timeout\"\nfr = \"Délai dépassé\"\ntr = \"Zaman aşımı\"\nit = \"Tempo scaduto\"\npt = \"Tempo esgotado\"\nzh = \"超时\"\nzh-TW = \"逾時\"\nsv = \"Tidsgräns nådd\"\nru = \"Тайм-аут\"\nes = \"Tiempo de espera\"\nde = \"Zeitüberschreitung\"\n\n[labels]\nen = \"labels\"\nfr = \"étiquettes\"\ntr = \"etiketler\"\nit = \"etichette\"\npt = \"etiquetas\"\nzh = \"标签\"\nzh-TW = \"標籤\"\nsv = \"etiketter\"\nru = \"метки\"\nes = \"etiquetas\"\nde = \"etiketten\"\n\n[not_enabled]\nen = \"not enabled\"\nfr = \"non activé\"\ntr = \"etkin değil\"\nit = \"non abilitato\"\npt = \"não ativado\"\nzh = \"未启用\"\nzh-TW = \"未啟用\"\nsv = \"ej aktiverad\"\nru = \"не активирован\"\nes = \"no habilitado\"\nde = \"nicht aktiviert\"\n\n[not_found]\nen = \"not found\"\nfr = \"non trouvé\"\ntr = \"bulunamadı\"\nit = \"non trovato\"\npt = \"não encontrado\"\nzh = \"未找到\"\nzh-TW = \"未找到\"\nsv = \"hittades inte\"\nru = \"не найдено\"\nes = \"no encontrado\"\nde = \"nicht gefunden\"\n\n[awaited]\nen = \"awaited\"\nfr = \"attendu\"\ntr = \"beklenen\"\nit = \"atteso\"\npt = \"aguardado\"\nzh = \"等待\"\nzh-TW = \"等待\"\nsv = \"väntade\"\nru = \"ожидаемый\"\nes = \"esperado\"\nde = \"erwartet\"\n\n[name]\nen = \"Name\"\nfr = \"Nom\"\ntr = \"Ad\"\nit = \"Nome\"\npt = \"Nome\"\nzh = \"名称\"\nzh-TW = \"名稱\"\nsv = \"Namn\"\nru = \"Имя\"\nes = \"Nombre\"\nde = \"Name\"\n\n[info]\nen = \"Info\"\nfr = \"Information\"\ntr = \"Bilgi\"\nit = \"Info\"\npt = \"Informação\"\nzh = \"信息\"\nzh-TW = \"資訊\"\nsv = \"Information\"\nru = \"Информация\"\nes = \"Información\"\nde = \"Info\"\n\n[geo]\nen = \"Geo\"\nfr = \"Géo\"\ntr = \"Coğrafi\"\nit = \"Geo\"\npt = \"Geo\"\nzh = \"地理坐标\"\nzh-TW = \"地理座標\"\nsv = \"Geo\"\nru = \"Гео\"\nes = \"Geo\"\nde = \"Geo\"\n\n[pos]\nen = \"Pos\"\nfr = \"Pos\"\ntr = \"Poz\"\nit = \"Pos\"\npt = \"Pos\"\nzh = \"位置\"\nzh-TW = \"位置\"\nsv = \"Pos\"\nru = \"Поз\"\nes = \"Pos\"\nde = \"Pos\"\n\n[ext]\nen = \"Ext\"\nfr = \"Ext\"\ntr = \"Uzantı\"\nit = \"Est\"\npt = \"Ext\"\nzh = \"扩展\"\nzh-TW = \"擴充\"\nsv = \"Ext\"\nru = \"Расширение\"\nes = \"Ext\"\nde = \"Ext\"\n\n[help_tagline]\nen = \"A network diagnostic tool\"\nfr = \"Un outil de diagnostic réseau\"\ntr = \"Bir ağ analiz aracı\"\nit = \"Uno strumento diagnostico di rete\"\npt = \"Uma ferramenta de diagnóstico de rede\"\nzh = \"网络诊断工具\"\nzh-TW = \"網路診斷工具\"\nsv = \"Ett nätverksdiagnostikverktyg\"\nru = \"Инструмент диагностики сети\"\nes = \"Una herramienta de diagnóstico de red\"\nde = \"Ein Netzwerkdiagnosetool\"\n\n[help_show_settings]\nen = \"Press [%{key}] to show all settings\"\nfr = \"Appuyez sur [%{key}] pour afficher tous les paramètres\"\ntr = \"Tüm ayarları görmek için [%{key}] tuşuna basın\"\nit = \"Premi [%{key}] per visualizzare tutte le impostazioni\"\npt = \"Pressione [%{key}] para mostrar todas as configurações\"\nzh = \"按 [%{key}] 显示所有设置\"\nzh-TW = \"按 [%{key}] 顯示所有設定\"\nsv = \"Tryck på [%{key}] för att visa alla inställningar\"\nru = \"Нажмите [%{key}], чтобы показать все настройки\"\nes = \"Presiona [%{key}] para mostrar todas las configuraciones\"\nde = \"Drücken Sie [%{key}], um alle Einstellungen anzuzeigen\"\n\n[help_show_bindings]\nen = \"Press [%{key}] to show all bindings\"\nfr = \"Appuyez sur [%{key}] pour afficher tous les raccourcis clavier\"\ntr = \"Tüm bağlantıları görmek için [%{key}] tuşuna basın\"\nit = \"Premi [%{key}] per visualizzare tutti i collegamenti\"\npt = \"Pressione [%{key}] para mostrar todos os atalhos\"\nzh = \"按 [%{key}] 显示所有绑定\"\nzh-TW = \"按 [%{key}] 顯示所有綁定\"\nsv = \"Tryck på [%{key}] för att visa alla kortkommando\"\nru = \"Нажмите [%{key}], чтобы показать все привязки\"\nes = \"Presiona [%{key}] para mostrar todos los atajos\"\nde = \"Drücken Sie [%{key}], um alle Tastenbelegungen anzuzeigen\"\n\n[help_show_columns]\nen = \"Press [%{key}] to show all columns\"\nfr = \"Appuyez sur [%{key}] pour afficher toutes les colonnes\"\ntr = \"Tüm sütunları görmek için [%{key}] tuşuna basın\"\nit = \"Premi [%{key}] per visualizzare tutte le colonne\"\npt = \"Pressione [%{key}] para mostrar todas as colunas\"\nzh = \"按 [%{key}] 显示所有列\"\nzh-TW = \"按 [%{key}] 顯示所有欄\"\nsv = \"Tryck på [%{key}] för att visa alla kolumner\"\nru = \"Нажмите [%{key}], чтобы показать все столбцы\"\nes = \"Presiona [%{key}] para mostrar todas las columnas\"\nde = \"Drücken Sie [%{key}], um alle Spalten anzuzeigen\"\n\n[help_license]\nen = \"Distributed under the Apache License 2.0\"\nfr = \"Distribué sous licence Apache 2.0\"\ntr = \"Apache Lisansı 2.0 altında dağıtılmıştır\"\nit = \"Distribuito con licenza Apache 2.0\"\npt = \"Distribuído sob a licença Apache 2.0\"\nzh = \"以 Apache-2.0 许可分发\"\nzh-TW = \"以 Apache-2.0 授權分發\"\nsv = \"Distribueras under Apache License 2.0\"\nru = \"Распространяется под лицензией Apache 2.0\"\nes = \"Distribuido bajo la Licencia Apache 2.0\"\nde = \"Verteilt unter der Apache-Lizenz 2.0\"\n\n[help_copyright]\nen = \"Copyright 2022 Trippy Contributors\"\nfr = \"Copyright 2022 Contributeurs de Trippy\"\ntr = \"Telif Hakkı 2022 - Trippy'ye Katkıda Bulunanlar\"\nit = \"Copyright 2022 - Collaboratori di Trippy\"\npt = \"Direitos autorais 2022 Colaboradores do Trippy\"\nzh = \"版权所有 2022 Trippy 贡献者\"\nzh-TW = \"版權所有 2022 Trippy 貢獻者\"\nsv = \"Upphovsrätt 2022 Trippy-bidragsgivare\"\nru = \"Авторское право 2022 Участники Trippy\"\nes = \"Derechos de autor 2022 Contribuidores de Trippy\"\nde = \"Copyright 2022 Trippy Mitwirkende\"\n\n[geoip_not_enabled]\nen = \"GeoIp not enabled\"\nfr = \"GeoIp non activé\"\ntr = \"GeoIp etkin değil\"\nit = \"GeoIp non abilitato\"\npt = \"GeoIp não ativado\"\nzh = \"GeoIp 未启用\"\nzh-TW = \"GeoIp 未啟用\"\nsv = \"GeoIp inte aktiverad\"\nru = \"GeoIp не активирован\"\nes = \"GeoIp no habilitado\"\nde = \"GeoIp nicht aktiviert\"\n\n[geoip_no_data_for_hop]\nen = \"No GeoIp data for hop\"\nfr = \"Pas de données GeoIp pour le saut\"\ntr = \"Hop için GeoIp verisi yok\"\nit = \"Nessun dato GeoIp per il salto\"\npt = \"Nenhum dado GeoIp para o salto\"\nzh = \"无 GeoIp 数据\"\nzh-TW = \"無 GeoIp 資料\"\nsv = \"Inga GeoIp-data för hopp\"\nru = \"Нет данных GeoIp для прыжка\"\nes = \"No hay datos de GeoIp para el salto\"\nde = \"Keine GeoIp-Daten für den Hop\"\n\n[geoip_multiple_data_for_hop]\nen = \"Multiple GeoIp locations for hop\"\nfr = \"Emplacements GeoIp multiples pour le saut\"\ntr = \"Hop için birden fazla GeoIp konumu\"\nit = \"Posizioni GeoIp multiple per il salto\"\npt = \"Múltiplas localizações GeoIp para o salto\"\nzh = \"多个 GeoIp 位置\"\nzh-TW = \"多個 GeoIp 位置\"\nsv = \"Flera GeoIp-platser för hopp\"\nru = \"Несколько местоположений GeoIp для прыжка\"\nes = \"Múltiples ubicaciones de GeoIp para el salto\"\nde = \"Mehrere GeoIp-Standorte für den Hop\"\n\n[kilometer]\nen = \"km\"\nfr = \"km\"\ntr = \"km\"\nit = \"km\"\npt = \"km\"\nzh = \"公里\"\nzh-TW = \"公里\"\nsv = \"km\"\nru = \"км\"\nes = \"km\"\nde = \"km\"\n\n[settings_info]\nen = \"Info\"\nfr = \"Information\"\ntr = \"Bilgi\"\nit = \"Info\"\npt = \"Informação\"\nzh = \"信息\"\nzh-TW = \"資訊\"\nsv = \"Information\"\nru = \"Информация\"\nes = \"Información\"\nde = \"Info\"\n\n[settings_tab_tui_title]\nen = \"Tui\"\nfr = \"Tui\"\ntr = \"Tui\"\nit = \"Tui\"\npt = \"Tui\"\nzh = \"终端用户界面\"\nzh-TW = \"終端使用者介面\"\nsv = \"Tui\"\nru = \"Tui\"\nes = \"Tui\"\nde = \"Tui\"\n\n[settings_tab_trace_title]\nen = \"Trace\"\nfr = \"Tracer\"\ntr = \"İz\"\nit = \"Traccia\"\npt = \"Rastrear\"\nzh = \"跟踪\"\nzh-TW = \"追蹤\"\nsv = \"Spåra\"\nru = \"След\"\nes = \"Rastrear\"\nde = \"Trace\"\n\n[settings_tab_dns_title]\nen = \"DNS\"\nfr = \"DNS\"\ntr = \"DNS\"\nit = \"DNS\"\npt = \"DNS\"\nzh = \"DNS\"\nzh-TW = \"DNS\"\nsv = \"DNS\"\nru = \"DNS\"\nes = \"DNS\"\nde = \"DNS\"\n\n[settings_tab_geoip_title]\nen = \"GeoIp\"\nfr = \"GeoIp\"\ntr = \"GeoIp\"\nit = \"GeoIp\"\npt = \"GeoIp\"\nzh = \"GeoIp\"\nzh-TW = \"GeoIp\"\nsv = \"GeoIp\"\nru = \"GeoIp\"\nes = \"GeoIp\"\nde = \"GeoIp\"\n\n[settings_tab_bindings_title]\nen = \"Bindings\"\nfr = \"Raccourcis clavier\"\ntr = \"Bağlantılar\"\nit = \"Collegamenti\"\npt = \"Atalhos\"\nzh = \"绑定\"\nzh-TW = \"綁定\"\nsv = \"Kortkommando\"\nru = \"Привязки\"\nes = \"Atajos\"\nde = \"Tastenbelegungen\"\n\n[settings_tab_theme_title]\nen = \"Theme\"\nfr = \"Thème\"\ntr = \"Tema\"\nit = \"Tema\"\npt = \"Tema\"\nzh = \"主题\"\nzh-TW = \"主題\"\nsv = \"Tema\"\nru = \"Тема\"\nes = \"Tema\"\nde = \"Darstellung\"\n\n[settings_tab_columns_title]\nen = \"Columns\"\nfr = \"Colonnes\"\ntr = \"Sütunlar\"\nit = \"Colonne\"\npt = \"Colunas\"\nzh = \"列\"\nzh-TW = \"欄\"\nes = \"Columnas\"\nsv = \"Kolumner\"\nru = \"Столбцы\"\nde = \"Spalten\"\n\n[settings_tab_tui_desc]\nen = \"Settings which control how data is displayed in this Tui\"\nfr = \"Paramètres qui contrôlent la façon dont les données sont affichées dans ce Tui\"\ntr = \"Arayüzde verilerin nasıl görüntülendiğini kontrol eden ayarlar\"\nit = \"Impostazioni che controllano come i dati vengono visualizzati in questo Tui\"\npt = \"Configurações que controlam como os dados são exibidos neste Tui\"\nzh = \"数据显示方式设置\"\nzh-TW = \"資料顯示方式設定\"\nsv = \"Inställningar som styr hur data visas i detta Tui\"\nru = \"Настройки, которые контролируют, как данные отображаются в этом Tui\"\nes = \"Configuraciones que controlan cómo se muestran los datos en este Tui\"\nde = \"Einstellungen, die steuern, wie Daten in diesem Tui angezeigt werden\"\n\n[settings_tab_trace_desc]\nen = \"Settings which control the tracing strategy\"\nfr = \"Paramètres qui contrôlent la stratégie de traçage\"\ntr = \"İzleme stratejisini kontrol eden ayarlar\"\nit = \"Impostazioni che controllano la strategia di tracciamento\"\npt = \"Configurações que controlam a estratégia de rastreamento\"\nzh = \"跟踪策略设置\"\nzh-TW = \"追蹤策略設定\"\nsv = \"Inställningar som styr spårningsstrategin\"\nru = \"Настройки, которые контролируют стратегию трассировки\"\nes = \"Configuraciones que controlan la estrategia de rastreo\"\nde = \"Einstellungen, die die Tracing-Strategie steuern\"\n\n[settings_tab_dns_desc]\nen = \"Settings which control how DNS lookups are performed\"\nfr = \"Paramètres qui contrôlent la façon dont les recherches DNS sont effectuées\"\ntr = \"DNS aramalarının nasıl yapıldığını kontrol eden ayarlar\"\nit = \"Impostazioni che controllano come vengono eseguite le ricerche DNS\"\npt = \"Configurações que controlam como as pesquisas DNS são realizadas\"\nzh = \"DNS 查询设置\"\nzh-TW = \"DNS 查詢設定\"\nsv = \"Inställningar som styr hur DNS-uppslag utförs\"\nru = \"Настройки, которые контролируют, как выполняются DNS-запросы\"\nes = \"Configuraciones que controlan cómo se realizan las búsquedas de DNS\"\nde = \"Einstellungen, die steuern, wie DNS-Lookups durchgeführt werden\"\n\n[settings_tab_geoip_desc]\nen = \"Settings relating to GeoIp\"\nfr = \"Paramètres relatifs à GeoIp\"\ntr = \"GeoIp ile ilgili ayarlar\"\nit = \"Impostazioni relative a GeoIp\"\npt = \"Configurações relacionadas ao GeoIp\"\nzh = \"GeoIp 设置\"\nzh-TW = \"GeoIp 設定\"\nsv = \"Inställningar som rör GeoIp\"\nru = \"Настройки, касающиеся GeoIp\"\nes = \"Configuraciones relacionadas con GeoIp\"\nde = \"Einstellungen im Zusammenhang mit GeoIp\"\n\n[settings_tab_bindings_desc]\nen = \"Tui key bindings\"\nfr = \"Raccourcis clavier Tui\"\ntr = \"Tui tuş ayarları\"\nit = \"Collegamenti chiave Tui\"\npt = \"Atalhos de teclado Tui\"\nzh = \"按键绑定设置\"\nzh-TW = \"按鍵綁定設定\"\nsv = \"Tui-kortkommando\"\nru = \"Привязки клавиш Tui\"\nes = \"Atajos de teclado Tui\"\nde = \"Tui-Tastenbelegungen\"\n\n[settings_tab_theme_desc]\nen = \"Tui theme colors\"\nfr = \"Couleurs du thème Tui\"\ntr = \"Tui tema renkleri\"\nit = \"Colori del tema Tui\"\npt = \"Cores do tema Tui\"\nzh = \"主题颜色设置\"\nzh-TW = \"主題顏色設定\"\nsv = \"Tui-temafärger\"\nru = \"Цвета темы Tui\"\nes = \"Colores del tema Tui\"\nde = \"Tui-Themefarben\"\n\n[settings_tab_columns_desc]\nen = \"Tui table columns.  Press [%{c}] to toggle a column on or off and use the [%{d}] and [%{u}] keys to change the column order.\"\nfr = \"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.\"\ntr = \"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.\"\nit = \"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.\"\npt = \"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.\"\nzh = \"终端用户界面表格列。按 [%{c}] 切换列的显示和隐藏，使用 [%{d}] 和 [%{u}] 键更改列的顺序。\"\nzh-TW = \"終端使用者介面表格欄。按 [%{c}] 切換欄的顯示和隱藏，使用 [%{d}] 和 [%{u}] 鍵更改欄的順序。\"\nsv = \"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.\"\nru = \"Столбцы таблицы Tui. Нажмите [%{c}], чтобы включить или отключить столбец, и используйте клавиши [%{d}] и [%{u}], чтобы изменить порядок столбцов.\"\nes = \"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.\"\nde = \"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.\"\n\n[settings_table_header_setting]\nen = \"Setting\"\nfr = \"Paramètres\"\ntr = \"Ayar\"\nit = \"Impostazione\"\npt = \"Configuração\"\nzh = \"设置\"\nzh-TW = \"設定\"\nsv = \"Inställning\"\nru = \"Настройка\"\nes = \"Configuración\"\nde = \"Einstellung\"\n\n[settings_table_header_value]\nen = \"Value\"\nfr = \"Valeur\"\ntr = \"Değer\"\nit = \"Valore\"\npt = \"Valor\"\nzh = \"值\"\nzh-TW = \"值\"\nsv = \"Värde\"\nru = \"Значение\"\nes = \"Valor\"\nde = \"Wert\"\n\n[column_host]\nen = \"Host\"\nfr = \"Hôte\"\ntr = \"Ana bilgisayar\"\nit = \"Host\"\npt = \"Host\"\nzh = \"主机\"\nzh-TW = \"主機\"\nsv = \"Värd\"\nru = \"Хост\"\nes = \"Host\"\nde = \"Host\"\n\n[column_loss_pct]\nen = \"Loss%\"\nfr = \"% Perdus\"\ntr = \"Kayıp%\"\nit = \"Persi%\"\npt = \"% Perdidos\"\nzh = \"丢包率\"\nzh-TW = \"封包遺失率\"\nsv = \"Förlust%\"\nru = \"Потери%\"\nes = \"% Perdidos\"\nde = \"Verlust%\"\n\n[column_snd]\nen = \"Snd\"\nfr = \"Envoyés\"\ntr = \"Gönderilen\"\nit = \"Snd\"\npt = \"Enviados\"\nzh = \"发出\"\nzh-TW = \"發送\"\nsv = \"Skickat\"\nru = \"Отпр\"\nes = \"Enviados\"\nde = \"Snd\"\n\n[column_recv]\nen = \"Recv\"\nfr = \"Reçus\"\ntr = \"Alınan\"\nit = \"Recv\"\npt = \"Recebidos\"\nzh = \"接收\"\nzh-TW = \"接收\"\nsv = \"Mottagna\"\nru = \"Получ\"\nes = \"Recibidos\"\nde = \"Recv\"\n\n[column_last]\nen = \"Last\"\nfr = \"Dernier\"\ntr = \"Son\"\nit = \"Ultimo\"\npt = \"Último\"\nzh = \"最后\"\nzh-TW = \"最後\"\nsv = \"Senast\"\nru = \"Посл\"\nes = \"Último\"\nde = \"Letzte\"\n\n[column_avg]\nen = \"Avg\"\nfr = \"Moyenne\"\ntr = \"Ort\"\nit = \"Media\"\npt = \"Média\"\nzh = \"平均\"\nzh-TW = \"平均\"\nsv = \"Genomsnitt\"\nru = \"Сред\"\nes = \"Prom\"\nde = \"Durchschnitt\"\n\n[column_best]\nen = \"Best\"\nfr = \"Meilleur\"\ntr = \"En iyi\"\nit = \"Migliore\"\npt = \"Melhor\"\nzh = \"最佳\"\nzh-TW = \"最佳\"\nsv = \"Bäst\"\nru = \"Луч\"\nes = \"Mejor\"\nde = \"Beste\"\n\n[column_wrst]\nen = \"Wrst\"\nfr = \"Pire\"\ntr = \"En kötü\"\nit = \"Peggiore\"\npt = \"Pior\"\nzh = \"最差\"\nzh-TW = \"最差\"\nsv = \"Sämst\"\nru = \"Худ\"\nes = \"Peor\"\nde = \"Schlechteste\"\n\n[column_stdev]\nen = \"StDev\"\nfr = \"ÉcTyp\"\ntr = \"StDev\"\nit = \"StDev\"\npt = \"DesvPad\"\nzh = \"标准差\"\nzh-TW = \"標準差\"\nsv = \"StDev\"\nru = \"СКО\"\nes = \"DesvE\"\nde = \"StdAbw\"\n\n[column_sts]\nen = \"Sts\"\nfr = \"État\"\ntr = \"Sts\"\nit = \"Stato\"\npt = \"Est\"\nzh = \"状态\"\nzh-TW = \"狀態\"\nsv = \"Sts\"\nru = \"Статус\"\nes = \"Est\"\nde = \"Sts\"\n\n[column_jttr]\nen = \"Jttr\"\nfr = \"Gigue\"\ntr = \"Jttr\"\nit = \"Jttr\"\npt = \"Jttr\"\nzh = \"抖动\"\nzh-TW = \"抖動\"\nsv = \"Jttr\"\nru = \"Джитр\"\nes = \"Jttr\"\nde = \"Jttr\"\n\n[column_javg]\nen = \"Javg\"\nfr = \"GigMoy\"\ntr = \"Javg\"\nit = \"Javg\"\npt = \"Javg\"\nzh = \"均抖\"\nzh-TW = \"均抖\"\nsv = \"Javg\"\nru = \"СреднДжитр\"\nes = \"PromJit\"\nde = \"Javg\"\n\n[column_jmax]\nen = \"Jmax\"\nfr = \"GigMax\"\ntr = \"Jmax\"\nit = \"Jmax\"\npt = \"Jmax\"\nzh = \"最大抖\"\nzh-TW = \"最大抖\"\nsv = \"Jmax\"\nru = \"МаксДжитр\"\nes = \"JitMax\"\nde = \"Jmax\"\n\n[column_jint]\nen = \"Jint\"\nfr = \"GigInt\"\ntr = \"Jint\"\nit = \"Jint\"\npt = \"Jint\"\nzh = \"抖动间隔\"\nzh-TW = \"抖動間隔\"\nsv = \"Jint\"\nru = \"ИнтДжитр\"\nes = \"JitInt\"\nde = \"Jint\"\n\n[column_sprt]\nen = \"Sprt\"\nfr = \"Psrc\"\ntr = \"Sprt\"\nit = \"Sprt\"\nzh = \"源端\"\nzh-TW = \"來源端\"\nsv = \"Sprt\"\nru = \"Исх\"\nes = \"Sprt\"\nde = \"Sprt\"\n\n[column_dprt]\nen = \"Dprt\"\nfr = \"Pdest\"\ntr = \"Dprt\"\nit = \"Dprt\"\npt = \"Dprt\"\nzh = \"目标\"\nzh-TW = \"目標\"\nsv = \"Dprt\"\nru = \"Назн\"\nes = \"Dprt\"\nde = \"Dprt\"\n\n[column_seq]\nen = \"Seq\"\nfr = \"Seq\"\ntr = \"Seq\"\nit = \"Seq\"\npt = \"Seq\"\nzh = \"序列\"\nzh-TW = \"序列\"\nsv = \"Seq\"\nru = \"Посл\"\nes = \"Seq\"\nde = \"Seq\"\n\n[column_type]\nen = \"Type\"\nfr = \"Type\"\ntr = \"Type\"\nit = \"Tipo\"\npt = \"Tipo\"\nzh = \"类型\"\nzh-TW = \"類型\"\nsv = \"Typ\"\nru = \"Тип\"\nes = \"Tipo\"\nde = \"Typ\"\n\n[column_code]\nen = \"Code\"\nfr = \"Code\"\ntr = \"Code\"\nit = \"Codice\"\npt = \"Código\"\nzh = \"代码\"\nzh-TW = \"代碼\"\nsv = \"Kod\"\nru = \"Код\"\nes = \"Código\"\nde = \"Code\"\n\n[column_nat]\nen = \"NAT\"\nfr = \"NAT\"\ntr = \"NAT\"\nit = \"NAT\"\npt = \"NAT\"\nzh = \"网络地址转换\"\nzh-TW = \"網路位址轉換\"\nsv = \"NAT\"\nru = \"NAT\"\nes = \"NAT\"\nde = \"NAT\"\n\n[column_fail]\nen = \"Fail\"\nfr = \"Échec\"\ntr = \"Başarısız\"\nit = \"Falliti\"\npt = \"Falha\"\nzh = \"失败\"\nzh-TW = \"失敗\"\nsv = \"Misslyckades\"\nru = \"Неуд\"\nes = \"Falló\"\nde = \"Fehlgeschlagen\"\n\n[column_floss]\nen = \"Floss\"\nfr = \"Floss\"\ntr = \"Floss\"\nit = \"Floss\"\npt = \"Floss\"\nzh = \"Floss\"\nzh-TW = \"Floss\"\nsv = \"Floss\"\nru = \"Floss\"\nes = \"Floss\"\nde = \"Floss\"\n\n[column_bloss]\nen = \"Bloss\"\nfr = \"Bloss\"\ntr = \"Bloss\"\nit = \"Bloss\"\npt = \"Bloss\"\nzh = \"Bloss\"\nzh-TW = \"Bloss\"\nsv = \"Bloss\"\nru = \"Bloss\"\nes = \"Bloss\"\nde = \"Bloss\"\n\n[column_floss_pct]\nen = \"Floss%\"\nfr = \"Floss%\"\ntr = \"Floss%\"\nit = \"Floss%\"\npt = \"Floss%\"\nzh = \"Floss%\"\nzh-TW = \"Floss%\"\nsv = \"Floss%\"\nru = \"Floss%\"\nes = \"Floss%\"\nde = \"Floss%\"\n\n[column_dscp]\nen = \"DSCP\"\nfr = \"DSCP\"\ntr = \"DSCP\"\nit = \"DSCP\"\npt = \"DSCP\"\nzh = \"DSCP\"\nzh-TW = \"DSCP\"\nsv = \"DSCP\"\nru = \"DSCP\"\nes = \"DSCP\"\nde = \"DSCP\"\n\n[column_ecn]\nen = \"ECN\"\nfr = \"ECN\"\ntr = \"ECN\"\nit = \"ECN\"\npt = \"ECN\"\nzh = \"ECN\"\nzh-TW = \"ECN\"\nsv = \"ECN\"\nru = \"ECN\"\nes = \"ECN\"\nde = \"ECN\"\n\n[column_asn]\nen = \"ASN\"\nfr = \"ASN\"\ntr = \"ASN\"\nit = \"ASN\"\npt = \"ASN\"\nzh = \"ASN\"\nzh-TW = \"ASN\"\nsv = \"ASN\"\nru = \"ASN\"\nes = \"ASN\"\nde = \"ASN\"\n"
  },
  {
    "path": "crates/trippy-tui/src/app.rs",
    "content": "use crate::config::{LogFormat, LogSpanEvents, Mode, TrippyConfig};\nuse crate::frontend::TuiConfig;\nuse crate::geoip::GeoIpLookup;\nuse crate::locale;\nuse crate::{frontend, report};\nuse anyhow::{Error, anyhow};\nuse std::net::IpAddr;\nuse tracing::instrument;\nuse tracing_chrome::{ChromeLayerBuilder, FlushGuard};\nuse tracing_subscriber::fmt::format::FmtSpan;\nuse tracing_subscriber::layer::SubscriberExt;\nuse tracing_subscriber::util::SubscriberInitExt;\nuse trippy_core::{Builder, Tracer};\nuse trippy_dns::{DnsResolver, Resolver};\nuse trippy_privilege::Privilege;\n\n/// Run the trippy application.\npub fn run_trippy(cfg: &TrippyConfig, pid: u16) -> anyhow::Result<()> {\n    let locale = locale::set_locale(cfg.tui_locale.as_deref());\n    let _guard = configure_logging(cfg);\n    tracing::debug!(?cfg);\n    let resolver = start_dns_resolver(cfg)?;\n    let geoip_lookup = create_geoip_lookup(cfg, &locale)?;\n    let addrs = resolve_targets(cfg, &resolver)?;\n    if addrs.is_empty() {\n        return Err(anyhow!(\n            \"failed to find any valid IP addresses for {} for address family {}\",\n            cfg.targets.join(\", \"),\n            cfg.addr_family,\n        ));\n    }\n    let traces = start_tracers(cfg, &addrs, pid)?;\n    Privilege::drop_privileges()?;\n    run_frontend(cfg, &locale, resolver, geoip_lookup, traces)\n}\n\n/// Start all tracers.\n#[instrument(skip(cfg), level = \"trace\")]\nfn start_tracers(\n    cfg: &TrippyConfig,\n    addrs: &[TargetInfo],\n    pid: u16,\n) -> anyhow::Result<Vec<TraceInfo>> {\n    addrs\n        .iter()\n        .enumerate()\n        .map(|(i, TargetInfo { hostname, addr })| {\n            start_tracer(cfg, hostname, *addr, pid + i as u16)\n        })\n        .collect::<anyhow::Result<Vec<_>>>()\n}\n\n/// Start a tracer to a given target.\n#[instrument(skip(cfg), level = \"trace\")]\nfn start_tracer(\n    cfg: &TrippyConfig,\n    target_host: &str,\n    target_addr: IpAddr,\n    trace_identifier: u16,\n) -> Result<TraceInfo, Error> {\n    let (tracer, _) = Builder::new(target_addr)\n        .interface(cfg.interface.clone())\n        .source_addr(cfg.source_addr)\n        .privilege_mode(cfg.privilege_mode)\n        .protocol(cfg.protocol)\n        .packet_size(cfg.packet_size)\n        .payload_pattern(cfg.payload_pattern)\n        .tos(cfg.tos)\n        .icmp_extension_parse_mode(cfg.icmp_extension_parse_mode)\n        .read_timeout(cfg.read_timeout)\n        .tcp_connect_timeout(cfg.min_round_duration)\n        .trace_identifier(trace_identifier)\n        .max_rounds(cfg.max_rounds)\n        .first_ttl(cfg.first_ttl)\n        .max_ttl(cfg.max_ttl)\n        .grace_duration(cfg.grace_duration)\n        .max_inflight(cfg.max_inflight)\n        .initial_sequence(cfg.initial_sequence)\n        .multipath_strategy(cfg.multipath_strategy)\n        .port_direction(cfg.port_direction)\n        .min_round_duration(cfg.min_round_duration)\n        .max_round_duration(cfg.max_round_duration)\n        .max_flows(cfg.max_flows())\n        .max_samples(cfg.max_samples)\n        .drop_privileges(true)\n        .build()?\n        .spawn()?;\n    Ok(make_trace_info(tracer, target_host.to_string()))\n}\n\n/// Run the TUI, stream or report.\n#[instrument(skip_all, level = \"trace\")]\nfn run_frontend(\n    args: &TrippyConfig,\n    locale: &str,\n    resolver: DnsResolver,\n    geoip_lookup: GeoIpLookup,\n    traces: Vec<TraceInfo>,\n) -> anyhow::Result<()> {\n    match args.mode {\n        Mode::Tui => frontend::run_frontend(\n            traces,\n            make_tui_config(args, locale.to_string()),\n            resolver,\n            geoip_lookup,\n        )?,\n        Mode::Stream => report::stream::report(&traces[0], &resolver)?,\n        Mode::Csv => report::csv::report(&traces[0], args.report_cycles, &resolver)?,\n        Mode::Json => report::json::report(&traces[0], args.report_cycles, &resolver)?,\n        Mode::Pretty => report::table::report_pretty(&traces[0], args.report_cycles, &resolver)?,\n        Mode::Markdown => report::table::report_md(&traces[0], args.report_cycles, &resolver)?,\n        Mode::Dot => report::dot::report(&traces[0], args.report_cycles)?,\n        Mode::Flows => report::flows::report(&traces[0], args.report_cycles)?,\n        Mode::Silent => report::silent::report(&traces[0], args.report_cycles)?,\n    }\n    Ok(())\n}\n\n/// Resolve targets.\n#[instrument(skip_all, level = \"trace\")]\nfn resolve_targets(cfg: &TrippyConfig, resolver: &DnsResolver) -> anyhow::Result<Vec<TargetInfo>> {\n    cfg.targets\n        .iter()\n        .flat_map(|target| match resolver.lookup(target) {\n            Ok(addrs) => addrs\n                .into_iter()\n                .enumerate()\n                .take_while(|(i, _)| if cfg.dns_resolve_all { true } else { *i == 0 })\n                .map(|(i, addr)| {\n                    let hostname = if cfg.dns_resolve_all {\n                        format!(\"{} [{}]\", target, i + 1)\n                    } else {\n                        target.clone()\n                    };\n                    Ok(TargetInfo { hostname, addr })\n                })\n                .collect::<Vec<_>>()\n                .into_iter(),\n            Err(e) => vec![Err(anyhow!(\"failed to resolve target: {target} ({e})\"))].into_iter(),\n        })\n        .collect::<anyhow::Result<Vec<_>>>()\n}\n\n/// Start the DNS resolver.\n#[instrument(skip_all, level = \"trace\")]\nfn start_dns_resolver(cfg: &TrippyConfig) -> anyhow::Result<DnsResolver> {\n    Ok(DnsResolver::start(trippy_dns::Config::new(\n        cfg.dns_resolve_method,\n        cfg.addr_family,\n        cfg.dns_timeout,\n        cfg.dns_ttl,\n    ))?)\n}\n\n#[instrument(skip_all, level = \"trace\")]\nfn create_geoip_lookup(cfg: &TrippyConfig, locale: &str) -> anyhow::Result<GeoIpLookup> {\n    if let Some(path) = cfg.geoip_mmdb_file.as_ref() {\n        GeoIpLookup::from_file(path, String::from(locale))\n    } else {\n        Ok(GeoIpLookup::empty())\n    }\n}\n\nfn configure_logging(cfg: &TrippyConfig) -> Option<FlushGuard> {\n    if cfg.verbose {\n        let fmt_span = match cfg.log_span_events {\n            LogSpanEvents::Off => FmtSpan::NONE,\n            LogSpanEvents::Active => FmtSpan::ACTIVE,\n            LogSpanEvents::Full => FmtSpan::FULL,\n        };\n        match cfg.log_format {\n            LogFormat::Compact => {\n                tracing_subscriber::fmt()\n                    .with_span_events(fmt_span)\n                    .with_env_filter(&cfg.log_filter)\n                    .compact()\n                    .init();\n            }\n            LogFormat::Pretty => {\n                tracing_subscriber::fmt()\n                    .with_span_events(fmt_span)\n                    .with_env_filter(&cfg.log_filter)\n                    .pretty()\n                    .init();\n            }\n            LogFormat::Json => {\n                tracing_subscriber::fmt()\n                    .with_span_events(fmt_span)\n                    .with_env_filter(&cfg.log_filter)\n                    .json()\n                    .init();\n            }\n            LogFormat::Chrome => {\n                let (chrome_layer, guard) = ChromeLayerBuilder::new()\n                    .writer(std::io::stdout())\n                    .include_args(true)\n                    .build();\n                tracing_subscriber::registry().with(chrome_layer).init();\n                return Some(guard);\n            }\n        }\n    }\n    None\n}\n\n/// Make the TUI configuration.\nfn make_tui_config(args: &TrippyConfig, locale: String) -> TuiConfig {\n    TuiConfig::new(\n        args.tui_refresh_rate,\n        args.tui_privacy_max_ttl,\n        args.tui_preserve_screen,\n        args.tui_address_mode,\n        args.dns_lookup_as_info,\n        args.tui_as_mode,\n        args.tui_icmp_extension_mode,\n        args.tui_geoip_mode,\n        args.tui_max_addrs,\n        args.tui_theme,\n        &args.tui_bindings,\n        &args.tui_custom_columns,\n        args.geoip_mmdb_file.clone(),\n        args.dns_resolve_all,\n        locale,\n        args.tui_timezone,\n    )\n}\n\n/// Make the per-trace information.\nconst fn make_trace_info(tracer: Tracer, target: String) -> TraceInfo {\n    TraceInfo::new(tracer, target)\n}\n\n/// Information about a `Trace` needed for the Tui, stream and reports.\n#[derive(Debug, Clone)]\npub struct TraceInfo {\n    pub data: Tracer,\n    pub target_hostname: String,\n}\n\nimpl TraceInfo {\n    #[must_use]\n    pub const fn new(data: Tracer, target_hostname: String) -> Self {\n        Self {\n            data,\n            target_hostname,\n        }\n    }\n}\n\n/// Information about a tracing target.\n#[derive(Debug, Clone)]\nstruct TargetInfo {\n    pub hostname: String,\n    pub addr: IpAddr,\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/config/binding.rs",
    "content": "use crate::config::file::ConfigBindings;\nuse anyhow::anyhow;\nuse crossterm::event::{KeyCode, KeyModifiers};\nuse serde::Deserialize;\nuse std::collections::HashMap;\nuse std::fmt::{Display, Formatter};\nuse std::str::FromStr;\nuse strum::{AsRefStr, EnumString, VariantNames};\n\n/// Tui keyboard bindings.\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub struct TuiBindings {\n    pub toggle_help: TuiKeyBinding,\n    pub toggle_help_alt: TuiKeyBinding,\n    pub toggle_settings: TuiKeyBinding,\n    pub toggle_settings_tui: TuiKeyBinding,\n    pub toggle_settings_trace: TuiKeyBinding,\n    pub toggle_settings_dns: TuiKeyBinding,\n    pub toggle_settings_geoip: TuiKeyBinding,\n    pub toggle_settings_bindings: TuiKeyBinding,\n    pub toggle_settings_theme: TuiKeyBinding,\n    pub toggle_settings_columns: TuiKeyBinding,\n    pub previous_hop: TuiKeyBinding,\n    pub next_hop: TuiKeyBinding,\n    pub previous_trace: TuiKeyBinding,\n    pub next_trace: TuiKeyBinding,\n    pub previous_hop_address: TuiKeyBinding,\n    pub next_hop_address: TuiKeyBinding,\n    pub address_mode_ip: TuiKeyBinding,\n    pub address_mode_host: TuiKeyBinding,\n    pub address_mode_both: TuiKeyBinding,\n    pub toggle_freeze: TuiKeyBinding,\n    pub toggle_chart: TuiKeyBinding,\n    pub toggle_map: TuiKeyBinding,\n    pub toggle_flows: TuiKeyBinding,\n    pub expand_privacy: TuiKeyBinding,\n    pub contract_privacy: TuiKeyBinding,\n    pub expand_hosts: TuiKeyBinding,\n    pub contract_hosts: TuiKeyBinding,\n    pub expand_hosts_max: TuiKeyBinding,\n    pub contract_hosts_min: TuiKeyBinding,\n    pub chart_zoom_in: TuiKeyBinding,\n    pub chart_zoom_out: TuiKeyBinding,\n    pub clear_trace_data: TuiKeyBinding,\n    pub clear_dns_cache: TuiKeyBinding,\n    pub clear_selection: TuiKeyBinding,\n    pub toggle_as_info: TuiKeyBinding,\n    pub toggle_hop_details: TuiKeyBinding,\n    pub quit: TuiKeyBinding,\n    pub quit_preserve_screen: TuiKeyBinding,\n}\n\nimpl Default for TuiBindings {\n    fn default() -> Self {\n        Self {\n            toggle_help: TuiKeyBinding::new(KeyCode::Char('h')),\n            toggle_help_alt: TuiKeyBinding::new(KeyCode::Char('?')),\n            toggle_settings: TuiKeyBinding::new(KeyCode::Char('s')),\n            toggle_settings_tui: TuiKeyBinding::new(KeyCode::Char('1')),\n            toggle_settings_trace: TuiKeyBinding::new(KeyCode::Char('2')),\n            toggle_settings_dns: TuiKeyBinding::new(KeyCode::Char('3')),\n            toggle_settings_geoip: TuiKeyBinding::new(KeyCode::Char('4')),\n            toggle_settings_bindings: TuiKeyBinding::new(KeyCode::Char('5')),\n            toggle_settings_theme: TuiKeyBinding::new(KeyCode::Char('6')),\n            toggle_settings_columns: TuiKeyBinding::new(KeyCode::Char('7')),\n            previous_hop: TuiKeyBinding::new(KeyCode::Up),\n            next_hop: TuiKeyBinding::new(KeyCode::Down),\n            previous_trace: TuiKeyBinding::new(KeyCode::Left),\n            next_trace: TuiKeyBinding::new(KeyCode::Right),\n            previous_hop_address: TuiKeyBinding::new(KeyCode::Char(',')),\n            next_hop_address: TuiKeyBinding::new(KeyCode::Char('.')),\n            address_mode_ip: TuiKeyBinding::new(KeyCode::Char('i')),\n            address_mode_host: TuiKeyBinding::new(KeyCode::Char('n')),\n            address_mode_both: TuiKeyBinding::new(KeyCode::Char('b')),\n            toggle_freeze: TuiKeyBinding::new_with_modifier(\n                KeyCode::Char('f'),\n                KeyModifiers::CONTROL,\n            ),\n            toggle_chart: TuiKeyBinding::new(KeyCode::Char('c')),\n            toggle_map: TuiKeyBinding::new(KeyCode::Char('m')),\n            toggle_flows: TuiKeyBinding::new(KeyCode::Char('f')),\n            expand_privacy: TuiKeyBinding::new(KeyCode::Char('p')),\n            contract_privacy: TuiKeyBinding::new(KeyCode::Char('o')),\n            expand_hosts: TuiKeyBinding::new(KeyCode::Char(']')),\n            contract_hosts: TuiKeyBinding::new(KeyCode::Char('[')),\n            expand_hosts_max: TuiKeyBinding::new(KeyCode::Char('}')),\n            contract_hosts_min: TuiKeyBinding::new(KeyCode::Char('{')),\n            chart_zoom_in: TuiKeyBinding::new(KeyCode::Char('=')),\n            chart_zoom_out: TuiKeyBinding::new(KeyCode::Char('-')),\n            clear_trace_data: TuiKeyBinding::new_with_modifier(\n                KeyCode::Char('r'),\n                KeyModifiers::CONTROL,\n            ),\n            clear_dns_cache: TuiKeyBinding::new_with_modifier(\n                KeyCode::Char('k'),\n                KeyModifiers::CONTROL,\n            ),\n            clear_selection: TuiKeyBinding::new(KeyCode::Esc),\n            toggle_as_info: TuiKeyBinding::new(KeyCode::Char('z')),\n            toggle_hop_details: TuiKeyBinding::new(KeyCode::Char('d')),\n            quit: TuiKeyBinding::new(KeyCode::Char('q')),\n            quit_preserve_screen: TuiKeyBinding::new_with_modifier(\n                KeyCode::Char('q'),\n                KeyModifiers::SHIFT,\n            ),\n        }\n    }\n}\n\nimpl TuiBindings {\n    /// Validate the bindings.\n    ///\n    /// Returns any duplicate bindings.\n    pub fn find_duplicates(&self) -> Vec<String> {\n        let (_, duplicates) = [\n            (self.toggle_help, TuiCommandItem::ToggleHelp),\n            (self.toggle_help_alt, TuiCommandItem::ToggleHelpAlt),\n            (self.toggle_settings, TuiCommandItem::ToggleSettings),\n            (self.toggle_settings_tui, TuiCommandItem::ToggleSettings),\n            (self.toggle_settings_trace, TuiCommandItem::ToggleSettings),\n            (self.toggle_settings_dns, TuiCommandItem::ToggleSettings),\n            (self.toggle_settings_geoip, TuiCommandItem::ToggleSettings),\n            (\n                self.toggle_settings_bindings,\n                TuiCommandItem::ToggleSettings,\n            ),\n            (self.toggle_settings_theme, TuiCommandItem::ToggleSettings),\n            (self.toggle_settings_columns, TuiCommandItem::ToggleSettings),\n            (self.previous_hop, TuiCommandItem::PreviousHop),\n            (self.next_hop, TuiCommandItem::NextHop),\n            (self.previous_trace, TuiCommandItem::PreviousTrace),\n            (self.next_trace, TuiCommandItem::NextTrace),\n            (\n                self.previous_hop_address,\n                TuiCommandItem::PreviousHopAddress,\n            ),\n            (self.next_hop_address, TuiCommandItem::NextHopAddress),\n            (self.address_mode_ip, TuiCommandItem::AddressModeIp),\n            (self.address_mode_host, TuiCommandItem::AddressModeHost),\n            (self.address_mode_both, TuiCommandItem::AddressModeBoth),\n            (self.toggle_freeze, TuiCommandItem::ToggleFreeze),\n            (self.toggle_chart, TuiCommandItem::ToggleChart),\n            (self.toggle_map, TuiCommandItem::ToggleMap),\n            (self.toggle_flows, TuiCommandItem::ToggleFlows),\n            (self.expand_privacy, TuiCommandItem::ExpandPrivacy),\n            (self.contract_privacy, TuiCommandItem::ContractPrivacy),\n            (self.expand_hosts, TuiCommandItem::ExpandHosts),\n            (self.expand_hosts_max, TuiCommandItem::ExpandHostsMax),\n            (self.contract_hosts, TuiCommandItem::ContractHosts),\n            (self.contract_hosts_min, TuiCommandItem::ContractHostsMin),\n            (self.chart_zoom_in, TuiCommandItem::ChartZoomIn),\n            (self.chart_zoom_out, TuiCommandItem::ChartZoomOut),\n            (self.clear_trace_data, TuiCommandItem::ClearTraceData),\n            (self.clear_dns_cache, TuiCommandItem::ClearDnsCache),\n            (self.clear_selection, TuiCommandItem::ClearSelection),\n            (self.toggle_as_info, TuiCommandItem::ToggleASInfo),\n            (self.toggle_hop_details, TuiCommandItem::ToggleHopDetails),\n            (self.quit, TuiCommandItem::Quit),\n            (\n                self.quit_preserve_screen,\n                TuiCommandItem::QuitPreserveScreen,\n            ),\n        ]\n        .iter()\n        .fold(\n            (HashMap::<TuiKeyBinding, TuiCommandItem>::new(), Vec::new()),\n            |(mut all, mut dups), (binding, item)| {\n                if let Some(existing) = all.get(binding) {\n                    dups.push(format!(\n                        \"{}: [{} and {}]\",\n                        binding,\n                        item.as_ref(),\n                        existing.as_ref()\n                    ));\n                } else {\n                    all.insert(*binding, *item);\n                }\n                (all, dups)\n            },\n        );\n        duplicates\n    }\n}\n\nimpl From<(HashMap<TuiCommandItem, TuiKeyBinding>, ConfigBindings)> for TuiBindings {\n    #[expect(clippy::too_many_lines, clippy::or_fun_call)]\n    fn from(value: (HashMap<TuiCommandItem, TuiKeyBinding>, ConfigBindings)) -> Self {\n        let (cmd_items, cfg) = value;\n        Self {\n            toggle_help: *cmd_items\n                .get(&TuiCommandItem::ToggleHelp)\n                .or(cfg.toggle_help.as_ref())\n                .unwrap_or(&Self::default().toggle_help),\n            toggle_help_alt: *cmd_items\n                .get(&TuiCommandItem::ToggleHelpAlt)\n                .or(cfg.toggle_help_alt.as_ref())\n                .unwrap_or(&Self::default().toggle_help_alt),\n            toggle_settings: *cmd_items\n                .get(&TuiCommandItem::ToggleSettings)\n                .or(cfg.toggle_settings.as_ref())\n                .unwrap_or(&Self::default().toggle_settings),\n            toggle_settings_tui: *cmd_items\n                .get(&TuiCommandItem::ToggleSettingsTui)\n                .or(cfg.toggle_settings_tui.as_ref())\n                .unwrap_or(&Self::default().toggle_settings_tui),\n            toggle_settings_trace: *cmd_items\n                .get(&TuiCommandItem::ToggleSettingsTrace)\n                .or(cfg.toggle_settings_trace.as_ref())\n                .unwrap_or(&Self::default().toggle_settings_trace),\n            toggle_settings_dns: *cmd_items\n                .get(&TuiCommandItem::ToggleSettingsDns)\n                .or(cfg.toggle_settings_dns.as_ref())\n                .unwrap_or(&Self::default().toggle_settings_dns),\n            toggle_settings_geoip: *cmd_items\n                .get(&TuiCommandItem::ToggleSettingsGeoip)\n                .or(cfg.toggle_settings_geoip.as_ref())\n                .unwrap_or(&Self::default().toggle_settings_geoip),\n            toggle_settings_bindings: *cmd_items\n                .get(&TuiCommandItem::ToggleSettingsBindings)\n                .or(cfg.toggle_settings_bindings.as_ref())\n                .unwrap_or(&Self::default().toggle_settings_bindings),\n            toggle_settings_theme: *cmd_items\n                .get(&TuiCommandItem::ToggleSettingsTheme)\n                .or(cfg.toggle_settings_theme.as_ref())\n                .unwrap_or(&Self::default().toggle_settings_theme),\n            toggle_settings_columns: *cmd_items\n                .get(&TuiCommandItem::ToggleSettingsColumns)\n                .or(cfg.toggle_settings_columns.as_ref())\n                .unwrap_or(&Self::default().toggle_settings_columns),\n            previous_hop: *cmd_items\n                .get(&TuiCommandItem::PreviousHop)\n                .or(cfg.previous_hop.as_ref())\n                .unwrap_or(&Self::default().previous_hop),\n            next_hop: *cmd_items\n                .get(&TuiCommandItem::NextHop)\n                .or(cfg.next_hop.as_ref())\n                .unwrap_or(&Self::default().next_hop),\n            previous_trace: *cmd_items\n                .get(&TuiCommandItem::PreviousTrace)\n                .or(cfg.previous_trace.as_ref())\n                .unwrap_or(&Self::default().previous_trace),\n            next_trace: *cmd_items\n                .get(&TuiCommandItem::NextTrace)\n                .or(cfg.next_trace.as_ref())\n                .unwrap_or(&Self::default().next_trace),\n            previous_hop_address: *cmd_items\n                .get(&TuiCommandItem::PreviousHopAddress)\n                .or(cfg.previous_hop_address.as_ref())\n                .unwrap_or(&Self::default().previous_hop_address),\n            next_hop_address: *cmd_items\n                .get(&TuiCommandItem::NextHopAddress)\n                .or(cfg.next_hop_address.as_ref())\n                .unwrap_or(&Self::default().next_hop_address),\n            address_mode_ip: *cmd_items\n                .get(&TuiCommandItem::AddressModeIp)\n                .or(cfg.address_mode_ip.as_ref())\n                .unwrap_or(&Self::default().address_mode_ip),\n            address_mode_host: *cmd_items\n                .get(&TuiCommandItem::AddressModeHost)\n                .or(cfg.address_mode_host.as_ref())\n                .unwrap_or(&Self::default().address_mode_host),\n            address_mode_both: *cmd_items\n                .get(&TuiCommandItem::AddressModeBoth)\n                .or(cfg.address_mode_both.as_ref())\n                .unwrap_or(&Self::default().address_mode_both),\n            toggle_freeze: *cmd_items\n                .get(&TuiCommandItem::ToggleFreeze)\n                .or(cfg.toggle_freeze.as_ref())\n                .unwrap_or(&Self::default().toggle_freeze),\n            toggle_chart: *cmd_items\n                .get(&TuiCommandItem::ToggleChart)\n                .or(cfg.toggle_chart.as_ref())\n                .unwrap_or(&Self::default().toggle_chart),\n            toggle_flows: *cmd_items\n                .get(&TuiCommandItem::ToggleFlows)\n                .or(cfg.toggle_flows.as_ref())\n                .unwrap_or(&Self::default().toggle_flows),\n            expand_privacy: *cmd_items\n                .get(&TuiCommandItem::ExpandPrivacy)\n                .or(cfg.expand_privacy.as_ref())\n                .unwrap_or(&Self::default().expand_privacy),\n            contract_privacy: *cmd_items\n                .get(&TuiCommandItem::ContractPrivacy)\n                .or(cfg.contract_privacy.as_ref())\n                .unwrap_or(&Self::default().contract_privacy),\n            toggle_map: *cmd_items\n                .get(&TuiCommandItem::ToggleMap)\n                .or(cfg.toggle_map.as_ref())\n                .unwrap_or(&Self::default().toggle_map),\n            expand_hosts: *cmd_items\n                .get(&TuiCommandItem::ExpandHosts)\n                .or(cfg.expand_hosts.as_ref())\n                .unwrap_or(&Self::default().expand_hosts),\n            contract_hosts: *cmd_items\n                .get(&TuiCommandItem::ContractHosts)\n                .or(cfg.contract_hosts.as_ref())\n                .unwrap_or(&Self::default().contract_hosts),\n            expand_hosts_max: *cmd_items\n                .get(&TuiCommandItem::ExpandHostsMax)\n                .or(cfg.expand_hosts_max.as_ref())\n                .unwrap_or(&Self::default().expand_hosts_max),\n            contract_hosts_min: *cmd_items\n                .get(&TuiCommandItem::ContractHostsMin)\n                .or(cfg.contract_hosts_min.as_ref())\n                .unwrap_or(&Self::default().contract_hosts_min),\n            chart_zoom_in: *cmd_items\n                .get(&TuiCommandItem::ChartZoomIn)\n                .or(cfg.chart_zoom_in.as_ref())\n                .unwrap_or(&Self::default().chart_zoom_in),\n            chart_zoom_out: *cmd_items\n                .get(&TuiCommandItem::ChartZoomOut)\n                .or(cfg.chart_zoom_out.as_ref())\n                .unwrap_or(&Self::default().chart_zoom_out),\n            clear_trace_data: *cmd_items\n                .get(&TuiCommandItem::ClearTraceData)\n                .or(cfg.clear_trace_data.as_ref())\n                .unwrap_or(&Self::default().clear_trace_data),\n            clear_dns_cache: *cmd_items\n                .get(&TuiCommandItem::ClearDnsCache)\n                .or(cfg.clear_dns_cache.as_ref())\n                .unwrap_or(&Self::default().clear_dns_cache),\n            clear_selection: *cmd_items\n                .get(&TuiCommandItem::ClearSelection)\n                .or(cfg.clear_selection.as_ref())\n                .unwrap_or(&Self::default().clear_selection),\n            toggle_as_info: *cmd_items\n                .get(&TuiCommandItem::ToggleASInfo)\n                .or(cfg.toggle_as_info.as_ref())\n                .unwrap_or(&Self::default().toggle_as_info),\n            toggle_hop_details: *cmd_items\n                .get(&TuiCommandItem::ToggleHopDetails)\n                .or(cfg.toggle_hop_details.as_ref())\n                .unwrap_or(&Self::default().toggle_hop_details),\n            quit: *cmd_items\n                .get(&TuiCommandItem::Quit)\n                .or(cfg.quit.as_ref())\n                .unwrap_or(&Self::default().quit),\n            quit_preserve_screen: *cmd_items\n                .get(&TuiCommandItem::QuitPreserveScreen)\n                .or(cfg.quit_preserve_screen.as_ref())\n                .unwrap_or(&Self::default().quit_preserve_screen),\n        }\n    }\n}\n\n/// Tui key binding.\n#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize)]\n#[serde(try_from = \"String\")]\npub struct TuiKeyBinding {\n    pub code: KeyCode,\n    pub modifier: KeyModifiers,\n}\n\nimpl TuiKeyBinding {\n    pub const fn new(code: KeyCode) -> Self {\n        Self {\n            code,\n            modifier: KeyModifiers::NONE,\n        }\n    }\n\n    pub const fn new_with_modifier(code: KeyCode, modifier: KeyModifiers) -> Self {\n        Self { code, modifier }\n    }\n}\n\nimpl TryFrom<String> for TuiKeyBinding {\n    type Error = anyhow::Error;\n\n    fn try_from(value: String) -> Result<Self, Self::Error> {\n        Self::try_from(value.as_ref())\n    }\n}\n\nimpl TryFrom<&str> for TuiKeyBinding {\n    type Error = anyhow::Error;\n\n    fn try_from(value: &str) -> Result<Self, Self::Error> {\n        const ALL_MODIFIERS: [(&str, KeyModifiers); 6] = [\n            (\"shift\", KeyModifiers::SHIFT),\n            (\"ctrl\", KeyModifiers::CONTROL),\n            (\"alt\", KeyModifiers::ALT),\n            (\"super\", KeyModifiers::SUPER),\n            (\"hyper\", KeyModifiers::HYPER),\n            (\"meta\", KeyModifiers::META),\n        ];\n        const ALL_SPECIAL_KEYS: [(&str, KeyCode); 16] = [\n            (\"backspace\", KeyCode::Backspace),\n            (\"enter\", KeyCode::Enter),\n            (\"left\", KeyCode::Left),\n            (\"right\", KeyCode::Right),\n            (\"up\", KeyCode::Up),\n            (\"down\", KeyCode::Down),\n            (\"home\", KeyCode::Home),\n            (\"end\", KeyCode::End),\n            (\"pageup\", KeyCode::PageUp),\n            (\"pagedown\", KeyCode::PageDown),\n            (\"tab\", KeyCode::Tab),\n            (\"backtab\", KeyCode::BackTab),\n            (\"delete\", KeyCode::Delete),\n            (\"insert\", KeyCode::Insert),\n            (\"null\", KeyCode::Null),\n            (\"esc\", KeyCode::Esc),\n        ];\n        fn parse_keycode(value: &str) -> anyhow::Result<KeyCode> {\n            Ok(if value.len() == 1 {\n                KeyCode::Char(char::from_str(value)?.to_ascii_lowercase())\n            } else {\n                ALL_SPECIAL_KEYS\n                    .iter()\n                    .find_map(|(keycode_str, keycode)| {\n                        if keycode_str.eq_ignore_ascii_case(value) {\n                            Some(*keycode)\n                        } else {\n                            None\n                        }\n                    })\n                    .ok_or_else(|| anyhow!(\"unknown key binding '{value}'\"))?\n            })\n        }\n        fn parse_modifiers(modifiers: &str) -> anyhow::Result<KeyModifiers> {\n            modifiers\n                .split('+')\n                .try_fold(KeyModifiers::NONE, |modifiers, token| {\n                    ALL_MODIFIERS\n                        .iter()\n                        .find_map(|(modifier_token, modifier)| {\n                            if modifier_token.eq_ignore_ascii_case(token) {\n                                Some(modifiers | *modifier)\n                            } else {\n                                None\n                            }\n                        })\n                        .ok_or_else(|| anyhow!(\"unknown modifier '{token}'\",))\n                })\n        }\n        match value.rsplit_once('+') {\n            Some((modifiers, value)) => Ok(Self {\n                code: parse_keycode(value)?,\n                modifier: parse_modifiers(modifiers)?,\n            }),\n            None => Ok(Self {\n                code: parse_keycode(value)?,\n                modifier: KeyModifiers::NONE,\n            }),\n        }\n    }\n}\n\n#[cfg(test)]\nmod binding_tests {\n    use super::*;\n    use test_case::test_case;\n\n    #[test_case(\"c\", KeyCode::Char('c'), KeyModifiers::NONE; \"char without any modifier\")]\n    #[test_case(\"1\", KeyCode::Char('1'), KeyModifiers::NONE; \"number without any modifier\")]\n    #[test_case(\",\", KeyCode::Char(','), KeyModifiers::NONE; \"punctuation without any modifier\")]\n    #[test_case(\"backspace\", KeyCode::Backspace, KeyModifiers::NONE; \"backspace without any modifier\")]\n    #[test_case(\"enter\", KeyCode::Enter, KeyModifiers::NONE; \"enter without any modifier\")]\n    #[test_case(\"left\", KeyCode::Left, KeyModifiers::NONE; \"left without any modifier\")]\n    #[test_case(\"right\", KeyCode::Right, KeyModifiers::NONE; \"right without any modifier\")]\n    #[test_case(\"up\", KeyCode::Up, KeyModifiers::NONE; \"up without any modifier\")]\n    #[test_case(\"down\", KeyCode::Down, KeyModifiers::NONE; \"down without any modifier\")]\n    #[test_case(\"home\", KeyCode::Home, KeyModifiers::NONE; \"home without any modifier\")]\n    #[test_case(\"end\", KeyCode::End, KeyModifiers::NONE; \"end without any modifier\")]\n    #[test_case(\"pageup\", KeyCode::PageUp, KeyModifiers::NONE; \"pageup without any modifier\")]\n    #[test_case(\"pagedown\", KeyCode::PageDown, KeyModifiers::NONE; \"pagedown without any modifier\")]\n    #[test_case(\"tab\", KeyCode::Tab, KeyModifiers::NONE; \"tab without any modifier\")]\n    #[test_case(\"backtab\", KeyCode::BackTab, KeyModifiers::NONE; \"backtab without any modifier\")]\n    #[test_case(\"delete\", KeyCode::Delete, KeyModifiers::NONE; \"delete without any modifier\")]\n    #[test_case(\"insert\", KeyCode::Insert, KeyModifiers::NONE; \"insert without any modifier\")]\n    #[test_case(\"null\", KeyCode::Null, KeyModifiers::NONE; \"null without any modifier\")]\n    #[test_case(\"esc\", KeyCode::Esc, KeyModifiers::NONE; \"escape without any modifier\")]\n    #[test_case(\"shift+c\", KeyCode::Char('c'), KeyModifiers::SHIFT; \"with shift modifier\")]\n    #[test_case(\"ctrl+i\", KeyCode::Char('i'), KeyModifiers::CONTROL; \"i with ctrl modifier\")]\n    #[test_case(\"shift+I\", KeyCode::Char('i'), KeyModifiers::SHIFT; \"I with shift modifier\")]\n    #[test_case(\"alt+c\", KeyCode::Char('c'), KeyModifiers::ALT; \"with alt modifier\")]\n    #[test_case(\"super+c\", KeyCode::Char('c'), KeyModifiers::SUPER; \"with super modifier\")]\n    #[test_case(\"hyper+c\", KeyCode::Char('c'), KeyModifiers::HYPER; \"with hyper modifier\")]\n    #[test_case(\"meta+c\", KeyCode::Char('c'), KeyModifiers::META; \"with meta modifier\")]\n    #[test_case(\"alt+shift+k\", KeyCode::Char('k'), KeyModifiers::ALT | KeyModifiers::SHIFT; \"with alt shift modifier\")]\n    #[test_case(\"ctrl+up\", KeyCode::Up, KeyModifiers::CONTROL; \"up with ctrl modifier\")]\n    #[test_case(\"shift+ctrl+alt+super+hyper+meta+k\", KeyCode::Char('k'), KeyModifiers::all(); \"with all modifiers\")]\n    fn test_key_binding(input: &str, code: KeyCode, modifiers: KeyModifiers) -> anyhow::Result<()> {\n        let binding = TuiKeyBinding::try_from(input)?;\n        assert_eq!(binding.code, code);\n        assert_eq!(binding.modifier, modifiers);\n        Ok(())\n    }\n\n    #[test]\n    fn test_unknown_modifier() {\n        let binding = TuiKeyBinding::try_from(\"foo+c\");\n        assert!(binding.is_err());\n        assert_eq!(&binding.unwrap_err().to_string(), \"unknown modifier 'foo'\");\n    }\n\n    #[test]\n    fn test_unknown_second_modifier() {\n        let binding = TuiKeyBinding::try_from(\"alt+foo+c\");\n        assert!(binding.is_err());\n        assert_eq!(&binding.unwrap_err().to_string(), \"unknown modifier 'foo'\");\n    }\n\n    #[test]\n    fn test_unknown_key() {\n        let binding = TuiKeyBinding::try_from(\"foo\");\n        assert!(binding.is_err());\n        assert_eq!(\n            &binding.unwrap_err().to_string(),\n            \"unknown key binding 'foo'\"\n        );\n    }\n}\n\nimpl Display for TuiKeyBinding {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        if self.modifier.contains(KeyModifiers::SHIFT) {\n            write!(f, \"shift+\")?;\n        }\n        if self.modifier.contains(KeyModifiers::CONTROL) {\n            write!(f, \"ctrl+\")?;\n        }\n        if self.modifier.contains(KeyModifiers::ALT) {\n            write!(f, \"alt+\")?;\n        }\n        if self.modifier.contains(KeyModifiers::SUPER) {\n            write!(f, \"super+\")?;\n        }\n        if self.modifier.contains(KeyModifiers::HYPER) {\n            write!(f, \"hyper+\")?;\n        }\n        if self.modifier.contains(KeyModifiers::META) {\n            write!(f, \"meta+\")?;\n        }\n        match self.code {\n            KeyCode::Backspace => write!(f, \"backspace\"),\n            KeyCode::Enter => write!(f, \"enter\"),\n            KeyCode::Left => write!(f, \"left\"),\n            KeyCode::Right => write!(f, \"right\"),\n            KeyCode::Up => write!(f, \"up\"),\n            KeyCode::Down => write!(f, \"down\"),\n            KeyCode::Home => write!(f, \"home\"),\n            KeyCode::End => write!(f, \"end\"),\n            KeyCode::PageUp => write!(f, \"pageup\"),\n            KeyCode::PageDown => write!(f, \"pagedown\"),\n            KeyCode::Tab => write!(f, \"tab\"),\n            KeyCode::BackTab => write!(f, \"backtab\"),\n            KeyCode::Delete => write!(f, \"delete\"),\n            KeyCode::Insert => write!(f, \"insert\"),\n            KeyCode::Char(c) => write!(f, \"{c}\"),\n            KeyCode::Null => write!(f, \"null\"),\n            KeyCode::Esc => write!(f, \"esc\"),\n            _ => write!(f, \"unknown\"),\n        }\n    }\n}\n\n/// A Tui command that can be bound to a key.\n#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, EnumString, VariantNames)]\n#[strum(serialize_all = \"kebab-case\")]\n#[derive(AsRefStr)]\npub enum TuiCommandItem {\n    /// Toggle the help dialog.\n    ToggleHelp,\n    /// Alternative command to toggle the help dialog.\n    ToggleHelpAlt,\n    /// Toggle the settings dialog.\n    ToggleSettings,\n    /// Toggle the TUI settings dialog tab.\n    ToggleSettingsTui,\n    /// Toggle the trace settings dialog tab.\n    ToggleSettingsTrace,\n    /// Toggle the DNS settings dialog tab.\n    ToggleSettingsDns,\n    /// Toggle the `GeoIp` settings dialog tab.\n    ToggleSettingsGeoip,\n    /// Toggle the bindings settings dialog tab.\n    ToggleSettingsBindings,\n    /// Toggle the theme settings dialog tab.\n    ToggleSettingsTheme,\n    /// Toggle the columns settings dialog tab.\n    ToggleSettingsColumns,\n    /// Move down to the next hop.\n    NextHop,\n    /// Move up to the previous hop.\n    PreviousHop,\n    /// Move right to the next trace.\n    NextTrace,\n    /// Move left to the previous trace.\n    PreviousTrace,\n    /// Move to the next hop address.\n    NextHopAddress,\n    /// Move to the previous hop address.\n    PreviousHopAddress,\n    /// Show IP address mode.\n    AddressModeIp,\n    /// Show hostname mode.\n    AddressModeHost,\n    /// Show hostname and IP address mode.\n    AddressModeBoth,\n    /// Toggle freezing the display.\n    ToggleFreeze,\n    /// Toggle the chart.\n    ToggleChart,\n    /// Toggle the map.\n    ToggleMap,\n    /// Toggle the flows panel.\n    ToggleFlows,\n    /// Toggle hop privacy mode.\n    ///\n    /// Deprecated: use `ExpandPrivacy` and `ContractPrivacy` instead.\n    #[strum(serialize = \"toggle-privacy\")]\n    DeprecatedTogglePrivacy,\n    /// Expand hop privacy.\n    ExpandPrivacy,\n    /// Contract hop privacy.\n    ContractPrivacy,\n    /// Expand hosts.\n    ExpandHosts,\n    /// Expand hosts to max.\n    ExpandHostsMax,\n    /// Contract hosts.\n    ContractHosts,\n    /// Contract hosts to min.\n    ContractHostsMin,\n    /// Zoom chart in.\n    ChartZoomIn,\n    /// Zoom chart out.\n    ChartZoomOut,\n    /// Clear all tracing data.\n    ClearTraceData,\n    /// Clear DNS cache.\n    ClearDnsCache,\n    /// Clear hop selection.\n    ClearSelection,\n    /// Toggle autonomous system (AS) info.\n    ToggleASInfo,\n    /// Toggle hop details.\n    ToggleHopDetails,\n    /// Quit the application.\n    Quit,\n    /// Quit the application and preserve the screen.\n    QuitPreserveScreen,\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/config/cmd.rs",
    "content": "use crate::config::binding::TuiCommandItem;\nuse crate::config::theme::TuiThemeItem;\nuse crate::config::{\n    AddressFamilyConfig, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, IcmpExtensionMode,\n    LogFormat, LogSpanEvents, Mode, MultipathStrategyConfig, ProtocolConfig, TuiColor,\n    TuiKeyBinding,\n};\nuse anyhow::anyhow;\nuse clap::Parser;\nuse clap_complete::Shell;\nuse std::net::IpAddr;\nuse std::str::FromStr;\nuse std::time::Duration;\n\n/// Trace a route to a host and record statistics\n#[expect(clippy::doc_markdown)]\n#[derive(Parser, Debug)]\n#[command(name = \"trip\", author, version, about, long_about = None, arg_required_else_help(true), styles=clap_cargo::style::CLAP_STYLING)]\npub struct Args {\n    /// A space delimited list of hostnames and IPs to trace\n    #[arg(required_unless_present_any([\"print_tui_theme_items\", \"print_tui_binding_commands\", \"print_config_template\", \"generate\", \"generate_man\", \"print_locales\"]), env = \"TRIP_TARGETS\")]\n    pub targets: Vec<String>,\n\n    /// Config file\n    #[arg(value_enum, short = 'c', long, value_hint = clap::ValueHint::FilePath, env = \"TRIP_CONFIG_FILE\")]\n    pub config_file: Option<String>,\n\n    /// Output mode [default: tui]\n    #[arg(value_enum, short = 'm', long, env = \"TRIP_MODE\")]\n    pub mode: Option<Mode>,\n\n    /// Trace without requiring elevated privileges on supported platforms [default: false]\n    #[arg(short = 'u', long, env = \"TRIP_UNPRIVILEGED\")]\n    pub unprivileged: bool,\n\n    /// Tracing protocol [default: icmp]\n    #[arg(value_enum, short = 'p', long, env = \"TRIP_PROTOCOL\")]\n    pub protocol: Option<ProtocolConfig>,\n\n    /// Trace using the UDP protocol\n    #[arg(\n        long,\n        conflicts_with = \"protocol\",\n        conflicts_with = \"tcp\",\n        conflicts_with = \"icmp\",\n        env = \"TRIP_UDP\"\n    )]\n    pub udp: bool,\n\n    /// Trace using the TCP protocol\n    #[arg(\n        long,\n        conflicts_with = \"protocol\",\n        conflicts_with = \"udp\",\n        conflicts_with = \"icmp\",\n        env = \"TRIP_TCP\"\n    )]\n    pub tcp: bool,\n\n    /// Trace using the ICMP protocol\n    #[arg(\n        long,\n        conflicts_with = \"protocol\",\n        conflicts_with = \"udp\",\n        conflicts_with = \"tcp\",\n        env = \"TRIP_ICMP\"\n    )]\n    pub icmp: bool,\n\n    /// The address family [default: system]\n    #[arg(value_enum, short = 'F', long, env = \"TRIP_ADDR_FAMILY\")]\n    pub addr_family: Option<AddressFamilyConfig>,\n\n    /// Use IPv4 only\n    #[arg(\n        short = '4',\n        long,\n        conflicts_with = \"ipv6\",\n        conflicts_with = \"addr_family\",\n        env = \"TRIP_IPV4\"\n    )]\n    pub ipv4: bool,\n\n    /// Use IPv6 only\n    #[arg(\n        short = '6',\n        long,\n        conflicts_with = \"ipv4\",\n        conflicts_with = \"addr_family\",\n        env = \"TRIP_IPV6\"\n    )]\n    pub ipv6: bool,\n\n    /// The target port (TCP & UDP only) [default: 80]\n    #[arg(long, short = 'P', env = \"TRIP_TARGET_PORT\")]\n    pub target_port: Option<u16>,\n\n    /// The source port (TCP & UDP only) [default: auto]\n    #[arg(long, short = 'S', env = \"TRIP_SOURCE_PORT\")]\n    pub source_port: Option<u16>,\n\n    /// The source IP address [default: auto]\n    #[arg(short = 'A', long, value_parser = parse_addr, conflicts_with = \"interface\", env = \"TRIP_SOURCE_ADDRESS\")]\n    pub source_address: Option<IpAddr>,\n\n    /// The network interface [default: auto]\n    #[arg(short = 'I', long, env = \"TRIP_INTERFACE\")]\n    pub interface: Option<String>,\n\n    /// The minimum duration of every round [default: 1s]\n    #[arg(short = 'i', long, value_parser = parse_duration, env = \"TRIP_MIN_ROUND_DURATION\")]\n    pub min_round_duration: Option<Duration>,\n\n    /// The maximum duration of every round [default: 1s]\n    #[arg(short = 'T', long, value_parser = parse_duration, env = \"TRIP_MAX_ROUND_DURATION\")]\n    pub max_round_duration: Option<Duration>,\n\n    /// The period of time to wait for additional ICMP responses after the target has responded\n    /// [default: 100ms]\n    #[arg(short = 'g', long, value_parser = parse_duration, env = \"TRIP_GRACE_DURATION\")]\n    pub grace_duration: Option<Duration>,\n\n    /// The initial sequence number [default: 33434]\n    #[arg(long, env = \"TRIP_INITIAL_SEQUENCE\")]\n    pub initial_sequence: Option<u16>,\n\n    /// The Equal-cost Multi-Path routing strategy (UDP only) [default: classic]\n    #[arg(value_enum, short = 'R', long, env = \"TRIP_MULTIPATH_STRATEGY\")]\n    pub multipath_strategy: Option<MultipathStrategyConfig>,\n\n    /// The maximum number of in-flight ICMP echo requests [default: 24]\n    #[arg(short = 'U', long, env = \"TRIP_MAX_INFLIGHT\")]\n    pub max_inflight: Option<u8>,\n\n    /// The TTL to start from [default: 1]\n    #[arg(short = 'f', long, env = \"TRIP_FIRST_TTL\")]\n    pub first_ttl: Option<u8>,\n\n    /// The maximum number of TTL hops [default: 64]\n    #[arg(short = 't', long, env = \"TRIP_MAX_TTL\")]\n    pub max_ttl: Option<u8>,\n\n    /// The size of IP packet to send (IP header + ICMP header + payload) [default: 84]\n    #[arg(long, env = \"TRIP_PACKET_SIZE\")]\n    pub packet_size: Option<u16>,\n\n    /// The repeating pattern in the payload of the ICMP packet [default: 0]\n    #[arg(long, env = \"TRIP_PAYLOAD_PATTERN\")]\n    pub payload_pattern: Option<u8>,\n\n    /// The TOS (i.e. DSCP+ECN) IP header value (IPv4 only) [default: 0]\n    #[arg(short = 'Q', long, env = \"TRIP_TOS\")]\n    pub tos: Option<u8>,\n\n    /// Parse ICMP extensions\n    #[arg(short = 'e', long, env = \"TRIP_ICMP_EXTENSIONS\")]\n    pub icmp_extensions: bool,\n\n    /// The socket read timeout [default: 10ms]\n    #[arg(long, value_parser = parse_duration, env = \"TRIP_READ_TIMEOUT\")]\n    pub read_timeout: Option<Duration>,\n\n    /// How to perform DNS queries [default: system]\n    #[arg(value_enum, short = 'r', long, env = \"TRIP_DNS_RESOLVE_METHOD\")]\n    pub dns_resolve_method: Option<DnsResolveMethodConfig>,\n\n    /// Trace to all IPs resolved from DNS lookup [default: false]\n    #[arg(short = 'y', long, env = \"TRIP_DNS_RESOLVE_ALL\")]\n    pub dns_resolve_all: bool,\n\n    /// The maximum time to wait to perform DNS queries [default: 5s]\n    #[arg(long, value_parser = parse_duration, env = \"TRIP_DNS_TIMEOUT\")]\n    pub dns_timeout: Option<Duration>,\n\n    /// The time-to-live (TTL) of DNS entries [default: 300s]\n    #[arg(long, value_parser = parse_duration, env = \"TRIP_DNS_TTL\")]\n    pub dns_ttl: Option<Duration>,\n\n    /// Lookup autonomous system (AS) information during DNS queries [default: false]\n    #[arg(long, short = 'z', env = \"TRIP_DNS_LOOKUP_AS_INFO\")]\n    pub dns_lookup_as_info: bool,\n\n    /// The maximum number of samples to record per hop [default: 256]\n    #[arg(long, short = 's', env = \"TRIP_MAX_SAMPLES\")]\n    pub max_samples: Option<usize>,\n\n    /// The maximum number of flows to record [default: 64]\n    #[arg(long, env = \"TRIP_MAX_FLOWS\")]\n    pub max_flows: Option<usize>,\n\n    /// How to render addresses [default: host]\n    #[arg(value_enum, short = 'a', long, env = \"TRIP_TUI_ADDRESS_MODE\")]\n    pub tui_address_mode: Option<AddressMode>,\n\n    /// How to render autonomous system (AS) information [default: asn]\n    #[arg(value_enum, long, env = \"TRIP_TUI_AS_MODE\")]\n    pub tui_as_mode: Option<AsMode>,\n\n    /// Custom columns to be displayed in the TUI hops table [default: holsravbwdt]\n    #[arg(long, env = \"TRIP_TUI_CUSTOM_COLUMNS\")]\n    pub tui_custom_columns: Option<String>,\n\n    /// How to render ICMP extensions [default: off]\n    #[arg(value_enum, long, env = \"TRIP_TUI_ICMP_EXTENSION_MODE\")]\n    pub tui_icmp_extension_mode: Option<IcmpExtensionMode>,\n\n    /// How to render GeoIp information [default: short]\n    #[arg(value_enum, long, env = \"TRIP_TUI_GEOIP_MODE\")]\n    pub tui_geoip_mode: Option<GeoIpMode>,\n\n    /// The maximum number of addresses to show per hop [default: auto]\n    #[arg(short = 'M', long, env = \"TRIP_TUI_MAX_ADDRS\")]\n    pub tui_max_addrs: Option<u8>,\n\n    /// Preserve the screen on exit [default: false]\n    #[arg(long, env = \"TRIP_TUI_PRESERVE_SCREEN\")]\n    pub tui_preserve_screen: bool,\n\n    /// The TUI refresh rate [default: 100ms]\n    #[arg(long, value_parser = parse_duration, env = \"TRIP_TUI_REFRESH_RATE\")]\n    pub tui_refresh_rate: Option<Duration>,\n\n    /// The maximum ttl of hops which will be masked for privacy [default: none]\n    ///\n    /// If set, the source IP address and hostname will also be hidden.\n    #[arg(long, env = \"TRIP_TUI_PRIVACY_MAX_TTL\")]\n    pub tui_privacy_max_ttl: Option<u8>,\n\n    /// The locale to use for the TUI [default: auto]\n    #[arg(long, env = \"TRIP_TUI_LOCALE\")]\n    pub tui_locale: Option<String>,\n\n    /// The timezone to use for the TUI [default: auto]\n    ///\n    /// The timezone must be a valid IANA timezone identifier.\n    #[arg(long, env = \"TRIP_TUI_TIMEZONE\")]\n    pub tui_timezone: Option<String>,\n\n    /// The TUI theme colors [item=color,item=color,..]\n    #[arg(long, value_delimiter(','), value_parser = parse_tui_theme_color_value, env = \"TRIP_TUI_THEME_COLORS\")]\n    pub tui_theme_colors: Vec<(TuiThemeItem, TuiColor)>,\n\n    /// Print all TUI theme items and exit\n    #[arg(long, env = \"TRIP_PRINT_TUI_THEME_ITEMS\")]\n    pub print_tui_theme_items: bool,\n\n    /// The TUI key bindings [command=key,command=key,..]\n    #[arg(long, value_delimiter(','), value_parser = parse_tui_binding_value, env = \"TRIP_TUI_KEY_BINDINGS\")]\n    pub tui_key_bindings: Vec<(TuiCommandItem, TuiKeyBinding)>,\n\n    /// Print all TUI commands that can be bound and exit\n    #[arg(long, env = \"TRIP_PRINT_TUI_BINDING_COMMANDS\")]\n    pub print_tui_binding_commands: bool,\n\n    /// The number of report cycles to run [default: 10]\n    #[arg(short = 'C', long, env = \"TRIP_REPORT_CYCLES\")]\n    pub report_cycles: Option<usize>,\n\n    /// The supported MaxMind or IPinfo GeoIp mmdb file\n    #[arg(short = 'G', long, value_hint = clap::ValueHint::FilePath, env = \"TRIP_GEOIP_MMDB_FILE\")]\n    pub geoip_mmdb_file: Option<String>,\n\n    /// Generate shell completion\n    #[arg(long, env = \"TRIP_GENERATE\")]\n    pub generate: Option<Shell>,\n\n    /// Generate ROFF man page\n    #[arg(long, env = \"TRIP_GENERATE_MAN\")]\n    pub generate_man: bool,\n\n    /// Print a template toml config file and exit\n    #[arg(long, env = \"TRIP_PRINT_CONFIG_TEMPLATE\")]\n    pub print_config_template: bool,\n\n    /// Print all available TUI locales and exit\n    #[arg(long, env = \"TRIP_PRINT_LOCALES\")]\n    pub print_locales: bool,\n\n    /// The debug log format [default: pretty]\n    #[arg(long, env = \"TRIP_LOG_FORMAT\")]\n    pub log_format: Option<LogFormat>,\n\n    /// The debug log filter [default: trippy=debug]\n    #[arg(long, env = \"TRIP_LOG_FILTER\")]\n    pub log_filter: Option<String>,\n\n    /// The debug log format [default: off]\n    #[arg(long, env = \"TRIP_LOG_SPAN_EVENTS\")]\n    pub log_span_events: Option<LogSpanEvents>,\n\n    /// Enable verbose debug logging\n    #[arg(short = 'v', long, default_value_t = false, env = \"TRIP_VERBOSE\")]\n    pub verbose: bool,\n}\n\nfn parse_tui_theme_color_value(value: &str) -> anyhow::Result<(TuiThemeItem, TuiColor)> {\n    let pos = value\n        .find('=')\n        .ok_or_else(|| anyhow!(\"invalid theme value: expected format `item=value`\"))?;\n    let item = TuiThemeItem::try_from(&value[..pos])?;\n    let color = TuiColor::try_from(&value[pos + 1..])?;\n    Ok((item, color))\n}\n\nfn parse_tui_binding_value(value: &str) -> anyhow::Result<(TuiCommandItem, TuiKeyBinding)> {\n    let pos = value\n        .find('=')\n        .ok_or_else(|| anyhow!(\"invalid binding value: expected format `item=value`\"))?;\n    let item = TuiCommandItem::try_from(&value[..pos])?;\n    let binding = TuiKeyBinding::try_from(&value[pos + 1..])?;\n    if item == TuiCommandItem::DeprecatedTogglePrivacy {\n        return Err(anyhow!(\n            \"toggle-privacy is deprecated, use expand-privacy and contract-privacy instead\"\n        ));\n    }\n    Ok((item, binding))\n}\n\nfn parse_duration(value: &str) -> anyhow::Result<Duration> {\n    Ok(humantime::parse_duration(value)?)\n}\n\nfn parse_addr(value: &str) -> anyhow::Result<IpAddr> {\n    Ok(IpAddr::from_str(value)?)\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/config/columns.rs",
    "content": "use anyhow::anyhow;\nuse itertools::Itertools;\nuse std::collections::HashSet;\nuse std::fmt::{Display, Formatter};\n\n/// The columns to display in the hops table of the TUI.\n#[derive(Debug, Clone, Eq, PartialEq)]\npub struct TuiColumns(pub Vec<TuiColumn>);\n\nimpl TryFrom<&str> for TuiColumns {\n    type Error = anyhow::Error;\n\n    fn try_from(value: &str) -> Result<Self, Self::Error> {\n        Ok(Self(\n            value\n                .chars()\n                .map(TuiColumn::try_from)\n                .collect::<Result<Vec<_>, Self::Error>>()?,\n        ))\n    }\n}\n\nimpl Default for TuiColumns {\n    fn default() -> Self {\n        Self::try_from(super::constants::DEFAULT_CUSTOM_COLUMNS).expect(\"custom columns\")\n    }\n}\n\nimpl TuiColumns {\n    /// Validate the columns.\n    ///\n    /// Returns any duplicate columns.\n    pub fn find_duplicates(&self) -> Vec<String> {\n        let (_, duplicates) = self.0.iter().fold(\n            (HashSet::<TuiColumn>::new(), Vec::new()),\n            |(mut all, mut dups), column| {\n                if all.iter().contains(column) {\n                    dups.push(column.to_string());\n                } else {\n                    all.insert(*column);\n                }\n                (all, dups)\n            },\n        );\n        duplicates\n    }\n}\n\n/// A TUI hops table column.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]\npub enum TuiColumn {\n    /// The ttl for a hop.\n    Ttl,\n    /// The hostname for a hostname.\n    Host,\n    /// The packet loss % for a hop.\n    LossPct,\n    /// The number of probes sent for a hop.\n    Sent,\n    /// The number of responses received for a hop.\n    Received,\n    /// The last RTT for a hop.\n    Last,\n    /// The rolling average RTT for a hop.\n    Average,\n    /// The best RTT for a hop.\n    Best,\n    /// The worst RTT for a hop.\n    Worst,\n    /// The stddev of RTT for a hop.\n    StdDev,\n    /// The status of a hop.\n    Status,\n    /// The current jitter i.e. round-trip difference with the last round-trip.\n    Jitter,\n    /// The average jitter time for all probes at this hop.\n    Javg,\n    /// The worst round-trip jitter time for all probes at this hop.\n    Jmax,\n    /// The smoothed jitter value for all probes at this hop.\n    Jinta,\n    /// The source port for last probe for this hop.\n    LastSrcPort,\n    /// The destination port for last probe for this hop.\n    LastDestPort,\n    /// The sequence number for the last probe for this hop.\n    LastSeq,\n    /// The icmp packet type for the last probe for this hop.\n    LastIcmpPacketType,\n    /// The icmp packet code for the last probe for this hop.\n    LastIcmpPacketCode,\n    /// The NAT detection status for the last probe for this hop.\n    LastNatStatus,\n    /// The number of probes that failed for a hop.\n    Failed,\n    /// The number of probes with forward loss for a hop.\n    Floss,\n    /// The number of probes with backward loss for a hop.\n    Bloss,\n    /// The forward loss % for a hop.\n    FlossPct,\n    /// The Differentiated Services Code Point of the Original Datagram for a hop.\n    Dscp,\n    /// The Explicit Congestion Notification of the Original Datagram for a hop.\n    Ecn,\n    /// The autonomous system number for a hop.\n    Asn,\n}\n\nimpl TryFrom<char> for TuiColumn {\n    type Error = anyhow::Error;\n\n    fn try_from(value: char) -> Result<Self, Self::Error> {\n        match value {\n            'h' => Ok(Self::Ttl),\n            'o' => Ok(Self::Host),\n            'l' => Ok(Self::LossPct),\n            's' => Ok(Self::Sent),\n            'r' => Ok(Self::Received),\n            'a' => Ok(Self::Last),\n            'v' => Ok(Self::Average),\n            'b' => Ok(Self::Best),\n            'w' => Ok(Self::Worst),\n            'd' => Ok(Self::StdDev),\n            't' => Ok(Self::Status),\n            'j' => Ok(Self::Jitter),\n            'g' => Ok(Self::Javg),\n            'x' => Ok(Self::Jmax),\n            'i' => Ok(Self::Jinta),\n            'S' => Ok(Self::LastSrcPort),\n            'P' => Ok(Self::LastDestPort),\n            'Q' => Ok(Self::LastSeq),\n            'T' => Ok(Self::LastIcmpPacketType),\n            'C' => Ok(Self::LastIcmpPacketCode),\n            'N' => Ok(Self::LastNatStatus),\n            'f' => Ok(Self::Failed),\n            'F' => Ok(Self::Floss),\n            'B' => Ok(Self::Bloss),\n            'D' => Ok(Self::FlossPct),\n            'K' => Ok(Self::Dscp),\n            'M' => Ok(Self::Ecn),\n            'A' => Ok(Self::Asn),\n            c => Err(anyhow!(format!(\"unknown column code: {c}\"))),\n        }\n    }\n}\n\nimpl Display for TuiColumn {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Ttl => write!(f, \"h\"),\n            Self::Host => write!(f, \"o\"),\n            Self::LossPct => write!(f, \"l\"),\n            Self::Sent => write!(f, \"s\"),\n            Self::Received => write!(f, \"r\"),\n            Self::Last => write!(f, \"a\"),\n            Self::Average => write!(f, \"v\"),\n            Self::Best => write!(f, \"b\"),\n            Self::Worst => write!(f, \"w\"),\n            Self::StdDev => write!(f, \"d\"),\n            Self::Status => write!(f, \"t\"),\n            Self::Jitter => write!(f, \"j\"),\n            Self::Javg => write!(f, \"g\"),\n            Self::Jmax => write!(f, \"x\"),\n            Self::Jinta => write!(f, \"i\"),\n            Self::LastSrcPort => write!(f, \"S\"),\n            Self::LastDestPort => write!(f, \"P\"),\n            Self::LastSeq => write!(f, \"Q\"),\n            Self::LastIcmpPacketType => write!(f, \"T\"),\n            Self::LastIcmpPacketCode => write!(f, \"C\"),\n            Self::LastNatStatus => write!(f, \"N\"),\n            Self::Failed => write!(f, \"f\"),\n            Self::Floss => write!(f, \"F\"),\n            Self::Bloss => write!(f, \"B\"),\n            Self::FlossPct => write!(f, \"D\"),\n            Self::Dscp => write!(f, \"K\"),\n            Self::Ecn => write!(f, \"M\"),\n            Self::Asn => write!(f, \"A\"),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use test_case::test_case;\n\n    ///Test for expected column matches to characters\n    #[test_case('h', TuiColumn::Ttl)]\n    #[test_case('o', TuiColumn::Host)]\n    #[test_case('l', TuiColumn::LossPct)]\n    #[test_case('s', TuiColumn::Sent)]\n    #[test_case('r', TuiColumn::Received)]\n    #[test_case('a', TuiColumn::Last)]\n    #[test_case('v', TuiColumn::Average)]\n    #[test_case('b', TuiColumn::Best)]\n    #[test_case('w', TuiColumn::Worst)]\n    #[test_case('d', TuiColumn::StdDev)]\n    #[test_case('t', TuiColumn::Status)]\n    #[test_case('A', TuiColumn::Asn)]\n    fn test_try_from_char_for_tui_column(c: char, t: TuiColumn) {\n        assert_eq!(TuiColumn::try_from(c).unwrap(), t);\n    }\n\n    ///Negative test for invalid characters\n    #[test_case('k' ; \"invalid k\")]\n    #[test_case('z' ; \"invalid z\")]\n    fn test_try_invalid_char_for_tui_column(c: char) {\n        // Negative test for an unknown character\n        assert!(TuiColumn::try_from(c).is_err());\n    }\n\n    ///Test for `TuiColumn` type match of Display\n    #[test_case(TuiColumn::Ttl, \"h\")]\n    #[test_case(TuiColumn::Host, \"o\")]\n    #[test_case(TuiColumn::LossPct, \"l\")]\n    #[test_case(TuiColumn::Sent, \"s\")]\n    #[test_case(TuiColumn::Received, \"r\")]\n    #[test_case(TuiColumn::Last, \"a\")]\n    #[test_case(TuiColumn::Average, \"v\")]\n    #[test_case(TuiColumn::Best, \"b\")]\n    #[test_case(TuiColumn::Worst, \"w\")]\n    #[test_case(TuiColumn::StdDev, \"d\")]\n    #[test_case(TuiColumn::Status, \"t\")]\n    #[test_case(TuiColumn::Asn, \"A\")]\n    fn test_display_formatting_for_tui_column(t: TuiColumn, letter: &'static str) {\n        assert_eq!(format!(\"{t}\"), letter);\n    }\n\n    #[test]\n    fn test_try_from_str_for_tui_columns() {\n        let valid_input = \"hol\";\n        let tui_columns = TuiColumns::try_from(valid_input).unwrap();\n        assert_eq!(\n            tui_columns,\n            TuiColumns(vec![TuiColumn::Ttl, TuiColumn::Host, TuiColumn::LossPct])\n        );\n\n        // Test for invalid characters in the input\n        let invalid_input = \"xyz\";\n        assert!(TuiColumns::try_from(invalid_input).is_err());\n    }\n\n    #[test]\n    fn test_default_for_tui_columns() {\n        let default_columns = TuiColumns::default();\n        assert_eq!(\n            default_columns,\n            TuiColumns(vec![\n                TuiColumn::Ttl,\n                TuiColumn::Host,\n                TuiColumn::LossPct,\n                TuiColumn::Sent,\n                TuiColumn::Received,\n                TuiColumn::Last,\n                TuiColumn::Average,\n                TuiColumn::Best,\n                TuiColumn::Worst,\n                TuiColumn::StdDev,\n                TuiColumn::Status\n            ])\n        );\n    }\n\n    #[test]\n    fn test_find_duplicates_for_tui_columns() {\n        let columns_with_duplicates = TuiColumns(vec![\n            TuiColumn::Ttl,\n            TuiColumn::Host,\n            TuiColumn::LossPct,\n            TuiColumn::Host, // Duplicate\n        ]);\n\n        let duplicates = columns_with_duplicates.find_duplicates();\n        assert_eq!(duplicates, vec![\"o\".to_string()]);\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/config/constants.rs",
    "content": "use crate::config::{\n    AddressFamilyConfig, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, IcmpExtensionMode,\n    LogFormat, LogSpanEvents, Mode,\n};\nuse std::time::Duration;\n\n/// The default value for `mode`.\npub const DEFAULT_MODE: Mode = Mode::Tui;\n\n/// The default value for `dns-resolve-all`.\npub const DEFAULT_DNS_RESOLVE_ALL: bool = false;\n\n/// The default value for `log-format`.\npub const DEFAULT_LOG_FORMAT: LogFormat = LogFormat::Pretty;\n\n/// The default value for `log-span-events`.\npub const DEFAULT_LOG_SPAN_EVENTS: LogSpanEvents = LogSpanEvents::Off;\n\n/// The default value for `log-filter`.\npub const DEFAULT_LOG_FILTER: &str = \"trippy=debug\";\n\n/// The default value for `tui-preserve-screen`.\npub const DEFAULT_TUI_PRESERVE_SCREEN: bool = false;\n\n/// The default value for `tui-as-mode`.\npub const DEFAULT_TUI_AS_MODE: AsMode = AsMode::Asn;\n\n/// The default value for `tui-custom-columns`.\npub const DEFAULT_CUSTOM_COLUMNS: &str = \"holsravbwdt\";\n\n/// The default value for `tui-icmp-extension-mode`.\npub const DEFAULT_TUI_ICMP_EXTENSION_MODE: IcmpExtensionMode = IcmpExtensionMode::Off;\n\n/// The default value for `tui-geoip-mode`.\npub const DEFAULT_TUI_GEOIP_MODE: GeoIpMode = GeoIpMode::Off;\n\n/// The default value for `tui-max-addrs`.\npub const DEFAULT_TUI_MAX_ADDRS: u8 = 0;\n\n/// The default value for `tui-address-mode`.\npub const DEFAULT_TUI_ADDRESS_MODE: AddressMode = AddressMode::Host;\n\n/// The default value for `tui-refresh-rate`.\npub const DEFAULT_TUI_REFRESH_RATE: Duration = Duration::from_millis(100);\n\n/// The default value for `dns-resolve-method`.\npub const DEFAULT_DNS_RESOLVE_METHOD: DnsResolveMethodConfig = DnsResolveMethodConfig::System;\n\n/// The default value for `addr-family`.\npub const DEFAULT_ADDR_FAMILY: AddressFamilyConfig = AddressFamilyConfig::System;\n\n/// The default value for `dns-lookup-as-info`.\npub const DEFAULT_DNS_LOOKUP_AS_INFO: bool = false;\n\n/// The default value for `dns-timeout`.\npub const DEFAULT_DNS_TIMEOUT: Duration = Duration::from_millis(5000);\n\n/// The default value for `dns-ttl`.\npub const DEFAULT_DNS_TTL: Duration = Duration::from_secs(300);\n\n/// The default value for `report-cycles`.\npub const DEFAULT_REPORT_CYCLES: usize = 10;\n\n/// The minimum TUI refresh rate.\npub const TUI_MIN_REFRESH_RATE_MS: Duration = Duration::from_millis(50);\n\n/// The maximum TUI refresh rate.\npub const TUI_MAX_REFRESH_RATE_MS: Duration = Duration::from_millis(1000);\n\n/// The minimum socket read timeout.\npub const MIN_READ_TIMEOUT_MS: Duration = Duration::from_millis(10);\n\n/// The maximum socket read timeout.\npub const MAX_READ_TIMEOUT_MS: Duration = Duration::from_millis(100);\n\n/// The minimum grace duration.\npub const MIN_GRACE_DURATION_MS: Duration = Duration::from_millis(10);\n\n/// The maximum grace duration.\npub const MAX_GRACE_DURATION_MS: Duration = Duration::from_millis(1000);\n\n/// The minimum IPv4 packet size we allow.\npub const MIN_PACKET_SIZE_IPV4: u16 = 28;\n\n/// The minimum IPv6 packet size we allow.\npub const MIN_PACKET_SIZE_IPV6: u16 = 48;\n\n/// The maximum packet size we allow.\npub const MAX_PACKET_SIZE: u16 = 1024;\n"
  },
  {
    "path": "crates/trippy-tui/src/config/file.rs",
    "content": "use crate::config::binding::TuiKeyBinding;\nuse crate::config::theme::TuiColor;\nuse crate::config::{\n    AddressFamilyConfig, AddressMode, AsMode, DnsResolveMethodConfig, GeoIpMode, IcmpExtensionMode,\n    LogFormat, LogSpanEvents, Mode, MultipathStrategyConfig, ProtocolConfig,\n};\nuse anyhow::Context;\nuse encoding_rs_io::DecodeReaderBytes;\nuse etcetera::BaseStrategy;\nuse serde::Deserialize;\nuse std::fs::File;\nuse std::io::{BufReader, Read};\nuse std::net::IpAddr;\nuse std::path::Path;\nuse std::str::FromStr;\nuse std::time::Duration;\nuse trippy_core::defaults;\n\nconst DEFAULT_CONFIG_FILE: &str = \"trippy.toml\";\nconst DEFAULT_HIDDEN_CONFIG_FILE: &str = \".trippy.toml\";\n\n/// Read the config from the default location of user config for the platform.\n///\n/// Returns the parsed `Some(ConfigFile)` if the config file exists, `None` otherwise.\n///\n/// Trippy will attempt to locate a `trippy.toml` or `.trippy.toml`\n/// config file in one of the following locations:\n///     - the current directory\n///     - the user home directory\n///     - the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config`\n///     - the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy`\n///     - the Windows data directory (Windows only): `%APPDATA%`\n///\n/// Note that only the first config file found is used, no attempt is\n/// made to merge the values from multiple files.\npub fn read_default_config_file() -> anyhow::Result<Option<ConfigFile>> {\n    use etcetera::base_strategy as base;\n    if let Some(file) = read_files(\"\")? {\n        Ok(Some(file))\n    } else {\n        let basedirs = base::choose_base_strategy()?;\n        if let Some(file) = read_files(basedirs.home_dir())? {\n            Ok(Some(file))\n        } else if let Some(file) = read_files(basedirs.config_dir())? {\n            Ok(Some(file))\n        } else if let Some(file) = read_files(basedirs.config_dir().join(\"trippy\"))? {\n            Ok(Some(file))\n        } else {\n            Ok(None)\n        }\n    }\n}\n\n/// Read the config from the given path.\npub fn read_config_file<P: AsRef<Path>>(path: P) -> anyhow::Result<ConfigFile> {\n    let file = File::open(path.as_ref())\n        .with_context(|| format!(\"config file not found: {}\", path.as_ref().display()))?;\n    let mut decoder = DecodeReaderBytes::new(BufReader::new(file));\n    let mut dest = String::new();\n    decoder.read_to_string(&mut dest)?;\n    Ok(toml::from_str(&dest)?)\n}\n\nfn read_files<P: AsRef<Path>>(dir: P) -> anyhow::Result<Option<ConfigFile>> {\n    if let Some(file) = read_file(dir.as_ref(), DEFAULT_CONFIG_FILE)? {\n        Ok(Some(file))\n    } else if let Some(file) = read_file(dir.as_ref(), DEFAULT_HIDDEN_CONFIG_FILE)? {\n        Ok(Some(file))\n    } else {\n        Ok(None)\n    }\n}\n\nfn read_file<P: AsRef<Path>>(dir: P, file: &str) -> anyhow::Result<Option<ConfigFile>> {\n    let path = dir.as_ref().join(file);\n    if path.exists() {\n        Ok(Some(read_config_file(path)?))\n    } else {\n        Ok(None)\n    }\n}\n\n#[derive(Debug, Eq, PartialEq, Deserialize)]\n#[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\npub struct ConfigFile {\n    pub trippy: Option<ConfigTrippy>,\n    pub strategy: Option<ConfigStrategy>,\n    pub theme_colors: Option<ConfigThemeColors>,\n    pub bindings: Option<ConfigBindings>,\n    pub tui: Option<ConfigTui>,\n    pub dns: Option<ConfigDns>,\n    pub report: Option<ConfigReport>,\n}\n\nimpl Default for ConfigFile {\n    fn default() -> Self {\n        Self {\n            trippy: Some(ConfigTrippy::default()),\n            strategy: Some(ConfigStrategy::default()),\n            theme_colors: Some(ConfigThemeColors::default()),\n            bindings: Some(ConfigBindings::default()),\n            tui: Some(ConfigTui::default()),\n            dns: Some(ConfigDns::default()),\n            report: Some(ConfigReport::default()),\n        }\n    }\n}\n\n#[derive(Debug, Eq, PartialEq, Deserialize)]\n#[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\npub struct ConfigTrippy {\n    pub mode: Option<Mode>,\n    pub unprivileged: Option<bool>,\n    pub log_format: Option<LogFormat>,\n    pub log_filter: Option<String>,\n    pub log_span_events: Option<LogSpanEvents>,\n}\n\nimpl Default for ConfigTrippy {\n    fn default() -> Self {\n        Self {\n            mode: Some(super::constants::DEFAULT_MODE),\n            unprivileged: Some(defaults::DEFAULT_PRIVILEGE_MODE.is_unprivileged()),\n            log_format: Some(super::constants::DEFAULT_LOG_FORMAT),\n            log_filter: Some(String::from(super::constants::DEFAULT_LOG_FILTER)),\n            log_span_events: Some(super::constants::DEFAULT_LOG_SPAN_EVENTS),\n        }\n    }\n}\n\n#[derive(Debug, Eq, PartialEq, Deserialize)]\n#[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\npub struct ConfigStrategy {\n    pub protocol: Option<ProtocolConfig>,\n    pub addr_family: Option<AddressFamilyConfig>,\n    pub target_port: Option<u16>,\n    pub source_port: Option<u16>,\n    #[serde(default)]\n    #[serde(deserialize_with = \"addr_deser\")]\n    pub source_address: Option<IpAddr>,\n    pub interface: Option<String>,\n    #[serde(default)]\n    #[serde(deserialize_with = \"humantime_deser\")]\n    pub min_round_duration: Option<Duration>,\n    #[serde(default)]\n    #[serde(deserialize_with = \"humantime_deser\")]\n    pub max_round_duration: Option<Duration>,\n    pub initial_sequence: Option<u16>,\n    pub multipath_strategy: Option<MultipathStrategyConfig>,\n    #[serde(default)]\n    #[serde(deserialize_with = \"humantime_deser\")]\n    pub grace_duration: Option<Duration>,\n    pub max_inflight: Option<u8>,\n    pub first_ttl: Option<u8>,\n    pub max_ttl: Option<u8>,\n    pub packet_size: Option<u16>,\n    pub payload_pattern: Option<u8>,\n    pub tos: Option<u8>,\n    pub icmp_extensions: Option<bool>,\n    #[serde(default)]\n    #[serde(deserialize_with = \"humantime_deser\")]\n    pub read_timeout: Option<Duration>,\n    pub max_samples: Option<usize>,\n    pub max_flows: Option<usize>,\n}\n\nimpl Default for ConfigStrategy {\n    fn default() -> Self {\n        Self {\n            protocol: Some(ProtocolConfig::from(defaults::DEFAULT_STRATEGY_PROTOCOL)),\n            addr_family: Some(super::constants::DEFAULT_ADDR_FAMILY),\n            target_port: None,\n            source_port: None,\n            source_address: None,\n            interface: None,\n            min_round_duration: Some(defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION),\n            max_round_duration: Some(defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION),\n            initial_sequence: Some(defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE),\n            multipath_strategy: Some(MultipathStrategyConfig::from(\n                defaults::DEFAULT_STRATEGY_MULTIPATH,\n            )),\n            grace_duration: Some(defaults::DEFAULT_STRATEGY_GRACE_DURATION),\n            max_inflight: Some(defaults::DEFAULT_STRATEGY_MAX_INFLIGHT),\n            first_ttl: Some(defaults::DEFAULT_STRATEGY_FIRST_TTL),\n            max_ttl: Some(defaults::DEFAULT_STRATEGY_MAX_TTL),\n            packet_size: Some(defaults::DEFAULT_STRATEGY_PACKET_SIZE),\n            payload_pattern: Some(defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN),\n            tos: Some(defaults::DEFAULT_STRATEGY_TOS),\n            icmp_extensions: Some(defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE.is_enabled()),\n            read_timeout: Some(defaults::DEFAULT_STRATEGY_READ_TIMEOUT),\n            max_samples: Some(defaults::DEFAULT_MAX_SAMPLES),\n            max_flows: Some(defaults::DEFAULT_MAX_FLOWS),\n        }\n    }\n}\n\n#[derive(Debug, Eq, PartialEq, Deserialize)]\n#[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\n#[expect(clippy::struct_field_names)]\npub struct ConfigDns {\n    pub dns_resolve_method: Option<DnsResolveMethodConfig>,\n    pub dns_resolve_all: Option<bool>,\n    pub dns_lookup_as_info: Option<bool>,\n    #[serde(default)]\n    #[serde(deserialize_with = \"humantime_deser\")]\n    pub dns_timeout: Option<Duration>,\n    #[serde(default)]\n    #[serde(deserialize_with = \"humantime_deser\")]\n    pub dns_ttl: Option<Duration>,\n}\n\nimpl Default for ConfigDns {\n    fn default() -> Self {\n        Self {\n            dns_resolve_method: Some(super::constants::DEFAULT_DNS_RESOLVE_METHOD),\n            dns_resolve_all: Some(super::constants::DEFAULT_DNS_RESOLVE_ALL),\n            dns_lookup_as_info: Some(super::constants::DEFAULT_DNS_LOOKUP_AS_INFO),\n            dns_timeout: Some(super::constants::DEFAULT_DNS_TIMEOUT),\n            dns_ttl: Some(super::constants::DEFAULT_DNS_TTL),\n        }\n    }\n}\n\n#[derive(Debug, Eq, PartialEq, Deserialize)]\n#[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\npub struct ConfigReport {\n    pub report_cycles: Option<usize>,\n}\n\nimpl Default for ConfigReport {\n    fn default() -> Self {\n        Self {\n            report_cycles: Some(super::constants::DEFAULT_REPORT_CYCLES),\n        }\n    }\n}\n\n#[derive(Debug, Eq, PartialEq, Deserialize)]\n#[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\npub struct ConfigTui {\n    pub tui_preserve_screen: Option<bool>,\n    #[serde(default)]\n    #[serde(deserialize_with = \"humantime_deser\")]\n    pub tui_refresh_rate: Option<Duration>,\n    pub tui_privacy_max_ttl: Option<u8>,\n    pub tui_address_mode: Option<AddressMode>,\n    pub tui_as_mode: Option<AsMode>,\n    pub tui_icmp_extension_mode: Option<IcmpExtensionMode>,\n    pub tui_geoip_mode: Option<GeoIpMode>,\n    pub tui_max_addrs: Option<u8>,\n    pub geoip_mmdb_file: Option<String>,\n    pub tui_custom_columns: Option<String>,\n    pub tui_locale: Option<String>,\n    pub tui_timezone: Option<String>,\n    #[serde(rename = \"tui-max-samples\")]\n    pub deprecated_tui_max_samples: Option<usize>,\n    #[serde(rename = \"tui-max-flows\")]\n    pub deprecated_tui_max_flows: Option<usize>,\n}\n\nimpl Default for ConfigTui {\n    fn default() -> Self {\n        Self {\n            tui_preserve_screen: Some(super::constants::DEFAULT_TUI_PRESERVE_SCREEN),\n            tui_refresh_rate: Some(super::constants::DEFAULT_TUI_REFRESH_RATE),\n            tui_privacy_max_ttl: None,\n            tui_address_mode: Some(super::constants::DEFAULT_TUI_ADDRESS_MODE),\n            tui_as_mode: Some(super::constants::DEFAULT_TUI_AS_MODE),\n            tui_custom_columns: Some(String::from(super::constants::DEFAULT_CUSTOM_COLUMNS)),\n            tui_icmp_extension_mode: Some(super::constants::DEFAULT_TUI_ICMP_EXTENSION_MODE),\n            tui_geoip_mode: Some(super::constants::DEFAULT_TUI_GEOIP_MODE),\n            tui_max_addrs: Some(super::constants::DEFAULT_TUI_MAX_ADDRS),\n            tui_locale: None,\n            tui_timezone: None,\n            geoip_mmdb_file: None,\n            deprecated_tui_max_samples: None,\n            deprecated_tui_max_flows: None,\n        }\n    }\n}\n\n#[derive(Debug, Eq, PartialEq, Deserialize)]\n#[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\n#[expect(clippy::struct_field_names)]\npub struct ConfigThemeColors {\n    pub bg_color: Option<TuiColor>,\n    pub border_color: Option<TuiColor>,\n    pub text_color: Option<TuiColor>,\n    pub tab_text_color: Option<TuiColor>,\n    pub hops_table_header_bg_color: Option<TuiColor>,\n    pub hops_table_header_text_color: Option<TuiColor>,\n    pub hops_table_row_active_text_color: Option<TuiColor>,\n    pub hops_table_row_inactive_text_color: Option<TuiColor>,\n    pub hops_chart_selected_color: Option<TuiColor>,\n    pub hops_chart_unselected_color: Option<TuiColor>,\n    pub hops_chart_axis_color: Option<TuiColor>,\n    pub frequency_chart_bar_color: Option<TuiColor>,\n    pub frequency_chart_text_color: Option<TuiColor>,\n    pub flows_chart_bar_selected_color: Option<TuiColor>,\n    pub flows_chart_bar_unselected_color: Option<TuiColor>,\n    pub flows_chart_text_current_color: Option<TuiColor>,\n    pub flows_chart_text_non_current_color: Option<TuiColor>,\n    pub samples_chart_color: Option<TuiColor>,\n    pub samples_chart_lost_color: Option<TuiColor>,\n    pub help_dialog_bg_color: Option<TuiColor>,\n    pub help_dialog_text_color: Option<TuiColor>,\n    pub settings_dialog_bg_color: Option<TuiColor>,\n    pub settings_tab_text_color: Option<TuiColor>,\n    pub settings_table_header_text_color: Option<TuiColor>,\n    pub settings_table_header_bg_color: Option<TuiColor>,\n    pub settings_table_row_text_color: Option<TuiColor>,\n    pub map_world_color: Option<TuiColor>,\n    pub map_radius_color: Option<TuiColor>,\n    pub map_selected_color: Option<TuiColor>,\n    pub map_info_panel_border_color: Option<TuiColor>,\n    pub map_info_panel_bg_color: Option<TuiColor>,\n    pub map_info_panel_text_color: Option<TuiColor>,\n    pub info_bar_bg_color: Option<TuiColor>,\n    pub info_bar_text_color: Option<TuiColor>,\n}\n\nimpl Default for ConfigThemeColors {\n    fn default() -> Self {\n        let theme = super::theme::TuiTheme::default();\n        Self {\n            bg_color: Some(theme.bg),\n            border_color: Some(theme.border),\n            text_color: Some(theme.text),\n            tab_text_color: Some(theme.tab_text),\n            hops_table_header_bg_color: Some(theme.hops_table_header_bg),\n            hops_table_header_text_color: Some(theme.hops_table_header_text),\n            hops_table_row_active_text_color: Some(theme.hops_table_row_active_text),\n            hops_table_row_inactive_text_color: Some(theme.hops_table_row_inactive_text),\n            hops_chart_selected_color: Some(theme.hops_chart_selected),\n            hops_chart_unselected_color: Some(theme.hops_chart_unselected),\n            hops_chart_axis_color: Some(theme.hops_chart_axis),\n            frequency_chart_bar_color: Some(theme.frequency_chart_bar),\n            frequency_chart_text_color: Some(theme.frequency_chart_text),\n            flows_chart_bar_selected_color: Some(theme.flows_chart_bar_selected),\n            flows_chart_bar_unselected_color: Some(theme.flows_chart_bar_unselected),\n            flows_chart_text_current_color: Some(theme.flows_chart_text_current),\n            flows_chart_text_non_current_color: Some(theme.flows_chart_text_non_current),\n            samples_chart_color: Some(theme.samples_chart),\n            samples_chart_lost_color: Some(theme.samples_chart_lost),\n            help_dialog_bg_color: Some(theme.help_dialog_bg),\n            help_dialog_text_color: Some(theme.help_dialog_text),\n            settings_dialog_bg_color: Some(theme.settings_dialog_bg),\n            settings_tab_text_color: Some(theme.settings_tab_text),\n            settings_table_header_text_color: Some(theme.settings_table_header_text),\n            settings_table_header_bg_color: Some(theme.settings_table_header_bg),\n            settings_table_row_text_color: Some(theme.settings_table_row_text),\n            map_world_color: Some(theme.map_world),\n            map_radius_color: Some(theme.map_radius),\n            map_selected_color: Some(theme.map_selected),\n            map_info_panel_border_color: Some(theme.map_info_panel_border),\n            map_info_panel_bg_color: Some(theme.map_info_panel_bg),\n            map_info_panel_text_color: Some(theme.map_info_panel_text),\n            info_bar_bg_color: Some(theme.info_bar_bg),\n            info_bar_text_color: Some(theme.info_bar_text),\n        }\n    }\n}\n\n#[derive(Debug, Eq, PartialEq, Deserialize)]\n#[serde(rename_all = \"kebab-case\", deny_unknown_fields)]\npub struct ConfigBindings {\n    pub toggle_help: Option<TuiKeyBinding>,\n    pub toggle_help_alt: Option<TuiKeyBinding>,\n    pub toggle_settings: Option<TuiKeyBinding>,\n    pub toggle_settings_tui: Option<TuiKeyBinding>,\n    pub toggle_settings_trace: Option<TuiKeyBinding>,\n    pub toggle_settings_dns: Option<TuiKeyBinding>,\n    pub toggle_settings_geoip: Option<TuiKeyBinding>,\n    pub toggle_settings_bindings: Option<TuiKeyBinding>,\n    pub toggle_settings_theme: Option<TuiKeyBinding>,\n    pub toggle_settings_columns: Option<TuiKeyBinding>,\n    pub previous_hop: Option<TuiKeyBinding>,\n    pub next_hop: Option<TuiKeyBinding>,\n    pub previous_trace: Option<TuiKeyBinding>,\n    pub next_trace: Option<TuiKeyBinding>,\n    pub previous_hop_address: Option<TuiKeyBinding>,\n    pub next_hop_address: Option<TuiKeyBinding>,\n    pub address_mode_ip: Option<TuiKeyBinding>,\n    pub address_mode_host: Option<TuiKeyBinding>,\n    pub address_mode_both: Option<TuiKeyBinding>,\n    pub toggle_freeze: Option<TuiKeyBinding>,\n    pub toggle_chart: Option<TuiKeyBinding>,\n    pub toggle_flows: Option<TuiKeyBinding>,\n    #[serde(rename = \"toggle-privacy\")]\n    pub deprecated_toggle_privacy: Option<TuiKeyBinding>,\n    pub expand_privacy: Option<TuiKeyBinding>,\n    pub contract_privacy: Option<TuiKeyBinding>,\n    pub toggle_map: Option<TuiKeyBinding>,\n    pub expand_hosts: Option<TuiKeyBinding>,\n    pub contract_hosts: Option<TuiKeyBinding>,\n    pub expand_hosts_max: Option<TuiKeyBinding>,\n    pub contract_hosts_min: Option<TuiKeyBinding>,\n    pub chart_zoom_in: Option<TuiKeyBinding>,\n    pub chart_zoom_out: Option<TuiKeyBinding>,\n    pub clear_trace_data: Option<TuiKeyBinding>,\n    pub clear_dns_cache: Option<TuiKeyBinding>,\n    pub clear_selection: Option<TuiKeyBinding>,\n    pub toggle_as_info: Option<TuiKeyBinding>,\n    pub toggle_hop_details: Option<TuiKeyBinding>,\n    pub quit: Option<TuiKeyBinding>,\n    pub quit_preserve_screen: Option<TuiKeyBinding>,\n}\n\nimpl Default for ConfigBindings {\n    fn default() -> Self {\n        let bindings = super::binding::TuiBindings::default();\n        Self {\n            toggle_help: Some(bindings.toggle_help),\n            toggle_help_alt: Some(bindings.toggle_help_alt),\n            toggle_settings: Some(bindings.toggle_settings),\n            toggle_settings_tui: Some(bindings.toggle_settings_tui),\n            toggle_settings_trace: Some(bindings.toggle_settings_trace),\n            toggle_settings_dns: Some(bindings.toggle_settings_dns),\n            toggle_settings_geoip: Some(bindings.toggle_settings_geoip),\n            toggle_settings_bindings: Some(bindings.toggle_settings_bindings),\n            toggle_settings_theme: Some(bindings.toggle_settings_theme),\n            toggle_settings_columns: Some(bindings.toggle_settings_columns),\n            previous_hop: Some(bindings.previous_hop),\n            next_hop: Some(bindings.next_hop),\n            previous_trace: Some(bindings.previous_trace),\n            next_trace: Some(bindings.next_trace),\n            previous_hop_address: Some(bindings.previous_hop_address),\n            next_hop_address: Some(bindings.next_hop_address),\n            address_mode_ip: Some(bindings.address_mode_ip),\n            address_mode_host: Some(bindings.address_mode_host),\n            address_mode_both: Some(bindings.address_mode_both),\n            toggle_freeze: Some(bindings.toggle_freeze),\n            toggle_chart: Some(bindings.toggle_chart),\n            toggle_flows: Some(bindings.toggle_flows),\n            deprecated_toggle_privacy: None,\n            expand_privacy: Some(bindings.expand_privacy),\n            contract_privacy: Some(bindings.contract_privacy),\n            toggle_map: Some(bindings.toggle_map),\n            expand_hosts: Some(bindings.expand_hosts),\n            contract_hosts: Some(bindings.contract_hosts),\n            expand_hosts_max: Some(bindings.expand_hosts_max),\n            contract_hosts_min: Some(bindings.contract_hosts_min),\n            chart_zoom_in: Some(bindings.chart_zoom_in),\n            chart_zoom_out: Some(bindings.chart_zoom_out),\n            clear_trace_data: Some(bindings.clear_trace_data),\n            clear_dns_cache: Some(bindings.clear_dns_cache),\n            clear_selection: Some(bindings.clear_selection),\n            toggle_as_info: Some(bindings.toggle_as_info),\n            toggle_hop_details: Some(bindings.toggle_hop_details),\n            quit: Some(bindings.quit),\n            quit_preserve_screen: Some(bindings.quit_preserve_screen),\n        }\n    }\n}\n\nfn humantime_deser<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>\nwhere\n    D: serde::Deserializer<'de>,\n{\n    humantime::parse_duration(&String::deserialize(deserializer)?)\n        .map_err(serde::de::Error::custom)\n        .map(Some)\n}\n\nfn addr_deser<'de, D>(deserializer: D) -> Result<Option<IpAddr>, D::Error>\nwhere\n    D: serde::Deserializer<'de>,\n{\n    IpAddr::from_str(&String::deserialize(deserializer)?)\n        .map_err(serde::de::Error::custom)\n        .map(Some)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_parse_config_sample() {\n        let config: ConfigFile =\n            toml::from_str(include_str!(\"../../trippy-config-sample.toml\")).unwrap();\n        pretty_assertions::assert_eq!(ConfigFile::default(), config);\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/config/theme.rs",
    "content": "use crate::config::file::ConfigThemeColors;\nuse anyhow::anyhow;\nuse serde::Deserialize;\nuse std::collections::HashMap;\nuse strum::{EnumString, VariantNames};\n\n/// Tui color theme.\n#[derive(Debug, Clone, Copy, Eq, PartialEq)]\npub struct TuiTheme {\n    /// The default background color.\n    ///\n    /// This may be overridden for specific components.\n    pub bg: TuiColor,\n    /// The default color of borders.\n    ///\n    /// This may be overridden for specific components.\n    pub border: TuiColor,\n    /// The default color of text.\n    ///\n    /// This may be overridden for specific components.\n    pub text: TuiColor,\n    /// The color of the text in traces tabs.\n    pub tab_text: TuiColor,\n    /// The background color of the hops table header.\n    pub hops_table_header_bg: TuiColor,\n    /// The color of text in the hops table header.\n    pub hops_table_header_text: TuiColor,\n    /// The color of text of active rows in the hops table.\n    pub hops_table_row_active_text: TuiColor,\n    /// The color of text of inactive rows in the hops table.\n    pub hops_table_row_inactive_text: TuiColor,\n    /// The color of the selected series in the hops chart.\n    pub hops_chart_selected: TuiColor,\n    /// The color of the unselected series in the hops chart.\n    pub hops_chart_unselected: TuiColor,\n    /// The color of the axis in the hops chart.\n    pub hops_chart_axis: TuiColor,\n    /// The color of bars in the frequency chart.\n    pub frequency_chart_bar: TuiColor,\n    /// The color of text in the bars of the frequency chart.\n    pub frequency_chart_text: TuiColor,\n    /// The color of the selected flow bar in the flows chart.\n    pub flows_chart_bar_selected: TuiColor,\n    /// The color of the unselected flow bar in the flows chart.\n    pub flows_chart_bar_unselected: TuiColor,\n    /// The color of the current flow text in the flows chart.\n    pub flows_chart_text_current: TuiColor,\n    /// The color of the non-current flow text in the flows chart.\n    pub flows_chart_text_non_current: TuiColor,\n    /// The color of the samples chart.\n    pub samples_chart: TuiColor,\n    /// The color of the samples chart for lost probes.\n    pub samples_chart_lost: TuiColor,\n    /// The background color of the help dialog.\n    pub help_dialog_bg: TuiColor,\n    /// The color of the text in the help dialog.\n    pub help_dialog_text: TuiColor,\n    /// The background color of the settings dialog.\n    pub settings_dialog_bg: TuiColor,\n    /// The color of the text in settings dialog tabs.\n    pub settings_tab_text: TuiColor,\n    /// The color of text in the settings table header.\n    pub settings_table_header_text: TuiColor,\n    /// The background color of the settings table header.\n    pub settings_table_header_bg: TuiColor,\n    /// The color of text of rows in the settings table.\n    pub settings_table_row_text: TuiColor,\n    /// The color of the map world diagram.\n    pub map_world: TuiColor,\n    /// The color of the map accuracy radius circle.\n    pub map_radius: TuiColor,\n    /// The color of the map selected item box.\n    pub map_selected: TuiColor,\n    /// The color of border of the map info panel.\n    pub map_info_panel_border: TuiColor,\n    /// The background color of the map info panel.\n    pub map_info_panel_bg: TuiColor,\n    /// The color of text in the map info panel.\n    pub map_info_panel_text: TuiColor,\n    /// The color of the info bar background.\n    pub info_bar_bg: TuiColor,\n    /// The color of the info bar text.\n    pub info_bar_text: TuiColor,\n}\n\nimpl Default for TuiTheme {\n    fn default() -> Self {\n        Self {\n            bg: TuiColor::Black,\n            border: TuiColor::Gray,\n            text: TuiColor::Gray,\n            tab_text: TuiColor::Green,\n            hops_table_header_bg: TuiColor::White,\n            hops_table_header_text: TuiColor::Black,\n            hops_table_row_active_text: TuiColor::Gray,\n            hops_table_row_inactive_text: TuiColor::DarkGray,\n            hops_chart_selected: TuiColor::Green,\n            hops_chart_unselected: TuiColor::Gray,\n            hops_chart_axis: TuiColor::DarkGray,\n            frequency_chart_bar: TuiColor::Green,\n            frequency_chart_text: TuiColor::Gray,\n            flows_chart_bar_selected: TuiColor::Green,\n            flows_chart_bar_unselected: TuiColor::DarkGray,\n            flows_chart_text_current: TuiColor::LightGreen,\n            flows_chart_text_non_current: TuiColor::White,\n            samples_chart: TuiColor::Yellow,\n            samples_chart_lost: TuiColor::Red,\n            help_dialog_bg: TuiColor::Blue,\n            help_dialog_text: TuiColor::Gray,\n            settings_dialog_bg: TuiColor::Blue,\n            settings_tab_text: TuiColor::Green,\n            settings_table_header_text: TuiColor::Black,\n            settings_table_header_bg: TuiColor::White,\n            settings_table_row_text: TuiColor::Gray,\n            map_world: TuiColor::White,\n            map_radius: TuiColor::Yellow,\n            map_selected: TuiColor::Green,\n            map_info_panel_border: TuiColor::Gray,\n            map_info_panel_bg: TuiColor::Black,\n            map_info_panel_text: TuiColor::Gray,\n            info_bar_bg: TuiColor::White,\n            info_bar_text: TuiColor::Black,\n        }\n    }\n}\n\nimpl From<(HashMap<TuiThemeItem, TuiColor>, ConfigThemeColors)> for TuiTheme {\n    #[expect(clippy::too_many_lines, clippy::or_fun_call)]\n    fn from(value: (HashMap<TuiThemeItem, TuiColor>, ConfigThemeColors)) -> Self {\n        let (color_map, cfg) = value;\n        Self {\n            bg: *color_map\n                .get(&TuiThemeItem::BgColor)\n                .or(cfg.bg_color.as_ref())\n                .unwrap_or(&Self::default().bg),\n            border: *color_map\n                .get(&TuiThemeItem::BorderColor)\n                .or(cfg.border_color.as_ref())\n                .unwrap_or(&Self::default().border),\n            text: *color_map\n                .get(&TuiThemeItem::TextColor)\n                .or(cfg.text_color.as_ref())\n                .unwrap_or(&Self::default().text),\n            tab_text: *color_map\n                .get(&TuiThemeItem::TabTextColor)\n                .or(cfg.tab_text_color.as_ref())\n                .unwrap_or(&Self::default().tab_text),\n            hops_table_header_bg: *color_map\n                .get(&TuiThemeItem::HopsTableHeaderBgColor)\n                .or(cfg.hops_table_header_bg_color.as_ref())\n                .unwrap_or(&Self::default().hops_table_header_bg),\n            hops_table_header_text: *color_map\n                .get(&TuiThemeItem::HopsTableHeaderTextColor)\n                .or(cfg.hops_table_header_text_color.as_ref())\n                .unwrap_or(&Self::default().hops_table_header_text),\n            hops_table_row_active_text: *color_map\n                .get(&TuiThemeItem::HopsTableRowActiveTextColor)\n                .or(cfg.hops_table_row_active_text_color.as_ref())\n                .unwrap_or(&Self::default().hops_table_row_active_text),\n            hops_table_row_inactive_text: *color_map\n                .get(&TuiThemeItem::HopsTableRowInactiveTextColor)\n                .or(cfg.hops_table_row_inactive_text_color.as_ref())\n                .unwrap_or(&Self::default().hops_table_row_inactive_text),\n            hops_chart_selected: *color_map\n                .get(&TuiThemeItem::HopsChartSelectedColor)\n                .or(cfg.hops_chart_selected_color.as_ref())\n                .unwrap_or(&Self::default().hops_chart_selected),\n            hops_chart_unselected: *color_map\n                .get(&TuiThemeItem::HopsChartUnselectedColor)\n                .or(cfg.hops_chart_unselected_color.as_ref())\n                .unwrap_or(&Self::default().hops_chart_unselected),\n            hops_chart_axis: *color_map\n                .get(&TuiThemeItem::HopsChartAxisColor)\n                .or(cfg.hops_chart_axis_color.as_ref())\n                .unwrap_or(&Self::default().hops_chart_axis),\n            frequency_chart_bar: *color_map\n                .get(&TuiThemeItem::FrequencyChartBarColor)\n                .or(cfg.frequency_chart_bar_color.as_ref())\n                .unwrap_or(&Self::default().frequency_chart_bar),\n            frequency_chart_text: *color_map\n                .get(&TuiThemeItem::FrequencyChartTextColor)\n                .or(cfg.frequency_chart_text_color.as_ref())\n                .unwrap_or(&Self::default().frequency_chart_text),\n            flows_chart_bar_selected: *color_map\n                .get(&TuiThemeItem::FlowsChartBarSelectedColor)\n                .or(cfg.flows_chart_bar_selected_color.as_ref())\n                .unwrap_or(&Self::default().flows_chart_bar_selected),\n            flows_chart_bar_unselected: *color_map\n                .get(&TuiThemeItem::FlowsChartBarUnselectedColor)\n                .or(cfg.flows_chart_bar_unselected_color.as_ref())\n                .unwrap_or(&Self::default().flows_chart_bar_unselected),\n            flows_chart_text_current: *color_map\n                .get(&TuiThemeItem::FlowsChartTextCurrentColor)\n                .or(cfg.flows_chart_text_current_color.as_ref())\n                .unwrap_or(&Self::default().flows_chart_text_current),\n            flows_chart_text_non_current: *color_map\n                .get(&TuiThemeItem::FlowsChartTextNonCurrentColor)\n                .or(cfg.flows_chart_text_non_current_color.as_ref())\n                .unwrap_or(&Self::default().flows_chart_text_non_current),\n            samples_chart: *color_map\n                .get(&TuiThemeItem::SamplesChartColor)\n                .or(cfg.samples_chart_color.as_ref())\n                .unwrap_or(&Self::default().samples_chart),\n            samples_chart_lost: *color_map\n                .get(&TuiThemeItem::SamplesChartLostColor)\n                .or(cfg.samples_chart_lost_color.as_ref())\n                .unwrap_or(&Self::default().samples_chart_lost),\n            help_dialog_bg: *color_map\n                .get(&TuiThemeItem::HelpDialogBgColor)\n                .or(cfg.help_dialog_bg_color.as_ref())\n                .unwrap_or(&Self::default().help_dialog_bg),\n            help_dialog_text: *color_map\n                .get(&TuiThemeItem::HelpDialogTextColor)\n                .or(cfg.help_dialog_text_color.as_ref())\n                .unwrap_or(&Self::default().help_dialog_text),\n            settings_dialog_bg: *color_map\n                .get(&TuiThemeItem::SettingsDialogBgColor)\n                .or(cfg.settings_dialog_bg_color.as_ref())\n                .unwrap_or(&Self::default().settings_dialog_bg),\n            settings_tab_text: *color_map\n                .get(&TuiThemeItem::SettingsTabTextColor)\n                .or(cfg.settings_tab_text_color.as_ref())\n                .unwrap_or(&Self::default().settings_tab_text),\n            settings_table_header_text: *color_map\n                .get(&TuiThemeItem::SettingsTableHeaderTextColor)\n                .or(cfg.settings_table_header_text_color.as_ref())\n                .unwrap_or(&Self::default().settings_table_header_text),\n            settings_table_header_bg: *color_map\n                .get(&TuiThemeItem::SettingsTableHeaderBgColor)\n                .or(cfg.settings_table_header_bg_color.as_ref())\n                .unwrap_or(&Self::default().settings_table_header_bg),\n            settings_table_row_text: *color_map\n                .get(&TuiThemeItem::SettingsTableRowTextColor)\n                .or(cfg.settings_table_row_text_color.as_ref())\n                .unwrap_or(&Self::default().settings_table_row_text),\n            map_world: *color_map\n                .get(&TuiThemeItem::MapWorldColor)\n                .or(cfg.map_world_color.as_ref())\n                .unwrap_or(&Self::default().map_world),\n            map_radius: *color_map\n                .get(&TuiThemeItem::MapRadiusColor)\n                .or(cfg.map_radius_color.as_ref())\n                .unwrap_or(&Self::default().map_radius),\n            map_selected: *color_map\n                .get(&TuiThemeItem::MapSelectedColor)\n                .or(cfg.map_selected_color.as_ref())\n                .unwrap_or(&Self::default().map_selected),\n            map_info_panel_border: *color_map\n                .get(&TuiThemeItem::MapInfoPanelBorderColor)\n                .or(cfg.map_info_panel_border_color.as_ref())\n                .unwrap_or(&Self::default().map_info_panel_border),\n            map_info_panel_bg: *color_map\n                .get(&TuiThemeItem::MapInfoPanelBgColor)\n                .or(cfg.map_info_panel_bg_color.as_ref())\n                .unwrap_or(&Self::default().map_info_panel_bg),\n            map_info_panel_text: *color_map\n                .get(&TuiThemeItem::MapInfoPanelTextColor)\n                .or(cfg.map_info_panel_text_color.as_ref())\n                .unwrap_or(&Self::default().map_info_panel_text),\n            info_bar_bg: *color_map\n                .get(&TuiThemeItem::InfoBarBgColor)\n                .or(cfg.info_bar_bg_color.as_ref())\n                .unwrap_or(&Self::default().info_bar_bg),\n            info_bar_text: *color_map\n                .get(&TuiThemeItem::InfoBarTextColor)\n                .or(cfg.info_bar_text_color.as_ref())\n                .unwrap_or(&Self::default().info_bar_text),\n        }\n    }\n}\n\n/// A TUI theme item.\n#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, EnumString, VariantNames)]\n#[strum(serialize_all = \"kebab-case\")]\n#[expect(clippy::enum_variant_names)]\npub enum TuiThemeItem {\n    /// The default background color.\n    BgColor,\n    /// The default color of borders.\n    BorderColor,\n    /// The default color of text.\n    TextColor,\n    /// The color of the text in traces tabs.\n    TabTextColor,\n    /// The background color of the hops table header.\n    HopsTableHeaderBgColor,\n    /// The color of text in the hops table header.\n    HopsTableHeaderTextColor,\n    /// The color of text of active rows in the hops table.\n    HopsTableRowActiveTextColor,\n    /// The color of text of inactive rows in the hops table.\n    HopsTableRowInactiveTextColor,\n    /// The color of the selected series in the hops chart.\n    HopsChartSelectedColor,\n    /// The color of the unselected series in the hops chart.\n    HopsChartUnselectedColor,\n    /// The color of the axis in the hops chart.\n    HopsChartAxisColor,\n    /// The color of bars in the frequency chart.\n    FrequencyChartBarColor,\n    /// The color of text in the bars of the frequency chart.\n    FrequencyChartTextColor,\n    /// The color of the selected flow bar in the flows chart.\n    FlowsChartBarSelectedColor,\n    /// The color of the unselected flow bar in the flows chart.\n    FlowsChartBarUnselectedColor,\n    /// The color of the current flow text in the flows chart.\n    FlowsChartTextCurrentColor,\n    /// The color of the non-current flow text in the flows chart.\n    FlowsChartTextNonCurrentColor,\n    /// The color of the samples chart.\n    SamplesChartColor,\n    /// The color of the samples chart for lost probes.\n    SamplesChartLostColor,\n    /// The background color of the help dialog.\n    HelpDialogBgColor,\n    /// The color of the text in the help dialog.\n    HelpDialogTextColor,\n    /// The color of the text in settings tabs.\n    SettingsTabTextColor,\n    /// The background color of the settings dialog.\n    SettingsDialogBgColor,\n    /// The color of text in the settings table header.\n    SettingsTableHeaderTextColor,\n    /// The background color of the settings table header.\n    SettingsTableHeaderBgColor,\n    /// The color of text of rows in the settings table.\n    SettingsTableRowTextColor,\n    /// The color of the map world diagram.\n    MapWorldColor,\n    /// The color of the map accuracy radius circle.\n    MapRadiusColor,\n    /// The color of the map selected item box.\n    MapSelectedColor,\n    /// The color of border of the map info panel.\n    MapInfoPanelBorderColor,\n    /// The background color of the map info panel.\n    MapInfoPanelBgColor,\n    /// The color of text in the map info panel.\n    MapInfoPanelTextColor,\n    /// The color of the info bar background.\n    InfoBarBgColor,\n    /// The color of the info bar text.\n    InfoBarTextColor,\n}\n\n/// A TUI color.\n#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize)]\n#[serde(try_from = \"String\")]\npub enum TuiColor {\n    // ANSI colors\n    Black,\n    Red,\n    Green,\n    Yellow,\n    Blue,\n    Magenta,\n    Cyan,\n    Gray,\n    DarkGray,\n    LightRed,\n    LightGreen,\n    LightYellow,\n    LightBlue,\n    LightMagenta,\n    LightCyan,\n    White,\n    // Other colors\n    AliceBlue,\n    AntiqueWhite,\n    Aqua,\n    Aquamarine,\n    Azure,\n    Beige,\n    Bisque,\n    BlanchedAlmond,\n    BlueViolet,\n    Brown,\n    BurlyWood,\n    CadetBlue,\n    Chartreuse,\n    Chocolate,\n    Coral,\n    CornflowerBlue,\n    CornSilk,\n    Crimson,\n    DarkBlue,\n    DarkCyan,\n    DarkGoldenrod,\n    DarkGreen,\n    DarkKhaki,\n    DarkMagenta,\n    DarkOliveGreen,\n    DarkOrange,\n    DarkOrchid,\n    DarkRed,\n    DarkSalmon,\n    DarkSeaGreen,\n    DarkSlateBlue,\n    DarkSlateGray,\n    DarkTurquoise,\n    DarkViolet,\n    DeepPink,\n    DeepSkyBlue,\n    DimGray,\n    DodgerBlue,\n    Firebrick,\n    FloralWhite,\n    ForestGreen,\n    Fuchsia,\n    Gainsboro,\n    GhostWhite,\n    Gold,\n    Goldenrod,\n    GreenYellow,\n    Honeydew,\n    HotPink,\n    IndianRed,\n    Indigo,\n    Ivory,\n    Khaki,\n    Lavender,\n    LavenderBlush,\n    LawnGreen,\n    LemonChiffon,\n    LightCoral,\n    LightGoldenrodYellow,\n    LightGray,\n    LightPink,\n    LightSalmon,\n    LightSeaGreen,\n    LightSkyBlue,\n    LightSlateGray,\n    LightSteelBlue,\n    Lime,\n    LimeGreen,\n    Linen,\n    Maroon,\n    MediumAquamarine,\n    MediumBlue,\n    MediumOrchid,\n    MediumPurple,\n    MediumSeaGreen,\n    MediumSlateBlue,\n    MediumSpringGreen,\n    MediumTurquoise,\n    MediumVioletRed,\n    MidnightBlue,\n    MintCream,\n    MistyRose,\n    Moccasin,\n    NavajoWhite,\n    Navy,\n    OldLace,\n    Olive,\n    OliveDrab,\n    Orange,\n    OrangeRed,\n    Orchid,\n    PaleGoldenrod,\n    PaleGreen,\n    PaleTurquoise,\n    PaleVioletRed,\n    PapayaWhip,\n    PeachPuff,\n    Peru,\n    Pink,\n    Plum,\n    PowderBlue,\n    Purple,\n    RebeccaPurple,\n    RosyBrown,\n    RoyalBlue,\n    SaddleBrown,\n    Salmon,\n    SandyBrown,\n    SeaGreen,\n    SeaShell,\n    Sienna,\n    Silver,\n    SkyBlue,\n    SlateBlue,\n    SlateGray,\n    Snow,\n    SpringGreen,\n    SteelBlue,\n    Tan,\n    Teal,\n    Thistle,\n    Tomato,\n    Turquoise,\n    Violet,\n    Wheat,\n    WhiteSmoke,\n    YellowGreen,\n    Rgb(u8, u8, u8),\n}\n\nimpl TryFrom<String> for TuiColor {\n    type Error = anyhow::Error;\n\n    fn try_from(value: String) -> Result<Self, Self::Error> {\n        Self::try_from(value.as_ref())\n    }\n}\n\nimpl TryFrom<&str> for TuiColor {\n    type Error = anyhow::Error;\n\n    #[expect(clippy::too_many_lines)]\n    fn try_from(value: &str) -> Result<Self, Self::Error> {\n        match value.to_ascii_lowercase().replace('-', \"\").as_ref() {\n            \"black\" => Ok(Self::Black),\n            \"red\" => Ok(Self::Red),\n            \"green\" => Ok(Self::Green),\n            \"yellow\" => Ok(Self::Yellow),\n            \"blue\" => Ok(Self::Blue),\n            \"magenta\" => Ok(Self::Magenta),\n            \"cyan\" => Ok(Self::Cyan),\n            \"gray\" => Ok(Self::Gray),\n            \"darkgray\" => Ok(Self::DarkGray),\n            \"lightred\" => Ok(Self::LightRed),\n            \"lightgreen\" => Ok(Self::LightGreen),\n            \"lightyellow\" => Ok(Self::LightYellow),\n            \"lightblue\" => Ok(Self::LightBlue),\n            \"lightmagenta\" => Ok(Self::LightMagenta),\n            \"lightcyan\" => Ok(Self::LightCyan),\n            \"white\" => Ok(Self::White),\n            \"aliceblue\" => Ok(Self::AliceBlue),\n            \"antiquewhite\" => Ok(Self::AntiqueWhite),\n            \"aqua\" => Ok(Self::Aqua),\n            \"aquamarine\" => Ok(Self::Aquamarine),\n            \"azure\" => Ok(Self::Azure),\n            \"beige\" => Ok(Self::Beige),\n            \"bisque\" => Ok(Self::Bisque),\n            \"blanchedalmond\" => Ok(Self::BlanchedAlmond),\n            \"blueviolet\" => Ok(Self::BlueViolet),\n            \"brown\" => Ok(Self::Brown),\n            \"burlywood\" => Ok(Self::BurlyWood),\n            \"cadetblue\" => Ok(Self::CadetBlue),\n            \"chartreuse\" => Ok(Self::Chartreuse),\n            \"chocolate\" => Ok(Self::Chocolate),\n            \"coral\" => Ok(Self::Coral),\n            \"cornflowerblue\" => Ok(Self::CornflowerBlue),\n            \"cornsilk\" => Ok(Self::CornSilk),\n            \"crimson\" => Ok(Self::Crimson),\n            \"darkblue\" => Ok(Self::DarkBlue),\n            \"darkcyan\" => Ok(Self::DarkCyan),\n            \"darkgoldenrod\" => Ok(Self::DarkGoldenrod),\n            \"darkgreen\" => Ok(Self::DarkGreen),\n            \"darkkhaki\" => Ok(Self::DarkKhaki),\n            \"darkmagenta\" => Ok(Self::DarkMagenta),\n            \"darkolivegreen\" => Ok(Self::DarkOliveGreen),\n            \"darkorange\" => Ok(Self::DarkOrange),\n            \"darkorchid\" => Ok(Self::DarkOrchid),\n            \"darkred\" => Ok(Self::DarkRed),\n            \"darksalmon\" => Ok(Self::DarkSalmon),\n            \"darkseagreen\" => Ok(Self::DarkSeaGreen),\n            \"darkslateblue\" => Ok(Self::DarkSlateBlue),\n            \"darkslategray\" | \"darkslategrey\" => Ok(Self::DarkSlateGray),\n            \"darkturquoise\" => Ok(Self::DarkTurquoise),\n            \"darkviolet\" => Ok(Self::DarkViolet),\n            \"deeppink\" => Ok(Self::DeepPink),\n            \"deepskyblue\" => Ok(Self::DeepSkyBlue),\n            \"dimgray\" | \"dimgrey\" => Ok(Self::DimGray),\n            \"dodgerblue\" => Ok(Self::DodgerBlue),\n            \"firebrick\" => Ok(Self::Firebrick),\n            \"floralwhite\" => Ok(Self::FloralWhite),\n            \"forestgreen\" => Ok(Self::ForestGreen),\n            \"fuchsia\" => Ok(Self::Fuchsia),\n            \"gainsboro\" => Ok(Self::Gainsboro),\n            \"ghostwhite\" => Ok(Self::GhostWhite),\n            \"gold\" => Ok(Self::Gold),\n            \"goldenrod\" => Ok(Self::Goldenrod),\n            \"greenyellow\" => Ok(Self::GreenYellow),\n            \"honeydew\" => Ok(Self::Honeydew),\n            \"hotpink\" => Ok(Self::HotPink),\n            \"indianred\" => Ok(Self::IndianRed),\n            \"indigo\" => Ok(Self::Indigo),\n            \"ivory\" => Ok(Self::Ivory),\n            \"khaki\" => Ok(Self::Khaki),\n            \"lavender\" => Ok(Self::Lavender),\n            \"lavenderblush\" => Ok(Self::LavenderBlush),\n            \"lawngreen\" => Ok(Self::LawnGreen),\n            \"lemonchiffon\" => Ok(Self::LemonChiffon),\n            \"lightcoral\" => Ok(Self::LightCoral),\n            \"lightgoldenrodyellow\" => Ok(Self::LightGoldenrodYellow),\n            \"lightgray\" | \"lightgrey\" => Ok(Self::LightGray),\n            \"lightpink\" => Ok(Self::LightPink),\n            \"lightsalmon\" => Ok(Self::LightSalmon),\n            \"lightseagreen\" => Ok(Self::LightSeaGreen),\n            \"lightskyblue\" => Ok(Self::LightSkyBlue),\n            \"lightslategray\" | \"lightslategrey\" => Ok(Self::LightSlateGray),\n            \"lightsteelblue\" => Ok(Self::LightSteelBlue),\n            \"lime\" => Ok(Self::Lime),\n            \"limegreen\" => Ok(Self::LimeGreen),\n            \"linen\" => Ok(Self::Linen),\n            \"maroon\" => Ok(Self::Maroon),\n            \"mediumaquamarine\" => Ok(Self::MediumAquamarine),\n            \"mediumblue\" => Ok(Self::MediumBlue),\n            \"mediumorchid\" => Ok(Self::MediumOrchid),\n            \"mediumpurple\" => Ok(Self::MediumPurple),\n            \"mediumseagreen\" => Ok(Self::MediumSeaGreen),\n            \"mediumslateblue\" => Ok(Self::MediumSlateBlue),\n            \"mediumspringgreen\" => Ok(Self::MediumSpringGreen),\n            \"mediumturquoise\" => Ok(Self::MediumTurquoise),\n            \"mediumvioletred\" => Ok(Self::MediumVioletRed),\n            \"midnightblue\" => Ok(Self::MidnightBlue),\n            \"mintcream\" => Ok(Self::MintCream),\n            \"mistyrose\" => Ok(Self::MistyRose),\n            \"moccasin\" => Ok(Self::Moccasin),\n            \"navajowhite\" => Ok(Self::NavajoWhite),\n            \"navy\" => Ok(Self::Navy),\n            \"oldlace\" => Ok(Self::OldLace),\n            \"olive\" => Ok(Self::Olive),\n            \"olivedrab\" => Ok(Self::OliveDrab),\n            \"orange\" => Ok(Self::Orange),\n            \"orangered\" => Ok(Self::OrangeRed),\n            \"orchid\" => Ok(Self::Orchid),\n            \"palegoldenrod\" => Ok(Self::PaleGoldenrod),\n            \"palegreen\" => Ok(Self::PaleGreen),\n            \"paleturquoise\" => Ok(Self::PaleTurquoise),\n            \"palevioletred\" => Ok(Self::PaleVioletRed),\n            \"papayawhip\" => Ok(Self::PapayaWhip),\n            \"peachpuff\" => Ok(Self::PeachPuff),\n            \"peru\" => Ok(Self::Peru),\n            \"pink\" => Ok(Self::Pink),\n            \"plum\" => Ok(Self::Plum),\n            \"powderblue\" => Ok(Self::PowderBlue),\n            \"purple\" => Ok(Self::Purple),\n            \"rebeccapurple\" => Ok(Self::RebeccaPurple),\n            \"rosybrown\" => Ok(Self::RosyBrown),\n            \"royalblue\" => Ok(Self::RoyalBlue),\n            \"saddlebrown\" => Ok(Self::SaddleBrown),\n            \"salmon\" => Ok(Self::Salmon),\n            \"sandybrown\" => Ok(Self::SandyBrown),\n            \"seagreen\" => Ok(Self::SeaGreen),\n            \"seashell\" => Ok(Self::SeaShell),\n            \"sienna\" => Ok(Self::Sienna),\n            \"silver\" => Ok(Self::Silver),\n            \"skyblue\" => Ok(Self::SkyBlue),\n            \"slateblue\" => Ok(Self::SlateBlue),\n            \"slategray\" | \"slategrey\" => Ok(Self::SlateGray),\n            \"snow\" => Ok(Self::Snow),\n            \"springgreen\" => Ok(Self::SpringGreen),\n            \"steelblue\" => Ok(Self::SteelBlue),\n            \"tan\" => Ok(Self::Tan),\n            \"teal\" => Ok(Self::Teal),\n            \"thistle\" => Ok(Self::Thistle),\n            \"tomato\" => Ok(Self::Tomato),\n            \"turquoise\" => Ok(Self::Turquoise),\n            \"violet\" => Ok(Self::Violet),\n            \"wheat\" => Ok(Self::Wheat),\n            \"whitesmoke\" => Ok(Self::WhiteSmoke),\n            \"yellowgreen\" => Ok(Self::YellowGreen),\n            rgb_hex if value.len() == 6 && value.chars().all(|c| c.is_ascii_hexdigit()) => {\n                let red = u8::from_str_radix(&rgb_hex[0..2], 16)?;\n                let green = u8::from_str_radix(&rgb_hex[2..4], 16)?;\n                let blue = u8::from_str_radix(&rgb_hex[4..6], 16)?;\n                Ok(Self::Rgb(red, green, blue))\n            }\n            _ => Err(anyhow!(\"unknown color: {value}\")),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/config.rs",
    "content": "use anyhow::anyhow;\nuse clap::ValueEnum;\nuse clap_complete::Shell;\nuse file::ConfigFile;\nuse itertools::Itertools;\nuse serde::Deserialize;\nuse std::collections::HashMap;\nuse std::net::IpAddr;\nuse std::str::FromStr;\nuse std::time::Duration;\nuse trippy_core::{\n    IcmpExtensionParseMode, MAX_TTL, MultipathStrategy, PortDirection, PrivilegeMode, Protocol,\n    defaults,\n};\nuse trippy_dns::{IpAddrFamily, ResolveMethod};\n\nmod binding;\nmod cmd;\nmod columns;\nmod constants;\nmod file;\nmod theme;\n\nuse crate::config::file::{ConfigBindings, ConfigTui};\npub use binding::{TuiBindings, TuiCommandItem, TuiKeyBinding};\npub use cmd::Args;\npub use columns::{TuiColumn, TuiColumns};\npub use theme::{TuiColor, TuiTheme, TuiThemeItem};\nuse trippy_privilege::Privilege;\n\n/// The tool mode.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum Mode {\n    /// Display interactive TUI.\n    Tui,\n    /// Display a continuous stream of tracing data\n    Stream,\n    /// Generate a pretty text table report for N cycles.\n    Pretty,\n    /// Generate a Markdown text table report for N cycles.\n    Markdown,\n    /// Generate a CSV report for N cycles.\n    Csv,\n    /// Generate a JSON report for N cycles.\n    Json,\n    /// Generate a Graphviz DOT file for N cycles.\n    Dot,\n    /// Display all flows for N cycles.\n    Flows,\n    /// Do not generate any tracing output for N cycles.\n    Silent,\n}\n\n/// The tracing protocol.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum ProtocolConfig {\n    /// Internet Control Message Protocol\n    Icmp,\n    /// User Datagram Protocol\n    Udp,\n    /// Transmission Control Protocol\n    Tcp,\n}\n\nimpl From<Protocol> for ProtocolConfig {\n    fn from(value: Protocol) -> Self {\n        match value {\n            Protocol::Icmp => Self::Icmp,\n            Protocol::Udp => Self::Udp,\n            Protocol::Tcp => Self::Tcp,\n        }\n    }\n}\n\n/// The address family.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum AddressFamilyConfig {\n    /// IPv4 only.\n    Ipv4,\n    /// IPv6 only.\n    Ipv6,\n    /// IPv6 with a fallback to IPv4.\n    #[serde(rename = \"ipv6-then-ipv4\")]\n    Ipv6ThenIpv4,\n    /// IPv4 with a fallback to IPv6.\n    #[serde(rename = \"ipv4-then-ipv6\")]\n    Ipv4ThenIpv6,\n    /// If the OS resolver is being used then use the first IP address returned,\n    /// otherwise lookup IPv4 with a fallback to IPv6.\n    System,\n}\n\nimpl From<IpAddrFamily> for AddressFamilyConfig {\n    fn from(value: IpAddrFamily) -> Self {\n        match value {\n            IpAddrFamily::Ipv4Only => Self::Ipv4,\n            IpAddrFamily::Ipv6Only => Self::Ipv6,\n            IpAddrFamily::Ipv6thenIpv4 => Self::Ipv6ThenIpv4,\n            IpAddrFamily::Ipv4thenIpv6 => Self::Ipv4ThenIpv6,\n            IpAddrFamily::System => Self::System,\n        }\n    }\n}\n\n/// The strategy Equal-cost Multi-Path routing strategy.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum MultipathStrategyConfig {\n    /// The src or dest port is used to store the sequence number.\n    Classic,\n    /// The UDP `checksum` field is used to store the sequence number.\n    Paris,\n    /// The IP `identifier` field is used to store the sequence number.\n    Dublin,\n}\n\nimpl From<MultipathStrategy> for MultipathStrategyConfig {\n    fn from(value: MultipathStrategy) -> Self {\n        match value {\n            MultipathStrategy::Classic => Self::Classic,\n            MultipathStrategy::Paris => Self::Paris,\n            MultipathStrategy::Dublin => Self::Dublin,\n        }\n    }\n}\n\n/// How to render the addresses.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum AddressMode {\n    /// Show IP address only.\n    Ip,\n    /// Show reverse-lookup DNS hostname only.\n    Host,\n    /// Show both IP address and reverse-lookup DNS hostname.\n    Both,\n}\n\n/// How to render autonomous system (AS) information.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum AsMode {\n    /// Show the ASN.\n    Asn,\n    /// Display the AS prefix.\n    Prefix,\n    /// Display the country code.\n    CountryCode,\n    /// Display the registry name.\n    Registry,\n    /// Display the allocated date.\n    Allocated,\n    /// Display the AS name.\n    Name,\n}\n\n/// How to render `icmp` extensions in the hops table.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum IcmpExtensionMode {\n    /// Do not show `icmp` extensions.\n    Off,\n    /// Show MPLS label(s) only.\n    Mpls,\n    /// Show full `icmp` extension data for all known extensions.\n    ///\n    /// For MPLS the fields shown are `label`, `ttl`, `exp` & `bos`.\n    Full,\n    /// Show full `icmp` extension data for all classes.\n    ///\n    /// This is the same as `Full`, but also shows `class`, `subtype` and\n    /// `object` for unknown extensions.\n    All,\n}\n\n/// How to render `GeoIp` information in the hop table.\n///\n/// Note that the hop details view is always shown using the `Long` representation.\n#[expect(clippy::doc_markdown)]\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum GeoIpMode {\n    /// Do not display GeoIp data.\n    Off,\n    /// Show short format.\n    ///\n    /// The `city` name is shown, `subdivision` and `country` codes are shown, `continent` is not\n    /// displayed.\n    ///\n    /// For example:\n    ///\n    /// `Los Angeles, CA, US`\n    Short,\n    /// Show long format.\n    ///\n    /// The `city`, `subdivision`, `country` and `continent` names are shown.\n    ///\n    /// `Los Angeles, California, United States, North America`\n    Long,\n    /// Show latitude and Longitude format.\n    ///\n    /// `lat=34.0544, long=-118.2441`\n    Location,\n}\n\n/// How DNS queries will be resolved.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum DnsResolveMethodConfig {\n    /// Resolve using the OS resolver.\n    System,\n    /// Resolve using the `/etc/resolv.conf` DNS configuration.\n    Resolv,\n    /// Resolve using the Google `8.8.8.8` DNS service.\n    Google,\n    /// Resolve using the Cloudflare `1.1.1.1` DNS service.\n    Cloudflare,\n}\n\n/// How to format log data.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum LogFormat {\n    /// Display log data in a compact format.\n    Compact,\n    /// Display log data in a pretty format.\n    Pretty,\n    /// Display log data in a json format.\n    Json,\n    /// Display log data in Chrome trace format.\n    Chrome,\n}\n\n/// How to log event spans.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub enum LogSpanEvents {\n    /// Do not display event spans.\n    Off,\n    /// Display enter and exit event spans.\n    Active,\n    /// Display all event spans.\n    Full,\n}\n\n/// The action to perform.\n#[derive(Debug, Eq, PartialEq)]\npub enum TrippyAction {\n    /// Run Trippy.\n    Trippy(Box<TrippyConfig>),\n    /// Print all TUI theme items and exit.\n    PrintTuiThemeItems,\n    /// Print all TUI commands that can be bound and exit.\n    PrintTuiBindingCommands,\n    /// Print a template toml config file and exit.\n    PrintConfigTemplate,\n    /// Generate shell completion and exit.\n    PrintShellCompletions(Shell),\n    /// Generate a man page and exit.\n    PrintManPage,\n    /// Print all available locales and exit.\n    PrintLocales,\n}\n\nimpl TrippyAction {\n    pub fn from(args: Args, privilege: &Privilege, pid: u16) -> anyhow::Result<Self> {\n        Ok(if args.print_tui_theme_items {\n            Self::PrintTuiThemeItems\n        } else if args.print_tui_binding_commands {\n            Self::PrintTuiBindingCommands\n        } else if args.print_config_template {\n            Self::PrintConfigTemplate\n        } else if let Some(shell) = args.generate {\n            Self::PrintShellCompletions(shell)\n        } else if args.generate_man {\n            Self::PrintManPage\n        } else if args.print_locales {\n            Self::PrintLocales\n        } else {\n            Self::Trippy(Box::new(TrippyConfig::from(args, privilege, pid)?))\n        })\n    }\n}\n\n/// Fully parsed and validated configuration.\n#[derive(Debug, Eq, PartialEq)]\npub struct TrippyConfig {\n    pub targets: Vec<String>,\n    pub protocol: Protocol,\n    pub addr_family: IpAddrFamily,\n    pub first_ttl: u8,\n    pub max_ttl: u8,\n    pub min_round_duration: Duration,\n    pub max_round_duration: Duration,\n    pub grace_duration: Duration,\n    pub max_inflight: u8,\n    pub initial_sequence: u16,\n    pub tos: u8,\n    pub icmp_extension_parse_mode: IcmpExtensionParseMode,\n    pub read_timeout: Duration,\n    pub packet_size: u16,\n    pub payload_pattern: u8,\n    pub source_addr: Option<IpAddr>,\n    pub interface: Option<String>,\n    pub multipath_strategy: MultipathStrategy,\n    pub port_direction: PortDirection,\n    pub dns_timeout: Duration,\n    pub dns_ttl: Duration,\n    pub dns_resolve_method: ResolveMethod,\n    pub dns_lookup_as_info: bool,\n    pub max_samples: usize,\n    pub max_flows: usize,\n    pub tui_preserve_screen: bool,\n    pub tui_refresh_rate: Duration,\n    pub tui_privacy_max_ttl: Option<u8>,\n    pub tui_address_mode: AddressMode,\n    pub tui_as_mode: AsMode,\n    pub tui_custom_columns: TuiColumns,\n    pub tui_icmp_extension_mode: IcmpExtensionMode,\n    pub tui_geoip_mode: GeoIpMode,\n    pub tui_max_addrs: Option<u8>,\n    pub tui_locale: Option<String>,\n    pub tui_timezone: Option<chrono_tz::Tz>,\n    pub tui_theme: TuiTheme,\n    pub tui_bindings: TuiBindings,\n    pub mode: Mode,\n    pub privilege_mode: PrivilegeMode,\n    pub dns_resolve_all: bool,\n    pub report_cycles: usize,\n    pub geoip_mmdb_file: Option<String>,\n    pub max_rounds: Option<usize>,\n    pub verbose: bool,\n    pub log_format: LogFormat,\n    pub log_filter: String,\n    pub log_span_events: LogSpanEvents,\n}\n\nimpl TrippyConfig {\n    pub fn from(args: Args, privilege: &Privilege, pid: u16) -> anyhow::Result<Self> {\n        let cfg_file = if let Some(cfg) = &args.config_file {\n            file::read_config_file(cfg)?\n        } else {\n            file::read_default_config_file()?.unwrap_or_default()\n        };\n        Self::build_config(args, cfg_file, privilege, pid)\n    }\n\n    /// The maximum number of flows allowed.\n    ///\n    /// This is restricted to 1 for the classic strategy.\n    pub const fn max_flows(&self) -> usize {\n        match self.multipath_strategy {\n            MultipathStrategy::Classic => 1,\n            _ => self.max_flows,\n        }\n    }\n\n    #[expect(clippy::too_many_lines)]\n    fn build_config(\n        args: Args,\n        cfg_file: ConfigFile,\n        privilege: &Privilege,\n        pid: u16,\n    ) -> anyhow::Result<Self> {\n        let has_privileges = privilege.has_privileges();\n        let needs_privileges = privilege.needs_privileges();\n        let cfg_file_trace = cfg_file.trippy.unwrap_or_default();\n        let cfg_file_strategy = cfg_file.strategy.unwrap_or_default();\n        let cfg_file_tui_bindings = cfg_file.bindings.unwrap_or_default();\n        let cfg_file_tui_theme_colors = cfg_file.theme_colors.unwrap_or_default();\n        let cfg_file_tui = cfg_file.tui.unwrap_or_default();\n        let cfg_file_dns = cfg_file.dns.unwrap_or_default();\n        let cfg_file_report = cfg_file.report.unwrap_or_default();\n        validate_deprecated(&cfg_file_tui, &cfg_file_tui_bindings)?;\n        let mode = cfg_layer(args.mode, cfg_file_trace.mode, constants::DEFAULT_MODE);\n        let unprivileged = cfg_layer_bool_flag(\n            args.unprivileged,\n            cfg_file_trace.unprivileged,\n            defaults::DEFAULT_PRIVILEGE_MODE.is_unprivileged(),\n        );\n        let privilege_mode = if unprivileged {\n            PrivilegeMode::Unprivileged\n        } else {\n            PrivilegeMode::Privileged\n        };\n        let dns_resolve_all = cfg_layer_bool_flag(\n            args.dns_resolve_all,\n            cfg_file_dns.dns_resolve_all,\n            constants::DEFAULT_DNS_RESOLVE_ALL,\n        );\n        let verbose = args.verbose;\n        let log_format = cfg_layer(\n            args.log_format,\n            cfg_file_trace.log_format,\n            constants::DEFAULT_LOG_FORMAT,\n        );\n        let log_filter = cfg_layer(\n            args.log_filter,\n            cfg_file_trace.log_filter,\n            String::from(constants::DEFAULT_LOG_FILTER),\n        );\n        let log_span_events = cfg_layer(\n            args.log_span_events,\n            cfg_file_trace.log_span_events,\n            constants::DEFAULT_LOG_SPAN_EVENTS,\n        );\n        let protocol = cfg_layer(\n            args.protocol,\n            cfg_file_strategy.protocol,\n            ProtocolConfig::from(defaults::DEFAULT_STRATEGY_PROTOCOL),\n        );\n        let addr_family_cfg = cfg_layer(\n            args.addr_family,\n            cfg_file_strategy.addr_family,\n            constants::DEFAULT_ADDR_FAMILY,\n        );\n        let target_port = cfg_layer_opt(args.target_port, cfg_file_strategy.target_port);\n        let source_port = cfg_layer_opt(args.source_port, cfg_file_strategy.source_port);\n        let source_addr = cfg_layer_opt(args.source_address, cfg_file_strategy.source_address);\n        let interface = cfg_layer_opt(args.interface, cfg_file_strategy.interface);\n        let min_round_duration = cfg_layer(\n            args.min_round_duration,\n            cfg_file_strategy.min_round_duration,\n            defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION,\n        );\n        let max_round_duration = cfg_layer(\n            args.max_round_duration,\n            cfg_file_strategy.max_round_duration,\n            defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION,\n        );\n        let initial_sequence = cfg_layer(\n            args.initial_sequence,\n            cfg_file_strategy.initial_sequence,\n            defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE,\n        );\n        let multipath_strategy_cfg = cfg_layer(\n            args.multipath_strategy,\n            cfg_file_strategy.multipath_strategy,\n            MultipathStrategyConfig::from(defaults::DEFAULT_STRATEGY_MULTIPATH),\n        );\n        let grace_duration = cfg_layer(\n            args.grace_duration,\n            cfg_file_strategy.grace_duration,\n            defaults::DEFAULT_STRATEGY_GRACE_DURATION,\n        );\n        let max_inflight = cfg_layer(\n            args.max_inflight,\n            cfg_file_strategy.max_inflight,\n            defaults::DEFAULT_STRATEGY_MAX_INFLIGHT,\n        );\n        let first_ttl = cfg_layer(\n            args.first_ttl,\n            cfg_file_strategy.first_ttl,\n            defaults::DEFAULT_STRATEGY_FIRST_TTL,\n        );\n        let max_ttl = cfg_layer(\n            args.max_ttl,\n            cfg_file_strategy.max_ttl,\n            defaults::DEFAULT_STRATEGY_MAX_TTL,\n        );\n        let packet_size = cfg_layer(\n            args.packet_size,\n            cfg_file_strategy.packet_size,\n            defaults::DEFAULT_STRATEGY_PACKET_SIZE,\n        );\n        let payload_pattern = cfg_layer(\n            args.payload_pattern,\n            cfg_file_strategy.payload_pattern,\n            defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN,\n        );\n        let tos = cfg_layer(\n            args.tos,\n            cfg_file_strategy.tos,\n            defaults::DEFAULT_STRATEGY_TOS,\n        );\n        let icmp_extensions = cfg_layer_bool_flag(\n            args.icmp_extensions,\n            cfg_file_strategy.icmp_extensions,\n            defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE.is_enabled(),\n        );\n        let icmp_extension_parse_mode = if icmp_extensions {\n            IcmpExtensionParseMode::Enabled\n        } else {\n            IcmpExtensionParseMode::Disabled\n        };\n        let read_timeout = cfg_layer(\n            args.read_timeout,\n            cfg_file_strategy.read_timeout,\n            defaults::DEFAULT_STRATEGY_READ_TIMEOUT,\n        );\n        let max_samples = cfg_layer(\n            args.max_samples,\n            cfg_file_strategy.max_samples,\n            defaults::DEFAULT_MAX_SAMPLES,\n        );\n        let max_flows = cfg_layer(\n            args.max_flows,\n            cfg_file_strategy.max_flows,\n            defaults::DEFAULT_MAX_FLOWS,\n        );\n        let tui_preserve_screen = cfg_layer_bool_flag(\n            args.tui_preserve_screen,\n            cfg_file_tui.tui_preserve_screen,\n            constants::DEFAULT_TUI_PRESERVE_SCREEN,\n        );\n        let tui_refresh_rate = cfg_layer(\n            args.tui_refresh_rate,\n            cfg_file_tui.tui_refresh_rate,\n            constants::DEFAULT_TUI_REFRESH_RATE,\n        );\n        let tui_privacy_max_ttl =\n            cfg_layer_opt(args.tui_privacy_max_ttl, cfg_file_tui.tui_privacy_max_ttl);\n        let tui_address_mode = cfg_layer(\n            args.tui_address_mode,\n            cfg_file_tui.tui_address_mode,\n            constants::DEFAULT_TUI_ADDRESS_MODE,\n        );\n        let tui_as_mode = cfg_layer(\n            args.tui_as_mode,\n            cfg_file_tui.tui_as_mode,\n            constants::DEFAULT_TUI_AS_MODE,\n        );\n        let columns = cfg_layer(\n            args.tui_custom_columns,\n            cfg_file_tui.tui_custom_columns,\n            String::from(constants::DEFAULT_CUSTOM_COLUMNS),\n        );\n        let tui_custom_columns = TuiColumns::try_from(columns.as_str())?;\n        let tui_icmp_extension_mode = cfg_layer(\n            args.tui_icmp_extension_mode,\n            cfg_file_tui.tui_icmp_extension_mode,\n            constants::DEFAULT_TUI_ICMP_EXTENSION_MODE,\n        );\n        let tui_geoip_mode = cfg_layer(\n            args.tui_geoip_mode,\n            cfg_file_tui.tui_geoip_mode,\n            constants::DEFAULT_TUI_GEOIP_MODE,\n        );\n        let tui_max_addrs = cfg_layer_opt(args.tui_max_addrs, cfg_file_tui.tui_max_addrs);\n        let dns_resolve_method_config = cfg_layer(\n            args.dns_resolve_method,\n            cfg_file_dns.dns_resolve_method,\n            constants::DEFAULT_DNS_RESOLVE_METHOD,\n        );\n        let tui_locale = cfg_layer_opt(args.tui_locale, cfg_file_tui.tui_locale);\n        let timezone = cfg_layer_opt(args.tui_timezone, cfg_file_tui.tui_timezone);\n        let tui_timezone = timezone\n            .as_deref()\n            .map(chrono_tz::Tz::from_str)\n            .transpose()?;\n        let dns_lookup_as_info = cfg_layer_bool_flag(\n            args.dns_lookup_as_info,\n            cfg_file_dns.dns_lookup_as_info,\n            constants::DEFAULT_DNS_LOOKUP_AS_INFO,\n        );\n        let dns_timeout = cfg_layer(\n            args.dns_timeout,\n            cfg_file_dns.dns_timeout,\n            constants::DEFAULT_DNS_TIMEOUT,\n        );\n        let dns_ttl = cfg_layer(\n            args.dns_ttl,\n            cfg_file_dns.dns_ttl,\n            constants::DEFAULT_DNS_TTL,\n        );\n        let report_cycles = cfg_layer(\n            args.report_cycles,\n            cfg_file_report.report_cycles,\n            constants::DEFAULT_REPORT_CYCLES,\n        );\n        let geoip_mmdb_file = cfg_layer_opt(args.geoip_mmdb_file, cfg_file_tui.geoip_mmdb_file);\n        let protocol = match (args.udp, args.tcp, args.icmp, protocol) {\n            (false, false, false, ProtocolConfig::Udp) | (true, _, _, _) => Protocol::Udp,\n            (false, false, false, ProtocolConfig::Tcp) | (_, true, _, _) => Protocol::Tcp,\n            (false, false, false, ProtocolConfig::Icmp) | (_, _, true, _) => Protocol::Icmp,\n        };\n        #[expect(clippy::match_same_arms)]\n        let addr_family = match (\n            args.ipv4,\n            args.ipv6,\n            addr_family_cfg,\n            multipath_strategy_cfg,\n        ) {\n            (false, false, AddressFamilyConfig::Ipv4, _) => IpAddrFamily::Ipv4Only,\n            (false, false, AddressFamilyConfig::Ipv6, _) => IpAddrFamily::Ipv6Only,\n            (false, false, AddressFamilyConfig::Ipv4ThenIpv6, _) => IpAddrFamily::Ipv4thenIpv6,\n            (false, false, AddressFamilyConfig::Ipv6ThenIpv4, _) => IpAddrFamily::Ipv6thenIpv4,\n            (false, false, AddressFamilyConfig::System, _) => IpAddrFamily::System,\n            (true, _, _, _) => IpAddrFamily::Ipv4Only,\n            (_, true, _, _) => IpAddrFamily::Ipv6Only,\n        };\n        let multipath_strategy = match multipath_strategy_cfg {\n            MultipathStrategyConfig::Classic => MultipathStrategy::Classic,\n            MultipathStrategyConfig::Paris => MultipathStrategy::Paris,\n            MultipathStrategyConfig::Dublin => MultipathStrategy::Dublin,\n        };\n        let port_direction = match (protocol, source_port, target_port, multipath_strategy_cfg) {\n            (Protocol::Icmp, _, _, _) => PortDirection::None,\n            (Protocol::Udp, None, None, _) => PortDirection::new_fixed_src(pid.max(1024)),\n            (Protocol::Udp, Some(src), None, _) => {\n                validate_source_port(src)?;\n                PortDirection::new_fixed_src(src)\n            }\n            (Protocol::Tcp, None, None, _) => PortDirection::new_fixed_dest(80),\n            (Protocol::Tcp, Some(src), None, _) => PortDirection::new_fixed_src(src),\n            (_, None, Some(dest), _) => PortDirection::new_fixed_dest(dest),\n            (\n                Protocol::Udp,\n                Some(src),\n                Some(dest),\n                MultipathStrategyConfig::Dublin | MultipathStrategyConfig::Paris,\n            ) => {\n                validate_source_port(src)?;\n                PortDirection::new_fixed_both(src, dest)\n            }\n            (_, Some(_), Some(_), _) => {\n                return Err(anyhow!(\n                    \"only one of source-port and target-port may be fixed (except IPv4/udp protocol with dublin or paris strategy)\"\n                ));\n            }\n        };\n        let dns_resolve_method = match dns_resolve_method_config {\n            DnsResolveMethodConfig::System => ResolveMethod::System,\n            DnsResolveMethodConfig::Resolv => ResolveMethod::Resolv,\n            DnsResolveMethodConfig::Google => ResolveMethod::Google,\n            DnsResolveMethodConfig::Cloudflare => ResolveMethod::Cloudflare,\n        };\n        let max_rounds = match mode {\n            Mode::Stream | Mode::Tui => None,\n            Mode::Pretty\n            | Mode::Markdown\n            | Mode::Csv\n            | Mode::Json\n            | Mode::Dot\n            | Mode::Flows\n            | Mode::Silent => Some(report_cycles),\n        };\n        let tui_max_addrs = match tui_max_addrs {\n            Some(n) if n > 0 => Some(n),\n            _ => None,\n        };\n        validate_privilege(privilege_mode, has_privileges, needs_privileges)?;\n        validate_logging(mode, verbose)?;\n        validate_strategy(multipath_strategy, unprivileged)?;\n        validate_protocol_strategy(protocol, multipath_strategy)?;\n        validate_multi(mode, protocol, &args.targets, dns_resolve_all)?;\n        validate_flows(mode, multipath_strategy)?;\n        validate_ttl(first_ttl, max_ttl)?;\n        validate_max_inflight(max_inflight)?;\n        validate_read_timeout(read_timeout)?;\n        validate_round_duration(min_round_duration, max_round_duration)?;\n        validate_grace_duration(grace_duration)?;\n        validate_packet_size(addr_family, packet_size)?;\n        validate_tos(addr_family, tos)?;\n        validate_tui_refresh_rate(tui_refresh_rate)?;\n        validate_report_cycles(report_cycles)?;\n        validate_dns(dns_resolve_method, dns_lookup_as_info)?;\n        validate_geoip(tui_geoip_mode, geoip_mmdb_file.as_ref())?;\n        validate_tui_custom_columns(&tui_custom_columns)?;\n        let tui_theme_items = args\n            .tui_theme_colors\n            .into_iter()\n            .collect::<HashMap<TuiThemeItem, TuiColor>>();\n        let tui_theme = TuiTheme::from((tui_theme_items, cfg_file_tui_theme_colors));\n        let tui_binding_items = args\n            .tui_key_bindings\n            .into_iter()\n            .collect::<HashMap<TuiCommandItem, TuiKeyBinding>>();\n        let tui_bindings = TuiBindings::from((tui_binding_items, cfg_file_tui_bindings));\n        validate_bindings(&tui_bindings)?;\n        Ok(Self {\n            targets: args.targets,\n            protocol,\n            addr_family,\n            first_ttl,\n            max_ttl,\n            min_round_duration,\n            max_round_duration,\n            grace_duration,\n            max_inflight,\n            initial_sequence,\n            multipath_strategy,\n            read_timeout,\n            packet_size,\n            payload_pattern,\n            tos,\n            icmp_extension_parse_mode,\n            source_addr,\n            interface,\n            port_direction,\n            dns_timeout,\n            dns_ttl,\n            dns_resolve_method,\n            dns_lookup_as_info,\n            max_samples,\n            max_flows,\n            tui_preserve_screen,\n            tui_refresh_rate,\n            tui_privacy_max_ttl,\n            tui_address_mode,\n            tui_as_mode,\n            tui_custom_columns,\n            tui_icmp_extension_mode,\n            tui_geoip_mode,\n            tui_max_addrs,\n            tui_locale,\n            tui_timezone,\n            tui_theme,\n            tui_bindings,\n            mode,\n            privilege_mode,\n            dns_resolve_all,\n            report_cycles,\n            geoip_mmdb_file,\n            max_rounds,\n            verbose,\n            log_format,\n            log_filter,\n            log_span_events,\n        })\n    }\n}\n\nimpl Default for TrippyConfig {\n    fn default() -> Self {\n        Self {\n            targets: vec![],\n            protocol: defaults::DEFAULT_STRATEGY_PROTOCOL,\n            addr_family: dns_resolve_family(constants::DEFAULT_ADDR_FAMILY),\n            first_ttl: defaults::DEFAULT_STRATEGY_FIRST_TTL,\n            max_ttl: defaults::DEFAULT_STRATEGY_MAX_TTL,\n            min_round_duration: defaults::DEFAULT_STRATEGY_MIN_ROUND_DURATION,\n            max_round_duration: defaults::DEFAULT_STRATEGY_MAX_ROUND_DURATION,\n            grace_duration: defaults::DEFAULT_STRATEGY_GRACE_DURATION,\n            max_inflight: defaults::DEFAULT_STRATEGY_MAX_INFLIGHT,\n            initial_sequence: defaults::DEFAULT_STRATEGY_INITIAL_SEQUENCE,\n            tos: defaults::DEFAULT_STRATEGY_TOS,\n            icmp_extension_parse_mode: defaults::DEFAULT_ICMP_EXTENSION_PARSE_MODE,\n            read_timeout: defaults::DEFAULT_STRATEGY_READ_TIMEOUT,\n            packet_size: defaults::DEFAULT_STRATEGY_PACKET_SIZE,\n            payload_pattern: defaults::DEFAULT_STRATEGY_PAYLOAD_PATTERN,\n            source_addr: None,\n            interface: None,\n            multipath_strategy: defaults::DEFAULT_STRATEGY_MULTIPATH,\n            port_direction: PortDirection::None,\n            dns_timeout: constants::DEFAULT_DNS_TIMEOUT,\n            dns_ttl: constants::DEFAULT_DNS_TTL,\n            dns_resolve_method: dns_resolve_method(constants::DEFAULT_DNS_RESOLVE_METHOD),\n            dns_lookup_as_info: constants::DEFAULT_DNS_LOOKUP_AS_INFO,\n            max_samples: defaults::DEFAULT_MAX_SAMPLES,\n            max_flows: defaults::DEFAULT_MAX_FLOWS,\n            tui_preserve_screen: constants::DEFAULT_TUI_PRESERVE_SCREEN,\n            tui_refresh_rate: constants::DEFAULT_TUI_REFRESH_RATE,\n            tui_privacy_max_ttl: None,\n            tui_address_mode: constants::DEFAULT_TUI_ADDRESS_MODE,\n            tui_as_mode: constants::DEFAULT_TUI_AS_MODE,\n            tui_icmp_extension_mode: constants::DEFAULT_TUI_ICMP_EXTENSION_MODE,\n            tui_geoip_mode: constants::DEFAULT_TUI_GEOIP_MODE,\n            tui_max_addrs: None,\n            tui_locale: None,\n            tui_timezone: None,\n            tui_theme: TuiTheme::default(),\n            tui_bindings: TuiBindings::default(),\n            mode: constants::DEFAULT_MODE,\n            privilege_mode: defaults::DEFAULT_PRIVILEGE_MODE,\n            dns_resolve_all: constants::DEFAULT_DNS_RESOLVE_ALL,\n            report_cycles: constants::DEFAULT_REPORT_CYCLES,\n            geoip_mmdb_file: None,\n            max_rounds: None,\n            verbose: false,\n            log_format: constants::DEFAULT_LOG_FORMAT,\n            log_filter: String::from(constants::DEFAULT_LOG_FILTER),\n            log_span_events: constants::DEFAULT_LOG_SPAN_EVENTS,\n            tui_custom_columns: TuiColumns::default(),\n        }\n    }\n}\n\nconst fn dns_resolve_method(dns_resolve_method: DnsResolveMethodConfig) -> ResolveMethod {\n    match dns_resolve_method {\n        DnsResolveMethodConfig::System => ResolveMethod::System,\n        DnsResolveMethodConfig::Resolv => ResolveMethod::Resolv,\n        DnsResolveMethodConfig::Google => ResolveMethod::Google,\n        DnsResolveMethodConfig::Cloudflare => ResolveMethod::Cloudflare,\n    }\n}\n\nconst fn dns_resolve_family(dns_resolve_family: AddressFamilyConfig) -> IpAddrFamily {\n    match dns_resolve_family {\n        AddressFamilyConfig::Ipv4 => IpAddrFamily::Ipv4Only,\n        AddressFamilyConfig::Ipv6 => IpAddrFamily::Ipv6Only,\n        AddressFamilyConfig::Ipv6ThenIpv4 => IpAddrFamily::Ipv6thenIpv4,\n        AddressFamilyConfig::Ipv4ThenIpv6 => IpAddrFamily::Ipv4thenIpv6,\n        AddressFamilyConfig::System => IpAddrFamily::System,\n    }\n}\n\nfn cfg_layer<T>(fst: Option<T>, snd: Option<T>, def: T) -> T {\n    match (fst, snd) {\n        (Some(val), _) | (None, Some(val)) => val,\n        (None, None) => def,\n    }\n}\n\nfn cfg_layer_opt<T>(fst: Option<T>, snd: Option<T>) -> Option<T> {\n    match (fst, snd) {\n        (Some(val), _) | (None, Some(val)) => Some(val),\n        (None, None) => None,\n    }\n}\n\nconst fn cfg_layer_bool_flag(fst: bool, snd: Option<bool>, default: bool) -> bool {\n    match (fst, snd) {\n        (true, _) => true,\n        (false, Some(val)) => val,\n        (false, None) => default,\n    }\n}\n\n/// Check for deprecated fields.\nfn validate_deprecated(\n    cfg_file_tui: &ConfigTui,\n    cfg_file_tui_bindings: &ConfigBindings,\n) -> anyhow::Result<()> {\n    if cfg_file_tui.deprecated_tui_max_samples.is_some() {\n        Err(anyhow!(\n            \"tui-max-samples in [tui] section is deprecated, use max-samples in [strategy] section instead\"\n        ))\n    } else if cfg_file_tui.deprecated_tui_max_flows.is_some() {\n        Err(anyhow!(\n            \"tui-max-flows in [tui] section is deprecated, use max-flows in [strategy] section instead\"\n        ))\n    } else if cfg_file_tui_bindings.deprecated_toggle_privacy.is_some() {\n        Err(anyhow!(\n            \"toggle-privacy in [bindings] section is deprecated, use expand-privacy and contract-privacy instead\"\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate privileges.\nfn validate_privilege(\n    privilege_mode: PrivilegeMode,\n    has_privileges: bool,\n    needs_privileges: bool,\n) -> anyhow::Result<()> {\n    const PRIVILEGE_URL: &str = \"https://github.com/fujiapple852/trippy#privileges\";\n    match (privilege_mode, has_privileges, needs_privileges) {\n        (PrivilegeMode::Privileged, true, _) | (PrivilegeMode::Unprivileged, _, false) => Ok(()),\n        (PrivilegeMode::Privileged, false, true) => Err(anyhow!(format!(\n            \"privileges are required\\n\\nsee {PRIVILEGE_URL} for details\"\n        ))),\n        (PrivilegeMode::Privileged, false, false) => Err(anyhow!(format!(\n            \"privileges are required (hint: try adding -u to run in unprivileged mode)\\n\\nsee {PRIVILEGE_URL} for details\"\n        ))),\n        (PrivilegeMode::Unprivileged, false, true) => Err(anyhow!(format!(\n            \"unprivileged mode not supported on this platform\\n\\nsee {PRIVILEGE_URL} for details\"\n        ))),\n        (PrivilegeMode::Unprivileged, true, true) => Err(anyhow!(format!(\n            \"unprivileged mode not supported on this platform (hint: process is privileged so disable unprivileged mode)\\n\\nsee {PRIVILEGE_URL} for details\"\n        ))),\n    }\n}\n\n/// Validate the TUI custom columns.\nfn validate_tui_custom_columns(tui_custom_columns: &TuiColumns) -> anyhow::Result<()> {\n    let duplicates = tui_custom_columns.find_duplicates();\n    if tui_custom_columns.0.is_empty() {\n        Err(anyhow!(\n            \"Missing or no custom columns - The command line or config file value is blank\"\n        ))\n    } else if duplicates.is_empty() {\n        Ok(())\n    } else {\n        let dup_str = duplicates.iter().join(\", \");\n        Err(anyhow!(\"Duplicate custom columns: {dup_str}\"))\n    }\n}\n\n/// Validate the logging mode.\nfn validate_logging(mode: Mode, verbose: bool) -> anyhow::Result<()> {\n    if matches!(mode, Mode::Tui) && verbose {\n        Err(anyhow!(\"cannot enable verbose logging in tui mode\"))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate the multipath strategy against the privilege mode.\nfn validate_strategy(strategy: MultipathStrategy, unprivileged: bool) -> anyhow::Result<()> {\n    match (strategy, unprivileged) {\n        (MultipathStrategy::Dublin, true) => Err(anyhow!(\n            \"Dublin tracing strategy cannot be used in unprivileged mode\"\n        )),\n        (MultipathStrategy::Paris, true) => Err(anyhow!(\n            \"Paris tracing strategy cannot be used in unprivileged mode\"\n        )),\n        _ => Ok(()),\n    }\n}\n\n/// Validate the protocol against the multipath strategy.\nfn validate_protocol_strategy(\n    protocol: Protocol,\n    strategy: MultipathStrategy,\n) -> anyhow::Result<()> {\n    match (protocol, strategy) {\n        (Protocol::Tcp | Protocol::Icmp, MultipathStrategy::Classic) | (Protocol::Udp, _) => Ok(()),\n        (Protocol::Icmp, MultipathStrategy::Paris) => {\n            Err(anyhow!(\"Paris multipath strategy not support for icmp\"))\n        }\n        (Protocol::Icmp, MultipathStrategy::Dublin) => {\n            Err(anyhow!(\"Dublin multipath strategy not support for icmp\"))\n        }\n        (Protocol::Tcp, MultipathStrategy::Paris) => Err(anyhow!(\n            \"Paris multipath strategy not yet supported for tcp\"\n        )),\n        (Protocol::Tcp, MultipathStrategy::Dublin) => Err(anyhow!(\n            \"Dublin multipath strategy not yet supported for tcp\"\n        )),\n    }\n}\n\n/// We only allow multiple targets to be specified for the Tui and for `Icmp` tracing.\nfn validate_multi(\n    mode: Mode,\n    protocol: Protocol,\n    targets: &[String],\n    dns_resolve_all: bool,\n) -> anyhow::Result<()> {\n    match (mode, protocol) {\n        (Mode::Stream | Mode::Pretty | Mode::Markdown | Mode::Csv | Mode::Json, _)\n            if targets.len() > 1 || dns_resolve_all =>\n        {\n            Err(anyhow!(\n                \"only a single target may be specified for this mode\"\n            ))\n        }\n        (_, Protocol::Tcp | Protocol::Udp) if targets.len() > 1 || dns_resolve_all => Err(anyhow!(\n            \"only a single target may be specified for TCP and UDP tracing\"\n        )),\n        _ => Ok(()),\n    }\n}\n\n/// Validate that flows and dot mode are only used with paris or dublin\n/// multipath strategy.\nfn validate_flows(mode: Mode, strategy: MultipathStrategy) -> anyhow::Result<()> {\n    match (mode, strategy) {\n        (Mode::Flows | Mode::Dot, MultipathStrategy::Classic) => Err(anyhow!(\n            \"this mode requires the paris or dublin multipath strategy\"\n        )),\n        _ => Ok(()),\n    }\n}\n\n/// Validate `first_ttl` and `max_ttl`.\nfn validate_ttl(first_ttl: u8, max_ttl: u8) -> anyhow::Result<()> {\n    if !(1..=MAX_TTL).contains(&first_ttl) {\n        Err(anyhow!(\n            \"first-ttl ({first_ttl}) must be in the range 1..{MAX_TTL}\"\n        ))\n    } else if !(1..=MAX_TTL).contains(&max_ttl) {\n        Err(anyhow!(\n            \"max-ttl ({max_ttl}) must be in the range 1..{MAX_TTL}\"\n        ))\n    } else if first_ttl > max_ttl {\n        Err(anyhow!(\n            \"first-ttl ({first_ttl}) must be less than or equal to max-ttl ({max_ttl})\"\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate `max_inflight`.\nfn validate_max_inflight(max_inflight: u8) -> anyhow::Result<()> {\n    if max_inflight == 0 {\n        Err(anyhow!(\n            \"max-inflight ({max_inflight}) must be greater than zero\"\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate `read_timeout`.\nfn validate_read_timeout(read_timeout: Duration) -> anyhow::Result<()> {\n    if read_timeout < constants::MIN_READ_TIMEOUT_MS\n        || read_timeout > constants::MAX_READ_TIMEOUT_MS\n    {\n        Err(anyhow!(\n            \"read-timeout ({:?}) must be between {:?} and {:?} inclusive\",\n            read_timeout,\n            constants::MIN_READ_TIMEOUT_MS,\n            constants::MAX_READ_TIMEOUT_MS\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate `min_round_duration` and `max_round_duration`.\nfn validate_round_duration(\n    min_round_duration: Duration,\n    max_round_duration: Duration,\n) -> anyhow::Result<()> {\n    if min_round_duration > max_round_duration {\n        Err(anyhow!(\n            \"max-round-duration ({max_round_duration:?}) must not be less than min-round-duration ({min_round_duration:?})\"\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate `grace_duration`.\nfn validate_grace_duration(grace_duration: Duration) -> anyhow::Result<()> {\n    if grace_duration < constants::MIN_GRACE_DURATION_MS\n        || grace_duration > constants::MAX_GRACE_DURATION_MS\n    {\n        Err(anyhow!(\n            \"grace-duration ({:?}) must be between {:?} and {:?} inclusive\",\n            grace_duration,\n            constants::MIN_GRACE_DURATION_MS,\n            constants::MAX_GRACE_DURATION_MS\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate `packet_size`.\nfn validate_packet_size(address_family: IpAddrFamily, packet_size: u16) -> anyhow::Result<()> {\n    let min_size = match address_family {\n        IpAddrFamily::Ipv4Only => constants::MIN_PACKET_SIZE_IPV4,\n        IpAddrFamily::Ipv6Only\n        | IpAddrFamily::Ipv6thenIpv4\n        | IpAddrFamily::Ipv4thenIpv6\n        | IpAddrFamily::System => constants::MIN_PACKET_SIZE_IPV6,\n    };\n    if (min_size..=constants::MAX_PACKET_SIZE).contains(&packet_size) {\n        Ok(())\n    } else {\n        Err(anyhow!(\n            \"packet-size ({}) must be between {} and {} inclusive for {}\",\n            packet_size,\n            min_size,\n            constants::MAX_PACKET_SIZE,\n            address_family,\n        ))\n    }\n}\n\n/// Validate `source_port`.\nfn validate_source_port(source_port: u16) -> anyhow::Result<()> {\n    if source_port < 1024 {\n        Err(anyhow!(\"source-port ({source_port}) must be >= 1024\"))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate `tui_refresh_rate`.\nfn validate_tui_refresh_rate(tui_refresh_rate: Duration) -> anyhow::Result<()> {\n    if tui_refresh_rate < constants::TUI_MIN_REFRESH_RATE_MS\n        || tui_refresh_rate > constants::TUI_MAX_REFRESH_RATE_MS\n    {\n        Err(anyhow!(\n            \"tui-refresh-rate ({:?}) must be between {:?} and {:?} inclusive\",\n            tui_refresh_rate,\n            constants::TUI_MIN_REFRESH_RATE_MS,\n            constants::TUI_MAX_REFRESH_RATE_MS\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate `report_cycles`.\nfn validate_report_cycles(report_cycles: usize) -> anyhow::Result<()> {\n    if report_cycles == 0 {\n        Err(anyhow!(\n            \"report-cycles ({report_cycles}) must be greater than zero\"\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate `dns_resolve_method` and `dns_lookup_as_info`.\nfn validate_dns(dns_resolve_method: ResolveMethod, dns_lookup_as_info: bool) -> anyhow::Result<()> {\n    match dns_resolve_method {\n        ResolveMethod::System if dns_lookup_as_info => Err(anyhow!(\n            \"AS lookup not supported by resolver `system` (use '-r' to choose another resolver)\"\n        )),\n        _ => Ok(()),\n    }\n}\n\nfn validate_geoip(\n    tui_geoip_mode: GeoIpMode,\n    geoip_mmdb_file: Option<&String>,\n) -> anyhow::Result<()> {\n    if matches!(\n        tui_geoip_mode,\n        GeoIpMode::Short | GeoIpMode::Long | GeoIpMode::Location\n    ) && geoip_mmdb_file.is_none()\n    {\n        Err(anyhow!(\n            \"geoip-mmdb-file must be given for tui-geoip-mode of `{tui_geoip_mode:?}`\"\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n/// Validate key bindings.\nfn validate_bindings(bindings: &TuiBindings) -> anyhow::Result<()> {\n    let duplicates = bindings.find_duplicates();\n    if duplicates.is_empty() {\n        Ok(())\n    } else {\n        let dup_str = duplicates.iter().join(\", \");\n        Err(anyhow!(\"Duplicate key bindings: {dup_str}\"))\n    }\n}\n\n/// Validate `tos`.\nfn validate_tos(address_family: IpAddrFamily, tos: u8) -> anyhow::Result<()> {\n    if cfg!(target_os = \"windows\") && address_family != IpAddrFamily::Ipv4Only && tos != 0 {\n        Err(anyhow!(\n            \"setting tos is only supported for IPv4 on Windows (hint: try setting --ipv4 to enforce IPv4)\"\n        ))\n    } else {\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::util::{insta, remove_whitespace};\n    use crossterm::event::KeyCode;\n    use std::net::{Ipv4Addr, Ipv6Addr};\n    use std::str::FromStr;\n    use test_case::test_case;\n    use trippy_core::Port;\n\n    #[test]\n    fn test_config_default() {\n        let args = args(&[\"trip\", \"example.com\"]).unwrap();\n        let cfg_file = ConfigFile::default();\n        let platform = dummy_platform();\n        let config = TrippyConfig::build_config(args, cfg_file, &platform, 0).unwrap();\n        let expected = TrippyConfig {\n            targets: vec![String::from(\"example.com\")],\n            ..TrippyConfig::default()\n        };\n        pretty_assertions::assert_eq!(expected, config);\n    }\n\n    #[test]\n    fn test_config_sample() {\n        let args = args(&[\"trip\", \"example.com\"]).unwrap();\n        let cfg_file: ConfigFile =\n            toml::from_str(include_str!(\"../trippy-config-sample.toml\")).unwrap();\n        let platform = dummy_platform();\n        let config = TrippyConfig::build_config(args, cfg_file, &platform, 0).unwrap();\n        let expected = TrippyConfig {\n            targets: vec![String::from(\"example.com\")],\n            ..TrippyConfig::default()\n        };\n        pretty_assertions::assert_eq!(expected, config);\n    }\n\n    #[test_case(\"trip\"; \"show default help\")]\n    #[test_case(\"trip -h\"; \"show short help\")]\n    #[test_case(\"trip --help\"; \"show long help\")]\n    fn test_help(cmd: &str) {\n        compare_snapshot(cmd, parse_config(cmd));\n    }\n\n    #[test_case(\"trip --version\", Err(anyhow!(format!(\"trip {}\", clap::crate_version!()))); \"show version\")]\n    #[test_case(\"trip -V\", Err(anyhow!(format!(\"trip {}\", clap::crate_version!()))); \"show version short\")]\n    fn test_version_help(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com --config-file trippy.toml\", Ok(cfg().build()); \"custom config file\")]\n    #[test_case(\"trip example.com -c trippy.toml\", Ok(cfg().build()); \"custom config file short\")]\n    fn test_config(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().mode(Mode::Tui).build()); \"default mode\")]\n    #[test_case(\"trip example.com --mode tui\", Ok(cfg().mode(Mode::Tui).build()); \"tui mode\")]\n    #[test_case(\"trip example.com --mode stream\", Ok(cfg().mode(Mode::Stream).build()); \"stream mode\")]\n    #[test_case(\"trip example.com --mode pretty\", Ok(cfg().mode(Mode::Pretty).max_rounds(Some(10)).build()); \"pretty mode\")]\n    #[test_case(\"trip example.com --mode markdown\", Ok(cfg().mode(Mode::Markdown).max_rounds(Some(10)).build()); \"markdown mode\")]\n    #[test_case(\"trip example.com --mode csv\", Ok(cfg().mode(Mode::Csv).max_rounds(Some(10)).build()); \"csv mode\")]\n    #[test_case(\"trip example.com --mode json\", Ok(cfg().mode(Mode::Json).max_rounds(Some(10)).build()); \"json mode\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --mode silent\", Ok(cfg().mode(Mode::Silent).max_rounds(Some(10)).build()); \"silent mode\")]\n    #[test_case(\"trip example.com -m tui\", Ok(cfg().mode(Mode::Tui).build()); \"tui mode short\")]\n    #[test_case(\"trip example.com --mode foo\", Err(anyhow!(format!(\"error: invalid value 'foo' for '--mode <MODE>' [possible values: tui, stream, pretty, markdown, csv, json, dot, flows, silent] For more information, try '--help'.\"))); \"invalid mode\")]\n    #[test_case(\"trip example.com --mode dot\", Err(anyhow!(format!(\"this mode requires the paris or dublin multipath strategy\"))); \"invalid dot mode\")]\n    #[test_case(\"trip example.com --mode flows\", Err(anyhow!(format!(\"this mode requires the paris or dublin multipath strategy\"))); \"invalid flows mode\")]\n    fn test_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().build()); \"single target\")]\n    #[test_case(\"trip example.com foo.com bar.com\", Ok(cfg_multi().build()); \"multiple targets\")]\n    #[test_case(\"trip example.com -U 20\", Ok(cfg().max_inflight(20).build()); \"single target before args\")]\n    #[test_case(\"trip -U 20 example.com\", Ok(cfg().max_inflight(20).build()); \"single target after args\")]\n    #[test_case(\"trip example.com foo.com bar.com -U 20\", Ok(cfg_multi().max_inflight(20).build()); \"multiple targets before args\")]\n    #[test_case(\"trip -U 20 example.com foo.com bar.com\", Ok(cfg_multi().max_inflight(20).build()); \"multiple targets after args\")]\n    #[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\")]\n    fn test_target(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com --dummy\", Err(anyhow!(\"error: unexpected argument '--dummy' found\")); \"invalid argument\")]\n    fn test_unexpected(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare_lines(parse_config(cmd), expected, Some(1));\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); \"default strategy\")]\n    #[test_case(\"trip example.com --multipath-strategy classic\", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); \"classic strategy\")]\n    #[test_case(\"trip example.com -R classic\", Ok(cfg().multipath_strategy(MultipathStrategy::Classic).build()); \"classic strategy short\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --multipath-strategy tokyo\", Err(anyhow!(\"error: invalid value 'tokyo' for '--multipath-strategy <MULTIPATH_STRATEGY>' [possible values: classic, paris, dublin] For more information, try '--help'.\")); \"invalid strategy\")]\n    #[test_case(\"trip example.com --icmp --multipath-strategy paris\", Err(anyhow!(\"Paris multipath strategy not support for icmp\")); \"paris with invalid protocol icmp\")]\n    #[test_case(\"trip example.com --icmp --multipath-strategy dublin\", Err(anyhow!(\"Dublin multipath strategy not support for icmp\")); \"dublin with invalid protocol icmp\")]\n    #[test_case(\"trip example.com --tcp --multipath-strategy paris\", Err(anyhow!(\"Paris multipath strategy not yet supported for tcp\")); \"paris with invalid protocol tcp\")]\n    #[test_case(\"trip example.com --tcp --multipath-strategy dublin\", Err(anyhow!(\"Dublin multipath strategy not yet supported for tcp\")); \"dublin with invalid protocol tcp\")]\n    fn test_multipath(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); \"default protocol\")]\n    #[test_case(\"trip example.com --protocol icmp\", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); \"icmp protocol\")]\n    #[test_case(\"trip example.com --protocol udp\", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); \"udp protocol\")]\n    #[test_case(\"trip example.com --protocol tcp\", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); \"tcp protocol\")]\n    #[test_case(\"trip example.com --protocol foo\", Err(anyhow!(\"error: invalid value 'foo' for '--protocol <PROTOCOL>' [possible values: icmp, udp, tcp] For more information, try '--help'.\")); \"invalid protocol\")]\n    #[test_case(\"trip example.com -p icmp\", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); \"icmp protocol short\")]\n    #[test_case(\"trip example.com -p udp\", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); \"udp protocol short\")]\n    #[test_case(\"trip example.com -p tcp\", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); \"tcp protocol short\")]\n    #[test_case(\"trip example.com -p foo\", Err(anyhow!(\"error: invalid value 'foo' for '--protocol <PROTOCOL>' [possible values: icmp, udp, tcp] For more information, try '--help'.\")); \"invalid protocol short\")]\n    #[test_case(\"trip example.com --icmp\", Ok(cfg().protocol(Protocol::Icmp).port_direction(PortDirection::None).build()); \"icmp protocol shortcut\")]\n    #[test_case(\"trip example.com --udp\", Ok(cfg().protocol(Protocol::Udp).port_direction(PortDirection::FixedSrc(Port(1024))).build()); \"udp protocol shortcut\")]\n    #[test_case(\"trip example.com --tcp\", Ok(cfg().protocol(Protocol::Tcp).port_direction(PortDirection::FixedDest(Port(80))).build()); \"tcp protocol shortcut\")]\n    fn test_protocol(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --udp --source-port 123\", Err(anyhow!(\"source-port (123) must be >= 1024\")); \"udp protocol invalid src port\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    fn test_ports(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().addr_family(IpAddrFamily::System).build()); \"default address family\")]\n    #[test_case(\"trip example.com --addr-family ipv4\", Ok(cfg().addr_family(IpAddrFamily::Ipv4Only).build()); \"ipv4 address family\")]\n    #[test_case(\"trip example.com --addr-family ipv6\", Ok(cfg().addr_family(IpAddrFamily::Ipv6Only).build()); \"ipv6 address family\")]\n    #[test_case(\"trip example.com --addr-family ipv4-then-ipv6\", Ok(cfg().addr_family(IpAddrFamily::Ipv4thenIpv6).build()); \"ipv4 then ipv6 address family\")]\n    #[test_case(\"trip example.com --addr-family ipv6-then-ipv4\", Ok(cfg().addr_family(IpAddrFamily::Ipv6thenIpv4).build()); \"ipv6 then ipv4 address family\")]\n    #[test_case(\"trip example.com --addr-family system\", Ok(cfg().addr_family(IpAddrFamily::System).build()); \"system address family\")]\n    #[test_case(\"trip example.com -F ipv4\", Ok(cfg().addr_family(IpAddrFamily::Ipv4Only).build()); \"custom address family short\")]\n    #[test_case(\"trip example.com --addr-family foo\", Err(anyhow!(\"error: invalid value 'foo' for '--addr-family <ADDR_FAMILY>' [possible values: ipv4, ipv6, ipv6-then-ipv4, ipv4-then-ipv6, system] For more information, try '--help'.\")); \"invalid address family\")]\n    #[test_case(\"trip example.com -4\", Ok(cfg().addr_family(IpAddrFamily::Ipv4Only).build()); \"ipv4 address family shortcut\")]\n    #[test_case(\"trip example.com -6\", Ok(cfg().addr_family(IpAddrFamily::Ipv6Only).build()); \"ipv6 address family shortcut\")]\n    #[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\")]\n    fn test_addr_family(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().first_ttl(1).build()); \"default first ttl\")]\n    #[test_case(\"trip example.com --first-ttl 5\", Ok(cfg().first_ttl(5).build()); \"custom first ttl\")]\n    #[test_case(\"trip example.com -f 5\", Ok(cfg().first_ttl(5).build()); \"custom first ttl short\")]\n    #[test_case(\"trip example.com --first-ttl 0\", Err(anyhow!(\"first-ttl (0) must be in the range 1..254\")); \"invalid low first ttl\")]\n    #[test_case(\"trip example.com --first-ttl 500\", Err(anyhow!(\"error: invalid value '500' for '--first-ttl <FIRST_TTL>': 500 is not in 0..=255 For more information, try '--help'.\")); \"invalid high first ttl\")]\n    #[test_case(\"trip example.com\", Ok(cfg().first_ttl(1).build()); \"default max ttl\")]\n    #[test_case(\"trip example.com --max-ttl 5\", Ok(cfg().max_ttl(5).build()); \"custom max ttl\")]\n    #[test_case(\"trip example.com -t 5\", Ok(cfg().max_ttl(5).build()); \"custom max ttl short\")]\n    #[test_case(\"trip example.com --max-ttl 0\", Err(anyhow!(\"max-ttl (0) must be in the range 1..254\")); \"invalid low max ttl\")]\n    #[test_case(\"trip example.com --max-ttl 500\", Err(anyhow!(\"error: invalid value '500' for '--max-ttl <MAX_TTL>': 500 is not in 0..=255 For more information, try '--help'.\")); \"invalid high max ttl\")]\n    #[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\")]\n    #[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\")]\n    fn test_ttl(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().min_round_duration(Duration::from_millis(1000)).build()); \"default min round duration\")]\n    #[test_case(\"trip example.com --min-round-duration 250ms\", Ok(cfg().min_round_duration(Duration::from_millis(250)).build()); \"custom min round duration\")]\n    #[test_case(\"trip example.com -i 250ms\", Ok(cfg().min_round_duration(Duration::from_millis(250)).build()); \"custom min round duration short\")]\n    #[test_case(\"trip example.com --min-round-duration 0\", Ok(cfg().min_round_duration(Duration::from_millis(0)).build()); \"zero min round duration\")]\n    #[test_case(\"trip example.com\", Ok(cfg().min_round_duration(Duration::from_millis(1000)).build()); \"default max round duration\")]\n    #[test_case(\"trip example.com --max-round-duration 1250ms\", Ok(cfg().max_round_duration(Duration::from_millis(1250)).build()); \"custom max round duration\")]\n    #[test_case(\"trip example.com -T 2s\", Ok(cfg().max_round_duration(Duration::from_millis(2000)).build()); \"custom max round duration short\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    fn test_round_duration(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().grace_duration(Duration::from_millis(100)).build()); \"default grace duration\")]\n    #[test_case(\"trip example.com --grace-duration 10ms\", Ok(cfg().grace_duration(Duration::from_millis(10)).build()); \"custom grace duration\")]\n    #[test_case(\"trip example.com -g 50ms\", Ok(cfg().grace_duration(Duration::from_millis(50)).build()); \"custom grace duration short\")]\n    #[test_case(\"trip example.com --grace-duration 0\", Err(anyhow!(\"grace-duration (0ns) must be between 10ms and 1s inclusive\")); \"invalid format grace duration\")]\n    #[test_case(\"trip example.com --grace-duration 9ms\", Err(anyhow!(\"grace-duration (9ms) must be between 10ms and 1s inclusive\")); \"invalid low grace duration\")]\n    #[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\")]\n    fn test_grace_duration(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().max_inflight(24).build()); \"default max inflight\")]\n    #[test_case(\"trip example.com --max-inflight 12\", Ok(cfg().max_inflight(12).build()); \"custom max inflight\")]\n    #[test_case(\"trip example.com -U 20\", Ok(cfg().max_inflight(20).build()); \"custom max inflight short\")]\n    #[test_case(\"trip example.com --max-inflight foo\", Err(anyhow!(\"error: invalid value 'foo' for '--max-inflight <MAX_INFLIGHT>': invalid digit found in string For more information, try '--help'.\")); \"invalid format max inflight\")]\n    #[test_case(\"trip example.com --max-inflight 0\", Err(anyhow!(\"max-inflight (0) must be greater than zero\")); \"invalid low max inflight\")]\n    #[test_case(\"trip example.com --max-inflight 300\", Err(anyhow!(\"error: invalid value '300' for '--max-inflight <MAX_INFLIGHT>': 300 is not in 0..=255 For more information, try '--help'.\")); \"invalid high max inflight\")]\n    fn test_max_inflight(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().initial_sequence(33434).build()); \"default initial sequence\")]\n    #[test_case(\"trip example.com --initial-sequence 5000\", Ok(cfg().initial_sequence(5000).build()); \"custom initial sequence\")]\n    #[test_case(\"trip example.com --initial-sequence foo\", Err(anyhow!(\"error: invalid value 'foo' for '--initial-sequence <INITIAL_SEQUENCE>': invalid digit found in string For more information, try '--help'. \")); \"invalid format initial sequence\")]\n    #[test_case(\"trip example.com --initial-sequence 100000\", Err(anyhow!(\"error: invalid value '100000' for '--initial-sequence <INITIAL_SEQUENCE>': 100000 is not in 0..=65535 For more information, try '--help'.\")); \"invalid high initial sequence\")]\n    fn test_init_seq(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tos(0).build()); \"default tos\")]\n    #[test_case(\"trip example.com --tos 255 -4\", Ok(cfg().tos(0xFF).addr_family(IpAddrFamily::Ipv4Only).build()); \"custom tos\")]\n    #[test_case(\"trip example.com -Q 255 -4\", Ok(cfg().tos(0xFF).addr_family(IpAddrFamily::Ipv4Only).build()); \"custom tos short\")]\n    #[test_case(\"trip example.com --tos foo\", Err(anyhow!(\"error: invalid value 'foo' for '--tos <TOS>': invalid digit found in string For more information, try '--help'.\")); \"invalid format tos\")]\n    #[test_case(\"trip example.com --tos 300\", Err(anyhow!(\"error: invalid value '300' for '--tos <TOS>': 300 is not in 0..=255 For more information, try '--help'.\")); \"invalid high tos\")]\n    #[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\"))]\n    fn test_tos(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().icmp_extension_parse_mode(IcmpExtensionParseMode::Disabled).build()); \"default icmp extensions\")]\n    #[test_case(\"trip example.com --icmp-extensions\", Ok(cfg().icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled).build()); \"enabled icmp extensions\")]\n    #[test_case(\"trip example.com -e\", Ok(cfg().icmp_extension_parse_mode(IcmpExtensionParseMode::Enabled).build()); \"enabled icmp extensions short\")]\n    fn test_icmp_extensions(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().read_timeout(Duration::from_millis(10)).build()); \"default read timeout\")]\n    #[test_case(\"trip example.com --read-timeout 20ms\", Ok(cfg().read_timeout(Duration::from_millis(20)).build()); \"custom read timeout\")]\n    #[test_case(\"trip example.com --read-timeout 20\", Err(anyhow!(\"error: invalid value '20' for '--read-timeout <READ_TIMEOUT>': time unit needed, for example 20sec or 20ms For more information, try '--help'.\")); \"invalid custom read timeout\")]\n    #[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\")]\n    #[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\")]\n    fn test_read_timeout(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().packet_size(84).build()); \"default packet size\")]\n    #[test_case(\"trip example.com --packet-size 120\", Ok(cfg().packet_size(120).build()); \"custom packet size\")]\n    #[test_case(\"trip example.com --packet-size foo\", Err(anyhow!(\"error: invalid value 'foo' for '--packet-size <PACKET_SIZE>': invalid digit found in string For more information, try '--help'.\")); \"invalid format packet size\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --packet-size 100000\", Err(anyhow!(\"error: invalid value '100000' for '--packet-size <PACKET_SIZE>': 100000 is not in 0..=65535 For more information, try '--help'.\")); \"invalid out of range packet size\")]\n    fn test_packet_size(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().payload_pattern(0).build()); \"default payload pattern size\")]\n    #[test_case(\"trip example.com --payload-pattern 255\", Ok(cfg().payload_pattern(0xFF).build()); \"custom payload pattern\")]\n    #[test_case(\"trip example.com --payload-pattern foo\", Err(anyhow!(\"error: invalid value 'foo' for '--payload-pattern <PAYLOAD_PATTERN>': invalid digit found in string For more information, try '--help'.\")); \"invalid format payload pattern\")]\n    #[test_case(\"trip example.com --payload-pattern 256\", Err(anyhow!(\"error: invalid value '256' for '--payload-pattern <PAYLOAD_PATTERN>': 256 is not in 0..=255 For more information, try '--help'.\")); \"invalid out of range payload pattern\")]\n    fn test_payload_pattern(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().source_addr(None).build()); \"default source address\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --source-address foobar\", Err(anyhow!(\"error: invalid value 'foobar' for '--source-address <SOURCE_ADDRESS>': invalid IP address syntax For more information, try '--help'.\")); \"invalid source address\")]\n    fn test_source_address(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().interface(None).build()); \"default interface\")]\n    #[test_case(\"trip example.com --interface en0\", Ok(cfg().interface(Some(String::from(\"en0\"))).build()); \"custom interface\")]\n    #[test_case(\"trip example.com -I tun0\", Ok(cfg().interface(Some(String::from(\"tun0\"))).build()); \"custom interface short\")]\n    fn test_interface(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().dns_timeout(Duration::from_millis(5000)).build()); \"default dns timeout\")]\n    #[test_case(\"trip example.com --dns-timeout 20ms\", Ok(cfg().dns_timeout(Duration::from_millis(20)).build()); \"custom dns timeout\")]\n    #[test_case(\"trip example.com --dns-timeout 20\", Err(anyhow!(\"error: invalid value '20' for '--dns-timeout <DNS_TIMEOUT>': time unit needed, for example 20sec or 20ms For more information, try '--help'.\")); \"invalid custom dns timeout\")]\n    fn test_dns_timeout(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().dns_ttl(Duration::from_secs(300)).build()); \"default dns ttl\")]\n    #[test_case(\"trip example.com --dns-ttl 10secs\", Ok(cfg().dns_ttl(Duration::from_secs(10)).build()); \"custom dns ttl\")]\n    #[test_case(\"trip example.com --dns-ttl 20\", Err(anyhow!(\"error: invalid value '20' for '--dns-ttl <DNS_TTL>': time unit needed, for example 20sec or 20ms For more information, try '--help'.\")); \"invalid custom dns ttl\")]\n    fn test_dns_ttl(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().dns_resolve_method(ResolveMethod::System).build()); \"default resolve method\")]\n    #[test_case(\"trip example.com --dns-resolve-method system\", Ok(cfg().dns_resolve_method(ResolveMethod::System).build()); \"custom resolve method system\")]\n    #[test_case(\"trip example.com -r system\", Ok(cfg().dns_resolve_method(ResolveMethod::System).build()); \"custom resolve method system short\")]\n    #[test_case(\"trip example.com --dns-resolve-method google\", Ok(cfg().dns_resolve_method(ResolveMethod::Google).build()); \"custom resolve method google\")]\n    #[test_case(\"trip example.com --dns-resolve-method cloudflare\", Ok(cfg().dns_resolve_method(ResolveMethod::Cloudflare).build()); \"custom resolve method cloudflare\")]\n    #[test_case(\"trip example.com --dns-resolve-method resolv\", Ok(cfg().dns_resolve_method(ResolveMethod::Resolv).build()); \"custom resolve method resolv\")]\n    #[test_case(\"trip example.com --dns-resolve-method foobar\", Err(anyhow!(\"error: invalid value 'foobar' for '--dns-resolve-method <DNS_RESOLVE_METHOD>' [possible values: system, resolv, google, cloudflare] For more information, try '--help'.\")); \"invalid resolve method\")]\n    fn test_dns_resolve(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().dns_resolve_all(false).build()); \"default dns resolve all\")]\n    #[test_case(\"trip example.com --dns-resolve-all\", Ok(cfg().dns_resolve_all(true).build()); \"custom dns resolve all\")]\n    #[test_case(\"trip example.com -y\", Ok(cfg().dns_resolve_all(true).build()); \"custom dns resolve all short\")]\n    fn test_dns_resolve_all(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().dns_lookup_as_info(false).build()); \"default dns lookup as info\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    fn test_lookup_as_info(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().max_samples(256).build()); \"default max samples\")]\n    #[test_case(\"trip example.com --max-samples 100\", Ok(cfg().max_samples(100).build()); \"custom max samples\")]\n    #[test_case(\"trip example.com -s 100\", Ok(cfg().max_samples(100).build()); \"custom max samples short\")]\n    #[test_case(\"trip example.com --max-samples foo\", Err(anyhow!(\"error: invalid value 'foo' for '--max-samples <MAX_SAMPLES>': invalid digit found in string For more information, try '--help'.\")); \"invalid max samples\")]\n    fn test_max_samples(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().max_flows(64).build()); \"default max flows\")]\n    #[test_case(\"trip example.com --max-flows 100\", Ok(cfg().max_flows(100).build()); \"custom max flows\")]\n    #[test_case(\"trip example.com --max-flows foo\", Err(anyhow!(\"error: invalid value 'foo' for '--max-flows <MAX_FLOWS>': invalid digit found in string For more information, try '--help'.\")); \"invalid max flows\")]\n    fn test_max_flows(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_preserve_screen(false).build()); \"default tui preserve screen\")]\n    #[test_case(\"trip example.com --tui-preserve-screen\", Ok(cfg().tui_preserve_screen(true).build()); \"enable tui preserve screen\")]\n    fn test_tui_preserve_screen(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_refresh_rate(Duration::from_millis(100)).build()); \"default tui refresh rate\")]\n    #[test_case(\"trip example.com --tui-refresh-rate 200ms\", Ok(cfg().tui_refresh_rate(Duration::from_millis(200)).build()); \"custom tui refresh rate\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --tui-refresh-rate foo\", Err(anyhow!(\"error: invalid value 'foo' for '--tui-refresh-rate <TUI_REFRESH_RATE>': expected number at 0 For more information, try '--help'.\")); \"invalid format tui refresh rate\")]\n    #[test_case(\"trip example.com --tui-refresh-rate 10xx\", Err(anyhow!(\"error: invalid value '10xx' for '--tui-refresh-rate <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\")]\n    fn test_tui_refresh_rate(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_privacy_max_ttl(None).build()); \"default tui privacy max ttl\")]\n    #[test_case(\"trip example.com --tui-privacy-max-ttl 4\", Ok(cfg().tui_privacy_max_ttl(Some(4)).build()); \"custom tui privacy max ttl\")]\n    #[test_case(\"trip example.com --tui-privacy-max-ttl foo\", Err(anyhow!(\"error: invalid value 'foo' for '--tui-privacy-max-ttl <TUI_PRIVACY_MAX_TTL>': invalid digit found in string For more information, try '--help'.\")); \"invalid tui privacy max ttl\")]\n    fn test_tui_privacy_max_ttl(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_locale(None).build()); \"default tui locale\")]\n    #[test_case(\"trip example.com --tui-locale fr\", Ok(cfg().tui_locale(Some(String::from(\"fr\"))).build()); \"custom tui locale\")]\n    fn test_tui_locale(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_timezone(None).build()); \"default tui timezone\")]\n    #[test_case(\"trip example.com --tui-timezone UTC\", Ok(cfg().tui_timezone(Some(chrono_tz::Tz::UTC)).build()); \"custom tui timezone UTC\")]\n    #[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\")]\n    #[test_case(\"trip example.com --tui-timezone xxx\", Err(anyhow!(\"failed to parse timezone\")); \"invalid tui timezone\")]\n    fn test_tui_timezone(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_address_mode(AddressMode::Host).build()); \"default tui address mode\")]\n    #[test_case(\"trip example.com --tui-address-mode ip\", Ok(cfg().tui_address_mode(AddressMode::Ip).build()); \"ip tui address mode\")]\n    #[test_case(\"trip example.com --tui-address-mode host\", Ok(cfg().tui_address_mode(AddressMode::Host).build()); \"host tui address mode\")]\n    #[test_case(\"trip example.com --tui-address-mode both\", Ok(cfg().tui_address_mode(AddressMode::Both).build()); \"both tui address mode\")]\n    #[test_case(\"trip example.com -a both\", Ok(cfg().tui_address_mode(AddressMode::Both).build()); \"custom tui address mode short\")]\n    #[test_case(\"trip example.com --tui-address-mode foo\", Err(anyhow!(\"error: invalid value 'foo' for '--tui-address-mode <TUI_ADDRESS_MODE>' [possible values: ip, host, both] For more information, try '--help'.\")); \"invalid tui address mode\")]\n    fn test_tui_address_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_as_mode(AsMode::Asn).build()); \"default tui as mode\")]\n    #[test_case(\"trip example.com --tui-as-mode asn\", Ok(cfg().tui_as_mode(AsMode::Asn).build()); \"asn tui as mode\")]\n    #[test_case(\"trip example.com --tui-as-mode prefix\", Ok(cfg().tui_as_mode(AsMode::Prefix).build()); \"prefix tui as mode\")]\n    #[test_case(\"trip example.com --tui-as-mode country-code\", Ok(cfg().tui_as_mode(AsMode::CountryCode).build()); \"country code tui as mode\")]\n    #[test_case(\"trip example.com --tui-as-mode registry\", Ok(cfg().tui_as_mode(AsMode::Registry).build()); \"registry tui as mode\")]\n    #[test_case(\"trip example.com --tui-as-mode allocated\", Ok(cfg().tui_as_mode(AsMode::Allocated).build()); \"allocated tui as mode\")]\n    #[test_case(\"trip example.com --tui-as-mode name\", Ok(cfg().tui_as_mode(AsMode::Name).build()); \"name tui as mode\")]\n    #[test_case(\"trip example.com --tui-as-mode foo\", Err(anyhow!(\"error: invalid value 'foo' for '--tui-as-mode <TUI_AS_MODE>' [possible values: asn, prefix, country-code, registry, allocated, name] For more information, try '--help'.\")); \"invalid tui as mode\")]\n    fn test_tui_as_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_custom_columns(TuiColumns::default()).build()); \"default tui custom columns\")]\n    #[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\")]\n    #[test_case(\"trip example.com --tui-custom-columns hh\", Err(anyhow!(\"Duplicate custom columns: h\")); \"invalid duplicate tui custom columns\")]\n    #[test_case(\"trip example.com --tui-custom-columns u\", Err(anyhow!(\"unknown column code: u\")); \"invalid unknown tui custom columns\")]\n    fn test_tui_custom_columns(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Off).build()); \"default tui icmp extension mode\")]\n    #[test_case(\"trip example.com --tui-icmp-extension-mode off\", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Off).build()); \"off tui icmp extension mode\")]\n    #[test_case(\"trip example.com --tui-icmp-extension-mode mpls\", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Mpls).build()); \"mpls tui icmp extension mode\")]\n    #[test_case(\"trip example.com --tui-icmp-extension-mode full\", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::Full).build()); \"full tui icmp extension mode\")]\n    #[test_case(\"trip example.com --tui-icmp-extension-mode all\", Ok(cfg().tui_icmp_extension_mode(IcmpExtensionMode::All).build()); \"all tui icmp extension mode\")]\n    #[test_case(\"trip example.com --tui-icmp-extension-mode foo\", Err(anyhow!(\"error: invalid value 'foo' for '--tui-icmp-extension-mode <TUI_ICMP_EXTENSION_MODE>' [possible values: off, mpls, full, all] For more information, try '--help'.\")); \"invalid tui icmp extension mode\")]\n    fn test_tui_icmp_extension_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_geoip_mode(GeoIpMode::Off).build()); \"default tui geoip mode\")]\n    #[test_case(\"trip example.com --tui-geoip-mode off\", Ok(cfg().tui_geoip_mode(GeoIpMode::Off).build()); \"off tui geoip mode\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --tui-geoip-mode foo\", Err(anyhow!(\"error: invalid value 'foo' for '--tui-geoip-mode <TUI_GEOIP_MODE>' [possible values: off, short, long, location] For more information, try '--help'.\")); \"invalid tui geoip mode\")]\n    fn test_tui_geoip_mode(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_max_addrs(None).build()); \"default tui max addrs\")]\n    #[test_case(\"trip example.com --tui-max-addrs 5\", Ok(cfg().tui_max_addrs(Some(5)).build()); \"custom tui max addrs\")]\n    #[test_case(\"trip example.com -M 7\", Ok(cfg().tui_max_addrs(Some(7)).build()); \"custom tui max addrs short\")]\n    #[test_case(\"trip example.com --tui-max-addrs foo\", Err(anyhow!(\"error: invalid value 'foo' for '--tui-max-addrs <TUI_MAX_ADDRS>': invalid digit found in string For more information, try '--help'.\")); \"invalid tui max addrs\")]\n    fn test_tui_max_addrs(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_theme(TuiTheme::default()).build()); \"default tui theme\")]\n    #[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\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --tui-theme-colors bg-color=0\", Err(anyhow!(\"error: invalid value 'bg-color=0' for '--tui-theme-colors <TUI_THEME_COLORS>': unknown color: 0 For more information, try '--help'.\")); \"invalid tui theme truncated hex value\")]\n    #[test_case(\"trip example.com --tui-theme-colors bg-color=foo\", Err(anyhow!(\"error: invalid value 'bg-color=foo' for '--tui-theme-colors <TUI_THEME_COLORS>': unknown color: foo For more information, try '--help'. \")); \"invalid tui theme invalid named color\")]\n    #[test_case(\"trip example.com --tui-theme-colors foo-color=red\", Err(anyhow!(\"error: invalid value 'foo-color=red' for '--tui-theme-colors <TUI_THEME_COLORS>': Matching variant not found For more information, try '--help'.\")); \"invalid tui theme invalid item\")]\n    #[test_case(\"trip example.com --tui-theme-colors foo\", Err(anyhow!(\"error: invalid value 'foo' for '--tui-theme-colors <TUI_THEME_COLORS>': invalid theme value: expected format `item=value` For more information, try '--help'.\")); \"invalid tui theme invalid syntax\")]\n    #[test_case(\"trip example.com --tui-theme-colors bg-color=red, text-color=blue\", Err(anyhow!(\"error: invalid value '' for '--tui-theme-colors <TUI_THEME_COLORS>': invalid theme value: expected format `item=value` For more information, try '--help'.\")); \"invalid tui theme invalid multiple with space\")]\n    fn test_tui_theme(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().tui_bindings(TuiBindings::default()).build()); \"default tui bindings\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --tui-key-bindings foo=h\", Err(anyhow!(\"error: invalid value 'foo=h' for '--tui-key-bindings <TUI_KEY_BINDINGS>': Matching variant not found For more information, try '--help'.\")); \"invalid tui binding command\")]\n    #[test_case(\"trip example.com --tui-key-bindings toggle-help=123\", Err(anyhow!(\"error: invalid value 'toggle-help=123' for '--tui-key-bindings <TUI_KEY_BINDINGS>': unknown key binding '123' For more information, try '--help'.\")); \"invalid tui binding key\")]\n    #[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\")]\n    #[test_case(\"trip example.com --tui-key-bindings toggle-help=h, toggle-map=m\", Err(anyhow!(\"error: invalid value '' for '--tui-key-bindings <TUI_KEY_BINDINGS>': invalid binding value: expected format `item=value` For more information, try '--help'.\")); \"invalid tui binding multiple with space\")]\n    fn test_tui_bindings(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().report_cycles(10).max_rounds(None).build()); \"default report cycles\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --report-cycles 0\", Err(anyhow!(\"report-cycles (0) must be greater than zero\")); \"invalid low report cycles\")]\n    #[test_case(\"trip example.com --report-cycles foo\", Err(anyhow!(\"error: invalid value 'foo' for '--report-cycles <REPORT_CYCLES>': invalid digit found in string For more information, try '--help'.\")); \"invalid report cycles\")]\n    fn test_report_cycles(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().geoip_mmdb_file(None).build()); \"default geoip mmdb file\")]\n    #[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\")]\n    #[test_case(\"trip example.com -G foo.mmdb\", Ok(cfg().geoip_mmdb_file(Some(String::from(\"foo.mmdb\"))).build()); \"custom geoip mmdb file short\")]\n    fn test_geoip_mmdb_file(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().verbose(false).build()); \"default verbose\")]\n    #[test_case(\"trip example.com --mode silent --verbose\", Ok(cfg().verbose(true).mode(Mode::Silent).max_rounds(Some(10)).build()); \"enable verbose\")]\n    #[test_case(\"trip example.com --mode silent -v\", Ok(cfg().verbose(true).mode(Mode::Silent).max_rounds(Some(10)).build()); \"enable verbose short\")]\n    #[test_case(\"trip example.com --mode tui --verbose\", Err(anyhow!(\"cannot enable verbose logging in tui mode\")); \"invalid verbose mode\")]\n    fn test_verbose(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().log_filter(String::from(\"trippy=debug\")).build()); \"default log filter\")]\n    #[test_case(\"trip example.com --log-filter info,trippy=trace\", Ok(cfg().log_filter(String::from(\"info,trippy=trace\")).build()); \"custom log filter\")]\n    fn test_log_filter(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().log_format(LogFormat::Pretty).build()); \"default log format\")]\n    #[test_case(\"trip example.com --log-format compact\", Ok(cfg().log_format(LogFormat::Compact).build()); \"compact log format\")]\n    #[test_case(\"trip example.com --log-format pretty\", Ok(cfg().log_format(LogFormat::Pretty).build()); \"pretty log format\")]\n    #[test_case(\"trip example.com --log-format json\", Ok(cfg().log_format(LogFormat::Json).build()); \"json log format\")]\n    #[test_case(\"trip example.com --log-format chrome\", Ok(cfg().log_format(LogFormat::Chrome).build()); \"chrome log format\")]\n    #[test_case(\"trip example.com --log-format foo\", Err(anyhow!(\"error: invalid value 'foo' for '--log-format <LOG_FORMAT>' [possible values: compact, pretty, json, chrome] For more information, try '--help'.\")); \"invalid log format\")]\n    fn test_log_format(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", Ok(cfg().log_span_events(LogSpanEvents::Off).build()); \"default log span\")]\n    #[test_case(\"trip example.com --log-span-events off\", Ok(cfg().log_span_events(LogSpanEvents::Off).build()); \"off log span\")]\n    #[test_case(\"trip example.com --log-span-events active\", Ok(cfg().log_span_events(LogSpanEvents::Active).build()); \"active log span\")]\n    #[test_case(\"trip example.com --log-span-events full\", Ok(cfg().log_span_events(LogSpanEvents::Full).build()); \"full log span\")]\n    #[test_case(\"trip example.com --log-span-events foo\", Err(anyhow!(\"error: invalid value 'foo' for '--log-span-events <LOG_SPAN_EVENTS>' [possible values: off, active, full] For more information, try '--help'.\")); \"invalid log span\")]\n    fn test_log_span(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare(parse_config(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com\", true, false, Ok(cfg().privilege_mode(PrivilegeMode::Privileged).build()); \"default privilege mode\")]\n    #[test_case(\"trip example.com --unprivileged\", true, false, Ok(cfg().privilege_mode(PrivilegeMode::Unprivileged).build()); \"unprivileged mode\")]\n    #[test_case(\"trip example.com -u\", true, false, Ok(cfg().privilege_mode(PrivilegeMode::Unprivileged).build()); \"unprivileged mode short\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com\", true, true, Ok(cfg().privilege_mode(PrivilegeMode::Privileged).build()); \"has privilege and needs\")]\n    #[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\")]\n    #[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\")]\n    #[test_case(\"trip example.com --unprivileged\", false, false, Ok(cfg().privilege_mode(PrivilegeMode::Unprivileged).build()); \"no privilege and not needs in unprivileged mode\")]\n    #[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\")]\n    #[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\")]\n    fn test_privilege(\n        cmd: &str,\n        has_privileges: bool,\n        needs_privileges: bool,\n        expected: anyhow::Result<TrippyConfig>,\n    ) {\n        compare(\n            parse_config_with_privileges(cmd, has_privileges, needs_privileges),\n            expected,\n        );\n    }\n\n    #[test_case(\"trip --print-config-template\", Ok(TrippyAction::PrintConfigTemplate); \"print config template\")]\n    #[test_case(\"trip --print-tui-binding-commands\", Ok(TrippyAction::PrintTuiBindingCommands); \"print the tui binding commands\")]\n    #[test_case(\"trip --print-tui-theme-items\", Ok(TrippyAction::PrintTuiThemeItems); \"print the tui theme items\")]\n    #[test_case(\"trip --generate elvish\", Ok(TrippyAction::PrintShellCompletions(Shell::Elvish)); \"generate elvish shell completions\")]\n    #[test_case(\"trip --generate fish\", Ok(TrippyAction::PrintShellCompletions(Shell::Fish)); \"generate fish shell completions\")]\n    #[test_case(\"trip --generate powershell\", Ok(TrippyAction::PrintShellCompletions(Shell::PowerShell)); \"generate powershell shell completions\")]\n    #[test_case(\"trip --generate zsh\", Ok(TrippyAction::PrintShellCompletions(Shell::Zsh)); \"generate zsh shell completions\")]\n    #[test_case(\"trip --generate bash\", Ok(TrippyAction::PrintShellCompletions(Shell::Bash)); \"generate bash shell completions\")]\n    #[test_case(\"trip --generate foo\", Err(anyhow!(\"error: invalid value 'foo' for '--generate <GENERATE>' [possible values: bash, elvish, fish, powershell, zsh] For more information, try '--help'.\")); \"generate invalid shell completions\")]\n    #[test_case(\"trip --generate-man\", Ok(TrippyAction::PrintManPage); \"generate man page\")]\n    #[test_case(\"trip --print-locales\", Ok(TrippyAction::PrintLocales); \"print all locales\")]\n    fn test_action(cmd: &str, expected: anyhow::Result<TrippyAction>) {\n        compare(parse_action(cmd), expected);\n    }\n\n    #[test_case(\"trip example.com --tui-max-samples foo\", Err(anyhow!(\"error: unexpected argument '--tui-max-samples' found\")); \"deprecated tui max samples\")]\n    #[test_case(\"trip example.com --tui-max-flows foo\", Err(anyhow!(\"error: unexpected argument '--tui-max-flows' found\")); \"deprecated tui max flows\")]\n    #[test_case(\"trip example.com --tui-key-bindings toggle-privacy=o\", Err(anyhow!(\"error: invalid value 'toggle-privacy=o' for '--tui-key-bindings <TUI_KEY_BINDINGS>': toggle-privacy is deprecated, use expand-privacy and contract-privacy instead\")); \"deprecated toggle-privacy key binding\")]\n    fn test_deprecated(cmd: &str, expected: anyhow::Result<TrippyConfig>) {\n        compare_lines(parse_config(cmd), expected, Some(0));\n    }\n\n    fn parse_action(cmd: &str) -> anyhow::Result<TrippyAction> {\n        TrippyAction::from(parse(cmd)?, &dummy_platform(), 0)\n    }\n\n    fn parse_config(cmd: &str) -> anyhow::Result<TrippyConfig> {\n        let args = parse(cmd)?;\n        let cfg_file = ConfigFile::default();\n        let platform = dummy_platform();\n        TrippyConfig::build_config(args, cfg_file, &platform, 0)\n    }\n\n    fn parse_config_with_privileges(\n        cmd: &str,\n        has_privileges: bool,\n        needs_privileges: bool,\n    ) -> anyhow::Result<TrippyConfig> {\n        let args = parse(cmd)?;\n        let cfg_file = ConfigFile::default();\n        let privilege = Privilege::new(has_privileges, needs_privileges);\n        TrippyConfig::build_config(args, cfg_file, &privilege, 0)\n    }\n\n    fn parse(cmd: &str) -> anyhow::Result<Args> {\n        use clap::Parser;\n        Ok(Args::try_parse_from(\n            cmd.split(' ').map(std::ffi::OsString::from),\n        )?)\n    }\n\n    fn compare<T>(actual: anyhow::Result<T>, expected: anyhow::Result<T>)\n    where\n        T: PartialEq + Eq + std::fmt::Debug,\n    {\n        compare_lines(actual, expected, None);\n    }\n\n    fn compare_lines<T>(\n        actual: anyhow::Result<T>,\n        expected: anyhow::Result<T>,\n        lines: Option<usize>,\n    ) where\n        T: PartialEq + Eq + std::fmt::Debug,\n    {\n        match (actual, expected) {\n            (Ok(cfg), Ok(exp)) => {\n                pretty_assertions::assert_eq!(cfg, exp);\n            }\n            (Err(err), Err(exp_err)) => {\n                if let Some(lines) = lines {\n                    let fst = err\n                        .to_string()\n                        .lines()\n                        .nth(lines)\n                        .map(ToString::to_string)\n                        .unwrap_or_default();\n                    let snd = exp_err\n                        .to_string()\n                        .lines()\n                        .nth(lines)\n                        .map(ToString::to_string)\n                        .unwrap_or_default();\n                    if remove_whitespace(fst) != remove_whitespace(snd) {\n                        pretty_assertions::assert_eq!(err.to_string(), exp_err.to_string());\n                    }\n                } else if remove_whitespace(err.to_string())\n                    != remove_whitespace(exp_err.to_string())\n                {\n                    pretty_assertions::assert_eq!(err.to_string(), exp_err.to_string());\n                }\n            }\n            (Ok(_), Err(exp_err)) => {\n                panic!(\"expected err {}\", exp_err.to_string().trim());\n            }\n            (Err(err), Ok(_)) => {\n                panic!(\"unexpected err {}\", err.to_string().trim());\n            }\n        }\n    }\n\n    fn compare_snapshot<T>(name: &str, actual: anyhow::Result<T>)\n    where\n        T: std::fmt::Debug,\n    {\n        insta(name, || match actual {\n            Ok(act) => {\n                insta::assert_debug_snapshot!(act);\n            }\n            Err(err) => {\n                insta::assert_snapshot!(remove_whitespace(err.to_string()));\n            }\n        });\n    }\n\n    fn cfg() -> TrippyConfigBuilder {\n        TrippyConfigBuilder::new(vec![String::from(\"example.com\")])\n    }\n\n    fn cfg_multi() -> TrippyConfigBuilder {\n        TrippyConfigBuilder::new(vec![\n            String::from(\"example.com\"),\n            String::from(\"foo.com\"),\n            String::from(\"bar.com\"),\n        ])\n    }\n\n    const fn dummy_platform() -> Privilege {\n        Privilege::new(true, false)\n    }\n\n    fn args(args: &[&str]) -> anyhow::Result<Args> {\n        use clap::Parser;\n        Ok(Args::try_parse_from(\n            args.iter().map(std::ffi::OsString::from),\n        )?)\n    }\n\n    pub struct TrippyConfigBuilder {\n        config: TrippyConfig,\n    }\n\n    impl TrippyConfigBuilder {\n        pub fn new(targets: Vec<String>) -> Self {\n            Self {\n                config: TrippyConfig {\n                    targets,\n                    ..TrippyConfig::default()\n                },\n            }\n        }\n\n        pub fn mode(self, mode: Mode) -> Self {\n            Self {\n                config: TrippyConfig {\n                    mode,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn privilege_mode(self, privilege_mode: PrivilegeMode) -> Self {\n            Self {\n                config: TrippyConfig {\n                    privilege_mode,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn protocol(self, protocol: Protocol) -> Self {\n            Self {\n                config: TrippyConfig {\n                    protocol,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn addr_family(self, addr_family: IpAddrFamily) -> Self {\n            Self {\n                config: TrippyConfig {\n                    addr_family,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn first_ttl(self, first_ttl: u8) -> Self {\n            Self {\n                config: TrippyConfig {\n                    first_ttl,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn max_ttl(self, max_ttl: u8) -> Self {\n            Self {\n                config: TrippyConfig {\n                    max_ttl,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn min_round_duration(self, min_round_duration: Duration) -> Self {\n            Self {\n                config: TrippyConfig {\n                    min_round_duration,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn max_round_duration(self, max_round_duration: Duration) -> Self {\n            Self {\n                config: TrippyConfig {\n                    max_round_duration,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn grace_duration(self, grace_duration: Duration) -> Self {\n            Self {\n                config: TrippyConfig {\n                    grace_duration,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn max_inflight(self, max_inflight: u8) -> Self {\n            Self {\n                config: TrippyConfig {\n                    max_inflight,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn initial_sequence(self, initial_sequence: u16) -> Self {\n            Self {\n                config: TrippyConfig {\n                    initial_sequence,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tos(self, tos: u8) -> Self {\n            Self {\n                config: TrippyConfig { tos, ..self.config },\n            }\n        }\n\n        pub fn icmp_extension_parse_mode(\n            self,\n            icmp_extension_parse_mode: IcmpExtensionParseMode,\n        ) -> Self {\n            Self {\n                config: TrippyConfig {\n                    icmp_extension_parse_mode,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn read_timeout(self, read_timeout: Duration) -> Self {\n            Self {\n                config: TrippyConfig {\n                    read_timeout,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn packet_size(self, packet_size: u16) -> Self {\n            Self {\n                config: TrippyConfig {\n                    packet_size,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn payload_pattern(self, payload_pattern: u8) -> Self {\n            Self {\n                config: TrippyConfig {\n                    payload_pattern,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn source_addr(self, source_addr: Option<IpAddr>) -> Self {\n            Self {\n                config: TrippyConfig {\n                    source_addr,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn interface(self, interface: Option<String>) -> Self {\n            Self {\n                config: TrippyConfig {\n                    interface,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn port_direction(self, port_direction: PortDirection) -> Self {\n            Self {\n                config: TrippyConfig {\n                    port_direction,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn multipath_strategy(self, multipath_strategy: MultipathStrategy) -> Self {\n            Self {\n                config: TrippyConfig {\n                    multipath_strategy,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn dns_timeout(self, dns_timeout: Duration) -> Self {\n            Self {\n                config: TrippyConfig {\n                    dns_timeout,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn dns_ttl(self, dns_ttl: Duration) -> Self {\n            Self {\n                config: TrippyConfig {\n                    dns_ttl,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn dns_resolve_method(self, dns_resolve_method: ResolveMethod) -> Self {\n            Self {\n                config: TrippyConfig {\n                    dns_resolve_method,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn dns_lookup_as_info(self, dns_lookup_as_info: bool) -> Self {\n            Self {\n                config: TrippyConfig {\n                    dns_lookup_as_info,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn dns_resolve_all(self, dns_resolve_all: bool) -> Self {\n            Self {\n                config: TrippyConfig {\n                    dns_resolve_all,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn max_samples(self, tui_max_samples: usize) -> Self {\n            Self {\n                config: TrippyConfig {\n                    max_samples: tui_max_samples,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn max_flows(self, max_flows: usize) -> Self {\n            Self {\n                config: TrippyConfig {\n                    max_flows,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_preserve_screen(self, tui_preserve_screen: bool) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_preserve_screen,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_refresh_rate(self, tui_refresh_rate: Duration) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_refresh_rate,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_privacy_max_ttl(self, tui_privacy_max_ttl: Option<u8>) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_privacy_max_ttl,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_locale(self, tui_locale: Option<String>) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_locale,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_timezone(self, tui_timezone: Option<chrono_tz::Tz>) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_timezone,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_address_mode(self, tui_address_mode: AddressMode) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_address_mode,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_as_mode(self, tui_as_mode: AsMode) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_as_mode,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_custom_columns(self, tui_custom_columns: TuiColumns) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_custom_columns,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_icmp_extension_mode(self, tui_icmp_extension_mode: IcmpExtensionMode) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_icmp_extension_mode,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_geoip_mode(self, tui_geoip_mode: GeoIpMode) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_geoip_mode,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_max_addrs(self, tui_max_addrs: Option<u8>) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_max_addrs,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn tui_theme(self, tui_theme: TuiTheme) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_theme,\n                    ..self.config\n                },\n            }\n        }\n\n        #[expect(clippy::large_types_passed_by_value)]\n        pub fn tui_bindings(self, tui_bindings: TuiBindings) -> Self {\n            Self {\n                config: TrippyConfig {\n                    tui_bindings,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn report_cycles(self, report_cycles: usize) -> Self {\n            Self {\n                config: TrippyConfig {\n                    report_cycles,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn geoip_mmdb_file(self, geoip_mmdb_file: Option<String>) -> Self {\n            Self {\n                config: TrippyConfig {\n                    geoip_mmdb_file,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn max_rounds(self, max_rounds: Option<usize>) -> Self {\n            Self {\n                config: TrippyConfig {\n                    max_rounds,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn verbose(self, verbose: bool) -> Self {\n            Self {\n                config: TrippyConfig {\n                    verbose,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn log_format(self, log_format: LogFormat) -> Self {\n            Self {\n                config: TrippyConfig {\n                    log_format,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn log_filter(self, log_filter: String) -> Self {\n            Self {\n                config: TrippyConfig {\n                    log_filter,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn log_span_events(self, log_span_events: LogSpanEvents) -> Self {\n            Self {\n                config: TrippyConfig {\n                    log_span_events,\n                    ..self.config\n                },\n            }\n        }\n\n        pub fn build(self) -> TrippyConfig {\n            self.config\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/binding.rs",
    "content": "use crate::config::{TuiBindings, TuiKeyBinding};\nuse crossterm::event::{KeyCode, KeyEvent, KeyModifiers};\nuse itertools::Itertools;\nuse std::fmt::{Display, Formatter};\n\n/// Tui key bindings.\n#[derive(Debug, Clone, Copy)]\npub struct Bindings {\n    pub toggle_help: KeyBinding,\n    pub toggle_help_alt: KeyBinding,\n    pub toggle_settings: KeyBinding,\n    pub toggle_settings_tui: KeyBinding,\n    pub toggle_settings_trace: KeyBinding,\n    pub toggle_settings_dns: KeyBinding,\n    pub toggle_settings_geoip: KeyBinding,\n    pub toggle_settings_bindings: KeyBinding,\n    pub toggle_settings_theme: KeyBinding,\n    pub toggle_settings_columns: KeyBinding,\n    pub previous_hop: KeyBinding,\n    pub next_hop: KeyBinding,\n    pub previous_trace: KeyBinding,\n    pub next_trace: KeyBinding,\n    pub previous_hop_address: KeyBinding,\n    pub next_hop_address: KeyBinding,\n    pub address_mode_ip: KeyBinding,\n    pub address_mode_host: KeyBinding,\n    pub address_mode_both: KeyBinding,\n    pub toggle_freeze: KeyBinding,\n    pub toggle_chart: KeyBinding,\n    pub toggle_map: KeyBinding,\n    pub toggle_flows: KeyBinding,\n    pub expand_privacy: KeyBinding,\n    pub contract_privacy: KeyBinding,\n    pub expand_hosts: KeyBinding,\n    pub contract_hosts: KeyBinding,\n    pub expand_hosts_max: KeyBinding,\n    pub contract_hosts_min: KeyBinding,\n    pub chart_zoom_in: KeyBinding,\n    pub chart_zoom_out: KeyBinding,\n    pub clear_trace_data: KeyBinding,\n    pub clear_dns_cache: KeyBinding,\n    pub clear_selection: KeyBinding,\n    pub toggle_as_info: KeyBinding,\n    pub toggle_hop_details: KeyBinding,\n    pub quit: KeyBinding,\n    pub quit_preserve_screen: KeyBinding,\n}\n\nimpl From<TuiBindings> for Bindings {\n    fn from(value: TuiBindings) -> Self {\n        Self {\n            toggle_help: KeyBinding::from(value.toggle_help),\n            toggle_help_alt: KeyBinding::from(value.toggle_help_alt),\n            toggle_settings: KeyBinding::from(value.toggle_settings),\n            toggle_settings_tui: KeyBinding::from(value.toggle_settings_tui),\n            toggle_settings_trace: KeyBinding::from(value.toggle_settings_trace),\n            toggle_settings_dns: KeyBinding::from(value.toggle_settings_dns),\n            toggle_settings_geoip: KeyBinding::from(value.toggle_settings_geoip),\n            toggle_settings_bindings: KeyBinding::from(value.toggle_settings_bindings),\n            toggle_settings_theme: KeyBinding::from(value.toggle_settings_theme),\n            toggle_settings_columns: KeyBinding::from(value.toggle_settings_columns),\n            previous_hop: KeyBinding::from(value.previous_hop),\n            next_hop: KeyBinding::from(value.next_hop),\n            previous_trace: KeyBinding::from(value.previous_trace),\n            next_trace: KeyBinding::from(value.next_trace),\n            previous_hop_address: KeyBinding::from(value.previous_hop_address),\n            next_hop_address: KeyBinding::from(value.next_hop_address),\n            address_mode_ip: KeyBinding::from(value.address_mode_ip),\n            address_mode_host: KeyBinding::from(value.address_mode_host),\n            address_mode_both: KeyBinding::from(value.address_mode_both),\n            toggle_freeze: KeyBinding::from(value.toggle_freeze),\n            toggle_chart: KeyBinding::from(value.toggle_chart),\n            toggle_map: KeyBinding::from(value.toggle_map),\n            toggle_flows: KeyBinding::from(value.toggle_flows),\n            expand_privacy: KeyBinding::from(value.expand_privacy),\n            contract_privacy: KeyBinding::from(value.contract_privacy),\n            expand_hosts: KeyBinding::from(value.expand_hosts),\n            contract_hosts: KeyBinding::from(value.contract_hosts),\n            expand_hosts_max: KeyBinding::from(value.expand_hosts_max),\n            contract_hosts_min: KeyBinding::from(value.contract_hosts_min),\n            chart_zoom_in: KeyBinding::from(value.chart_zoom_in),\n            chart_zoom_out: KeyBinding::from(value.chart_zoom_out),\n            clear_trace_data: KeyBinding::from(value.clear_trace_data),\n            clear_dns_cache: KeyBinding::from(value.clear_dns_cache),\n            clear_selection: KeyBinding::from(value.clear_selection),\n            toggle_as_info: KeyBinding::from(value.toggle_as_info),\n            toggle_hop_details: KeyBinding::from(value.toggle_hop_details),\n            quit: KeyBinding::from(value.quit),\n            quit_preserve_screen: KeyBinding::from(value.quit_preserve_screen),\n        }\n    }\n}\n\n/// Tui key binding.\n#[derive(Debug, Clone, Copy)]\npub struct KeyBinding {\n    pub code: KeyCode,\n    pub modifiers: KeyModifiers,\n}\n\nimpl KeyBinding {\n    pub fn check(&self, event: KeyEvent) -> bool {\n        let code_match = match (event.code, self.code) {\n            (KeyCode::Char(c1), KeyCode::Char(c2)) => c1.eq_ignore_ascii_case(&c2),\n            (c1, c2) => c1 == c2,\n        };\n        code_match && self.modifiers == event.modifiers\n    }\n}\n\nimpl From<TuiKeyBinding> for KeyBinding {\n    fn from(value: TuiKeyBinding) -> Self {\n        Self {\n            code: value.code,\n            modifiers: value.modifier,\n        }\n    }\n}\n\nimpl Display for KeyBinding {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        let modifiers = &[\n            self.modifiers\n                .contains(KeyModifiers::SHIFT)\n                .then_some(\"shift\"),\n            self.modifiers\n                .contains(KeyModifiers::CONTROL)\n                .then_some(\"ctrl\"),\n            self.modifiers.contains(KeyModifiers::ALT).then_some(\"alt\"),\n            self.modifiers\n                .contains(KeyModifiers::SUPER)\n                .then_some(\"super\"),\n            self.modifiers\n                .contains(KeyModifiers::HYPER)\n                .then_some(\"hyper\"),\n            self.modifiers\n                .contains(KeyModifiers::META)\n                .then_some(\"meta\"),\n        ]\n        .into_iter()\n        .flatten()\n        .join(\"+\");\n        if !modifiers.is_empty() {\n            write!(f, \"{modifiers}+\")?;\n        }\n        match self.code {\n            KeyCode::Backspace => write!(f, \"backspace\"),\n            KeyCode::Enter => write!(f, \"enter\"),\n            KeyCode::Left => write!(f, \"left\"),\n            KeyCode::Right => write!(f, \"right\"),\n            KeyCode::Up => write!(f, \"up\"),\n            KeyCode::Down => write!(f, \"down\"),\n            KeyCode::Home => write!(f, \"home\"),\n            KeyCode::End => write!(f, \"end\"),\n            KeyCode::PageUp => write!(f, \"pageup\"),\n            KeyCode::PageDown => write!(f, \"pagedown\"),\n            KeyCode::Tab => write!(f, \"tab\"),\n            KeyCode::BackTab => write!(f, \"backtab\"),\n            KeyCode::Delete => write!(f, \"delete\"),\n            KeyCode::Insert => write!(f, \"insert\"),\n            KeyCode::Char(c) => write!(f, \"{c}\"),\n            KeyCode::Esc => write!(f, \"esc\"),\n            _ => write!(f, \"unknown\"),\n        }\n    }\n}\n\npub const CTRL_C: KeyBinding = KeyBinding {\n    code: KeyCode::Char('c'),\n    modifiers: KeyModifiers::CONTROL,\n};\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/columns.rs",
    "content": "use crate::config::{TuiColumn, TuiColumns};\nuse crate::t;\nuse ratatui::layout::{Constraint, Rect};\nuse std::borrow::Cow;\nuse std::fmt::{Debug, Display, Formatter};\nuse strum::{EnumIter, IntoEnumIterator};\nuse unicode_width::UnicodeWidthStr;\n\n/// The columns to display in the hops table of the TUI.\n#[derive(Debug, Clone, Eq, PartialEq)]\npub struct Columns(Vec<Column>);\n\nimpl Columns {\n    /// Column width constraints.\n    ///\n    /// All columns are returned as `Constraint::Min(width)`.\n    ///\n    /// For `Fixed(n)` columns the width is as specified in `n`.\n    /// For `Variable` columns the width is calculated by subtracting the total\n    /// size of all `Fixed` columns from the width of the containing `Rect` and\n    /// dividing by the number of `Variable` columns.\n    pub fn constraints(&self, rect: Rect) -> Vec<Constraint> {\n        let total_fixed_width = self\n            .columns()\n            .map(|c| match c.typ.width() {\n                ColumnWidth::Fixed(width) => width,\n                ColumnWidth::Variable => 0,\n            })\n            .sum();\n        let variable_width_count = self\n            .columns()\n            .filter(|c| matches!(c.typ.width(), ColumnWidth::Variable))\n            .count() as u16;\n        let variable_width =\n            rect.width.saturating_sub(total_fixed_width) / variable_width_count.max(1);\n        self.columns()\n            .map(|c| match c.typ.width() {\n                ColumnWidth::Fixed(width) => Constraint::Min(width),\n                ColumnWidth::Variable => Constraint::Min(variable_width),\n            })\n            .collect()\n    }\n\n    pub fn columns(&self) -> impl Iterator<Item = &Column> {\n        self.0\n            .iter()\n            .filter(|c| matches!(c.status, ColumnStatus::Shown))\n    }\n\n    pub fn all_columns(&self) -> impl Iterator<Item = &Column> {\n        self.0.iter()\n    }\n\n    pub fn all_columns_count(&self) -> usize {\n        self.0.len()\n    }\n\n    pub fn toggle(&mut self, index: usize) {\n        self.0[index].status = match self.0[index].status {\n            ColumnStatus::Shown => ColumnStatus::Hidden,\n            ColumnStatus::Hidden => ColumnStatus::Shown,\n        };\n    }\n\n    pub fn move_down(&mut self, index: usize) {\n        if index < self.0.len() {\n            let removed = self.0.remove(index);\n            self.0.insert(index + 1, removed);\n        }\n    }\n\n    pub fn move_up(&mut self, index: usize) {\n        if index > 0 {\n            let removed = self.0.remove(index);\n            self.0.insert(index - 1, removed);\n        }\n    }\n}\n\nimpl From<TuiColumns> for Columns {\n    fn from(value: TuiColumns) -> Self {\n        let enabled: Vec<_> = value.0.into_iter().map(Column::from).collect();\n        let disabled: Vec<_> = ColumnType::iter()\n            .filter(|ct| enabled.iter().all(|c| c.typ != *ct))\n            .map(Column::new_hidden)\n            .collect();\n        let all = enabled.into_iter().chain(disabled).collect();\n        Self(all)\n    }\n}\n\nimpl Display for Columns {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        let output: Vec<char> = self\n            .0\n            .iter()\n            .filter_map(|c| {\n                if c.status == ColumnStatus::Shown {\n                    Some(c.typ.into())\n                } else {\n                    None\n                }\n            })\n            .collect();\n        write!(f, \"{}\", String::from_iter(output))\n    }\n}\n\n#[derive(Debug, Clone, Eq, PartialEq)]\npub struct Column {\n    pub typ: ColumnType,\n    pub status: ColumnStatus,\n}\n\nimpl Column {\n    pub const fn new_shown(typ: ColumnType) -> Self {\n        Self {\n            typ,\n            status: ColumnStatus::Shown,\n        }\n    }\n    pub const fn new_hidden(typ: ColumnType) -> Self {\n        Self {\n            typ,\n            status: ColumnStatus::Hidden,\n        }\n    }\n}\n\n#[derive(Debug, Clone, Eq, PartialEq)]\npub enum ColumnStatus {\n    Shown,\n    Hidden,\n}\n\nimpl Display for ColumnStatus {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Shown => write!(f, \"{}\", t!(\"on\")),\n            Self::Hidden => write!(f, \"{}\", t!(\"off\")),\n        }\n    }\n}\n\n/// A TUI hops table column.\n#[derive(Debug, Copy, Clone, Eq, PartialEq, EnumIter)]\npub enum ColumnType {\n    /// The ttl for a hop.\n    Ttl,\n    /// The hostname for a hostname.\n    Host,\n    /// The packet loss % for a hop.\n    LossPct,\n    /// The number of probes sent for a hop.\n    Sent,\n    /// The number of responses received for a hop.\n    Received,\n    /// The last RTT for a hop.\n    Last,\n    /// The rolling average RTT for a hop.\n    Average,\n    /// The best RTT for a hop.\n    Best,\n    /// The worst RTT for a hop.\n    Worst,\n    /// The stddev of RTT for a hop.\n    StdDev,\n    /// The status of a hop.\n    Status,\n    /// The current jitter i.e. round-trip difference with the last round-trip.\n    Jitter,\n    /// The average jitter time for all probes at this hop.\n    Javg,\n    /// The worst round-trip jitter time for all probes at this hop.\n    Jmax,\n    /// The smoothed jitter value for all probes at this hop.\n    Jinta,\n    /// The source port for last probe for this hop.\n    LastSrcPort,\n    /// The destination port for last probe for this hop.\n    LastDestPort,\n    /// The sequence number for the last probe for this hop.\n    LastSeq,\n    /// The icmp packet type for the last probe for this hop.\n    LastIcmpPacketType,\n    /// The icmp packet code for the last probe for this hop.\n    LastIcmpPacketCode,\n    /// The NAT detection status for the last probe for this hop.\n    LastNatStatus,\n    /// The number of probes that failed for a hop.\n    Failed,\n    /// The number of probes with forward loss for a hop.\n    Floss,\n    /// The number of probes with backward loss for a hop.\n    Bloss,\n    /// The forward loss % for a hop.\n    FlossPct,\n    /// The Differentiated Services Code Point of the Original Datagram for a hop.\n    Dscp,\n    /// The Explicit Congestion Notification of the Original Datagram for a hop.\n    Ecn,\n    /// The autonomous system number for a hop.\n    Asn,\n}\n\nimpl From<ColumnType> for char {\n    fn from(col_type: ColumnType) -> Self {\n        match col_type {\n            ColumnType::Ttl => 'h',\n            ColumnType::Host => 'o',\n            ColumnType::LossPct => 'l',\n            ColumnType::Sent => 's',\n            ColumnType::Received => 'r',\n            ColumnType::Last => 'a',\n            ColumnType::Average => 'v',\n            ColumnType::Best => 'b',\n            ColumnType::Worst => 'w',\n            ColumnType::StdDev => 'd',\n            ColumnType::Status => 't',\n            ColumnType::Jitter => 'j',\n            ColumnType::Javg => 'g',\n            ColumnType::Jmax => 'x',\n            ColumnType::Jinta => 'i',\n            ColumnType::LastSrcPort => 'S',\n            ColumnType::LastDestPort => 'P',\n            ColumnType::LastSeq => 'Q',\n            ColumnType::LastIcmpPacketType => 'T',\n            ColumnType::LastIcmpPacketCode => 'C',\n            ColumnType::LastNatStatus => 'N',\n            ColumnType::Failed => 'f',\n            ColumnType::Floss => 'F',\n            ColumnType::Bloss => 'B',\n            ColumnType::FlossPct => 'D',\n            ColumnType::Dscp => 'K',\n            ColumnType::Ecn => 'M',\n            ColumnType::Asn => 'A',\n        }\n    }\n}\n\nimpl From<TuiColumn> for Column {\n    fn from(value: TuiColumn) -> Self {\n        match value {\n            TuiColumn::Ttl => Self::new_shown(ColumnType::Ttl),\n            TuiColumn::Host => Self::new_shown(ColumnType::Host),\n            TuiColumn::LossPct => Self::new_shown(ColumnType::LossPct),\n            TuiColumn::Sent => Self::new_shown(ColumnType::Sent),\n            TuiColumn::Received => Self::new_shown(ColumnType::Received),\n            TuiColumn::Last => Self::new_shown(ColumnType::Last),\n            TuiColumn::Average => Self::new_shown(ColumnType::Average),\n            TuiColumn::Best => Self::new_shown(ColumnType::Best),\n            TuiColumn::Worst => Self::new_shown(ColumnType::Worst),\n            TuiColumn::StdDev => Self::new_shown(ColumnType::StdDev),\n            TuiColumn::Status => Self::new_shown(ColumnType::Status),\n            TuiColumn::Jitter => Self::new_shown(ColumnType::Jitter),\n            TuiColumn::Javg => Self::new_shown(ColumnType::Javg),\n            TuiColumn::Jmax => Self::new_shown(ColumnType::Jmax),\n            TuiColumn::Jinta => Self::new_shown(ColumnType::Jinta),\n            TuiColumn::LastSrcPort => Self::new_shown(ColumnType::LastSrcPort),\n            TuiColumn::LastDestPort => Self::new_shown(ColumnType::LastDestPort),\n            TuiColumn::LastSeq => Self::new_shown(ColumnType::LastSeq),\n            TuiColumn::LastIcmpPacketType => Self::new_shown(ColumnType::LastIcmpPacketType),\n            TuiColumn::LastIcmpPacketCode => Self::new_shown(ColumnType::LastIcmpPacketCode),\n            TuiColumn::LastNatStatus => Self::new_shown(ColumnType::LastNatStatus),\n            TuiColumn::Failed => Self::new_shown(ColumnType::Failed),\n            TuiColumn::Floss => Self::new_shown(ColumnType::Floss),\n            TuiColumn::Bloss => Self::new_shown(ColumnType::Bloss),\n            TuiColumn::FlossPct => Self::new_shown(ColumnType::FlossPct),\n            TuiColumn::Dscp => Self::new_shown(ColumnType::Dscp),\n            TuiColumn::Ecn => Self::new_shown(ColumnType::Ecn),\n            TuiColumn::Asn => Self::new_shown(ColumnType::Asn),\n        }\n    }\n}\n\nimpl Display for ColumnType {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.name())\n    }\n}\n\nimpl ColumnType {\n    /// The name of the column in the current locale.\n    pub(self) fn name(&self) -> Cow<'_, str> {\n        match self {\n            Self::Ttl => Cow::Borrowed(\"#\"),\n            Self::Host => t!(\"column_host\"),\n            Self::LossPct => t!(\"column_loss_pct\"),\n            Self::Sent => t!(\"column_snd\"),\n            Self::Received => t!(\"column_recv\"),\n            Self::Last => t!(\"column_last\"),\n            Self::Average => t!(\"column_avg\"),\n            Self::Best => t!(\"column_best\"),\n            Self::Worst => t!(\"column_wrst\"),\n            Self::StdDev => t!(\"column_stdev\"),\n            Self::Status => t!(\"column_sts\"),\n            Self::Jitter => t!(\"column_jttr\"),\n            Self::Javg => t!(\"column_javg\"),\n            Self::Jmax => t!(\"column_jmax\"),\n            Self::Jinta => t!(\"column_jint\"),\n            Self::LastSrcPort => t!(\"column_sprt\"),\n            Self::LastDestPort => t!(\"column_dprt\"),\n            Self::LastSeq => t!(\"column_seq\"),\n            Self::LastIcmpPacketType => t!(\"column_type\"),\n            Self::LastIcmpPacketCode => t!(\"column_code\"),\n            Self::LastNatStatus => t!(\"column_nat\"),\n            Self::Failed => t!(\"column_fail\"),\n            Self::Floss => t!(\"column_floss\"),\n            Self::Bloss => t!(\"column_bloss\"),\n            Self::FlossPct => t!(\"column_floss_pct\"),\n            Self::Dscp => t!(\"column_dscp\"),\n            Self::Ecn => t!(\"column_ecn\"),\n            Self::Asn => t!(\"column_asn\"),\n        }\n    }\n\n    /// The width of the column.\n    ///\n    /// For most columns the width is calculated based on the column name in\n    /// the current locale.\n    ///\n    /// For the `Ttl` column the width is fixed as it is always a single\n    /// character.\n    ///\n    /// The `Host` column is variable as it should use the remaining space.\n    pub(self) fn width(self) -> ColumnWidth {\n        let width = self.name().width() as u16 + 2;\n        #[expect(clippy::match_same_arms)]\n        match self {\n            Self::Ttl => ColumnWidth::Fixed(4),\n            Self::Host => ColumnWidth::Variable,\n            Self::LossPct => ColumnWidth::Fixed(width.max(8)),\n            Self::Sent => ColumnWidth::Fixed(width.max(7)),\n            Self::Received => ColumnWidth::Fixed(width.max(7)),\n            Self::Last => ColumnWidth::Fixed(width.max(7)),\n            Self::Average => ColumnWidth::Fixed(width.max(7)),\n            Self::Best => ColumnWidth::Fixed(width.max(7)),\n            Self::Worst => ColumnWidth::Fixed(width.max(7)),\n            Self::StdDev => ColumnWidth::Fixed(width.max(8)),\n            Self::Status => ColumnWidth::Fixed(width.max(7)),\n            Self::Jitter => ColumnWidth::Fixed(width.max(7)),\n            Self::Javg => ColumnWidth::Fixed(width.max(7)),\n            Self::Jmax => ColumnWidth::Fixed(width.max(7)),\n            Self::Jinta => ColumnWidth::Fixed(width.max(8)),\n            Self::LastSrcPort => ColumnWidth::Fixed(width.max(7)),\n            Self::LastDestPort => ColumnWidth::Fixed(width.max(7)),\n            Self::LastSeq => ColumnWidth::Fixed(width.max(7)),\n            Self::LastIcmpPacketType => ColumnWidth::Fixed(width.max(7)),\n            Self::LastIcmpPacketCode => ColumnWidth::Fixed(width.max(7)),\n            Self::LastNatStatus => ColumnWidth::Fixed(width.max(7)),\n            Self::Failed => ColumnWidth::Fixed(width.max(7)),\n            Self::Floss => ColumnWidth::Fixed(width.max(7)),\n            Self::Bloss => ColumnWidth::Fixed(width.max(7)),\n            Self::FlossPct => ColumnWidth::Fixed(width.max(8)),\n            Self::Dscp => ColumnWidth::Fixed(width.max(7)),\n            Self::Ecn => ColumnWidth::Fixed(width.max(7)),\n            Self::Asn => ColumnWidth::Fixed(width.max(8)),\n        }\n    }\n}\n\n/// Table column layout constraints.\n#[derive(Debug, PartialEq)]\nenum ColumnWidth {\n    /// A fixed size column.\n    Fixed(u16),\n    /// A column that will use the remaining space.\n    Variable,\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use ratatui::layout::Constraint::Min;\n    use test_case::test_case;\n\n    #[test]\n    fn test_columns_conversion_from_tui_columns() {\n        let tui_columns = TuiColumns(vec![\n            TuiColumn::Ttl,\n            TuiColumn::Host,\n            TuiColumn::LossPct,\n            TuiColumn::Sent,\n            TuiColumn::Received,\n            TuiColumn::Last,\n            TuiColumn::Average,\n            TuiColumn::Best,\n            TuiColumn::Worst,\n            TuiColumn::StdDev,\n            TuiColumn::Status,\n        ]);\n        let columns = Columns::from(tui_columns);\n        assert_eq!(\n            columns,\n            Columns(vec![\n                Column::new_shown(ColumnType::Ttl),\n                Column::new_shown(ColumnType::Host),\n                Column::new_shown(ColumnType::LossPct),\n                Column::new_shown(ColumnType::Sent),\n                Column::new_shown(ColumnType::Received),\n                Column::new_shown(ColumnType::Last),\n                Column::new_shown(ColumnType::Average),\n                Column::new_shown(ColumnType::Best),\n                Column::new_shown(ColumnType::Worst),\n                Column::new_shown(ColumnType::StdDev),\n                Column::new_shown(ColumnType::Status),\n                Column::new_hidden(ColumnType::Jitter),\n                Column::new_hidden(ColumnType::Javg),\n                Column::new_hidden(ColumnType::Jmax),\n                Column::new_hidden(ColumnType::Jinta),\n                Column::new_hidden(ColumnType::LastSrcPort),\n                Column::new_hidden(ColumnType::LastDestPort),\n                Column::new_hidden(ColumnType::LastSeq),\n                Column::new_hidden(ColumnType::LastIcmpPacketType),\n                Column::new_hidden(ColumnType::LastIcmpPacketCode),\n                Column::new_hidden(ColumnType::LastNatStatus),\n                Column::new_hidden(ColumnType::Failed),\n                Column::new_hidden(ColumnType::Floss),\n                Column::new_hidden(ColumnType::Bloss),\n                Column::new_hidden(ColumnType::FlossPct),\n                Column::new_hidden(ColumnType::Dscp),\n                Column::new_hidden(ColumnType::Ecn),\n                Column::new_hidden(ColumnType::Asn),\n            ])\n        );\n    }\n\n    #[test]\n    fn test_column_conversion_from_tui_column() {\n        let tui_column = TuiColumn::Received;\n        let column = Column::from(tui_column);\n\n        assert_eq!(column.typ, ColumnType::Received);\n        assert_eq!(column.status, ColumnStatus::Shown);\n    }\n\n    #[test_case(ColumnType::Ttl, \"#\")]\n    #[test_case(ColumnType::Host, \"Host\")]\n    #[test_case(ColumnType::LossPct, \"Loss%\")]\n    #[test_case(ColumnType::Sent, \"Snd\")]\n    #[test_case(ColumnType::Received, \"Recv\")]\n    #[test_case(ColumnType::Last, \"Last\")]\n    #[test_case(ColumnType::Average, \"Avg\")]\n    #[test_case(ColumnType::Best, \"Best\")]\n    #[test_case(ColumnType::Worst, \"Wrst\")]\n    #[test_case(ColumnType::StdDev, \"StDev\")]\n    #[test_case(ColumnType::Status, \"Sts\")]\n    #[test_case(ColumnType::Asn, \"ASN\")]\n    fn test_column_display_formatting(c: ColumnType, heading: &'static str) {\n        assert_eq!(format!(\"{c}\"), heading);\n    }\n\n    #[test_case(ColumnType::Ttl, & ColumnWidth::Fixed(4))]\n    #[test_case(ColumnType::Host, & ColumnWidth::Variable)]\n    #[test_case(ColumnType::Asn, & ColumnWidth::Fixed(8))]\n    #[test_case(ColumnType::LossPct, & ColumnWidth::Fixed(8))]\n    fn test_column_width(column_type: ColumnType, width: &ColumnWidth) {\n        assert_eq!(column_type.width(), *width);\n    }\n\n    #[test]\n    fn test_column_constraints() {\n        let columns = Columns::from(TuiColumns::default());\n        let constraints = columns.constraints(Rect::new(0, 0, 80, 0));\n        assert_eq!(\n            vec![\n                Min(4),\n                Min(11),\n                Min(8),\n                Min(7),\n                Min(7),\n                Min(7),\n                Min(7),\n                Min(7),\n                Min(7),\n                Min(8),\n                Min(7)\n            ],\n            constraints\n        );\n    }\n\n    /// Expect to test the Column Into <char> flow.\n    #[test]\n    fn test_columns_into_string_short() {\n        let cols = Columns(vec![\n            Column::new_shown(ColumnType::Ttl),\n            Column::new_shown(ColumnType::Host),\n            Column::new_shown(ColumnType::LossPct),\n            Column::new_shown(ColumnType::Sent),\n        ]);\n        assert_eq!(\"hols\", format!(\"{cols}\"));\n    }\n\n    /// Happy path test for full set of columns.\n    #[test]\n    fn test_columns_into_string_happy_path() {\n        let cols = Columns(vec![\n            Column::new_shown(ColumnType::Ttl),\n            Column::new_shown(ColumnType::Host),\n            Column::new_shown(ColumnType::LossPct),\n            Column::new_shown(ColumnType::Sent),\n            Column::new_shown(ColumnType::Received),\n            Column::new_shown(ColumnType::Last),\n            Column::new_shown(ColumnType::Average),\n            Column::new_shown(ColumnType::Best),\n            Column::new_shown(ColumnType::Worst),\n            Column::new_shown(ColumnType::StdDev),\n            Column::new_shown(ColumnType::Status),\n        ]);\n        assert_eq!(\"holsravbwdt\", format!(\"{cols}\"));\n    }\n\n    /// Reverse subset test for subset of columns.\n    #[test]\n    fn test_columns_into_string_reverse_str() {\n        let cols = Columns(vec![\n            Column::new_shown(ColumnType::Status),\n            Column::new_shown(ColumnType::Last),\n            Column::new_shown(ColumnType::StdDev),\n            Column::new_shown(ColumnType::Worst),\n            Column::new_shown(ColumnType::Best),\n            Column::new_shown(ColumnType::Average),\n            Column::new_shown(ColumnType::Received),\n        ]);\n        assert_eq!(\"tadwbvr\", format!(\"{cols}\"));\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/config.rs",
    "content": "use crate::config::{AddressMode, AsMode, GeoIpMode, TuiColumns, TuiTheme};\nuse crate::config::{IcmpExtensionMode, TuiBindings};\nuse crate::frontend::binding::Bindings;\nuse crate::frontend::columns::Columns;\nuse crate::frontend::theme::Theme;\nuse chrono_tz::Tz;\nuse std::time::Duration;\n\n/// Tui configuration.\n#[derive(Debug)]\npub struct TuiConfig {\n    /// Refresh rate.\n    pub refresh_rate: Duration,\n    /// The maximum ttl of hops which will be masked for privacy.\n    pub privacy_max_ttl: Option<u8>,\n    /// Preserve screen on exit.\n    pub preserve_screen: bool,\n    /// How to render addresses.\n    pub address_mode: AddressMode,\n    /// Lookup autonomous system (AS) information.\n    pub lookup_as_info: bool,\n    /// How to render autonomous system (AS) data.\n    pub as_mode: AsMode,\n    /// How to render ICMP extensions.\n    pub icmp_extension_mode: IcmpExtensionMode,\n    /// How to render `GeoIp` data.\n    pub geoip_mode: GeoIpMode,\n    /// The maximum number of addresses to show per hop.\n    pub max_addrs: Option<u8>,\n    /// The Tui color theme.\n    pub theme: Theme,\n    /// The Tui keyboard bindings.\n    pub bindings: Bindings,\n    /// The columns to display in the hops table.\n    pub tui_columns: Columns,\n    pub geoip_mmdb_file: Option<String>,\n    pub dns_resolve_all: bool,\n    /// The current locale.\n    pub locale: String,\n    pub timezone: Option<Tz>,\n}\n\nimpl TuiConfig {\n    #[expect(clippy::too_many_arguments)]\n    pub fn new(\n        refresh_rate: Duration,\n        privacy_max_ttl: Option<u8>,\n        preserve_screen: bool,\n        address_mode: AddressMode,\n        lookup_as_info: bool,\n        as_mode: AsMode,\n        icmp_extension_mode: IcmpExtensionMode,\n        geoip_mode: GeoIpMode,\n        max_addrs: Option<u8>,\n        tui_theme: TuiTheme,\n        tui_bindings: &TuiBindings,\n        tui_columns: &TuiColumns,\n        geoip_mmdb_file: Option<String>,\n        dns_resolve_all: bool,\n        locale: String,\n        timezone: Option<Tz>,\n    ) -> Self {\n        Self {\n            refresh_rate,\n            privacy_max_ttl,\n            preserve_screen,\n            address_mode,\n            lookup_as_info,\n            as_mode,\n            icmp_extension_mode,\n            geoip_mode,\n            max_addrs,\n            theme: Theme::from(tui_theme),\n            bindings: Bindings::from(*tui_bindings),\n            tui_columns: Columns::from(tui_columns.clone()),\n            geoip_mmdb_file,\n            dns_resolve_all,\n            locale,\n            timezone,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/app.rs",
    "content": "use crate::frontend::render::{bar, body, flows, footer, header, help, settings, tabs};\nuse crate::frontend::tui_app::TuiApp;\nuse ratatui::Frame;\nuse ratatui::layout::{Constraint, Direction, Layout};\n\n/// Render the application main screen.\n///\n/// The layout of the TUI is as follows:\n///\n///  ____________________________________\n/// |               Header               |\n///  ------------------------------------\n/// |               Tabs                 |\n///  ------------------------------------\n/// |               Flows                |\n///  ------------------------------------\n/// |                                    |\n/// |                                    |\n/// |                                    |\n/// |         Hops / Chart / Map         |\n/// |                                    |\n/// |                                    |\n/// |                                    |\n///  ------------------------------------\n/// |     History     |    Frequency     |\n/// |                 |                  |\n///  ------------------------------------\n///  ====== info configuration bar ======\n///\n/// - Header: the title, target, clock and basic keyboard controls\n/// - Tab: a tab for each target (shown if > 1 target requested, can't be used with flows)\n/// - Flows: a navigable chart of individual trace flows (toggled on/off, can't be used with tabs)\n/// - Hops: a table where each row represents a single hop (time-to-live) in the trace\n/// - History: a graph of historic round-trip ping samples for the target host\n/// - Frequency: a histogram of sample frequencies by round-trip time for the target host\n/// - Info bar: a bar showing the current value for configurable items\n///\n/// On startup a splash screen is shown in place of the hops table, until the completion of the\n/// first round.\npub fn render(f: &mut Frame<'_>, app: &mut TuiApp) {\n    let constraints = if app.trace_info.len() > 1 {\n        LAYOUT_WITH_TABS.as_slice()\n    } else if app.show_flows {\n        LAYOUT_WITH_FLOWS.as_slice()\n    } else {\n        LAYOUT_WITHOUT_TABS.as_slice()\n    };\n    let chunks = Layout::default()\n        .direction(Direction::Vertical)\n        .constraints(constraints.as_ref())\n        .split(f.area());\n    header::render(f, app, chunks[0]);\n    if app.trace_info.len() > 1 {\n        tabs::render(f, chunks[1], app);\n        body::render(f, chunks[2], app);\n        footer::render(f, chunks[3], app);\n        bar::render(f, chunks[4], app);\n    } else if app.show_flows {\n        flows::render(f, chunks[1], app);\n        body::render(f, chunks[2], app);\n        footer::render(f, chunks[3], app);\n        bar::render(f, chunks[4], app);\n    } else {\n        body::render(f, chunks[1], app);\n        footer::render(f, chunks[2], app);\n        bar::render(f, chunks[3], app);\n    }\n    if app.show_settings {\n        settings::render(f, app);\n    } else if app.show_help {\n        help::render(f, app);\n    }\n}\n\nconst LAYOUT_WITHOUT_TABS: [Constraint; 4] = [\n    Constraint::Length(4),\n    Constraint::Min(10),\n    Constraint::Length(6),\n    Constraint::Length(1),\n];\n\nconst LAYOUT_WITH_TABS: [Constraint; 5] = [\n    Constraint::Length(4),\n    Constraint::Length(3),\n    Constraint::Min(10),\n    Constraint::Length(6),\n    Constraint::Length(1),\n];\n\nconst LAYOUT_WITH_FLOWS: [Constraint; 5] = [\n    Constraint::Length(4),\n    Constraint::Length(6),\n    Constraint::Min(10),\n    Constraint::Length(6),\n    Constraint::Length(1),\n];\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/bar.rs",
    "content": "use crate::config::AddressMode;\nuse crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse ratatui::Frame;\nuse ratatui::layout::{Alignment, Rect};\nuse ratatui::prelude::{Line, Span, Style};\nuse ratatui::widgets::Paragraph;\nuse std::borrow::Cow;\nuse std::net::IpAddr;\nuse trippy_core::{PrivilegeMode, Protocol};\nuse trippy_dns::ResolveMethod;\n\npub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) {\n    let protocol = Span::raw(match app.tracer_config().data.protocol() {\n        Protocol::Icmp => format!(\n            \"{}/ICMP\",\n            fmt_target_family(app.tracer_config().data.target_addr()),\n        ),\n        Protocol::Udp => format!(\n            \"{}/UDP/{}\",\n            fmt_target_family(app.tracer_config().data.target_addr()),\n            app.tracer_config().data.multipath_strategy(),\n        ),\n        Protocol::Tcp => format!(\n            \"{}/TCP\",\n            fmt_target_family(app.tracer_config().data.target_addr()),\n        ),\n    });\n\n    let privilege_mode = Span::raw(fmt_privilege_mode(\n        app.tracer_config().data.privilege_mode(),\n    ));\n\n    let as_mode = match app.resolver.config().resolve_method {\n        ResolveMethod::System => Span::raw(\"□ ASN\"),\n        ResolveMethod::Resolv | ResolveMethod::Google | ResolveMethod::Cloudflare => {\n            if app.tui_config.lookup_as_info {\n                Span::raw(\"■ ASN\")\n            } else {\n                Span::raw(\"□ ASN\")\n            }\n        }\n    };\n\n    let details = if app.show_hop_details {\n        Span::raw(format!(\"■ {}\", t!(\"details\")))\n    } else {\n        Span::raw(format!(\"□ {}\", t!(\"details\")))\n    };\n\n    let max_hosts = if let Some(m) = app.tui_config.max_addrs {\n        Span::raw(format!(\"»:{m:2}\"))\n    } else {\n        Span::raw(\"»: -\")\n    };\n\n    let privacy = if let Some(ttl) = app.tui_config.privacy_max_ttl {\n        Span::raw(format!(\"{}:{ttl:2}\", t!(\"privacy\")))\n    } else {\n        Span::raw(format!(\"{}: -\", t!(\"privacy\")))\n    };\n\n    let address_mode = match app.tui_config.address_mode {\n        AddressMode::Ip => Span::raw(\" ip \"),\n        AddressMode::Host => Span::raw(\"host\"),\n        AddressMode::Both => Span::raw(\"both\"),\n    };\n\n    let locale = Span::raw(app.tui_config.locale.as_str());\n\n    // these are configuration items that cannot be changed at runtime.\n    let left_line = Line::from(vec![\n        Span::raw(\" [\"),\n        protocol,\n        Span::raw(\"] [\"),\n        privilege_mode,\n        Span::raw(\"] [\"),\n        locale,\n        Span::raw(\"]\"),\n    ]);\n\n    // these are configuration items that can be toggled at runtime.\n    let right_line = Line::from(vec![\n        Span::raw(\" [\"),\n        as_mode,\n        Span::raw(\"] [\"),\n        details,\n        Span::raw(\"] [\"),\n        address_mode,\n        Span::raw(\"] [\"),\n        privacy,\n        Span::raw(\"] [\"),\n        max_hosts,\n        Span::raw(\"] \"),\n    ]);\n\n    let bar_style = Style::default()\n        .bg(app.tui_config.theme.info_bar_bg)\n        .fg(app.tui_config.theme.info_bar_text);\n    let left = Paragraph::new(left_line)\n        .style(bar_style)\n        .alignment(Alignment::Left);\n    let right = Paragraph::new(right_line)\n        .style(bar_style)\n        .alignment(Alignment::Right);\n\n    f.render_widget(right, rect);\n    f.render_widget(left, rect);\n}\n\nfn fmt_privilege_mode(privilege_mode: PrivilegeMode) -> Cow<'static, str> {\n    match privilege_mode {\n        PrivilegeMode::Privileged => t!(\"privileged\"),\n        PrivilegeMode::Unprivileged => t!(\"unprivileged\"),\n    }\n}\n\nconst fn fmt_target_family(target: IpAddr) -> &'static str {\n    match target {\n        IpAddr::V4(_) => \"IPv4\",\n        IpAddr::V6(_) => \"IPv6\",\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/body.rs",
    "content": "use crate::frontend::render::{bsod, chart, splash, table, world};\nuse crate::frontend::tui_app::TuiApp;\nuse ratatui::Frame;\nuse ratatui::layout::Rect;\n\n/// Render the body.\n///\n/// This is either an BSOD if there wa san error or the table of hop data or, if there is no data,\n/// the splash screen.\npub fn render(f: &mut Frame<'_>, rec: Rect, app: &mut TuiApp) {\n    if let Some(err) = app.selected_tracer_data.error() {\n        bsod::render(f, rec, err);\n    } else if app.tracer_data().hops().is_empty() {\n        splash::render(f, app, rec);\n    } else if app.show_chart {\n        chart::render(f, app, rec);\n    } else if app.show_map {\n        world::render(f, app, rec);\n    } else {\n        table::render(f, app, rec);\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/bsod.rs",
    "content": "use crate::t;\nuse ratatui::Frame;\nuse ratatui::layout::{Alignment, Constraint, Layout, Rect};\nuse ratatui::style::{Color, Modifier, Style};\nuse ratatui::text::{Line, Span};\nuse ratatui::widgets::{Block, BorderType, Borders, Paragraph};\n\n/// Render a blue screen of death.\npub fn render(f: &mut Frame<'_>, rect: Rect, error: &str) {\n    let chunks = Layout::default()\n        .constraints([Constraint::Percentage(35), Constraint::Percentage(65)].as_ref())\n        .split(rect);\n    let block = Block::default()\n        .title(Line::raw(t!(\"title_hops\")))\n        .borders(Borders::ALL)\n        .border_type(BorderType::Rounded)\n        .style(Style::default().bg(Color::Blue));\n    let line = vec![\n        Line::from(Span::styled(\n            t!(\"bsod_failed\"),\n            Style::default().add_modifier(Modifier::REVERSED),\n        )),\n        Line::from(\"\"),\n        Line::from(error),\n        Line::from(\"\"),\n        Line::raw(t!(\"bsod_quit\")),\n    ];\n    let paragraph = Paragraph::new(line).alignment(Alignment::Center);\n    f.render_widget(block, rect);\n    f.render_widget(paragraph, chunks[1]);\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/chart.rs",
    "content": "use crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse ratatui::Frame;\nuse ratatui::layout::{Alignment, Constraint, Rect};\nuse ratatui::style::Style;\nuse ratatui::symbols::Marker;\nuse ratatui::text::{Line, Span};\nuse ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType};\n\n/// Render the ping history for all hops as a chart.\npub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) {\n    let selected_hop = app.selected_hop_or_target();\n    let samples = app.selected_tracer_data.max_samples() / app.zoom_factor;\n    let series_data = app\n        .selected_tracer_data\n        .hops_for_flow(app.selected_flow)\n        .iter()\n        .map(|hop| {\n            hop.samples()\n                .iter()\n                .enumerate()\n                .take(samples)\n                .map(|(i, s)| (i as f64, s.as_secs_f64() * 1000_f64))\n                .collect::<Vec<_>>()\n        })\n        .collect::<Vec<_>>();\n    let max_sample = series_data\n        .iter()\n        .flatten()\n        .map(|&(_, s)| s)\n        .max_by_key(|&c| (c * 1000.0) as u64)\n        .unwrap_or_default();\n    let sets = series_data\n        .iter()\n        .enumerate()\n        .map(|(i, s)| {\n            Dataset::default()\n                .name(format!(\"{} {}\", t!(\"hop\"), i + 1))\n                .data(s)\n                .graph_type(GraphType::Line)\n                .marker(Marker::Braille)\n                .style(Style::default().fg({\n                    match i {\n                        i if i + 1 == selected_hop.ttl() as usize => {\n                            app.tui_config.theme.hops_chart_selected\n                        }\n                        _ => app.tui_config.theme.hops_chart_unselected,\n                    }\n                }))\n        })\n        .collect::<Vec<_>>();\n    let constraints = (Constraint::Ratio(1, 1), Constraint::Ratio(1, 1));\n    let chart = Chart::new(sets)\n        .x_axis(\n            Axis::default()\n                .title(Line::raw(t!(\"samples\")))\n                .bounds([0_f64, samples as f64])\n                .labels_alignment(Alignment::Right)\n                .labels(\n                    [\"0\".to_string(), format!(\"{samples} ({}x)\", app.zoom_factor)]\n                        .into_iter()\n                        .map(Span::from),\n                )\n                .style(Style::default().fg(app.tui_config.theme.hops_chart_axis)),\n        )\n        .y_axis(\n            Axis::default()\n                .title(Line::raw(t!(\"rtt\")))\n                .bounds([0_f64, max_sample])\n                .labels(\n                    [\n                        String::from(\"0.0\"),\n                        format!(\"{:.1}\", max_sample / 2_f64),\n                        format!(\"{max_sample:.1}\"),\n                    ]\n                    .into_iter()\n                    .map(Span::from),\n                )\n                .style(Style::default().fg(app.tui_config.theme.hops_chart_axis)),\n        )\n        .hidden_legend_constraints(constraints)\n        .style(\n            Style::default()\n                .bg(app.tui_config.theme.bg)\n                .fg(app.tui_config.theme.text),\n        )\n        .block(\n            Block::default()\n                .borders(Borders::ALL)\n                .border_type(BorderType::Rounded)\n                .border_style(Style::default().fg(app.tui_config.theme.border))\n                .title(Line::raw(t!(\"chart\"))),\n        );\n    f.render_widget(chart, rect);\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/flows.rs",
    "content": "use crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse ratatui::Frame;\nuse ratatui::layout::{Alignment, Rect};\nuse ratatui::style::{Modifier, Style};\nuse ratatui::text::Line;\nuse ratatui::widgets::{Bar, BarChart, BarGroup, Block, BorderType, Borders};\n\n/// Render the flows.\npub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) {\n    let round_flow_id = app.tracer_data().round_flow_id();\n    let data: Vec<_> = app\n        .flow_counts\n        .iter()\n        .map(|(flow_id, count)| {\n            let bar_color = if flow_id == &app.selected_flow {\n                app.tui_config.theme.flows_chart_bar_selected\n            } else {\n                app.tui_config.theme.flows_chart_bar_unselected\n            };\n            let label_color = if flow_id == &round_flow_id {\n                app.tui_config.theme.flows_chart_text_current\n            } else {\n                app.tui_config.theme.flows_chart_text_non_current\n            };\n            Bar::default()\n                .label(Line::from(format!(\"{flow_id}\")))\n                .value(*count as u64)\n                .style(Style::default().fg(bar_color))\n                .value_style(\n                    Style::default()\n                        .bg(bar_color)\n                        .fg(label_color)\n                        .add_modifier(Modifier::BOLD),\n                )\n        })\n        .collect();\n    let block = Block::default()\n        .title(Line::raw(t!(\"title_flows\")))\n        .title_alignment(Alignment::Left)\n        .borders(Borders::ALL)\n        .border_type(BorderType::Rounded)\n        .border_style(Style::default().fg(app.tui_config.theme.border))\n        .style(\n            Style::default()\n                .bg(app.tui_config.theme.bg)\n                .fg(app.tui_config.theme.text),\n        );\n    let group = BarGroup::default().bars(&data);\n    let flow_counts = BarChart::default()\n        .block(block)\n        .data(group)\n        .bar_width(4)\n        .bar_gap(1);\n    f.render_widget(flow_counts, rect);\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/footer.rs",
    "content": "use crate::frontend::render::{histogram, history};\nuse crate::frontend::tui_app::TuiApp;\nuse ratatui::Frame;\nuse ratatui::layout::{Constraint, Direction, Layout, Rect};\n\n/// Render the footer.\n///\n/// This contains the history and frequency charts.\npub fn render(f: &mut Frame<'_>, rec: Rect, app: &TuiApp) {\n    let bottom_chunks = Layout::default()\n        .direction(Direction::Horizontal)\n        .constraints([Constraint::Percentage(75), Constraint::Percentage(25)].as_ref())\n        .split(rec);\n    history::render(f, app, bottom_chunks[0]);\n    histogram::render(f, app, bottom_chunks[1]);\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/header.rs",
    "content": "use crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse chrono::SecondsFormat;\nuse humantime::format_duration;\nuse ratatui::Frame;\nuse ratatui::layout::{Alignment, Rect};\nuse ratatui::style::{Modifier, Style};\nuse ratatui::text::{Line, Span};\nuse ratatui::widgets::{Block, BorderType, Borders, Paragraph};\nuse std::net::IpAddr;\nuse std::str::FromStr;\nuse std::time::Duration;\nuse trippy_core::{Hop, PortDirection};\nuse trippy_dns::Resolver;\n\n/// Render the title, target, clock and basic keyboard controls.\n#[expect(clippy::too_many_lines)]\npub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) {\n    let header_block = Block::default()\n        .title(format!(\" {} v{} \", t!(\"trippy\"), clap::crate_version!()))\n        .title_alignment(Alignment::Center)\n        .borders(Borders::ALL)\n        .border_type(BorderType::Rounded)\n        .border_style(Style::default().fg(app.tui_config.theme.border))\n        .style(\n            Style::default()\n                .bg(app.tui_config.theme.bg)\n                .fg(app.tui_config.theme.text),\n        );\n    let now = if let Some(tz) = &app.tui_config.timezone {\n        chrono::Utc::now()\n            .with_timezone(tz)\n            .to_rfc3339_opts(SecondsFormat::Secs, true)\n    } else {\n        chrono::Local::now().to_rfc3339_opts(SecondsFormat::Secs, true)\n    };\n    let clock_span = Line::from(Span::raw(now));\n    let bold = Style::default().add_modifier(Modifier::BOLD);\n\n    let help_binding = app.tui_config.bindings.toggle_help.to_string();\n    let header_help = t!(\"header_help\");\n    let help_line_help = if header_help.starts_with(&help_binding) {\n        vec![\n            Span::styled(&help_binding, bold),\n            Span::raw(&header_help[help_binding.len()..]),\n        ]\n    } else {\n        vec![\n            Span::raw(\"[\"),\n            Span::styled(help_binding, bold),\n            Span::raw(\"]\"),\n            Span::raw(header_help),\n        ]\n    };\n\n    let settings_binding = app.tui_config.bindings.toggle_settings.to_string();\n    let header_settings = t!(\"header_settings\");\n    let help_line_settings = if header_settings.starts_with(&settings_binding) {\n        vec![\n            Span::styled(&settings_binding, bold),\n            Span::raw(&header_settings[settings_binding.len()..]),\n        ]\n    } else {\n        vec![\n            Span::raw(\"[\"),\n            Span::styled(settings_binding, bold),\n            Span::raw(\"]\"),\n            Span::raw(header_settings),\n        ]\n    };\n\n    let quit_binding = app.tui_config.bindings.quit.to_string();\n    let header_quit = t!(\"header_quit\");\n    let help_line_quit = if header_quit.starts_with(&quit_binding) {\n        vec![\n            Span::styled(&quit_binding, bold),\n            Span::raw(&header_quit[quit_binding.len()..]),\n        ]\n    } else {\n        vec![\n            Span::raw(\"[\"),\n            Span::styled(quit_binding, bold),\n            Span::raw(\"]\"),\n            Span::raw(header_quit),\n        ]\n    };\n\n    let help_span = Line::from(\n        [\n            help_line_help,\n            vec![Span::raw(\" \")],\n            help_line_settings,\n            vec![Span::raw(\" \")],\n            help_line_quit,\n        ]\n        .into_iter()\n        .flatten()\n        .collect::<Vec<_>>(),\n    );\n    let right_line = vec![clock_span, help_span];\n    let right = Paragraph::new(right_line)\n        .style(Style::default())\n        .block(header_block.clone())\n        .alignment(Alignment::Right);\n\n    let source = render_source(app);\n    let dest = render_destination(app);\n    let target = format!(\"{source} -> {dest}\");\n    let hop_count = app.tracer_data().hops_for_flow(app.selected_flow).len();\n    let discovered = if app.selected_tracer_data.max_flows() > 1 {\n        let plural_flows = if app.tracer_data().flows().len() > 1 {\n            t!(\"flows\")\n        } else {\n            t!(\"flow\")\n        };\n        let flow_count = app.tracer_data().flows().len();\n        format!(\n            \", {}\",\n            t!(\"discovered_flows\",\n                \"hop_count\" => hop_count,\n                \"flow_count\" => flow_count,\n                \"plural_flows\" => plural_flows\n            )\n        )\n    } else {\n        format!(\", {}\", t!(\"discovered\", \"hop_count\" => hop_count))\n    };\n    let left_line = vec![\n        Line::from(vec![\n            Span::styled(\n                format!(\"{}: \", t!(\"target\")),\n                Style::default().add_modifier(Modifier::BOLD),\n            ),\n            Span::raw(target),\n        ]),\n        Line::from(vec![\n            Span::styled(\n                format!(\"{}: \", t!(\"status\")),\n                Style::default().add_modifier(Modifier::BOLD),\n            ),\n            Span::raw(render_status(app)),\n            Span::raw(discovered),\n        ]),\n    ];\n\n    let left = Paragraph::new(left_line)\n        .style(Style::default())\n        .block(header_block)\n        .alignment(Alignment::Left);\n    f.render_widget(right, rect);\n    f.render_widget(left, rect);\n}\n\n/// Render the source address of the trace.\nfn render_source(app: &TuiApp) -> String {\n    fn format_ip(app: &TuiApp, src_addr: IpAddr) -> String {\n        match app.tracer_config().data.port_direction() {\n            PortDirection::None => {\n                format!(\"{src_addr}\")\n            }\n            PortDirection::FixedDest(_) => {\n                format!(\"{src_addr}:*\")\n            }\n            PortDirection::FixedSrc(src) | PortDirection::FixedBoth(src, _) => {\n                format!(\"{src_addr}:{}\", src.0)\n            }\n        }\n    }\n    fn format_both(app: &TuiApp, src_hostname: &str, src_addr: IpAddr) -> String {\n        match app.tracer_config().data.port_direction() {\n            PortDirection::None => {\n                format!(\"{src_addr} ({src_hostname})\")\n            }\n            PortDirection::FixedDest(_) => {\n                format!(\"{src_addr}:* ({src_hostname})\")\n            }\n            PortDirection::FixedSrc(src) | PortDirection::FixedBoth(src, _) => {\n                format!(\"{src_addr}:{} ({src_hostname})\", src.0)\n            }\n        }\n    }\n    if app.tui_config.privacy_max_ttl.is_some() {\n        format!(\"**{}**\", t!(\"hidden\"))\n    } else if let Some(addr) = app.tracer_config().data.source_addr() {\n        let entry = app.resolver.lazy_reverse_lookup_with_asinfo(addr);\n        if let Some(hostname) = entry.hostnames().next() {\n            format_both(app, hostname, addr)\n        } else {\n            format_ip(app, addr)\n        }\n    } else {\n        String::from(t!(\"unknown\"))\n    }\n}\n\n/// Render the destination address.\nfn render_destination(app: &TuiApp) -> String {\n    fn format_ip(app: &TuiApp, dest_addr: IpAddr) -> String {\n        match app.tracer_config().data.port_direction() {\n            PortDirection::None => {\n                format!(\"{dest_addr}\")\n            }\n            PortDirection::FixedSrc(_) => {\n                format!(\"{dest_addr}:*\")\n            }\n            PortDirection::FixedDest(dest) | PortDirection::FixedBoth(_, dest) => {\n                format!(\"{dest_addr}:{}\", dest.0)\n            }\n        }\n    }\n    fn format_both(app: &TuiApp, dest_hostname: &str, dest_addr: IpAddr) -> String {\n        match app.tracer_config().data.port_direction() {\n            PortDirection::None => {\n                format!(\"{dest_addr} ({dest_hostname})\")\n            }\n            PortDirection::FixedSrc(_) => {\n                format!(\"{dest_addr}:* ({dest_hostname})\")\n            }\n            PortDirection::FixedDest(dest) | PortDirection::FixedBoth(_, dest) => {\n                format!(\"{dest_addr}:{} ({dest_hostname})\", dest.0)\n            }\n        }\n    }\n    let dest_addr = app.tracer_config().data.target_addr();\n    let target_hostname = &app.tracer_config().target_hostname;\n    if let Ok(addr) = IpAddr::from_str(target_hostname) {\n        let entry = app.resolver.lazy_reverse_lookup_with_asinfo(addr);\n        let hostname = entry.hostnames().next().unwrap_or_else(|| target_hostname);\n        if hostname == target_hostname {\n            format_ip(app, addr)\n        } else {\n            format_both(app, hostname, addr)\n        }\n    } else {\n        format_both(app, target_hostname, dest_addr)\n    }\n}\n\n/// Render the headline status of the tracing.\nfn render_status(app: &TuiApp) -> String {\n    let failure_count: usize = app\n        .tracer_data()\n        .hops_for_flow(app.selected_flow)\n        .iter()\n        .map(Hop::total_failed)\n        .sum();\n    let failures = if failure_count > 0 {\n        let total_probes: usize = app\n            .tracer_data()\n            .hops_for_flow(app.selected_flow)\n            .iter()\n            .map(Hop::total_sent)\n            .sum();\n        let failure_rate = if total_probes > 0 {\n            (failure_count as f64 / total_probes as f64) * 100.0\n        } else {\n            0_f64\n        };\n        let failure_rate = format!(\"{failure_rate:.1}\");\n        format!(\n            \" [{}❗]\",\n            t!(\"status_failures\",\n                    \"failure_count\" => failure_count,\n                    \"total_probes\" => total_probes,\n                    \"failure_rate\" => failure_rate)\n        )\n    } else {\n        String::new()\n    };\n    if app.selected_tracer_data.error().is_some() {\n        String::from(t!(\"status_failed\"))\n    } else if let Some(start) = app.frozen_start {\n        let frozen = format_duration(Duration::from_secs(\n            start.elapsed().unwrap_or_default().as_secs(),\n        ));\n        format!(\"{} ({frozen}){failures}\", t!(\"status_frozen\"))\n    } else {\n        format!(\"{}{failures}\", t!(\"status_running\"))\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/help.rs",
    "content": "use crate::frontend::render::util;\nuse crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse ratatui::Frame;\nuse ratatui::layout::Alignment;\nuse ratatui::style::Style;\nuse ratatui::text::Line;\nuse ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph};\n\n/// Render help dialog.\npub fn render(f: &mut Frame<'_>, app: &TuiApp) {\n    let s = app.tui_config.bindings.toggle_settings;\n    let b = app.tui_config.bindings.toggle_settings_bindings;\n    let c = app.tui_config.bindings.toggle_settings_columns;\n    #[expect(clippy::needless_raw_string_hashes)]\n    let help_lines = vec![\n        Line::raw(r#\"                           \"#),\n        Line::raw(r#\" _____    _                \"#),\n        Line::raw(r#\"|_   _| _(_)_ __ _ __ _  _ \"#),\n        Line::raw(r#\"  | || '_| | '_ \\ '_ \\ || |\"#),\n        Line::raw(r#\"  |_||_| |_| .__/ .__/\\_, |\"#),\n        Line::raw(r#\"           |_|  |_|   |__/ \"#),\n        Line::raw(r#\"                           \"#),\n        Line::raw(t!(\"help_tagline\")),\n        Line::raw(r#\"                           \"#),\n        Line::raw(t!(\"help_show_settings\", key = s)),\n        Line::raw(t!(\"help_show_bindings\", key = b)),\n        Line::raw(t!(\"help_show_columns\", key = c)),\n        Line::raw(r#\"                           \"#),\n        Line::raw(r#\" https://github.com/fujiapple852/trippy \"#),\n        Line::raw(r#\"                           \"#),\n        Line::raw(t!(\"help_license\")),\n        Line::raw(r#\"                           \"#),\n        Line::raw(t!(\"help_copyright\")),\n    ];\n    let block = Block::default()\n        .title(format!(\" {} \", t!(\"title_help\")))\n        .title_alignment(Alignment::Center)\n        .borders(Borders::ALL)\n        .style(Style::default().bg(app.tui_config.theme.help_dialog_bg))\n        .border_type(BorderType::Double);\n    let control = Paragraph::new(help_lines)\n        .style(Style::default().fg(app.tui_config.theme.help_dialog_text))\n        .block(block.clone())\n        .alignment(Alignment::Center);\n    let area = util::centered_rect(60, 60, f.area());\n    f.render_widget(Clear, area);\n    f.render_widget(block, area);\n    f.render_widget(control, area);\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/histogram.rs",
    "content": "use crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse ratatui::Frame;\nuse ratatui::layout::Rect;\nuse ratatui::style::{Modifier, Style};\nuse ratatui::widgets::{BarChart, Block, BorderType, Borders};\nuse std::collections::BTreeMap;\nuse std::time::Duration;\n\n/// Render a histogram of ping frequencies.\npub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) {\n    let selected_hop = app.selected_hop_or_target();\n    let freq_data = sample_frequency(selected_hop.samples());\n    let freq_data_ref: Vec<_> = freq_data.iter().map(|(b, c)| (b.as_str(), *c)).collect();\n    let barchart = BarChart::default()\n        .block(\n            Block::default()\n                .title(format!(\"{} #{}\", t!(\"title_frequency\"), selected_hop.ttl()))\n                .style(\n                    Style::default()\n                        .bg(app.tui_config.theme.bg)\n                        .fg(app.tui_config.theme.text),\n                )\n                .borders(Borders::ALL)\n                .border_type(BorderType::Rounded)\n                .border_style(Style::default().fg(app.tui_config.theme.border)),\n        )\n        .data(freq_data_ref.as_slice())\n        .bar_width(4)\n        .bar_gap(1)\n        .bar_style(Style::default().fg(app.tui_config.theme.frequency_chart_bar))\n        .value_style(\n            Style::default()\n                .bg(app.tui_config.theme.frequency_chart_bar)\n                .fg(app.tui_config.theme.frequency_chart_text)\n                .add_modifier(Modifier::BOLD),\n        );\n    f.render_widget(barchart, rect);\n}\n\n/// Return the frequency % grouped by sample duration.\nfn sample_frequency(samples: &[Duration]) -> Vec<(String, u64)> {\n    let sample_count = samples.len();\n    let mut count_by_duration: BTreeMap<u128, u64> = BTreeMap::new();\n    for sample in samples {\n        if !sample.is_zero() {\n            *count_by_duration.entry(sample.as_millis()).or_default() += 1;\n        }\n    }\n    count_by_duration\n        .iter()\n        .map(|(ping, count)| {\n            let ping = format!(\"{ping}\");\n            let freq_pct = ((*count as f64 / sample_count as f64) * 100_f64) as u64;\n            (ping, freq_pct)\n        })\n        .collect()\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/history.rs",
    "content": "use crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse ratatui::Frame;\nuse ratatui::layout::Rect;\nuse ratatui::style::Style;\nuse ratatui::widgets::{Block, BorderType, Borders, Sparkline};\n\n/// Render the ping history for the final hop which is typically the target.\npub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) {\n    let selected_hop = app.selected_hop_or_target();\n    let data = selected_hop\n        .samples()\n        .iter()\n        .take(rect.width as usize)\n        .map(|s| {\n            if s.is_zero() {\n                None\n            } else {\n                Some(s.as_micros() as u64)\n            }\n        })\n        .collect::<Vec<_>>();\n    let history = Sparkline::default()\n        .block(\n            Block::default()\n                .title(format!(\"{} #{}\", t!(\"title_samples\"), selected_hop.ttl()))\n                .style(\n                    Style::default()\n                        .bg(app.tui_config.theme.bg)\n                        .fg(app.tui_config.theme.text),\n                )\n                .borders(Borders::ALL)\n                .border_type(BorderType::Rounded)\n                .border_style(Style::default().fg(app.tui_config.theme.border)),\n        )\n        .data(data.as_slice())\n        .style(\n            Style::default()\n                .bg(app.tui_config.theme.bg)\n                .fg(app.tui_config.theme.samples_chart),\n        )\n        .absent_value_style(\n            Style::default()\n                .bg(app.tui_config.theme.bg)\n                .fg(app.tui_config.theme.samples_chart_lost),\n        )\n        .absent_value_symbol(ratatui::symbols::bar::FULL);\n    f.render_widget(history, rect);\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/settings.rs",
    "content": "use crate::config::{AddressMode, AsMode, GeoIpMode, IcmpExtensionMode};\nuse crate::frontend::render::util;\nuse crate::frontend::theme;\nuse crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse humantime::format_duration;\nuse ratatui::Frame;\nuse ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};\nuse ratatui::style::{Modifier, Style};\nuse ratatui::text::{Line, Span};\nuse ratatui::widgets::{\n    Block, BorderType, Borders, Cell, Clear, Paragraph, Row, Table, Tabs, Wrap,\n};\nuse trippy_core::PortDirection;\nuse trippy_dns::ResolveMethod;\n\n/// Render settings dialog.\npub fn render(f: &mut Frame<'_>, app: &mut TuiApp) {\n    let all_settings = format_all_settings(app);\n    let (name, info, items) = &all_settings[app.settings_tab_selected];\n    let area = util::centered_rect(60, 60, f.area());\n    let chunks = Layout::default()\n        .direction(Direction::Vertical)\n        .constraints(SETTINGS_TABLE_WIDTH.as_ref())\n        .split(area);\n    f.render_widget(Clear, area);\n    render_settings_tabs(f, app, chunks[0]);\n    render_settings_table(f, app, chunks[1], name, items);\n    render_settings_info(f, app, chunks[2], info);\n}\n\n/// Render settings tabs.\nfn render_settings_tabs(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) {\n    let titles: Vec<_> = settings_tabs()\n        .into_iter()\n        .map(|(title, _)| {\n            Line::from(Span::styled(\n                title,\n                Style::default().fg(app.tui_config.theme.settings_tab_text),\n            ))\n        })\n        .collect();\n    let tabs = Tabs::new(titles)\n        .block(\n            Block::default()\n                .title(format!(\" {} \", t!(\"title_settings\")))\n                .title_alignment(Alignment::Center)\n                .borders(Borders::ALL)\n                .style(Style::default().bg(app.tui_config.theme.settings_dialog_bg))\n                .border_type(BorderType::Double),\n        )\n        .select(app.settings_tab_selected)\n        .style(Style::default())\n        .highlight_style(Style::default().add_modifier(Modifier::BOLD));\n    f.render_widget(tabs, rect);\n}\n\n/// Render settings table.\nfn render_settings_table(\n    f: &mut Frame<'_>,\n    app: &mut TuiApp,\n    rect: Rect,\n    name: &str,\n    items: &[SettingsItem],\n) {\n    let header_cells = settings_table_header().into_iter().map(|h| {\n        Cell::from(h).style(Style::default().fg(app.tui_config.theme.settings_table_header_text))\n    });\n    let header = Row::new(header_cells)\n        .style(Style::default().bg(app.tui_config.theme.settings_table_header_bg))\n        .height(1)\n        .bottom_margin(0);\n    let rows = items.iter().map(|item| {\n        Row::new(vec![\n            Cell::from(item.item.as_str()),\n            Cell::from(item.value.as_str()),\n        ])\n        .style(Style::default().fg(app.tui_config.theme.settings_table_row_text))\n    });\n    let item_width = items\n        .iter()\n        .map(|item| item.item.len() as u16)\n        .max()\n        .unwrap_or_default()\n        .max(30);\n    let table_widths = [Constraint::Min(item_width), Constraint::Length(60)];\n    let table = Table::new(rows, table_widths)\n        .header(header)\n        .block(\n            Block::default()\n                .title(format!(\" {name} \"))\n                .title_alignment(Alignment::Left)\n                .borders(Borders::ALL)\n                .style(Style::default().bg(app.tui_config.theme.settings_dialog_bg))\n                .border_type(BorderType::Plain),\n        )\n        .style(\n            Style::default()\n                .bg(app.tui_config.theme.bg)\n                .fg(app.tui_config.theme.text),\n        )\n        .row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));\n    f.render_stateful_widget(table, rect, &mut app.setting_table_state);\n}\n\n/// Render settings info footer.\nfn render_settings_info(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, info: &str) {\n    let info = Paragraph::new(info)\n        .style(Style::default())\n        .wrap(Wrap::default())\n        .block(\n            Block::default()\n                .title(format!(\" {} \", t!(\"settings_info\")))\n                .title_alignment(Alignment::Center)\n                .borders(Borders::ALL)\n                .style(Style::default().bg(app.tui_config.theme.settings_dialog_bg))\n                .border_type(BorderType::Plain),\n        )\n        .alignment(Alignment::Left);\n    f.render_widget(info, rect);\n}\n\n/// Format all settings.\nfn format_all_settings(app: &TuiApp) -> Vec<(String, String, Vec<SettingsItem>)> {\n    let tui_settings = format_tui_settings(app);\n    let trace_settings = format_trace_settings(app);\n    let dns_settings = format_dns_settings(app);\n    let geoip_settings = format_geoip_settings(app);\n    let bindings_settings = format_binding_settings(app);\n    let theme_settings = format_theme_settings(app);\n    let columns_settings = format_columns_settings(app);\n    let toggle_column = app.tui_config.bindings.toggle_chart.to_string();\n    let move_down = app.tui_config.bindings.next_hop_address.to_string();\n    let move_up = app.tui_config.bindings.previous_hop_address.to_string();\n    vec![\n        (\n            t!(\"settings_tab_tui_title\").to_string(),\n            t!(\"settings_tab_tui_desc\").to_string(),\n            tui_settings,\n        ),\n        (\n            t!(\"settings_tab_trace_title\").to_string(),\n            t!(\"settings_tab_trace_desc\").to_string(),\n            trace_settings,\n        ),\n        (\n            t!(\"settings_tab_dns_title\").to_string(),\n            t!(\"settings_tab_dns_desc\").to_string(),\n            dns_settings,\n        ),\n        (\n            t!(\"settings_tab_geoip_title\").to_string(),\n            t!(\"settings_tab_geoip_desc\").to_string(),\n            geoip_settings,\n        ),\n        (\n            t!(\"settings_tab_bindings_title\").to_string(),\n            t!(\"settings_tab_bindings_desc\").to_string(),\n            bindings_settings,\n        ),\n        (\n            t!(\"settings_tab_theme_title\").to_string(),\n            t!(\"settings_tab_theme_desc\").to_string(),\n            theme_settings,\n        ),\n        (\n            t!(\"settings_tab_columns_title\").to_string(),\n            t!(\n                \"settings_tab_columns_desc\",\n                c = toggle_column,\n                d = move_down,\n                u = move_up\n            ),\n            columns_settings,\n        ),\n    ]\n}\n\n/// Format Tui settings.\nfn format_tui_settings(app: &TuiApp) -> Vec<SettingsItem> {\n    vec![\n        SettingsItem::new(\n            \"tui-preserve-screen\",\n            format!(\"{}\", app.tui_config.preserve_screen),\n        ),\n        SettingsItem::new(\n            \"tui-refresh-rate\",\n            format!(\"{}\", format_duration(app.tui_config.refresh_rate)),\n        ),\n        SettingsItem::new(\n            \"tui-privacy-max-ttl\",\n            app.tui_config\n                .privacy_max_ttl\n                .map_or_else(|| t!(\"off\").to_string(), |m| m.to_string()),\n        ),\n        SettingsItem::new(\n            \"tui-address-mode\",\n            format_address_mode(app.tui_config.address_mode),\n        ),\n        SettingsItem::new(\"tui-as-mode\", format_as_mode(app.tui_config.as_mode)),\n        SettingsItem::new(\n            \"tui-icmp-extension-mode\",\n            format_extension_mode(app.tui_config.icmp_extension_mode),\n        ),\n        SettingsItem::new(\n            \"tui-geoip-mode\",\n            format_geoip_mode(app.tui_config.geoip_mode),\n        ),\n        SettingsItem::new(\n            \"tui-max-addrs\",\n            app.tui_config\n                .max_addrs\n                .map_or_else(|| t!(\"auto\").to_string(), |m| m.to_string()),\n        ),\n        SettingsItem::new(\n            \"tui-custom-columns\",\n            format!(\"{}\", app.tui_config.tui_columns),\n        ),\n        SettingsItem::new(\n            \"tui-timezone\",\n            app.tui_config\n                .timezone\n                .map_or_else(|| t!(\"auto\").to_string(), |tz| tz.to_string()),\n        ),\n    ]\n}\n\n/// Format trace settings.\nfn format_trace_settings(app: &TuiApp) -> Vec<SettingsItem> {\n    let cfg = app.tracer_config();\n    let interface = if let Some(iface) = cfg.data.interface() {\n        iface.to_string()\n    } else {\n        t!(\"auto\").to_string()\n    };\n    let (src_port, dst_port) = match cfg.data.port_direction() {\n        PortDirection::None => (t!(\"na\").to_string(), t!(\"na\").to_string()),\n        PortDirection::FixedDest(dst) => (t!(\"auto\").to_string(), format!(\"{}\", dst.0)),\n        PortDirection::FixedSrc(src) => (format!(\"{}\", src.0), t!(\"auto\").to_string()),\n        PortDirection::FixedBoth(src, dst) => (format!(\"{}\", src.0), format!(\"{}\", dst.0)),\n    };\n    vec![\n        SettingsItem::new(\"first-ttl\", format!(\"{}\", cfg.data.first_ttl().0)),\n        SettingsItem::new(\"max-ttl\", format!(\"{}\", cfg.data.max_ttl().0)),\n        SettingsItem::new(\n            \"min-round-duration\",\n            format!(\"{}\", format_duration(cfg.data.min_round_duration())),\n        ),\n        SettingsItem::new(\n            \"max-round-duration\",\n            format!(\"{}\", format_duration(cfg.data.max_round_duration())),\n        ),\n        SettingsItem::new(\n            \"grace-duration\",\n            format!(\"{}\", format_duration(cfg.data.grace_duration())),\n        ),\n        SettingsItem::new(\"max-inflight\", format!(\"{}\", cfg.data.max_inflight().0)),\n        SettingsItem::new(\n            \"initial-sequence\",\n            format!(\"{}\", cfg.data.initial_sequence().0),\n        ),\n        SettingsItem::new(\n            \"read-timeout\",\n            format!(\"{}\", format_duration(cfg.data.read_timeout())),\n        ),\n        SettingsItem::new(\"packet-size\", format!(\"{}\", cfg.data.packet_size().0)),\n        SettingsItem::new(\n            \"payload-pattern\",\n            format!(\"{}\", cfg.data.payload_pattern().0),\n        ),\n        SettingsItem::new(\"tos\", format!(\"{}\", cfg.data.tos().0)),\n        SettingsItem::new(\n            \"icmp-extensions\",\n            format!(\"{}\", cfg.data.icmp_extension_parse_mode()),\n        ),\n        SettingsItem::new(\"interface\", interface),\n        SettingsItem::new(\n            \"multipath-strategy\",\n            cfg.data.multipath_strategy().to_string(),\n        ),\n        SettingsItem::new(\"target-port\", dst_port),\n        SettingsItem::new(\"source-port\", src_port),\n        SettingsItem::new(\n            \"max-samples\",\n            format!(\"{}\", app.selected_tracer_data.max_samples()),\n        ),\n        SettingsItem::new(\n            \"max-flows\",\n            format!(\"{}\", app.selected_tracer_data.max_flows()),\n        ),\n    ]\n}\n\n/// Format DNS settings.\nfn format_dns_settings(app: &TuiApp) -> Vec<SettingsItem> {\n    vec![\n        SettingsItem::new(\n            \"dns-timeout\",\n            format!(\"{}\", format_duration(app.resolver.config().timeout)),\n        ),\n        SettingsItem::new(\n            \"dns-ttl\",\n            format!(\"{}\", format_duration(app.resolver.config().ttl)),\n        ),\n        SettingsItem::new(\n            \"dns-resolve-method\",\n            format_dns_method(app.resolver.config().resolve_method),\n        ),\n        SettingsItem::new(\n            \"dns-resolve-all\",\n            format!(\"{}\", app.tui_config.dns_resolve_all),\n        ),\n        SettingsItem::new(\n            \"dns-lookup-as-info\",\n            format!(\"{}\", app.tui_config.lookup_as_info),\n        ),\n    ]\n}\n\n/// Format `GeoIp` settings.\nfn format_geoip_settings(app: &TuiApp) -> Vec<SettingsItem> {\n    vec![SettingsItem::new(\n        \"geoip-mmdb-file\",\n        app.tui_config\n            .geoip_mmdb_file\n            .as_deref()\n            .map_or_else(|| t!(\"none\").to_string(), ToString::to_string),\n    )]\n}\n\n/// Format binding settings.\nfn format_binding_settings(app: &TuiApp) -> Vec<SettingsItem> {\n    let binds = &app.tui_config.bindings;\n    vec![\n        SettingsItem::new(\"toggle-help\", format!(\"{}\", binds.toggle_help)),\n        SettingsItem::new(\"toggle-help-alt\", format!(\"{}\", binds.toggle_help_alt)),\n        SettingsItem::new(\"toggle-settings\", format!(\"{}\", binds.toggle_settings)),\n        SettingsItem::new(\n            \"toggle-settings-tui\",\n            format!(\"{}\", binds.toggle_settings_tui),\n        ),\n        SettingsItem::new(\n            \"toggle-settings-trace\",\n            format!(\"{}\", binds.toggle_settings_trace),\n        ),\n        SettingsItem::new(\n            \"toggle-settings-dns\",\n            format!(\"{}\", binds.toggle_settings_dns),\n        ),\n        SettingsItem::new(\n            \"toggle-settings-geoip\",\n            format!(\"{}\", binds.toggle_settings_geoip),\n        ),\n        SettingsItem::new(\n            \"toggle-settings-bindings\",\n            format!(\"{}\", binds.toggle_settings_bindings),\n        ),\n        SettingsItem::new(\n            \"toggle-settings-theme\",\n            format!(\"{}\", binds.toggle_settings_theme),\n        ),\n        SettingsItem::new(\n            \"toggle-settings-columns\",\n            format!(\"{}\", binds.toggle_settings_columns),\n        ),\n        SettingsItem::new(\"next-hop\", format!(\"{}\", binds.next_hop)),\n        SettingsItem::new(\"previous-hop\", format!(\"{}\", binds.previous_hop)),\n        SettingsItem::new(\"next-trace\", format!(\"{}\", binds.next_trace)),\n        SettingsItem::new(\"previous-trace\", format!(\"{}\", binds.previous_trace)),\n        SettingsItem::new(\"next-hop-address\", format!(\"{}\", binds.next_hop_address)),\n        SettingsItem::new(\n            \"previous-hop-address\",\n            format!(\"{}\", binds.previous_hop_address),\n        ),\n        SettingsItem::new(\"address-mode-ip\", format!(\"{}\", binds.address_mode_ip)),\n        SettingsItem::new(\"address-mode-host\", format!(\"{}\", binds.address_mode_host)),\n        SettingsItem::new(\"address-mode-both\", format!(\"{}\", binds.address_mode_both)),\n        SettingsItem::new(\"toggle-freeze\", format!(\"{}\", binds.toggle_freeze)),\n        SettingsItem::new(\"toggle-chart\", format!(\"{}\", binds.toggle_chart)),\n        SettingsItem::new(\"toggle-map\", format!(\"{}\", binds.toggle_map)),\n        SettingsItem::new(\"toggle-flows\", format!(\"{}\", binds.toggle_flows)),\n        SettingsItem::new(\"expand-privacy\", format!(\"{}\", binds.expand_privacy)),\n        SettingsItem::new(\"contract-privacy\", format!(\"{}\", binds.contract_privacy)),\n        SettingsItem::new(\"expand-hosts\", format!(\"{}\", binds.expand_hosts)),\n        SettingsItem::new(\"expand-hosts-max\", format!(\"{}\", binds.expand_hosts_max)),\n        SettingsItem::new(\"contract-hosts\", format!(\"{}\", binds.contract_hosts)),\n        SettingsItem::new(\n            \"contract-hosts-min\",\n            format!(\"{}\", binds.contract_hosts_min),\n        ),\n        SettingsItem::new(\"chart-zoom-in\", format!(\"{}\", binds.chart_zoom_in)),\n        SettingsItem::new(\"chart-zoom-out\", format!(\"{}\", binds.chart_zoom_out)),\n        SettingsItem::new(\"clear-trace-data\", format!(\"{}\", binds.clear_trace_data)),\n        SettingsItem::new(\"clear-dns-cache\", format!(\"{}\", binds.clear_dns_cache)),\n        SettingsItem::new(\"clear-selection\", format!(\"{}\", binds.clear_selection)),\n        SettingsItem::new(\"toggle-as-info\", format!(\"{}\", binds.toggle_as_info)),\n        SettingsItem::new(\n            \"toggle-hop-details\",\n            format!(\"{}\", binds.toggle_hop_details),\n        ),\n        SettingsItem::new(\"quit\", format!(\"{}\", binds.quit)),\n        SettingsItem::new(\n            \"quit-preserve-screen\",\n            format!(\"{}\", binds.quit_preserve_screen),\n        ),\n    ]\n}\n\n/// Format theme settings.\n#[expect(clippy::too_many_lines)]\nfn format_theme_settings(app: &TuiApp) -> Vec<SettingsItem> {\n    let theme = &app.tui_config.theme;\n    vec![\n        SettingsItem::new(\"bg-color\", theme::fmt_color(theme.bg)),\n        SettingsItem::new(\"border-color\", theme::fmt_color(theme.border)),\n        SettingsItem::new(\"text-color\", theme::fmt_color(theme.text)),\n        SettingsItem::new(\"tab-text-color\", theme::fmt_color(theme.tab_text)),\n        SettingsItem::new(\n            \"hops-table-header-bg-color\",\n            theme::fmt_color(theme.hops_table_header_bg),\n        ),\n        SettingsItem::new(\n            \"hops-table-header-text-color\",\n            theme::fmt_color(theme.hops_table_header_text),\n        ),\n        SettingsItem::new(\n            \"hops-table-row-active-text-color\",\n            theme::fmt_color(theme.hops_table_row_active_text),\n        ),\n        SettingsItem::new(\n            \"hops-table-row-inactive-text-color\",\n            theme::fmt_color(theme.hops_table_row_inactive_text),\n        ),\n        SettingsItem::new(\n            \"hops-chart-selected-color\",\n            theme::fmt_color(theme.hops_chart_selected),\n        ),\n        SettingsItem::new(\n            \"hops-chart-unselected-color\",\n            theme::fmt_color(theme.hops_chart_unselected),\n        ),\n        SettingsItem::new(\n            \"hops-chart-axis-color\",\n            theme::fmt_color(theme.hops_chart_axis),\n        ),\n        SettingsItem::new(\n            \"frequency-chart-bar-color\",\n            theme::fmt_color(theme.frequency_chart_bar),\n        ),\n        SettingsItem::new(\n            \"frequency-chart-text-color\",\n            theme::fmt_color(theme.frequency_chart_text),\n        ),\n        SettingsItem::new(\n            \"flows-chart-bar-selected-color\",\n            theme::fmt_color(theme.flows_chart_bar_selected),\n        ),\n        SettingsItem::new(\n            \"flows-chart-bar-unselected-color\",\n            theme::fmt_color(theme.flows_chart_bar_unselected),\n        ),\n        SettingsItem::new(\n            \"flows-chart-text-current-color\",\n            theme::fmt_color(theme.flows_chart_text_current),\n        ),\n        SettingsItem::new(\n            \"flows-chart-text-non-current-color\",\n            theme::fmt_color(theme.flows_chart_text_non_current),\n        ),\n        SettingsItem::new(\n            \"samples-chart-color \",\n            theme::fmt_color(theme.samples_chart),\n        ),\n        SettingsItem::new(\n            \"help-dialog-bg-color\",\n            theme::fmt_color(theme.help_dialog_bg),\n        ),\n        SettingsItem::new(\n            \"help-dialog-text-color\",\n            theme::fmt_color(theme.help_dialog_text),\n        ),\n        SettingsItem::new(\n            \"settings-dialog-bg-color\",\n            theme::fmt_color(theme.settings_dialog_bg),\n        ),\n        SettingsItem::new(\n            \"settings-tab-text-color\",\n            theme::fmt_color(theme.settings_tab_text),\n        ),\n        SettingsItem::new(\n            \"settings-table-header-text-color\",\n            theme::fmt_color(theme.settings_table_header_text),\n        ),\n        SettingsItem::new(\n            \"settings-table-header-bg-color\",\n            theme::fmt_color(theme.settings_table_header_bg),\n        ),\n        SettingsItem::new(\n            \"settings-table-row-text-color\",\n            theme::fmt_color(theme.settings_table_row_text),\n        ),\n        SettingsItem::new(\"map-world-color\", theme::fmt_color(theme.map_world)),\n        SettingsItem::new(\"map-radius-color\", theme::fmt_color(theme.map_radius)),\n        SettingsItem::new(\"map-selected-color\", theme::fmt_color(theme.map_selected)),\n        SettingsItem::new(\n            \"map-info-panel-border-color\",\n            theme::fmt_color(theme.map_info_panel_border),\n        ),\n        SettingsItem::new(\n            \"map-info-panel-bg-color\",\n            theme::fmt_color(theme.map_info_panel_bg),\n        ),\n        SettingsItem::new(\n            \"map-info-panel-text-color\",\n            theme::fmt_color(theme.map_info_panel_text),\n        ),\n        SettingsItem::new(\"info-bar-bg-color\", theme::fmt_color(theme.info_bar_bg)),\n        SettingsItem::new(\"info-bar-text-color\", theme::fmt_color(theme.info_bar_text)),\n    ]\n}\n\n/// Format columns settings.\nfn format_columns_settings(app: &TuiApp) -> Vec<SettingsItem> {\n    app.tui_config\n        .tui_columns\n        .all_columns()\n        .map(|c| SettingsItem::new(c.typ.to_string(), c.status.to_string()))\n        .collect()\n}\n\n/// The index of the columns tab.\npub const SETTINGS_TAB_COLUMNS: usize = 6;\n\n/// The name and number of items for each tabs in the setting dialog.\npub fn settings_tabs() -> [(String, usize); 7] {\n    [\n        (t!(\"settings_tab_tui_title\").to_string(), 10),\n        (t!(\"settings_tab_trace_title\").to_string(), 18),\n        (t!(\"settings_tab_dns_title\").to_string(), 5),\n        (t!(\"settings_tab_geoip_title\").to_string(), 1),\n        (t!(\"settings_tab_bindings_title\").to_string(), 37),\n        (t!(\"settings_tab_theme_title\").to_string(), 33),\n        (t!(\"settings_tab_columns_title\").to_string(), 0),\n    ]\n}\n\n/// The settings table header.\npub fn settings_table_header() -> [String; 2] {\n    [\n        t!(\"settings_table_header_setting\").to_string(),\n        t!(\"settings_table_header_value\").to_string(),\n    ]\n}\n\nconst SETTINGS_TABLE_WIDTH: [Constraint; 3] = [\n    Constraint::Length(3),\n    Constraint::Min(1),\n    Constraint::Length(4),\n];\n\nstruct SettingsItem {\n    item: String,\n    value: String,\n}\n\nimpl SettingsItem {\n    pub fn new(item: impl Into<String>, value: String) -> Self {\n        Self {\n            item: item.into(),\n            value,\n        }\n    }\n}\n\n/// Format the `DnsResolveMethod`.\nfn format_dns_method(resolve_method: ResolveMethod) -> String {\n    match resolve_method {\n        ResolveMethod::System => String::from(\"system\"),\n        ResolveMethod::Resolv => String::from(\"resolv\"),\n        ResolveMethod::Google => String::from(\"google\"),\n        ResolveMethod::Cloudflare => String::from(\"cloudflare\"),\n    }\n}\n\nfn format_extension_mode(icmp_extension_mode: IcmpExtensionMode) -> String {\n    match icmp_extension_mode {\n        IcmpExtensionMode::Off => \"off\".to_string(),\n        IcmpExtensionMode::Mpls => \"mpls\".to_string(),\n        IcmpExtensionMode::Full => \"full\".to_string(),\n        IcmpExtensionMode::All => \"all\".to_string(),\n    }\n}\n\n/// Format the `AsMode`.\nfn format_as_mode(as_mode: AsMode) -> String {\n    match as_mode {\n        AsMode::Asn => \"asn\".to_string(),\n        AsMode::Prefix => \"prefix\".to_string(),\n        AsMode::CountryCode => \"country-code\".to_string(),\n        AsMode::Registry => \"registry\".to_string(),\n        AsMode::Allocated => \"allocated\".to_string(),\n        AsMode::Name => \"name\".to_string(),\n    }\n}\n\n/// Format the `AddressMode`.\nfn format_address_mode(address_mode: AddressMode) -> String {\n    match address_mode {\n        AddressMode::Ip => \"ip\".to_string(),\n        AddressMode::Host => \"host\".to_string(),\n        AddressMode::Both => \"both\".to_string(),\n    }\n}\n\n/// Format the `GeoIpMode`.\nfn format_geoip_mode(geoip_mode: GeoIpMode) -> String {\n    match geoip_mode {\n        GeoIpMode::Off => \"off\".to_string(),\n        GeoIpMode::Short => \"short\".to_string(),\n        GeoIpMode::Long => \"long\".to_string(),\n        GeoIpMode::Location => \"location\".to_string(),\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/splash.rs",
    "content": "use crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse ratatui::Frame;\nuse ratatui::layout::{Alignment, Constraint, Layout, Rect};\nuse ratatui::style::Style;\nuse ratatui::text::{Line, Span};\nuse ratatui::widgets::{Block, BorderType, Borders, Paragraph};\nuse std::borrow::Cow;\n\n/// Render the splash screen.\n///\n/// This is shown on startup whilst we await the first round of data to be available.\npub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) {\n    let chunks = Layout::default()\n        .constraints([Constraint::Percentage(35), Constraint::Percentage(65)].as_ref())\n        .split(rect);\n    let block = Block::default()\n        .title(Line::raw(t!(\"title_hops\")))\n        .borders(Borders::ALL)\n        .border_type(BorderType::Rounded)\n        .border_style(Style::default().fg(app.tui_config.theme.border))\n        .style(\n            Style::default()\n                .bg(app.tui_config.theme.bg)\n                .fg(app.tui_config.theme.text),\n        );\n    #[expect(clippy::needless_raw_string_hashes)]\n    let splash: Vec<Cow<'static, str>> = vec![\n        r#\" _____    _                \"#.into(),\n        r#\"|_   _| _(_)_ __ _ __ _  _ \"#.into(),\n        r#\"  | || '_| | '_ \\ '_ \\ || |\"#.into(),\n        r#\"  |_||_| |_| .__/ .__/\\_, |\"#.into(),\n        r#\"           |_|  |_|   |__/ \"#.into(),\n        \"\".into(),\n        t!(\"awaiting_data\"),\n    ];\n    let line: Vec<_> = splash\n        .into_iter()\n        .map(|line| Line::from(Span::styled(line, Style::default())))\n        .collect();\n    let paragraph = Paragraph::new(line).alignment(Alignment::Center);\n    f.render_widget(block, rect);\n    f.render_widget(paragraph, chunks[1]);\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/table.rs",
    "content": "use crate::config::{AddressMode, AsMode, GeoIpMode, IcmpExtensionMode};\nuse crate::frontend::columns::{ColumnType, Columns};\nuse crate::frontend::config::TuiConfig;\nuse crate::frontend::theme::Theme;\nuse crate::frontend::tui_app::TuiApp;\nuse crate::geoip::{GeoIpCity, GeoIpLookup};\nuse crate::t;\nuse itertools::Itertools;\nuse ratatui::Frame;\nuse ratatui::layout::Rect;\nuse ratatui::prelude::Line;\nuse ratatui::style::{Modifier, Style};\nuse ratatui::widgets::{Block, BorderType, Borders, Cell, Row, Table};\nuse std::fmt::Write;\nuse std::net::IpAddr;\nuse std::rc::Rc;\nuse trippy_core::{\n    Dscp, Ecn, Extension, Extensions, IcmpPacketType, MplsLabelStackMember, UnknownExtension,\n};\nuse trippy_core::{Hop, NatStatus};\nuse trippy_dns::{AsInfo, DnsEntry, DnsResolver, Resolved, Resolver, Unresolved};\n\n/// Render the table of data about the hops.\n///\n/// For each hop, we show by default:\n///\n/// - The time-to-live (indexed from 1) at this hop (`#`)\n/// - The host(s) reported at this hop (`Host`)\n/// - The packet loss % for all probes at this hop (`Loss%`)\n/// - The number of requests sent for all probes at this hop (`Snt`)\n/// - The number of replies received for all probes at this hop (`Recv`)\n/// - The round-trip time of the most recent probe at this hop (`Last`)\n/// - The average round-trip time for all probes at this hop (`Avg`)\n/// - The best round-trip time for all probes at this hop (`Best`)\n/// - The worst round-trip time for all probes at this hop (`Wrst`)\n/// - The standard deviation round-trip time for all probes at this hop (`StDev`)\n/// - The status of this hop (`Sts`)\n///\n/// Optional columns that can be added:\n///\n/// - The current jitter i.e. round-trip difference with the last round-trip ('Jttr')\n/// - The average jitter time for all probes at this hop ('Javg')\n/// - The worst round-trip jitter time for all probes at this hop ('Jmax')\n/// - The smoothed jitter value for all probes at this hop ('Jinta')\npub fn render(f: &mut Frame<'_>, app: &mut TuiApp, rect: Rect) {\n    let config = &app.tui_config;\n    let widths = config.tui_columns.constraints(rect);\n    let header = render_table_header(app.tui_config.theme, &config.tui_columns);\n    let selected_style = Style::default().add_modifier(Modifier::REVERSED);\n    let rows = app\n        .tracer_data()\n        .hops_for_flow(app.selected_flow)\n        .iter()\n        .map(|hop| {\n            render_table_row(\n                app,\n                hop,\n                &app.resolver,\n                &app.geoip_lookup,\n                &app.tui_config,\n                &config.tui_columns,\n            )\n        });\n    let table = Table::new(rows, widths.as_slice())\n        .header(header)\n        .block(\n            Block::default()\n                .borders(Borders::ALL)\n                .border_type(BorderType::Rounded)\n                .border_style(Style::default().fg(app.tui_config.theme.border))\n                .title(Line::raw(t!(\"title_hops\"))),\n        )\n        .style(\n            Style::default()\n                .bg(app.tui_config.theme.bg)\n                .fg(app.tui_config.theme.text),\n        )\n        .row_highlight_style(selected_style)\n        .column_spacing(1);\n    f.render_stateful_widget(table, rect, &mut app.table_state);\n}\n\n/// Render the table header.\nfn render_table_header(theme: Theme, table_columns: &Columns) -> Row<'static> {\n    let header_cells = table_columns.columns().map(|c| {\n        Cell::from(c.typ.to_string()).style(Style::default().fg(theme.hops_table_header_text))\n    });\n    Row::new(header_cells)\n        .style(Style::default().bg(theme.hops_table_header_bg))\n        .height(1)\n        .bottom_margin(0)\n}\n\n/// Render a single row in the table of hops.\nfn render_table_row(\n    app: &TuiApp,\n    hop: &Hop,\n    dns: &DnsResolver,\n    geoip_lookup: &GeoIpLookup,\n    config: &TuiConfig,\n    custom_columns: &Columns,\n) -> Row<'static> {\n    let is_selected_hop = app.selected_hop().is_some_and(|h| h.ttl() == hop.ttl());\n    let is_in_round = app.tracer_data().is_in_round(hop, app.selected_flow);\n    let (_, row_height) = if is_selected_hop && app.show_hop_details {\n        render_hostname_with_details(app, hop, dns, geoip_lookup, config)\n    } else {\n        render_hostname(app, hop, dns, geoip_lookup)\n    };\n    let cells: Vec<Cell<'_>> = custom_columns\n        .columns()\n        .map(|column| {\n            new_cell(\n                column.typ,\n                is_selected_hop,\n                app,\n                hop,\n                dns,\n                geoip_lookup,\n                config,\n            )\n        })\n        .collect();\n    let row_color = if is_in_round {\n        config.theme.hops_table_row_active_text\n    } else {\n        config.theme.hops_table_row_inactive_text\n    };\n    Row::new(cells)\n        .height(row_height)\n        .bottom_margin(0)\n        .style(Style::default().fg(row_color))\n}\n\n///Returns a Cell matched on short char of the Column\nfn new_cell(\n    column: ColumnType,\n    is_selected_hop: bool,\n    app: &TuiApp,\n    hop: &Hop,\n    dns: &DnsResolver,\n    geoip_lookup: &GeoIpLookup,\n    config: &TuiConfig,\n) -> Cell<'static> {\n    let is_target = app.tracer_data().is_target(hop, app.selected_flow);\n    let total_recv = hop.total_recv();\n    match column {\n        ColumnType::Ttl => render_usize_cell(hop.ttl().into()),\n        ColumnType::Host => {\n            let (host_cell, _) = if is_selected_hop && app.show_hop_details {\n                render_hostname_with_details(app, hop, dns, geoip_lookup, config)\n            } else {\n                render_hostname(app, hop, dns, geoip_lookup)\n            };\n            host_cell\n        }\n        ColumnType::LossPct => render_pct_cell(hop.loss_pct()),\n        ColumnType::Sent => render_usize_cell(hop.total_sent()),\n        ColumnType::Received => render_usize_cell(hop.total_recv()),\n        ColumnType::Failed => render_usize_cell(hop.total_failed()),\n        ColumnType::Last => render_float_cell(hop.last_ms(), 1, total_recv),\n        ColumnType::Average => render_avg_cell(hop),\n        ColumnType::Best => render_float_cell(hop.best_ms(), 1, total_recv),\n        ColumnType::Worst => render_float_cell(hop.worst_ms(), 1, total_recv),\n        ColumnType::StdDev => render_stddev_cell(hop),\n        ColumnType::Status => render_status_cell(hop, is_target),\n        ColumnType::Jitter => render_float_cell(hop.jitter_ms(), 1, total_recv),\n        ColumnType::Javg => render_float_cell(Some(hop.javg_ms()), 1, total_recv),\n        ColumnType::Jmax => render_float_cell(hop.jmax_ms(), 1, total_recv),\n        ColumnType::Jinta => render_float_cell(Some(hop.jinta()), 1, total_recv),\n        ColumnType::LastSrcPort => render_port_cell(hop.last_src_port()),\n        ColumnType::LastDestPort => render_port_cell(hop.last_dest_port()),\n        ColumnType::LastSeq => render_usize_cell(usize::from(hop.last_sequence())),\n        ColumnType::LastIcmpPacketType => render_icmp_packet_type_cell(hop.last_icmp_packet_type()),\n        ColumnType::LastIcmpPacketCode => render_icmp_packet_code_cell(hop.last_icmp_packet_type()),\n        ColumnType::LastNatStatus => render_nat_cell(hop.last_nat_status()),\n        ColumnType::Floss => render_usize_cell(hop.total_forward_loss()),\n        ColumnType::Bloss => render_usize_cell(hop.total_backward_loss()),\n        ColumnType::FlossPct => render_pct_cell(hop.forward_loss_pct()),\n        ColumnType::Dscp => render_dscp_cell(hop.dscp()),\n        ColumnType::Ecn => render_ecn_cell(hop.ecn()),\n        ColumnType::Asn => render_asn_cell(hop, dns, config),\n    }\n}\n\nfn render_usize_cell(value: usize) -> Cell<'static> {\n    Cell::from(format!(\"{value}\"))\n}\n\nfn render_nat_cell(value: NatStatus) -> Cell<'static> {\n    Cell::from(match value {\n        NatStatus::NotApplicable => t!(\"na\"),\n        NatStatus::NotDetected => t!(\"no\"),\n        NatStatus::Detected => t!(\"yes\"),\n    })\n}\n\nfn render_pct_cell(value: f64) -> Cell<'static> {\n    Cell::from(format!(\"{value:.1}%\"))\n}\n\nfn render_avg_cell(hop: &Hop) -> Cell<'static> {\n    Cell::from(if hop.total_recv() > 0 {\n        format!(\"{:.1}\", hop.avg_ms())\n    } else {\n        String::default()\n    })\n}\n\nfn render_stddev_cell(hop: &Hop) -> Cell<'static> {\n    Cell::from(if hop.total_recv() > 1 {\n        format!(\"{:.1}\", hop.stddev_ms())\n    } else {\n        String::default()\n    })\n}\n\nfn render_float_cell(value: Option<f64>, places: usize, total_recv: usize) -> Cell<'static> {\n    Cell::from(if total_recv > 0 {\n        value.map(|v| format!(\"{v:.places$}\")).unwrap_or_default()\n    } else {\n        String::default()\n    })\n}\n\nfn render_status_cell(hop: &Hop, is_target: bool) -> Cell<'static> {\n    let lost = hop.total_sent() - hop.total_recv();\n    Cell::from(match (lost, is_target) {\n        (lost, target) if target && lost == hop.total_sent() => \"🔴\",\n        (lost, target) if target && lost > 0 => \"🟡\",\n        (lost, target) if !target && lost == hop.total_sent() => \"🟤\",\n        (lost, target) if !target && lost > 0 => \"🔵\",\n        _ => \"🟢\",\n    })\n}\n\nfn render_icmp_packet_type_cell(icmp_packet_type: Option<IcmpPacketType>) -> Cell<'static> {\n    match icmp_packet_type {\n        None => Cell::from(\"n/a\"),\n        Some(IcmpPacketType::TimeExceeded(_)) => Cell::from(\"TE\"),\n        Some(IcmpPacketType::EchoReply(_)) => Cell::from(\"ER\"),\n        Some(IcmpPacketType::Unreachable(_)) => Cell::from(\"DU\"),\n        Some(IcmpPacketType::NotApplicable) => Cell::from(\"NA\"),\n    }\n}\n\nfn render_icmp_packet_code_cell(icmp_packet_type: Option<IcmpPacketType>) -> Cell<'static> {\n    match icmp_packet_type {\n        Some(\n            IcmpPacketType::Unreachable(code)\n            | IcmpPacketType::TimeExceeded(code)\n            | IcmpPacketType::EchoReply(code),\n        ) => Cell::from(format!(\"{}\", code.0)),\n        _ => Cell::from(t!(\"na\")),\n    }\n}\n\nfn render_port_cell(port: u16) -> Cell<'static> {\n    if port > 0 {\n        Cell::from(format!(\"{port}\"))\n    } else {\n        Cell::from(t!(\"na\"))\n    }\n}\n\nfn render_dscp_cell(dscp: Option<Dscp>) -> Cell<'static> {\n    match dscp {\n        Some(Dscp::DF) => Cell::from(\"DF\"),\n        Some(Dscp::AF11) => Cell::from(\"AF11\"),\n        Some(Dscp::AF12) => Cell::from(\"AF12\"),\n        Some(Dscp::AF13) => Cell::from(\"AF13\"),\n        Some(Dscp::AF21) => Cell::from(\"AF21\"),\n        Some(Dscp::AF22) => Cell::from(\"AF22\"),\n        Some(Dscp::AF23) => Cell::from(\"AF23\"),\n        Some(Dscp::AF31) => Cell::from(\"AF31\"),\n        Some(Dscp::AF32) => Cell::from(\"AF32\"),\n        Some(Dscp::AF33) => Cell::from(\"AF33\"),\n        Some(Dscp::AF41) => Cell::from(\"AF41\"),\n        Some(Dscp::AF42) => Cell::from(\"AF42\"),\n        Some(Dscp::AF43) => Cell::from(\"AF43\"),\n        Some(Dscp::CS1) => Cell::from(\"CS1\"),\n        Some(Dscp::CS2) => Cell::from(\"CS2\"),\n        Some(Dscp::CS3) => Cell::from(\"CS3\"),\n        Some(Dscp::CS4) => Cell::from(\"CS4\"),\n        Some(Dscp::CS5) => Cell::from(\"CS5\"),\n        Some(Dscp::CS6) => Cell::from(\"CS6\"),\n        Some(Dscp::CS7) => Cell::from(\"CS7\"),\n        Some(Dscp::EF) => Cell::from(\"EF\"),\n        Some(Dscp::VA) => Cell::from(\"VA\"),\n        Some(Dscp::LE) => Cell::from(\"LE\"),\n        Some(Dscp::Other(other)) => Cell::from(format!(\"0x{other:02x}\")),\n        None => Cell::from(t!(\"na\")),\n    }\n}\n\nfn render_ecn_cell(ecn: Option<Ecn>) -> Cell<'static> {\n    match ecn {\n        Some(Ecn::NotECT) => Cell::from(\"NotECT\"),\n        Some(Ecn::ECT1) => Cell::from(\"ECT1\"),\n        Some(Ecn::ECT0) => Cell::from(\"ECT0\"),\n        Some(Ecn::CE) => Cell::from(\"CE\"),\n        None => Cell::from(t!(\"na\")),\n    }\n}\n\nfn render_asn_cell(hop: &Hop, dns: &DnsResolver, config: &TuiConfig) -> Cell<'static> {\n    if hop.total_recv() == 0 {\n        Cell::from(t!(\"na\"))\n    } else if config.privacy_max_ttl >= Some(hop.ttl()) {\n        Cell::from(\"****\".to_string())\n    } else if !config.lookup_as_info {\n        Cell::from(t!(\"na\"))\n    } else {\n        let (addrs, _) = visible_addresses(hop, config.max_addrs);\n        let content = addrs\n            .into_iter()\n            .map(|(addr, _)| format_asinfo_cell(*addr, dns))\n            .join(\"\\n\");\n        Cell::from(content)\n    }\n}\n\n/// Render hostname table cell (normal mode).\nfn render_hostname(\n    app: &TuiApp,\n    hop: &Hop,\n    dns: &DnsResolver,\n    geoip_lookup: &GeoIpLookup,\n) -> (Cell<'static>, u16) {\n    let (hostname, count) = if hop.total_recv() > 0 {\n        if app.tui_config.privacy_max_ttl >= Some(hop.ttl()) {\n            (format!(\"**{}**\", t!(\"hidden\")), 1)\n        } else {\n            let (addrs, count) = visible_addresses(hop, app.tui_config.max_addrs);\n            let hostnames = addrs\n                .into_iter()\n                .map(|(addr, &freq)| {\n                    format_address(addr, freq, hop, dns, geoip_lookup, &app.tui_config)\n                })\n                .join(\"\\n\");\n            (hostnames, count)\n        }\n    } else {\n        (format!(\"{}\", t!(\"no_response\")), 1)\n    };\n    (Cell::from(hostname), count)\n}\n\n/// calculate which addresses will be visible.\nfn visible_addresses(hop: &Hop, max_addrs: Option<u8>) -> (Vec<(&IpAddr, &usize)>, u16) {\n    match max_addrs {\n        None => {\n            let addrs: Vec<_> = hop.addrs_with_counts().collect();\n            let count = hop.addr_count().clamp(1, u8::MAX as usize);\n            (addrs, count as u16)\n        }\n        Some(max_addr) => {\n            let addrs: Vec<_> = hop\n                .addrs_with_counts()\n                .sorted_unstable_by_key(|&(_, cnt)| cnt)\n                .rev()\n                .take(max_addr as usize)\n                .collect();\n            let count = hop.addr_count().clamp(1, max_addr as usize);\n            (addrs, count as u16)\n        }\n    }\n}\n\n/// Perform a reverse DNS lookup for an address and format the result.\nfn format_address(\n    addr: &IpAddr,\n    freq: usize,\n    hop: &Hop,\n    dns: &DnsResolver,\n    geoip_lookup: &GeoIpLookup,\n    config: &TuiConfig,\n) -> String {\n    let addr_fmt = match config.address_mode {\n        AddressMode::Ip => addr.to_string(),\n        AddressMode::Host => {\n            if config.lookup_as_info {\n                let entry = dns.lazy_reverse_lookup_with_asinfo(*addr);\n                format_dns_entry(entry, true, config.as_mode)\n            } else {\n                let entry = dns.lazy_reverse_lookup(*addr);\n                format_dns_entry(entry, false, config.as_mode)\n            }\n        }\n        AddressMode::Both => {\n            let hostname = if config.lookup_as_info {\n                let entry = dns.lazy_reverse_lookup_with_asinfo(*addr);\n                format_dns_entry(entry, true, config.as_mode)\n            } else {\n                let entry = dns.lazy_reverse_lookup(*addr);\n                format_dns_entry(entry, false, config.as_mode)\n            };\n            format!(\"{hostname} ({addr})\")\n        }\n    };\n    let exp_fmt = format_extensions(config, hop);\n    let geo_fmt = match config.geoip_mode {\n        GeoIpMode::Off => None,\n        GeoIpMode::Short => geoip_lookup\n            .lookup(*addr)\n            .unwrap_or_default()\n            .map(|geo| geo.short_name()),\n        GeoIpMode::Long => geoip_lookup\n            .lookup(*addr)\n            .unwrap_or_default()\n            .map(|geo| geo.long_name()),\n        GeoIpMode::Location => geoip_lookup\n            .lookup(*addr)\n            .unwrap_or_default()\n            .map(|geo| geo.location()),\n    };\n    let freq_fmt = if hop.addr_count() > 1 {\n        Some(format!(\n            \"{:.1}%\",\n            (freq as f64 / hop.total_recv() as f64) * 100_f64\n        ))\n    } else {\n        None\n    };\n    let nat = match hop.last_nat_status() {\n        NatStatus::Detected => Some(\"NAT\"),\n        _ => None,\n    };\n    let mut address = addr_fmt;\n    if let Some(geo) = geo_fmt.as_deref() {\n        let _ = write!(address, \" [{geo}]\");\n    }\n    if let Some(exp) = exp_fmt {\n        let _ = write!(address, \" [{exp}]\");\n    }\n    if let Some(nat) = nat {\n        let _ = write!(address, \" [{nat}]\");\n    }\n    if let Some(freq) = freq_fmt {\n        let _ = write!(address, \" [{freq}]\");\n    }\n    address\n}\n\n/// Format a `DnsEntry` with or without autonomous system (AS) information (if available)\nfn format_dns_entry(dns_entry: DnsEntry, lookup_as_info: bool, as_mode: AsMode) -> String {\n    match dns_entry {\n        DnsEntry::Resolved(Resolved::Normal(_, hosts)) => hosts.join(\" \"),\n        DnsEntry::Resolved(Resolved::WithAsInfo(_, hosts, asinfo)) => {\n            if lookup_as_info && !asinfo.asn.is_empty() {\n                format!(\"{} {}\", format_asinfo(&asinfo, as_mode), hosts.join(\" \"))\n            } else {\n                hosts.join(\" \")\n            }\n        }\n        DnsEntry::NotFound(Unresolved::Normal(ip)) | DnsEntry::Pending(ip) => format!(\"{ip}\"),\n        DnsEntry::NotFound(Unresolved::WithAsInfo(ip, asinfo)) => {\n            if lookup_as_info && !asinfo.asn.is_empty() {\n                format!(\"{} {}\", format_asinfo(&asinfo, as_mode), ip)\n            } else {\n                format!(\"{ip}\")\n            }\n        }\n        DnsEntry::Failed(ip) => format!(\"{}: {ip}\", t!(\"dns_failed\")),\n        DnsEntry::Timeout(ip) => format!(\"{}: {ip}\", t!(\"dns_timeout\")),\n    }\n}\n\n/// Format `AsInfo` based on the `ASDisplayMode`.\nfn format_asinfo(asinfo: &AsInfo, as_mode: AsMode) -> String {\n    match as_mode {\n        AsMode::Asn => format!(\"AS{}\", asinfo.asn),\n        AsMode::Prefix => format!(\"AS{} [{}]\", asinfo.asn, asinfo.prefix),\n        AsMode::CountryCode => format!(\"AS{} [{}]\", asinfo.asn, asinfo.cc),\n        AsMode::Registry => format!(\"AS{} [{}]\", asinfo.asn, asinfo.registry),\n        AsMode::Allocated => format!(\"AS{} [{}]\", asinfo.asn, asinfo.allocated),\n        AsMode::Name => format!(\"AS{} [{}]\", asinfo.asn, asinfo.name),\n    }\n}\n\nfn format_asinfo_cell(addr: IpAddr, dns: &DnsResolver) -> String {\n    match dns.lazy_reverse_lookup_with_asinfo(addr) {\n        DnsEntry::Resolved(Resolved::WithAsInfo(_, _, asinfo))\n        | DnsEntry::NotFound(Unresolved::WithAsInfo(_, asinfo))\n            if !asinfo.asn.is_empty() =>\n        {\n            format_asinfo(&asinfo, AsMode::Asn)\n        }\n        DnsEntry::Pending(_) => String::new(),\n        DnsEntry::Failed(_) | DnsEntry::Timeout(_) => \"?????\".to_string(),\n        _ => t!(\"na\").to_string(),\n    }\n}\n\n/// Format `icmp` extensions.\nfn format_extensions(config: &TuiConfig, hop: &Hop) -> Option<String> {\n    if let Some(extensions) = hop.extensions() {\n        match config.icmp_extension_mode {\n            IcmpExtensionMode::Off => None,\n            IcmpExtensionMode::Mpls => format_extensions_mpls(extensions),\n            IcmpExtensionMode::Full => format_extensions_full(extensions),\n            IcmpExtensionMode::All => Some(format_extensions_all(extensions)),\n        }\n    } else {\n        None\n    }\n}\n\n/// Format MPLS extensions as: `labels: 12345, 6789`.\n///\n/// If not MPLS extensions are present then None is returned.\nfn format_extensions_mpls(extensions: &Extensions) -> Option<String> {\n    let labels = extensions\n        .extensions\n        .iter()\n        .filter_map(|ext| match ext {\n            Extension::Unknown(_) => None,\n            Extension::Mpls(stack) => Some(stack),\n        })\n        .flat_map(|ext| &ext.members)\n        .map(|mem| mem.label)\n        .format(\", \")\n        .to_string();\n    if labels.is_empty() {\n        None\n    } else {\n        Some(format!(\"{}: {labels}\", t!(\"labels\")))\n    }\n}\n\n/// Format all known extensions with full details.\n///\n/// For MPLS: `mpls(label=48320, ttl=1, exp=0, bos=1), mpls(...)`\nfn format_extensions_full(extensions: &Extensions) -> Option<String> {\n    let formatted = extensions\n        .extensions\n        .iter()\n        .filter_map(|ext| match ext {\n            Extension::Unknown(_) => None,\n            Extension::Mpls(stack) => Some(stack),\n        })\n        .flat_map(|ext| &ext.members)\n        .map(format_ext_mpls_stack_member)\n        .format(\", \")\n        .to_string();\n    if formatted.is_empty() {\n        None\n    } else {\n        Some(formatted)\n    }\n}\n\n/// Format a list all known and unknown extensions with full details.\n///\n/// `mpls(label=48320, ttl=1, exp=0, bos=1), unknown(class=1, sub=1, object=0b c8 c1 01), ...`\nfn format_extensions_all(extensions: &Extensions) -> String {\n    extensions\n        .extensions\n        .iter()\n        .flat_map(|ext| match ext {\n            Extension::Unknown(unknown) => vec![format_ext_unknown(unknown)],\n            Extension::Mpls(stack) => stack\n                .members\n                .iter()\n                .map(format_ext_mpls_stack_member)\n                .collect::<Vec<_>>(),\n        })\n        .format(\", \")\n        .to_string()\n}\n\n/// Format a MPLS `icmp` extension object.\npub fn format_ext_mpls_stack_member(member: &MplsLabelStackMember) -> String {\n    format!(\n        \"mpls(label={}, ttl={}, exp={}, bos={})\",\n        member.label, member.ttl, member.exp, member.bos\n    )\n}\n\n/// Format an unknown `icmp` extension object.\npub fn format_ext_unknown(unknown: &UnknownExtension) -> String {\n    format!(\n        \"unknown(class={}, subtype={}, object={:02x})\",\n        unknown.class_num,\n        unknown.class_subtype,\n        unknown.bytes.iter().format(\" \")\n    )\n}\n\n/// Render hostname table cell (detailed mode).\nfn render_hostname_with_details(\n    app: &TuiApp,\n    hop: &Hop,\n    dns: &DnsResolver,\n    geoip_lookup: &GeoIpLookup,\n    config: &TuiConfig,\n) -> (Cell<'static>, u16) {\n    let rendered = if hop.total_recv() > 0 {\n        if config.privacy_max_ttl >= Some(hop.ttl()) {\n            format!(\"**{}**\", t!(\"hidden\"))\n        } else {\n            let index = app.selected_hop_address;\n            format_details(hop, index, dns, geoip_lookup, config)\n        }\n    } else {\n        format!(\"{}\", t!(\"no_response\"))\n    };\n    (Cell::from(rendered), 7)\n}\n\n/// Format hop details.\nfn format_details(\n    hop: &Hop,\n    offset: usize,\n    dns: &DnsResolver,\n    geoip_lookup: &GeoIpLookup,\n    config: &TuiConfig,\n) -> String {\n    let Some(addr) = hop.addrs().nth(offset) else {\n        return format!(\"Error: no addr for index {offset}\");\n    };\n    let count = hop.addr_count();\n    let index = offset + 1;\n    let geoip = geoip_lookup.lookup(*addr).unwrap_or_default();\n    let dns_entry = if config.lookup_as_info {\n        dns.lazy_reverse_lookup_with_asinfo(*addr)\n    } else {\n        dns.lazy_reverse_lookup(*addr)\n    };\n    let ext = hop.extensions();\n    let nat = hop.last_nat_status();\n    match dns_entry {\n        DnsEntry::Pending(addr) => {\n            fmt_details_line(addr, index, count, None, None, geoip, ext, nat, config)\n        }\n        DnsEntry::Resolved(Resolved::WithAsInfo(addr, hosts, asinfo)) => fmt_details_line(\n            addr,\n            index,\n            count,\n            Some(hosts),\n            Some(asinfo),\n            geoip,\n            ext,\n            nat,\n            config,\n        ),\n        DnsEntry::NotFound(Unresolved::WithAsInfo(addr, asinfo)) => fmt_details_line(\n            addr,\n            index,\n            count,\n            Some(vec![]),\n            Some(asinfo),\n            geoip,\n            ext,\n            nat,\n            config,\n        ),\n        DnsEntry::Resolved(Resolved::Normal(addr, hosts)) => fmt_details_line(\n            addr,\n            index,\n            count,\n            Some(hosts),\n            None,\n            geoip,\n            ext,\n            nat,\n            config,\n        ),\n        DnsEntry::NotFound(Unresolved::Normal(addr)) => fmt_details_line(\n            addr,\n            index,\n            count,\n            Some(vec![]),\n            None,\n            geoip,\n            ext,\n            nat,\n            config,\n        ),\n        DnsEntry::Failed(ip) => {\n            format!(\"{}: {ip}\", t!(\"dns_failed\"))\n        }\n        DnsEntry::Timeout(ip) => {\n            format!(\"{}: {ip}\", t!(\"dns_timeout\"))\n        }\n    }\n}\n\n/// Format hostname detail lines.\n///\n/// Format as follows:\n///\n/// ```text\n/// 172.217.24.78 [1 of 2]\n/// Host: hkg07s50-in-f14.1e100.net\n/// AS Name: AS15169 GOOGLE, US\n/// AS Info: 142.250.0.0/15 arin 2012-05-24\n/// Geo: United States, North America\n/// Pos: 37.751, -97.822 (~1000km)\n/// Ext: [mpls(label=48268, ttl=1, exp=0, bos=1)]\n/// ```\n#[expect(clippy::too_many_arguments)]\nfn fmt_details_line(\n    addr: IpAddr,\n    index: usize,\n    count: usize,\n    hostnames: Option<Vec<String>>,\n    asinfo: Option<AsInfo>,\n    geoip: Option<Rc<GeoIpCity>>,\n    extensions: Option<&Extensions>,\n    nat: NatStatus,\n    config: &TuiConfig,\n) -> String {\n    let as_fmt = match (config.lookup_as_info, asinfo) {\n        (false, _) => format!(\n            \"AS {}: <{}>\\nAS {}: <{}>\",\n            t!(\"name\"),\n            t!(\"not_enabled\"),\n            t!(\"info\"),\n            t!(\"not_enabled\")\n        ),\n        (true, None) => format!(\n            \"AS {}: <{}>\\nAS {}: <{}>\",\n            t!(\"name\"),\n            t!(\"info\"),\n            t!(\"awaited\"),\n            t!(\"awaited\")\n        ),\n        (true, Some(info)) if info.asn.is_empty() => {\n            format!(\n                \"AS {}: <{}>\\nAS {}: <{}>\",\n                t!(\"name\"),\n                t!(\"not_found\"),\n                t!(\"info\"),\n                t!(\"not_found\")\n            )\n        }\n        (true, Some(info)) => format!(\n            \"AS {}: AS{} {}\\nAS {}: {} {} {}\",\n            t!(\"name\"),\n            info.asn,\n            info.name,\n            t!(\"info\"),\n            info.prefix,\n            info.registry,\n            info.allocated\n        ),\n    };\n    let hosts_rendered = if let Some(hosts) = hostnames {\n        if hosts.is_empty() {\n            format!(\"{}: <{}>\", t!(\"host\"), t!(\"not_found\"))\n        } else {\n            format!(\"{}: {}\", t!(\"host\"), hosts.join(\" \"))\n        }\n    } else {\n        format!(\"{}: <{}>\", t!(\"host\"), t!(\"awaited\"))\n    };\n    let geoip_fmt = if let Some(geo) = geoip {\n        let (lat, long, radius) = geo.coordinates().unwrap_or_default();\n        format!(\n            \"{}: {}\\n{}: {}, {} (~{}{})\",\n            t!(\"geo\"),\n            geo.long_name(),\n            t!(\"pos\"),\n            lat,\n            long,\n            radius,\n            t!(\"kilometer\"),\n        )\n    } else {\n        format!(\n            \"{}: <{}>\\n{}: <{}>\",\n            t!(\"geo\"),\n            t!(\"not_found\"),\n            t!(\"pos\"),\n            t!(\"not_found\")\n        )\n    };\n    let ext_fmt = if let Some(extensions) = extensions {\n        format!(\"{}: [{}]\", t!(\"ext\"), format_extensions_all(extensions))\n    } else {\n        format!(\"{}: <{}>\", t!(\"ext\"), t!(\"none\"))\n    };\n    let nat_fmt = match nat {\n        NatStatus::Detected => \" [NAT]\",\n        _ => \"\",\n    };\n    format!(\n        \"{addr}{nat_fmt} [{index} of {count}]\\n{hosts_rendered}\\n{as_fmt}\\n{geoip_fmt}\\n{ext_fmt}\"\n    )\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/tabs.rs",
    "content": "use crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse ratatui::Frame;\nuse ratatui::layout::{Alignment, Rect};\nuse ratatui::style::{Modifier, Style};\nuse ratatui::text::{Line, Span};\nuse ratatui::widgets::{Block, BorderType, Borders, Tabs};\n\n/// Render the tabs, one per trace.\npub fn render(f: &mut Frame<'_>, rect: Rect, app: &TuiApp) {\n    let tabs_block = Block::default()\n        .title(Line::raw(t!(\"title_traces\")))\n        .title_alignment(Alignment::Left)\n        .borders(Borders::ALL)\n        .border_type(BorderType::Rounded)\n        .border_style(Style::default().fg(app.tui_config.theme.border))\n        .style(\n            Style::default()\n                .bg(app.tui_config.theme.bg)\n                .fg(app.tui_config.theme.text),\n        );\n    let titles: Vec<_> = app\n        .trace_info\n        .iter()\n        .map(|trace| {\n            Line::from(Span::styled(\n                &trace.target_hostname,\n                Style::default().fg(app.tui_config.theme.tab_text),\n            ))\n        })\n        .collect();\n    let tabs = Tabs::new(titles)\n        .block(tabs_block)\n        .select(app.trace_selected)\n        .style(Style::default())\n        .highlight_style(Style::default().add_modifier(Modifier::BOLD));\n    f.render_widget(tabs, rect);\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/util.rs",
    "content": "use ratatui::layout::{Constraint, Direction, Layout, Rect};\n\npub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {\n    let popup_layout = Layout::default()\n        .direction(Direction::Vertical)\n        .constraints(\n            [\n                Constraint::Percentage((100 - percent_y) / 2),\n                Constraint::Percentage(percent_y),\n                Constraint::Percentage((100 - percent_y) / 2),\n            ]\n            .as_ref(),\n        )\n        .split(r);\n\n    Layout::default()\n        .direction(Direction::Horizontal)\n        .constraints(\n            [\n                Constraint::Percentage((100 - percent_x) / 2),\n                Constraint::Percentage(percent_x),\n                Constraint::Percentage((100 - percent_x) / 2),\n            ]\n            .as_ref(),\n        )\n        .split(popup_layout[1])[1]\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render/world.rs",
    "content": "use crate::frontend::tui_app::TuiApp;\nuse crate::t;\nuse itertools::Itertools;\nuse ratatui::Frame;\nuse ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin, Rect};\nuse ratatui::prelude::Line;\nuse ratatui::style::{Color, Style};\nuse ratatui::symbols::Marker;\nuse ratatui::text::Span;\nuse ratatui::widgets::canvas::{Canvas, Circle, Context, Map, MapResolution, Rectangle};\nuse ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph};\nuse std::collections::HashMap;\nuse trippy_core::Hop;\n\n/// Render the `GeoIp` map.\npub fn render(f: &mut Frame<'_>, app: &TuiApp, rect: Rect) {\n    let entries = build_map_entries(app);\n    let chunks = Layout::default()\n        .direction(Direction::Vertical)\n        .constraints(MAP_LAYOUT)\n        .split(rect);\n    let info_rect = chunks[1].inner(Margin {\n        vertical: 0,\n        horizontal: 16,\n    });\n    render_map_canvas(f, app, rect, &entries);\n    render_map_info_panel(f, app, info_rect, &entries);\n}\n\n/// Render the map canvas.\nfn render_map_canvas(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: &[MapEntry]) {\n    let theme = app.tui_config.theme;\n    let map = Canvas::default()\n        .background_color(app.tui_config.theme.bg)\n        .block(\n            Block::default()\n                .title(Line::raw(t!(\"title_map\")))\n                .borders(Borders::ALL)\n                .border_style(Style::default().fg(app.tui_config.theme.border))\n                .style(\n                    Style::default()\n                        .bg(app.tui_config.theme.bg)\n                        .fg(app.tui_config.theme.text),\n                ),\n        )\n        .paint(|ctx| {\n            render_map_canvas_world(ctx, theme.map_world);\n            ctx.layer();\n            for entry in entries {\n                let any_show = entry\n                    .hops\n                    .iter()\n                    .any(|hop| Some(*hop) > app.tui_config.privacy_max_ttl);\n                if any_show {\n                    render_map_canvas_pin(ctx, entry);\n                    render_map_canvas_radius(ctx, entry, theme.map_radius);\n                    render_map_canvas_selected(\n                        ctx,\n                        entry,\n                        app.selected_hop_or_target(),\n                        theme.map_selected,\n                    );\n                }\n            }\n        })\n        .marker(Marker::Braille)\n        .x_bounds([-180.0, 180.0])\n        .y_bounds([-90.0, 90.0]);\n    f.render_widget(Clear, rect);\n    f.render_widget(map, rect);\n}\n\n/// Render the map canvas world.\nfn render_map_canvas_world(ctx: &mut Context<'_>, color: Color) {\n    ctx.draw(&Map {\n        color,\n        resolution: MapResolution::High,\n    });\n}\n\n/// Render the map canvas pin.\nfn render_map_canvas_pin(ctx: &mut Context<'_>, entry: &MapEntry) {\n    let MapEntry {\n        latitude,\n        longitude,\n        ..\n    } = entry;\n    ctx.print(*longitude, *latitude, Span::styled(\"📍\", Style::default()));\n}\n\n/// Render the map canvas accuracy radius circle.\nfn render_map_canvas_radius(ctx: &mut Context<'_>, entry: &MapEntry, color: Color) {\n    let MapEntry {\n        latitude,\n        longitude,\n        radius,\n        ..\n    } = entry;\n    let radius_degrees = f64::from(*radius) / 110_f64;\n    if radius_degrees > 2_f64 {\n        let circle_widget = Circle {\n            x: *longitude,\n            y: *latitude,\n            radius: radius_degrees,\n            color,\n        };\n        ctx.draw(&circle_widget);\n    }\n}\n\n/// Render the map canvas selected item box.\nfn render_map_canvas_selected(\n    ctx: &mut Context<'_>,\n    entry: &MapEntry,\n    selected_hop: &Hop,\n    color: Color,\n) {\n    let MapEntry {\n        latitude,\n        longitude,\n        hops,\n        ..\n    } = entry;\n    if hops.contains(&selected_hop.ttl()) {\n        ctx.draw(&Rectangle {\n            x: longitude - 5.0_f64,\n            y: latitude - 5.0_f64,\n            width: 10.0_f64,\n            height: 10.0_f64,\n            color,\n        });\n    }\n}\n\n/// Render the map info panel.\nfn render_map_info_panel(f: &mut Frame<'_>, app: &TuiApp, rect: Rect, entries: &[MapEntry]) {\n    let theme = app.tui_config.theme;\n    let selected_hop = app.selected_hop_or_target();\n    let locations = entries\n        .iter()\n        .filter_map(|entry| {\n            if entry.hops.contains(&selected_hop.ttl()) {\n                Some(format!(\"{} [{}]\", entry.long_name, entry.location))\n            } else {\n                None\n            }\n        })\n        .collect::<Vec<_>>();\n    let info = if app.tui_config.privacy_max_ttl >= Some(selected_hop.ttl()) {\n        format!(\"**{}**\", t!(\"hidden\"))\n    } else {\n        match locations.as_slice() {\n            _ if app.tui_config.geoip_mmdb_file.is_none() => t!(\"geoip_not_enabled\").to_string(),\n            [] if selected_hop.addr_count() > 0 => format!(\n                \"{} {} ({})\",\n                t!(\"geoip_no_data_for_hop\"),\n                selected_hop.ttl(),\n                selected_hop.addrs().join(\", \")\n            ),\n            [] => format!(\"{} {}\", t!(\"geoip_no_data_for_hop\"), selected_hop.ttl()),\n            [loc] => loc.clone(),\n            _ => format!(\n                \"{} {}\",\n                t!(\"geoip_multiple_data_for_hop\"),\n                selected_hop.ttl()\n            ),\n        }\n    };\n    let info_panel = Paragraph::new(info)\n        .block(\n            Block::default()\n                .title(format!(\"{} {}\", t!(\"hop\"), selected_hop.ttl()))\n                .borders(Borders::ALL)\n                .border_type(BorderType::Rounded)\n                .border_style(Style::default().fg(theme.map_info_panel_border))\n                .style(\n                    Style::default()\n                        .bg(theme.map_info_panel_bg)\n                        .fg(theme.map_info_panel_text),\n                ),\n        )\n        .alignment(Alignment::Left);\n    f.render_widget(Clear, rect);\n    f.render_widget(info_panel, rect);\n}\n\n/// An entry to render on the map.\nstruct MapEntry {\n    long_name: String,\n    location: String,\n    latitude: f64,\n    longitude: f64,\n    radius: u16,\n    hops: Vec<u8>,\n}\n\n/// Build a vec of `MapEntry` for all hops.\n///\n/// Each entry represent a single `GeoIp` location, which may be associated with multiple hops.\nfn build_map_entries(app: &TuiApp) -> Vec<MapEntry> {\n    let mut geo_map: HashMap<String, MapEntry> = HashMap::new();\n    for hop in app.tracer_data().hops_for_flow(app.selected_flow) {\n        for addr in hop.addrs() {\n            if let Some(geo) = app.geoip_lookup.lookup(*addr).unwrap_or_default() {\n                if let Some((latitude, longitude, radius)) = geo.coordinates() {\n                    let entry = geo_map.entry(geo.long_name()).or_insert_with(|| MapEntry {\n                        long_name: geo.long_name(),\n                        location: format!(\"{latitude}, {longitude} ~{radius}{}\", t!(\"kilometer\")),\n                        latitude,\n                        longitude,\n                        radius,\n                        hops: vec![],\n                    });\n                    entry.hops.push(hop.ttl());\n                }\n            }\n        }\n    }\n    geo_map.into_values().collect_vec()\n}\n\nconst MAP_LAYOUT: [Constraint; 3] = [\n    Constraint::Min(1),\n    Constraint::Length(3),\n    Constraint::Length(1),\n];\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/render.rs",
    "content": "pub mod app;\npub mod bar;\npub mod body;\npub mod bsod;\npub mod chart;\npub mod flows;\npub mod footer;\npub mod header;\npub mod help;\npub mod histogram;\npub mod history;\npub mod settings;\npub mod splash;\npub mod table;\npub mod tabs;\npub mod util;\npub mod world;\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/theme.rs",
    "content": "use crate::config::{TuiColor, TuiTheme};\nuse ratatui::style::Color;\n\n/// Tui color theme.\n#[derive(Debug, Clone, Copy)]\npub struct Theme {\n    /// The default background color.\n    ///\n    /// This may be overridden for specific components.\n    pub bg: Color,\n    /// The default color of borders.\n    ///\n    /// This may be overridden for specific components.\n    pub border: Color,\n    /// The default color of text.\n    ///\n    /// This may be overridden for specific components.\n    pub text: Color,\n    /// The color of the text in traces tabs.\n    pub tab_text: Color,\n    /// The background color of the hops table header.\n    pub hops_table_header_bg: Color,\n    /// The color of text in the hops table header.\n    pub hops_table_header_text: Color,\n    /// The color of text of active rows in the hops table.\n    pub hops_table_row_active_text: Color,\n    /// The color of text of inactive rows in the hops table.\n    pub hops_table_row_inactive_text: Color,\n    /// The color of the selected series in the hops chart.\n    pub hops_chart_selected: Color,\n    /// The color of the unselected series in the hops chart.\n    pub hops_chart_unselected: Color,\n    /// The color of the axis in the hops chart.\n    pub hops_chart_axis: Color,\n    /// The color of bars in the frequency chart.\n    pub frequency_chart_bar: Color,\n    /// The color of text in the bars of the frequency chart.\n    pub frequency_chart_text: Color,\n    /// The color of the selected flow bar in the flows chart.\n    pub flows_chart_bar_selected: Color,\n    /// The color of the unselected flow bar in the flows chart.\n    pub flows_chart_bar_unselected: Color,\n    /// The color of the current flow text in the flows chart.\n    pub flows_chart_text_current: Color,\n    /// The color of the non-current flow text in the flows chart.\n    pub flows_chart_text_non_current: Color,\n    /// The color of the samples chart.\n    pub samples_chart: Color,\n    /// The color of the samples chart for lost probes.\n    pub samples_chart_lost: Color,\n    /// The background color of the help dialog.\n    pub help_dialog_bg: Color,\n    /// The color of the text in the help dialog.\n    pub help_dialog_text: Color,\n    /// The background color of the settings dialog.\n    pub settings_dialog_bg: Color,\n    /// The color of the text in settings dialog tabs.\n    pub settings_tab_text: Color,\n    /// The color of text in the settings table header.\n    pub settings_table_header_text: Color,\n    /// The background color of the settings table header.\n    pub settings_table_header_bg: Color,\n    /// The color of text of rows in the settings table.\n    pub settings_table_row_text: Color,\n    /// The color of the map world diagram.\n    pub map_world: Color,\n    /// The color of the map accuracy radius circle.\n    pub map_radius: Color,\n    /// The color of the map selected item box.\n    pub map_selected: Color,\n    /// The color of border of the map info panel.\n    pub map_info_panel_border: Color,\n    /// The background color of the map info panel.\n    pub map_info_panel_bg: Color,\n    /// The color of text in the map info panel.\n    pub map_info_panel_text: Color,\n    /// The color of the info bar background.\n    pub info_bar_bg: Color,\n    /// The color of the info bar text.\n    pub info_bar_text: Color,\n}\n\nimpl From<TuiTheme> for Theme {\n    fn from(value: TuiTheme) -> Self {\n        Self {\n            bg: Color::from(value.bg),\n            border: Color::from(value.border),\n            text: Color::from(value.text),\n            tab_text: Color::from(value.tab_text),\n            hops_table_header_bg: Color::from(value.hops_table_header_bg),\n            hops_table_header_text: Color::from(value.hops_table_header_text),\n            hops_table_row_active_text: Color::from(value.hops_table_row_active_text),\n            hops_table_row_inactive_text: Color::from(value.hops_table_row_inactive_text),\n            hops_chart_selected: Color::from(value.hops_chart_selected),\n            hops_chart_unselected: Color::from(value.hops_chart_unselected),\n            hops_chart_axis: Color::from(value.hops_chart_axis),\n            frequency_chart_bar: Color::from(value.frequency_chart_bar),\n            frequency_chart_text: Color::from(value.frequency_chart_text),\n            flows_chart_bar_selected: Color::from(value.flows_chart_bar_selected),\n            flows_chart_bar_unselected: Color::from(value.flows_chart_bar_unselected),\n            flows_chart_text_current: Color::from(value.flows_chart_text_current),\n            flows_chart_text_non_current: Color::from(value.flows_chart_text_non_current),\n            samples_chart: Color::from(value.samples_chart),\n            samples_chart_lost: Color::from(value.samples_chart_lost),\n            help_dialog_bg: Color::from(value.help_dialog_bg),\n            help_dialog_text: Color::from(value.help_dialog_text),\n            settings_dialog_bg: Color::from(value.settings_dialog_bg),\n            settings_tab_text: Color::from(value.settings_tab_text),\n            settings_table_header_text: Color::from(value.settings_table_header_text),\n            settings_table_header_bg: Color::from(value.settings_table_header_bg),\n            settings_table_row_text: Color::from(value.settings_table_row_text),\n            map_world: Color::from(value.map_world),\n            map_radius: Color::from(value.map_radius),\n            map_selected: Color::from(value.map_selected),\n            map_info_panel_border: Color::from(value.map_info_panel_border),\n            map_info_panel_bg: Color::from(value.map_info_panel_bg),\n            map_info_panel_text: Color::from(value.map_info_panel_text),\n            info_bar_bg: Color::from(value.info_bar_bg),\n            info_bar_text: Color::from(value.info_bar_text),\n        }\n    }\n}\n\nimpl From<TuiColor> for Color {\n    #[expect(clippy::too_many_lines)]\n    fn from(value: TuiColor) -> Self {\n        match value {\n            TuiColor::Black => Self::Black,\n            TuiColor::Red => Self::Red,\n            TuiColor::Green => Self::Green,\n            TuiColor::Yellow => Self::Yellow,\n            TuiColor::Blue => Self::Blue,\n            TuiColor::Magenta => Self::Magenta,\n            TuiColor::Cyan => Self::Cyan,\n            TuiColor::Gray => Self::Gray,\n            TuiColor::DarkGray => Self::DarkGray,\n            TuiColor::LightRed => Self::LightRed,\n            TuiColor::LightGreen => Self::LightGreen,\n            TuiColor::LightYellow => Self::LightYellow,\n            TuiColor::LightBlue => Self::LightBlue,\n            TuiColor::LightMagenta => Self::LightMagenta,\n            TuiColor::LightCyan => Self::LightCyan,\n            TuiColor::White => Self::White,\n            TuiColor::AliceBlue => Self::from_u32(0x00f0_f8ff),\n            TuiColor::AntiqueWhite => Self::from_u32(0x00fa_ebd7),\n            TuiColor::Aqua => Self::from_u32(0x0000_ffff),\n            TuiColor::Aquamarine => Self::from_u32(0x007f_ffd4),\n            TuiColor::Azure => Self::from_u32(0x00f0_ffff),\n            TuiColor::Beige => Self::from_u32(0x00f5_f5dc),\n            TuiColor::Bisque => Self::from_u32(0x00ff_e4c4),\n            TuiColor::BlanchedAlmond => Self::from_u32(0x00ff_ebcd),\n            TuiColor::BlueViolet => Self::from_u32(0x008a_2be2),\n            TuiColor::Brown => Self::from_u32(0x00a5_2a2a),\n            TuiColor::BurlyWood => Self::from_u32(0x00de_b887),\n            TuiColor::CadetBlue => Self::from_u32(0x005f_9ea0),\n            TuiColor::Chartreuse => Self::from_u32(0x007f_ff00),\n            TuiColor::Chocolate => Self::from_u32(0x00d2_691e),\n            TuiColor::Coral => Self::from_u32(0x00ff_7f50),\n            TuiColor::CornflowerBlue => Self::from_u32(0x0064_95ed),\n            TuiColor::CornSilk => Self::from_u32(0x00ff_f8dc),\n            TuiColor::Crimson => Self::from_u32(0x00dc_143c),\n            TuiColor::DarkBlue => Self::from_u32(0x0000_008b),\n            TuiColor::DarkCyan => Self::from_u32(0x0000_8b8b),\n            TuiColor::DarkGoldenrod => Self::from_u32(0x00b8_860b),\n            TuiColor::DarkGreen => Self::from_u32(0x0000_6400),\n            TuiColor::DarkKhaki => Self::from_u32(0x00bd_b76b),\n            TuiColor::DarkMagenta => Self::from_u32(0x008b_008b),\n            TuiColor::DarkOliveGreen => Self::from_u32(0x0055_6b2f),\n            TuiColor::DarkOrange => Self::from_u32(0x00ff_8c00),\n            TuiColor::DarkOrchid => Self::from_u32(0x0099_32cc),\n            TuiColor::DarkRed => Self::from_u32(0x008b_0000),\n            TuiColor::DarkSalmon => Self::from_u32(0x00e9_967a),\n            TuiColor::DarkSeaGreen => Self::from_u32(0x008f_bc8f),\n            TuiColor::DarkSlateBlue => Self::from_u32(0x0048_3d8b),\n            TuiColor::DarkSlateGray => Self::from_u32(0x002f_4f4f),\n            TuiColor::DarkTurquoise => Self::from_u32(0x0000_ced1),\n            TuiColor::DarkViolet => Self::from_u32(0x0094_00d3),\n            TuiColor::DeepPink => Self::from_u32(0x00ff_1493),\n            TuiColor::DeepSkyBlue => Self::from_u32(0x0000_bfff),\n            TuiColor::DimGray => Self::from_u32(0x0069_6969),\n            TuiColor::DodgerBlue => Self::from_u32(0x001e_90ff),\n            TuiColor::Firebrick => Self::from_u32(0x00b2_2222),\n            TuiColor::FloralWhite => Self::from_u32(0x00ff_faf0),\n            TuiColor::ForestGreen => Self::from_u32(0x0022_8b22),\n            TuiColor::Fuchsia => Self::from_u32(0x00ff_00ff),\n            TuiColor::Gainsboro => Self::from_u32(0x00dc_dcdc),\n            TuiColor::GhostWhite => Self::from_u32(0x00f8_f8ff),\n            TuiColor::Gold => Self::from_u32(0x00ff_d700),\n            TuiColor::Goldenrod => Self::from_u32(0x00da_a520),\n            TuiColor::GreenYellow => Self::from_u32(0x00ad_ff2f),\n            TuiColor::Honeydew => Self::from_u32(0x00f0_fff0),\n            TuiColor::HotPink => Self::from_u32(0x00ff_69b4),\n            TuiColor::IndianRed => Self::from_u32(0x00cd_5c5c),\n            TuiColor::Indigo => Self::from_u32(0x004b_0082),\n            TuiColor::Ivory => Self::from_u32(0x00ff_fff0),\n            TuiColor::Khaki => Self::from_u32(0x00f0_e68c),\n            TuiColor::Lavender => Self::from_u32(0x00e6_e6fa),\n            TuiColor::LavenderBlush => Self::from_u32(0x00ff_f0f5),\n            TuiColor::LawnGreen => Self::from_u32(0x007c_fc00),\n            TuiColor::LemonChiffon => Self::from_u32(0x00ff_facd),\n            TuiColor::LightCoral => Self::from_u32(0x00f0_8080),\n            TuiColor::LightGoldenrodYellow => Self::from_u32(0x00fa_fad2),\n            TuiColor::LightGray => Self::from_u32(0x00d3_d3d3),\n            TuiColor::LightPink => Self::from_u32(0x00ff_b6c1),\n            TuiColor::LightSalmon => Self::from_u32(0x00ff_a07a),\n            TuiColor::LightSeaGreen => Self::from_u32(0x0020_b2aa),\n            TuiColor::LightSkyBlue => Self::from_u32(0x0087_cefa),\n            TuiColor::LightSlateGray => Self::from_u32(0x0077_8899),\n            TuiColor::LightSteelBlue => Self::from_u32(0x00b0_c4de),\n            TuiColor::Lime => Self::from_u32(0x0000_ff00),\n            TuiColor::LimeGreen => Self::from_u32(0x0032_cd32),\n            TuiColor::Linen => Self::from_u32(0x00fa_f0e6),\n            TuiColor::Maroon => Self::from_u32(0x0080_0000),\n            TuiColor::MediumAquamarine => Self::from_u32(0x0066_cdaa),\n            TuiColor::MediumBlue => Self::from_u32(0x0000_00cd),\n            TuiColor::MediumOrchid => Self::from_u32(0x00ba_55d3),\n            TuiColor::MediumPurple => Self::from_u32(0x0093_70db),\n            TuiColor::MediumSeaGreen => Self::from_u32(0x003c_b371),\n            TuiColor::MediumSlateBlue => Self::from_u32(0x007b_68ee),\n            TuiColor::MediumSpringGreen => Self::from_u32(0x0000_fa9a),\n            TuiColor::MediumTurquoise => Self::from_u32(0x0048_d1cc),\n            TuiColor::MediumVioletRed => Self::from_u32(0x00c7_1585),\n            TuiColor::MidnightBlue => Self::from_u32(0x0019_1970),\n            TuiColor::MintCream => Self::from_u32(0x00f5_fffa),\n            TuiColor::MistyRose => Self::from_u32(0x00ff_e4e1),\n            TuiColor::Moccasin => Self::from_u32(0x00ff_e4b5),\n            TuiColor::NavajoWhite => Self::from_u32(0x00ff_dead),\n            TuiColor::Navy => Self::from_u32(0x0000_0080),\n            TuiColor::OldLace => Self::from_u32(0x00fd_f5e6),\n            TuiColor::Olive => Self::from_u32(0x0080_8000),\n            TuiColor::OliveDrab => Self::from_u32(0x006b_8e23),\n            TuiColor::Orange => Self::from_u32(0x00ff_a500),\n            TuiColor::OrangeRed => Self::from_u32(0x00ff_4500),\n            TuiColor::Orchid => Self::from_u32(0x00da_70d6),\n            TuiColor::PaleGoldenrod => Self::from_u32(0x00ee_e8aa),\n            TuiColor::PaleGreen => Self::from_u32(0x0098_fb98),\n            TuiColor::PaleTurquoise => Self::from_u32(0x00af_eeee),\n            TuiColor::PaleVioletRed => Self::from_u32(0x00db_7093),\n            TuiColor::PapayaWhip => Self::from_u32(0x00ff_efd5),\n            TuiColor::PeachPuff => Self::from_u32(0x00ff_dab9),\n            TuiColor::Peru => Self::from_u32(0x00cd_853f),\n            TuiColor::Pink => Self::from_u32(0x00ff_c0cb),\n            TuiColor::Plum => Self::from_u32(0x00dd_a0dd),\n            TuiColor::PowderBlue => Self::from_u32(0x00b0_e0e6),\n            TuiColor::Purple => Self::from_u32(0x0080_0080),\n            TuiColor::RebeccaPurple => Self::from_u32(0x0066_3399),\n            TuiColor::RosyBrown => Self::from_u32(0x00bc_8f8f),\n            TuiColor::RoyalBlue => Self::from_u32(0x0041_69e1),\n            TuiColor::SaddleBrown => Self::from_u32(0x008b_4513),\n            TuiColor::Salmon => Self::from_u32(0x00fa_8072),\n            TuiColor::SandyBrown => Self::from_u32(0x00f4_a460),\n            TuiColor::SeaGreen => Self::from_u32(0x002e_8b57),\n            TuiColor::SeaShell => Self::from_u32(0x00ff_f5ee),\n            TuiColor::Sienna => Self::from_u32(0x00a0_522d),\n            TuiColor::Silver => Self::from_u32(0x00c0_c0c0),\n            TuiColor::SkyBlue => Self::from_u32(0x0087_ceeb),\n            TuiColor::SlateBlue => Self::from_u32(0x006a_5acd),\n            TuiColor::SlateGray => Self::from_u32(0x0070_8090),\n            TuiColor::Snow => Self::from_u32(0x00ff_fafa),\n            TuiColor::SpringGreen => Self::from_u32(0x0000_ff7f),\n            TuiColor::SteelBlue => Self::from_u32(0x0046_82b4),\n            TuiColor::Tan => Self::from_u32(0x00d2_b48c),\n            TuiColor::Teal => Self::from_u32(0x0000_8080),\n            TuiColor::Thistle => Self::from_u32(0x00d8_bfd8),\n            TuiColor::Tomato => Self::from_u32(0x00ff_6347),\n            TuiColor::Turquoise => Self::from_u32(0x0040_e0d0),\n            TuiColor::Violet => Self::from_u32(0x00ee_82ee),\n            TuiColor::Wheat => Self::from_u32(0x00f5_deb3),\n            TuiColor::WhiteSmoke => Self::from_u32(0x00f5_f5f5),\n            TuiColor::YellowGreen => Self::from_u32(0x009a_cd32),\n            TuiColor::Rgb(r, g, b) => Self::Rgb(r, g, b),\n        }\n    }\n}\n\npub fn fmt_color(color: Color) -> String {\n    match color {\n        Color::Black => \"black\".to_string(),\n        Color::Red => \"red\".to_string(),\n        Color::Green => \"green\".to_string(),\n        Color::Yellow => \"yellow\".to_string(),\n        Color::Blue => \"blue\".to_string(),\n        Color::Magenta => \"magenta\".to_string(),\n        Color::Cyan => \"cyan\".to_string(),\n        Color::Gray => \"gray\".to_string(),\n        Color::DarkGray => \"darkgray\".to_string(),\n        Color::LightRed => \"lightred\".to_string(),\n        Color::LightGreen => \"lightgreen\".to_string(),\n        Color::LightYellow => \"lightyellow\".to_string(),\n        Color::LightBlue => \"lightblue\".to_string(),\n        Color::LightMagenta => \"lightmagenta\".to_string(),\n        Color::LightCyan => \"lightcyan\".to_string(),\n        Color::White => \"white\".to_string(),\n        Color::Rgb(r, g, b) => format!(\"{r:02x}{g:02x}{b:02x}\"),\n        _ => \"unknown\".to_string(),\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend/tui_app.rs",
    "content": "use crate::app::TraceInfo;\nuse crate::frontend::config::TuiConfig;\nuse crate::frontend::render::settings::{SETTINGS_TAB_COLUMNS, settings_tabs};\nuse crate::geoip::GeoIpLookup;\nuse itertools::Itertools;\nuse ratatui::widgets::TableState;\nuse std::time::SystemTime;\nuse trippy_core::FlowId;\nuse trippy_core::Hop;\nuse trippy_core::State;\nuse trippy_dns::{DnsResolver, ResolveMethod};\n\npub struct TuiApp {\n    pub selected_tracer_data: State,\n    pub trace_info: Vec<TraceInfo>,\n    pub tui_config: TuiConfig,\n    /// The state of the hop table.\n    pub table_state: TableState,\n    /// The state of the settings table.\n    pub setting_table_state: TableState,\n    /// The selected trace.\n    pub trace_selected: usize,\n    /// The selected tab in the settings dialog.\n    pub settings_tab_selected: usize,\n    /// The index of the current address to show for the selected hop.\n    ///\n    /// Only used in detail mode.\n    pub selected_hop_address: usize,\n    /// The `FlowId` of the selected flow.\n    ///\n    /// FlowId(0) represents the unified flow for the trace.\n    pub selected_flow: FlowId,\n    /// Ordered flow ids with counts.\n    pub flow_counts: Vec<(FlowId, usize)>,\n    pub resolver: DnsResolver,\n    pub geoip_lookup: GeoIpLookup,\n    pub show_help: bool,\n    pub show_settings: bool,\n    pub show_hop_details: bool,\n    pub show_flows: bool,\n    pub show_chart: bool,\n    pub show_map: bool,\n    pub frozen_start: Option<SystemTime>,\n    pub zoom_factor: usize,\n}\n\nimpl TuiApp {\n    pub fn new(\n        tui_config: TuiConfig,\n        resolver: DnsResolver,\n        geoip_lookup: GeoIpLookup,\n        trace_info: Vec<TraceInfo>,\n    ) -> Self {\n        Self {\n            selected_tracer_data: State::default(),\n            trace_info,\n            tui_config,\n            table_state: TableState::default(),\n            setting_table_state: TableState::default(),\n            trace_selected: 0,\n            settings_tab_selected: 0,\n            selected_hop_address: 0,\n            selected_flow: State::default_flow_id(),\n            flow_counts: vec![],\n            resolver,\n            geoip_lookup,\n            show_help: false,\n            show_settings: false,\n            show_hop_details: false,\n            show_flows: false,\n            show_chart: false,\n            show_map: false,\n            frozen_start: None,\n            zoom_factor: 1,\n        }\n    }\n\n    pub const fn tracer_data(&self) -> &State {\n        &self.selected_tracer_data\n    }\n\n    pub fn snapshot_trace_data(&mut self) {\n        self.selected_tracer_data = self.trace_info[self.trace_selected].data.snapshot();\n    }\n\n    pub fn clear_trace_data(&self) {\n        self.trace_info[self.trace_selected].data.clear();\n    }\n\n    pub fn selected_hop_or_target(&self) -> &Hop {\n        self.table_state.selected().map_or_else(\n            || self.tracer_data().target_hop(self.selected_flow),\n            |s| &self.tracer_data().hops_for_flow(self.selected_flow)[s],\n        )\n    }\n\n    pub fn selected_hop(&self) -> Option<&Hop> {\n        self.table_state\n            .selected()\n            .map(|s| &self.tracer_data().hops_for_flow(self.selected_flow)[s])\n    }\n\n    pub fn tracer_config(&self) -> &TraceInfo {\n        &self.trace_info[self.trace_selected]\n    }\n\n    pub fn clamp_selected_hop(&mut self) {\n        let hop_count = self.tracer_data().hops_for_flow(self.selected_flow).len();\n        if let Some(selected) = self.table_state.selected() {\n            if selected > hop_count - 1 {\n                self.table_state.select(Some(hop_count - 1));\n            }\n        }\n    }\n\n    pub fn update_order_flow_counts(&mut self) {\n        pub fn order_flows(\n            &(flow_id1, count1): &(FlowId, usize),\n            &(flow_id2, count2): &(FlowId, usize),\n        ) -> std::cmp::Ordering {\n            match count1.cmp(&count2) {\n                std::cmp::Ordering::Equal => flow_id2.cmp(&flow_id1),\n                ord => ord,\n            }\n        }\n        self.flow_counts = self\n            .tracer_data()\n            .flows()\n            .iter()\n            .map(|&(_, flow_id)| {\n                let count = self.tracer_data().round_count(flow_id);\n                (flow_id, count)\n            })\n            .sorted_by(order_flows)\n            .rev()\n            .take(self.selected_tracer_data.max_flows())\n            .collect::<Vec<_>>();\n    }\n\n    pub fn next_hop(&mut self) {\n        let hop_count = self.tracer_data().hops_for_flow(self.selected_flow).len();\n        if hop_count == 0 {\n            return;\n        }\n        let max_index = 0.max(hop_count.saturating_sub(1));\n        let i = match self.table_state.selected() {\n            Some(i) => {\n                if i < max_index {\n                    i + 1\n                } else {\n                    i\n                }\n            }\n            None => 0,\n        };\n        self.table_state.select(Some(i));\n        self.selected_hop_address = 0;\n    }\n\n    pub fn previous_hop(&mut self) {\n        let hop_count = self.tracer_data().hops_for_flow(self.selected_flow).len();\n        if hop_count == 0 {\n            return;\n        }\n        let i = match self.table_state.selected() {\n            Some(i) => {\n                if i > 0 {\n                    i - 1\n                } else {\n                    i\n                }\n            }\n            None => 0.max(hop_count.saturating_sub(1)),\n        };\n        self.table_state.select(Some(i));\n        self.selected_hop_address = 0;\n    }\n\n    pub fn next_trace(&mut self) {\n        if self.trace_info.len() > 1 && self.trace_selected < self.trace_info.len() - 1 {\n            self.trace_selected += 1;\n            self.clear();\n        }\n    }\n\n    pub fn previous_trace(&mut self) {\n        if self.trace_info.len() > 1 && self.trace_selected > 0 {\n            self.trace_selected -= 1;\n            self.clear();\n        }\n    }\n\n    pub fn next_hop_address(&mut self) {\n        if let Some(hop) = self.selected_hop() {\n            if self.selected_hop_address < hop.addr_count() - 1 {\n                self.selected_hop_address += 1;\n            }\n        }\n    }\n\n    pub fn previous_hop_address(&mut self) {\n        if self.selected_hop().is_some() && self.selected_hop_address > 0 {\n            self.selected_hop_address -= 1;\n        }\n    }\n\n    pub fn flow_count(&self) -> usize {\n        self.selected_tracer_data.flows().len()\n    }\n\n    pub fn next_flow(&mut self) {\n        if self.show_flows {\n            let (cur_index, _) = self\n                .flow_counts\n                .iter()\n                .find_position(|(flow_id, _)| *flow_id == self.selected_flow)\n                .unwrap();\n            if cur_index < self.flow_counts.len() - 1 {\n                self.selected_flow = self.flow_counts[cur_index + 1].0;\n            }\n        }\n    }\n\n    pub fn previous_flow(&mut self) {\n        if self.show_flows {\n            let (cur_index, _) = self\n                .flow_counts\n                .iter()\n                .find_position(|(flow_id, _)| *flow_id == self.selected_flow)\n                .unwrap();\n            if cur_index > 0 {\n                self.selected_flow = self.flow_counts[cur_index - 1].0;\n            }\n        }\n    }\n\n    pub fn next_settings_tab(&mut self) {\n        if self.settings_tab_selected < settings_tabs().len() - 1 {\n            self.settings_tab_selected += 1;\n        }\n        self.setting_table_state.select(Some(0));\n    }\n\n    pub fn previous_settings_tab(&mut self) {\n        if self.settings_tab_selected > 0 {\n            self.settings_tab_selected -= 1;\n        }\n        self.setting_table_state.select(Some(0));\n    }\n\n    pub fn next_settings_item(&mut self) {\n        let count = self.get_settings_items_count();\n        let max_index = 0.max(count.saturating_sub(1));\n        let i = match self.setting_table_state.selected() {\n            Some(i) => {\n                if i < max_index {\n                    i + 1\n                } else {\n                    i\n                }\n            }\n            None => 0,\n        };\n        self.setting_table_state.select(Some(i));\n    }\n\n    pub fn previous_settings_item(&mut self) {\n        let count = self.get_settings_items_count();\n        let i = match self.setting_table_state.selected() {\n            Some(i) => {\n                if i > 0 {\n                    i - 1\n                } else {\n                    i\n                }\n            }\n            None => 0.max(count.saturating_sub(1)),\n        };\n        self.setting_table_state.select(Some(i));\n    }\n\n    fn get_settings_items_count(&self) -> usize {\n        if self.settings_tab_selected == SETTINGS_TAB_COLUMNS {\n            self.tui_config.tui_columns.all_columns_count()\n        } else {\n            settings_tabs()[self.settings_tab_selected].1\n        }\n    }\n\n    pub fn toggle_column_visibility(&mut self) {\n        if self.settings_tab_selected == SETTINGS_TAB_COLUMNS {\n            if let Some(selected) = self.setting_table_state.selected() {\n                self.tui_config.tui_columns.toggle(selected);\n            }\n        }\n    }\n\n    pub fn move_column_down(&mut self) {\n        if self.settings_tab_selected == SETTINGS_TAB_COLUMNS {\n            let count = self.tui_config.tui_columns.all_columns_count();\n            if let Some(selected) = self.setting_table_state.selected() {\n                if selected < count - 1 {\n                    self.tui_config.tui_columns.move_down(selected);\n                    self.setting_table_state.select(Some(selected + 1));\n                }\n            }\n        }\n    }\n\n    pub fn move_column_up(&mut self) {\n        if self.settings_tab_selected == SETTINGS_TAB_COLUMNS {\n            if let Some(selected) = self.setting_table_state.selected() {\n                if selected > 0 {\n                    self.tui_config.tui_columns.move_up(selected);\n                    self.setting_table_state.select(Some(selected - 1));\n                }\n            }\n        }\n    }\n\n    pub fn clear(&mut self) {\n        self.table_state.select(None);\n        self.selected_hop_address = 0;\n    }\n\n    pub fn toggle_help(&mut self) {\n        self.show_help = !self.show_help;\n    }\n\n    pub fn toggle_settings(&mut self) {\n        self.show_settings = !self.show_settings;\n    }\n\n    pub fn show_settings_columns(&mut self, column_index: usize) {\n        self.show_settings = true;\n        if self.settings_tab_selected != column_index {\n            self.settings_tab_selected = column_index;\n            self.setting_table_state.select(Some(0));\n        }\n    }\n\n    pub fn toggle_hop_details(&mut self) {\n        if self.show_hop_details {\n            self.tui_config.max_addrs = None;\n        } else {\n            self.tui_config.max_addrs = Some(1);\n        }\n        self.show_hop_details = !self.show_hop_details;\n    }\n\n    pub fn toggle_freeze(&mut self) {\n        self.frozen_start = match self.frozen_start {\n            None => Some(SystemTime::now()),\n            Some(_) => None,\n        };\n    }\n\n    pub fn toggle_chart(&mut self) {\n        self.show_chart = !self.show_chart;\n        self.show_map = false;\n    }\n\n    pub fn toggle_map(&mut self) {\n        self.show_map = !self.show_map;\n        self.show_chart = false;\n    }\n\n    pub fn toggle_flows(&mut self) {\n        if self.trace_info.len() == 1 && self.selected_tracer_data.max_flows() > 1 {\n            if self.show_flows {\n                self.selected_flow = FlowId(0);\n                self.show_flows = false;\n                self.selected_hop_address = 0;\n            } else if self.flow_count() > 0 {\n                self.selected_flow = FlowId(1);\n                self.show_flows = true;\n                self.selected_hop_address = 0;\n            }\n        }\n    }\n\n    pub fn expand_privacy(&mut self) {\n        let hop_count = self.tracer_data().hops_for_flow(self.selected_flow).len();\n        if let Some(privacy_max_ttl) = self.tui_config.privacy_max_ttl {\n            if usize::from(privacy_max_ttl) < hop_count {\n                self.tui_config.privacy_max_ttl = Some(privacy_max_ttl + 1);\n            }\n        } else {\n            self.tui_config.privacy_max_ttl = Some(0);\n        }\n    }\n\n    pub fn contract_privacy(&mut self) {\n        if let Some(privacy_max_ttl) = self.tui_config.privacy_max_ttl {\n            if privacy_max_ttl > 0 {\n                self.tui_config.privacy_max_ttl = Some(privacy_max_ttl - 1);\n            } else {\n                self.tui_config.privacy_max_ttl = None;\n            }\n        }\n    }\n\n    pub fn toggle_asinfo(&mut self) {\n        match self.resolver.config().resolve_method {\n            ResolveMethod::Resolv | ResolveMethod::Google | ResolveMethod::Cloudflare => {\n                self.tui_config.lookup_as_info = !self.tui_config.lookup_as_info;\n                self.resolver.flush();\n            }\n            ResolveMethod::System => {}\n        }\n    }\n\n    pub fn expand_hosts(&mut self) {\n        self.tui_config.max_addrs = match self.tui_config.max_addrs {\n            None => Some(1),\n            Some(i) if Some(i) < self.max_hosts() => Some(i + 1),\n            Some(i) => Some(i),\n        }\n    }\n\n    pub fn contract_hosts(&mut self) {\n        self.tui_config.max_addrs = match self.tui_config.max_addrs {\n            Some(i) if i > 1 => Some(i - 1),\n            _ => None,\n        }\n    }\n\n    pub fn zoom_in(&mut self) {\n        if self.zoom_factor < MAX_ZOOM_FACTOR {\n            self.zoom_factor += 1;\n        }\n    }\n\n    pub fn zoom_out(&mut self) {\n        if self.zoom_factor > 1 {\n            self.zoom_factor -= 1;\n        }\n    }\n\n    pub fn expand_hosts_max(&mut self) {\n        self.tui_config.max_addrs = self.max_hosts();\n    }\n\n    pub fn contract_hosts_min(&mut self) {\n        self.tui_config.max_addrs = Some(1);\n    }\n\n    /// The maximum number of hosts per hop for the currently selected trace.\n    pub fn max_hosts(&self) -> Option<u8> {\n        self.selected_tracer_data\n            .hops_for_flow(self.selected_flow)\n            .iter()\n            .map(|h| h.addrs().count())\n            .max()\n            .and_then(|i| u8::try_from(i).ok())\n    }\n}\n\nconst MAX_ZOOM_FACTOR: usize = 16;\n"
  },
  {
    "path": "crates/trippy-tui/src/frontend.rs",
    "content": "use crate::app::TraceInfo;\nuse crate::config::AddressMode;\nuse crate::frontend::binding::CTRL_C;\nuse crate::geoip::GeoIpLookup;\npub use config::TuiConfig;\nuse crossterm::event::KeyEventKind;\nuse crossterm::{\n    event::{self, Event},\n    execute,\n    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},\n};\nuse ratatui::layout::Position;\nuse ratatui::{\n    Terminal,\n    backend::{Backend, CrosstermBackend},\n};\nuse std::io;\nuse trippy_dns::DnsResolver;\nuse tui_app::TuiApp;\n\nmod binding;\nmod columns;\nmod config;\nmod render;\nmod theme;\nmod tui_app;\n\n/// Run the frontend TUI.\npub fn run_frontend(\n    traces: Vec<TraceInfo>,\n    tui_config: TuiConfig,\n    resolver: DnsResolver,\n    geoip_lookup: GeoIpLookup,\n) -> anyhow::Result<()> {\n    enable_raw_mode()?;\n    let mut stdout = io::stdout();\n    execute!(stdout, EnterAlternateScreen)?;\n    let original_hook = std::panic::take_hook();\n    std::panic::set_hook(Box::new(move |panic| {\n        disable_raw_mode().expect(\"disable_raw_mode\");\n        execute!(io::stdout(), LeaveAlternateScreen).expect(\"execute LeaveAlternateScreen\");\n        original_hook(panic);\n    }));\n    let backend = CrosstermBackend::new(stdout);\n    let mut terminal = Terminal::new(backend)?;\n    let preserve_screen = tui_config.preserve_screen;\n    let mut app = TuiApp::new(tui_config, resolver, geoip_lookup, traces);\n    let res = run_app(&mut terminal, &mut app);\n    disable_raw_mode()?;\n    if preserve_screen || matches!(res, Ok(ExitAction::PreserveScreen)) {\n        terminal.set_cursor_position(Position::new(0, terminal.size()?.height))?;\n        terminal.backend_mut().append_lines(1)?;\n    } else {\n        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;\n    }\n    terminal.show_cursor()?;\n    if let Err(err) = res {\n        println!(\"{err:?}\");\n    }\n    Ok(())\n}\n\n/// The exit action to take when the frontend exits.\nenum ExitAction {\n    /// Exit the frontend normally.\n    Normal,\n    /// Preserve the screen on exit.\n    PreserveScreen,\n}\n\n#[expect(clippy::too_many_lines)]\nfn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut TuiApp) -> io::Result<ExitAction> {\n    loop {\n        if app.frozen_start.is_none() {\n            app.snapshot_trace_data();\n            app.clamp_selected_hop();\n            app.update_order_flow_counts();\n        }\n        terminal.draw(|f| render::app::render(f, app))?;\n        if event::poll(app.tui_config.refresh_rate)? {\n            if let Event::Key(key) = event::read()? {\n                if key.kind == KeyEventKind::Press {\n                    let bindings = &app.tui_config.bindings;\n                    if app.show_help {\n                        if bindings.toggle_help.check(key)\n                            || bindings.toggle_help_alt.check(key)\n                            || bindings.clear_selection.check(key)\n                            || bindings.quit.check(key)\n                        {\n                            app.toggle_help();\n                        } else if bindings.toggle_settings.check(key) {\n                            app.toggle_help();\n                            app.toggle_settings();\n                        } else if bindings.toggle_settings_tui.check(key) {\n                            app.toggle_help();\n                            app.show_settings_columns(0);\n                        } else if bindings.toggle_settings_trace.check(key) {\n                            app.toggle_help();\n                            app.show_settings_columns(1);\n                        } else if bindings.toggle_settings_dns.check(key) {\n                            app.toggle_help();\n                            app.show_settings_columns(2);\n                        } else if bindings.toggle_settings_geoip.check(key) {\n                            app.toggle_help();\n                            app.show_settings_columns(3);\n                        } else if bindings.toggle_settings_bindings.check(key) {\n                            app.toggle_help();\n                            app.show_settings_columns(4);\n                        } else if bindings.toggle_settings_theme.check(key) {\n                            app.toggle_help();\n                            app.show_settings_columns(5);\n                        } else if bindings.toggle_settings_columns.check(key) {\n                            app.toggle_help();\n                            app.show_settings_columns(6);\n                        }\n                    } else if app.show_settings {\n                        if bindings.toggle_settings.check(key)\n                            || bindings.clear_selection.check(key)\n                            || bindings.quit.check(key)\n                        {\n                            app.toggle_settings();\n                        } else if bindings.toggle_settings_tui.check(key) {\n                            app.show_settings_columns(0);\n                        } else if bindings.toggle_settings_trace.check(key) {\n                            app.show_settings_columns(1);\n                        } else if bindings.toggle_settings_dns.check(key) {\n                            app.show_settings_columns(2);\n                        } else if bindings.toggle_settings_geoip.check(key) {\n                            app.show_settings_columns(3);\n                        } else if bindings.toggle_settings_bindings.check(key) {\n                            app.show_settings_columns(4);\n                        } else if bindings.toggle_settings_theme.check(key) {\n                            app.show_settings_columns(5);\n                        } else if bindings.toggle_settings_columns.check(key) {\n                            app.show_settings_columns(6);\n                        } else if bindings.previous_trace.check(key) {\n                            app.previous_settings_tab();\n                        } else if bindings.next_trace.check(key) {\n                            app.next_settings_tab();\n                        } else if bindings.next_hop.check(key) {\n                            app.next_settings_item();\n                        } else if bindings.previous_hop.check(key) {\n                            app.previous_settings_item();\n                        } else if bindings.toggle_chart.check(key) {\n                            app.toggle_column_visibility();\n                        } else if bindings.next_hop_address.check(key) {\n                            app.move_column_down();\n                        } else if bindings.previous_hop_address.check(key) {\n                            app.move_column_up();\n                        }\n                    } else if bindings.toggle_help.check(key) || bindings.toggle_help_alt.check(key)\n                    {\n                        app.toggle_help();\n                    } else if bindings.toggle_settings.check(key) {\n                        app.toggle_settings();\n                    } else if bindings.toggle_settings_tui.check(key) {\n                        app.show_settings_columns(0);\n                    } else if bindings.toggle_settings_trace.check(key) {\n                        app.show_settings_columns(1);\n                    } else if bindings.toggle_settings_dns.check(key) {\n                        app.show_settings_columns(2);\n                    } else if bindings.toggle_settings_geoip.check(key) {\n                        app.show_settings_columns(3);\n                    } else if bindings.toggle_settings_bindings.check(key) {\n                        app.show_settings_columns(4);\n                    } else if bindings.toggle_settings_theme.check(key) {\n                        app.show_settings_columns(5);\n                    } else if bindings.toggle_settings_columns.check(key) {\n                        app.show_settings_columns(6);\n                    } else if bindings.next_hop.check(key) {\n                        app.next_hop();\n                    } else if bindings.previous_hop.check(key) {\n                        app.previous_hop();\n                    } else if bindings.previous_trace.check(key) {\n                        if app.show_flows {\n                            app.previous_flow();\n                        } else {\n                            app.previous_trace();\n                        }\n                    } else if bindings.next_trace.check(key) {\n                        if app.show_flows {\n                            app.next_flow();\n                        } else {\n                            app.next_trace();\n                        }\n                    } else if bindings.next_hop_address.check(key) {\n                        app.next_hop_address();\n                    } else if bindings.previous_hop_address.check(key) {\n                        app.previous_hop_address();\n                    } else if bindings.address_mode_ip.check(key) {\n                        app.tui_config.address_mode = AddressMode::Ip;\n                    } else if bindings.address_mode_host.check(key) {\n                        app.tui_config.address_mode = AddressMode::Host;\n                    } else if bindings.address_mode_both.check(key) {\n                        app.tui_config.address_mode = AddressMode::Both;\n                    } else if bindings.toggle_freeze.check(key) {\n                        app.toggle_freeze();\n                    } else if bindings.toggle_chart.check(key) {\n                        app.toggle_chart();\n                    } else if bindings.toggle_map.check(key) {\n                        app.toggle_map();\n                    } else if bindings.toggle_flows.check(key) {\n                        app.toggle_flows();\n                    } else if bindings.expand_privacy.check(key) {\n                        app.expand_privacy();\n                    } else if bindings.contract_privacy.check(key) {\n                        app.contract_privacy();\n                    } else if bindings.contract_hosts_min.check(key) {\n                        app.contract_hosts_min();\n                    } else if bindings.expand_hosts_max.check(key) {\n                        app.expand_hosts_max();\n                    } else if bindings.contract_hosts.check(key) {\n                        app.contract_hosts();\n                    } else if bindings.expand_hosts.check(key) {\n                        app.expand_hosts();\n                    } else if bindings.chart_zoom_in.check(key) {\n                        app.zoom_in();\n                    } else if bindings.chart_zoom_out.check(key) {\n                        app.zoom_out();\n                    } else if bindings.clear_trace_data.check(key) {\n                        app.clear();\n                        app.clear_trace_data();\n                    } else if bindings.clear_dns_cache.check(key) {\n                        app.resolver.flush();\n                    } else if bindings.clear_selection.check(key) {\n                        app.clear();\n                    } else if bindings.toggle_as_info.check(key) {\n                        app.toggle_asinfo();\n                    } else if bindings.toggle_hop_details.check(key) {\n                        app.toggle_hop_details();\n                    } else if bindings.quit.check(key) || CTRL_C.check(key) {\n                        return Ok(ExitAction::Normal);\n                    } else if bindings.quit_preserve_screen.check(key) {\n                        return Ok(ExitAction::PreserveScreen);\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/geoip.rs",
    "content": "use anyhow::Context;\nuse itertools::Itertools;\nuse maxminddb::Reader;\nuse std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::net::IpAddr;\nuse std::path::Path;\nuse std::rc::Rc;\nuse std::str::FromStr;\n\n#[derive(Debug, Clone, Default)]\npub struct GeoIpCity {\n    latitude: Option<f64>,\n    longitude: Option<f64>,\n    accuracy_radius: Option<u16>,\n    city: Option<String>,\n    subdivision: Option<String>,\n    subdivision_code: Option<String>,\n    country: Option<String>,\n    country_code: Option<String>,\n    continent: Option<String>,\n}\n\nimpl GeoIpCity {\n    pub fn short_name(&self) -> String {\n        [\n            self.city.as_ref(),\n            self.subdivision_code.as_ref(),\n            self.country_code.as_ref(),\n        ]\n        .into_iter()\n        .flatten()\n        .join(\", \")\n    }\n\n    pub fn long_name(&self) -> String {\n        [\n            self.city.as_ref(),\n            self.subdivision.as_ref(),\n            self.country.as_ref(),\n            self.continent.as_ref(),\n        ]\n        .into_iter()\n        .flatten()\n        .join(\", \")\n    }\n\n    pub fn location(&self) -> String {\n        format!(\n            \"{}, {} (~{}km)\",\n            self.latitude.unwrap_or_default(),\n            self.longitude.unwrap_or_default(),\n            self.accuracy_radius.unwrap_or_default(),\n        )\n    }\n\n    pub const fn coordinates(&self) -> Option<(f64, f64, u16)> {\n        match (self.latitude, self.longitude, self.accuracy_radius) {\n            (Some(lat), Some(long), Some(radius)) => Some((lat, long, radius)),\n            _ => None,\n        }\n    }\n}\n\nmod ipinfo {\n    use serde::{Deserialize, Serialize};\n    use serde_with::serde_as;\n\n    /// The `IPinfo` mmdb database format.\n    ///\n    /// Support both the \"IP to Geolocation Extended\" and \"IP to Country + ASN\" database formats.\n    ///\n    /// IP to Geolocation Extended Database:\n    /// See <https://ipinfo.io/developers/ip-to-geolocation-extended/>\n    ///\n    /// IP to Country + ASN Database;\n    /// See <https://ipinfo.io/developers/ip-to-country-asn-database/>\n    #[serde_as]\n    #[derive(Debug, Serialize, Deserialize)]\n    pub struct IpInfoGeoIp {\n        /// \"42.48948\"\n        #[serde(default)]\n        #[serde_as(as = \"serde_with::NoneAsEmptyString\")]\n        pub latitude: Option<String>,\n        /// \"-83.14465\"\n        #[serde(default)]\n        #[serde_as(as = \"serde_with::NoneAsEmptyString\")]\n        pub longitude: Option<String>,\n        /// \"500\"\n        #[serde(default)]\n        #[serde_as(as = \"serde_with::NoneAsEmptyString\")]\n        pub radius: Option<String>,\n        /// \"Royal Oak\"\n        #[serde(default)]\n        #[serde_as(as = \"serde_with::NoneAsEmptyString\")]\n        pub city: Option<String>,\n        /// \"Michigan\"\n        #[serde(default)]\n        #[serde_as(as = \"serde_with::NoneAsEmptyString\")]\n        pub region: Option<String>,\n        /// \"48067\"\n        #[serde(default)]\n        #[serde_as(as = \"serde_with::NoneAsEmptyString\")]\n        pub postal_code: Option<String>,\n        /// \"US\"\n        #[serde(default)]\n        #[serde_as(as = \"serde_with::NoneAsEmptyString\")]\n        pub country: Option<String>,\n        /// \"Japan\"\n        #[serde(default)]\n        #[serde_as(as = \"serde_with::NoneAsEmptyString\")]\n        pub country_name: Option<String>,\n        /// \"Asia\"\n        #[serde(default)]\n        #[serde_as(as = \"serde_with::NoneAsEmptyString\")]\n        pub continent_name: Option<String>,\n    }\n\n    #[cfg(test)]\n    mod tests {\n        use super::*;\n\n        #[test]\n        fn test_empty() {\n            let json = \"{}\";\n            let value: IpInfoGeoIp = serde_json::from_str(json).unwrap();\n            assert_eq!(None, value.latitude);\n            assert_eq!(None, value.longitude);\n            assert_eq!(None, value.radius);\n            assert_eq!(None, value.city);\n            assert_eq!(None, value.region);\n            assert_eq!(None, value.postal_code);\n            assert_eq!(None, value.country.as_deref());\n            assert_eq!(None, value.country_name.as_deref());\n            assert_eq!(None, value.continent_name.as_deref());\n        }\n\n        #[test]\n        fn test_country_asn_db_format() {\n            let json = r#\"\n                {\n                    \"start_ip\": \"40.96.54.192\",\n                    \"end_ip\": \"40.96.54.255\",\n                    \"country\": \"JP\",\n                    \"country_name\": \"Japan\",\n                    \"continent\": \"AS\",\n                    \"continent_name\": \"Asia\",\n                    \"asn\": \"AS8075\",\n                    \"as_name\": \"Microsoft Corporation\",\n                    \"as_domain\": \"microsoft.com\"\n                }\n                \"#;\n            let value: IpInfoGeoIp = serde_json::from_str(json).unwrap();\n            assert_eq!(None, value.latitude);\n            assert_eq!(None, value.longitude);\n            assert_eq!(None, value.radius);\n            assert_eq!(None, value.city);\n            assert_eq!(None, value.region);\n            assert_eq!(None, value.postal_code);\n            assert_eq!(Some(\"JP\"), value.country.as_deref());\n            assert_eq!(Some(\"Japan\"), value.country_name.as_deref());\n            assert_eq!(Some(\"Asia\"), value.continent_name.as_deref());\n        }\n\n        #[test]\n        fn test_extended_db_format() {\n            let json = r#\"\n                {\n                    \"start_ip\": \"60.127.10.249\",\n                    \"end_ip\": \"60.127.10.249\",\n                    \"join_key\": \"60.127.0.0\",\n                    \"city\": \"Yokohama\",\n                    \"region\": \"Kanagawa\",\n                    \"country\": \"JP\",\n                    \"latitude\": \"35.43333\",\n                    \"longitude\": \"139.65\",\n                    \"postal_code\": \"220-8588\",\n                    \"timezone\": \"Asia/Tokyo\",\n                    \"geoname_id\": \"1848354\",\n                    \"radius\": \"500\"\n                }\n                \"#;\n            let value: IpInfoGeoIp = serde_json::from_str(json).unwrap();\n            assert_eq!(Some(\"35.43333\"), value.latitude.as_deref());\n            assert_eq!(Some(\"139.65\"), value.longitude.as_deref());\n            assert_eq!(Some(\"500\"), value.radius.as_deref());\n            assert_eq!(Some(\"Yokohama\"), value.city.as_deref());\n            assert_eq!(Some(\"Kanagawa\"), value.region.as_deref());\n            assert_eq!(Some(\"220-8588\"), value.postal_code.as_deref());\n            assert_eq!(Some(\"JP\"), value.country.as_deref());\n            assert_eq!(None, value.country_name.as_deref());\n            assert_eq!(None, value.continent_name.as_deref());\n        }\n    }\n}\n\nimpl From<ipinfo::IpInfoGeoIp> for GeoIpCity {\n    fn from(value: ipinfo::IpInfoGeoIp) -> Self {\n        Self {\n            latitude: value.latitude.and_then(|val| f64::from_str(&val).ok()),\n            longitude: value.longitude.and_then(|val| f64::from_str(&val).ok()),\n            accuracy_radius: value.radius.and_then(|val| u16::from_str(&val).ok()),\n            city: value.city,\n            subdivision: value.region,\n            subdivision_code: value.postal_code,\n            country: value.country_name,\n            country_code: value.country,\n            continent: value.continent_name,\n        }\n    }\n}\n\nimpl From<(maxminddb::geoip2::City<'_>, &str)> for GeoIpCity {\n    fn from((value, locale): (maxminddb::geoip2::City<'_>, &str)) -> Self {\n        let city = localized_name(&value.city.names, locale);\n        let subdivision = value\n            .subdivisions\n            .first()\n            .and_then(|c| localized_name(&c.names, locale));\n        let subdivision_code = value\n            .subdivisions\n            .first()\n            .and_then(|c| c.iso_code.as_ref().map(ToString::to_string));\n        let country = localized_name(&value.country.names, locale);\n        let country_code = value.country.iso_code.map(ToString::to_string);\n        let continent = localized_name(&value.continent.names, locale);\n        let latitude = value.location.latitude;\n        let longitude = value.location.longitude;\n        let accuracy_radius = value.location.accuracy_radius;\n        Self {\n            latitude,\n            longitude,\n            accuracy_radius,\n            city,\n            subdivision,\n            subdivision_code,\n            country,\n            country_code,\n            continent,\n        }\n    }\n}\n\n/// The fallback locale.\n///\n/// The `MaxMind` support documentation says:\n///\n/// > Our geolocation name data includes the names of the continent, country, city, and\n/// > subdivisions of the location of the IP address. We include the country names in\n/// > English, Simplified Chinese, Spanish, Brazilian Portuguese, Russian, Japanese, French,\n/// > and German.\n/// >\n/// > Please note: Not every place name is always available in each language. We recommend checking\n/// > English names as a default for cases where a localized name is not available in your preferred\n/// > language.\nconst FALLBACK_LOCALE: &str = \"en\";\n\n/// Alias for a cache of `GeoIp` data.\ntype Cache = RefCell<HashMap<IpAddr, Option<Rc<GeoIpCity>>>>;\n\n/// Lookup `GeoIpCity` data form an `IpAddr`.\n#[derive(Debug)]\npub struct GeoIpLookup {\n    reader: Option<Reader<Vec<u8>>>,\n    cache: Cache,\n    locale: String,\n}\n\nimpl GeoIpLookup {\n    /// Create a new `GeoIpLookup` from a `MaxMind` DB file.\n    pub fn from_file<P: AsRef<Path>>(path: P, locale: String) -> anyhow::Result<Self> {\n        let reader = maxminddb::Reader::open_readfile(path.as_ref())\n            .context(format!(\"{}\", path.as_ref().display()))?;\n        Ok(Self {\n            reader: Some(reader),\n            cache: RefCell::new(HashMap::new()),\n            locale,\n        })\n    }\n\n    /// Create a `GeoIpLookup` that returns `None` for all `IpAddr` lookups.\n    pub fn empty() -> Self {\n        Self {\n            reader: None,\n            cache: RefCell::new(HashMap::new()),\n            locale: FALLBACK_LOCALE.to_string(),\n        }\n    }\n\n    /// Lookup an `GeoIpCity` for an `IpAddr`.\n    ///\n    /// If an entry is found it is cached and returned, otherwise None is returned.\n    pub fn lookup(&self, addr: IpAddr) -> anyhow::Result<Option<Rc<GeoIpCity>>> {\n        if let Some(reader) = &self.reader {\n            if let Some(geo) = self.cache.borrow().get(&addr) {\n                return Ok(geo.clone());\n            }\n            let lookup_result = reader.lookup(addr)?;\n            let city_data = if reader.metadata.database_type.starts_with(\"ipinfo\") {\n                lookup_result\n                    .decode::<ipinfo::IpInfoGeoIp>()?\n                    .map(GeoIpCity::from)\n            } else {\n                lookup_result\n                    .decode::<maxminddb::geoip2::City<'_>>()?\n                    .map(|city| GeoIpCity::from((city, self.locale.as_ref())))\n            };\n            let cached = city_data.map(Rc::new);\n            self.cache.borrow_mut().insert(addr, cached.clone());\n            Ok(cached)\n        } else {\n            Ok(None)\n        }\n    }\n}\n\nfn localized_name(names: &maxminddb::geoip2::Names<'_>, locale: &str) -> Option<String> {\n    lookup_locale(names, locale)\n        .or_else(|| lookup_locale(names, FALLBACK_LOCALE))\n        .map(ToString::to_string)\n}\n\n/// Map a Trippy locale code to the closest `maxminddb` locale field.\n///\n/// - `pt*` (e.g. `pt`, `pt-BR`, `pt-PT`) use `brazilian_portuguese`\n/// - `zh*` (e.g. `zh`, `zh-TW`) use `simplified_chinese`\n/// - Other languages that are supported map directly (`en`, `de`, `es`, `fr`, `ja`, `ru`).\nfn lookup_locale<'a>(names: &maxminddb::geoip2::Names<'a>, code: &str) -> Option<&'a str> {\n    if code.starts_with(\"pt\") {\n        names.brazilian_portuguese\n    } else if code.starts_with(\"zh\") {\n        names.simplified_chinese\n    } else {\n        match code {\n            \"de\" => names.german,\n            \"en\" => names.english,\n            \"es\" => names.spanish,\n            \"fr\" => names.french,\n            \"ja\" => names.japanese,\n            \"ru\" => names.russian,\n            _ => None,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/lib.rs",
    "content": "#![allow(\n    clippy::struct_excessive_bools,\n    clippy::cast_sign_loss,\n    clippy::struct_field_names\n)]\n#![forbid(unsafe_code)]\n\nuse crate::config::TrippyAction;\nuse clap::Parser;\nuse config::Args;\nuse std::process;\nuse trippy_privilege::Privilege;\n\nmod app;\nmod config;\nmod frontend;\nmod geoip;\nmod locale;\nmod print;\nmod report;\nmod util;\n\n/// Run the Trippy application.\npub fn trippy() -> anyhow::Result<()> {\n    let args = Args::parse();\n    let privilege = Privilege::acquire_privileges()?;\n    let pid = u16::try_from(process::id() % u32::from(u16::MAX))?;\n    match TrippyAction::from(args, &privilege, pid)? {\n        TrippyAction::Trippy(cfg) => app::run_trippy(&cfg, pid)?,\n        TrippyAction::PrintTuiThemeItems => print::print_tui_theme_items(),\n        TrippyAction::PrintTuiBindingCommands => print::print_tui_binding_commands(),\n        TrippyAction::PrintConfigTemplate => print::print_config_template(),\n        TrippyAction::PrintManPage => print::print_man_page()?,\n        TrippyAction::PrintShellCompletions(shell) => print::print_shell_completions(shell)?,\n        TrippyAction::PrintLocales => print::print_locales(),\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/locale.rs",
    "content": "use itertools::Itertools;\nuse serde::Deserialize;\nuse std::cell::RefCell;\nuse std::collections::HashMap;\nuse std::str::FromStr;\nuse std::sync::OnceLock;\nuse unic_langid::LanguageIdentifier;\n\nconst FALLBACK_LOCALE: &str = \"en\";\n\n/// Set the locale for the application.\n///\n/// If the given locale is `None` the system locale is tried. If the system locale cannot be\n/// determined then the fallback locale is used.\n///\n/// In all cases, the language part of the locale is used if the full locale is not supported.\npub fn set_locale(locale: Option<&str>) -> String {\n    let new_locale = calculate_locale(locale, sys_locale::get_locale().as_deref());\n    store_locale(&new_locale);\n    new_locale\n}\n\n/// Get the available locales.\npub fn available_locales() -> Vec<&'static str> {\n    data()\n        .0\n        .iter()\n        .flat_map(|(_, v)| v.0.keys().map(AsRef::as_ref))\n        .unique()\n        .sorted_unstable()\n        .collect::<Vec<_>>()\n}\n\n/// A macro to translate an item to the current locale.\n#[macro_export]\nmacro_rules! t {\n    ($key:expr) => {\n        std::borrow::Cow::Borrowed($crate::locale::__translate($key))\n    };\n    ($key:expr, $($kt:ident = $kv:expr),+) => {\n        {\n            let string = t!($key);\n            $(\n                let string = string.replace(concat!(\"%{\", stringify!($kt), \"}\"), &$kv.to_string());\n            )+\n            string\n        }\n    };\n    ($key:expr, $($kt:literal => $kv:expr),+) => {\n        {\n            let string = t!($key);\n            $(\n                let string = string.replace(concat!(\"%{\", $kt, \"}\"), &$kv.to_string());\n            )+\n            string\n        }\n    };\n}\n\n/// Translate an item to the current locale.\n///\n/// This function is public as it is used by the `t!` macro, however is not considered part of the\n/// public interface.\n#[doc(hidden)]\npub fn __translate(item: &str) -> &str {\n    let locale = CURRENT_LOCALE.with(Clone::clone);\n    let binding = locale.borrow();\n    translate_locale(item, binding.as_str())\n}\n\n/// Translate an item to a specific locale.\n///\n/// If the item does not exists, the key is returned. Otherwise, if item does not contain the\n/// locale, the fallback locale is used. If the fallback locale does not exist, the key is\n/// returned.\nfn translate_locale<'a>(item: &'a str, locale: &str) -> &'a str {\n    if let Some(key) = data().0.get(item) {\n        if let Some(value) = key.0.get(locale) {\n            value\n        } else if let Some(value) = key.0.get(FALLBACK_LOCALE) {\n            value\n        } else {\n            item\n        }\n    } else {\n        item\n    }\n}\n\n/// Get the locale data.\nfn data() -> &'static Data {\n    static DATA: OnceLock<Data> = OnceLock::new();\n    DATA.get_or_init(|| {\n        toml::from_str(include_str!(\"../locales.toml\")).expect(\"Failed to parse locales.toml\")\n    })\n}\n\n/// This is a map of a item name (i.e. `title_hops`, `awaiting_data`, etc.) to the locale `Item`.\n#[derive(Debug, Deserialize)]\nstruct Data(HashMap<String, Item>);\n\n/// This is a map of locale keys (i.e. `en`, `zh`, etc.) to the translated value.\n#[derive(Debug, Deserialize)]\nstruct Item(HashMap<String, String>);\n\n/// calculate the locale to use.\nfn calculate_locale(cfg_locale: Option<&str>, sys_locale: Option<&str>) -> String {\n    let preferred = cfg_locale.or(sys_locale).unwrap_or(FALLBACK_LOCALE);\n    let locales = available_locales();\n    locales\n        .contains(&preferred)\n        .then(|| preferred.to_string())\n        .or_else(|| {\n            LanguageIdentifier::from_str(preferred).ok().and_then(|id| {\n                let lang = id.language.to_string();\n                id.region\n                    .map(|r| format!(\"{lang}-{r}\"))\n                    .filter(|s| locales.contains(&s.as_str()))\n                    .or_else(|| locales.contains(&lang.as_str()).then_some(lang))\n            })\n        })\n        .unwrap_or_else(|| FALLBACK_LOCALE.to_string())\n}\n\nthread_local! {\n    static CURRENT_LOCALE: RefCell<String> = RefCell::new(String::from(FALLBACK_LOCALE));\n}\n\nfn store_locale(new_locale: &str) {\n    CURRENT_LOCALE.with(|locale| *locale.borrow_mut() = String::from(new_locale));\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use test_case::test_case;\n\n    #[test_case(None, None, \"en\"; \"no_locale\")]\n    #[test_case(Some(\"en\"), None, \"en\"; \"cfg_locale\")]\n    #[test_case(None, Some(\"en\"), \"en\"; \"sys_locale\")]\n    #[test_case(Some(\"en\"), Some(\"en\"), \"en\"; \"both_locales\")]\n    #[test_case(Some(\"en\"), Some(\"zh\"), \"en\"; \"both_locales_mismatch\")]\n    #[test_case(Some(\"zh\"), Some(\"en\"), \"zh\"; \"both_locales_mismatch_reverse\")]\n    #[test_case(Some(\"en-US\"), None, \"en\"; \"cfg_locale_dash\")]\n    #[test_case(None, Some(\"en-US\"), \"en\"; \"sys_locale_dash\")]\n    #[test_case(Some(\"en-US\"), Some(\"en-US\"), \"en\"; \"both_locales_dash\")]\n    #[test_case(Some(\"en-US\"), Some(\"zh-CN\"), \"en\"; \"both_locales_mismatch_dash\")]\n    #[test_case(Some(\"zh-CN\"), Some(\"en-US\"), \"zh\"; \"both_locales_mismatch_reverse_dash\")]\n    #[test_case(Some(\"en_US\"), None, \"en\"; \"cfg_locale_underscore\")]\n    #[test_case(None, Some(\"en_US\"), \"en\"; \"sys_locale_underscore\")]\n    #[test_case(Some(\"xx\"), None, \"en\"; \"cfg_locale_unknown\")]\n    #[test_case(None, Some(\"xx\"), \"en\"; \"sys_locale_unknown\")]\n    #[test_case(Some(\"xx\"), Some(\"xx\"), \"en\"; \"both_locales_unknown\")]\n    #[test_case(Some(\"en-\"), None, \"en\"; \"cfg_locale_invalid_dash\")]\n    #[test_case(Some(\"en_\"), None, \"en\"; \"cfg_locale_invalid_underscore\")]\n    #[test_case(Some(\"en?\"), None, \"en\"; \"cfg_locale_invalid_accepted\")]\n    #[test_case(Some(\"zh-Hant-TW\"), None, \"zh-TW\"; \"cfg_locale_ignore_script\")]\n    #[test_case(None, Some(\"zh-Hant-TW\"), \"zh-TW\"; \"sys_locale_ignore_script\")]\n    #[test_case(Some(\"zh-Hant-TW\"), Some(\"zh-Hant-TW\"), \"zh-TW\"; \"both_locales_ignore_script\")]\n    fn test_set_locale(cfg_locale: Option<&str>, sys_locale: Option<&str>, expected: &str) {\n        assert_eq!(calculate_locale(cfg_locale, sys_locale), expected);\n    }\n\n    #[test]\n    fn test_available_languages() {\n        assert_eq!(\n            available_locales(),\n            vec![\n                \"de\", \"en\", \"es\", \"fr\", \"it\", \"pt\", \"ru\", \"sv\", \"tr\", \"zh\", \"zh-TW\"\n            ]\n        );\n    }\n\n    #[test]\n    fn test_data_deserialize() {\n        assert!(!data().0.is_empty());\n    }\n\n    #[test]\n    fn test_translate() {\n        assert_eq!(translate_locale(\"title_hops\", \"en\"), \"Hops\");\n        assert_eq!(translate_locale(\"title_hops\", \"zh\"), \"跳\");\n        assert_eq!(translate_locale(\"title_hops\", \"zh-TW\"), \"跳\");\n        assert_eq!(translate_locale(\"unknown_item\", \"en\"), \"unknown_item\");\n        assert_eq!(translate_locale(\"unknown_locale\", \"xx\"), \"unknown_locale\");\n    }\n\n    #[test]\n    fn test_translate_macro() {\n        assert_eq!(t!(\"title_hops\"), \"Hops\");\n        assert_eq!(t!(\"awaiting_data\"), \"Awaiting data...\");\n        assert_eq!(t!(\"unknown_item\"), \"unknown_item\");\n    }\n\n    #[test]\n    fn test_zh_tw_translations() {\n        // Test key Traditional Chinese translations\n        assert_eq!(translate_locale(\"auto\", \"zh-TW\"), \"自動\");\n        assert_eq!(translate_locale(\"status_failed\", \"zh-TW\"), \"失敗\");\n        assert_eq!(translate_locale(\"status_running\", \"zh-TW\"), \"執行中\");\n        assert_eq!(translate_locale(\"title_settings\", \"zh-TW\"), \"設定\");\n        assert_eq!(translate_locale(\"help_tagline\", \"zh-TW\"), \"網路診斷工具\");\n        assert_eq!(translate_locale(\"column_loss_pct\", \"zh-TW\"), \"封包遺失率\");\n        assert_eq!(translate_locale(\"rtt\", \"zh-TW\"), \"往返時間\");\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/print.rs",
    "content": "use crate::config::{Args, TuiCommandItem, TuiThemeItem};\nuse crate::locale::available_locales;\nuse clap::CommandFactory;\nuse clap_complete::Shell;\nuse std::process;\nuse strum::VariantNames;\n\npub fn print_tui_theme_items() {\n    println!(\"{}\", tui_theme_items());\n    process::exit(0);\n}\n\npub fn print_tui_binding_commands() {\n    println!(\"{}\", tui_binding_commands());\n    process::exit(0);\n}\n\npub fn print_config_template() {\n    println!(\"{}\", include_str!(\"../trippy-config-sample.toml\"));\n    process::exit(0);\n}\n\npub fn print_shell_completions(shell: Shell) -> anyhow::Result<()> {\n    println!(\"{}\", shell_completions(shell)?);\n    process::exit(0);\n}\n\npub fn print_man_page() -> anyhow::Result<()> {\n    println!(\"{}\", man_page()?);\n    process::exit(0);\n}\n\npub fn print_locales() {\n    println!(\"TUI locales: {}\", available_locales().join(\", \"));\n    process::exit(0);\n}\n\nfn tui_theme_items() -> String {\n    format!(\n        \"TUI theme color items: {}\",\n        TuiThemeItem::VARIANTS.join(\", \")\n    )\n}\n\nfn tui_binding_commands() -> String {\n    format!(\n        \"TUI binding commands: {}\",\n        TuiCommandItem::VARIANTS.join(\", \")\n    )\n}\n\nfn shell_completions(shell: Shell) -> anyhow::Result<String> {\n    let mut cmd = Args::command();\n    let name = cmd.get_name().to_string();\n    let mut buffer: Vec<u8> = vec![];\n    clap_complete::generate(shell, &mut cmd, name, &mut buffer);\n    Ok(String::from_utf8(buffer)?)\n}\n\nfn man_page() -> anyhow::Result<String> {\n    let cmd = Args::command();\n    let mut buffer: Vec<u8> = vec![];\n    clap_mangen::Man::new(cmd).render(&mut buffer)?;\n    Ok(String::from_utf8(buffer)?)\n}\n\n#[cfg(test)]\npub mod tests {\n    use super::*;\n    use crate::util::{insta, remove_whitespace};\n    use test_case::test_case;\n\n    #[test_case(&tui_theme_items(), \"tui theme items match\"; \"tui theme items match\")]\n    #[test_case(&tui_binding_commands(), \"tui binding commands match\"; \"tui binding commands match\")]\n    #[test_case(&shell_completions(Shell::Bash).unwrap(), \"generate bash shell completions\"; \"generate bash shell completions\")]\n    #[test_case(&shell_completions(Shell::Elvish).unwrap(), \"generate elvish shell completions\"; \"generate elvish shell completions\")]\n    #[test_case(&shell_completions(Shell::Fish).unwrap(), \"generate fish shell completions\"; \"generate fish shell completions\")]\n    #[test_case(&shell_completions(Shell::PowerShell).unwrap(), \"generate powershell shell completions\"; \"generate powershell shell completions\")]\n    #[test_case(&shell_completions(Shell::Zsh).unwrap(), \"generate zsh shell completions\"; \"generate zsh shell completions\")]\n    #[test_case(&man_page().unwrap(), \"generate man page\"; \"generate man page\")]\n    fn test_output(actual: &str, name: &str) {\n        insta(name, || {\n            insta::assert_snapshot!(remove_whitespace(actual.to_string()));\n        });\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/report/csv.rs",
    "content": "use crate::app::TraceInfo;\nuse crate::report::types::fixed_width;\nuse itertools::Itertools;\nuse serde::Serialize;\nuse std::net::IpAddr;\nuse tracing::instrument;\nuse trippy_dns::Resolver;\n\n/// Generate a CSV report of trace data.\n#[instrument(skip_all, level = \"trace\")]\npub fn report<R: Resolver>(\n    info: &TraceInfo,\n    report_cycles: usize,\n    resolver: &R,\n) -> anyhow::Result<()> {\n    let trace = super::wait_for_round(&info.data, report_cycles)?;\n    let mut writer = csv::Writer::from_writer(std::io::stdout());\n    for hop in trace.hops() {\n        let row = CsvRow::new(\n            &info.target_hostname,\n            info.data.target_addr(),\n            hop,\n            resolver,\n        );\n        writer.serialize(row)?;\n    }\n    Ok(())\n}\n\n#[derive(Serialize)]\npub struct CsvRow {\n    #[serde(rename = \"Target\")]\n    pub target_hostname: String,\n    #[serde(rename = \"TargetIp\")]\n    pub target_addr: IpAddr,\n    #[serde(rename = \"Hop\")]\n    pub ttl: u8,\n    #[serde(rename = \"IPs\")]\n    pub ip: String,\n    #[serde(rename = \"Addrs\")]\n    pub host: String,\n    #[serde(rename = \"Loss%\")]\n    #[serde(serialize_with = \"fixed_width\")]\n    pub loss_pct: f64,\n    #[serde(rename = \"Snt\")]\n    pub sent: usize,\n    #[serde(rename = \"Recv\")]\n    pub recv: usize,\n    #[serde(rename = \"Last\")]\n    pub last: String,\n    #[serde(rename = \"Avg\")]\n    #[serde(serialize_with = \"fixed_width\")]\n    pub avg: f64,\n    #[serde(rename = \"Best\")]\n    pub best: String,\n    #[serde(rename = \"Wrst\")]\n    pub worst: String,\n    #[serde(rename = \"StdDev\")]\n    #[serde(serialize_with = \"fixed_width\")]\n    pub stddev: f64,\n}\n\nimpl CsvRow {\n    fn new<R: Resolver>(\n        target: &str,\n        target_addr: IpAddr,\n        hop: &trippy_core::Hop,\n        resolver: &R,\n    ) -> Self {\n        let ttl = hop.ttl();\n        let ips = hop.addrs().join(\":\");\n        let ip = if ips.is_empty() {\n            String::from(\"???\")\n        } else {\n            ips\n        };\n        let hosts = hop.addrs().map(|ip| resolver.reverse_lookup(*ip)).join(\":\");\n        let host = if hosts.is_empty() {\n            String::from(\"???\")\n        } else {\n            hosts\n        };\n        let sent = hop.total_sent();\n        let recv = hop.total_recv();\n        let last = hop\n            .last_ms()\n            .map_or_else(|| String::from(\"???\"), |last| format!(\"{last:.1}\"));\n        let best = hop\n            .best_ms()\n            .map_or_else(|| String::from(\"???\"), |best| format!(\"{best:.1}\"));\n        let worst = hop\n            .worst_ms()\n            .map_or_else(|| String::from(\"???\"), |worst| format!(\"{worst:.1}\"));\n        let stddev = hop.stddev_ms();\n        let avg = hop.avg_ms();\n        let loss_pct = hop.loss_pct();\n\n        Self {\n            target_hostname: String::from(target),\n            target_addr,\n            ttl,\n            ip,\n            host,\n            loss_pct,\n            sent,\n            last,\n            recv,\n            avg,\n            best,\n            worst,\n            stddev,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/report/dot.rs",
    "content": "use crate::app::TraceInfo;\nuse petgraph::dot::{Config, Dot};\nuse petgraph::graphmap::DiGraphMap;\nuse std::fmt::{Debug, Formatter};\nuse std::net::{IpAddr, Ipv4Addr};\nuse tracing::instrument;\nuse trippy_core::FlowEntry;\n\n/// Run a trace and generate a dot file.\n#[instrument(skip_all, level = \"trace\")]\npub fn report(info: &TraceInfo, report_cycles: usize) -> anyhow::Result<()> {\n    struct DotWrapper<'a>(Dot<'a, &'a DiGraphMap<IpAddr, ()>>);\n    impl Debug for DotWrapper<'_> {\n        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n            self.0.fmt(f)\n        }\n    }\n    super::wait_for_round(&info.data, report_cycles)?;\n    let trace = info.data.snapshot();\n    let mut graph: DiGraphMap<IpAddr, ()> = DiGraphMap::new();\n    for (flow, _id) in trace.flows() {\n        for (fst, snd) in flow.entries.windows(2).map(|pair| (pair[0], pair[1])) {\n            match (fst, snd) {\n                (FlowEntry::Known(addr1), FlowEntry::Known(addr2)) => {\n                    graph.add_edge(addr1, addr2, ());\n                }\n                (FlowEntry::Known(addr1), FlowEntry::Unknown) => {\n                    graph.add_edge(addr1, IpAddr::V4(Ipv4Addr::UNSPECIFIED), ());\n                }\n                (FlowEntry::Unknown, FlowEntry::Known(addr2)) => {\n                    graph.add_edge(IpAddr::V4(Ipv4Addr::UNSPECIFIED), addr2, ());\n                }\n                _ => {}\n            }\n        }\n    }\n    let dot = DotWrapper(Dot::with_config(&graph, &[Config::EdgeNoLabel]));\n    print!(\"{dot:?}\");\n    Ok(())\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/report/flows.rs",
    "content": "use crate::app::TraceInfo;\nuse tracing::instrument;\n\n/// Run a trace and report all flows observed.\n#[instrument(skip_all, level = \"trace\")]\npub fn report(info: &TraceInfo, report_cycles: usize) -> anyhow::Result<()> {\n    super::wait_for_round(&info.data, report_cycles)?;\n    let trace = info.data.snapshot();\n    for (flow, flow_id) in trace.flows() {\n        println!(\"flow {flow_id}: {flow}\");\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/report/json.rs",
    "content": "use crate::app::TraceInfo;\nuse crate::report::types::{Hop, Host, Info, Report};\nuse tracing::instrument;\nuse trippy_dns::Resolver;\n\n/// Generate a json report of trace data.\n#[instrument(skip_all, level = \"trace\")]\npub fn report<R: Resolver>(\n    info: &TraceInfo,\n    report_cycles: usize,\n    resolver: &R,\n) -> anyhow::Result<()> {\n    let start_timestamp = chrono::Utc::now();\n    let trace = super::wait_for_round(&info.data, report_cycles)?;\n    let end_timestamp = chrono::Utc::now();\n    let hops: Vec<Hop> = trace\n        .hops()\n        .iter()\n        .map(|hop| Hop::from((hop, resolver)))\n        .collect();\n    let report = Report {\n        info: Info {\n            target: Host {\n                ip: info.data.target_addr(),\n                hostname: info.target_hostname.clone(),\n            },\n            start_timestamp,\n            end_timestamp,\n        },\n        hops,\n    };\n    serde_json::to_writer_pretty(std::io::stdout(), &report)?;\n    Ok(())\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/report/silent.rs",
    "content": "use crate::app::TraceInfo;\nuse tracing::instrument;\n\n/// Run a trace without generating any output.\n#[instrument(skip_all, level = \"trace\")]\npub fn report(info: &TraceInfo, report_cycles: usize) -> anyhow::Result<()> {\n    super::wait_for_round(&info.data, report_cycles)?;\n    Ok(())\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/report/stream.rs",
    "content": "use crate::app::TraceInfo;\nuse crate::report::types::Hop;\nuse anyhow::anyhow;\nuse std::thread::sleep;\nuse tracing::instrument;\nuse trippy_dns::Resolver;\n\n/// Display a continuous stream of trace data.\n#[instrument(skip_all, level = \"trace\")]\npub fn report<R: Resolver>(info: &TraceInfo, resolver: &R) -> anyhow::Result<()> {\n    println!(\n        \"Tracing to {} ({})\",\n        info.target_hostname,\n        info.data.target_addr()\n    );\n    loop {\n        let trace_data = &info.data.snapshot();\n        if let Some(err) = trace_data.error() {\n            return Err(anyhow!(\"error: {err}\"));\n        }\n        for hop in trace_data.hops() {\n            let hop = Hop::from((hop, resolver));\n            let ttl = hop.ttl;\n            let addrs = hop.hosts.to_string();\n            let exts = hop.extensions.to_string();\n            let sent = hop.sent;\n            let recv = hop.recv;\n            let last = hop.last;\n            let best = hop.best;\n            let worst = hop.worst;\n            let stddev = hop.stddev;\n            let avg = hop.avg;\n            let loss_pct = hop.loss_pct;\n            println!(\n                \"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}\"\n            );\n        }\n        sleep(info.data.min_round_duration());\n    }\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/report/table.rs",
    "content": "use crate::app::TraceInfo;\nuse comfy_table::presets::{ASCII_MARKDOWN, UTF8_FULL};\nuse comfy_table::{ContentArrangement, Table};\nuse itertools::Itertools;\nuse tracing::instrument;\nuse trippy_dns::Resolver;\n\n/// Generate a Markdown table report of trace data.\n#[instrument(skip_all, level = \"trace\")]\npub fn report_md<R: Resolver>(\n    info: &TraceInfo,\n    report_cycles: usize,\n    resolver: &R,\n) -> anyhow::Result<()> {\n    run_report_table(info, report_cycles, resolver, ASCII_MARKDOWN)\n}\n\n/// Generate a pretty table report of trace data.\n#[instrument(skip_all, level = \"trace\")]\npub fn report_pretty<R: Resolver>(\n    info: &TraceInfo,\n    report_cycles: usize,\n    resolver: &R,\n) -> anyhow::Result<()> {\n    run_report_table(info, report_cycles, resolver, UTF8_FULL)\n}\n\nfn run_report_table<R: Resolver>(\n    info: &TraceInfo,\n    report_cycles: usize,\n    resolver: &R,\n    preset: &str,\n) -> anyhow::Result<()> {\n    let trace = super::wait_for_round(&info.data, report_cycles)?;\n    let columns = vec![\n        \"Hop\", \"IPs\", \"Addrs\", \"Loss%\", \"Snt\", \"Recv\", \"Last\", \"Avg\", \"Best\", \"Wrst\", \"StdDev\",\n    ];\n    let mut table = Table::new();\n    table\n        .load_preset(preset)\n        .set_content_arrangement(ContentArrangement::Dynamic)\n        .set_header(columns);\n    for hop in trace.hops() {\n        let ttl = hop.ttl().to_string();\n        let ips = hop.addrs().join(\"\\n\");\n        let ip = if ips.is_empty() {\n            String::from(\"???\")\n        } else {\n            ips\n        };\n        let hosts = hop\n            .addrs()\n            .map(|ip| resolver.reverse_lookup(*ip).to_string())\n            .join(\"\\n\");\n        let host = if hosts.is_empty() {\n            String::from(\"???\")\n        } else {\n            hosts\n        };\n        let sent = hop.total_sent().to_string();\n        let recv = hop.total_recv().to_string();\n        let last = hop\n            .last_ms()\n            .map_or_else(|| String::from(\"???\"), |last| format!(\"{last:.1}\"));\n        let best = hop\n            .best_ms()\n            .map_or_else(|| String::from(\"???\"), |best| format!(\"{best:.1}\"));\n        let worst = hop\n            .worst_ms()\n            .map_or_else(|| String::from(\"???\"), |worst| format!(\"{worst:.1}\"));\n        let stddev = format!(\"{:.1}\", hop.stddev_ms());\n        let avg = format!(\"{:.1}\", hop.avg_ms());\n        let loss_pct = format!(\"{:.1}\", hop.loss_pct());\n        table.add_row(vec![\n            &ttl, &ip, &host, &loss_pct, &sent, &recv, &last, &avg, &best, &worst, &stddev,\n        ]);\n    }\n    println!(\"{table}\");\n    Ok(())\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/report/types.rs",
    "content": "use chrono::Utc;\nuse itertools::Itertools;\nuse serde::{Serialize, Serializer};\nuse std::fmt::{Display, Formatter};\nuse std::net::IpAddr;\nuse trippy_core::NatStatus;\nuse trippy_dns::Resolver;\n\n#[derive(Serialize)]\npub struct Report {\n    pub info: Info,\n    pub hops: Vec<Hop>,\n}\n\n#[derive(Serialize)]\npub struct Info {\n    pub target: Host,\n    pub start_timestamp: chrono::DateTime<Utc>,\n    pub end_timestamp: chrono::DateTime<Utc>,\n}\n\n#[derive(Serialize)]\npub struct Hop {\n    pub ttl: u8,\n    pub hosts: Hosts,\n    pub extensions: Extensions,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub loss_pct: f64,\n    pub sent: usize,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub last: f64,\n    pub recv: usize,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub avg: f64,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub best: f64,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub worst: f64,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub stddev: f64,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub jitter: f64,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub javg: f64,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub jmax: f64,\n    #[serde(serialize_with = \"fixed_width\")]\n    pub jinta: f64,\n    pub nat: Option<bool>,\n    pub tos: u8,\n}\n\nimpl<R: Resolver> From<(&trippy_core::Hop, &R)> for Hop {\n    fn from((value, resolver): (&trippy_core::Hop, &R)) -> Self {\n        let hosts = Hosts::from((value.addrs(), resolver));\n        let extensions = value.extensions().map(Extensions::from).unwrap_or_default();\n        Self {\n            ttl: value.ttl(),\n            hosts,\n            extensions,\n            loss_pct: value.loss_pct(),\n            sent: value.total_sent(),\n            last: value.last_ms().unwrap_or_default(),\n            recv: value.total_recv(),\n            avg: value.avg_ms(),\n            best: value.best_ms().unwrap_or_default(),\n            worst: value.worst_ms().unwrap_or_default(),\n            stddev: value.stddev_ms(),\n            jitter: value.jitter_ms().unwrap_or_default(),\n            javg: value.javg_ms(),\n            jmax: value.jmax_ms().unwrap_or_default(),\n            jinta: value.jinta(),\n            nat: match value.last_nat_status() {\n                NatStatus::NotApplicable => None,\n                NatStatus::NotDetected => Some(false),\n                NatStatus::Detected => Some(true),\n            },\n            tos: value.tos().unwrap_or_default().0,\n        }\n    }\n}\n\n#[derive(Serialize)]\npub struct Hosts(pub Vec<Host>);\n\nimpl<'a, R: Resolver, I: Iterator<Item = &'a IpAddr>> From<(I, &R)> for Hosts {\n    fn from((value, resolver): (I, &R)) -> Self {\n        Self(\n            value\n                .map(|ip| Host {\n                    ip: *ip,\n                    hostname: resolver.reverse_lookup(*ip).to_string(),\n                })\n                .collect(),\n        )\n    }\n}\n\nimpl Display for Hosts {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.0.iter().format(\", \"))\n    }\n}\n\n#[derive(Serialize)]\npub struct Host {\n    pub ip: IpAddr,\n    pub hostname: String,\n}\n\nimpl Display for Host {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.ip)\n    }\n}\n\n#[derive(Default, Serialize)]\n#[serde(transparent)]\npub struct Extensions {\n    pub extensions: Vec<Extension>,\n}\n\nimpl From<&trippy_core::Extensions> for Extensions {\n    fn from(value: &trippy_core::Extensions) -> Self {\n        Self {\n            extensions: value\n                .extensions\n                .iter()\n                .cloned()\n                .map(Extension::from)\n                .collect(),\n        }\n    }\n}\n\nimpl Display for Extensions {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.extensions.iter().format(\" + \"))\n    }\n}\n\n#[derive(Serialize)]\npub enum Extension {\n    #[serde(rename = \"unknown\")]\n    Unknown(UnknownExtension),\n    #[serde(rename = \"mpls\")]\n    Mpls(MplsLabelStack),\n}\n\nimpl From<trippy_core::Extension> for Extension {\n    fn from(value: trippy_core::Extension) -> Self {\n        match value {\n            trippy_core::Extension::Unknown(unknown) => {\n                Self::Unknown(UnknownExtension::from(unknown))\n            }\n            trippy_core::Extension::Mpls(mpls) => Self::Mpls(MplsLabelStack::from(mpls)),\n        }\n    }\n}\n\nimpl Display for Extension {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Unknown(unknown) => unknown.fmt(f),\n            Self::Mpls(mpls) => mpls.fmt(f),\n        }\n    }\n}\n\n#[derive(Serialize)]\npub struct MplsLabelStack {\n    pub members: Vec<MplsLabelStackMember>,\n}\n\nimpl From<trippy_core::MplsLabelStack> for MplsLabelStack {\n    fn from(value: trippy_core::MplsLabelStack) -> Self {\n        Self {\n            members: value\n                .members\n                .into_iter()\n                .map(MplsLabelStackMember::from)\n                .collect(),\n        }\n    }\n}\n\nimpl Display for MplsLabelStack {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"mpls(labels={})\", self.members.iter().format(\", \"))\n    }\n}\n\n#[derive(Serialize)]\npub struct MplsLabelStackMember {\n    pub label: u32,\n    pub exp: u8,\n    pub bos: u8,\n    pub ttl: u8,\n}\n\nimpl From<trippy_core::MplsLabelStackMember> for MplsLabelStackMember {\n    fn from(value: trippy_core::MplsLabelStackMember) -> Self {\n        Self {\n            label: value.label,\n            exp: value.exp,\n            bos: value.bos,\n            ttl: value.ttl,\n        }\n    }\n}\n\nimpl Display for MplsLabelStackMember {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.label)\n    }\n}\n\n#[derive(Serialize)]\npub struct UnknownExtension {\n    pub class_num: u8,\n    pub class_subtype: u8,\n    pub bytes: Vec<u8>,\n}\n\nimpl From<trippy_core::UnknownExtension> for UnknownExtension {\n    fn from(value: trippy_core::UnknownExtension) -> Self {\n        Self {\n            class_num: value.class_num,\n            class_subtype: value.class_subtype,\n            bytes: value.bytes,\n        }\n    }\n}\n\nimpl Display for UnknownExtension {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(\n            f,\n            \"unknown(class={}, subtype={}, bytes=[{:02x}])\",\n            self.class_num,\n            self.class_subtype,\n            self.bytes.iter().format(\" \")\n        )\n    }\n}\n\n#[expect(clippy::trivially_copy_pass_by_ref)]\npub fn fixed_width<S>(val: &f64, serializer: S) -> Result<S::Ok, S::Error>\nwhere\n    S: Serializer,\n{\n    serializer.serialize_str(&format!(\"{val:.2}\"))\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/report.rs",
    "content": "use anyhow::anyhow;\nuse trippy_core::State;\nuse trippy_core::Tracer;\n\npub mod csv;\npub mod dot;\npub mod flows;\npub mod json;\npub mod silent;\npub mod stream;\npub mod table;\nmod types;\n\n/// Block until trace data for round `round` is available.\nfn wait_for_round(trace_data: &Tracer, report_cycles: usize) -> anyhow::Result<State> {\n    let mut trace = trace_data.snapshot();\n    while trace.round(State::default_flow_id()).is_none()\n        || trace.round(State::default_flow_id()) < Some(report_cycles - 1)\n    {\n        trace = trace_data.snapshot();\n        if let Some(err) = trace.error() {\n            return Err(anyhow!(\"error: {err}\"));\n        }\n    }\n    Ok(trace)\n}\n"
  },
  {
    "path": "crates/trippy-tui/src/util.rs",
    "content": "#[cfg(test)]\npub fn insta<F: FnOnce()>(name: &str, f: F) {\n    let mut settings = insta::Settings::new();\n    settings.set_snapshot_suffix(name.replace(' ', \"_\"));\n    settings.set_snapshot_path(\"../tests/resources/snapshots\");\n    settings.set_omit_expression(true);\n    settings.bind(f);\n}\n\n#[cfg(test)]\npub fn remove_whitespace(mut s: String) -> String {\n    s.retain(|c| !c.is_whitespace());\n    s\n}\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__config__tests__compare_snapshot@trip.snap",
    "content": "---\nsource: crates/trippy-tui/src/config.rs\n---\nAnetworkdiagnostictoolUsage:trip[OPTIONS][TARGETS]...Arguments:[TARGETS]...AspacedelimitedlistofhostnamesandIPstotrace[env:TRIP_TARGETS=]Options:-c,--config-file<CONFIG_FILE>Configfile[env:TRIP_CONFIG_FILE=]-m,--mode<MODE>Outputmode[default:tui][env:TRIP_MODE=][possiblevalues:tui,stream,pretty,markdown,csv,json,dot,flows,silent]-u,--unprivilegedTracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false][env:TRIP_UNPRIVILEGED=]-p,--protocol<PROTOCOL>Tracingprotocol[default:icmp][env:TRIP_PROTOCOL=][possiblevalues:icmp,udp,tcp]--udpTraceusingtheUDPprotocol[env:TRIP_UDP=]--tcpTraceusingtheTCPprotocol[env:TRIP_TCP=]--icmpTraceusingtheICMPprotocol[env:TRIP_ICMP=]-F,--addr-family<ADDR_FAMILY>Theaddressfamily[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-port<TARGET_PORT>Thetargetport(TCP&UDPonly)[default:80][env:TRIP_TARGET_PORT=]-S,--source-port<SOURCE_PORT>Thesourceport(TCP&UDPonly)[default:auto][env:TRIP_SOURCE_PORT=]-A,--source-address<SOURCE_ADDRESS>ThesourceIPaddress[default:auto][env:TRIP_SOURCE_ADDRESS=]-I,--interface<INTERFACE>Thenetworkinterface[default:auto][env:TRIP_INTERFACE=]-i,--min-round-duration<MIN_ROUND_DURATION>Theminimumdurationofeveryround[default:1s][env:TRIP_MIN_ROUND_DURATION=]-T,--max-round-duration<MAX_ROUND_DURATION>Themaximumdurationofeveryround[default:1s][env:TRIP_MAX_ROUND_DURATION=]-g,--grace-duration<GRACE_DURATION>TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms][env:TRIP_GRACE_DURATION=]--initial-sequence<INITIAL_SEQUENCE>Theinitialsequencenumber[default:33434][env:TRIP_INITIAL_SEQUENCE=]-R,--multipath-strategy<MULTIPATH_STRATEGY>TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic][env:TRIP_MULTIPATH_STRATEGY=][possiblevalues:classic,paris,dublin]-U,--max-inflight<MAX_INFLIGHT>Themaximumnumberofin-flightICMPechorequests[default:24][env:TRIP_MAX_INFLIGHT=]-f,--first-ttl<FIRST_TTL>TheTTLtostartfrom[default:1][env:TRIP_FIRST_TTL=]-t,--max-ttl<MAX_TTL>ThemaximumnumberofTTLhops[default:64][env:TRIP_MAX_TTL=]--packet-size<PACKET_SIZE>ThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84][env:TRIP_PACKET_SIZE=]--payload-pattern<PAYLOAD_PATTERN>TherepeatingpatterninthepayloadoftheICMPpacket[default:0][env:TRIP_PAYLOAD_PATTERN=]-Q,--tos<TOS>TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0][env:TRIP_TOS=]-e,--icmp-extensionsParseICMPextensions[env:TRIP_ICMP_EXTENSIONS=]--read-timeout<READ_TIMEOUT>Thesocketreadtimeout[default:10ms][env:TRIP_READ_TIMEOUT=]-r,--dns-resolve-method<DNS_RESOLVE_METHOD>HowtoperformDNSqueries[default:system][env:TRIP_DNS_RESOLVE_METHOD=][possiblevalues:system,resolv,google,cloudflare]-y,--dns-resolve-allTracetoallIPsresolvedfromDNSlookup[default:false][env:TRIP_DNS_RESOLVE_ALL=]--dns-timeout<DNS_TIMEOUT>ThemaximumtimetowaittoperformDNSqueries[default:5s][env:TRIP_DNS_TIMEOUT=]--dns-ttl<DNS_TTL>Thetime-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-samples<MAX_SAMPLES>Themaximumnumberofsamplestorecordperhop[default:256][env:TRIP_MAX_SAMPLES=]--max-flows<MAX_FLOWS>Themaximumnumberofflowstorecord[default:64][env:TRIP_MAX_FLOWS=]-a,--tui-address-mode<TUI_ADDRESS_MODE>Howtorenderaddresses[default:host][env:TRIP_TUI_ADDRESS_MODE=][possiblevalues:ip,host,both]--tui-as-mode<TUI_AS_MODE>Howtorenderautonomoussystem(AS)information[default:asn][env:TRIP_TUI_AS_MODE=][possiblevalues:asn,prefix,country-code,registry,allocated,name]--tui-custom-columns<TUI_CUSTOM_COLUMNS>CustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt][env:TRIP_TUI_CUSTOM_COLUMNS=]--tui-icmp-extension-mode<TUI_ICMP_EXTENSION_MODE>HowtorenderICMPextensions[default:off][env:TRIP_TUI_ICMP_EXTENSION_MODE=][possiblevalues:off,mpls,full,all]--tui-geoip-mode<TUI_GEOIP_MODE>HowtorenderGeoIpinformation[default:short][env:TRIP_TUI_GEOIP_MODE=][possiblevalues:off,short,long,location]-M,--tui-max-addrs<TUI_MAX_ADDRS>Themaximumnumberofaddressestoshowperhop[default:auto][env:TRIP_TUI_MAX_ADDRS=]--tui-preserve-screenPreservethescreenonexit[default:false][env:TRIP_TUI_PRESERVE_SCREEN=]--tui-refresh-rate<TUI_REFRESH_RATE>TheTUIrefreshrate[default:100ms][env:TRIP_TUI_REFRESH_RATE=]--tui-privacy-max-ttl<TUI_PRIVACY_MAX_TTL>Themaximumttlofhopswhichwillbemaskedforprivacy[default:none][env:TRIP_TUI_PRIVACY_MAX_TTL=]--tui-locale<TUI_LOCALE>ThelocaletousefortheTUI[default:auto][env:TRIP_TUI_LOCALE=]--tui-timezone<TUI_TIMEZONE>ThetimezonetousefortheTUI[default:auto][env:TRIP_TUI_TIMEZONE=]--tui-theme-colors<TUI_THEME_COLORS>TheTUIthemecolors[item=color,item=color,..][env:TRIP_TUI_THEME_COLORS=]--print-tui-theme-itemsPrintallTUIthemeitemsandexit[env:TRIP_PRINT_TUI_THEME_ITEMS=]--tui-key-bindings<TUI_KEY_BINDINGS>TheTUIkeybindings[command=key,command=key,..][env:TRIP_TUI_KEY_BINDINGS=]--print-tui-binding-commandsPrintallTUIcommandsthatcanbeboundandexit[env:TRIP_PRINT_TUI_BINDING_COMMANDS=]-C,--report-cycles<REPORT_CYCLES>Thenumberofreportcyclestorun[default:10][env:TRIP_REPORT_CYCLES=]-G,--geoip-mmdb-file<GEOIP_MMDB_FILE>ThesupportedMaxMindorIPinfoGeoIpmmdbfile[env:TRIP_GEOIP_MMDB_FILE=]--generate<GENERATE>Generateshellcompletion[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-format<LOG_FORMAT>Thedebuglogformat[default:pretty][env:TRIP_LOG_FORMAT=][possiblevalues:compact,pretty,json,chrome]--log-filter<LOG_FILTER>Thedebuglogfilter[default:trippy=debug][env:TRIP_LOG_FILTER=]--log-span-events<LOG_SPAN_EVENTS>Thedebuglogformat[default:off][env:TRIP_LOG_SPAN_EVENTS=][possiblevalues:off,active,full]-v,--verboseEnableverbosedebuglogging[env:TRIP_VERBOSE=]-h,--helpPrinthelp(seemorewith'--help')-V,--versionPrintversion\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__config__tests__compare_snapshot@trip_--help.snap",
    "content": "---\nsource: crates/trippy-tui/src/config.rs\n---\nAnetworkdiagnostictoolUsage:trip[OPTIONS][TARGETS]...Arguments:[TARGETS]...AspacedelimitedlistofhostnamesandIPstotrace[env:TRIP_TARGETS=]Options:-c,--config-file<CONFIG_FILE>Configfile[env:TRIP_CONFIG_FILE=]-m,--mode<MODE>Outputmode[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,--protocol<PROTOCOL>Tracingprotocol[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-family<ADDR_FAMILY>Theaddressfamily[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-port<TARGET_PORT>Thetargetport(TCP&UDPonly)[default:80][env:TRIP_TARGET_PORT=]-S,--source-port<SOURCE_PORT>Thesourceport(TCP&UDPonly)[default:auto][env:TRIP_SOURCE_PORT=]-A,--source-address<SOURCE_ADDRESS>ThesourceIPaddress[default:auto][env:TRIP_SOURCE_ADDRESS=]-I,--interface<INTERFACE>Thenetworkinterface[default:auto][env:TRIP_INTERFACE=]-i,--min-round-duration<MIN_ROUND_DURATION>Theminimumdurationofeveryround[default:1s][env:TRIP_MIN_ROUND_DURATION=]-T,--max-round-duration<MAX_ROUND_DURATION>Themaximumdurationofeveryround[default:1s][env:TRIP_MAX_ROUND_DURATION=]-g,--grace-duration<GRACE_DURATION>TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms][env:TRIP_GRACE_DURATION=]--initial-sequence<INITIAL_SEQUENCE>Theinitialsequencenumber[default:33434][env:TRIP_INITIAL_SEQUENCE=]-R,--multipath-strategy<MULTIPATH_STRATEGY>TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic]Possiblevalues:-classic:Thesrcordestportisusedtostorethesequencenumber-paris:TheUDP`checksum`fieldisusedtostorethesequencenumber-dublin:TheIP`identifier`fieldisusedtostorethesequencenumber[env:TRIP_MULTIPATH_STRATEGY=]-U,--max-inflight<MAX_INFLIGHT>Themaximumnumberofin-flightICMPechorequests[default:24][env:TRIP_MAX_INFLIGHT=]-f,--first-ttl<FIRST_TTL>TheTTLtostartfrom[default:1][env:TRIP_FIRST_TTL=]-t,--max-ttl<MAX_TTL>ThemaximumnumberofTTLhops[default:64][env:TRIP_MAX_TTL=]--packet-size<PACKET_SIZE>ThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84][env:TRIP_PACKET_SIZE=]--payload-pattern<PAYLOAD_PATTERN>TherepeatingpatterninthepayloadoftheICMPpacket[default:0][env:TRIP_PAYLOAD_PATTERN=]-Q,--tos<TOS>TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0][env:TRIP_TOS=]-e,--icmp-extensionsParseICMPextensions[env:TRIP_ICMP_EXTENSIONS=]--read-timeout<READ_TIMEOUT>Thesocketreadtimeout[default:10ms][env:TRIP_READ_TIMEOUT=]-r,--dns-resolve-method<DNS_RESOLVE_METHOD>HowtoperformDNSqueries[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-timeout<DNS_TIMEOUT>ThemaximumtimetowaittoperformDNSqueries[default:5s][env:TRIP_DNS_TIMEOUT=]--dns-ttl<DNS_TTL>Thetime-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-samples<MAX_SAMPLES>Themaximumnumberofsamplestorecordperhop[default:256][env:TRIP_MAX_SAMPLES=]--max-flows<MAX_FLOWS>Themaximumnumberofflowstorecord[default:64][env:TRIP_MAX_FLOWS=]-a,--tui-address-mode<TUI_ADDRESS_MODE>Howtorenderaddresses[default:host]Possiblevalues:-ip:ShowIPaddressonly-host:Showreverse-lookupDNShostnameonly-both:ShowbothIPaddressandreverse-lookupDNShostname[env:TRIP_TUI_ADDRESS_MODE=]--tui-as-mode<TUI_AS_MODE>Howtorenderautonomoussystem(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-columns<TUI_CUSTOM_COLUMNS>CustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt][env:TRIP_TUI_CUSTOM_COLUMNS=]--tui-icmp-extension-mode<TUI_ICMP_EXTENSION_MODE>HowtorenderICMPextensions[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-mode<TUI_GEOIP_MODE>HowtorenderGeoIpinformation[default:short]Possiblevalues:-off:DonotdisplayGeoIpdata-short:Showshortformat-long:Showlongformat-location:ShowlatitudeandLongitudeformat[env:TRIP_TUI_GEOIP_MODE=]-M,--tui-max-addrs<TUI_MAX_ADDRS>Themaximumnumberofaddressestoshowperhop[default:auto][env:TRIP_TUI_MAX_ADDRS=]--tui-preserve-screenPreservethescreenonexit[default:false][env:TRIP_TUI_PRESERVE_SCREEN=]--tui-refresh-rate<TUI_REFRESH_RATE>TheTUIrefreshrate[default:100ms][env:TRIP_TUI_REFRESH_RATE=]--tui-privacy-max-ttl<TUI_PRIVACY_MAX_TTL>Themaximumttlofhopswhichwillbemaskedforprivacy[default:none]Ifset,thesourceIPaddressandhostnamewillalsobehidden.[env:TRIP_TUI_PRIVACY_MAX_TTL=]--tui-locale<TUI_LOCALE>ThelocaletousefortheTUI[default:auto][env:TRIP_TUI_LOCALE=]--tui-timezone<TUI_TIMEZONE>ThetimezonetousefortheTUI[default:auto]ThetimezonemustbeavalidIANAtimezoneidentifier.[env:TRIP_TUI_TIMEZONE=]--tui-theme-colors<TUI_THEME_COLORS>TheTUIthemecolors[item=color,item=color,..][env:TRIP_TUI_THEME_COLORS=]--print-tui-theme-itemsPrintallTUIthemeitemsandexit[env:TRIP_PRINT_TUI_THEME_ITEMS=]--tui-key-bindings<TUI_KEY_BINDINGS>TheTUIkeybindings[command=key,command=key,..][env:TRIP_TUI_KEY_BINDINGS=]--print-tui-binding-commandsPrintallTUIcommandsthatcanbeboundandexit[env:TRIP_PRINT_TUI_BINDING_COMMANDS=]-C,--report-cycles<REPORT_CYCLES>Thenumberofreportcyclestorun[default:10][env:TRIP_REPORT_CYCLES=]-G,--geoip-mmdb-file<GEOIP_MMDB_FILE>ThesupportedMaxMindorIPinfoGeoIpmmdbfile[env:TRIP_GEOIP_MMDB_FILE=]--generate<GENERATE>Generateshellcompletion[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-format<LOG_FORMAT>Thedebuglogformat[default:pretty]Possiblevalues:-compact:Displaylogdatainacompactformat-pretty:Displaylogdatainaprettyformat-json:Displaylogdatainajsonformat-chrome:DisplaylogdatainChrometraceformat[env:TRIP_LOG_FORMAT=]--log-filter<LOG_FILTER>Thedebuglogfilter[default:trippy=debug][env:TRIP_LOG_FILTER=]--log-span-events<LOG_SPAN_EVENTS>Thedebuglogformat[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\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__config__tests__compare_snapshot@trip_-h.snap",
    "content": "---\nsource: crates/trippy-tui/src/config.rs\n---\nAnetworkdiagnostictoolUsage:trip[OPTIONS][TARGETS]...Arguments:[TARGETS]...AspacedelimitedlistofhostnamesandIPstotrace[env:TRIP_TARGETS=]Options:-c,--config-file<CONFIG_FILE>Configfile[env:TRIP_CONFIG_FILE=]-m,--mode<MODE>Outputmode[default:tui][env:TRIP_MODE=][possiblevalues:tui,stream,pretty,markdown,csv,json,dot,flows,silent]-u,--unprivilegedTracewithoutrequiringelevatedprivilegesonsupportedplatforms[default:false][env:TRIP_UNPRIVILEGED=]-p,--protocol<PROTOCOL>Tracingprotocol[default:icmp][env:TRIP_PROTOCOL=][possiblevalues:icmp,udp,tcp]--udpTraceusingtheUDPprotocol[env:TRIP_UDP=]--tcpTraceusingtheTCPprotocol[env:TRIP_TCP=]--icmpTraceusingtheICMPprotocol[env:TRIP_ICMP=]-F,--addr-family<ADDR_FAMILY>Theaddressfamily[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-port<TARGET_PORT>Thetargetport(TCP&UDPonly)[default:80][env:TRIP_TARGET_PORT=]-S,--source-port<SOURCE_PORT>Thesourceport(TCP&UDPonly)[default:auto][env:TRIP_SOURCE_PORT=]-A,--source-address<SOURCE_ADDRESS>ThesourceIPaddress[default:auto][env:TRIP_SOURCE_ADDRESS=]-I,--interface<INTERFACE>Thenetworkinterface[default:auto][env:TRIP_INTERFACE=]-i,--min-round-duration<MIN_ROUND_DURATION>Theminimumdurationofeveryround[default:1s][env:TRIP_MIN_ROUND_DURATION=]-T,--max-round-duration<MAX_ROUND_DURATION>Themaximumdurationofeveryround[default:1s][env:TRIP_MAX_ROUND_DURATION=]-g,--grace-duration<GRACE_DURATION>TheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms][env:TRIP_GRACE_DURATION=]--initial-sequence<INITIAL_SEQUENCE>Theinitialsequencenumber[default:33434][env:TRIP_INITIAL_SEQUENCE=]-R,--multipath-strategy<MULTIPATH_STRATEGY>TheEqual-costMulti-Pathroutingstrategy(UDPonly)[default:classic][env:TRIP_MULTIPATH_STRATEGY=][possiblevalues:classic,paris,dublin]-U,--max-inflight<MAX_INFLIGHT>Themaximumnumberofin-flightICMPechorequests[default:24][env:TRIP_MAX_INFLIGHT=]-f,--first-ttl<FIRST_TTL>TheTTLtostartfrom[default:1][env:TRIP_FIRST_TTL=]-t,--max-ttl<MAX_TTL>ThemaximumnumberofTTLhops[default:64][env:TRIP_MAX_TTL=]--packet-size<PACKET_SIZE>ThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84][env:TRIP_PACKET_SIZE=]--payload-pattern<PAYLOAD_PATTERN>TherepeatingpatterninthepayloadoftheICMPpacket[default:0][env:TRIP_PAYLOAD_PATTERN=]-Q,--tos<TOS>TheTOS(i.e.DSCP+ECN)IPheadervalue(IPv4only)[default:0][env:TRIP_TOS=]-e,--icmp-extensionsParseICMPextensions[env:TRIP_ICMP_EXTENSIONS=]--read-timeout<READ_TIMEOUT>Thesocketreadtimeout[default:10ms][env:TRIP_READ_TIMEOUT=]-r,--dns-resolve-method<DNS_RESOLVE_METHOD>HowtoperformDNSqueries[default:system][env:TRIP_DNS_RESOLVE_METHOD=][possiblevalues:system,resolv,google,cloudflare]-y,--dns-resolve-allTracetoallIPsresolvedfromDNSlookup[default:false][env:TRIP_DNS_RESOLVE_ALL=]--dns-timeout<DNS_TIMEOUT>ThemaximumtimetowaittoperformDNSqueries[default:5s][env:TRIP_DNS_TIMEOUT=]--dns-ttl<DNS_TTL>Thetime-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-samples<MAX_SAMPLES>Themaximumnumberofsamplestorecordperhop[default:256][env:TRIP_MAX_SAMPLES=]--max-flows<MAX_FLOWS>Themaximumnumberofflowstorecord[default:64][env:TRIP_MAX_FLOWS=]-a,--tui-address-mode<TUI_ADDRESS_MODE>Howtorenderaddresses[default:host][env:TRIP_TUI_ADDRESS_MODE=][possiblevalues:ip,host,both]--tui-as-mode<TUI_AS_MODE>Howtorenderautonomoussystem(AS)information[default:asn][env:TRIP_TUI_AS_MODE=][possiblevalues:asn,prefix,country-code,registry,allocated,name]--tui-custom-columns<TUI_CUSTOM_COLUMNS>CustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt][env:TRIP_TUI_CUSTOM_COLUMNS=]--tui-icmp-extension-mode<TUI_ICMP_EXTENSION_MODE>HowtorenderICMPextensions[default:off][env:TRIP_TUI_ICMP_EXTENSION_MODE=][possiblevalues:off,mpls,full,all]--tui-geoip-mode<TUI_GEOIP_MODE>HowtorenderGeoIpinformation[default:short][env:TRIP_TUI_GEOIP_MODE=][possiblevalues:off,short,long,location]-M,--tui-max-addrs<TUI_MAX_ADDRS>Themaximumnumberofaddressestoshowperhop[default:auto][env:TRIP_TUI_MAX_ADDRS=]--tui-preserve-screenPreservethescreenonexit[default:false][env:TRIP_TUI_PRESERVE_SCREEN=]--tui-refresh-rate<TUI_REFRESH_RATE>TheTUIrefreshrate[default:100ms][env:TRIP_TUI_REFRESH_RATE=]--tui-privacy-max-ttl<TUI_PRIVACY_MAX_TTL>Themaximumttlofhopswhichwillbemaskedforprivacy[default:none][env:TRIP_TUI_PRIVACY_MAX_TTL=]--tui-locale<TUI_LOCALE>ThelocaletousefortheTUI[default:auto][env:TRIP_TUI_LOCALE=]--tui-timezone<TUI_TIMEZONE>ThetimezonetousefortheTUI[default:auto][env:TRIP_TUI_TIMEZONE=]--tui-theme-colors<TUI_THEME_COLORS>TheTUIthemecolors[item=color,item=color,..][env:TRIP_TUI_THEME_COLORS=]--print-tui-theme-itemsPrintallTUIthemeitemsandexit[env:TRIP_PRINT_TUI_THEME_ITEMS=]--tui-key-bindings<TUI_KEY_BINDINGS>TheTUIkeybindings[command=key,command=key,..][env:TRIP_TUI_KEY_BINDINGS=]--print-tui-binding-commandsPrintallTUIcommandsthatcanbeboundandexit[env:TRIP_PRINT_TUI_BINDING_COMMANDS=]-C,--report-cycles<REPORT_CYCLES>Thenumberofreportcyclestorun[default:10][env:TRIP_REPORT_CYCLES=]-G,--geoip-mmdb-file<GEOIP_MMDB_FILE>ThesupportedMaxMindorIPinfoGeoIpmmdbfile[env:TRIP_GEOIP_MMDB_FILE=]--generate<GENERATE>Generateshellcompletion[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-format<LOG_FORMAT>Thedebuglogformat[default:pretty][env:TRIP_LOG_FORMAT=][possiblevalues:compact,pretty,json,chrome]--log-filter<LOG_FILTER>Thedebuglogfilter[default:trippy=debug][env:TRIP_LOG_FILTER=]--log-span-events<LOG_SPAN_EVENTS>Thedebuglogformat[default:off][env:TRIP_LOG_SPAN_EVENTS=][possiblevalues:off,active,full]-v,--verboseEnableverbosedebuglogging[env:TRIP_VERBOSE=]-h,--helpPrinthelp(seemorewith'--help')-V,--versionPrintversion\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_bash_shell_completions.snap",
    "content": "---\nsource: crates/trippy-tui/src/print.rs\n---\n_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\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_elvish_shell_completions.snap",
    "content": "---\nsource: crates/trippy-tui/src/print.rs\n---\nusebuiltin;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]}\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_fish_shell_completions.snap",
    "content": "---\nsource: crates/trippy-tui/src/print.rs\n---\ncomplete-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'\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_man_page.snap",
    "content": "---\nsource: crates/trippy-tui/src/print.rs\n---\n.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<CONFIG_FILE>\\fRConfigfile.RSMayalsobespecifiedwiththe\\fBTRIP_CONFIG_FILE\\fRenvironmentvariable..RE.TP\\fB\\-m\\fR,\\fB\\-\\-mode\\fR\\fI<MODE>\\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<PROTOCOL>\\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<ADDR_FAMILY>\\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<TARGET_PORT>\\fRThetargetport(TCP&UDPonly)[default:80].RSMayalsobespecifiedwiththe\\fBTRIP_TARGET_PORT\\fRenvironmentvariable..RE.TP\\fB\\-S\\fR,\\fB\\-\\-source\\-port\\fR\\fI<SOURCE_PORT>\\fRThesourceport(TCP&UDPonly)[default:auto].RSMayalsobespecifiedwiththe\\fBTRIP_SOURCE_PORT\\fRenvironmentvariable..RE.TP\\fB\\-A\\fR,\\fB\\-\\-source\\-address\\fR\\fI<SOURCE_ADDRESS>\\fRThesourceIPaddress[default:auto].RSMayalsobespecifiedwiththe\\fBTRIP_SOURCE_ADDRESS\\fRenvironmentvariable..RE.TP\\fB\\-I\\fR,\\fB\\-\\-interface\\fR\\fI<INTERFACE>\\fRThenetworkinterface[default:auto].RSMayalsobespecifiedwiththe\\fBTRIP_INTERFACE\\fRenvironmentvariable..RE.TP\\fB\\-i\\fR,\\fB\\-\\-min\\-round\\-duration\\fR\\fI<MIN_ROUND_DURATION>\\fRTheminimumdurationofeveryround[default:1s].RSMayalsobespecifiedwiththe\\fBTRIP_MIN_ROUND_DURATION\\fRenvironmentvariable..RE.TP\\fB\\-T\\fR,\\fB\\-\\-max\\-round\\-duration\\fR\\fI<MAX_ROUND_DURATION>\\fRThemaximumdurationofeveryround[default:1s].RSMayalsobespecifiedwiththe\\fBTRIP_MAX_ROUND_DURATION\\fRenvironmentvariable..RE.TP\\fB\\-g\\fR,\\fB\\-\\-grace\\-duration\\fR\\fI<GRACE_DURATION>\\fRTheperiodoftimetowaitforadditionalICMPresponsesafterthetargethasresponded[default:100ms].RSMayalsobespecifiedwiththe\\fBTRIP_GRACE_DURATION\\fRenvironmentvariable..RE.TP\\fB\\-\\-initial\\-sequence\\fR\\fI<INITIAL_SEQUENCE>\\fRTheinitialsequencenumber[default:33434].RSMayalsobespecifiedwiththe\\fBTRIP_INITIAL_SEQUENCE\\fRenvironmentvariable..RE.TP\\fB\\-R\\fR,\\fB\\-\\-multipath\\-strategy\\fR\\fI<MULTIPATH_STRATEGY>\\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<MAX_INFLIGHT>\\fRThemaximumnumberofin\\-flightICMPechorequests[default:24].RSMayalsobespecifiedwiththe\\fBTRIP_MAX_INFLIGHT\\fRenvironmentvariable..RE.TP\\fB\\-f\\fR,\\fB\\-\\-first\\-ttl\\fR\\fI<FIRST_TTL>\\fRTheTTLtostartfrom[default:1].RSMayalsobespecifiedwiththe\\fBTRIP_FIRST_TTL\\fRenvironmentvariable..RE.TP\\fB\\-t\\fR,\\fB\\-\\-max\\-ttl\\fR\\fI<MAX_TTL>\\fRThemaximumnumberofTTLhops[default:64].RSMayalsobespecifiedwiththe\\fBTRIP_MAX_TTL\\fRenvironmentvariable..RE.TP\\fB\\-\\-packet\\-size\\fR\\fI<PACKET_SIZE>\\fRThesizeofIPpackettosend(IPheader+ICMPheader+payload)[default:84].RSMayalsobespecifiedwiththe\\fBTRIP_PACKET_SIZE\\fRenvironmentvariable..RE.TP\\fB\\-\\-payload\\-pattern\\fR\\fI<PAYLOAD_PATTERN>\\fRTherepeatingpatterninthepayloadoftheICMPpacket[default:0].RSMayalsobespecifiedwiththe\\fBTRIP_PAYLOAD_PATTERN\\fRenvironmentvariable..RE.TP\\fB\\-Q\\fR,\\fB\\-\\-tos\\fR\\fI<TOS>\\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<READ_TIMEOUT>\\fRThesocketreadtimeout[default:10ms].RSMayalsobespecifiedwiththe\\fBTRIP_READ_TIMEOUT\\fRenvironmentvariable..RE.TP\\fB\\-r\\fR,\\fB\\-\\-dns\\-resolve\\-method\\fR\\fI<DNS_RESOLVE_METHOD>\\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<DNS_TIMEOUT>\\fRThemaximumtimetowaittoperformDNSqueries[default:5s].RSMayalsobespecifiedwiththe\\fBTRIP_DNS_TIMEOUT\\fRenvironmentvariable..RE.TP\\fB\\-\\-dns\\-ttl\\fR\\fI<DNS_TTL>\\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<MAX_SAMPLES>\\fRThemaximumnumberofsamplestorecordperhop[default:256].RSMayalsobespecifiedwiththe\\fBTRIP_MAX_SAMPLES\\fRenvironmentvariable..RE.TP\\fB\\-\\-max\\-flows\\fR\\fI<MAX_FLOWS>\\fRThemaximumnumberofflowstorecord[default:64].RSMayalsobespecifiedwiththe\\fBTRIP_MAX_FLOWS\\fRenvironmentvariable..RE.TP\\fB\\-a\\fR,\\fB\\-\\-tui\\-address\\-mode\\fR\\fI<TUI_ADDRESS_MODE>\\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<TUI_AS_MODE>\\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<TUI_CUSTOM_COLUMNS>\\fRCustomcolumnstobedisplayedintheTUIhopstable[default:holsravbwdt].RSMayalsobespecifiedwiththe\\fBTRIP_TUI_CUSTOM_COLUMNS\\fRenvironmentvariable..RE.TP\\fB\\-\\-tui\\-icmp\\-extension\\-mode\\fR\\fI<TUI_ICMP_EXTENSION_MODE>\\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<TUI_GEOIP_MODE>\\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<TUI_MAX_ADDRS>\\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<TUI_REFRESH_RATE>\\fRTheTUIrefreshrate[default:100ms].RSMayalsobespecifiedwiththe\\fBTRIP_TUI_REFRESH_RATE\\fRenvironmentvariable..RE.TP\\fB\\-\\-tui\\-privacy\\-max\\-ttl\\fR\\fI<TUI_PRIVACY_MAX_TTL>\\fRThemaximumttlofhopswhichwillbemaskedforprivacy[default:none]Ifset,thesourceIPaddressandhostnamewillalsobehidden..RSMayalsobespecifiedwiththe\\fBTRIP_TUI_PRIVACY_MAX_TTL\\fRenvironmentvariable..RE.TP\\fB\\-\\-tui\\-locale\\fR\\fI<TUI_LOCALE>\\fRThelocaletousefortheTUI[default:auto].RSMayalsobespecifiedwiththe\\fBTRIP_TUI_LOCALE\\fRenvironmentvariable..RE.TP\\fB\\-\\-tui\\-timezone\\fR\\fI<TUI_TIMEZONE>\\fRThetimezonetousefortheTUI[default:auto]ThetimezonemustbeavalidIANAtimezoneidentifier..RSMayalsobespecifiedwiththe\\fBTRIP_TUI_TIMEZONE\\fRenvironmentvariable..RE.TP\\fB\\-\\-tui\\-theme\\-colors\\fR\\fI<TUI_THEME_COLORS>\\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<TUI_KEY_BINDINGS>\\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<REPORT_CYCLES>\\fRThenumberofreportcyclestorun[default:10].RSMayalsobespecifiedwiththe\\fBTRIP_REPORT_CYCLES\\fRenvironmentvariable..RE.TP\\fB\\-G\\fR,\\fB\\-\\-geoip\\-mmdb\\-file\\fR\\fI<GEOIP_MMDB_FILE>\\fRThesupportedMaxMindorIPinfoGeoIpmmdbfile.RSMayalsobespecifiedwiththe\\fBTRIP_GEOIP_MMDB_FILE\\fRenvironmentvariable..RE.TP\\fB\\-\\-generate\\fR\\fI<GENERATE>\\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<LOG_FORMAT>\\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<LOG_FILTER>\\fRThedebuglogfilter[default:trippy=debug].RSMayalsobespecifiedwiththe\\fBTRIP_LOG_FILTER\\fRenvironmentvariable..RE.TP\\fB\\-\\-log\\-span\\-events\\fR\\fI<LOG_SPAN_EVENTS>\\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<fujiapple852@gmail.com>\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_powershell_shell_completions.snap",
    "content": "---\nsource: crates/trippy-tui/src/print.rs\n---\nusingnamespaceSystem.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}\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@generate_zsh_shell_completions.snap",
    "content": "---\nsource: crates/trippy-tui/src/print.rs\n---\n#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\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@tui_binding_commands_match.snap",
    "content": "---\nsource: crates/trippy-tui/src/print.rs\n---\nTUIbindingcommands: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\n"
  },
  {
    "path": "crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@tui_theme_items_match.snap",
    "content": "---\nsource: crates/trippy-tui/src/print.rs\n---\nTUIthemecoloritems: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\n"
  },
  {
    "path": "deny.toml",
    "content": "[licenses]\nversion = 2\nallow = [\"Apache-2.0\", \"MIT\", \"Unicode-DFS-2016\", \"ISC\", \"BSD-2-Clause\", \"BSD-3-Clause\", \"WTFPL\", \"Unicode-3.0\", \"Zlib\"]\nconfidence-threshold = 0.8\nexceptions = []\n\n[advisories]\nversion = 2\ndb-path = \"~/.cargo/advisory-db\"\ndb-urls = [\"https://github.com/rustsec/advisory-db\"]\nignore = [\n  # allow unmaintained paste crate\n  { id = \"RUSTSEC-2024-0436\" },\n]\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "# build output\ndist/\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n\n# environment variables\n.env\n.env.production\n\n# macOS-specific files\n.DS_Store\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Starlight Starter Kit: Basics\n\n[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)\n\n```\nnpm create astro@latest -- --template starlight\n```\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)\n[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)\n[![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)\n[![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)\n\n> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!\n\n## 🚀 Project Structure\n\nInside of your Astro + Starlight project, you'll see the following folders and files:\n\n```\n.\n├── public/\n├── src/\n│   ├── assets/\n│   ├── content/\n│   │   ├── docs/\n│   │   └── config.ts\n│   └── env.d.ts\n├── astro.config.mjs\n├── package.json\n└── tsconfig.json\n```\n\nStarlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.\n\nImages can be added to `src/assets/` and embedded in Markdown with a relative link.\n\nStatic assets, like favicons, can be placed in the `public/` directory.\n\n## 🧞 Commands\n\nAll commands are run from the root of the project, from a terminal:\n\n| Command                   | Action                                           |\n| :------------------------ | :----------------------------------------------- |\n| `npm install`             | Installs dependencies                            |\n| `npm run dev`             | Starts local dev server at `localhost:4321`      |\n| `npm run build`           | Build your production site to `./dist/`          |\n| `npm run preview`         | Preview your build locally, before deploying     |\n| `npm run astro ...`       | Run CLI commands like `astro add`, `astro check` |\n| `npm run astro -- --help` | Get help using the Astro CLI                     |\n\n## 👀 Want to learn more?\n\nCheck 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).\n"
  },
  {
    "path": "docs/astro.config.mjs",
    "content": "import { defineConfig } from 'astro/config';\nimport starlight from '@astrojs/starlight';\nimport starlightVersions from 'starlight-versions'\n\n// https://astro.build/config\nexport default defineConfig({\n    site: 'https://trippy.rs',\n    integrations: [\n        starlight({\n            plugins: [\n              starlightVersions({\n                versions: [{ slug: '0.12.2' }, { slug: '0.13.0' }],\n              }),\n            ],\n            title: 'Trippy',\n            customCss: [\n              // Relative path to your custom CSS file\n              './src/styles/custom.css',\n            ],\n             editLink: {\n               baseUrl: 'https://github.com/fujiapple852/trippy/edit/master/docs/',\n             },\n            logo: {\n              light: './src/assets/Trippy-Horizontal.svg',\n              dark: './src/assets/Trippy-Horizontal-DarkMode.svg',\n              replacesTitle: true,\n            },\n            head: [\n              {\n                tag: 'link',\n                attrs: {\n                  rel: 'apple-touch-icon',\n                  href: '/apple-touch-icon.png',\n                },\n              },\n              {\n                tag: 'script',\n                attrs: {\n                  defer: true,\n                  src: 'https://cloud.umami.is/script.js',\n                  'data-website-id': '02e6fe53-a5b1-4f2a-b3e6-87124b1b276b',\n                  'data-astro-rerun': true\n                }\n              }\n            ],\n            social: [\n                { icon: 'github', label: 'github', href: 'https://github.com/fujiapple852/trippy' },\n                { icon: 'zulip', label: 'zulip', href: 'https://trippy.zulipchat.com' },\n                { icon: 'matrix', label: 'matrix', href: 'https://matrix.to/#/#trippy-dev:matrix.org' },\n                { icon: 'x.com', label: 'x.com', href: 'https://x.com/FujiApple852v2' },\n            ],\n            sidebar: [\n                {\n                    label: 'Start Here',\n                    autogenerate: { directory: 'start' }\n                },\n                {\n                    label: 'Guides',\n                    autogenerate: { directory: 'guides' }\n                },\n                {\n                    label: 'Reference',\n                    autogenerate: { directory: 'reference' },\n                },\n                {\n                    label: 'Development',\n                    autogenerate: { directory: 'development' },\n                },\n            ],\n        }),\n    ],\n});\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"trippy\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"start\": \"astro dev\",\n    \"build\": \"astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\"\n  },\n  \"dependencies\": {\n    \"@astrojs/starlight\": \"^0.34.3\",\n    \"astro\": \"^5.8.1\",\n    \"sharp\": \"^0.32.5\",\n    \"starlight-versions\": \"^0.5.3\"\n  }\n}\n"
  },
  {
    "path": "docs/public/CNAME",
    "content": "trippy.rs\n"
  },
  {
    "path": "docs/src/content/config.ts",
    "content": "import { defineCollection } from 'astro:content';\nimport { docsSchema } from '@astrojs/starlight/schema';\nimport { docsVersionsLoader } from 'starlight-versions/loader'\n\nexport const collections = {\n    docs: defineCollection({ schema: docsSchema() }),\n    versions: defineCollection({ loader: docsVersionsLoader() }),\n};\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/development/crates.md",
    "content": "---\ntitle: Crates\ndescription: A reference for the Trippy crates.\nslug: 0.12.2/development/crates\n---\n\nThe following table lists the crates that are provided by Trippy. See [crates](crates/README.md) for more information.\n\n| Crate                                                         | Description                                                                         |\n| ------------------------------------------------------------- | ----------------------------------------------------------------------------------- |\n| [trippy](https://crates.io/crates/trippy)                     | A binary crate for the Trippy application and a library crate                       |\n| [trippy-core](https://crates.io/crates/trippy-core)           | A library crate providing the core Trippy tracing functionality                     |\n| [trippy-packet](https://crates.io/crates/trippy-packet)       | A library crate which provides packet wire formats and packet parsing functionality |\n| [trippy-dns](https://crates.io/crates/trippy-dns)             | A library crate for performing forward and reverse lazy DNS resolution              |\n| [trippy-privilege](https://crates.io/crates/trippy-privilege) | A library crate for discovering platform privileges                                 |\n| [trippy-tui](https://crates.io/crates/trippy-tui)             | A library crate for the Trippy terminal user interface                              |\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/guides/faq.md",
    "content": "---\ntitle: Frequently Asked Questions\ndescription: Frequently asked questions about Trippy.\nsidebar:\n  order: 5\nslug: 0.12.2/guides/faq\n---\n\n## Why does Trippy show \"Awaiting data...\"?\n\n:::caution\nIf you are using Windows you _must_ [configure](/guides/windows_firewall)\nthe Windows Defender firewall to allow incoming ICMP traffic\n:::\n\nWhen Trippy shows “Awaiting data...” it means that it has received zero responses for the probes sent in a trace. This\nindicates that either probes are not being sent or, more typically, responses are not being received.\n\nCheck that local and network firewalls allow ICMP traffic and that the system `traceroute` (or `tracert.exe` on\nWindows) works as expected. Note that on Windows, even if `tracert.exe` works as expected, you\n_must_ [configure](/guides/windows_firewall) the Windows Defender\nfirewall to allow incoming ICMP traffic.\n\nFor deeper diagnostics you can run tools such as https://www.wireshark.org and https://www.tcpdump.org to verify that\nicmp requests and responses are being send and received.\n\n<a name=\"windows-defender\"></a>\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/guides/privileges.md",
    "content": "---\ntitle: Privileges\ndescription: A reference for the Trippy privileges.\nsidebar:\n  order: 2\nslug: 0.12.2/guides/privileges\n---\n\nTrippy normally requires elevated privileges due to the use of raw sockets. Enabling the required privileges for your\nplatform can be achieved in several ways, as outlined below. Trippy can also be used without elevated privileged on\ncertain platforms, with some limitations.\n\n## Unix\n\n1: Run as `root` user via `sudo`:\n\n```shell\nsudo trip example.com\n```\n\n2: `chown` `trip` as the `root` user and set the `setuid` bit:\n\n```shell\nsudo chown root $(which trip) && sudo chmod +s $(which trip)\n```\n\n3: [Linux only] Set the `CAP_NET_RAW` capability:\n\n```shell\nsudo setcap CAP_NET_RAW+p $(which trip)\n```\n\n:::note\nTrippy is a capability aware application and will add `CAP_NET_RAW` to the effective set if it is present in the allowed\nset. Trippy will drop all capabilities after creating the raw sockets.\n:::\n\n## Windows\n\nTrippy must be run with Administrator privileges on Windows.\n\n## Unprivileged mode\n\nTrippy allows running in an unprivileged mode for all tracing modes (`ICMP`, `UDP` and `TCP`) on platforms which support\nthat feature.\n\n:::note\nUnprivileged mode is currently only supported on macOS. Linux support is possible and may be added in the future.\nUnprivileged mode is not supported on NetBSD, FreeBSD or Windows as these platforms do not support the `IPPROTO_ICMP`\nsocket type. See [#101](https://github.com/fujiapple852/trippy/issues/101) for further information.\n:::\n\nThe unprivileged mode can be enabled by adding the `--unprivileged` (`-u`) command line flag or by adding the\n`unprivileged` entry in the `trippy` section of the [configuration file](/0.12.2/reference/configuration):\n\n```toml\n[trippy]\nunprivileged = true\n```\n\n:::note\nThe `paris` and `dublin` `ECMP` strategies are not supported in unprivileged mode as these require manipulating the\n`UDP` and `IP` and headers which in turn requires the use of a raw socket.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/guides/recommendation.md",
    "content": "---\ntitle: Recommended Tracing Settings\ndescription: Recommended settings for Trippy.\nsidebar:\n  order: 3\nslug: 0.12.2/guides/recommendation\n---\n\nTrippy provides a variety of configurable features which can be used to perform different types of analysis. The choice\nof settings will depend on the analysis you wish to perform and the environment in which you are working. This guide\nlists some common options along with some basic guidance on when they might be appropriate.\n\n:::note\nThe Windows `tracert` tool uses ICMP by default, whereas most Unix `traceroute` tools use UDP by default.\n:::\n\n## ICMP\n\nBy default Trippy will run an ICMP trace to the target. This will typically produce a consistent path to the target (a\nsingle flow) for each round of tracing which makes it easy to read and analyse. This is a useful mode for general\nnetwork troubleshooting.\n\nHowever, many routers are configured to rate-limit ICMP traffic which can make it difficult to get an accurate picture\nof packet loss. In addition, ICMP traffic is not typically subject to ECMP routing and so may not reflect the path that\nwould taken by other protocols such as UDP and TCP.\n\nTo run a simple ICMP trace:\n\n```shell\ntrip example.com\n```\n\nDue to the rate-limiting of ICMP traffic, some people prefer to hide the `Loss%` and `Recv` columns in the Tui as\nthese are easy to misinterpret.\n\n```shell\ntrip example.com --tui-custom-columns hosavbwdt\n```\n\nThese settings can be made permanent by adding them to the Trippy configuration file:\n\n```toml\n[tui]\ncustom-columns = \"hosavbwdt\"\n```\n\n:::note\nThe `Sts` column shows different color codes to reflect packet loss at intermediate vs the target hop, see the\n[Column Reference](/0.12.2/reference/column) for more information.\n:::\n\n#### UDP/Dublin with fixed ports\n\nUDP tracing provides a more realistic view of the path taken by traffic that is subject to ECMP routing.\n\nSetting a fixed target port in the range 33434-33534 may allow Trippy to determine that the probe has reached the target\nas many routers and firewalls are configured to allow UDP probes in that range and will respond with a Destination\nUnreachable response.\n\nHowever, running a UDP trace with a fixed target port and a variable source port will typically result in different\npaths being followed for each probe within each round of tracing. This can make it difficult to interpret the output as\ndifferent hosts will reply for a given hop (time-to-live) across rounds.\n\nBy using the `dublin` ECMP strategy, which encodes the sequence number in the IP `identifier` field, Trippy can fix both\nthe source and target ports, typically resulting in a _single_ path for each probe within each round of tracing.\n\n:::note\nUDP/Dublin for IPv6 encodes the sequence number as the payload length as the IP `identifier` field is not available in\nIPv6.\n:::\n\n:::note\nKeep in mind that every probe is an _independent trial_ and each may traverse a completely different path. In practice,\nICMP probes often follow a single path, whereas the path of UDP and TCP probes is typically determined by the 5-tuple of\nprotocol, source and destination IP addresses and ports.\n\nAlso beware that the return path may not be the same as the forward path, and may also differ for each probe. Strategies\nsuch as `dublin` and `paris` assist in controlling the path taken by the forward probes, but do not help control the\nreturn path. Therefore it is recommended to run a trace in both directions to get a complete picture.\n:::\n\nTo run a UDP trace with fixed source and target ports using the `dublin` ECMP strategy:\n\n```shell\ntrip example.com --udp --multipath-strategy dublin --source-port 5000 --target-port 33434\n```\n\n:::note\nThe source port can be any valid port number, but the target port should usually be in the range 33434-33534 or whatever\nrange is open to UDP probes on the target host.\n:::\n\nThese settings can be made permanent by adding them to the Trippy configuration file:\n\n```toml\n[strategy]\nprotocol = \"udp\"\nmultipath-strategy = \"dublin\"\nsource-port = 5000\ntarget-port = 33434\n```\n\n## UDP/Dublin with fixed target port and variable source port\n\nAs an extension to the above, if you do not fix the source port when using the `dublin` ECMP strategy, Trippy will\nvary the source port per _round_ of tracing (i.e. each probe within a given round will share the same source port, and\nthe source port will vary for each round). This will typically result in the _same_ path being followed for _each_ probe\nwithin a given round, but _different_ paths being followed for each round.\n\nThese individual flows can be explored in the Trippy Tui by pressing the `toggle-flows` key binding (`f` key by\ndefault).\n\nAdding the columns `Seq`, `Sprt` and `Dprt` to the Tui will show the sequence number, source port and destination port\nrespectively which makes this easier to visualize.\n\n```shell\ntrip example.com --udp --multipath-strategy dublin --target-port 33434 --tui-custom-columns holsravbwdtSPQ\n```\n\nThese settings can be made permanent by adding them to the Trippy configuration file:\n\n```toml\n[strategy]\nprotocol = \"udp\"\nmultipath-strategy = \"dublin\"\ntarget-port = 33434\n\n[tui]\ncustom-columns = \"holsravbwdtSPQ\"\n```\n\nTo make the flows easier to visualize, you can generate a Graphviz DOT file report of all tracing flows:\n\n```shell\ntrip example.com --udp --multipath-strategy dublin --target-port 33434 -m dot -C 5\n```\n\n## UDP/Paris\n\nUDP with the `paris` ECMP strategy offers the same benefits as the `dublin` strategy with fixed ports and can be used\nin the same way.\n\nThey differ in the way they encode the sequence number in the probe. The `dublin` strategy uses the IP `identifier`\nfield, whereas the `paris` strategy uses the UDP `checksum` field.\n\nTo run a UDP trace with fixed source and target ports using the `paris` ECMP strategy:\n\n```shell\ntrip example.com --udp --multipath-strategy paris --source-port 5000 --target-port 33434\n```\n\nThe `paris` strategy does not work behind NAT as the UDP `checksum` field is typically modified by NAT devices.\nTherefore the `dublin` strategy is recommended when NAT is present.\n\n:::note\nTrippy can detect the presence of NAT devices in some circumstances when using the `dublin` strategy and the `Nat`\ncolumn can be shown in the Tui to indicate when NAT is detected. See the [Column Reference](/0.12.2/reference/column) for more\ninformation.\n:::\n\n#### TCP\n\nTCP tracing is similar to UDP tracing in that it provides a more realistic view of the path taken by traffic that is\nsubject to ECMP routing.\n\nTCP tracing defaults to using a target port of 80 and sets the source port as the sequence number which will typically\nresult in a different path being followed for each probe within each round of tracing.\n\nTo run a TCP trace:\n\n```shell\ntrip example.com --tcp\n```\n\nTCP tracing is useful for diagnosing issues with TCP connections and higher layer protocols such as HTTP. Often UDP\ntracing can be used in place of TCP to diagnose IP layer network issues and, as it provides ways to control the path\ntaken by the probes, it is often preferred.\n\n:::note\nTrippy does not support the `dublin` or `paris` ECMP strategies for TCP tracing and so you cannot fix both the source\nand target ports. See the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/guides/usage.md",
    "content": "---\ntitle: Usage Examples\ndescription: Examples of how to use the Trippy command line interface.\nsidebar:\n  order: 1\nslug: 0.12.2/guides/usage\n---\n\nBasic usage with default parameters:\n\n```shell\ntrip example.com\n```\n\nTrace without requiring elevated privileges (supported platforms only, see [privileges](/0.12.2/guides/privileges)):\n\n```shell\ntrip example.com --unprivileged\n```\n\nTrace using the `udp` (or `tcp` or `icmp`) protocol (also aliases `--icmp`, `--udp` & `--tcp`):\n\n```shell\ntrip example.com -p udp\n```\n\nTrace to multiple targets simultaneously (`icmp` protocol only,\nsee [#72](https://github.com/fujiapple852/trippy/issues/72)):\n\n```shell\ntrip example.com google.com crates.io\n```\n\nTrace with a minimum round time of `250ms` and a grace period of `50ms`:\n\n```shell\ntrip example.com -i 250ms -g 50ms\n```\n\nTrace with a custom first and maximum `time-to-live`:\n\n```shell\ntrip example.com --first-ttl 2 --max-ttl 10\n```\n\nUse custom destination port `443` for `tcp` tracing:\n\n```shell\ntrip example.com -p tcp -P 443\n```\n\nUse custom source port `5000` for `udp` tracing:\n\n```shell\ntrip example.com -p udp -S 5000\n```\n\nUse the `dublin` (or `paris`) ECMP routing strategy for `udp` with fixed source and destination ports:\n\n```shell\ntrip example.com -p udp -R dublin -S 5000 -P 3500\n```\n\nTrace with a custom source address:\n\n```shell\ntrip example.com -p tcp -A 127.0.0.1\n```\n\nTrace with a source address determined by the IPv4 address for interface `en0`:\n\n```shell\ntrip example.com -p tcp -I en0\n```\n\nTrace using `IPv6`:\n\n```shell\ntrip example.com -6\n```\n\nTrace using `ipv4-then-ipv6` fallback (or `ipv6-then-ipv4` or `ipv4` or `ipv6`):\n\n```shell\ntrip example.com --addr-family ipv4-then-ipv6\n```\n\nGenerate a `json` (or `csv`, `pretty`, `markdown`) tracing report with 5 rounds of data:\n\n```shell\ntrip example.com -m json -C 5\n```\n\nGenerate a [Graphviz](https://graphviz.org) `DOT` file report of all tracing flows for a TCP trace after 5 rounds:\n\n```shell\ntrip example.com --tcp -m dot -C 5\n```\n\nGenerate a textual report of all tracing flows for a UDP trace after 5 rounds:\n\n```shell\ntrip example.com --udp -m flows -C 5\n```\n\nPerform DNS queries using the `google` DNS resolver (or `cloudflare`, `system`, `resolv`):\n\n```shell\ntrip example.com -r google\n```\n\nLookup [AS][autonomous_system] information for all discovered IP addresses (not yet available for the `system` resolver,\nsee [#66](https://github.com/fujiapple852/trippy/issues/66)):\n\n```shell\ntrip example.com -r google -z\n```\n\nSet the reverse DNS lookup cache time-to-live to be 60 seconds:\n\n```shell\ntrip example.com --dns-ttl 60sec\n```\n\nLookup and display `short` (or `long` or `location` or `off`) GeoIp information from a `mmdb` file:\n\n```shell\ntrip example.com --geoip-mmdb-file GeoLite2-City.mmdb --tui-geoip-mode short\n```\n\nParse `icmp` extensions:\n\n```shell\ntrip example.com -e\n```\n\nHide the IP address, hostname and GeoIp for the first two hops:\n\n```shell\ntrip example.com --tui-privacy-max-ttl 2\n```\n\nCustomize Tui columns (see [Column Reference](/0.12.2/reference/column)):\n\n```shell\ntrip example.com --tui-custom-columns holsravbwdt\n```\n\nCustomize the color theme:\n\n```shell\ntrip example.com --tui-theme-colors bg-color=blue,text-color=ffff00\n```\n\nList all Tui items that can have a custom color theme:\n\n```shell\ntrip --print-tui-theme-items\n```\n\nCustomize the key bindings:\n\n```shell\ntrip example.com --tui-key-bindings previous-hop=k,next-hop=j,quit=shift-q\n```\n\nList all Tui commands that can have a custom key binding:\n\n```shell\ntrip --print-tui-binding-commands\n```\n\nSpecify the location of the Trippy config file:\n\n```shell\ntrip example.com --config-file /path/to/trippy.toml\n```\n\nGenerate a template configuration file:\n\n```shell\ntrip --print-config-template > trippy.toml\n```\n\nGenerate `bash` shell completions (or `fish`, `powershell`, `zsh`, `elvish`):\n\n```shell\ntrip --generate bash\n```\n\nGenerate `ROFF` man page:\n\n```shell\ntrip --generate-man\n```\n\nUse the `de` Tui locale:\n\n```shell\ntrip example.com --tui-locale de\n```\n\nList supported Tui locales:\n\n```shell\ntrip --print-locales\n```\n\nRun in `silent` tracing mode and output `compact` trace logging with `full` span events:\n\n```shell\ntrip example.com -m silent -v --log-format compact --log-span-events full\n```\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/guides/windows_firewall.md",
    "content": "---\ntitle: Windows Defender Firewall\ndescription: Allow incoming ICMP traffic in the Windows Defender firewall.\nsidebar:\n  order: 4\nslug: 0.12.2/guides/windows_firewall\n---\n\nThe Windows Defender firewall rule can be created using PowerShell.\n\n```shell\nNew-NetFirewallRule -DisplayName \"ICMPv4 Trippy Allow\" -Name ICMPv4_TRIPPY_ALLOW -Protocol ICMPv4 -Action Allow\nNew-NetFirewallRule -DisplayName \"ICMPv6 Trippy Allow\" -Name ICMPv6_TRIPPY_ALLOW -Protocol ICMPv6 -Action Allow\n```\n\nThe rules can be enabled as follows:\n\n```shell\nEnable-NetFirewallRule ICMPv4_TRIPPY_ALLOW\nEnable-NetFirewallRule ICMPv6_TRIPPY_ALLOW\n```\n\nThe rules can be disabled as follows:\n\n```shell\nDisable-NetFirewallRule ICMPv4_TRIPPY_ALLOW\nDisable-NetFirewallRule ICMPv6_TRIPPY_ALLOW\n```\n\nThere is a [step-by-step guide to manually configure the Windows Defender firewall rule](https://github.com/fujiapple852/trippy/issues/578#issuecomment-1565149826).\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/index.mdx",
    "content": "---\ntitle: \"Trippy: a network diagnostic tool\"\ndescription: a network diagnostic tool.\ntemplate: splash\nhero:\n  tagline: Trippy combines the functionality of traceroute and ping and is designed to assist with the analysis of networking issues.\n  image:\n    alt: Trippy, man!\n    light: ../../../assets/0.12.2/Trippy-Emblem.svg\n    dark: ../../../assets/0.12.2/Trippy-Emblem-DarkMode.svg\n  actions:\n    - text: Get Started\n      link: /0.12.2/start/getting-started/\n      icon: right-arrow\n    - text: Read the docs\n      link: /0.12.2/reference/cli/\n      icon: open-book\n      variant: secondary\n    - text: View on GitHub\n      link: https://github.com/fujiapple852/trippy\n      icon: github\n      variant: secondary\nslug: 0.12.2\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\nimport { Icon } from '@astrojs/starlight/components';\n\n<CardGrid stagger>\n\t<Card title=\"Powerful tracing features\" icon=\"rocket\">\n\t    - `ICMP`, `UDP` & `TCP` over `IPv4` & `IPv6` protocols\n\t    - Fully customizable tracing options\n\t    - `dublin` and `paris` `ECMP` strategies\n\t    - `ICMP` extensions objects (i.e. `MPLS`)\n\t    - Reverse `DNS` and `ASN` lookups\n\t    - `NAT` detection\n\n\t\t![Trippy main screen](../../../assets/0.12.2/main_screen.png)\n\t</Card>\n\n\t<Card title=\"Visualize GeoIp on a world map\" icon=\"star\">\n        - Lookup GeoIp information and show on world map\n        - Support for both `MaxMind` and `IPinfo` databases\n\n\t\t![Trippy GeoIp world map](../../../assets/0.12.2/world_map.png)\n\t</Card>\n\n    <Card title=\"Run on your platform\" icon=\"star\">\n        - Runs on `Linux`, `macOS`, `Windows`, `*BSD`\n        - Supports `x86_64`, `aarch64`, `arm7` architectures\n        - Available from most native package managers\n        - Run in unprivileged mode\n\n        ![Trippy on Windows](../../../assets/0.12.2/windows.png)\n    </Card>\n\n\t<Card title=\"Highly customizable TUI\" icon=\"seti:config\">\n\t    - Customizable columns, color themes and key bindings\n\t    - Hop detail navigation mode\n\t    - Hop privacy mode\n\t    - Show individual tracing flows\n\t    - Various charts and statistics\n\t    - Persist configuration to file\n\n\t\t![Trippy settings](../../../assets/0.12.2/settings.png)\n\t</Card>\n\n\t<Card title=\"Trace in your language\" icon=\"translate\">\n\t    TUI available in 10 languages:\n\t    - Chinese 🇨🇳, English 🇺🇸, French 🇫🇷, German 🇩🇪, Italian 🇮🇹, Portuguese 🇵🇹, Russian 🇷🇺, Spanish 🇪🇸, Swedish 🇸🇪 and Turkish 🇹🇷\n\n\t\t![Trippy main screen in Chinese](../../../assets/0.12.2/help_screen_zh.png)\n\t</Card>\n\n</CardGrid>\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/reference/bindings.md",
    "content": "---\ntitle: Key Bindings Reference\ndescription: A reference for customizing the Trippy TUI key bindings.\nsidebar:\n  order: 3\nslug: 0.12.2/reference/bindings\n---\n\nThe following table lists the default Tui command key bindings. These can be overridden with the `--tui-key-bindings`\ncommand line option or in the `bindings` section of the configuration file.\n\n| Command                    | Description                                     | Default   |\n| -------------------------- | ----------------------------------------------- | --------- |\n| `toggle-help`              | Toggle help                                     | `h`       |\n| `toggle-help-alt`          | Toggle help (alternative binding)               | `?`       |\n| `toggle-settings`          | Toggle settings                                 | `s`       |\n| `toggle-settings-tui`      | Open settings (Tui tab)                         | `1`       |\n| `toggle-settings-trace`    | Open settings (Trace tab)                       | `2`       |\n| `toggle-settings-dns`      | Open settings (Dns tab)                         | `3`       |\n| `toggle-settings-geoip`    | Open settings (GeoIp tab)                       | `4`       |\n| `toggle-settings-bindings` | Open settings (Bindings tab)                    | `5`       |\n| `toggle-settings-theme`    | Open settings (Theme tab)                       | `6`       |\n| `toggle-settings-columns`  | Open settings (Columns tab)                     | `7`       |\n| `next-hop`                 | Select next hop                                 | `down`    |\n| `previous-hop`             | Select previous hop                             | `up`      |\n| `next-trace`               | Select next trace                               | `right`   |\n| `previous-trace`           | Select previous trace                           | `left`    |\n| `next-hop-address`         | Select next hop address                         | `.`       |\n| `previous-hop-address`     | Select previous hop address                     | `,`       |\n| `address-mode-ip`          | Show IP address only                            | `i`       |\n| `address-mode-host`        | Show hostname only                              | `n`       |\n| `address-mode-both`        | Show both IP address and hostname               | `b`       |\n| `toggle-freeze`            | Toggle freezing the display                     | `ctrl+f`  |\n| `toggle-chart`             | Toggle the chart                                | `c`       |\n| `toggle-map`               | Toggle the GeoIp map                            | `m`       |\n| `toggle-flows`             | Toggle the flows                                | `f`       |\n| `expand-privacy`           | Expand hop privacy                              | `p`       |\n| `contract-privacy`         | Contract hop privacy                            | `o`       |\n| `expand-hosts`             | Expand the hosts shown per hop                  | `]`       |\n| `expand-hosts-max`         | Expand the hosts shown per hop to the maximum   | `}`       |\n| `contract-hosts`           | Contract the hosts shown per hop                | `[`       |\n| `contract-hosts-min`       | Contract the hosts shown per hop to the minimum | `{`       |\n| `chart-zoom-in`            | Zoom in the chart                               | `=`       |\n| `chart-zoom-out`           | Zoom out the chart                              | `-`       |\n| `clear-trace-data`         | Clear all trace data                            | `ctrl+r`  |\n| `clear-dns-cache`          | Flush the DNS cache                             | `ctrl+k`  |\n| `clear-selection`          | Clear the current selection                     | `esc`     |\n| `toggle-as-info`           | Toggle AS info display                          | `z`       |\n| `toggle-hop-details`       | Toggle hop details                              | `d`       |\n| `quit`                     | Quit the application                            | `q`       |\n| `quit-preserve-screen`     | Quit the application and preserve the screen    | `shift+q` |\n\nThe supported modifiers are: `shift`, `ctrl`, `alt`, `super`, `hyper` & `meta`. Multiple modifiers may be specified, for\nexample `ctrl+shift+b`.\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/reference/cli.md",
    "content": "---\ntitle: CLI Reference\ndescription: A reference for the Trippy command line interface.\nsidebar:\n  order: 1\nslug: 0.12.2/reference/cli\n---\n\n```text\nA network diagnostic tool\n\nUsage: trip [OPTIONS] [TARGETS]...\n\nArguments:\n  [TARGETS]...\n          A space delimited list of hostnames and IPs to trace\n\nOptions:\n  -c, --config-file <CONFIG_FILE>\n          Config file\n\n  -m, --mode <MODE>\n          Output mode [default: tui]\n\n          Possible values:\n          - tui:      Display interactive TUI\n          - stream:   Display a continuous stream of tracing data\n          - pretty:   Generate a pretty text table report for N cycles\n          - markdown: Generate a Markdown text table report for N cycles\n          - csv:      Generate a CSV report for N cycles\n          - json:     Generate a JSON report for N cycles\n          - dot:      Generate a Graphviz DOT file for N cycles\n          - flows:    Display all flows for N cycles\n          - silent:   Do not generate any tracing output for N cycles\n\n  -u, --unprivileged\n          Trace without requiring elevated privileges on supported platforms\n          [default: false]\n\n  -p, --protocol <PROTOCOL>\n          Tracing protocol [default: icmp]\n\n          Possible values:\n          - icmp: Internet Control Message Protocol\n          - udp:  User Datagram Protocol\n          - tcp:  Transmission Control Protocol\n\n      --udp\n          Trace using the UDP protocol\n\n      --tcp\n          Trace using the TCP protocol\n\n      --icmp\n          Trace using the ICMP protocol\n\n  -F, --addr-family <ADDR_FAMILY>\n          The address family [default: Ipv4thenIpv6]\n\n          Possible values:\n          - ipv4:           IPv4 only\n          - ipv6:           IPv6 only\n          - ipv6-then-ipv4: IPv6 with a fallback to IPv4\n          - ipv4-then-ipv6: IPv4 with a fallback to IPv6\n\n  -4, --ipv4\n          Use IPv4 only\n\n  -6, --ipv6\n          Use IPv6 only\n\n  -P, --target-port <TARGET_PORT>\n          The target port (TCP & UDP only) [default: 80]\n\n  -S, --source-port <SOURCE_PORT>\n          The source port (TCP & UDP only) [default: auto]\n\n  -A, --source-address <SOURCE_ADDRESS>\n          The source IP address [default: auto]\n\n  -I, --interface <INTERFACE>\n          The network interface [default: auto]\n\n  -i, --min-round-duration <MIN_ROUND_DURATION>\n          The minimum duration of every round [default: 1s]\n\n  -T, --max-round-duration <MAX_ROUND_DURATION>\n          The maximum duration of every round [default: 1s]\n\n  -g, --grace-duration <GRACE_DURATION>\n          The period of time to wait for additional ICMP responses after the\n          target has responded [default: 100ms]\n\n      --initial-sequence <INITIAL_SEQUENCE>\n          The initial sequence number [default: 33434]\n\n  -R, --multipath-strategy <MULTIPATH_STRATEGY>\n          The Equal-cost Multi-Path routing strategy (UDP only) [default:\n          classic]\n\n          Possible values:\n          - classic:\n            The src or dest port is used to store the sequence number\n          - paris:\n            The UDP `checksum` field is used to store the sequence number\n          - dublin:\n            The IP `identifier` field is used to store the sequence number\n\n  -U, --max-inflight <MAX_INFLIGHT>\n          The maximum number of in-flight ICMP echo requests [default: 24]\n\n  -f, --first-ttl <FIRST_TTL>\n          The TTL to start from [default: 1]\n\n  -t, --max-ttl <MAX_TTL>\n          The maximum number of TTL hops [default: 64]\n\n      --packet-size <PACKET_SIZE>\n          The size of IP packet to send (IP header + ICMP header + payload)\n          [default: 84]\n\n      --payload-pattern <PAYLOAD_PATTERN>\n          The repeating pattern in the payload of the ICMP packet [default: 0]\n\n  -Q, --tos <TOS>\n          The TOS (i.e. DSCP+ECN) IP header value (TCP and UDP only) [default: 0]\n\n  -e, --icmp-extensions\n          Parse ICMP extensions\n\n      --read-timeout <READ_TIMEOUT>\n          The socket read timeout [default: 10ms]\n\n  -r, --dns-resolve-method <DNS_RESOLVE_METHOD>\n          How to perform DNS queries [default: system]\n\n          Possible values:\n          - system:     Resolve using the OS resolver\n          - resolv:     Resolve using the `/etc/resolv.conf` DNS configuration\n          - google:     Resolve using the Google `8.8.8.8` DNS service\n          - cloudflare: Resolve using the Cloudflare `1.1.1.1` DNS service\n\n  -y, --dns-resolve-all\n          Trace to all IPs resolved from DNS lookup [default: false]\n\n      --dns-timeout <DNS_TIMEOUT>\n          The maximum time to wait to perform DNS queries [default: 5s]\n\n      --dns-ttl <DNS_TTL>\n          The time-to-live (TTL) of DNS entries [default: 300s]\n\n  -z, --dns-lookup-as-info\n          Lookup autonomous system (AS) information during DNS queries [default:\n          false]\n\n  -s, --max-samples <MAX_SAMPLES>\n          The maximum number of samples to record per hop [default: 256]\n\n      --max-flows <MAX_FLOWS>\n          The maximum number of flows to record [default: 64]\n\n  -a, --tui-address-mode <TUI_ADDRESS_MODE>\n          How to render addresses [default: host]\n\n          Possible values:\n          - ip:   Show IP address only\n          - host: Show reverse-lookup DNS hostname only\n          - both: Show both IP address and reverse-lookup DNS hostname\n\n      --tui-as-mode <TUI_AS_MODE>\n          How to render autonomous system (AS) information [default: asn]\n\n          Possible values:\n          - asn:          Show the ASN\n          - prefix:       Display the AS prefix\n          - country-code: Display the country code\n          - registry:     Display the registry name\n          - allocated:    Display the allocated date\n          - name:         Display the AS name\n\n      --tui-custom-columns <TUI_CUSTOM_COLUMNS>\n          Custom columns to be displayed in the TUI hops table [default:\n          holsravbwdt]\n\n      --tui-icmp-extension-mode <TUI_ICMP_EXTENSION_MODE>\n          How to render ICMP extensions [default: off]\n\n          Possible values:\n          - off:  Do not show `icmp` extensions\n          - mpls: Show MPLS label(s) only\n          - full: Show full `icmp` extension data for all known extensions\n          - all:  Show full `icmp` extension data for all classes\n\n      --tui-geoip-mode <TUI_GEOIP_MODE>\n          How to render GeoIp information [default: short]\n\n          Possible values:\n          - off:      Do not display GeoIp data\n          - short:    Show short format\n          - long:     Show long format\n          - location: Show latitude and Longitude format\n\n  -M, --tui-max-addrs <TUI_MAX_ADDRS>\n          The maximum number of addresses to show per hop [default: auto]\n\n      --tui-preserve-screen\n          Preserve the screen on exit [default: false]\n\n      --tui-refresh-rate <TUI_REFRESH_RATE>\n          The TUI refresh rate [default: 100ms]\n\n      --tui-privacy-max-ttl <TUI_PRIVACY_MAX_TTL>\n          The maximum ttl of hops which will be masked for privacy [default: none]\n\n          If set, the source IP address and hostname will also be hidden.\n\n      --tui-locale <TUI_LOCALE>\n          The locale to use for the TUI [default: auto]\n\n      --tui-theme-colors <TUI_THEME_COLORS>\n          The TUI theme colors [item=color,item=color,..]\n\n      --print-tui-theme-items\n          Print all TUI theme items and exit\n\n      --tui-key-bindings <TUI_KEY_BINDINGS>\n          The TUI key bindings [command=key,command=key,..]\n\n      --print-tui-binding-commands\n          Print all TUI commands that can be bound and exit\n\n  -C, --report-cycles <REPORT_CYCLES>\n          The number of report cycles to run [default: 10]\n\n  -G, --geoip-mmdb-file <GEOIP_MMDB_FILE>\n          The supported MaxMind or IPinfo GeoIp mmdb file\n\n      --generate <GENERATE>\n          Generate shell completion\n\n          [possible values: bash, elvish, fish, powershell, zsh]\n\n      --generate-man\n          Generate ROFF man page\n\n      --print-config-template\n          Print a template toml config file and exit\n\n      --print-locales\n          Print all available TUI locales and exit\n\n      --log-format <LOG_FORMAT>\n          The debug log format [default: pretty]\n\n          Possible values:\n          - compact: Display log data in a compact format\n          - pretty:  Display log data in a pretty format\n          - json:    Display log data in a json format\n          - chrome:  Display log data in Chrome trace format\n\n      --log-filter <LOG_FILTER>\n          The debug log filter [default: trippy=debug]\n\n      --log-span-events <LOG_SPAN_EVENTS>\n          The debug log format [default: off]\n\n          Possible values:\n          - off:    Do not display event spans\n          - active: Display enter and exit event spans\n          - full:   Display all event spans\n\n  -v, --verbose\n          Enable verbose debug logging\n\n  -h, --help\n          Print help (see a summary with '-h')\n\n  -V, --version\n          Print version\n```\n\n:::note\nTrippy command line arguments may be given in any order and my occur both before and after the targets.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/reference/column.md",
    "content": "---\ntitle: Column Reference\ndescription: A reference for customizing the Trippy TUI columns.\nsidebar:\n  order: 4\nslug: 0.12.2/reference/column\n---\n\nThe following table lists the columns that are available for display in the Tui. These can be overridden with the\n`--tui-custom-columns` command line option or in the `tui-custom-columns` attribute in the `tui` section of the\nconfiguration file.\n\n| Column   | Code | Description                                                                                                                                                                                                                                                                                                                                           |\n| -------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `#`      | `h`  | The time-to-live (TTL) for the hop                                                                                                                                                                                                                                                                                                                    |\n| `Host`   | `o`  | The hostname(s) and IP address(s) for the host(s) for the hop<br/>May include AS info, GeoIp and ICMP extensions<br/>Shows full hop details in hop detail navigation mode                                                                                                                                                                             |\n| `Loss%`  | `l`  | The packet loss % for the hop                                                                                                                                                                                                                                                                                                                         |\n| `Snd`    | `s`  | The number of probes sent for the hop                                                                                                                                                                                                                                                                                                                 |\n| `Recv`   | `r`  | The number of probe responses received for the hop                                                                                                                                                                                                                                                                                                    |\n| `Last`   | `a`  | The round-trip-time (RTT) of the last probe for the hop                                                                                                                                                                                                                                                                                               |\n| `Avg`    | `v`  | The average RTT of all probes for the hop                                                                                                                                                                                                                                                                                                             |\n| `Best`   | `b`  | The best RTT of all probes for the hop                                                                                                                                                                                                                                                                                                                |\n| `Wrst`   | `w`  | The worst RTT of all probes for the hop                                                                                                                                                                                                                                                                                                               |\n| `StDev`  | `d`  | The standard deviation of all probes for the hop                                                                                                                                                                                                                                                                                                      |\n| `Sts`    | `t`  | The status for the hop:<br/>- 🟢 Healthy hop<br/>- 🔵 Non-target hop with packet loss (does not necessarily indicate a problem)<br/>- 🟤 Non-target hop is unresponsive (does not necessarily indicate a problem) <br/>- 🟡 Target hop with packet loss (likely indicates a problem)<br/>- 🔴 Target hop is unresponsive (likely indicates a problem) |\n| `Jttr`   | `j`  | The round-trip-time (RTT) difference between consecutive rounds for the hop                                                                                                                                                                                                                                                                           |\n| `Javg`   | `g`  | The average jitter of all probes for the hop                                                                                                                                                                                                                                                                                                          |\n| `Jmax`   | `x`  | The maximum jitter of all probes for the hop                                                                                                                                                                                                                                                                                                          |\n| `Jint`   | `i`  | The smoothed jitter value of all probes for the hop                                                                                                                                                                                                                                                                                                   |\n| `Seq`    | `Q`  | The sequence number for the last probe for the hop                                                                                                                                                                                                                                                                                                    |\n| `Sprt`   | `S`  | The source port for the last probe for the hop                                                                                                                                                                                                                                                                                                        |\n| `Dprt`   | `P`  | The destination port for the last probe for the hop                                                                                                                                                                                                                                                                                                   |\n| `Type`   | `T`  | The icmp packet type for the last probe for the hop:<br/>- TE: TimeExceeded<br/>- ER: EchoReply<br/>- DU: DestinationUnreachable<br/>- NA: NotApplicable                                                                                                                                                                                              |\n| `Code`   | `C`  | The icmp packet code for the last probe for the hop                                                                                                                                                                                                                                                                                                   |\n| `Nat`    | `N`  | The NAT detection status for the hop                                                                                                                                                                                                                                                                                                                  |\n| `Fail`   | `f`  | The number of probes which failed to send for the hop                                                                                                                                                                                                                                                                                                 |\n| `Floss`  | `F`  | A _heuristic_ for the number of probes with _forward loss_ for the hop                                                                                                                                                                                                                                                                                |\n| `Bloss`  | `B`  | A _heuristic_ for the number of probes with _backward loss_ for the hop                                                                                                                                                                                                                                                                               |\n| `Floss%` | `D`  | The _forward loss_ % for the hop                                                                                                                                                                                                                                                                                                                      |\n\nThe default columns are `holsravbwdt`.\n\n:::note\nThe columns will be shown in the order specified in the configuration.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/reference/configuration.md",
    "content": "---\ntitle: Configuration Reference\ndescription: A reference for customizing the Trippy configuration.\nsidebar:\n  order: 2\nslug: 0.12.2/reference/configuration\n---\n\nTrippy can be configured with via command line arguments or an optional configuration file. If a given configuration\nitem is specified in both the configuration file and via a command line argument then the latter will take precedence.\n\nThe configuration file location may be provided to Trippy via the `-c` (`--config-file`) argument. If not provided,\nTrippy will attempt to locate a `trippy.toml` or `.trippy.toml` configuration file in one of the following locations:\n\n- The current directory\n- The user home directory\n- the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config`\n- the Windows data directory (Windows only): `%APPDATA%`\n\nA template configuration file\nfor [0.12.2](https://github.com/fujiapple852/trippy/blob/0.12.2/trippy-config-sample.toml) is available to\ndownload, or can be generated with the following command:\n\n```shell\ntrip --print-config-template > trippy.toml\n```\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/reference/locale.md",
    "content": "---\ntitle: Locale Reference\ndescription: A reference for customizing the Trippy TUI locale.\nsidebar:\n  order: 6\n  badge:\n    text: New\n    variant: note\nslug: 0.12.2/reference/locale\n---\n\nThe following table lists the supported locales for the Tui. These can be overridden with the `--tui-locale` command\nline option or in the `tui-locale` attribute in the `tui` section of the configuration file.\n\n| Locale | Language   | Region |\n| ------ | ---------- | ------ |\n| `zh`   | Chinese    | all    |\n| `en`   | English    | all    |\n| `fr`   | French     | all    |\n| `de`   | German     | all    |\n| `it`   | Italian    | all    |\n| `pt`   | Portuguese | all    |\n| `ru`   | Russian    | all    |\n| `es`   | Spanish    | all    |\n| `sv`   | Swedish    | all    |\n| `tr`   | Turkish    | all    |\n\n:::note\nIf you are able to help validate translations for Trippy, or if you wish to add translations for any additional\nlanguages, please see the [tracking issue](https://github.com/fujiapple852/trippy/issues/506) for details of how to\ncontribute.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/reference/theme.md",
    "content": "---\ntitle: Theme Reference\ndescription: A reference for customizing the Trippy TUI theme.\nsidebar:\n  order: 5\nslug: 0.12.2/reference/theme\n---\n\nThe following table lists the default Tui color theme. These can be overridden with the `--tui-theme-colors` command\nline option or in the `theme-colors` section of the configuration file.\n\n| Item                                 | Description                                               | Default      |\n| ------------------------------------ | --------------------------------------------------------- | ------------ |\n| `bg-color`                           | The default background color                              | `Black`      |\n| `border-color`                       | The default color of borders                              | `Gray`       |\n| `text-color`                         | The default color of text                                 | `Gray`       |\n| `tab-text-color`                     | The color of the text in traces tabs                      | `Green`      |\n| `hops-table-header-bg-color`         | The background color of the hops table header             | `White`      |\n| `hops-table-header-text-color`       | The color of text in the hops table header                | `Black`      |\n| `hops-table-row-active-text-color`   | The color of text of active rows in the hops table        | `Gray`       |\n| `hops-table-row-inactive-text-color` | The color of text of inactive rows in the hops table      | `DarkGray`   |\n| `hops-chart-selected-color`          | The color of the selected series in the hops chart        | `Green`      |\n| `hops-chart-unselected-color`        | The color of the unselected series in the hops chart      | `Gray`       |\n| `hops-chart-axis-color`              | The color of the axis in the hops chart                   | `DarkGray`   |\n| `frequency-chart-bar-color`          | The color of bars in the frequency chart                  | `Green`      |\n| `frequency-chart-text-color`         | The color of text in the bars of the frequency chart      | `Gray`       |\n| `flows-chart-bar-selected-color`     | The color of the selected flow bar in the flows chart     | `Green`      |\n| `flows-chart-bar-unselected-color`   | The color of the unselected flow bar in the flows chart   | `DarkGray`   |\n| `flows-chart-text-current-color`     | The color of the current flow text in the flows chart     | `LightGreen` |\n| `flows-chart-text-non-current-color` | The color of the non-current flow text in the flows chart | `White`      |\n| `samples-chart-color`                | The color of the samples chart                            | `Yellow`     |\n| `samples-chart-lost-color`           | The color of the samples chart for lost probes            | `Red`        |\n| `help-dialog-bg-color`               | The background color of the help dialog                   | `Blue`       |\n| `help-dialog-text-color`             | The color of the text in the help dialog                  | `Gray`       |\n| `settings-dialog-bg-color`           | The background color of the settings dialog               | `blue`       |\n| `settings-tab-text-color`            | The color of the text in settings dialog tabs             | `green`      |\n| `settings-table-header-text-color`   | The color of text in the settings table header            | `black`      |\n| `settings-table-header-bg-color`     | The background color of the settings table header         | `white`      |\n| `settings-table-row-text-color`      | The color of text of rows in the settings table           | `gray`       |\n| `map-world-color`                    | The color of the map world diagram                        | `white`      |\n| `map-radius-color`                   | The color of the map accuracy radius circle               | `yellow`     |\n| `map-selected-color`                 | The color of the map selected item box                    | `green`      |\n| `map-info-panel-border-color`        | The color of border of the map info panel                 | `gray`       |\n| `map-info-panel-bg-color`            | The background color of the map info panel                | `black`      |\n| `map-info-panel-text-color`          | The color of text in the map info panel                   | `gray`       |\n| `info-bar-bg-color`                  | The background color of the information bar               | `white`      |\n| `info-bar-text-color`                | The color of text in the information bar                  | `black`      |\n\nThe supported [ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) are:\n\n- `Black`, `Red`, `Green`, `Yellow`, `Blue`, `Magenta`, `Cyan`, `Gray`, `DarkGray`, `LightRed`, `LightGreen`,\n  `LightYellow`, `LightBlue`, `LightMagenta`, `LightCyan`, `White`\n\nIn addition, CSS [named colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color) (i.e. SkyBlue) and raw hex\nvalues (i.e. ffffff) may be used but note that these are only supported on some platforms and terminals and may not\nrender correctly elsewhere.\n\nColor names are case-insensitive and may contain dashes.\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/reference/version.md",
    "content": "---\ntitle: Version Reference\ndescription: A reference for the Trippy versions.\nsidebar:\n  order: 7\nslug: 0.12.2/reference/version\n---\n\nThe following table lists this versions of Trippy that are available and links to the corresponding release note and\ndocumentation:\n\n| Version    | Release Date | Status      | Release Note                                                       | Documentation                                              |\n| ---------- | ------------ | ----------- | ------------------------------------------------------------------ | ---------------------------------------------------------- |\n| 0.13.0-dev | 2025-05-05   | Development | n/a                                                                | [docs](https://trippy.rs)                                  |\n| 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)                           |\n| 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) |\n| 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) |\n| 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)  |\n| 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)  |\n| 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)  |\n| 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)  |\n\n:::note\nOnly the _latest patch versions_ of both the _current_ and _previous_ releases of Trippy are supported.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/start/features.md",
    "content": "---\ntitle: Features\ndescription: Learn about the features of Trippy.\nsidebar:\n  order: 3\nslug: 0.12.2/start/features\n---\n\n- Trace using multiple protocols:\n  - `ICMP`, `UDP` & `TCP`\n  - `IPv4` & `IPv6`\n- Customizable tracing options:\n  - packet size & payload pattern\n  - start and maximum time-to-live (TTL)\n  - minimum and maximum round duration\n  - round end grace period & maximum number of unknown hops\n  - source & destination port (`TCP` & `UDP`)\n  - source address and source interface\n  - `TOS` (aka `DSCP + ECN`)\n- Support for `classic`, `paris`\n  and `dublin` [Equal Cost Multi-path Routing](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing)\n  strategies ([tracking issue](https://github.com/fujiapple852/trippy/issues/274))\n- RFC4884 [ICMP Multi-Part Messages](https://datatracker.ietf.org/doc/html/rfc4884)\n  - Generic Extension Objects\n  - MPLS Label Stacks\n- Unprivileged mode\n- NAT detection\n- Tui interface:\n  - Trace multiple targets simultaneously from a single instance of Trippy\n  - Per hop stats (sent, received, loss%, last, avg, best, worst, stddev, jitter & status)\n  - Per hop round-trip-time (RTT) history and frequency distributing charts\n  - Interactive chart of RTT for all hops in a trace with zooming capability\n  - Interactive GeoIp world map\n  - Isolate and filter by individual tracing flows\n  - Customizable color theme & key bindings\n  - Customizable column order and visibility\n  - Configuration via both command line arguments and a configuration file\n  - Show multiple hosts per hop with ability to cap display to N hosts and show frequency %\n  - Show hop details and navigate hosts within each hop\n  - Freeze/unfreeze the Tui, reset the stats, flush the cache, preserve screen on exit\n  - Responsive UI with adjustable refresh rate\n  - Hop privacy\n  - Multiple language support\n- DNS:\n  - Use system, external (Google `8.8.8.8` or Cloudflare `1.1.1.1`) or custom resolver\n  - Lazy reverse DNS queries\n  - Lookup [autonomous system][autonomous_system] number (ASN) and name\n- GeoIp:\n  - Lookup and display GeoIp information from local [MaxMind](https://www.maxmind.com)\n    and [IPinfo](https://ipinfo.io) `mmdb` files\n- Generate tracing reports:\n  - `json`, `csv` & tabular (pretty-printed and markdown)\n  - Tracing `flows` report\n  - Graphviz `dot` charts\n  - configurable reporting cycles\n- Runs on multiple platform (macOS, Linux, Windows, NetBSD, FreeBSD, OpenBSD)\n- Capabilities aware application (Linux only)\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/start/getting-started.mdx",
    "content": "---\ntitle: Getting Started\ndescription: Get started with Trippy.\nsidebar:\n  order: 1\nslug: 0.12.2/start/getting-started\n---\n\nimport { Steps } from '@astrojs/starlight/components';\n\nThe following steps will guide you through the process of installing and running Trippy.\n\n<Steps>\n\n1. Install Trippy:\n\n    Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled binaries, or source.\n\n    For example, to install Trippy from `cargo`:\n\n    ```shell\n    cargo install trippy --locked\n    ```\n\n    See the [installation guide](/0.12.2/start/installation) for details of how to install Trippy on your system.\n\n2. Run Trippy:\n\n   To run a basic trace to `example.com` with default settings, use the following command:\n\n   ```shell\n   sudo trip example.com\n   ```\n\n   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).\n\n3. Customize the key bindings, theme and columns:\n\n   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.\n\n   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.\n\n4. Review the tracing recommendations:\n\n   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.\n\n</Steps>\n\nHappy tracing!\n"
  },
  {
    "path": "docs/src/content/docs/0.12.2/start/installation.md",
    "content": "---\ntitle: Installation\ndescription: Install Trippy on your platform.\nsidebar:\n  order: 2\nslug: 0.12.2/start/installation\n---\n\nThe following sections provide instructions for installing Trippy on your platform.\n\nTrippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled\nbinaries, or source.\n\n## Distributions\n\nTrippy is available for a variety of platforms and package managers.\n\n### Cargo\n\n[![Crates.io](https://img.shields.io/crates/v/trippy)](https://crates.io/crates/trippy/0.12.2)\n\n```shell\ncargo install trippy --locked\n```\n\n### APT (Debian)\n\n[![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy)\n\n```shell\napt install trippy\n```\n\n:::note\nOnly available for Debian 13 (`trixie`) and later.\n:::\n\n### PPA (Ubuntu)\n\n[![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.12.2-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages)\n\n```shell\nadd-apt-repository ppa:fujiapple/trippy\napt update && apt install trippy\n```\n\n:::note\nOnly available for Ubuntu 24.04 (`Noble`) and 22.04 (`Jammy`).\n:::\n\n### Snap (Linux)\n\n[![trippy](https://snapcraft.io/trippy/badge.svg)](https://snapcraft.io/trippy)\n\n```shell\nsnap install trippy\n```\n\n### Homebrew (macOS)\n\n[![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/trippy.svg)](https://formulae.brew.sh/formula/trippy)\n\n```shell\nbrew install trippy\n```\n\n### WinGet (Windows)\n\n[![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)\n\n```shell\nwinget install trippy\n```\n\n### Scoop (Windows)\n\n[![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)\n\n```shell\nscoop install trippy\n```\n\n### Chocolatey (Windows)\n\n[![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy)\n\n```shell\nchoco install trippy\n```\n\n### NetBSD\n\n[![pkgsrc current package](https://repology.org/badge/version-for-repo/pkgsrc_current/trippy.svg)](https://pkgsrc.se/net/trippy)\n\n```shell\npkgin install trippy\n```\n\n### FreeBSD\n\n[![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/trippy.svg)](https://www.freshports.org/net/trippy/)\n\n```shell\npkg install trippy\n```\n\n### OpenBSD\n\n[![OpenBSD port](https://repology.org/badge/version-for-repo/openbsd/trippy.svg)](https://openports.pl/path/net/trippy)\n\n```shell\npkg_add trippy\n```\n\n### Arch Linux\n\n[![Arch package](https://repology.org/badge/version-for-repo/arch/trippy.svg)](https://archlinux.org/packages/extra/x86_64/trippy)\n\n```shell\npacman -S trippy\n```\n\n### Gentoo Linux\n\n[![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/trippy.svg)](https://packages.gentoo.org/packages/net-analyzer/trippy)\n\n```shell\nemerge -av net-analyzer/trippy\n```\n\n### Nix\n\n[![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)\n\n```shell\nnix-env -iA trippy\n```\n\n### Docker\n\n[![Docker Image Version (latest by date)](https://img.shields.io/docker/v/fujiapple/trippy)](https://hub.docker.com/r/fujiapple/trippy/)\n\n```shell\ndocker run -it fujiapple/trippy\n```\n\n### All Repositories\n\n[![Packaging status](https://repology.org/badge/vertical-allrepos/trippy.svg)](https://repology.org/project/trippy/versions)\n\n## Downloads\n\nDownload the latest release for your platform.\n\n| OS      | Arch      | Env          | Current (0.12.2)                                                                                                              | Previous (0.11.0)                                                                                                             | Previous (0.10.0)                                                                                                             |\n| ------- | --------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| 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)       |\n| 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)      |\n| 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)      |\n| 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)     |\n| 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)  |\n| 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)   |\n| 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) |\n| 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)            |\n| 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)           |\n| 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)            |\n| 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)             |\n| 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)           |\n| 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)         |\n| 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)          |\n| 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)                            |\n| 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)    |\n| 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)   |\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/development/crates.md",
    "content": "---\ntitle: Crates\ndescription: A reference for the Trippy crates.\nslug: 0.13.0/development/crates\n---\n\nThe following table lists the crates that are provided by Trippy. See [crates](crates/README.md) for more information.\n\n| Crate                                                         | Description                                                                         |\n| ------------------------------------------------------------- | ----------------------------------------------------------------------------------- |\n| [trippy](https://crates.io/crates/trippy)                     | A binary crate for the Trippy application and a library crate                       |\n| [trippy-core](https://crates.io/crates/trippy-core)           | A library crate providing the core Trippy tracing functionality                     |\n| [trippy-packet](https://crates.io/crates/trippy-packet)       | A library crate which provides packet wire formats and packet parsing functionality |\n| [trippy-dns](https://crates.io/crates/trippy-dns)             | A library crate for performing forward and reverse lazy DNS resolution              |\n| [trippy-privilege](https://crates.io/crates/trippy-privilege) | A library crate for discovering platform privileges                                 |\n| [trippy-tui](https://crates.io/crates/trippy-tui)             | A library crate for the Trippy terminal user interface                              |\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/guides/faq.md",
    "content": "---\ntitle: Frequently Asked Questions\ndescription: Frequently asked questions about Trippy.\nsidebar:\n  order: 5\nslug: 0.13.0/guides/faq\n---\n\n## Why does Trippy show \"Awaiting data...\"?\n\n:::caution\nIf you are using Windows you _must_ [configure](/0.13.0/guides/windows_firewall)\nthe Windows Defender firewall to allow incoming ICMP traffic\n:::\n\nWhen Trippy shows “Awaiting data...” it means that it has received zero responses for the probes sent in a trace. This\nindicates that either probes are not being sent or, more typically, responses are not being received.\n\nCheck that local and network firewalls allow ICMP traffic and that the system `traceroute` (or `tracert.exe` on\nWindows) works as expected. Note that on Windows, even if `tracert.exe` works as expected, you\n_must_ [configure](/0.13.0/guides/windows_firewall) the Windows Defender\nfirewall to allow incoming ICMP traffic.\n\nFor deeper diagnostics you can run tools such as https://www.wireshark.org and https://www.tcpdump.org to verify that\nicmp requests and responses are being send and received.\n\n<a name=\"windows-defender\" />\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/guides/privileges.md",
    "content": "---\ntitle: Privileges\ndescription: A reference for the Trippy privileges.\nsidebar:\n  order: 2\nslug: 0.13.0/guides/privileges\n---\n\nTrippy normally requires elevated privileges due to the use of raw sockets. Enabling the required privileges for your\nplatform can be achieved in several ways, as outlined below. Trippy can also be used without elevated privileged on\ncertain platforms, with some limitations.\n\n## Unix\n\n1: Run as `root` user via `sudo`:\n\n```shell\nsudo trip example.com\n```\n\n2: `chown` `trip` as the `root` user and set the `setuid` bit:\n\n```shell\nsudo chown root $(which trip) && sudo chmod +s $(which trip)\n```\n\n3: \\[Linux only] Set the `CAP_NET_RAW` capability:\n\n```shell\nsudo setcap CAP_NET_RAW+p $(which trip)\n```\n\n:::note\nTrippy is a capability aware application and will add `CAP_NET_RAW` to the effective set if it is present in the allowed\nset. Trippy will drop all capabilities after creating the raw sockets.\n:::\n\n## Windows\n\nTrippy must be run with Administrator privileges on Windows.\n\n## Unprivileged mode\n\nTrippy allows running in an unprivileged mode for all tracing modes (`ICMP`, `UDP` and `TCP`) on platforms which support\nthat feature.\n\n:::note\nUnprivileged mode is currently only supported on macOS. Linux support is possible and may be added in the future.\nUnprivileged mode is not supported on NetBSD, FreeBSD or Windows as these platforms do not support the `IPPROTO_ICMP`\nsocket type. See [#101](https://github.com/fujiapple852/trippy/issues/101) for further information.\n:::\n\nThe unprivileged mode can be enabled by adding the `--unprivileged` (`-u`) command line flag or by adding the\n`unprivileged` entry in the `trippy` section of the [configuration file](/0.13.0/reference/configuration):\n\n```toml\n[trippy]\nunprivileged = true\n```\n\n:::note\nThe `paris` and `dublin` `ECMP` strategies are not supported in unprivileged mode as these require manipulating the\n`UDP` and `IP` and headers which in turn requires the use of a raw socket.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/guides/recommendation.md",
    "content": "---\ntitle: Recommended Tracing Settings\ndescription: Recommended settings for Trippy.\nsidebar:\n  order: 3\nslug: 0.13.0/guides/recommendation\n---\n\nTrippy provides a variety of configurable features which can be used to perform different types of analysis. The choice\nof settings will depend on the analysis you wish to perform and the environment in which you are working. This guide\nlists some common options along with some basic guidance on when they might be appropriate.\n\n:::note\nThe Windows `tracert` tool uses ICMP by default, whereas most Unix `traceroute` tools use UDP by default.\n:::\n\n## ICMP\n\nBy default Trippy will run an ICMP trace to the target. This will typically produce a consistent path to the target (a\nsingle flow) for each round of tracing which makes it easy to read and analyse. This is a useful mode for general\nnetwork troubleshooting.\n\nHowever, many routers are configured to rate-limit ICMP traffic which can make it difficult to get an accurate picture\nof packet loss. In addition, ICMP traffic is not typically subject to ECMP routing and so may not reflect the path that\nwould taken by other protocols such as UDP and TCP.\n\nTo run a simple ICMP trace:\n\n```shell\ntrip example.com\n```\n\nDue to the rate-limiting of ICMP traffic, some people prefer to hide the `Loss%` and `Recv` columns in the Tui as\nthese are easy to misinterpret.\n\n```shell\ntrip example.com --tui-custom-columns hosavbwdt\n```\n\nThese settings can be made permanent by adding them to the Trippy configuration file:\n\n```toml\n[tui]\ncustom-columns = \"hosavbwdt\"\n```\n\n:::note\nThe `Sts` column shows different color codes to reflect packet loss at intermediate vs the target hop, see the\n[Column Reference](/0.13.0/reference/column) for more information.\n:::\n\n#### UDP/Dublin with fixed ports\n\nUDP tracing provides a more realistic view of the path taken by traffic that is subject to ECMP routing.\n\nSetting a fixed target port in the range 33434-33534 may allow Trippy to determine that the probe has reached the target\nas many routers and firewalls are configured to allow UDP probes in that range and will respond with a Destination\nUnreachable response.\n\nHowever, running a UDP trace with a fixed target port and a variable source port will typically result in different\npaths being followed for each probe within each round of tracing. This can make it difficult to interpret the output as\ndifferent hosts will reply for a given hop (time-to-live) across rounds.\n\nBy using the `dublin` ECMP strategy, which encodes the sequence number in the IP `identifier` field, Trippy can fix both\nthe source and target ports, typically resulting in a _single_ path for each probe within each round of tracing.\n\n:::note\nUDP/Dublin for IPv6 encodes the sequence number as the payload length as the IP `identifier` field is not available in\nIPv6.\n:::\n\n:::note\nKeep in mind that every probe is an _independent trial_ and each may traverse a completely different path. In practice,\nICMP probes often follow a single path, whereas the path of UDP and TCP probes is typically determined by the 5-tuple of\nprotocol, source and destination IP addresses and ports.\n\nAlso beware that the return path may not be the same as the forward path, and may also differ for each probe. Strategies\nsuch as `dublin` and `paris` assist in controlling the path taken by the forward probes, but do not help control the\nreturn path. Therefore it is recommended to run a trace in both directions to get a complete picture.\n:::\n\nTo run a UDP trace with fixed source and target ports using the `dublin` ECMP strategy:\n\n```shell\ntrip example.com --udp --multipath-strategy dublin --source-port 5000 --target-port 33434\n```\n\n:::note\nThe source port can be any valid port number, but the target port should usually be in the range 33434-33534 or whatever\nrange is open to UDP probes on the target host.\n:::\n\nThese settings can be made permanent by adding them to the Trippy configuration file:\n\n```toml\n[strategy]\nprotocol = \"udp\"\nmultipath-strategy = \"dublin\"\nsource-port = 5000\ntarget-port = 33434\n```\n\n## UDP/Dublin with fixed target port and variable source port\n\nAs an extension to the above, if you do not fix the source port when using the `dublin` ECMP strategy, Trippy will\nvary the source port per _round_ of tracing (i.e. each probe within a given round will share the same source port, and\nthe source port will vary for each round). This will typically result in the _same_ path being followed for _each_ probe\nwithin a given round, but _different_ paths being followed for each round.\n\nThese individual flows can be explored in the Trippy Tui by pressing the `toggle-flows` key binding (`f` key by\ndefault).\n\nAdding the columns `Seq`, `Sprt` and `Dprt` to the Tui will show the sequence number, source port and destination port\nrespectively which makes this easier to visualize.\n\n```shell\ntrip example.com --udp --multipath-strategy dublin --target-port 33434 --tui-custom-columns holsravbwdtSPQ\n```\n\nThese settings can be made permanent by adding them to the Trippy configuration file:\n\n```toml\n[strategy]\nprotocol = \"udp\"\nmultipath-strategy = \"dublin\"\ntarget-port = 33434\n\n[tui]\ncustom-columns = \"holsravbwdtSPQ\"\n```\n\nTo make the flows easier to visualize, you can generate a Graphviz DOT file report of all tracing flows:\n\n```shell\ntrip example.com --udp --multipath-strategy dublin --target-port 33434 -m dot -C 5\n```\n\n## UDP/Paris\n\nUDP with the `paris` ECMP strategy offers the same benefits as the `dublin` strategy with fixed ports and can be used\nin the same way.\n\nThey differ in the way they encode the sequence number in the probe. The `dublin` strategy uses the IP `identifier`\nfield, whereas the `paris` strategy uses the UDP `checksum` field.\n\nTo run a UDP trace with fixed source and target ports using the `paris` ECMP strategy:\n\n```shell\ntrip example.com --udp --multipath-strategy paris --source-port 5000 --target-port 33434\n```\n\nThe `paris` strategy does not work behind NAT as the UDP `checksum` field is typically modified by NAT devices.\nTherefore the `dublin` strategy is recommended when NAT is present.\n\n:::note\nTrippy can detect the presence of NAT devices in some circumstances when using the `dublin` strategy and the `Nat`\ncolumn can be shown in the Tui to indicate when NAT is detected. See the [Column Reference](/0.13.0/reference/column) for more\ninformation.\n:::\n\n#### TCP\n\nTCP tracing is similar to UDP tracing in that it provides a more realistic view of the path taken by traffic that is\nsubject to ECMP routing.\n\nTCP tracing defaults to using a target port of 80 and sets the source port as the sequence number which will typically\nresult in a different path being followed for each probe within each round of tracing.\n\nTo run a TCP trace:\n\n```shell\ntrip example.com --tcp\n```\n\nTCP tracing is useful for diagnosing issues with TCP connections and higher layer protocols such as HTTP. Often UDP\ntracing can be used in place of TCP to diagnose IP layer network issues and, as it provides ways to control the path\ntaken by the probes, it is often preferred.\n\n:::note\nTrippy does not support the `dublin` or `paris` ECMP strategies for TCP tracing and so you cannot fix both the source\nand target ports. See the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/guides/usage.md",
    "content": "---\ntitle: Usage Examples\ndescription: Examples of how to use the Trippy command line interface.\nsidebar:\n  order: 1\nslug: 0.13.0/guides/usage\n---\n\nBasic usage with default parameters:\n\n```shell\ntrip example.com\n```\n\nTrace without requiring elevated privileges (supported platforms only, see [privileges](/0.13.0/guides/privileges)):\n\n```shell\ntrip example.com --unprivileged\n```\n\nTrace using the `udp` (or `tcp` or `icmp`) protocol (also aliases `--icmp`, `--udp` & `--tcp`):\n\n```shell\ntrip example.com -p udp\n```\n\nTrace to multiple targets simultaneously (`icmp` protocol only,\nsee [#72](https://github.com/fujiapple852/trippy/issues/72)):\n\n```shell\ntrip example.com google.com crates.io\n```\n\nTrace with a minimum round time of `250ms` and a grace period of `50ms`:\n\n```shell\ntrip example.com -i 250ms -g 50ms\n```\n\nTrace with a custom first and maximum `time-to-live`:\n\n```shell\ntrip example.com --first-ttl 2 --max-ttl 10\n```\n\nUse custom destination port `443` for `tcp` tracing:\n\n```shell\ntrip example.com -p tcp -P 443\n```\n\nUse custom source port `5000` for `udp` tracing:\n\n```shell\ntrip example.com -p udp -S 5000\n```\n\nUse the `dublin` (or `paris`) ECMP routing strategy for `udp` with fixed source and destination ports:\n\n```shell\ntrip example.com -p udp -R dublin -S 5000 -P 3500\n```\n\nTrace with a custom source address:\n\n```shell\ntrip example.com -p tcp -A 127.0.0.1\n```\n\nTrace with a source address determined by the IPv4 address for interface `en0`:\n\n```shell\ntrip example.com -p tcp -I en0\n```\n\nTrace using `IPv6`:\n\n```shell\ntrip example.com -6\n```\n\nTrace using `ipv4-then-ipv6` fallback (or `ipv6-then-ipv4` or `ipv4` or `ipv6`):\n\n```shell\ntrip example.com --addr-family ipv4-then-ipv6\n```\n\nGenerate a `json` (or `csv`, `pretty`, `markdown`) tracing report with 5 rounds of data:\n\n```shell\ntrip example.com -m json -C 5\n```\n\nGenerate a [Graphviz](https://graphviz.org) `DOT` file report of all tracing flows for a TCP trace after 5 rounds:\n\n```shell\ntrip example.com --tcp -m dot -C 5\n```\n\nGenerate a textual report of all tracing flows for a UDP trace after 5 rounds:\n\n```shell\ntrip example.com --udp -m flows -C 5\n```\n\nPerform DNS queries using the `google` DNS resolver (or `cloudflare`, `system`, `resolv`):\n\n```shell\ntrip example.com -r google\n```\n\nLookup \\[AS]\\[autonomous\\_system] information for all discovered IP addresses (not yet available for the `system` resolver,\nsee [#66](https://github.com/fujiapple852/trippy/issues/66)):\n\n```shell\ntrip example.com -r google -z\n```\n\nSet the reverse DNS lookup cache time-to-live to be 60 seconds:\n\n```shell\ntrip example.com --dns-ttl 60sec\n```\n\nLookup and display `short` (or `long` or `location` or `off`) GeoIp information from a `mmdb` file:\n\n```shell\ntrip example.com --geoip-mmdb-file GeoLite2-City.mmdb --tui-geoip-mode short\n```\n\nParse `icmp` extensions:\n\n```shell\ntrip example.com -e\n```\n\nHide the IP address, hostname and GeoIp for the first two hops:\n\n```shell\ntrip example.com --tui-privacy-max-ttl 2\n```\n\nCustomize Tui columns (see [Column Reference](/0.13.0/reference/column)):\n\n```shell\ntrip example.com --tui-custom-columns holsravbwdt\n```\n\nCustomize the color theme:\n\n```shell\ntrip example.com --tui-theme-colors bg-color=blue,text-color=ffff00\n```\n\nList all Tui items that can have a custom color theme:\n\n```shell\ntrip --print-tui-theme-items\n```\n\nCustomize the key bindings:\n\n```shell\ntrip example.com --tui-key-bindings previous-hop=k,next-hop=j,quit=shift-q\n```\n\nList all Tui commands that can have a custom key binding:\n\n```shell\ntrip --print-tui-binding-commands\n```\n\nSpecify the location of the Trippy config file:\n\n```shell\ntrip example.com --config-file /path/to/trippy.toml\n```\n\nGenerate a template configuration file:\n\n```shell\ntrip --print-config-template > trippy.toml\n```\n\nGenerate `bash` shell completions (or `fish`, `powershell`, `zsh`, `elvish`):\n\n```shell\ntrip --generate bash\n```\n\nGenerate `ROFF` man page:\n\n```shell\ntrip --generate-man\n```\n\nUse the `de` Tui locale:\n\n```shell\ntrip example.com --tui-locale de\n```\n\nList supported Tui locales:\n\n```shell\ntrip --print-locales\n```\n\nSet the Tui timezone to `UTC`:\n\n```shell\ntrip example.com --tui-timezone UTC\n```\n\nRun in `silent` tracing mode and output `compact` trace logging with `full` span events:\n\n```shell\ntrip example.com -m silent -v --log-format compact --log-span-events full\n```\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/guides/windows_firewall.md",
    "content": "---\ntitle: Windows Defender Firewall\ndescription: Allow incoming ICMP traffic in the Windows Defender firewall.\nsidebar:\n  order: 4\nslug: 0.13.0/guides/windows_firewall\n---\n\nThe Windows Defender firewall rule can be created using PowerShell.\n\n```shell\nNew-NetFirewallRule -DisplayName \"ICMPv4 Trippy Allow\" -Name ICMPv4_TRIPPY_ALLOW -Protocol ICMPv4 -Action Allow\nNew-NetFirewallRule -DisplayName \"ICMPv6 Trippy Allow\" -Name ICMPv6_TRIPPY_ALLOW -Protocol ICMPv6 -Action Allow\n```\n\nThe rules can be enabled as follows:\n\n```shell\nEnable-NetFirewallRule ICMPv4_TRIPPY_ALLOW\nEnable-NetFirewallRule ICMPv6_TRIPPY_ALLOW\n```\n\nThe rules can be disabled as follows:\n\n```shell\nDisable-NetFirewallRule ICMPv4_TRIPPY_ALLOW\nDisable-NetFirewallRule ICMPv6_TRIPPY_ALLOW\n```\n\nThere is a [step-by-step guide to manually configure the Windows Defender firewall rule](https://github.com/fujiapple852/trippy/issues/578#issuecomment-1565149826).\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/index.mdx",
    "content": "---\ntitle: \"Trippy: a network diagnostic tool\"\ndescription: a network diagnostic tool.\ntemplate: splash\nhero:\n  tagline: Trippy combines the functionality of traceroute and ping and is\n    designed to assist with the analysis of networking issues.\n  image:\n    alt: Trippy, man!\n    light: ../../../assets/0.13.0/Trippy-Emblem.svg\n    dark: ../../../assets/0.13.0/Trippy-Emblem-DarkMode.svg\n  actions:\n    - text: Get Started\n      link: /0.13.0/start/getting-started/\n      icon: right-arrow\n    - text: Read the docs\n      link: /0.13.0/reference/overview/\n      icon: open-book\n      variant: secondary\n    - text: View on GitHub\n      link: https://github.com/fujiapple852/trippy\n      icon: github\n      variant: secondary\nslug: 0.13.0\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\nimport { Icon } from '@astrojs/starlight/components';\n\n<CardGrid stagger>\n  <Card title=\"Powerful tracing features\" icon=\"rocket\">\n    * `ICMP`, `UDP` & `TCP` over `IPv4` & `IPv6` protocols\n    * Fully customizable tracing options\n    * `dublin` and `paris` `ECMP` strategies\n    * `ICMP` extensions objects (i.e. `MPLS`)\n    * Reverse `DNS` and `ASN` lookups\n    * `NAT` detection\n\n    ![Trippy main screen](../../../assets/0.13.0/main_screen.png)\n  </Card>\n\n  <Card title=\"Visualize GeoIp on a world map\" icon=\"star\">\n    * Lookup GeoIp information and show on world map\n    * Support for both `MaxMind` and `IPinfo` databases\n\n    ![Trippy GeoIp world map](../../../assets/0.13.0/world_map.png)\n  </Card>\n\n  <Card title=\"Run on your platform\" icon=\"star\">\n    * Runs on `Linux`, `macOS`, `Windows`, `*BSD`\n    * Supports `x86_64`, `aarch64`, `arm7` architectures\n    * Available from most native package managers\n    * Run in unprivileged mode\n\n    ![Trippy on Windows](../../../assets/0.13.0/windows.png)\n  </Card>\n\n  <Card title=\"Highly customizable TUI\" icon=\"seti:config\">\n    * Customizable columns, color themes and key bindings\n    * Hop detail navigation mode\n    * Hop privacy mode\n    * Show individual tracing flows\n    * Various charts and statistics\n    * Persist configuration to file\n\n    ![Trippy settings](../../../assets/0.13.0/settings.png)\n  </Card>\n\n  <Card title=\"Trace in your language\" icon=\"translate\">\n    TUI available in 10 languages:\n\n    * Chinese 🇨🇳, English 🇺🇸, French 🇫🇷, German 🇩🇪, Italian 🇮🇹, Portuguese 🇵🇹, Russian 🇷🇺, Spanish 🇪🇸, Swedish 🇸🇪 and Turkish 🇹🇷\n\n    ![Trippy main screen in Chinese](../../../assets/0.13.0/help_screen_zh.png)\n  </Card>\n</CardGrid>\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/reference/bindings.md",
    "content": "---\ntitle: Key Bindings Reference\ndescription: A reference for customizing the Trippy TUI key bindings.\nsidebar:\n  order: 3\nslug: 0.13.0/reference/bindings\n---\n\nThe following table lists the default Tui command key bindings. These can be overridden with the `--tui-key-bindings`\ncommand line option or in the `bindings` section of the configuration file.\n\n| Command                    | Description                                     | Default   |\n| -------------------------- | ----------------------------------------------- | --------- |\n| `toggle-help`              | Toggle help                                     | `h`       |\n| `toggle-help-alt`          | Toggle help (alternative binding)               | `?`       |\n| `toggle-settings`          | Toggle settings                                 | `s`       |\n| `toggle-settings-tui`      | Open settings (Tui tab)                         | `1`       |\n| `toggle-settings-trace`    | Open settings (Trace tab)                       | `2`       |\n| `toggle-settings-dns`      | Open settings (Dns tab)                         | `3`       |\n| `toggle-settings-geoip`    | Open settings (GeoIp tab)                       | `4`       |\n| `toggle-settings-bindings` | Open settings (Bindings tab)                    | `5`       |\n| `toggle-settings-theme`    | Open settings (Theme tab)                       | `6`       |\n| `toggle-settings-columns`  | Open settings (Columns tab)                     | `7`       |\n| `next-hop`                 | Select next hop                                 | `down`    |\n| `previous-hop`             | Select previous hop                             | `up`      |\n| `next-trace`               | Select next trace                               | `right`   |\n| `previous-trace`           | Select previous trace                           | `left`    |\n| `next-hop-address`         | Select next hop address                         | `.`       |\n| `previous-hop-address`     | Select previous hop address                     | `,`       |\n| `address-mode-ip`          | Show IP address only                            | `i`       |\n| `address-mode-host`        | Show hostname only                              | `n`       |\n| `address-mode-both`        | Show both IP address and hostname               | `b`       |\n| `toggle-freeze`            | Toggle freezing the display                     | `ctrl+f`  |\n| `toggle-chart`             | Toggle the chart                                | `c`       |\n| `toggle-map`               | Toggle the GeoIp map                            | `m`       |\n| `toggle-flows`             | Toggle the flows                                | `f`       |\n| `expand-privacy`           | Expand hop privacy                              | `p`       |\n| `contract-privacy`         | Contract hop privacy                            | `o`       |\n| `expand-hosts`             | Expand the hosts shown per hop                  | `]`       |\n| `expand-hosts-max`         | Expand the hosts shown per hop to the maximum   | `}`       |\n| `contract-hosts`           | Contract the hosts shown per hop                | `[`       |\n| `contract-hosts-min`       | Contract the hosts shown per hop to the minimum | `{`       |\n| `chart-zoom-in`            | Zoom in the chart                               | `=`       |\n| `chart-zoom-out`           | Zoom out the chart                              | `-`       |\n| `clear-trace-data`         | Clear all trace data                            | `ctrl+r`  |\n| `clear-dns-cache`          | Flush the DNS cache                             | `ctrl+k`  |\n| `clear-selection`          | Clear the current selection                     | `esc`     |\n| `toggle-as-info`           | Toggle AS info display                          | `z`       |\n| `toggle-hop-details`       | Toggle hop details                              | `d`       |\n| `quit`                     | Quit the application                            | `q`       |\n| `quit-preserve-screen`     | Quit the application and preserve the screen    | `shift+q` |\n\nThe supported modifiers are: `shift`, `ctrl`, `alt`, `super`, `hyper` & `meta`. Multiple modifiers may be specified, for\nexample `ctrl+shift+b`.\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/reference/cli.md",
    "content": "---\ntitle: CLI Reference\ndescription: A reference for the Trippy command line interface.\nsidebar:\n  order: 1\nslug: 0.13.0/reference/cli\n---\n\n```text\nA network diagnostic tool\n\nUsage: trip [OPTIONS] [TARGETS]...\n\nArguments:\n  [TARGETS]...\n          A space delimited list of hostnames and IPs to trace\n\nOptions:\n  -c, --config-file <CONFIG_FILE>\n          Config file\n\n  -m, --mode <MODE>\n          Output mode [default: tui]\n\n          Possible values:\n          - tui:      Display interactive TUI\n          - stream:   Display a continuous stream of tracing data\n          - pretty:   Generate a pretty text table report for N cycles\n          - markdown: Generate a Markdown text table report for N cycles\n          - csv:      Generate a CSV report for N cycles\n          - json:     Generate a JSON report for N cycles\n          - dot:      Generate a Graphviz DOT file for N cycles\n          - flows:    Display all flows for N cycles\n          - silent:   Do not generate any tracing output for N cycles\n\n  -u, --unprivileged\n          Trace without requiring elevated privileges on supported platforms\n          [default: false]\n\n  -p, --protocol <PROTOCOL>\n          Tracing protocol [default: icmp]\n\n          Possible values:\n          - icmp: Internet Control Message Protocol\n          - udp:  User Datagram Protocol\n          - tcp:  Transmission Control Protocol\n\n      --udp\n          Trace using the UDP protocol\n\n      --tcp\n          Trace using the TCP protocol\n\n      --icmp\n          Trace using the ICMP protocol\n\n  -F, --addr-family <ADDR_FAMILY>\n          The address family [default: ipv4-then-ipv6]\n\n          Possible values:\n          - ipv4:           IPv4 only\n          - ipv6:           IPv6 only\n          - ipv6-then-ipv4: IPv6 with a fallback to IPv4\n          - ipv4-then-ipv6: IPv4 with a fallback to IPv6\n          - system:         If the OS resolver is being used then use the first IP address returned, \n                            otherwise lookup IPv6 with a fallback to IPv4\n\n  -4, --ipv4\n          Use IPv4 only\n\n  -6, --ipv6\n          Use IPv6 only\n\n  -P, --target-port <TARGET_PORT>\n          The target port (TCP & UDP only) [default: 80]\n\n  -S, --source-port <SOURCE_PORT>\n          The source port (TCP & UDP only) [default: auto]\n\n  -A, --source-address <SOURCE_ADDRESS>\n          The source IP address [default: auto]\n\n  -I, --interface <INTERFACE>\n          The network interface [default: auto]\n\n  -i, --min-round-duration <MIN_ROUND_DURATION>\n          The minimum duration of every round [default: 1s]\n\n  -T, --max-round-duration <MAX_ROUND_DURATION>\n          The maximum duration of every round [default: 1s]\n\n  -g, --grace-duration <GRACE_DURATION>\n          The period of time to wait for additional ICMP responses after the\n          target has responded [default: 100ms]\n\n      --initial-sequence <INITIAL_SEQUENCE>\n          The initial sequence number [default: 33434]\n\n  -R, --multipath-strategy <MULTIPATH_STRATEGY>\n          The Equal-cost Multi-Path routing strategy (UDP only) [default:\n          classic]\n\n          Possible values:\n          - classic:\n            The src or dest port is used to store the sequence number\n          - paris:\n            The UDP `checksum` field is used to store the sequence number\n          - dublin:\n            The IP `identifier` field is used to store the sequence number\n\n  -U, --max-inflight <MAX_INFLIGHT>\n          The maximum number of in-flight ICMP echo requests [default: 24]\n\n  -f, --first-ttl <FIRST_TTL>\n          The TTL to start from [default: 1]\n\n  -t, --max-ttl <MAX_TTL>\n          The maximum number of TTL hops [default: 64]\n\n      --packet-size <PACKET_SIZE>\n          The size of IP packet to send (IP header + ICMP header + payload)\n          [default: 84]\n\n      --payload-pattern <PAYLOAD_PATTERN>\n          The repeating pattern in the payload of the ICMP packet [default: 0]\n\n  -Q, --tos <TOS>\n          The TOS (i.e. DSCP+ECN) IP header value (IPv4 only) [default: 0]\n\n  -e, --icmp-extensions\n          Parse ICMP extensions\n\n      --read-timeout <READ_TIMEOUT>\n          The socket read timeout [default: 10ms]\n\n  -r, --dns-resolve-method <DNS_RESOLVE_METHOD>\n          How to perform DNS queries [default: system]\n\n          Possible values:\n          - system:     Resolve using the OS resolver\n          - resolv:     Resolve using the `/etc/resolv.conf` DNS configuration\n          - google:     Resolve using the Google `8.8.8.8` DNS service\n          - cloudflare: Resolve using the Cloudflare `1.1.1.1` DNS service\n\n  -y, --dns-resolve-all\n          Trace to all IPs resolved from DNS lookup [default: false]\n\n      --dns-timeout <DNS_TIMEOUT>\n          The maximum time to wait to perform DNS queries [default: 5s]\n\n      --dns-ttl <DNS_TTL>\n          The time-to-live (TTL) of DNS entries [default: 300s]\n\n  -z, --dns-lookup-as-info\n          Lookup autonomous system (AS) information during DNS queries [default:\n          false]\n\n  -s, --max-samples <MAX_SAMPLES>\n          The maximum number of samples to record per hop [default: 256]\n\n      --max-flows <MAX_FLOWS>\n          The maximum number of flows to record [default: 64]\n\n  -a, --tui-address-mode <TUI_ADDRESS_MODE>\n          How to render addresses [default: host]\n\n          Possible values:\n          - ip:   Show IP address only\n          - host: Show reverse-lookup DNS hostname only\n          - both: Show both IP address and reverse-lookup DNS hostname\n\n      --tui-as-mode <TUI_AS_MODE>\n          How to render autonomous system (AS) information [default: asn]\n\n          Possible values:\n          - asn:          Show the ASN\n          - prefix:       Display the AS prefix\n          - country-code: Display the country code\n          - registry:     Display the registry name\n          - allocated:    Display the allocated date\n          - name:         Display the AS name\n\n      --tui-custom-columns <TUI_CUSTOM_COLUMNS>\n          Custom columns to be displayed in the TUI hops table [default:\n          holsravbwdt]\n\n      --tui-icmp-extension-mode <TUI_ICMP_EXTENSION_MODE>\n          How to render ICMP extensions [default: off]\n\n          Possible values:\n          - off:  Do not show `icmp` extensions\n          - mpls: Show MPLS label(s) only\n          - full: Show full `icmp` extension data for all known extensions\n          - all:  Show full `icmp` extension data for all classes\n\n      --tui-geoip-mode <TUI_GEOIP_MODE>\n          How to render GeoIp information [default: short]\n\n          Possible values:\n          - off:      Do not display GeoIp data\n          - short:    Show short format\n          - long:     Show long format\n          - location: Show latitude and Longitude format\n\n  -M, --tui-max-addrs <TUI_MAX_ADDRS>\n          The maximum number of addresses to show per hop [default: auto]\n\n      --tui-preserve-screen\n          Preserve the screen on exit [default: false]\n\n      --tui-refresh-rate <TUI_REFRESH_RATE>\n          The TUI refresh rate [default: 100ms]\n\n      --tui-privacy-max-ttl <TUI_PRIVACY_MAX_TTL>\n          The maximum ttl of hops which will be masked for privacy [default: none]\n\n          If set, the source IP address and hostname will also be hidden.\n\n      --tui-locale <TUI_LOCALE>\n          The locale to use for the TUI [default: auto]\n\n      --tui-timezone <TUI_TIMEZONE>\n          The timezone to use for the TUI [default: auto]\n\n          The timezone must be a valid IANA timezone identifier.\n\n      --tui-theme-colors <TUI_THEME_COLORS>\n          The TUI theme colors [item=color,item=color,..]\n\n      --print-tui-theme-items\n          Print all TUI theme items and exit\n\n      --tui-key-bindings <TUI_KEY_BINDINGS>\n          The TUI key bindings [command=key,command=key,..]\n\n      --print-tui-binding-commands\n          Print all TUI commands that can be bound and exit\n\n  -C, --report-cycles <REPORT_CYCLES>\n          The number of report cycles to run [default: 10]\n\n  -G, --geoip-mmdb-file <GEOIP_MMDB_FILE>\n          The supported MaxMind or IPinfo GeoIp mmdb file\n\n      --generate <GENERATE>\n          Generate shell completion\n\n          [possible values: bash, elvish, fish, powershell, zsh]\n\n      --generate-man\n          Generate ROFF man page\n\n      --print-config-template\n          Print a template toml config file and exit\n\n      --print-locales\n          Print all available TUI locales and exit\n\n      --log-format <LOG_FORMAT>\n          The debug log format [default: pretty]\n\n          Possible values:\n          - compact: Display log data in a compact format\n          - pretty:  Display log data in a pretty format\n          - json:    Display log data in a json format\n          - chrome:  Display log data in Chrome trace format\n\n      --log-filter <LOG_FILTER>\n          The debug log filter [default: trippy=debug]\n\n      --log-span-events <LOG_SPAN_EVENTS>\n          The debug log format [default: off]\n\n          Possible values:\n          - off:    Do not display event spans\n          - active: Display enter and exit event spans\n          - full:   Display all event spans\n\n  -v, --verbose\n          Enable verbose debug logging\n\n  -h, --help\n          Print help (see a summary with '-h')\n\n  -V, --version\n          Print version\n```\n\n:::note\nTrippy command line arguments may be given in any order and my occur both before and after the targets.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/reference/column.md",
    "content": "---\ntitle: Column Reference\ndescription: A reference for customizing the Trippy TUI columns.\nsidebar:\n  order: 4\nslug: 0.13.0/reference/column\n---\n\nThe following table lists the columns that are available for display in the Tui. These can be overridden with the\n`--tui-custom-columns` command line option or in the `tui-custom-columns` attribute in the `tui` section of the\nconfiguration file.\n\n| Column   | Code | Description                                                                                                                                                                                                                                                                                                                                                |\n| -------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `#`      | `h`  | The time-to-live (TTL) for the hop                                                                                                                                                                                                                                                                                                                         |\n| `Host`   | `o`  | The hostname(s) and IP address(s) for the host(s) for the hop<br />May include AS info, GeoIp and ICMP extensions<br />Shows full hop details in hop detail navigation mode                                                                                                                                                                                |\n| `Loss%`  | `l`  | The packet loss % for the hop                                                                                                                                                                                                                                                                                                                              |\n| `Snd`    | `s`  | The number of probes sent for the hop                                                                                                                                                                                                                                                                                                                      |\n| `Recv`   | `r`  | The number of probe responses received for the hop                                                                                                                                                                                                                                                                                                         |\n| `Last`   | `a`  | The round-trip-time (RTT) of the last probe for the hop                                                                                                                                                                                                                                                                                                    |\n| `Avg`    | `v`  | The average RTT of all probes for the hop                                                                                                                                                                                                                                                                                                                  |\n| `Best`   | `b`  | The best RTT of all probes for the hop                                                                                                                                                                                                                                                                                                                     |\n| `Wrst`   | `w`  | The worst RTT of all probes for the hop                                                                                                                                                                                                                                                                                                                    |\n| `StDev`  | `d`  | The standard deviation of all probes for the hop                                                                                                                                                                                                                                                                                                           |\n| `Sts`    | `t`  | The status for the hop:<br />- 🟢 Healthy hop<br />- 🔵 Non-target hop with packet loss (does not necessarily indicate a problem)<br />- 🟤 Non-target hop is unresponsive (does not necessarily indicate a problem) <br />- 🟡 Target hop with packet loss (likely indicates a problem)<br />- 🔴 Target hop is unresponsive (likely indicates a problem) |\n| `Jttr`   | `j`  | The round-trip-time (RTT) difference between consecutive rounds for the hop                                                                                                                                                                                                                                                                                |\n| `Javg`   | `g`  | The average jitter of all probes for the hop                                                                                                                                                                                                                                                                                                               |\n| `Jmax`   | `x`  | The maximum jitter of all probes for the hop                                                                                                                                                                                                                                                                                                               |\n| `Jint`   | `i`  | The smoothed jitter value of all probes for the hop                                                                                                                                                                                                                                                                                                        |\n| `Seq`    | `Q`  | The sequence number for the last probe for the hop                                                                                                                                                                                                                                                                                                         |\n| `Sprt`   | `S`  | The source port for the last probe for the hop                                                                                                                                                                                                                                                                                                             |\n| `Dprt`   | `P`  | The destination port for the last probe for the hop                                                                                                                                                                                                                                                                                                        |\n| `Type`   | `T`  | The icmp packet type for the last probe for the hop:<br />- TE: TimeExceeded<br />- ER: EchoReply<br />- DU: DestinationUnreachable<br />- NA: NotApplicable                                                                                                                                                                                               |\n| `Code`   | `C`  | The icmp packet code for the last probe for the hop                                                                                                                                                                                                                                                                                                        |\n| `Nat`    | `N`  | The NAT detection status for the hop                                                                                                                                                                                                                                                                                                                       |\n| `Fail`   | `f`  | The number of probes which failed to send for the hop                                                                                                                                                                                                                                                                                                      |\n| `Floss`  | `F`  | A _heuristic_ for the number of probes with _forward loss_ for the hop                                                                                                                                                                                                                                                                                     |\n| `Bloss`  | `B`  | A _heuristic_ for the number of probes with _backward loss_ for the hop                                                                                                                                                                                                                                                                                    |\n| `Floss%` | `D`  | The _forward loss_ % for the hop                                                                                                                                                                                                                                                                                                                           |\n| `Dscp`   | `K`  | Differentiated Services Code Point (DSCP) of the Original Datagram                                                                                                                                                                                                                                                                                         |\n| `Ecn`    | `M`  | Explicit Congestion Notification (ECN) of the Original Datagram                                                                                                                                                                                                                                                                                            |\n\nThe default columns are `holsravbwdt`.\n\n:::note\nThe columns will be shown in the order specified in the configuration.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/reference/configuration.md",
    "content": "---\ntitle: Configuration Reference\ndescription: A reference for customizing the Trippy configuration.\nsidebar:\n  order: 2\nslug: 0.13.0/reference/configuration\n---\n\nTrippy can be configured with via command line arguments or an optional configuration file. If a given configuration\nitem is specified in both the configuration file and via a command line argument then the latter will take precedence.\n\nThe configuration file location may be provided to Trippy via the `-c` (`--config-file`) argument. If not provided,\nTrippy will attempt to locate a `trippy.toml` or `.trippy.toml` configuration file in one of the following locations:\n\n- The current directory\n- The user home directory\n- the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config`\n- the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy`\n- the Windows data directory (Windows only): `%APPDATA%`\n\nA template configuration file\nfor [0.13.0](https://github.com/fujiapple852/trippy/blob/0.13.0/trippy-config-sample.toml) is available to\ndownload, or can be generated with the following command:\n\n```shell\ntrip --print-config-template > trippy.toml\n```\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/reference/locale.md",
    "content": "---\ntitle: Locale Reference\ndescription: A reference for customizing the Trippy TUI locale.\nsidebar:\n  order: 6\nslug: 0.13.0/reference/locale\n---\n\nThe following table lists the supported locales for the Tui. These can be overridden with the `--tui-locale` command\nline option or in the `tui-locale` attribute in the `tui` section of the configuration file.\n\n| Locale | Language   | Region |\n| ------ | ---------- | ------ |\n| `zh`   | Chinese    | all    |\n| `en`   | English    | all    |\n| `fr`   | French     | all    |\n| `de`   | German     | all    |\n| `it`   | Italian    | all    |\n| `pt`   | Portuguese | all    |\n| `ru`   | Russian    | all    |\n| `es`   | Spanish    | all    |\n| `sv`   | Swedish    | all    |\n| `tr`   | Turkish    | all    |\n\n:::note\nIf you are able to help validate translations for Trippy, or if you wish to add translations for any additional\nlanguages, please see the [tracking issue](https://github.com/fujiapple852/trippy/issues/506) for details of how to\ncontribute.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/reference/overview.mdx",
    "content": "---\ntitle: Trippy Reference\ndescription: Reference documentation for Trippy.\nsidebar:\n  order: 0\n  badge:\n    text: New\n    variant: note\nslug: 0.13.0/reference/overview\n---\n\nThis section provides complete reference documentation for Trippy.\n\n:::note\nThis reference documentation is intended for users who are already familiar with Trippy and want to learn more about\nits features and capabilities. If you are new to Trippy, it is recommend that you start by reading the [getting\nstarted](/0.13.0/start/getting-started) guide.\n:::\n\n### CLI Reference\n\nThe [CLI reference](/0.13.0/reference/cli) provides a complete list of the command line options available for Trippy. This\ninformation is available via the `--help` command line option and also in the man page on supported platforms.\n\n### Configuration Reference\n\nTrippy can be configured via an optional configuration file. The [configuration reference](/0.13.0/reference/configuration)\nprovides details of how to configure Trippy via the configuration file.\n\n### Key Bindings Reference\n\nThe Trippy TUI is highly customizable and allows you to change the key bindings to suit your preferences. The [key\nbindings reference](/0.13.0/reference/bindings) provides a complete list of the available key bindings and their\ndescriptions.\n\n### Column Reference\n\nThe list of columns that can be displayed in the TUI can be customized to suit your needs. The [column\nreference](/0.13.0/reference/column) provides a complete list of the available columns and their descriptions.\n\n### Theme Reference\n\nThe color schema of the TUI can be fully customized. The [theme reference](/0.13.0/reference/theme) provides a complete list of\nthe items which can be customized and their descriptions.\n\n### Locale Reference\n\nThe Trippy TUI supports multiple languages and regions. The [locale reference](/0.13.0/reference/locale) provides a complete\nlist of supported locales.\n\n### Version Reference\n\nAll versions of Trippy and their support status are listed in the [version reference](/0.13.0/reference/version).\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/reference/theme.md",
    "content": "---\ntitle: Theme Reference\ndescription: A reference for customizing the Trippy TUI theme.\nsidebar:\n  order: 5\nslug: 0.13.0/reference/theme\n---\n\nThe following table lists the default Tui color theme. These can be overridden with the `--tui-theme-colors` command\nline option or in the `theme-colors` section of the configuration file.\n\n| Item                                 | Description                                               | Default      |\n| ------------------------------------ | --------------------------------------------------------- | ------------ |\n| `bg-color`                           | The default background color                              | `Black`      |\n| `border-color`                       | The default color of borders                              | `Gray`       |\n| `text-color`                         | The default color of text                                 | `Gray`       |\n| `tab-text-color`                     | The color of the text in traces tabs                      | `Green`      |\n| `hops-table-header-bg-color`         | The background color of the hops table header             | `White`      |\n| `hops-table-header-text-color`       | The color of text in the hops table header                | `Black`      |\n| `hops-table-row-active-text-color`   | The color of text of active rows in the hops table        | `Gray`       |\n| `hops-table-row-inactive-text-color` | The color of text of inactive rows in the hops table      | `DarkGray`   |\n| `hops-chart-selected-color`          | The color of the selected series in the hops chart        | `Green`      |\n| `hops-chart-unselected-color`        | The color of the unselected series in the hops chart      | `Gray`       |\n| `hops-chart-axis-color`              | The color of the axis in the hops chart                   | `DarkGray`   |\n| `frequency-chart-bar-color`          | The color of bars in the frequency chart                  | `Green`      |\n| `frequency-chart-text-color`         | The color of text in the bars of the frequency chart      | `Gray`       |\n| `flows-chart-bar-selected-color`     | The color of the selected flow bar in the flows chart     | `Green`      |\n| `flows-chart-bar-unselected-color`   | The color of the unselected flow bar in the flows chart   | `DarkGray`   |\n| `flows-chart-text-current-color`     | The color of the current flow text in the flows chart     | `LightGreen` |\n| `flows-chart-text-non-current-color` | The color of the non-current flow text in the flows chart | `White`      |\n| `samples-chart-color`                | The color of the samples chart                            | `Yellow`     |\n| `samples-chart-lost-color`           | The color of the samples chart for lost probes            | `Red`        |\n| `help-dialog-bg-color`               | The background color of the help dialog                   | `Blue`       |\n| `help-dialog-text-color`             | The color of the text in the help dialog                  | `Gray`       |\n| `settings-dialog-bg-color`           | The background color of the settings dialog               | `blue`       |\n| `settings-tab-text-color`            | The color of the text in settings dialog tabs             | `green`      |\n| `settings-table-header-text-color`   | The color of text in the settings table header            | `black`      |\n| `settings-table-header-bg-color`     | The background color of the settings table header         | `white`      |\n| `settings-table-row-text-color`      | The color of text of rows in the settings table           | `gray`       |\n| `map-world-color`                    | The color of the map world diagram                        | `white`      |\n| `map-radius-color`                   | The color of the map accuracy radius circle               | `yellow`     |\n| `map-selected-color`                 | The color of the map selected item box                    | `green`      |\n| `map-info-panel-border-color`        | The color of border of the map info panel                 | `gray`       |\n| `map-info-panel-bg-color`            | The background color of the map info panel                | `black`      |\n| `map-info-panel-text-color`          | The color of text in the map info panel                   | `gray`       |\n| `info-bar-bg-color`                  | The background color of the information bar               | `white`      |\n| `info-bar-text-color`                | The color of text in the information bar                  | `black`      |\n\nThe supported [ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) are:\n\n- `Black`, `Red`, `Green`, `Yellow`, `Blue`, `Magenta`, `Cyan`, `Gray`, `DarkGray`, `LightRed`, `LightGreen`,\n  `LightYellow`, `LightBlue`, `LightMagenta`, `LightCyan`, `White`\n\nIn addition, CSS [named colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color) (i.e. SkyBlue) and raw hex\nvalues (i.e. ffffff) may be used but note that these are only supported on some platforms and terminals and may not\nrender correctly elsewhere.\n\nColor names are case-insensitive and may contain dashes.\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/reference/version.md",
    "content": "---\ntitle: Version Reference\ndescription: A reference for the Trippy versions.\nsidebar:\n  order: 7\nslug: 0.13.0/reference/version\n---\n\nThe following table lists this versions of Trippy that are available and links to the corresponding release note and\ndocumentation:\n\n| Version    | Release Date | Status      | Release Note                                                       | Documentation                                              |\n| ---------- | ------------ | ----------- | ------------------------------------------------------------------ | ---------------------------------------------------------- |\n| 0.14.0-dev | n/a          | Development | n/a                                                                | [docs](https://trippy.rs)                                  |\n| 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)                           |\n| 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)                           |\n| 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) |\n| 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) |\n| 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)  |\n| 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)  |\n| 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)  |\n| 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)  |\n\n:::note\nOnly the _latest patch versions_ of both the _current_ and _previous_ releases of Trippy are supported.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/start/features.md",
    "content": "---\ntitle: Features\ndescription: Learn about the features of Trippy.\nsidebar:\n  order: 3\nslug: 0.13.0/start/features\n---\n\n- Trace using multiple protocols:\n  - `ICMP`, `UDP` & `TCP`\n  - `IPv4` & `IPv6`\n- Customizable tracing options:\n  - packet size & payload pattern\n  - start and maximum time-to-live (TTL)\n  - minimum and maximum round duration\n  - round end grace period & maximum number of unknown hops\n  - source & destination port (`TCP` & `UDP`)\n  - source address and source interface\n  - `TOS` (aka `DSCP + ECN`)\n- Support for `classic`, `paris`\n  and `dublin` [Equal Cost Multi-path Routing](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing)\n  strategies ([tracking issue](https://github.com/fujiapple852/trippy/issues/274))\n- RFC4884 [ICMP Multi-Part Messages](https://datatracker.ietf.org/doc/html/rfc4884)\n  - Generic Extension Objects\n  - MPLS Label Stacks\n- Unprivileged mode\n- NAT detection\n- Tui interface:\n  - Trace multiple targets simultaneously from a single instance of Trippy\n  - Per hop stats (sent, received, loss%, last, avg, best, worst, stddev, jitter & status)\n  - Per hop round-trip-time (RTT) history and frequency distributing charts\n  - Interactive chart of RTT for all hops in a trace with zooming capability\n  - Interactive GeoIp world map\n  - Isolate and filter by individual tracing flows\n  - Customizable color theme & key bindings\n  - Customizable column order and visibility\n  - Configuration via both command line arguments and a configuration file\n  - Show multiple hosts per hop with ability to cap display to N hosts and show frequency %\n  - Show hop details and navigate hosts within each hop\n  - Freeze/unfreeze the Tui, reset the stats, flush the cache, preserve screen on exit\n  - Responsive UI with adjustable refresh rate\n  - Hop privacy\n  - Multiple language support\n  - Customizable timezone\n- DNS:\n  - Use system, external (Google `8.8.8.8` or Cloudflare `1.1.1.1`) or custom resolver\n  - Lazy reverse DNS queries\n  - Lookup \\[autonomous system]\\[autonomous\\_system] number (ASN) and name\n- GeoIp:\n  - Lookup and display GeoIp information from local [MaxMind](https://www.maxmind.com)\n    and [IPinfo](https://ipinfo.io) `mmdb` files\n- Generate tracing reports:\n  - `json`, `csv` & tabular (pretty-printed and markdown)\n  - Tracing `flows` report\n  - Graphviz `dot` charts\n  - configurable reporting cycles\n- Runs on multiple platform (macOS, Linux, Windows, NetBSD, FreeBSD, OpenBSD)\n- Capabilities aware application (Linux only)\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/start/getting-started.mdx",
    "content": "---\ntitle: Getting Started\ndescription: Get started with Trippy.\nsidebar:\n  order: 1\nslug: 0.13.0/start/getting-started\n---\n\nimport { Steps } from '@astrojs/starlight/components';\n\nThe following steps will guide you through the process of installing and running Trippy.\n\n<Steps>\n  1. Install Trippy:\n\n     Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled binaries, or source.\n\n     For example, to install Trippy from `cargo`:\n\n     ```shell\n     cargo install trippy --locked\n     ```\n\n     See the [installation guide](/0.13.0/start/installation) for details of how to install Trippy on your system.\n\n  2. Run Trippy:\n\n     To run a basic trace to `example.com` with default settings, use the following command:\n\n     ```shell\n     sudo trip example.com\n     ```\n\n     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).\n\n  3. Customize the key bindings, theme and columns:\n\n     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.\n\n     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.\n\n  4. Review the tracing recommendations:\n\n     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.\n</Steps>\n\nHappy tracing!\n"
  },
  {
    "path": "docs/src/content/docs/0.13.0/start/installation.md",
    "content": "---\ntitle: Installation\ndescription: Install Trippy on your platform.\nsidebar:\n  order: 2\nslug: 0.13.0/start/installation\n---\n\nThe following sections provide instructions for installing Trippy on your platform.\n\nTrippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled\nbinaries, or source.\n\n## Distributions\n\nTrippy is available for a variety of platforms and package managers.\n\n### Cargo\n\n[![Crates.io](https://img.shields.io/crates/v/trippy)](https://crates.io/crates/trippy/0.13.0)\n\n```shell\ncargo install trippy --locked\n```\n\n### APT (Debian)\n\n[![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy)\n\n```shell\napt install trippy\n```\n\n:::note\nOnly available for Debian 13 (`trixie`) and later.\n:::\n\n### PPA (Ubuntu)\n\n[![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.13.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages)\n\n```shell\nadd-apt-repository ppa:fujiapple/trippy\napt update && apt install trippy\n```\n\n:::note\nOnly available for Ubuntu 24.04 (`Noble`) and 22.04 (`Jammy`).\n:::\n\n### Snap (Linux)\n\n[![trippy](https://snapcraft.io/trippy/badge.svg)](https://snapcraft.io/trippy)\n\n```shell\nsnap install trippy\n```\n\n### Homebrew (macOS)\n\n[![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/trippy.svg)](https://formulae.brew.sh/formula/trippy)\n\n```shell\nbrew install trippy\n```\n\n### WinGet (Windows)\n\n[![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)\n\n```shell\nwinget install trippy\n```\n\n### Scoop (Windows)\n\n[![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)\n\n```shell\nscoop install trippy\n```\n\n### Chocolatey (Windows)\n\n[![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy)\n\n```shell\nchoco install trippy\n```\n\n### NetBSD\n\n[![pkgsrc current package](https://repology.org/badge/version-for-repo/pkgsrc_current/trippy.svg)](https://pkgsrc.se/net/trippy)\n\n```shell\npkgin install trippy\n```\n\n### FreeBSD\n\n[![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/trippy.svg)](https://www.freshports.org/net/trippy/)\n\n```shell\npkg install trippy\n```\n\n### OpenBSD\n\n[![OpenBSD port](https://repology.org/badge/version-for-repo/openbsd/trippy.svg)](https://openports.pl/path/net/trippy)\n\n```shell\npkg_add trippy\n```\n\n### Arch Linux\n\n[![Arch package](https://repology.org/badge/version-for-repo/arch/trippy.svg)](https://archlinux.org/packages/extra/x86_64/trippy)\n\n```shell\npacman -S trippy\n```\n\n### Gentoo Linux\n\n[![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/trippy.svg)](https://packages.gentoo.org/packages/net-analyzer/trippy)\n\n```shell\nemerge -av net-analyzer/trippy\n```\n\n### Void Linux\n\n[![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)\n\n```shell\nxbps-install -S trippy\n```\n\n### ALT Sisyphus\n\n[![ALT Sisyphus package](https://repology.org/badge/version-for-repo/altsisyphus/trippy.svg)](https://packages.altlinux.org/en/sisyphus/srpms/trippy/)\n\n```shell\napt-get install trippy\n```\n\n### Chimera Linux\n\n[![Chimera Linux package](https://repology.org/badge/version-for-repo/chimera/trippy.svg)](https://github.com/chimera-linux/cports/tree/master/user/trippy)\n\n```shell\napk add trippy\n```\n\n### Nix\n\n[![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)\n\n```shell\nnix-env -iA trippy\n```\n\n### Docker\n\n[![Docker Image Version (latest by date)](https://img.shields.io/docker/v/fujiapple/trippy)](https://hub.docker.com/r/fujiapple/trippy/)\n\n```shell\ndocker run -it fujiapple/trippy\n```\n\n### All Repositories\n\n[![Packaging status](https://repology.org/badge/vertical-allrepos/trippy.svg)](https://repology.org/project/trippy/versions)\n\n## Downloads\n\nDownload the latest release for your platform.\n\n| OS      | Arch      | Env          | Current (0.13.0)                                                                                                              | Previous (0.12.2)                                                                                                             | Previous (0.11.0)                                                                                                             |\n| ------- | --------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| 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)       |\n| 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)      |\n| 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)      |\n| 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)     |\n| 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)  |\n| 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)   |\n| 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) |\n| 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)            |\n| 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)           |\n| 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)            |\n| 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)             |\n| 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)           |\n| 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)         |\n| 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)          |\n| 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)                            |\n| 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)    |\n| 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)   |\n"
  },
  {
    "path": "docs/src/content/docs/development/crates.md",
    "content": "---\ntitle: Crates\ndescription: A reference for the Trippy crates.\n---\n\nThe following table lists the crates that are provided by Trippy. See [crates](crates/README.md) for more information.\n\n| Crate                                                         | Description                                                                         |\n| ------------------------------------------------------------- | ----------------------------------------------------------------------------------- |\n| [trippy](https://crates.io/crates/trippy)                     | A binary crate for the Trippy application and a library crate                       |\n| [trippy-core](https://crates.io/crates/trippy-core)           | A library crate providing the core Trippy tracing functionality                     |\n| [trippy-packet](https://crates.io/crates/trippy-packet)       | A library crate which provides packet wire formats and packet parsing functionality |\n| [trippy-dns](https://crates.io/crates/trippy-dns)             | A library crate for performing forward and reverse lazy DNS resolution              |\n| [trippy-privilege](https://crates.io/crates/trippy-privilege) | A library crate for discovering platform privileges                                 |\n| [trippy-tui](https://crates.io/crates/trippy-tui)             | A library crate for the Trippy terminal user interface                              |\n"
  },
  {
    "path": "docs/src/content/docs/guides/docker.md",
    "content": "---\ntitle: Run Trippy with Docker\ndescription: Learn how to run the Trippy CLI from the official Docker image.\nsidebar:\n  order: 5\n---\n\nTrippy is distributed as the [`fujiapple/trippy`](https://hub.docker.com/r/fujiapple/trippy/) image on Docker Hub. The\nimage bundles the `trip` binary compiled against Alpine Linux and configures it as the container entrypoint.\n\n## Quick start\n\nRun the image interactively and pass any CLI arguments directly after the image name:\n\n:::note\nBecause the entrypoint already invokes `trip`, you should not repeat the binary name.\n:::\n\n```shell\ndocker run -it --rm fujiapple/trippy example.com\n```\n\nTo display the built-in help you can pass standard flags:\n\n```shell\ndocker run -it --rm fujiapple/trippy --help\n```\n\n## Configuration\n\nTo provide a configuration file, mount host directories into the root of the container:\n\n```shell\ndocker run -it --rm -v \"/path/to/trippy.toml:/trippy.toml\" fujiapple/trippy example.com\n```\n\n## Networking considerations\n\nTrippy uses raw sockets to send probes. On Linux hosts Docker grants the required `CAP_NET_RAW` capability by\ndefault, so no additional flags are needed.\n\nWhen running inside more restrictive container runtimes ensure that the container retains this capability:\n\n```shell\ndocker run -it --rm --cap-add=NET_RAW fujiapple/trippy example.com\n```\n\n:::caution\nDocker Desktop for macOS has a known limitations with raw sockets. In particular, it resets the `ttl` field on outgoing\npackets to 64. As a result, intermediate hops are not discovered when tracing from a macOS host via Docker.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/guides/faq.md",
    "content": "---\ntitle: Frequently Asked Questions\ndescription: Frequently asked questions about Trippy.\nsidebar:\n  order: 6\n---\n\n## Why does Trippy show \"Awaiting data...\"?\n\n:::caution\nIf you are using Windows you _must_ [configure](/guides/windows_firewall)\nthe Windows Defender firewall to allow incoming ICMP traffic\n:::\n\nWhen Trippy shows “Awaiting data...” it means that it has received zero responses for the probes sent in a trace. This\nindicates that either probes are not being sent or, more typically, responses are not being received.\n\nCheck that local and network firewalls allow ICMP traffic and that the system `traceroute` (or `tracert.exe` on\nWindows) works as expected. Note that on Windows, even if `tracert.exe` works as expected, you\n_must_ [configure](/guides/windows_firewall) the Windows Defender\nfirewall to allow incoming ICMP traffic.\n\nFor deeper diagnostics you can run tools such as https://www.wireshark.org and https://www.tcpdump.org to verify that\nicmp requests and responses are being send and received.\n\n<a name=\"windows-defender\"></a>\n"
  },
  {
    "path": "docs/src/content/docs/guides/privileges.md",
    "content": "---\ntitle: Privileges\ndescription: A reference for the Trippy privileges.\nsidebar:\n  order: 2\n---\n\nTrippy normally requires elevated privileges due to the use of raw sockets. Enabling the required privileges for your\nplatform can be achieved in several ways, as outlined below. Trippy can also be used without elevated privileged on\ncertain platforms, with some limitations.\n\n## Unix\n\n### Run via `sudo`\n\nRun as `root` user via `sudo`:\n\n```shell\nsudo trip example.com\n```\n\n:::note\nWhen running `trip` via `sudo` you must ensure that the (optional) configuration file is stored relative to the `root`\nuser or specify the location of the configuration file via the `-c` (`--config-file`) command line argument. See\nthe [configuration reference](/reference/configuration).\n:::\n\n### Set the `setuid` bit\n\n`chown` `trip` as the `root` user and set the `setuid` bit:\n\n```shell\nsudo chown root $(which trip) && sudo chmod +s $(which trip)\n```\n\n### [Linux only] Set capabilities\n\nSet the `CAP_NET_RAW` capability:\n\n```shell\nsudo setcap CAP_NET_RAW+p $(which trip)\n```\n\n:::note\nTrippy is a capability aware application and will add `CAP_NET_RAW` to the effective set if it is present in the allowed\nset. Trippy will drop all capabilities after creating the raw sockets.\n:::\n\n## Windows\n\nTrippy must be run with Administrator privileges on Windows.\n\n## Unprivileged mode\n\nTrippy allows running in an unprivileged mode for all tracing modes (`ICMP`, `UDP` and `TCP`) on platforms which support\nthat feature.\n\n:::note\nUnprivileged mode is currently only supported on macOS. Linux support is possible and may be added in the future.\nUnprivileged mode is not supported on NetBSD, FreeBSD or Windows as these platforms do not support the `IPPROTO_ICMP`\nsocket type. See [#101](https://github.com/fujiapple852/trippy/issues/101) for further information.\n:::\n\nThe unprivileged mode can be enabled by adding the `--unprivileged` (`-u`) command line flag or by adding the\n`unprivileged` entry in the `trippy` section of the [configuration file](/reference/configuration):\n\n```toml\n[trippy]\nunprivileged = true\n```\n\n:::note\nThe `paris` and `dublin` `ECMP` strategies are not supported in unprivileged mode as these require manipulating the\n`UDP` and `IP` and headers which in turn requires the use of a raw socket.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/guides/recommendation.md",
    "content": "---\ntitle: Recommended Tracing Settings\ndescription: Recommended settings for Trippy.\nsidebar:\n  order: 3\n---\n\nTrippy provides a variety of configurable features which can be used to perform different types of analysis. The choice\nof settings will depend on the analysis you wish to perform and the environment in which you are working. This guide\nlists some common options along with some basic guidance on when they might be appropriate.\n\n:::note\nThe Windows `tracert` tool uses ICMP by default, whereas most Unix `traceroute` tools use UDP by default.\n:::\n\n## ICMP\n\nBy default Trippy will run an ICMP trace to the target. This will typically produce a consistent path to the target (a\nsingle flow) for each round of tracing which makes it easy to read and analyse. This is a useful mode for general\nnetwork troubleshooting.\n\nHowever, many routers are configured to rate-limit ICMP traffic which can make it difficult to get an accurate picture\nof packet loss. In addition, ICMP traffic is not typically subject to ECMP routing and so may not reflect the path that\nwould taken by other protocols such as UDP and TCP.\n\nTo run a simple ICMP trace:\n\n```shell\ntrip example.com\n```\n\nDue to the rate-limiting of ICMP traffic, some people prefer to hide the `Loss%` and `Recv` columns in the Tui as\nthese are easy to misinterpret.\n\n```shell\ntrip example.com --tui-custom-columns hosavbwdt\n```\n\nThese settings can be made permanent by adding them to the Trippy configuration file:\n\n```toml\n[tui]\ncustom-columns = \"hosavbwdt\"\n```\n\n:::note\nThe `Sts` column shows different color codes to reflect packet loss at intermediate vs the target hop, see the\n[Column Reference](/reference/column) for more information.\n:::\n\n#### UDP/Dublin with fixed ports\n\nUDP tracing provides a more realistic view of the path taken by traffic that is subject to ECMP routing.\n\nSetting a fixed target port in the range 33434-33534 may allow Trippy to determine that the probe has reached the target\nas many routers and firewalls are configured to allow UDP probes in that range and will respond with a Destination\nUnreachable response.\n\nHowever, running a UDP trace with a fixed target port and a variable source port will typically result in different\npaths being followed for each probe within each round of tracing. This can make it difficult to interpret the output as\ndifferent hosts will reply for a given hop (time-to-live) across rounds.\n\nBy using the `dublin` ECMP strategy, which encodes the sequence number in the IP `identifier` field, Trippy can fix both\nthe source and target ports, typically resulting in a _single_ path for each probe within each round of tracing.\n\n:::note\nUDP/Dublin for IPv6 encodes the sequence number as the payload length as the IP `identifier` field is not available in\nIPv6.\n:::\n\n:::note\nKeep in mind that every probe is an _independent trial_ and each may traverse a completely different path. In practice,\nICMP probes often follow a single path, whereas the path of UDP and TCP probes is typically determined by the 5-tuple of\nprotocol, source and destination IP addresses and ports.\n\nAlso beware that the return path may not be the same as the forward path, and may also differ for each probe. Strategies\nsuch as `dublin` and `paris` assist in controlling the path taken by the forward probes, but do not help control the\nreturn path. Therefore it is recommended to run a trace in both directions to get a complete picture.\n:::\n\nTo run a UDP trace with fixed source and target ports using the `dublin` ECMP strategy:\n\n```shell\ntrip example.com --udp --multipath-strategy dublin --source-port 5000 --target-port 33434\n```\n\n:::note\nThe source port can be any valid port number, but the target port should usually be in the range 33434-33534 or whatever\nrange is open to UDP probes on the target host.\n:::\n\nThese settings can be made permanent by adding them to the Trippy configuration file:\n\n```toml\n[strategy]\nprotocol = \"udp\"\nmultipath-strategy = \"dublin\"\nsource-port = 5000\ntarget-port = 33434\n```\n\n## UDP/Dublin with fixed target port and variable source port\n\nAs an extension to the above, if you do not fix the source port when using the `dublin` ECMP strategy, Trippy will\nvary the source port per _round_ of tracing (i.e. each probe within a given round will share the same source port, and\nthe source port will vary for each round). This will typically result in the _same_ path being followed for _each_ probe\nwithin a given round, but _different_ paths being followed for each round.\n\nThese individual flows can be explored in the Trippy Tui by pressing the `toggle-flows` key binding (`f` key by\ndefault).\n\nAdding the columns `Seq`, `Sprt` and `Dprt` to the Tui will show the sequence number, source port and destination port\nrespectively which makes this easier to visualize.\n\n```shell\ntrip example.com --udp --multipath-strategy dublin --target-port 33434 --tui-custom-columns holsravbwdtSPQ\n```\n\nThese settings can be made permanent by adding them to the Trippy configuration file:\n\n```toml\n[strategy]\nprotocol = \"udp\"\nmultipath-strategy = \"dublin\"\ntarget-port = 33434\n\n[tui]\ncustom-columns = \"holsravbwdtSPQ\"\n```\n\nTo make the flows easier to visualize, you can generate a Graphviz DOT file report of all tracing flows:\n\n```shell\ntrip example.com --udp --multipath-strategy dublin --target-port 33434 -m dot -C 5\n```\n\n## UDP/Paris\n\nUDP with the `paris` ECMP strategy offers the same benefits as the `dublin` strategy with fixed ports and can be used\nin the same way.\n\nThey differ in the way they encode the sequence number in the probe. The `dublin` strategy uses the IP `identifier`\nfield, whereas the `paris` strategy uses the UDP `checksum` field.\n\nTo run a UDP trace with fixed source and target ports using the `paris` ECMP strategy:\n\n```shell\ntrip example.com --udp --multipath-strategy paris --source-port 5000 --target-port 33434\n```\n\nThe `paris` strategy does not work behind NAT as the UDP `checksum` field is typically modified by NAT devices.\nTherefore the `dublin` strategy is recommended when NAT is present.\n\n:::note\nTrippy can detect the presence of NAT devices in some circumstances when using the `dublin` strategy and the `Nat`\ncolumn can be shown in the Tui to indicate when NAT is detected. See the [Column Reference](/reference/column) for more\ninformation.\n:::\n\n#### TCP\n\nTCP tracing is similar to UDP tracing in that it provides a more realistic view of the path taken by traffic that is\nsubject to ECMP routing.\n\nTCP tracing defaults to using a target port of 80 and sets the source port as the sequence number which will typically\nresult in a different path being followed for each probe within each round of tracing.\n\nTo run a TCP trace:\n\n```shell\ntrip example.com --tcp\n```\n\nTCP tracing is useful for diagnosing issues with TCP connections and higher layer protocols such as HTTP. Often UDP\ntracing can be used in place of TCP to diagnose IP layer network issues and, as it provides ways to control the path\ntaken by the probes, it is often preferred.\n\n:::note\nTrippy does not support the `dublin` or `paris` ECMP strategies for TCP tracing and so you cannot fix both the source\nand target ports. See the [tracking issue](https://github.com/fujiapple852/trippy/issues/274) for details.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/guides/usage.md",
    "content": "---\ntitle: Usage Examples\ndescription: Examples of how to use the Trippy command line interface.\nsidebar:\n  order: 1\n---\n\nBasic usage with default parameters:\n\n```shell\ntrip example.com\n```\n\nTrace without requiring elevated privileges (supported platforms only, see [privileges](/guides/privileges)):\n\n```shell\ntrip example.com --unprivileged\n```\n\nTrace using the `udp` (or `tcp` or `icmp`) protocol (also aliases `--icmp`, `--udp` & `--tcp`):\n\n```shell\ntrip example.com -p udp\n```\n\nTrace to multiple targets simultaneously (`icmp` protocol only,\nsee [#72](https://github.com/fujiapple852/trippy/issues/72)):\n\n```shell\ntrip example.com google.com crates.io\n```\n\nTrace with a minimum round time of `250ms` and a grace period of `50ms`:\n\n```shell\ntrip example.com -i 250ms -g 50ms\n```\n\nTrace with a custom first and maximum `time-to-live`:\n\n```shell\ntrip example.com --first-ttl 2 --max-ttl 10\n```\n\nUse custom destination port `443` for `tcp` tracing:\n\n```shell\ntrip example.com -p tcp -P 443\n```\n\nUse custom source port `5000` for `udp` tracing:\n\n```shell\ntrip example.com -p udp -S 5000\n```\n\nUse the `dublin` (or `paris`) ECMP routing strategy for `udp` with fixed source and destination ports:\n\n```shell\ntrip example.com -p udp -R dublin -S 5000 -P 3500\n```\n\nTrace with a custom source address:\n\n```shell\ntrip example.com -p tcp -A 127.0.0.1\n```\n\nTrace with a source address determined by the IPv4 address for interface `en0`:\n\n```shell\ntrip example.com -p tcp -I en0\n```\n\nTrace using `IPv6`:\n\n```shell\ntrip example.com -6\n```\n\nTrace using `ipv4-then-ipv6` fallback (or `ipv6-then-ipv4` or `ipv4` or `ipv6`):\n\n```shell\ntrip example.com --addr-family ipv4-then-ipv6\n```\n\nGenerate a `json` (or `csv`, `pretty`, `markdown`) tracing report with 5 rounds of data:\n\n```shell\ntrip example.com -m json -C 5\n```\n\nGenerate a [Graphviz](https://graphviz.org) `DOT` file report of all tracing flows for a TCP trace after 5 rounds:\n\n```shell\ntrip example.com --tcp -m dot -C 5\n```\n\nGenerate a textual report of all tracing flows for a UDP trace after 5 rounds:\n\n```shell\ntrip example.com --udp -m flows -C 5\n```\n\nPerform DNS queries using the `google` DNS resolver (or `cloudflare`, `system`, `resolv`):\n\n```shell\ntrip example.com -r google\n```\n\nLookup [AS][autonomous_system] information for all discovered IP addresses (not yet available for the `system` resolver,\nsee [#66](https://github.com/fujiapple852/trippy/issues/66)):\n\n```shell\ntrip example.com -r google -z\n```\n\nSet the reverse DNS lookup cache time-to-live to be 60 seconds:\n\n```shell\ntrip example.com --dns-ttl 60sec\n```\n\nLookup and display `short` (or `long` or `location` or `off`) GeoIp information from a `mmdb` file:\n\n```shell\ntrip example.com --geoip-mmdb-file GeoLite2-City.mmdb --tui-geoip-mode short\n```\n\nParse `icmp` extensions:\n\n```shell\ntrip example.com -e\n```\n\nHide the IP address, hostname and GeoIp for the first two hops:\n\n```shell\ntrip example.com --tui-privacy-max-ttl 2\n```\n\nCustomize Tui columns (see [Column Reference](/reference/column)):\n\n```shell\ntrip example.com --tui-custom-columns holsravbwdt\n```\n\nCustomize the color theme:\n\n```shell\ntrip example.com --tui-theme-colors bg-color=blue,text-color=ffff00\n```\n\nList all Tui items that can have a custom color theme:\n\n```shell\ntrip --print-tui-theme-items\n```\n\nCustomize the key bindings:\n\n```shell\ntrip example.com --tui-key-bindings previous-hop=k,next-hop=j,quit=shift-q\n```\n\nList all Tui commands that can have a custom key binding:\n\n```shell\ntrip --print-tui-binding-commands\n```\n\nSpecify the location of the Trippy config file:\n\n```shell\ntrip example.com --config-file /path/to/trippy.toml\n```\n\nGenerate a template configuration file:\n\n```shell\ntrip --print-config-template > trippy.toml\n```\n\nGenerate `bash` shell completions (or `fish`, `powershell`, `zsh`, `elvish`):\n\n```shell\ntrip --generate bash\n```\n\nGenerate `ROFF` man page:\n\n```shell\ntrip --generate-man\n```\n\nUse the `de` Tui locale:\n\n```shell\ntrip example.com --tui-locale de\n```\n\nList supported Tui locales:\n\n```shell\ntrip --print-locales\n```\n\nSet the Tui timezone to `UTC`:\n\n```shell\ntrip example.com --tui-timezone UTC\n```\n\nRun in `silent` tracing mode and output `compact` trace logging with `full` span events:\n\n```shell\ntrip example.com -m silent -v --log-format compact --log-span-events full\n```\n"
  },
  {
    "path": "docs/src/content/docs/guides/windows_firewall.md",
    "content": "---\ntitle: Windows Defender Firewall\ndescription: Allow incoming ICMP traffic in the Windows Defender firewall.\nsidebar:\n  order: 4\n---\n\nThe Windows Defender firewall rule can be created using PowerShell.\n\n```shell\nNew-NetFirewallRule -DisplayName \"ICMPv4 Trippy Allow\" -Name ICMPv4_TRIPPY_ALLOW -Protocol ICMPv4 -Action Allow\nNew-NetFirewallRule -DisplayName \"ICMPv6 Trippy Allow\" -Name ICMPv6_TRIPPY_ALLOW -Protocol ICMPv6 -Action Allow\n```\n\nThe rules can be enabled as follows:\n\n```shell\nEnable-NetFirewallRule ICMPv4_TRIPPY_ALLOW\nEnable-NetFirewallRule ICMPv6_TRIPPY_ALLOW\n```\n\nThe rules can be disabled as follows:\n\n```shell\nDisable-NetFirewallRule ICMPv4_TRIPPY_ALLOW\nDisable-NetFirewallRule ICMPv6_TRIPPY_ALLOW\n```\n\nThere is a [step-by-step guide to manually configure the Windows Defender firewall rule](https://github.com/fujiapple852/trippy/issues/578#issuecomment-1565149826).\n"
  },
  {
    "path": "docs/src/content/docs/index.mdx",
    "content": "---\ntitle: \"Trippy: a network diagnostic tool\"\ndescription: a network diagnostic tool.\ntemplate: splash\nhero:\n  tagline: Trippy combines the functionality of traceroute and ping and is designed to assist with the analysis of networking issues.\n  image:\n    alt: Trippy, man!\n    light: ../../assets/Trippy-Emblem.svg\n    dark: ../../assets/Trippy-Emblem-DarkMode.svg\n  actions:\n    - text: Get Started\n      link: /start/getting-started/\n      icon: right-arrow\n    - text: Read the docs\n      link: /reference/overview/\n      icon: open-book\n      variant: secondary\n    - text: View on GitHub\n      link: https://github.com/fujiapple852/trippy\n      icon: github\n      variant: secondary\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\nimport { Icon } from '@astrojs/starlight/components';\n\n<CardGrid stagger>\n\t<Card title=\"Powerful tracing features\" icon=\"rocket\">\n\t    - `ICMP`, `UDP` & `TCP` over `IPv4` & `IPv6` protocols\n\t    - Fully customizable tracing options\n\t    - `dublin` and `paris` `ECMP` strategies\n\t    - `ICMP` extensions objects (i.e. `MPLS`)\n\t    - Reverse `DNS` and `ASN` lookups\n\t    - `NAT` detection\n\n\t\t![Trippy main screen](../../assets/main_screen.png)\n\t</Card>\n\n\t<Card title=\"Visualize GeoIp on a world map\" icon=\"star\">\n        - Lookup GeoIp information and show on world map\n        - Support for both `MaxMind` and `IPinfo` databases\n\n\t\t![Trippy GeoIp world map](../../assets/world_map.png)\n\t</Card>\n\n    <Card title=\"Run on your platform\" icon=\"star\">\n        - Runs on `Linux`, `macOS`, `Windows`, `*BSD`\n        - Supports `x86_64`, `aarch64`, `arm7` architectures\n        - Available from most native package managers\n        - Run in unprivileged mode\n\n        ![Trippy on Windows](../../assets/windows.png)\n    </Card>\n\n\t<Card title=\"Highly customizable TUI\" icon=\"seti:config\">\n\t    - Customizable columns, color themes and key bindings\n\t    - Hop detail navigation mode\n\t    - Hop privacy mode\n\t    - Show individual tracing flows\n\t    - Various charts and statistics\n\t    - Persist configuration to file\n\n\t\t![Trippy settings](../../assets/settings.png)\n\t</Card>\n\n\t<Card title=\"Trace in your language\" icon=\"translate\">\n\t    TUI available in 10 languages:\n\t    - Chinese 🇨🇳, English 🇺🇸, French 🇫🇷, German 🇩🇪, Italian 🇮🇹, Portuguese 🇵🇹, Russian 🇷🇺, Spanish 🇪🇸, Swedish 🇸🇪 and Turkish 🇹🇷\n\n\t\t![Trippy main screen in Chinese](../../assets/help_screen_zh.png)\n\t</Card>\n\n</CardGrid>\n"
  },
  {
    "path": "docs/src/content/docs/reference/bindings.md",
    "content": "---\ntitle: Key Bindings Reference\ndescription: A reference for customizing the Trippy TUI key bindings.\nsidebar:\n  order: 3\n---\n\nThe following table lists the default Tui command key bindings. These can be overridden with the `--tui-key-bindings`\ncommand line option or in the `bindings` section of the configuration file.\n\n| Command                    | Description                                     | Default   |\n| -------------------------- | ----------------------------------------------- | --------- |\n| `toggle-help`              | Toggle help                                     | `h`       |\n| `toggle-help-alt`          | Toggle help (alternative binding)               | `?`       |\n| `toggle-settings`          | Toggle settings                                 | `s`       |\n| `toggle-settings-tui`      | Open settings (Tui tab)                         | `1`       |\n| `toggle-settings-trace`    | Open settings (Trace tab)                       | `2`       |\n| `toggle-settings-dns`      | Open settings (Dns tab)                         | `3`       |\n| `toggle-settings-geoip`    | Open settings (GeoIp tab)                       | `4`       |\n| `toggle-settings-bindings` | Open settings (Bindings tab)                    | `5`       |\n| `toggle-settings-theme`    | Open settings (Theme tab)                       | `6`       |\n| `toggle-settings-columns`  | Open settings (Columns tab)                     | `7`       |\n| `next-hop`                 | Select next hop                                 | `down`    |\n| `previous-hop`             | Select previous hop                             | `up`      |\n| `next-trace`               | Select next trace                               | `right`   |\n| `previous-trace`           | Select previous trace                           | `left`    |\n| `next-hop-address`         | Select next hop address                         | `.`       |\n| `previous-hop-address`     | Select previous hop address                     | `,`       |\n| `address-mode-ip`          | Show IP address only                            | `i`       |\n| `address-mode-host`        | Show hostname only                              | `n`       |\n| `address-mode-both`        | Show both IP address and hostname               | `b`       |\n| `toggle-freeze`            | Toggle freezing the display                     | `ctrl+f`  |\n| `toggle-chart`             | Toggle the chart                                | `c`       |\n| `toggle-map`               | Toggle the GeoIp map                            | `m`       |\n| `toggle-flows`             | Toggle the flows                                | `f`       |\n| `expand-privacy`           | Expand hop privacy                              | `p`       |\n| `contract-privacy`         | Contract hop privacy                            | `o`       |\n| `expand-hosts`             | Expand the hosts shown per hop                  | `]`       |\n| `expand-hosts-max`         | Expand the hosts shown per hop to the maximum   | `}`       |\n| `contract-hosts`           | Contract the hosts shown per hop                | `[`       |\n| `contract-hosts-min`       | Contract the hosts shown per hop to the minimum | `{`       |\n| `chart-zoom-in`            | Zoom in the chart                               | `=`       |\n| `chart-zoom-out`           | Zoom out the chart                              | `-`       |\n| `clear-trace-data`         | Clear all trace data                            | `ctrl+r`  |\n| `clear-dns-cache`          | Flush the DNS cache                             | `ctrl+k`  |\n| `clear-selection`          | Clear the current selection                     | `esc`     |\n| `toggle-as-info`           | Toggle AS info display                          | `z`       |\n| `toggle-hop-details`       | Toggle hop details                              | `d`       |\n| `quit`                     | Quit the application                            | `q`       |\n| `quit-preserve-screen`     | Quit the application and preserve the screen    | `shift+q` |\n\nThe supported modifiers are: `shift`, `ctrl`, `alt`, `super`, `hyper` & `meta`. Multiple modifiers may be specified, for\nexample `ctrl+shift+b`.\n"
  },
  {
    "path": "docs/src/content/docs/reference/cli.md",
    "content": "---\ntitle: CLI Reference\ndescription: A reference for the Trippy command line interface.\nsidebar:\n  order: 1\n---\n\n```text\nA network diagnostic tool\n\nUsage: trip [OPTIONS] [TARGETS]...\n\nArguments:\n  [TARGETS]...\n          A space delimited list of hostnames and IPs to trace\n\nOptions:\n  -c, --config-file <CONFIG_FILE>\n          Config file\n\n  -m, --mode <MODE>\n          Output mode [default: tui]\n\n          Possible values:\n          - tui:      Display interactive TUI\n          - stream:   Display a continuous stream of tracing data\n          - pretty:   Generate a pretty text table report for N cycles\n          - markdown: Generate a Markdown text table report for N cycles\n          - csv:      Generate a CSV report for N cycles\n          - json:     Generate a JSON report for N cycles\n          - dot:      Generate a Graphviz DOT file for N cycles\n          - flows:    Display all flows for N cycles\n          - silent:   Do not generate any tracing output for N cycles\n\n  -u, --unprivileged\n          Trace without requiring elevated privileges on supported platforms\n          [default: false]\n\n  -p, --protocol <PROTOCOL>\n          Tracing protocol [default: icmp]\n\n          Possible values:\n          - icmp: Internet Control Message Protocol\n          - udp:  User Datagram Protocol\n          - tcp:  Transmission Control Protocol\n\n      --udp\n          Trace using the UDP protocol\n\n      --tcp\n          Trace using the TCP protocol\n\n      --icmp\n          Trace using the ICMP protocol\n\n  -F, --addr-family <ADDR_FAMILY>\n          The address family [default: ipv4-then-ipv6]\n\n          Possible values:\n          - ipv4:           IPv4 only\n          - ipv6:           IPv6 only\n          - ipv6-then-ipv4: IPv6 with a fallback to IPv4\n          - ipv4-then-ipv6: IPv4 with a fallback to IPv6\n          - system:         If the OS resolver is being used then use the first IP address returned, \n                            otherwise lookup IPv4 with a fallback to IPv6\n\n  -4, --ipv4\n          Use IPv4 only\n\n  -6, --ipv6\n          Use IPv6 only\n\n  -P, --target-port <TARGET_PORT>\n          The target port (TCP & UDP only) [default: 80]\n\n  -S, --source-port <SOURCE_PORT>\n          The source port (TCP & UDP only) [default: auto]\n\n  -A, --source-address <SOURCE_ADDRESS>\n          The source IP address [default: auto]\n\n  -I, --interface <INTERFACE>\n          The network interface [default: auto]\n\n  -i, --min-round-duration <MIN_ROUND_DURATION>\n          The minimum duration of every round [default: 1s]\n\n  -T, --max-round-duration <MAX_ROUND_DURATION>\n          The maximum duration of every round [default: 1s]\n\n  -g, --grace-duration <GRACE_DURATION>\n          The period of time to wait for additional ICMP responses after the\n          target has responded [default: 100ms]\n\n      --initial-sequence <INITIAL_SEQUENCE>\n          The initial sequence number [default: 33434]\n\n  -R, --multipath-strategy <MULTIPATH_STRATEGY>\n          The Equal-cost Multi-Path routing strategy (UDP only) [default:\n          classic]\n\n          Possible values:\n          - classic:\n            The src or dest port is used to store the sequence number\n          - paris:\n            The UDP `checksum` field is used to store the sequence number\n          - dublin:\n            The IP `identifier` field is used to store the sequence number\n\n  -U, --max-inflight <MAX_INFLIGHT>\n          The maximum number of in-flight ICMP echo requests [default: 24]\n\n  -f, --first-ttl <FIRST_TTL>\n          The TTL to start from [default: 1]\n\n  -t, --max-ttl <MAX_TTL>\n          The maximum number of TTL hops [default: 64]\n\n      --packet-size <PACKET_SIZE>\n          The size of IP packet to send (IP header + ICMP header + payload)\n          [default: 84]\n\n      --payload-pattern <PAYLOAD_PATTERN>\n          The repeating pattern in the payload of the ICMP packet [default: 0]\n\n  -Q, --tos <TOS>\n          The TOS (i.e. DSCP+ECN) IP header value (IPv4 only) [default: 0]\n\n  -e, --icmp-extensions\n          Parse ICMP extensions\n\n      --read-timeout <READ_TIMEOUT>\n          The socket read timeout [default: 10ms]\n\n  -r, --dns-resolve-method <DNS_RESOLVE_METHOD>\n          How to perform DNS queries [default: system]\n\n          Possible values:\n          - system:     Resolve using the OS resolver\n          - resolv:     Resolve using the `/etc/resolv.conf` DNS configuration\n          - google:     Resolve using the Google `8.8.8.8` DNS service\n          - cloudflare: Resolve using the Cloudflare `1.1.1.1` DNS service\n\n  -y, --dns-resolve-all\n          Trace to all IPs resolved from DNS lookup [default: false]\n\n      --dns-timeout <DNS_TIMEOUT>\n          The maximum time to wait to perform DNS queries [default: 5s]\n\n      --dns-ttl <DNS_TTL>\n          The time-to-live (TTL) of DNS entries [default: 300s]\n\n  -z, --dns-lookup-as-info\n          Lookup autonomous system (AS) information during DNS queries [default:\n          false]\n\n  -s, --max-samples <MAX_SAMPLES>\n          The maximum number of samples to record per hop [default: 256]\n\n      --max-flows <MAX_FLOWS>\n          The maximum number of flows to record [default: 64]\n\n  -a, --tui-address-mode <TUI_ADDRESS_MODE>\n          How to render addresses [default: host]\n\n          Possible values:\n          - ip:   Show IP address only\n          - host: Show reverse-lookup DNS hostname only\n          - both: Show both IP address and reverse-lookup DNS hostname\n\n      --tui-as-mode <TUI_AS_MODE>\n          How to render autonomous system (AS) information [default: asn]\n\n          Possible values:\n          - asn:          Show the ASN\n          - prefix:       Display the AS prefix\n          - country-code: Display the country code\n          - registry:     Display the registry name\n          - allocated:    Display the allocated date\n          - name:         Display the AS name\n\n      --tui-custom-columns <TUI_CUSTOM_COLUMNS>\n          Custom columns to be displayed in the TUI hops table [default:\n          holsravbwdt]\n\n      --tui-icmp-extension-mode <TUI_ICMP_EXTENSION_MODE>\n          How to render ICMP extensions [default: off]\n\n          Possible values:\n          - off:  Do not show `icmp` extensions\n          - mpls: Show MPLS label(s) only\n          - full: Show full `icmp` extension data for all known extensions\n          - all:  Show full `icmp` extension data for all classes\n\n      --tui-geoip-mode <TUI_GEOIP_MODE>\n          How to render GeoIp information [default: short]\n\n          Possible values:\n          - off:      Do not display GeoIp data\n          - short:    Show short format\n          - long:     Show long format\n          - location: Show latitude and Longitude format\n\n  -M, --tui-max-addrs <TUI_MAX_ADDRS>\n          The maximum number of addresses to show per hop [default: auto]\n\n      --tui-preserve-screen\n          Preserve the screen on exit [default: false]\n\n      --tui-refresh-rate <TUI_REFRESH_RATE>\n          The TUI refresh rate [default: 100ms]\n\n      --tui-privacy-max-ttl <TUI_PRIVACY_MAX_TTL>\n          The maximum ttl of hops which will be masked for privacy [default: none]\n\n          If set, the source IP address and hostname will also be hidden.\n\n      --tui-locale <TUI_LOCALE>\n          The locale to use for the TUI [default: auto]\n\n      --tui-timezone <TUI_TIMEZONE>\n          The timezone to use for the TUI [default: auto]\n\n          The timezone must be a valid IANA timezone identifier.\n\n      --tui-theme-colors <TUI_THEME_COLORS>\n          The TUI theme colors [item=color,item=color,..]\n\n      --print-tui-theme-items\n          Print all TUI theme items and exit\n\n      --tui-key-bindings <TUI_KEY_BINDINGS>\n          The TUI key bindings [command=key,command=key,..]\n\n      --print-tui-binding-commands\n          Print all TUI commands that can be bound and exit\n\n  -C, --report-cycles <REPORT_CYCLES>\n          The number of report cycles to run [default: 10]\n\n  -G, --geoip-mmdb-file <GEOIP_MMDB_FILE>\n          The supported MaxMind or IPinfo GeoIp mmdb file\n\n      --generate <GENERATE>\n          Generate shell completion\n\n          [possible values: bash, elvish, fish, powershell, zsh]\n\n      --generate-man\n          Generate ROFF man page\n\n      --print-config-template\n          Print a template toml config file and exit\n\n      --print-locales\n          Print all available TUI locales and exit\n\n      --log-format <LOG_FORMAT>\n          The debug log format [default: pretty]\n\n          Possible values:\n          - compact: Display log data in a compact format\n          - pretty:  Display log data in a pretty format\n          - json:    Display log data in a json format\n          - chrome:  Display log data in Chrome trace format\n\n      --log-filter <LOG_FILTER>\n          The debug log filter [default: trippy=debug]\n\n      --log-span-events <LOG_SPAN_EVENTS>\n          The debug log format [default: off]\n\n          Possible values:\n          - off:    Do not display event spans\n          - active: Display enter and exit event spans\n          - full:   Display all event spans\n\n  -v, --verbose\n          Enable verbose debug logging\n\n  -h, --help\n          Print help (see a summary with '-h')\n\n  -V, --version\n          Print version\n```\n\n:::note\nTrippy command line arguments may be given in any order and my occur both before and after the targets. All options can\nalso be provided via environment variables using the `TRIP_` prefix (for example, `TRIP_PROTOCOL=tcp`). CLI flags take\nprecedence over environment variables when both are set.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/reference/column.md",
    "content": "---\ntitle: Column Reference\ndescription: A reference for customizing the Trippy TUI columns.\nsidebar:\n  order: 4\n---\n\nThe following table lists the columns that are available for display in the Tui. These can be overridden with the\n`--tui-custom-columns` command line option or in the `tui-custom-columns` attribute in the `tui` section of the\nconfiguration file.\n\n| Column   | Code | Description                                                                                                                                                                                                                                                                                                                                           |\n| -------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `#`      | `h`  | The time-to-live (TTL) for the hop                                                                                                                                                                                                                                                                                                                    |\n| `Host`   | `o`  | The hostname(s) and IP address(s) for the host(s) for the hop<br/>May include AS info, GeoIp and ICMP extensions<br/>Shows full hop details in hop detail navigation mode                                                                                                                                                                             |\n| `Loss%`  | `l`  | The packet loss % for the hop                                                                                                                                                                                                                                                                                                                         |\n| `Snd`    | `s`  | The number of probes sent for the hop                                                                                                                                                                                                                                                                                                                 |\n| `Recv`   | `r`  | The number of probe responses received for the hop                                                                                                                                                                                                                                                                                                    |\n| `Last`   | `a`  | The round-trip-time (RTT) of the last probe for the hop                                                                                                                                                                                                                                                                                               |\n| `Avg`    | `v`  | The average RTT of all probes for the hop                                                                                                                                                                                                                                                                                                             |\n| `Best`   | `b`  | The best RTT of all probes for the hop                                                                                                                                                                                                                                                                                                                |\n| `Wrst`   | `w`  | The worst RTT of all probes for the hop                                                                                                                                                                                                                                                                                                               |\n| `StDev`  | `d`  | The standard deviation of all probes for the hop                                                                                                                                                                                                                                                                                                      |\n| `Sts`    | `t`  | The status for the hop:<br/>- 🟢 Healthy hop<br/>- 🔵 Non-target hop with packet loss (does not necessarily indicate a problem)<br/>- 🟤 Non-target hop is unresponsive (does not necessarily indicate a problem) <br/>- 🟡 Target hop with packet loss (likely indicates a problem)<br/>- 🔴 Target hop is unresponsive (likely indicates a problem) |\n| `Jttr`   | `j`  | The round-trip-time (RTT) difference between consecutive rounds for the hop                                                                                                                                                                                                                                                                           |\n| `Javg`   | `g`  | The average jitter of all probes for the hop                                                                                                                                                                                                                                                                                                          |\n| `Jmax`   | `x`  | The maximum jitter of all probes for the hop                                                                                                                                                                                                                                                                                                          |\n| `Jint`   | `i`  | The smoothed jitter value of all probes for the hop                                                                                                                                                                                                                                                                                                   |\n| `Seq`    | `Q`  | The sequence number for the last probe for the hop                                                                                                                                                                                                                                                                                                    |\n| `Sprt`   | `S`  | The source port for the last probe for the hop                                                                                                                                                                                                                                                                                                        |\n| `Dprt`   | `P`  | The destination port for the last probe for the hop                                                                                                                                                                                                                                                                                                   |\n| `Type`   | `T`  | The icmp packet type for the last probe for the hop:<br/>- TE: TimeExceeded<br/>- ER: EchoReply<br/>- DU: DestinationUnreachable<br/>- NA: NotApplicable                                                                                                                                                                                              |\n| `Code`   | `C`  | The icmp packet code for the last probe for the hop                                                                                                                                                                                                                                                                                                   |\n| `NAT`    | `N`  | The NAT detection status for the hop                                                                                                                                                                                                                                                                                                                  |\n| `Fail`   | `f`  | The number of probes which failed to send for the hop                                                                                                                                                                                                                                                                                                 |\n| `Floss`  | `F`  | A _heuristic_ for the number of probes with _forward loss_ for the hop                                                                                                                                                                                                                                                                                |\n| `Bloss`  | `B`  | A _heuristic_ for the number of probes with _backward loss_ for the hop                                                                                                                                                                                                                                                                               |\n| `Floss%` | `D`  | The _forward loss_ % for the hop                                                                                                                                                                                                                                                                                                                      |\n| `DSCP`   | `K`  | Differentiated Services Code Point (DSCP) of the Original Datagram                                                                                                                                                                                                                                                                                    |\n| `ECN`    | `M`  | Explicit Congestion Notification (ECN) of the Original Datagram                                                                                                                                                                                                                                                                                       |\n| `ASN`    | `A`  | Autonomous System Number (ASN)                                                                                                                                                                                                                                                                                                                        |\n\nThe default columns are `holsravbwdt`.\n\n:::note\nThe columns will be shown in the order specified in the configuration.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/reference/configuration.md",
    "content": "---\ntitle: Configuration Reference\ndescription: A reference for customizing the Trippy configuration.\nsidebar:\n  order: 2\n---\n\nTrippy can be configured with via command line arguments or an optional configuration file. If a given configuration\nitem is specified in both the configuration file and via a command line argument then the latter will take precedence.\n\nThe configuration file location may be provided to Trippy via the `-c` (`--config-file`) argument. If not provided,\nTrippy will attempt to locate a `trippy.toml` or `.trippy.toml` configuration file in one of the following locations:\n\n- The current directory\n- The user home directory\n- the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config`\n- the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy`\n- the Windows data directory (Windows only): `%APPDATA%`\n\nA template configuration file\nfor [0.13.0](https://github.com/fujiapple852/trippy/blob/0.13.0/trippy-config-sample.toml) is available to\ndownload, or can be generated with the following command:\n\n```shell\ntrip --print-config-template > trippy.toml\n```\n"
  },
  {
    "path": "docs/src/content/docs/reference/locale.md",
    "content": "---\ntitle: Locale Reference\ndescription: A reference for customizing the Trippy TUI locale.\nsidebar:\n  order: 6\n---\n\nThe following table lists the supported locales for the Tui. These can be overridden with the `--tui-locale` command\nline option or in the `tui-locale` attribute in the `tui` section of the configuration file.\n\n| Locale | Language   | Region |\n| ------ | ---------- | ------ |\n| `zh`   | Chinese    | all    |\n| `en`   | English    | all    |\n| `fr`   | French     | all    |\n| `de`   | German     | all    |\n| `it`   | Italian    | all    |\n| `pt`   | Portuguese | all    |\n| `ru`   | Russian    | all    |\n| `es`   | Spanish    | all    |\n| `sv`   | Swedish    | all    |\n| `tr`   | Turkish    | all    |\n\n:::note\nIf you are able to help validate translations for Trippy, or if you wish to add translations for any additional\nlanguages, please see the [tracking issue](https://github.com/fujiapple852/trippy/issues/506) for details of how to\ncontribute.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/reference/overview.mdx",
    "content": "---\ntitle: Trippy Reference\ndescription: Reference documentation for Trippy.\nsidebar:\n  order: 0\n  badge:\n    text: New\n    variant: note\n---\n\nThis section provides complete reference documentation for Trippy.\n\n:::note\nThis reference documentation is intended for users who are already familiar with Trippy and want to learn more about\nits features and capabilities. If you are new to Trippy, it is recommend that you start by reading the [getting\nstarted](/start/getting-started) guide.\n:::\n\n### CLI Reference\n\nThe [CLI reference](/reference/cli) provides a complete list of the command line options available for Trippy. This\ninformation is available via the `--help` command line option and also in the man page on supported platforms.\n\n### Configuration Reference\n\nTrippy can be configured via an optional configuration file. The [configuration reference](/reference/configuration)\nprovides details of how to configure Trippy via the configuration file.\n\n### Key Bindings Reference\n\nThe Trippy TUI is highly customizable and allows you to change the key bindings to suit your preferences. The [key\nbindings reference](/reference/bindings) provides a complete list of the available key bindings and their\ndescriptions.\n\n### Column Reference\n\nThe list of columns that can be displayed in the TUI can be customized to suit your needs. The [column\nreference](/reference/column) provides a complete list of the available columns and their descriptions.\n\n### Theme Reference\n\nThe color schema of the TUI can be fully customized. The [theme reference](/reference/theme) provides a complete list of\nthe items which can be customized and their descriptions.\n\n### Locale Reference\n\nThe Trippy TUI supports multiple languages and regions. The [locale reference](/reference/locale) provides a complete\nlist of supported locales.\n\n### Version Reference\n\nAll versions of Trippy and their support status are listed in the [version reference](/reference/version)."
  },
  {
    "path": "docs/src/content/docs/reference/theme.md",
    "content": "---\ntitle: Theme Reference\ndescription: A reference for customizing the Trippy TUI theme.\nsidebar:\n  order: 5\n---\n\nThe following table lists the default Tui color theme. These can be overridden with the `--tui-theme-colors` command\nline option or in the `theme-colors` section of the configuration file.\n\n| Item                                 | Description                                               | Default      |\n| ------------------------------------ | --------------------------------------------------------- | ------------ |\n| `bg-color`                           | The default background color                              | `Black`      |\n| `border-color`                       | The default color of borders                              | `Gray`       |\n| `text-color`                         | The default color of text                                 | `Gray`       |\n| `tab-text-color`                     | The color of the text in traces tabs                      | `Green`      |\n| `hops-table-header-bg-color`         | The background color of the hops table header             | `White`      |\n| `hops-table-header-text-color`       | The color of text in the hops table header                | `Black`      |\n| `hops-table-row-active-text-color`   | The color of text of active rows in the hops table        | `Gray`       |\n| `hops-table-row-inactive-text-color` | The color of text of inactive rows in the hops table      | `DarkGray`   |\n| `hops-chart-selected-color`          | The color of the selected series in the hops chart        | `Green`      |\n| `hops-chart-unselected-color`        | The color of the unselected series in the hops chart      | `Gray`       |\n| `hops-chart-axis-color`              | The color of the axis in the hops chart                   | `DarkGray`   |\n| `frequency-chart-bar-color`          | The color of bars in the frequency chart                  | `Green`      |\n| `frequency-chart-text-color`         | The color of text in the bars of the frequency chart      | `Gray`       |\n| `flows-chart-bar-selected-color`     | The color of the selected flow bar in the flows chart     | `Green`      |\n| `flows-chart-bar-unselected-color`   | The color of the unselected flow bar in the flows chart   | `DarkGray`   |\n| `flows-chart-text-current-color`     | The color of the current flow text in the flows chart     | `LightGreen` |\n| `flows-chart-text-non-current-color` | The color of the non-current flow text in the flows chart | `White`      |\n| `samples-chart-color`                | The color of the samples chart                            | `Yellow`     |\n| `samples-chart-lost-color`           | The color of the samples chart for lost probes            | `Red`        |\n| `help-dialog-bg-color`               | The background color of the help dialog                   | `Blue`       |\n| `help-dialog-text-color`             | The color of the text in the help dialog                  | `Gray`       |\n| `settings-dialog-bg-color`           | The background color of the settings dialog               | `blue`       |\n| `settings-tab-text-color`            | The color of the text in settings dialog tabs             | `green`      |\n| `settings-table-header-text-color`   | The color of text in the settings table header            | `black`      |\n| `settings-table-header-bg-color`     | The background color of the settings table header         | `white`      |\n| `settings-table-row-text-color`      | The color of text of rows in the settings table           | `gray`       |\n| `map-world-color`                    | The color of the map world diagram                        | `white`      |\n| `map-radius-color`                   | The color of the map accuracy radius circle               | `yellow`     |\n| `map-selected-color`                 | The color of the map selected item box                    | `green`      |\n| `map-info-panel-border-color`        | The color of border of the map info panel                 | `gray`       |\n| `map-info-panel-bg-color`            | The background color of the map info panel                | `black`      |\n| `map-info-panel-text-color`          | The color of text in the map info panel                   | `gray`       |\n| `info-bar-bg-color`                  | The background color of the information bar               | `white`      |\n| `info-bar-text-color`                | The color of text in the information bar                  | `black`      |\n\nThe supported [ANSI colors](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) are:\n\n- `Black`, `Red`, `Green`, `Yellow`, `Blue`, `Magenta`, `Cyan`, `Gray`, `DarkGray`, `LightRed`, `LightGreen`,\n  `LightYellow`, `LightBlue`, `LightMagenta`, `LightCyan`, `White`\n\nIn addition, CSS [named colors](https://developer.mozilla.org/en-US/docs/Web/CSS/named-color) (i.e. SkyBlue) and raw hex\nvalues (i.e. ffffff) may be used but note that these are only supported on some platforms and terminals and may not\nrender correctly elsewhere.\n\nColor names are case-insensitive and may contain dashes.\n"
  },
  {
    "path": "docs/src/content/docs/reference/version.md",
    "content": "---\ntitle: Version Reference\ndescription: A reference for the Trippy versions.\nsidebar:\n  order: 7\n---\n\nThe following table lists this versions of Trippy that are available and links to the corresponding release note and\ndocumentation:\n\n| Version    | Release Date | Status      | Release Note                                                       | Documentation                                              |\n| ---------- | ------------ | ----------- | ------------------------------------------------------------------ | ---------------------------------------------------------- |\n| 0.14.0-dev | n/a          | Development | n/a                                                                | [docs](https://trippy.rs)                                  |\n| 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)                           |\n| 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)                           |\n| 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) |\n| 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) |\n| 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)  |\n| 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)  |\n| 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)  |\n| 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)  |\n\n:::note\nOnly the _latest patch versions_ of both the _current_ and _previous_ releases of Trippy are supported.\n:::\n"
  },
  {
    "path": "docs/src/content/docs/start/features.md",
    "content": "---\ntitle: Features\ndescription: Learn about the features of Trippy.\nsidebar:\n  order: 3\n---\n\n- Trace using multiple protocols:\n  - `ICMP`, `UDP` & `TCP`\n  - `IPv4` & `IPv6`\n- Customizable tracing options:\n  - packet size & payload pattern\n  - start and maximum time-to-live (TTL)\n  - minimum and maximum round duration\n  - round end grace period & maximum number of unknown hops\n  - source & destination port (`TCP` & `UDP`)\n  - source address and source interface\n  - `TOS` (aka `DSCP + ECN`)\n- Support for `classic`, `paris`\n  and `dublin` [Equal Cost Multi-path Routing](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing)\n  strategies ([tracking issue](https://github.com/fujiapple852/trippy/issues/274))\n- RFC4884 [ICMP Multi-Part Messages](https://datatracker.ietf.org/doc/html/rfc4884)\n  - Generic Extension Objects\n  - MPLS Label Stacks\n- Unprivileged mode\n- NAT detection\n- Tui interface:\n  - Trace multiple targets simultaneously from a single instance of Trippy\n  - Per hop stats (sent, received, loss%, last, avg, best, worst, stddev, jitter & status)\n  - Per hop round-trip-time (RTT) history and frequency distributing charts\n  - Interactive chart of RTT for all hops in a trace with zooming capability\n  - Interactive GeoIp world map\n  - Isolate and filter by individual tracing flows\n  - Customizable color theme & key bindings\n  - Customizable column order and visibility\n  - Configuration via both command line arguments and a configuration file\n  - Show multiple hosts per hop with ability to cap display to N hosts and show frequency %\n  - Show hop details and navigate hosts within each hop\n  - Freeze/unfreeze the Tui, reset the stats, flush the cache, preserve screen on exit\n  - Responsive UI with adjustable refresh rate\n  - Hop privacy\n  - Multiple language support\n  - Customizable timezone\n- DNS:\n  - Use system, external (Google `8.8.8.8` or Cloudflare `1.1.1.1`) or custom resolver\n  - Lazy reverse DNS queries\n  - Lookup [autonomous system][autonomous_system] number (ASN) and name\n- GeoIp:\n  - Lookup and display GeoIp information from local [MaxMind](https://www.maxmind.com)\n    and [IPinfo](https://ipinfo.io) `mmdb` files\n- Generate tracing reports:\n  - `json`, `csv` & tabular (pretty-printed and markdown)\n  - Tracing `flows` report\n  - Graphviz `dot` charts\n  - configurable reporting cycles\n- Runs on multiple platform (macOS, Linux, Windows, NetBSD, FreeBSD, OpenBSD)\n- Capabilities aware application (Linux only)\n"
  },
  {
    "path": "docs/src/content/docs/start/getting-started.mdx",
    "content": "---\ntitle: Getting Started\ndescription: Get started with Trippy.\nsidebar:\n  order: 1\n---\n\nimport { Steps } from '@astrojs/starlight/components';\n\nThe following steps will guide you through the process of installing and running Trippy.\n\n<Steps>\n\n1. Install Trippy:\n\n    Trippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled binaries, or source.\n\n    For example, to install Trippy from `cargo`:\n\n    ```shell\n    cargo install trippy --locked\n    ```\n\n    See the [installation guide](/start/installation) for details of how to install Trippy on your system.\n\n2. Run Trippy:\n\n   To run a basic trace to `example.com` with default settings, use the following command:\n\n   ```shell\n   sudo trip example.com\n   ```\n\n   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).\n\n3. Customize the key bindings, theme and columns:\n\n   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.\n\n   These settings can be made permanent by adding them to the Trippy configuration file, see the [configuration reference](/reference/configuration) for details.\n\n4. Review the tracing recommendations:\n\n   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.\n\n</Steps>\n\nHappy tracing!\n"
  },
  {
    "path": "docs/src/content/docs/start/installation.md",
    "content": "---\ntitle: Installation\ndescription: Install Trippy on your platform.\nsidebar:\n  order: 2\n---\n\nThe following sections provide instructions for installing Trippy on your platform.\n\nTrippy runs on Linux, BSD, macOS, and Windows. It can be installed from most common package managers, precompiled\nbinaries, or source.\n\n## Distributions\n\nTrippy is available for a variety of platforms and package managers.\n\n### Cargo\n\n[![Crates.io](https://img.shields.io/crates/v/trippy)](https://crates.io/crates/trippy/0.13.0)\n\n```shell\ncargo install trippy --locked\n```\n\n### APT (Debian)\n\n[![Debian 13 package](https://repology.org/badge/version-for-repo/debian_13/trippy.svg)](https://tracker.debian.org/pkg/trippy)\n\n```shell\napt install trippy\n```\n\n:::note\nOnly available for Debian 13 (`trixie`) and later.\n:::\n\n### PPA (Ubuntu)\n\n[![Ubuntu PPA](https://img.shields.io/badge/Ubuntu%20PPA-0.13.0-brightgreen)](https://launchpad.net/~fujiapple/+archive/ubuntu/trippy/+packages)\n\n```shell\nadd-apt-repository ppa:fujiapple/trippy\napt update && apt install trippy\n```\n\n:::note\nOnly available for Ubuntu 24.04 (`Noble`) and 22.04 (`Jammy`).\n:::\n\n### Snap (Linux)\n\n[![trippy](https://snapcraft.io/trippy/badge.svg)](https://snapcraft.io/trippy)\n\n```shell\nsnap install trippy\n```\n\n### Homebrew (macOS)\n\n[![Homebrew package](https://repology.org/badge/version-for-repo/homebrew/trippy.svg)](https://formulae.brew.sh/formula/trippy)\n\n```shell\nbrew install trippy\n```\n\n### WinGet (Windows)\n\n[![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)\n\n```shell\nwinget install trippy\n```\n\n### Scoop (Windows)\n\n[![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)\n\n```shell\nscoop install trippy\n```\n\n### Chocolatey (Windows)\n\n[![Chocolatey package](https://repology.org/badge/version-for-repo/chocolatey/trippy.svg)](https://community.chocolatey.org/packages/trippy)\n\n```shell\nchoco install trippy\n```\n\n### NetBSD\n\n[![pkgsrc current package](https://repology.org/badge/version-for-repo/pkgsrc_current/trippy.svg)](https://pkgsrc.se/net/trippy)\n\n```shell\npkgin install trippy\n```\n\n### FreeBSD\n\n[![FreeBSD port](https://repology.org/badge/version-for-repo/freebsd/trippy.svg)](https://www.freshports.org/net/trippy/)\n\n```shell\npkg install trippy\n```\n\n### OpenBSD\n\n[![OpenBSD port](https://repology.org/badge/version-for-repo/openbsd/trippy.svg)](https://openports.pl/path/net/trippy)\n\n```shell\npkg_add trippy\n```\n\n### Arch Linux\n\n[![Arch package](https://repology.org/badge/version-for-repo/arch/trippy.svg)](https://archlinux.org/packages/extra/x86_64/trippy)\n\n```shell\npacman -S trippy\n```\n\n### Gentoo Linux\n\n[![Gentoo package](https://repology.org/badge/version-for-repo/gentoo/trippy.svg)](https://packages.gentoo.org/packages/net-analyzer/trippy)\n\n```shell\nemerge -av net-analyzer/trippy\n```\n\n### Void Linux\n\n[![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)\n\n```shell\nxbps-install -S trippy\n```\n\n### ALT Sisyphus\n\n[![ALT Sisyphus package](https://repology.org/badge/version-for-repo/altsisyphus/trippy.svg)](https://packages.altlinux.org/en/sisyphus/srpms/trippy/)\n\n```shell\napt-get install trippy\n```\n\n### Chimera Linux\n\n[![Chimera Linux package](https://repology.org/badge/version-for-repo/chimera/trippy.svg)](https://github.com/chimera-linux/cports/tree/master/user/trippy)\n\n```shell\napk add trippy\n```\n\n### Nix\n\n[![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)\n\n```shell\nnix-env -iA trippy\n```\n\n### Docker\n\n[![Docker Image Version (latest by date)](https://img.shields.io/docker/v/fujiapple/trippy)](https://hub.docker.com/r/fujiapple/trippy/)\n\n```shell\ndocker run -it fujiapple/trippy\n```\n\n:::note\nSee the [Docker guide](/guides/docker) for more information.\n:::\n\n### All Repositories\n\n[![Packaging status](https://repology.org/badge/vertical-allrepos/trippy.svg)](https://repology.org/project/trippy/versions)\n\n## Downloads\n\nDownload the latest release for your platform.\n\n| OS      | Arch      | Env          | Current (0.13.0)                                                                                                              | Previous (0.12.2)                                                                                                             | Previous (0.11.0)                                                                                                             |\n| ------- | --------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| 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)       |\n| 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)      |\n| 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)      |\n| 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)     |\n| 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)  |\n| 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)   |\n| 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) |\n| 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)            |\n| 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)           |\n| 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)            |\n| 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)             |\n| 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)           |\n| 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)         |\n| 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)          |\n| 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)                            |\n| 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)    |\n| 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)   |\n"
  },
  {
    "path": "docs/src/content/versions/0.12.2.json",
    "content": "{\n  \"sidebar\": [\n    {\n      \"label\": \"Start Here\",\n      \"autogenerate\": {\n        \"directory\": \"start\"\n      }\n    },\n    {\n      \"label\": \"Guides\",\n      \"autogenerate\": {\n        \"directory\": \"guides\"\n      }\n    },\n    {\n      \"label\": \"Reference\",\n      \"autogenerate\": {\n        \"directory\": \"reference\"\n      }\n    },\n    {\n      \"label\": \"Development\",\n      \"autogenerate\": {\n        \"directory\": \"development\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "docs/src/content/versions/0.13.0.json",
    "content": "{\n  \"sidebar\": [\n    {\n      \"label\": \"Start Here\",\n      \"autogenerate\": {\n        \"directory\": \"start\"\n      }\n    },\n    {\n      \"label\": \"Guides\",\n      \"autogenerate\": {\n        \"directory\": \"guides\"\n      }\n    },\n    {\n      \"label\": \"Reference\",\n      \"autogenerate\": {\n        \"directory\": \"reference\"\n      }\n    },\n    {\n      \"label\": \"Development\",\n      \"autogenerate\": {\n        \"directory\": \"development\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "docs/src/env.d.ts",
    "content": "/// <reference path=\"../.astro/types.d.ts\" />\n/// <reference types=\"astro/client\" />\n"
  },
  {
    "path": "docs/src/styles/custom.css",
    "content": "/* Dark mode colors. */\n:root {\n\t--sl-color-accent-low: #131e4f;\n\t--sl-color-accent: #3447ff;\n\t--sl-color-accent-high: #b3c7ff;\n\t--sl-color-white: #ffffff;\n\t--sl-color-gray-1: #eceef2;\n\t--sl-color-gray-2: #c0c2c7;\n\t--sl-color-gray-3: #888b96;\n\t--sl-color-gray-4: #545861;\n\t--sl-color-gray-5: #353841;\n\t--sl-color-gray-6: #24272f;\n\t--sl-color-black: #17181c;\n}\n/* Light mode colors. */\n:root[data-theme='light'] {\n\t--sl-color-accent-low: #c7d6ff;\n\t--sl-color-accent: #364bff;\n\t--sl-color-accent-high: #182775;\n\t--sl-color-white: #17181c;\n\t--sl-color-gray-1: #24272f;\n\t--sl-color-gray-2: #353841;\n\t--sl-color-gray-3: #545861;\n\t--sl-color-gray-4: #888b96;\n\t--sl-color-gray-5: #c0c2c7;\n\t--sl-color-gray-6: #eceef2;\n\t--sl-color-gray-7: #f5f6f8;\n\t--sl-color-black: #ffffff;\n}"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\"\n}\n"
  },
  {
    "path": "dprint.json",
    "content": "{\n  \"markdown\": {\n  },\n  \"toml\": {\n    \"cargo.applyConventions\": false\n  },\n  \"excludes\": [],\n  \"plugins\": [\n    \"https://plugins.dprint.dev/markdown-0.17.8.wasm\",\n    \"https://plugins.dprint.dev/dockerfile-0.3.2.wasm\",\n    \"https://plugins.dprint.dev/toml-0.6.3.wasm\"\n  ]\n}\n"
  },
  {
    "path": "examples/README.md",
    "content": "## Examples\n\nThe following is a list of examples provided.\n\n> [!NOTE]\n> Examples must be run with privileges, see [README.md](../README.md#privileges) for more information.\n\n### `hello-world`\n\nA simple example that demonstrates running a traceroute and printing the output of each round.\n\n```shell\ncargo run -p hello-world\n```\n\n### `toy-traceroute`\n\nA toy clone of the BSD4.3 (macOS) system traceroute tool.\n\n```shell\ncargo run -p toy-traceroute 1.1.1.1\n```\n"
  },
  {
    "path": "examples/hello-world/Cargo.toml",
    "content": "[package]\nname = \"hello-world\"\nversion = \"0.1.0\"\nlicense = \"Apache-2.0\"\nedition = \"2024\"\nrust-version = \"1.85\"\npublish = false\n\n[dependencies]\ntrippy = { version = \"0.14.0-dev\", path = \"../../crates/trippy\", default-features = false, features = [\"core\"] }\nanyhow = \"1.0.86\"\n"
  },
  {
    "path": "examples/hello-world/src/main.rs",
    "content": "use std::str::FromStr;\nuse trippy::core::Builder;\n\nfn main() -> anyhow::Result<()> {\n    let addr = std::net::IpAddr::from_str(\"1.1.1.1\")?;\n    Builder::new(addr)\n        .build()?\n        .run_with(|round| println!(\"{round:?}\"))?;\n    Ok(())\n}\n"
  },
  {
    "path": "examples/toy-traceroute/Cargo.toml",
    "content": "[package]\nname = \"toy-traceroute\"\nversion = \"0.1.0\"\nlicense = \"Apache-2.0\"\nedition = \"2024\"\nrust-version = \"1.85\"\npublish = false\n\n[dependencies]\ntrippy = { version = \"0.14.0-dev\", path = \"../../crates/trippy\", default-features = false, features = [\"core\", \"dns\"] }\nanyhow = \"1.0.86\"\nitertools = \"0.14.0\"\nclap = { version = \"4.5.60\", features = [\"derive\"] }\n"
  },
  {
    "path": "examples/toy-traceroute/src/main.rs",
    "content": "use anyhow::anyhow;\nuse clap::Parser;\nuse itertools::Itertools;\nuse std::net::IpAddr;\nuse std::str::FromStr;\nuse std::time::Duration;\nuse trippy::core::{Builder, PortDirection, Protocol};\nuse trippy::dns::{Config, DnsResolver, Resolver};\n\n/// A toy clone of BSD4.3 (macOS) traceroute.\n///\n/// *** This is for demonstration purposes only. ***\n#[derive(Parser, Debug)]\n#[command(version, about, long_about = None, arg_required_else_help(true))]\nstruct Args {\n    host: String,\n    #[arg(short = 'f')]\n    first_ttl: Option<u8>,\n    #[arg(short = 'm')]\n    max_ttl: Option<u8>,\n    #[arg(short = 'i')]\n    interface: Option<String>,\n    #[arg(short = 'p')]\n    port: Option<u16>,\n    #[arg(short = 'q')]\n    nqueries: Option<usize>,\n    #[arg(short = 's')]\n    src_addr: Option<String>,\n    #[arg(short = 't')]\n    tos: Option<u8>,\n    #[arg(short = 'z')]\n    pausemsecs: Option<u64>,\n    #[arg(short = 'e')]\n    evasion: bool,\n}\n\nfn main() -> anyhow::Result<()> {\n    let args = Args::parse();\n    let hostname = args.host;\n    let interface = args.interface;\n    let src_addr = args\n        .src_addr\n        .as_ref()\n        .map(|addr| IpAddr::from_str(addr))\n        .transpose()?;\n    let port = args.port.unwrap_or(33434);\n    let first_ttl = args.first_ttl.unwrap_or(1);\n    let max_ttl = args.max_ttl.unwrap_or(64);\n    let nqueries = args.nqueries.unwrap_or(3);\n    let tos = args.tos.unwrap_or(0);\n    let pausemsecs = args.pausemsecs.unwrap_or(100);\n    let port_direction = if args.evasion {\n        PortDirection::new_fixed_dest(port)\n    } else {\n        PortDirection::new_fixed_src(port)\n    };\n    let resolver = DnsResolver::start(Config::default())?;\n    let addrs: Vec<_> = resolver\n        .lookup(&hostname)\n        .map_err(|_| anyhow!(format!(\"traceroute: unknown host {}\", hostname)))?\n        .into_iter()\n        .collect();\n    let addr = match addrs.as_slice() {\n        [] => return Err(anyhow!(\"traceroute: unknown host {}\", hostname)),\n        [addr] => *addr,\n        [addr, ..] => {\n            println!(\"traceroute: Warning: {hostname} has multiple addresses; using {addr}\");\n            *addr\n        }\n    };\n    let tracer = Builder::new(addr)\n        .interface(interface)\n        .source_addr(src_addr)\n        .protocol(Protocol::Udp)\n        .port_direction(port_direction)\n        .packet_size(52)\n        .first_ttl(first_ttl)\n        .max_ttl(max_ttl)\n        .tos(tos)\n        .max_flows(1)\n        .max_rounds(Some(nqueries))\n        .min_round_duration(Duration::from_millis(pausemsecs))\n        .max_round_duration(Duration::from_millis(pausemsecs))\n        .build()?;\n    println!(\n        \"traceroute to {} ({}), {} hops max, {} byte packets\",\n        &hostname,\n        tracer.target_addr(),\n        tracer.max_ttl().0,\n        tracer.packet_size().0\n    );\n    tracer.run()?;\n    let snapshot = &tracer.snapshot();\n    if let Some(err) = snapshot.error() {\n        return Err(anyhow!(\"error: {}\", err));\n    }\n    for hop in snapshot.hops() {\n        let ttl = hop.ttl();\n        let samples: String = hop\n            .samples()\n            .iter()\n            .map(|s| format!(\"{:.3} ms\", s.as_secs_f64() * 1000_f64))\n            .join(\"  \");\n        if hop.addr_count() > 0 {\n            for (i, addr) in hop.addrs().enumerate() {\n                let host = resolver.reverse_lookup(*addr).to_string();\n                let address = format!(\"{host} ({addr})\");\n                if i != 0 {\n                    println!(\"    {address} {samples}\");\n                } else {\n                    println!(\" {ttl}  {address} {samples}\");\n                }\n            }\n        } else {\n            println!(\" {ttl}  * * * {samples}\");\n        }\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "snap/snapcraft.yaml",
    "content": "name: trippy\nversion: '0.14.0-dev'\nsummary: A network diagnostic tool\ndescription: |\n  Trippy combines the functionality of traceroute and ping and is designed to \n  assist with the diagnosis of network issues.\n  \n  Features:\n  \n  - Trace using multiple protocols:\n  - `ICMP`, `UDP` & `TCP`\n  - `IPv4` & `IPv6`\n  - Customizable tracing options:\n      - packet size & payload pattern\n      - start and maximum time-to-live (TTL)\n      - minimum and maximum round duration\n      - round end grace period & maximum number of unknown hops\n      - source & destination port (`TCP` & `UDP`)\n      - source address and source interface\n      - `TOS` (aka `DSCP + ECN`)\n  - Equal Cost Multi-path Routing strategies (`classic`, `paris` and `dublin`)\n  - RFC4884 ICMP Multi-Part Messages\n    - Generic Extension Objects\n    - MPLS Label Stacks\n  - Unprivileged mode\n  - NAT detection\n  - Tui interface:\n      - Trace multiple targets simultaneously from a single instance of Trippy\n      - Per hop stats (sent, received, loss%, last, avg, best, worst, stddev, jitter & status)\n      - Per hop round-trip-time (RTT) history and frequency distributing charts\n      - Interactive chart of RTT for all hops in a trace with zooming capability\n      - Interactive GeoIp world map\n      - Isolate and filter by individual tracing flows\n      - Customizable color theme & key bindings\n      - Customizable column order and visibility\n      - Configuration via both command line arguments and a configuration file\n      - Show multiple hosts per hop with ability to cap display to N hosts and show frequency %\n      - Show hop details and navigate hosts within each hop\n      - Freeze/unfreeze the Tui, reset the stats, flush the cache, preserve screen on exit\n      - Responsive UI with adjustable refresh rate\n      - Hop privacy\n  - DNS:\n      - Use system, external (Google `8.8.8.8` or Cloudflare `1.1.1.1`) or custom resolver\n      - Lazy reverse DNS queries\n      - Lookup [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_(Internet)) number (ASN) and name\n  - GeoIp:\n      - Lookup and display GeoIp information from local [MaxMind](https://www.maxmind.com) and [IPinfo](https://ipinfo.io) `mmdb` files\n  - Generate tracing reports:\n      - `json`, `csv` & tabular (pretty-printed and markdown)\n      - Tracing `flows` report\n      - Graphviz `dot` charts\n      - configurable reporting cycles\n  - Runs on multiple platform (macOS, Linux, NetBSD, FreeBSD, Windows)\n  - Capabilities aware application (Linux only)\n  \n  This package auto-connects to the following snap interfaces:\n  \n    - `network`: to allow general outgoing network access\n    - `network-bind`: to allow binding to local ports\n    - `network-observe`: to allow enabling `CAP_NET_RAW` for using raw sockets\n    - `home`: to allow access to /home for reading the configuration file\ncontact: mailto:fujiapple852@gmail.com\nissues: https://github.com/fujiapple852/trippy/issues\nlicense: Apache-2.0\nsource-code: https://github.com/fujiapple852/trippy\nwebsite: https://trippy.rs\nbase: core20\ngrade: stable\nconfinement: strict\nparts:\n  trippy:\n    plugin: rust\n    source: .\n    organize:\n      trip: usr/bin/trip\napps:\n  trippy:\n    command: usr/bin/trip\n    plugs:\n      - network-bind\n      - network\n      - network-observe\n      - home\nplugs:\n  home:\n    read: all"
  },
  {
    "path": "trippy-config-sample.toml",
    "content": "# Sample config file for Trippy.\n#\n# Copy this template config file to your platform specific config dir.\n#\n# Trippy will attempt to locate a `trippy.toml` or `.trippy.toml` config file\n# in one of the following locations:\n#   the current directory\n#   the user home directory\n#   the XDG config directory (Unix only): `$XDG_CONFIG_HOME` or `~/.config`\n#   the XDG app config directory (Unix only): `$XDG_CONFIG_HOME/trippy` or `~/.config/trippy`\n#   the Windows data directory (Windows only): `%APPDATA%`\n#\n# You may override the config file name and location by passing the `-c`\n# (`--config-file`) command line argument.\n#\n# All sections and all items within each section are non-mandatory.\n\n#\n# General Trippy configuration.\n#\n[trippy]\n# The Trippy mode.\n#\n# Allowed values are:\n#   tui         - Display interactive Tui [default]\n#   stream      - Display a continuous stream of tracing data\n#   pretty      - Generate a pretty text table report for N cycles\n#   markdown    - Generate a Markdown text table report for N cycles\n#   csv         - Generate a CSV report for N cycles\n#   json        - Generate a JSON report for N cycles\n#   dot         - Generate a Graphviz DOT report for N cycles\n#   flows       - Display all flows for N cycles\n#   silent      - Do not generate any output for N cycles\n#\n# Note: the dot and flows modes are only allowed with paris or dublin\n# multipath strategy.\nmode = \"tui\"\n\n# Trace without requiring elevated privileges [default: false]\n#\n# Enabling will cause IPPROTO_ICMP sockets to be used.\n#\n# Note: not supported on all platforms.\nunprivileged = false\n\n# How to format log data.\n#\n# Allowed values are:\n#  compact      - Display log data in a compact format\n#  pretty       - Display log data in a pretty format [default]\n#  json         - Display log data in a json format\n#  chrome       - Display log data in Chrome trace format\nlog-format = \"pretty\"\n\n# The debug log filter [default: trippy=debug]\nlog-filter = \"trippy=debug\"\n\n# How to log event spans.\n#\n# Allowed values are:\n#  off          - Do not display event spans [default]\n#  active       - Display enter and exit event spans\n#  full         - Display all event spans\nlog-span-events = \"off\"\n\n#\n# Tracing strategy configuration.\n#\n[strategy]\n# The tracing protocol.\n#\n# Allowed values are:\n#   icmp [default]\n#   udp\n#   tcp\nprotocol = \"icmp\"\n\n# The address family.\n#\n# Allowed values are:\n#   ipv4            - Lookup IPv4 only\n#   ipv6            - Lookup IPv6 only\n#   ipv6-then-ipv4  - Lookup IPv6 with a fallback to IPv4\n#   ipv4-then-ipv6  - Lookup IPv4 with a fallback to IPv6\n#   system          - If the OS resolver is being used then use the first IP address returned,\n#                     otherwise lookup IPv4 with a fallback to IPv6 [default]\naddr-family = \"system\"\n\n# The target port (TCP & UDP only) [default: 80]\n#\n# Applicable for TCP and UDP protocols only.\n# target-port = 80\n\n# The source port (TCP & UDP only) [default: auto]\n#\n# Applicable for TCP and UDP protocols only.\n# source-port = 1234\n\n# The source IP address [default: auto]\n#\n# If unspecified the source address will be chosen automatically based on the tracing target.\n# source-address = \"1.2.3.4\"\n\n# The network interface [default: auto]\n#\n# If not specified the interface is chosen based on the source-address.\n# interface = \"en0\"\n\n# The minimum duration of every round [default: 1s]\n#\n# The minimum time that must elapse before a tracing round is considered\n# complete, regardless of whether the target is discovered or not.\nmin-round-duration = \"1s\"\n\n# The maximum duration of every round [default: 1s]\n#\n# The maximum time that may elapse before a tracing round is considered\n# complete, regardless of whether the target is discovered or not.\nmax-round-duration = \"1s\"\n\n# The round grace period [default: 100ms]\n#\n# The period of time to wait for additional probe responses after the target\n# has responded.\ngrace-duration = \"100ms\"\n\n# The initial sequence number [default: 33434]\ninitial-sequence = 33434\n\n# The Equal-cost Multi-Path routing strategy (UDP only)\n#\n# Allowed value are:\n#   classic - The src or dest port is used to store the sequence number [default]\n#   paris   - The UDP `checksum` field is used to store the sequence number\n#   dublin  - The IP `identifier` field is used to store the sequence number\n#\n# See https://github.com/fujiapple852/trippy/issues/274 for more details.\nmultipath-strategy = \"classic\"\n\n# The maximum number of in-flight ICMP echo requests [default: 24]\n#\n# The tracing strategy operates a sliding window protocol and will allow a\n# maximum number of probes to be inflight (sent, and not received or lost)\n# at any given time.\nmax-inflight = 24\n\n# The TTL to start from [default: 1]\nfirst-ttl = 1\n\n# The maximum number of TTL hops [default: 64]\nmax-ttl = 64\n\n# The size of IP packet to send [default: 84]\n#\n# For icmp this is the sum of the IP header, ICMP header and the payload.\n# Trippy will adjust the size of the payload to fill up to the packet size.\npacket-size = 84\n\n# The repeating pattern in the payload of the ICMP packet [default: 0]\npayload-pattern = 0\n\n# The TOS IP header value (IPv4 only) [default: 0]\n#\n# This is also known as DSCP+ECN.\ntos = 0\n\n# Whether to parse ICMP extensions.\n#\n# If enabled, all extensions attached to incoming ICMP TimeExceeded and DestinationUnavailable messages will be parsed\n# and provided as part of the trace response data.\n#\n# The following ICMP Extension Object Classes are supported:\n#   1 - MPLS Label Stack Class (RFC4950)\n#\n# Extension objects with an unknown class will be parsed to capture generic information including the class, subtype,\n# length and payload bytes.\nicmp-extensions = false\n\n# The socket read timeout [default: 10ms]\nread-timeout = \"10ms\"\n\n# The maximum number of samples to record per hop [default: 256]\nmax-samples = 256\n\n# The maximum number of flows to record [default: 64]\nmax-flows = 64\n\n#\n# DNS configuration.\n#\n[dns]\n# How DNS queries are resolved\n#\n# Allowed values are:\n#   system      - Resolve using the OS resolver [default]\n#   resolv      - Resolve using the `/etc/resolv.conf` DNS configuration\n#   google      - Resolve using the Google `8.8.8.8` DNS service\n#   cloudflare  - Resolve using the Cloudflare `1.1.1.1` DNS service\ndns-resolve-method = \"system\"\n\n# Trace to all IPs resolved from DNS lookup (ICMP only) [default: false]\n#\n# When set to true a trace will be started for all IPs resolved for all given targets.\n# When set to false a trace will be started for one arbitrarily chosen IP per given target.\ndns-resolve-all = false\n\n# Whether to lookup AS information [default: false]\n#\n# If enabled, AS (autonomous system) information is retrieved during DNS\n# queries.\ndns-lookup-as-info = false\n\n# The maximum time to wait to perform DNS queries [default: 5s]\ndns-timeout = \"5s\"\n\n# The time-to-live (TTL) for DNS entries [default: 300s]\ndns-ttl = \"300s\"\n\n#\n# Report generation configuration.\n#\n[report]\n# The number of report cycles to run [default: 10]\n#\n# Only applicable for modes pretty, markdown, csv and json.\nreport-cycles = 10\n\n#\n# General Tui Configuration.\n#\n[tui]\n# How to render addresses.\n#\n# Allowed values are:\n#   ip - Show IP address only\n#   host - Show reverse-lookup DNS hostname only [default]\n#   both - Show both IP address and reverse-lookup DNS hostname\ntui-address-mode = \"host\"\n\n# How to render autonomous system (AS) information.\n#\n# Allowed values are:\n#   asn             - Show the ASN [default]\n#   prefix          - Display the AS prefix\n#   country-code    - Display the country code\n#   registry        - Display the registry name\n#   allocated       - Display the allocated date\n#   name            - Display the AS name\ntui-as-mode = \"asn\"\n\n# Custom columns to be displayed in the TUI hops table.\n#\n# Default values:\n#\n#   h - Ttl\n#   o - Hostname\n#   l - Loss %\n#   s - Probes sent\n#   r - Responses received\n#   a - Last RTT\n#   v - Average RTT\n#   b - Best RTT\n#   w - Worst RTT\n#   d - Stddev\n#   t - Status\n#\n# Also available:\n#\n#   j - Jitter\n#   g - Jitter average\n#   x - Jitter max\n#   i - Jitter intra\n#   Q - Last probe sequence number\n#   S - Last probe source port\n#   P - Last probe destination port\n#   T - Last icmp packet type\n#   C - Last icmp packet code\n#   N - Last NAT status\n#   f - Probes failed\n#   F - Forward loss\n#   B - Backward loss\n#   D - Forward loss %\n#   K - Differentiated Services Code Point (DSCP) of the Original Datagram\n#   M - Explicit Congestion Notification (ECN) of the Original Datagram\n#   A - Autonomous System Number (ASN)\n#\n# The columns will be shown in the order specified.\ntui-custom-columns = \"holsravbwdt\"\n\n# How to render ICMP extensions.\n#\n#   off             - Do not show icmp extensions [default]\n#   mpls            - Show MPLS label(s) only\n#   full            - Show full icmp extension data for all known extensions\n#   all             - Show full icmp extension data for all classes\ntui-icmp-extension-mode = \"off\"\n\n# The mmdb file to use GeoIp lookup [default: none]\n#\n# Supported mmdb formats:\n#   MaxMind \"GeoLite2 City\"\n#   IPinfo \"IP to Country + ASN Database\"\n#   IPinfo \"IP to Geolocation Extended Database\"\n# geoip-mmdb-file = \"/path/to/geoip_file.mmdb\"\n\n# How to render GeoIp information.\n#\n# Allowed values are:\n#   off - Do not show GeoIp information [default]\n#   short - Show short format GeoIp information\n#   long - Show long format GeoIp information\n#   location - Show latitude and Longitude format GeoIp information\n#\n# Note this value is ignored unless a valid geoip-mmdb-file value is also provided.\ntui-geoip-mode = \"off\"\n\n# The maximum number of addresses to show per hop [default: auto]\n#\n# Use a zero value for `auto`.\ntui-max-addrs = 0\n\n# Whether to preserve the screen on exit [default: false]\ntui-preserve-screen = false\n\n# The Tui refresh rate [default: 100ms]\ntui-refresh-rate = \"100ms\"\n\n# The maximum ttl of hops which will be masked for privacy [default: none]\n# tui-privacy-max-ttl = 0\n\n# The locale to use for Tui [default: auto]\n# tui-locale = \"en-US\"\n\n# The timezone for displaying dates and time [default: auto]\n#\n# The timezone must be a valid IANA timezone identifier.\n# tui-timezone = \"UTC\"\n\n# Tui color theme configuration.\n#\n# The supported ANSI color values are:\n#   Black, Red, Green, Yellow, Blue, Magenta, Cyan, Gray, DarkGray, LightRed,\n#   LightGreen, LightYellow, LightBlue, LightMagenta, LightCyan, White\n#\n# In addition, CSS named colors (i.e. SkyBlue) and raw hex values (i.e. ffffff)\n# may be used but note that these are only supported on some platforms and\n# terminals and may not render correctly elsewhere.\n#\n# Color names are case-insensitive and may contain dashes.\n#\n# See https://github.com/fujiapple852/trippy#theme-reference for details.\n[theme-colors]\nbg-color = \"black\"\nborder-color = \"gray\"\ntext-color = \"gray\"\ntab-text-color = \"green\"\nhops-table-header-bg-color = \"white\"\nhops-table-header-text-color = \"black\"\nhops-table-row-active-text-color = \"gray\"\nhops-table-row-inactive-text-color = \"darkgray\"\nhops-chart-selected-color = \"green\"\nhops-chart-unselected-color = \"gray\"\nhops-chart-axis-color = \"darkgray\"\nfrequency-chart-bar-color = \"green\"\nfrequency-chart-text-color = \"gray\"\nflows-chart-bar-selected-color = \"green\"\nflows-chart-bar-unselected-color = \"darkgray\"\nflows-chart-text-current-color = \"lightgreen\"\nflows-chart-text-non-current-color = \"white\"\nsamples-chart-color = \"yellow\"\nsamples-chart-lost-color = \"red\"\nhelp-dialog-bg-color = \"blue\"\nhelp-dialog-text-color = \"gray\"\nsettings-dialog-bg-color = \"blue\"\nsettings-tab-text-color = \"green\"\nsettings-table-header-text-color = \"black\"\nsettings-table-header-bg-color = \"white\"\nsettings-table-row-text-color = \"gray\"\nmap-world-color = \"white\"\nmap-radius-color = \"yellow\"\nmap-selected-color = \"green\"\nmap-info-panel-border-color = \"gray\"\nmap-info-panel-bg-color = \"black\"\nmap-info-panel-text-color = \"gray\"\ninfo-bar-bg-color = \"white\"\ninfo-bar-text-color = \"black\"\n\n# Tui key bindings Configuration.\n#\n# The supported modifiers are: shift, ctrl, alt, super, hyper & meta. Multiple\n# modifiers may be specified, for example ctrl+shift+b.\n#\n# See https://github.com/fujiapple852/trippy#key-bindings-reference for details.\n[bindings]\ntoggle-help = \"h\"\ntoggle-help-alt = \"?\"\ntoggle-settings = \"s\"\ntoggle-settings-tui = \"1\"\ntoggle-settings-trace = \"2\"\ntoggle-settings-dns = \"3\"\ntoggle-settings-geoip = \"4\"\ntoggle-settings-bindings = \"5\"\ntoggle-settings-theme = \"6\"\ntoggle-settings-columns = \"7\"\nnext-hop = \"down\"\nprevious-hop = \"up\"\nnext-trace = \"right\"\nprevious-trace = \"left\"\nnext-hop-address = \".\"\nprevious-hop-address = \",\"\naddress-mode-ip = \"i\"\naddress-mode-host = \"n\"\naddress-mode-both = \"b\"\ntoggle-freeze = \"ctrl+f\"\ntoggle-chart = \"c\"\ntoggle-map = \"m\"\ntoggle-flows = \"f\"\nexpand-privacy = \"p\"\ncontract-privacy = \"o\"\nexpand-hosts = \"]\"\nexpand-hosts-max = \"}\"\ncontract-hosts = \"[\"\ncontract-hosts-min = \"{\"\nchart-zoom-in = \"=\"\nchart-zoom-out = \"-\"\nclear-trace-data = \"ctrl+r\"\nclear-dns-cache = \"ctrl+k\"\nclear-selection = \"esc\"\ntoggle-as-info = \"z\"\ntoggle-hop-details = \"d\"\nquit = \"q\"\nquit-preserve-screen = \"shift+q\"\n"
  },
  {
    "path": "ubuntu-ppa/Dockerfile",
    "content": "FROM ubuntu:noble\n\nENV DEBIAN_FRONTEND=noninteractive\n\nRUN apt-get update && apt-get install -y \\\n    gpg debmake debhelper devscripts equivs \\\n    distro-info-data distro-info software-properties-common cargo cargo-1.85 wget\n\nCOPY release.sh release.sh\n\nWORKDIR /data\n\nCMD [\"./ubuntu-ppa/release.sh\"]\n"
  },
  {
    "path": "ubuntu-ppa/README.debian",
    "content": "Trippy packaging for Debian and Ubuntu\n======================================\n\nTL;DR: to generate your own debian package with your own Rust toolchain,\nthe vendored dependencies need to be generated first with:\n    ./debian/rules vendor\nthen you can simply run:\n    debuild --prepend-path ~/.cargo/bin -sa\nas long as the original Trippy source archive\n`trippy_<version>.orig.tar.gz` exists in the parent directory.\n\n---\n\nThe debian directory contains the necessary files to generate a debian\npackage. In order for the package to be built without network access\n(a requirement for most automatic build systems, such as Debian's\nand Canonical's), we cannot rely on the Cargo automatic dependencies\nresolution.\n\nInstead, the `vendor` rule uses [`cargo vendor`] [1] to \"vendor\" all\ncrates.io dependencies for the project into a `debian/vendor.tar.xz`\ntarball. This tarball contains all remote sources from dependencies that\nare specified in the Cargo manifest. It is automatically extracted during\nthe build, which uses the [`--frozen`] [2] option to prevent Cargo from\nattempting to access the network. Once this tarball is generated you\nonly need to use `vendor` rule again if you want to refresh the sources\nof the dependencies.\n\n---\n\nThe creation and administration of a Personal Package Archive (PPA)\nis beyond the scope of this doc, but if you need to host Trippy in your\nPPA, you simply need to run:\n    debuild --prepend-path ~/.cargo/bin -S -sa\nfollowed by:\n    dput <your ppa> ../<source_package_name>.changes\n\nThe provided `debian` directory targets the Ubuntu Jammy 22.04 LTS\ndistribution. It is possible to target other distributions simply\nby editing the `debian/changelog` file and changing the version and\ndistribution fields:\n    trippy (0.14.0-dev-1ubuntu0.1~jammy1) jammy; urgency=medium\n    trippy (0.14.0-dev-1ubuntu0.1~mantis1) mantis; urgency=medium\n    trippy (0.14.0-dev-1ubuntu0.1~noble1) noble; urgency=medium\nIt is preferable to use `debchange` for this, eg:\n    debchange --distribution noble --newversion 0.14.0-dev-1ubuntu0.1~noble1\n\n---\n\nNOTES:\n- all `commands` are relative to the Trippy source directory.\n- the tarball is compressed with xz as per the [blog post] [3] I used\nas a reference.\n\nTODOS:\n- remove Windows-specific dependencies from the vendored dependencies, see\n[Cargo issue #11929] [4]\n- move the vendor tarball outside of the debian directory, but this can\nonly be done once it's been relieved of the Windows-specific dependencies.\n\nREFERENCES:\n[1]: https://doc.rust-lang.org/cargo/commands/cargo-vendor.html\n[2]: https://doc.rust-lang.org/cargo/commands/cargo.html?highlight=frozen#manifest-options\n[3]: https://blog.zhimingwang.org/packaging-rust-project-for-ubuntu-ppa \"Packaging a Rust project for Ubuntu PPA\"\n[4]: https://github.com/rust-lang/cargo/issues/11929\n"
  },
  {
    "path": "ubuntu-ppa/README.md",
    "content": "# Debian Release\n\n## Prerequisites\n\n- update the `cargo-1.xx` version in `ubuntu-ppa/Dockerfile` (note: both `cargo` and `cargo-1.xx` are needed)\n- update the `cargo-1.xx` and `rust-1.xx` versions in `control`\n- update the `cargo-1.xx` versions in `rules`\n- update the `cargo-1.xx` versions in `release.sh`\n- update the trippy `VERSION` in the `release.sh` script\n- update the `UPSTREAM` in the `release.sh` script (removing any `+repack{N}` suffix)\n- reset the `REVISION` to `1` in the `release.sh` script\n\n## Build and release the debian package\n\nCopy the pgp key to the repo _root_ directory:\n\n```bash\ncp /path/to/pgp.key .\n```\n\nBuild the debian ppa builder Docker image from the `ubuntu-ppa` directory:\n\n```bash\ndocker build . -t fujiapple/trippy-ppa-build:latest\n```\n\nRun the debian Docker image (from the _repo_ root directory):\n\n```bash\ndocker run -it -v (pwd):/data fujiapple/trippy-ppa-build\n```\n\nNote that the upload is simulated, remove the `-ss` flag from dput to upload the package to the PPA.\n"
  },
  {
    "path": "ubuntu-ppa/cargo.config",
    "content": "[source.crates-io]\nreplace-with = \"vendored-sources\"\n\n[source.vendored-sources]\ndirectory = \"vendor\"\n"
  },
  {
    "path": "ubuntu-ppa/changelog",
    "content": "trippy (0.14.0-dev-ppa2~ubuntu24.04) noble; urgency=medium\n\n  * New upstream release\n\n -- Fuji Apple <fujiapple852@gmail.com>  Wed, 21 Dec 2024 12:34:56 +0000"
  },
  {
    "path": "ubuntu-ppa/control",
    "content": "Source: trippy\nSection: contrib/net\nPriority: optional\nMaintainer: Fuji Apple <fujiapple852@gmail.com>\nRules-Requires-Root: no\nBuild-Depends: debhelper-compat (= 13),\n cargo-1.85,\n rustc-1.85,\n libstd-rust-dev\nStandards-Version: 4.6.2\nVcs-Browser: https://github.com/fujiapple852/trippy\nVcs-Git: https://github.com/fujiapple852/trippy.git\n\nPackage: trippy\nArchitecture: any\nDepends:\n ${shlibs:Depends},\n ${misc:Depends},\nDescription: network diagnostic tool combining traceroute and ping\n Trippy combines the functionality of traceroute and ping and\n is designed to assist with the analysis of network issues.\n"
  },
  {
    "path": "ubuntu-ppa/copyright",
    "content": "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nSource: https://github.com/fujiapple852/trippy\nUpstream-Name: trippy\nUpstream-Contact: FujiApple <fujiapple852@gmail.com>\n\nFiles:\n *\nCopyright:\n 2022-2024 Trippy Contributors (https://github.com/fujiapple852/trippy/graphs/contributors)\nLicense: Apache-2.0\n\nFiles:\n debian/*\nCopyright:\n 2024 Fuji Apple <fujiapple852@gmail.com>\nLicense: Apache-2.0\n\nLicense: Apache-2.0\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n .\n https://www.apache.org/licenses/LICENSE-2.0\n .\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\nComment:\n On Debian systems, the complete text of the Apache version 2.0 license\n can be found in \"/usr/share/common-licenses/Apache-2.0\".\n"
  },
  {
    "path": "ubuntu-ppa/release.sh",
    "content": "#!/bin/bash\n\nset -o errexit -o pipefail -o nounset\n\n# The Trippy version to release\nVERSION=\"0.14.0-dev\"\n\n# The upstream version to use in the PPA\n#\n# This should usually be the same as the VERSION, but if the upstream tarball has been repacked\n# (e.g. to remove unnecessary files or to fix the tarball) then this should be set to the upstream version\n# with a `+repack{N}` suffix, where `{N}` is the number of times the tarball has been repacked. i.e. `0.1.0+repack1`.\nUPSTREAM=\"0.14.0-dev\"\n\n# The revision number for the PPA\n#\n# This is incremented each time a new upload is made to the PPA so will always be one greater than repack number.\nREVISION=1\n\n# The Ubuntu series to build for\nSERIES=(\"noble\" \"jammy\")\n\nTARBALL=\"trippy_${UPSTREAM}.orig.tar.gz\"\nPACKAGE=\"trippy\"\nCHANGES=\"New upstream release\"\nexport DEBEMAIL=\"fujiapple852@gmail.com\"\nexport DEBFULLNAME=\"Fuji Apple\"\n\n# Import GPG key securely\nif [[ ! -f launchpad_secret_key.pgp ]]; then\n    echo \"Error: GPG key file 'launchpad_secret_key.pgp' not found.\" >&2\n    exit 1\nfi\ngpg --batch --import launchpad_secret_key.pgp\n\n# Extract GPG key ID\nGPG_KEY_ID=$(gpg --with-colons --import-options show-only --import launchpad_secret_key.pgp | awk -F: '/^sec/ {print $5}')\n\n# Check GPG key expiration\nif gpg --list-keys --with-colons \"${GPG_KEY_ID}\" | grep '^pub' | grep '[e]'; then\n    echo \"GPG key has expired. Please update your GPG key.\" >&2\n    exit 1\nfi\n\n# Download TARBALL\nwget -O \"${TARBALL}\" \"https://github.com/fujiapple852/trippy/archive/refs/tags/${VERSION}.tar.gz\"\nif [[ ! -f \"${TARBALL}\" ]]; then\n    echo \"Error: Failed to download TARBALL.\" >&2\n    exit 1\nfi\n\n# Vendor the cargo dependencies\n# We have to ensure we run cargo `vendor --locked` against the src in the tarball, not the src in the current directory.\ntar -xf \"${TARBALL}\"\npushd \"trippy-${VERSION}\"\nrm -f ../ubuntu-ppa/vendor.tar.xz\nrm -rf vendor\ncargo-1.85 vendor --locked\ntar -cJf ../ubuntu-ppa/vendor.tar.xz vendor\npopd\nrm -rf \"trippy-${VERSION}\"\n\nfor series in \"${SERIES[@]}\"; do\n    UBUNTU_VERSION=$(distro-info --series \"${series}\" -r | cut -d' ' -f1)\n    BUILD_DIR=\"build-${series}\"\n    mkdir -p \"${BUILD_DIR}\"\n    cp -r ubuntu-ppa \"${BUILD_DIR}/debian\"\n    cd \"${BUILD_DIR}\"\n\n    # Update changelog for the specific series\n    rm -f debian/changelog\n    dch --create --distribution \"${series}\" --PACKAGE \"${PACKAGE}\" \\\n        --newversion \"${UPSTREAM}-ppa${REVISION}~ubuntu${UBUNTU_VERSION}\" \"$CHANGES\"\n\n    # Build the source PACKAGE\n    debuild --prepend-path ~/.cargo/bin -S -sa\n\n    cd ..\ndone\n\n# The -ss flag can be added to simulate the upload\nfor changes_file in ./*.changes; do\n    dput ppa:fujiapple/trippy \"${changes_file}\"\ndone\n"
  },
  {
    "path": "ubuntu-ppa/rules",
    "content": "#!/usr/bin/make -f\n\n.PHONY: override_dh_strip\n\n%:\n\tdh $@\n\noverride_dh_auto_build:\n\tmkdir .cargo\n\tcp debian/cargo.config .cargo/config\n\ttar xJf debian/vendor.tar.xz\n\tcargo-1.85 build --release --frozen\n\noverride_dh_auto_clean:\n\tcargo-1.85 clean\n\trm -rf .cargo vendor\n\noverride_dh_strip:\n\tdh_strip --no-automatic-dbgsym\n"
  },
  {
    "path": "ubuntu-ppa/source/format",
    "content": "3.0 (quilt)\n"
  },
  {
    "path": "ubuntu-ppa/source/include-binaries",
    "content": "debian/vendor.tar.xz\n"
  },
  {
    "path": "ubuntu-ppa/trippy.docs",
    "content": "README.md\n"
  },
  {
    "path": "ubuntu-ppa/trippy.install",
    "content": "target/release/trip usr/bin\n"
  }
]