├── .clippy.toml ├── .codecov.yml ├── .github ├── CODEOWNERS ├── actionlint-matcher.json ├── dependabot.yml └── workflows │ ├── actionlint.yml │ └── check.yml ├── .gitignore ├── .rustfmt.toml ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── SECURITY.md ├── build.rs └── src ├── bsd.rs ├── lib.rs ├── linux.rs ├── routesocket.rs └── windows.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | allow-mixed-uninlined-format-args = false 2 | allow-unwrap-in-tests = true 3 | doc-valid-idents = ["NetBSD", "OpenBSD", ".."] 4 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | 3 | # Do not notify until at least three results have been uploaded from the CI pipeline. 4 | # (This corresponds to the three main platforms we support: Linux, macOS, and Windows.) 5 | codecov: 6 | notify: 7 | after_n_builds: 3 8 | comment: 9 | after_n_builds: 3 10 | 11 | coverage: 12 | status: 13 | project: 14 | default: 15 | threshold: 0.05% 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @KershawChang @martinthomson @larseggert @mxinden 2 | -------------------------------------------------------------------------------- /.github/actionlint-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "actionlint", 5 | "pattern": [ 6 | { 7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", 8 | "file": 1, 9 | "line": 2, 10 | "column": 3, 11 | "message": 4, 12 | "code": 5 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | # Disable all non-security updates. 11 | # 12 | open-pull-requests-limit: 0 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint GitHub Actions workflows 2 | on: 3 | push: 4 | branches: ["main"] 5 | paths: [".github/**"] 6 | pull_request: 7 | branches: ["main"] 8 | paths: [".github/**"] 9 | merge_group: 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref_name }} 13 | cancel-in-progress: true 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | actionlint: 20 | runs-on: ubuntu-24.04 21 | defaults: 22 | run: 23 | shell: bash 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | with: 27 | persist-credentials: false 28 | 29 | - name: Download actionlint 30 | id: get_actionlint 31 | run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 32 | 33 | - name: Check workflow files 34 | env: 35 | ACTIONLINT: ${{ steps.get_actionlint.outputs.executable }} 36 | run: | 37 | echo "::add-matcher::.github/actionlint-matcher.json" 38 | $ACTIONLINT -color 39 | 40 | zizmor: 41 | name: zizmor 🌈 42 | runs-on: ubuntu-24.04 43 | permissions: 44 | security-events: write 45 | steps: 46 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 47 | with: 48 | persist-credentials: false 49 | 50 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 51 | 52 | - run: uvx zizmor --persona auditor --format sarif . > results.sarif 53 | env: 54 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 57 | with: 58 | sarif_file: results.sarif 59 | category: zizmor 60 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["main"] 7 | merge_group: 8 | schedule: 9 | - cron: "26 3 * * *" # 03:26 UTC 10 | env: 11 | CARGO_TERM_COLOR: always 12 | RUST_BACKTRACE: 1 13 | RUST_TEST_TIME_UNIT: 10,30 14 | RUST_TEST_TIME_INTEGRATION: 10,30 15 | RUST_TEST_TIME_DOCTEST: 10,30 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref_name }} 19 | cancel-in-progress: true 20 | 21 | permissions: 22 | contents: read 23 | 24 | defaults: 25 | run: 26 | shell: bash 27 | 28 | jobs: 29 | toolchains: 30 | runs-on: ubuntu-24.04 31 | outputs: 32 | toolchains: ${{ steps.toolchains.outputs.toolchains }} 33 | steps: 34 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 35 | with: 36 | sparse-checkout: Cargo.toml 37 | persist-credentials: false 38 | - id: toolchains 39 | run: | 40 | msrv="$(grep rust-version Cargo.toml | tr -d '"' | cut -f3 -d\ )" 41 | echo "toolchains=[\"$msrv\", \"stable\", \"nightly\"]" >> "$GITHUB_OUTPUT" 42 | 43 | check: 44 | needs: toolchains 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, windows-2025] 49 | rust-toolchain: ${{ fromJSON(needs.toolchains.outputs.toolchains) }} 50 | type: [debug] 51 | include: 52 | # Also do some release builds on the latest OS versions. 53 | - os: ubuntu-24.04 54 | rust-toolchain: stable 55 | type: release 56 | - os: macos-15 57 | rust-toolchain: stable 58 | type: release 59 | - os: windows-2025 60 | rust-toolchain: stable 61 | type: release 62 | # Also do some debug builds on the oldest OS versions. 63 | - os: ubuntu-22.04 64 | rust-toolchain: stable 65 | type: debug 66 | - os: macos-13 67 | rust-toolchain: stable 68 | type: debug 69 | - os: windows-2022 70 | rust-toolchain: stable 71 | type: debug 72 | env: 73 | BUILD_TYPE: ${{ matrix.type == 'release' && '--release' || '' }} 74 | runs-on: ${{ matrix.os }} 75 | 76 | steps: 77 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 78 | with: 79 | persist-credentials: false 80 | 81 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 82 | with: 83 | repository: mozilla/neqo 84 | sparse-checkout: | 85 | .github/actions/rust 86 | path: neqo 87 | persist-credentials: false 88 | - uses: ./neqo/.github/actions/rust 89 | with: 90 | version: ${{ matrix.rust-toolchain }} 91 | components: ${{ matrix.rust-toolchain == 'nightly' && 'llvm-tools' || '' }} ${{ matrix.rust-toolchain == 'nightly' && 'rust-src' || '' }} 92 | tools: ${{ matrix.rust-toolchain == 'nightly' && 'cargo-llvm-cov, ' || '' }} 93 | token: ${{ secrets.GITHUB_TOKEN }} 94 | 95 | - name: Check 96 | run: | 97 | OPTIONS=(--all-targets) 98 | if [ "$BUILD_TYPE" ]; then 99 | OPTIONS+=("$BUILD_TYPE") 100 | fi 101 | cargo check "${OPTIONS[@]}" 102 | 103 | - name: Run tests and determine coverage 104 | env: 105 | RUST_LOG: trace 106 | TOOLCHAIN: ${{ matrix.rust-toolchain }} 107 | WINDOWS: ${{ startsWith(matrix.os, 'windows') && 'windows' || '' }}" 108 | run: | 109 | OPTIONS=(--no-fail-fast) 110 | if [ "$BUILD_TYPE" ]; then 111 | OPTIONS+=("$BUILD_TYPE") 112 | fi 113 | if [ "$TOOLCHAIN" == "nightly" ] && [ "${{ matrix.type }}" == "debug" ]; then 114 | cargo llvm-cov test --mcdc --include-ffi "${OPTIONS[@]}" --codecov --output-path codecov.json 115 | else 116 | if [ "$WINDOWS" == "windows" ]; then 117 | # The codegen_windows_bindings test only succeeds when run via llvm-cov?! 118 | OPTIONS+=(-- --skip codegen_windows_bindings) 119 | fi 120 | cargo test "${OPTIONS[@]}" 121 | fi 122 | cargo bench --no-run 123 | 124 | - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 125 | with: 126 | files: codecov.json 127 | fail_ci_if_error: false 128 | token: ${{ secrets.CODECOV_TOKEN }} 129 | verbose: true 130 | env: 131 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 132 | if: matrix.type == 'debug' && matrix.rust-toolchain == 'nightly' 133 | 134 | - name: Run tests with sanitizers 135 | if: (matrix.os == 'ubuntu-24.04' || matrix.os == 'macos-15') && matrix.rust-toolchain == 'nightly' 136 | env: 137 | RUST_LOG: trace 138 | ASAN_OPTIONS: detect_leaks=1:detect_stack_use_after_return=1 139 | run: | 140 | if [ "${{ matrix.os }}" = "ubuntu-24.04" ]; then 141 | sudo apt-get install -y --no-install-recommends llvm 142 | TARGET="x86_64-unknown-linux-gnu" 143 | SANITIZERS="address thread leak memory" 144 | elif [ "${{ matrix.os }}" = "macos-15" ]; then 145 | # llvm-symbolizer (as part of llvm) is installed by default on macOS runners 146 | TARGET="aarch64-apple-darwin" 147 | # no memory and leak sanitizer support yet 148 | SANITIZERS="address thread" 149 | # Suppress non-mtu leaks on macOS. TODO: Check occasionally if these are still needed. 150 | { 151 | echo "leak:dyld4::RuntimeState" 152 | echo "leak:fetchInitializingClassList" 153 | echo "leak:std::rt::lang_start_internal" 154 | } > suppressions.txt 155 | # shellcheck disable=SC2155 156 | export LSAN_OPTIONS="suppressions=$(pwd)/suppressions.txt" 157 | fi 158 | for sanitizer in $SANITIZERS; do 159 | echo "Running tests with $sanitizer sanitizer..." 160 | export RUSTFLAGS="-Z sanitizer=$sanitizer" 161 | export RUSTDOCFLAGS="$RUSTFLAGS" 162 | cargo +nightly test -Z build-std --target "$TARGET" 163 | done 164 | 165 | clippy: 166 | strategy: 167 | fail-fast: false 168 | matrix: 169 | os: [ubuntu-24.04, macos-15, windows-2025] 170 | runs-on: ${{ matrix.os }} 171 | steps: 172 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 173 | with: 174 | persist-credentials: false 175 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 176 | with: 177 | repository: mozilla/neqo 178 | sparse-checkout: | 179 | .github/actions/rust 180 | path: neqo 181 | persist-credentials: false 182 | - uses: ./neqo/.github/actions/rust 183 | with: 184 | components: clippy 185 | tools: cargo-hack 186 | token: ${{ secrets.GITHUB_TOKEN }} 187 | 188 | - run: cargo hack clippy --all-targets --feature-powerset --exclude-features gecko -- -D warnings 189 | - run: cargo doc --workspace --no-deps --document-private-items 190 | env: 191 | RUSTDOCFLAGS: "--deny rustdoc::broken_intra_doc_links --deny warnings" 192 | 193 | machete: 194 | runs-on: ubuntu-24.04 195 | steps: 196 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 197 | with: 198 | persist-credentials: false 199 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 200 | with: 201 | repository: mozilla/neqo 202 | sparse-checkout: | 203 | .github/actions/rust 204 | path: neqo 205 | persist-credentials: false 206 | - uses: ./neqo/.github/actions/rust 207 | with: 208 | tools: cargo-machete 209 | token: ${{ secrets.GITHUB_TOKEN }} 210 | 211 | # --with-metadata has false positives, see https://github.com/bnjbvr/cargo-machete/issues/127 212 | - run: cargo machete 213 | 214 | format: 215 | runs-on: ubuntu-24.04 216 | steps: 217 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 218 | with: 219 | persist-credentials: false 220 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 221 | with: 222 | repository: mozilla/neqo 223 | sparse-checkout: | 224 | .github/actions/rust 225 | path: neqo 226 | persist-credentials: false 227 | - uses: ./neqo/.github/actions/rust 228 | with: 229 | version: nightly 230 | components: rustfmt 231 | token: ${{ secrets.GITHUB_TOKEN }} 232 | 233 | - run: cargo fmt --all -- --check 234 | 235 | semver: 236 | runs-on: ubuntu-24.04 237 | steps: 238 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 239 | with: 240 | persist-credentials: false 241 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 242 | with: 243 | repository: mozilla/neqo 244 | sparse-checkout: | 245 | .github/actions/rust 246 | path: neqo 247 | persist-credentials: false 248 | - uses: ./neqo/.github/actions/rust 249 | with: 250 | tools: cargo-semver-checks 251 | token: ${{ secrets.GITHUB_TOKEN }} 252 | 253 | - run: cargo semver-checks --default-features 254 | 255 | readme: 256 | runs-on: ubuntu-24.04 257 | steps: 258 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 259 | with: 260 | persist-credentials: false 261 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 262 | with: 263 | repository: mozilla/neqo 264 | sparse-checkout: | 265 | .github/actions/rust 266 | path: neqo 267 | persist-credentials: false 268 | - uses: ./neqo/.github/actions/rust 269 | with: 270 | tools: cargo-readme 271 | token: ${{ secrets.GITHUB_TOKEN }} 272 | 273 | - run: | 274 | cargo readme -o /tmp/README.md 275 | diff -u README.md /tmp/README.md 276 | 277 | check-vm: 278 | strategy: 279 | fail-fast: false 280 | matrix: 281 | os: [freebsd, openbsd, netbsd, solaris] 282 | runs-on: ubuntu-24.04 283 | env: 284 | RUST_LOG: trace 285 | 286 | steps: 287 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 288 | with: 289 | persist-credentials: false 290 | - run: curl -o rustup.sh --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs 291 | 292 | - if: matrix.os == 'freebsd' 293 | uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0 294 | with: 295 | usesh: true 296 | envs: "CARGO_TERM_COLOR RUST_BACKTRACE RUST_LOG GITHUB_ACTIONS RUST_TEST_TIME_UNIT RUST_TEST_TIME_INTEGRATION RUST_TEST_TIME_DOCTEST" 297 | prepare: | # This executes as root 298 | set -e 299 | pkg install -y curl llvm 300 | run: | # This executes as user 301 | set -e 302 | sh rustup.sh --default-toolchain nightly --profile minimal --component clippy,llvm-tools -y 303 | . "$HOME/.cargo/env" 304 | cargo check --all-targets 305 | cargo clippy -- -D warnings 306 | cargo install cargo-llvm-cov --locked 307 | cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json 308 | cargo test --no-fail-fast --release 309 | rm -rf target # Don't sync this back to host 310 | 311 | - if: matrix.os == 'openbsd' 312 | uses: vmactions/openbsd-vm@0d65352eee1508bab7cb12d130536d3a556be487 # v1.1.8 313 | with: 314 | usesh: true 315 | envs: "CARGO_TERM_COLOR RUST_BACKTRACE RUST_LOG GITHUB_ACTIONS RUST_TEST_TIME_UNIT RUST_TEST_TIME_INTEGRATION RUST_TEST_TIME_DOCTEST" 316 | prepare: | # This executes as root 317 | set -e 318 | # TODO: Is there a way to not pin the version of llvm? -z to pkg_add doesn't work. 319 | pkg_add rust rust-clippy llvm-19.1.7p3 # rustup doesn't support OpenBSD at all 320 | run: | # This executes as user 321 | set -e 322 | export LIBCLANG_PATH=/usr/local/llvm19/lib 323 | cargo check --all-targets 324 | cargo clippy -- -D warnings 325 | # FIXME: No profiler support in openbsd currently, error is: 326 | # > error[E0463]: can't find crate for `profiler_builtins` 327 | # > = note: the compiler may have been built without the profiler runtime 328 | # export LLVM_COV=/usr/local/llvm19/bin/llvm-cov 329 | # export LLVM_PROFDATA=/usr/local/llvm19/bin/llvm-profdata 330 | # cargo install cargo-llvm-cov --locked 331 | # cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json 332 | cargo test --no-fail-fast # Remove this once profiler is supported 333 | cargo test --no-fail-fast --release 334 | rm -rf target # Don't sync this back to host 335 | 336 | - if: matrix.os == 'netbsd' 337 | uses: vmactions/netbsd-vm@46a58bbf03682b4cb24142b97fa315ae52bed573 # v1.1.8 338 | with: 339 | usesh: true 340 | envs: "CARGO_TERM_COLOR RUST_BACKTRACE RUST_LOG GITHUB_ACTIONS RUST_TEST_TIME_UNIT RUST_TEST_TIME_INTEGRATION RUST_TEST_TIME_DOCTEST" 341 | prepare: | # This executes as root 342 | set -e 343 | /usr/sbin/pkg_add pkgin 344 | pkgin -y install curl clang 345 | run: | # This executes as user 346 | set -e 347 | sh rustup.sh --default-toolchain nightly --profile minimal --component clippy,llvm-tools -y 348 | . "$HOME/.cargo/env" 349 | cargo check --all-targets 350 | cargo clippy -- -D warnings 351 | # FIXME: No profiler support in netbsd currently, error is: 352 | # > error[E0463]: can't find crate for `profiler_builtins` 353 | # > = note: the compiler may have been built without the profiler runtime 354 | # cargo install cargo-llvm-cov --locked 355 | # cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json 356 | cargo test --no-fail-fast # Remove this once profiler is supported 357 | cargo test --no-fail-fast --release 358 | rm -rf target # Don't sync this back to host 359 | 360 | - if: matrix.os == 'solaris' 361 | uses: vmactions/solaris-vm@170f1f96f376cf7467cc41627e0c7590932fccaa # v1.1.4 362 | with: 363 | release: "11.4-gcc" 364 | usesh: true 365 | envs: "CARGO_TERM_COLOR RUST_BACKTRACE RUST_LOG GITHUB_ACTIONS RUST_TEST_TIME_UNIT RUST_TEST_TIME_INTEGRATION RUST_TEST_TIME_DOCTEST" 366 | prepare: | # This executes as root 367 | set -e 368 | pkg install clang-libs 369 | run: | # This executes as also as root on Solaris 370 | set -e 371 | source <(curl -s https://raw.githubusercontent.com/psumbera/solaris-rust/refs/heads/main/sh.rust-web-install) || true # This doesn't exit with zero on success 372 | export LIBCLANG_PATH="/usr/lib/amd64" 373 | cargo check --all-targets 374 | cargo clippy -- -D warnings 375 | # FIXME: No profiler support in openbsd currently, error is: 376 | # > error[E0463]: can't find crate for `profiler_builtins` 377 | # > = note: the compiler may have been built without the profiler runtime 378 | # cargo install cargo-llvm-cov --locked 379 | # cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json 380 | cargo test --no-fail-fast # Remove this once profiler is supported 381 | cargo test --no-fail-fast --release 382 | rm -rf target # Don't sync this back to host 383 | 384 | - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 385 | with: 386 | files: codecov.json 387 | fail_ci_if_error: false 388 | token: ${{ secrets.CODECOV_TOKEN }} 389 | verbose: true 390 | env: 391 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 392 | 393 | check-cargo-lock: 394 | name: Ensure `Cargo.lock` contains all required dependencies 395 | runs-on: ubuntu-24.04 396 | steps: 397 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 398 | with: 399 | persist-credentials: false 400 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 401 | with: 402 | repository: mozilla/neqo 403 | sparse-checkout: | 404 | .github/actions/rust 405 | path: neqo 406 | persist-credentials: false 407 | - uses: ./neqo/.github/actions/rust 408 | with: 409 | token: ${{ secrets.GITHUB_TOKEN }} 410 | - run: cargo update -w --locked 411 | 412 | check-android: 413 | runs-on: ubuntu-24.04 414 | env: 415 | # https://searchfox.org/mozilla-central/search?q=NDK_VERSION =&path=python/mozboot/mozboot/android.py 416 | NDK_VERSION: 27.2.12479018 # r27c 417 | # https://searchfox.org/mozilla-central/search?q=\bapi_level=&path=taskcluster/scripts/misc/build-llvm-common.sh®exp=true 418 | API_LEVEL: 21 419 | 420 | strategy: 421 | matrix: 422 | include: 423 | - target: x86_64-linux-android 424 | emulator-arch: x86_64 425 | # Note that x86_64 image is only available for API 21+. See 426 | # https://github.com/ReactiveCircus/android-emulator-runner?tab=readme-ov-file#configurations. 427 | - target: i686-linux-android 428 | emulator-arch: x86 429 | # FIXME: https://github.com/ReactiveCircus/android-emulator-runner/issues/404 430 | # - target: armv7-linux-androideabi 431 | # emulator-arch: arm64-v8 432 | 433 | steps: 434 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 435 | with: 436 | persist-credentials: false 437 | 438 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 439 | with: 440 | repository: mozilla/neqo 441 | sparse-checkout: | 442 | .github/actions/rust 443 | path: neqo 444 | persist-credentials: false 445 | 446 | - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 447 | with: 448 | distribution: zulu 449 | java-version: 23 450 | 451 | - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 452 | - run: sdkmanager --install "ndk;${{ env.NDK_VERSION }}" 453 | 454 | - uses: ./neqo/.github/actions/rust 455 | with: 456 | version: stable 457 | targets: ${{ matrix.target }} 458 | tools: cargo-ndk 459 | token: ${{ secrets.GITHUB_TOKEN }} 460 | 461 | - run: cargo ndk --bindgen -t ${{ matrix.target }} test --no-run 462 | 463 | - env: 464 | TARGET: ${{ matrix.target }} 465 | run: | 466 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 467 | sudo udevadm control --reload-rules 468 | sudo udevadm trigger --name-match=kvm 469 | cat <<'EOF' > /tmp/rust-android-run-tests-on-emulator.sh 470 | #!/bin/bash 471 | set -e 472 | adb shell ip addr show 473 | export GITHUB_ACTIONS=1 474 | adb wait-for-device 475 | while [ -z "$(adb shell getprop sys.boot_completed | tr -d '\r')" ]; do sleep 1; done 476 | any_failures=0 477 | for test in $(find target/$TARGET/debug/deps/ -type f -executable ! -name "*.so" -name "*-*"); do 478 | adb push "$test" /data/local/tmp/ 479 | adb shell chmod +x /data/local/tmp/$(basename "$test") 480 | # See https://unix.stackexchange.com/a/451140/409256 481 | adb shell "API_LEVEL=$API_LEVEL /data/local/tmp/$(basename "$test") || echo _FAIL_" 2>&1 | tee output 482 | grep _FAIL_ output && any_failures=1 483 | done 484 | exit $any_failures 485 | EOF 486 | chmod a+x /tmp/rust-android-run-tests-on-emulator.sh 487 | 488 | - uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # v2.34.0 489 | with: 490 | api-level: ${{ env.API_LEVEL }} 491 | arch: ${{ matrix.emulator-arch == 'arm64-v8' && 'arm64-v8a' || matrix.emulator-arch }} 492 | ndk: ${{ env.NDK_VERSION }} 493 | emulator-boot-timeout: 120 494 | script: /tmp/rust-android-run-tests-on-emulator.sh 495 | env: 496 | TARGET: ${{ matrix.target }} 497 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | /.vscode/ 4 | /codecov.json 5 | /lcov.info 6 | /target/ 7 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Keep in sync with `Cargo.toml` `edition`. 2 | # 3 | # `rustfmt` envoked not through `cargo fmt` but directly does not pick up Rust 4 | # edition in `Cargo.toml`. Thus duplicate here. 5 | edition = "2021" 6 | 7 | comment_width=100 8 | wrap_comments=true 9 | 10 | imports_granularity="Crate" 11 | group_imports="StdExternalCrate" 12 | 13 | format_code_in_doc_comments=true 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette 4 | guidelines. For more details, please read the [Mozilla Community Participation 5 | Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | 9 | For more information on how to report violations of the Community Participation 10 | Guidelines, please read our '[How to 11 | Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' 12 | page. 13 | 14 | ## Project Specific Etiquette 15 | 16 | Please consider the advice in the [Bugzilla etiquette 17 | guide](https://bugzilla.mozilla.org/page.cgi?id=etiquette.html) when 18 | contributing to this project. 19 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "bindgen" 16 | version = "0.69.4" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" 19 | dependencies = [ 20 | "bitflags", 21 | "cexpr", 22 | "clang-sys", 23 | "itertools", 24 | "lazy_static", 25 | "lazycell", 26 | "proc-macro2", 27 | "quote", 28 | "regex", 29 | "rustc-hash", 30 | "shlex", 31 | "syn", 32 | ] 33 | 34 | [[package]] 35 | name = "bitflags" 36 | version = "2.6.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 39 | 40 | [[package]] 41 | name = "cexpr" 42 | version = "0.6.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 45 | dependencies = [ 46 | "nom", 47 | ] 48 | 49 | [[package]] 50 | name = "cfg-if" 51 | version = "1.0.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 54 | 55 | [[package]] 56 | name = "cfg_aliases" 57 | version = "0.2.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 60 | 61 | [[package]] 62 | name = "clang-sys" 63 | version = "1.7.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" 66 | dependencies = [ 67 | "glob", 68 | "libc", 69 | "libloading", 70 | ] 71 | 72 | [[package]] 73 | name = "either" 74 | version = "1.8.1" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 77 | 78 | [[package]] 79 | name = "glob" 80 | version = "0.3.1" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 83 | 84 | [[package]] 85 | name = "itertools" 86 | version = "0.10.5" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 89 | dependencies = [ 90 | "either", 91 | ] 92 | 93 | [[package]] 94 | name = "lazy_static" 95 | version = "1.4.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 98 | 99 | [[package]] 100 | name = "lazycell" 101 | version = "1.3.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 104 | 105 | [[package]] 106 | name = "libc" 107 | version = "0.2.158" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 110 | 111 | [[package]] 112 | name = "libloading" 113 | version = "0.8.3" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" 116 | dependencies = [ 117 | "cfg-if", 118 | "windows-targets", 119 | ] 120 | 121 | [[package]] 122 | name = "memchr" 123 | version = "2.7.4" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 126 | 127 | [[package]] 128 | name = "minimal-lexical" 129 | version = "0.2.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 132 | 133 | [[package]] 134 | name = "mozbuild" 135 | version = "0.1.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "903970ae2f248d7275214cf8f387f8ba0c4ea7e3d87a320e85493db60ce28616" 138 | 139 | [[package]] 140 | name = "mtu" 141 | version = "0.2.9" 142 | dependencies = [ 143 | "bindgen", 144 | "cfg_aliases", 145 | "libc", 146 | "mozbuild", 147 | "static_assertions", 148 | "windows", 149 | ] 150 | 151 | [[package]] 152 | name = "nom" 153 | version = "7.1.3" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 156 | dependencies = [ 157 | "memchr", 158 | "minimal-lexical", 159 | ] 160 | 161 | [[package]] 162 | name = "proc-macro2" 163 | version = "1.0.86" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 166 | dependencies = [ 167 | "unicode-ident", 168 | ] 169 | 170 | [[package]] 171 | name = "quote" 172 | version = "1.0.35" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 175 | dependencies = [ 176 | "proc-macro2", 177 | ] 178 | 179 | [[package]] 180 | name = "regex" 181 | version = "1.9.4" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" 184 | dependencies = [ 185 | "aho-corasick", 186 | "memchr", 187 | "regex-automata", 188 | "regex-syntax", 189 | ] 190 | 191 | [[package]] 192 | name = "regex-automata" 193 | version = "0.3.7" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" 196 | dependencies = [ 197 | "aho-corasick", 198 | "memchr", 199 | "regex-syntax", 200 | ] 201 | 202 | [[package]] 203 | name = "regex-syntax" 204 | version = "0.7.5" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" 207 | 208 | [[package]] 209 | name = "rustc-hash" 210 | version = "1.1.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 213 | 214 | [[package]] 215 | name = "shlex" 216 | version = "1.3.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 219 | 220 | [[package]] 221 | name = "static_assertions" 222 | version = "1.1.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 225 | 226 | [[package]] 227 | name = "syn" 228 | version = "2.0.87" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 231 | dependencies = [ 232 | "proc-macro2", 233 | "quote", 234 | "unicode-ident", 235 | ] 236 | 237 | [[package]] 238 | name = "unicode-ident" 239 | version = "1.0.6" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 242 | 243 | [[package]] 244 | name = "windows" 245 | version = "0.58.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" 248 | dependencies = [ 249 | "windows-core", 250 | "windows-targets", 251 | ] 252 | 253 | [[package]] 254 | name = "windows-core" 255 | version = "0.58.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" 258 | dependencies = [ 259 | "windows-implement", 260 | "windows-interface", 261 | "windows-result", 262 | "windows-strings", 263 | "windows-targets", 264 | ] 265 | 266 | [[package]] 267 | name = "windows-implement" 268 | version = "0.58.0" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" 271 | dependencies = [ 272 | "proc-macro2", 273 | "quote", 274 | "syn", 275 | ] 276 | 277 | [[package]] 278 | name = "windows-interface" 279 | version = "0.58.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" 282 | dependencies = [ 283 | "proc-macro2", 284 | "quote", 285 | "syn", 286 | ] 287 | 288 | [[package]] 289 | name = "windows-result" 290 | version = "0.2.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 293 | dependencies = [ 294 | "windows-targets", 295 | ] 296 | 297 | [[package]] 298 | name = "windows-strings" 299 | version = "0.1.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 302 | dependencies = [ 303 | "windows-result", 304 | "windows-targets", 305 | ] 306 | 307 | [[package]] 308 | name = "windows-targets" 309 | version = "0.52.6" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 312 | dependencies = [ 313 | "windows_aarch64_gnullvm", 314 | "windows_aarch64_msvc", 315 | "windows_i686_gnu", 316 | "windows_i686_gnullvm", 317 | "windows_i686_msvc", 318 | "windows_x86_64_gnu", 319 | "windows_x86_64_gnullvm", 320 | "windows_x86_64_msvc", 321 | ] 322 | 323 | [[package]] 324 | name = "windows_aarch64_gnullvm" 325 | version = "0.52.6" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 328 | 329 | [[package]] 330 | name = "windows_aarch64_msvc" 331 | version = "0.52.6" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 334 | 335 | [[package]] 336 | name = "windows_i686_gnu" 337 | version = "0.52.6" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 340 | 341 | [[package]] 342 | name = "windows_i686_gnullvm" 343 | version = "0.52.6" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 346 | 347 | [[package]] 348 | name = "windows_i686_msvc" 349 | version = "0.52.6" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 352 | 353 | [[package]] 354 | name = "windows_x86_64_gnu" 355 | version = "0.52.6" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 358 | 359 | [[package]] 360 | name = "windows_x86_64_gnullvm" 361 | version = "0.52.6" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 364 | 365 | [[package]] 366 | name = "windows_x86_64_msvc" 367 | version = "0.52.6" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 370 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mtu" 3 | description = "Obtain the local network interface MTU towards a given IP address." 4 | keywords = ["mozilla", "interface", "mtu"] 5 | categories = ["network-programming", "web-programming"] 6 | homepage = "https://github.com/mozilla/mtu/" 7 | repository = "https://github.com/mozilla/mtu/" 8 | authors = ["The Mozilla Necko Team "] 9 | readme = "README.md" 10 | version = "0.2.9" 11 | edition = "2021" 12 | license = "MIT OR Apache-2.0" 13 | # Don't increase beyond what Firefox is currently using: 14 | # https://searchfox.org/mozilla-central/search?q=MINIMUM_RUST_VERSION&path=python/mozboot/mozboot/util.py 15 | # In addition, currently Mozilla CI uses a Rust fork before the official Rust 1.82.0 release, thus stay on 1.81.0 for now. 16 | # https://bugzilla.mozilla.org/show_bug.cgi?id=1968057#c1 17 | rust-version = "1.81.0" 18 | 19 | [badges] 20 | codecov = { repository = "mozilla/mtu", branch = "main" } 21 | is-it-maintained-issue-resolution = { repository = "mozilla/mtu", branch = "main" } 22 | is-it-maintained-open-issues = { repository = "mozilla/mtu", branch = "main" } 23 | maintenance = { status = "actively-developed", branch = "main" } 24 | 25 | [dependencies] 26 | libc = { version = "0.2", default-features = false } 27 | static_assertions = { version = "1.1", default-features = false } 28 | 29 | [target.'cfg(windows)'.dependencies] 30 | windows = { version = ">=0.58,<0.60", features = [ 31 | "Win32_Foundation", 32 | "Win32_NetworkManagement_IpHelper", 33 | "Win32_NetworkManagement_Ndis", 34 | "Win32_Networking_WinSock", 35 | ] } 36 | 37 | [build-dependencies] 38 | cfg_aliases = { version = "0.2", default-features = false } 39 | mozbuild = { version = "0.1", default-features = false, optional = true } 40 | bindgen = { version = "0.69", default-features = false, features = ["runtime"] } 41 | 42 | [features] 43 | gecko = ["dep:mozbuild"] 44 | 45 | [lints.rust] 46 | absolute_paths_not_starting_with_crate = "warn" 47 | # TODO: Re-activate with MSRV 1.82.0. See 48 | # https://github.com/mozilla/mtu/pull/129 for details. 49 | # ambiguous_negative_literals = "warn" 50 | explicit_outlives_requirements = "warn" 51 | macro_use_extern_crate = "warn" 52 | missing_abi = "warn" 53 | non_ascii_idents = "warn" 54 | # TODO: Re-activate with MSRV 1.82.0. See 55 | # https://github.com/mozilla/mtu/pull/129 for details. 56 | # redundant_imports = "warn" 57 | redundant_lifetimes = "warn" 58 | trivial_numeric_casts = "warn" 59 | unit_bindings = "warn" 60 | unused_import_braces = "warn" 61 | unused_lifetimes = "warn" 62 | unused_macro_rules = "warn" 63 | unused_qualifications = "warn" 64 | 65 | [lints.clippy] 66 | cargo = { level = "warn", priority = -1 } 67 | nursery = { level = "warn", priority = -1 } 68 | pedantic = { level = "warn", priority = -1 } 69 | allow_attributes = "warn" 70 | allow_attributes_without_reason = "warn" 71 | cfg_not_test = "warn" 72 | clone_on_ref_ptr = "warn" 73 | create_dir = "warn" 74 | dbg_macro = "warn" 75 | empty_drop = "warn" 76 | empty_enum_variants_with_brackets = "warn" 77 | filetype_is_file = "warn" 78 | float_cmp_const = "warn" 79 | fn_to_numeric_cast_any = "warn" 80 | get_unwrap = "warn" 81 | if_then_some_else_none = "warn" 82 | infinite_loop = "warn" 83 | large_include_file = "warn" 84 | let_underscore_must_use = "warn" 85 | let_underscore_untyped = "warn" 86 | literal_string_with_formatting_args = "allow" # FIXME: Re-enable "warn" when MSRV is > 1.87. See https://github.com/rust-lang/rust-clippy/pull/13953#issuecomment-2676336899 87 | lossy_float_literal = "warn" 88 | mem_forget = "warn" 89 | mixed_read_write_in_expression = "warn" 90 | multiple_crate_versions = "allow" 91 | multiple_inherent_impl = "warn" 92 | mutex_atomic = "warn" 93 | mutex_integer = "warn" 94 | needless_raw_strings = "warn" 95 | pathbuf_init_then_push = "warn" 96 | pub_without_shorthand = "warn" 97 | rc_buffer = "warn" 98 | rc_mutex = "warn" 99 | redundant_type_annotations = "warn" 100 | ref_patterns = "warn" 101 | renamed_function_params = "warn" 102 | rest_pat_in_fully_bound_structs = "warn" 103 | self_named_module_files = "warn" 104 | semicolon_inside_block = "warn" 105 | string_lit_chars_any = "warn" 106 | string_to_string = "warn" 107 | suspicious_xor_used_as_pow = "warn" 108 | try_err = "warn" 109 | unnecessary_safety_comment = "warn" 110 | unnecessary_safety_doc = "warn" 111 | unnecessary_self_imports = "warn" 112 | unneeded_field_pattern = "warn" 113 | unused_result_ok = "warn" 114 | unused_trait_names = "warn" 115 | unwrap_in_result = "warn" 116 | unwrap_used = "warn" 117 | verbose_file_reads = "warn" 118 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Mozilla Foundation 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Coverage Status](https://codecov.io/gh/mozilla/mtu/branch/main/graph/badge.svg)](https://codecov.io/gh/mozilla/mtu) 2 | [![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/mozilla/mtu.svg)](https://isitmaintained.com/project/mozilla/mtu "Average time to resolve an issue") 3 | [![Percentage of issues still open](https://isitmaintained.com/badge/open/mozilla/mtu.svg)](https://isitmaintained.com/project/mozilla/mtu "Percentage of issues still open") 4 | ![Maintenance](https://img.shields.io/badge/maintenance-activly--developed-brightgreen.svg) 5 | 6 | # mtu 7 | 8 | A crate to return the name and maximum transmission unit (MTU) of the local network interface 9 | towards a given destination `SocketAddr`, optionally from a given local `SocketAddr`. 10 | 11 | ## Usage 12 | 13 | This crate exports a single function `interface_and_mtu` that returns the name and 14 | [maximum transmission unit (MTU)](https://en.wikipedia.org/wiki/Maximum_transmission_unit) 15 | of the outgoing network interface towards a remote destination identified by an `IpAddr`. 16 | 17 | ## Example 18 | 19 | ```rust 20 | let destination = IpAddr::V4(Ipv4Addr::LOCALHOST); 21 | let (name, mtu): (String, usize) = mtu::interface_and_mtu(destination).unwrap(); 22 | println!("MTU towards {destination} is {mtu} on {name}"); 23 | ``` 24 | 25 | ## Supported Platforms 26 | 27 | * Linux 28 | * Android 29 | * macOS 30 | * Windows 31 | * FreeBSD 32 | * NetBSD 33 | * OpenBSD 34 | * Solaris 35 | 36 | ## Notes 37 | 38 | The returned MTU may exceed the maximum IP packet size of 65,535 bytes on some platforms for 39 | some remote destinations. (For example, loopback destinations on Windows.) 40 | 41 | The returned interface name is obtained from the operating system. 42 | 43 | ## Contributing 44 | 45 | We're happy to receive PRs that improve this crate. Please take a look at our [community 46 | guidelines](CODE_OF_CONDUCT.md) beforehand. 47 | 48 | License: MIT OR Apache-2.0 49 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This document describes how security vulnerabilities in this project should be reported. 4 | 5 | ## Supported Versions 6 | 7 | Support for `mtu` is based on the Firefox version in which it has landed. 8 | Versions of `mtu` in [current versions of Firefox](https://whattrainisitnow.com/calendar/) are actively supported. 9 | 10 | The version of `mtu` that is active can be found in the Firefox repositories: 11 | 12 | - [release](https://hg.mozilla.org/mozilla-unified/file/release/third_party/rust/mtu-transport/Cargo.toml), 13 | - [beta](https://hg.mozilla.org/mozilla-unified/file/beta/third_party/rust/mtu-transport/Cargo.toml), and 14 | - [trunk/central](https://hg.mozilla.org/mozilla-unified/file/central/third_party/rust/mtu-transport/Cargo.toml), 15 | - [ESR 115](https://hg.mozilla.org/mozilla-unified/file/esr115/third_party/rust/mtu-transport/Cargo.toml). 16 | 17 | The listed version in these files corresponds to [tags](https://github.com/mozilla/mtu/tags) on this repository. 18 | Releases do not always correspond to a branch. 19 | 20 | We welcome reports of security vulnerabilities in any of these released versions or the latest code on the `main` branch. 21 | 22 | ## Reporting a Vulnerability 23 | 24 | To report a security problem with `mtu`, create a bug in Mozilla's Bugzilla instance in the [Core :: Networking](https://bugzilla.mozilla.org/enter_bug.cgi?product=Core&component=Networking) component. 25 | 26 | **IMPORTANT: For security issues, please make sure that you check the box labelled "Many users could be harmed by this security problem".** 27 | We advise that you check this option for anything that involves anything security-relevant, including memory safety, crashes, race conditions, and handling of confidential information. 28 | 29 | Review Mozilla's [guides on bug reporting](https://bugzilla.mozilla.org/page.cgi?id=bug-writing.html) before you open a bug. 30 | 31 | Mozilla operates a [bug bounty program](https://www.mozilla.org/en-US/security/bug-bounty/), for which this project is eligible. 32 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 or the MIT license 3 | // , at your 4 | // option. This file may not be copied, modified, or distributed 5 | // except according to those terms. 6 | 7 | #![expect(clippy::unwrap_used, reason = "OK in build scripts.")] 8 | 9 | use std::env; 10 | 11 | const BINDINGS: &str = "bindings.rs"; 12 | 13 | #[cfg(feature = "gecko")] 14 | fn clang_args() -> Vec { 15 | use mozbuild::{config::BINDGEN_SYSTEM_FLAGS, TOPOBJDIR}; 16 | 17 | let mut flags: Vec = BINDGEN_SYSTEM_FLAGS.iter().map(|s| s.to_string()).collect(); 18 | 19 | flags.push(String::from("-include")); 20 | flags.push( 21 | TOPOBJDIR 22 | .join("dist") 23 | .join("include") 24 | .join("mozilla-config.h") 25 | .to_str() 26 | .unwrap() 27 | .to_string(), 28 | ); 29 | flags 30 | } 31 | 32 | #[cfg(not(feature = "gecko"))] 33 | const fn clang_args() -> Vec { 34 | Vec::new() 35 | } 36 | 37 | fn bindgen() { 38 | let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS was not set"); 39 | 40 | // Platforms currently not supported. 41 | // 42 | // See . 43 | if matches!(target_os.as_str(), "ios" | "tvos" | "visionos") { 44 | return; 45 | } 46 | 47 | if target_os == "windows" { 48 | return; 49 | } 50 | 51 | let bindings = if matches!(target_os.as_str(), "linux" | "android") { 52 | bindgen::Builder::default() 53 | .header_contents("rtnetlink.h", "#include ") 54 | // Only generate bindings for the following types 55 | .allowlist_type("rtattr|rtmsg|ifinfomsg|nlmsghdr") 56 | } else { 57 | bindgen::Builder::default() 58 | .header_contents( 59 | "route.h", 60 | "#include \n#include \n#include \n#include ", 61 | ) 62 | // Only generate bindings for the following types and items 63 | .allowlist_type("rt_msghdr|rt_metrics|if_data") 64 | .allowlist_item("RTAX_MAX|RTM_GET|RTM_VERSION|RTA_DST|RTA_IFP") 65 | }; 66 | 67 | let bindings = bindings 68 | .clang_args(clang_args()) 69 | // Tell cargo to invalidate the built crate whenever any of the 70 | // included header files changed. 71 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 72 | // Constants should be generated as &CStr instead of &[u8]. 73 | .generate_cstr(true) 74 | // Always emit explicit padding fields. 75 | .explicit_padding(true) 76 | // Default trait should be derived when possible 77 | .derive_default(true) 78 | // Finish the builder and generate the bindings. 79 | .generate() 80 | // Unwrap the Result and panic on failure. 81 | .expect("Unable to generate bindings"); 82 | 83 | // Write the bindings to the $OUT_DIR/$BINDINGS file. 84 | let out_path = std::path::PathBuf::from(env::var("OUT_DIR").unwrap()).join(BINDINGS); 85 | bindings 86 | .write_to_file(out_path.clone()) 87 | .expect("Couldn't write bindings!"); 88 | println!("cargo:rustc-env=BINDINGS={}", out_path.display()); 89 | } 90 | 91 | fn main() { 92 | // Setup cfg aliases 93 | cfg_aliases::cfg_aliases! { 94 | bsd: { 95 | any( 96 | target_os = "freebsd", 97 | target_os = "openbsd", 98 | target_os = "netbsd", 99 | target_os = "solaris" 100 | ) 101 | } 102 | } 103 | 104 | bindgen(); 105 | } 106 | -------------------------------------------------------------------------------- /src/bsd.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 or the MIT license 3 | // , at your 4 | // option. This file may not be copied, modified, or distributed 5 | // except according to those terms. 6 | 7 | use std::{ 8 | ffi::CStr, 9 | io::{Error, ErrorKind, Read as _, Result, Write as _}, 10 | marker::PhantomData, 11 | net::IpAddr, 12 | num::TryFromIntError, 13 | ops::Deref, 14 | ptr, slice, 15 | }; 16 | 17 | use libc::{ 18 | freeifaddrs, getifaddrs, getpid, if_indextoname, ifaddrs, in6_addr, in_addr, sockaddr, 19 | sockaddr_dl, sockaddr_in, sockaddr_in6, sockaddr_storage, AF_UNSPEC, PF_ROUTE, 20 | }; 21 | use static_assertions::{const_assert, const_assert_eq}; 22 | 23 | #[allow( 24 | clippy::allow_attributes, 25 | non_camel_case_types, 26 | non_snake_case, 27 | clippy::struct_field_names, 28 | clippy::too_many_lines, 29 | clippy::cognitive_complexity, 30 | dead_code, // RTA_IFP is only used on NetBSD and Solaris 31 | reason = "Bindgen-generated code" 32 | )] 33 | mod bindings { 34 | include!(env!("BINDINGS")); 35 | } 36 | 37 | #[cfg(any(target_os = "netbsd", target_os = "solaris"))] 38 | use crate::bsd::bindings::RTA_IFP; 39 | use crate::{ 40 | aligned_by, 41 | bsd::bindings::{if_data, rt_msghdr, RTAX_MAX, RTA_DST}, 42 | default_err, 43 | routesocket::RouteSocket, 44 | unlikely_err, 45 | }; 46 | 47 | #[cfg(target_os = "macos")] 48 | const ALIGN: usize = size_of::(); 49 | 50 | #[cfg(bsd)] 51 | // See https://github.com/freebsd/freebsd-src/blob/524a425d30fce3d5e47614db796046830b1f6a83/sys/net/route.h#L362-L371 52 | // See https://github.com/NetBSD/src/blob/4b50954e98313db58d189dd87b4541929efccb09/sys/net/route.h#L329-L331 53 | // See https://github.com/Arquivotheca/Solaris-8/blob/2ad1d32f9eeed787c5adb07eb32544276e2e2444/osnet_volume/usr/src/cmd/cmd-inet/usr.sbin/route.c#L238-L239 54 | const ALIGN: usize = size_of::(); 55 | 56 | #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "openbsd"))] 57 | asserted_const_with_type!(RTM_ADDRS, i32, RTA_DST, u32); 58 | 59 | #[cfg(any(target_os = "netbsd", target_os = "solaris"))] 60 | asserted_const_with_type!(RTM_ADDRS, i32, RTA_DST | RTA_IFP, u32); 61 | 62 | #[cfg(not(target_os = "solaris"))] 63 | type AddressFamily = u8; 64 | 65 | #[cfg(target_os = "solaris")] 66 | type AddressFamily = u16; 67 | 68 | asserted_const_with_type!(AF_INET, AddressFamily, libc::AF_INET, i32); 69 | asserted_const_with_type!(AF_INET6, AddressFamily, libc::AF_INET6, i32); 70 | asserted_const_with_type!(AF_LINK, AddressFamily, libc::AF_LINK, i32); 71 | asserted_const_with_type!(RTM_VERSION, u8, bindings::RTM_VERSION, u32); 72 | asserted_const_with_type!(RTM_GET, u8, bindings::RTM_GET, u32); 73 | 74 | const_assert!(size_of::() + ALIGN <= u8::MAX as usize); 75 | const_assert!(size_of::() + ALIGN <= u8::MAX as usize); 76 | const_assert!(size_of::() <= u8::MAX as usize); 77 | 78 | struct IfAddrs(*mut ifaddrs); 79 | 80 | impl Default for IfAddrs { 81 | fn default() -> Self { 82 | Self(ptr::null_mut()) 83 | } 84 | } 85 | 86 | impl IfAddrs { 87 | fn new() -> Result { 88 | let mut ifap = Self::default(); 89 | // getifaddrs allocates memory for the linked list of interfaces that is freed by 90 | // `IfAddrs::drop`. 91 | if unsafe { getifaddrs(ptr::from_mut(&mut ifap.0)) } != 0 { 92 | return Err(Error::last_os_error()); 93 | } 94 | Ok(ifap) 95 | } 96 | 97 | const fn iter(&self) -> IfAddrPtr { 98 | IfAddrPtr { 99 | ptr: self.0, 100 | _ref: PhantomData, 101 | } 102 | } 103 | } 104 | 105 | impl Drop for IfAddrs { 106 | fn drop(&mut self) { 107 | if !self.0.is_null() { 108 | // Free the memory allocated by `getifaddrs`. 109 | unsafe { 110 | freeifaddrs(self.0); 111 | } 112 | } 113 | } 114 | } 115 | 116 | struct IfAddrPtr<'a> { 117 | ptr: *mut ifaddrs, 118 | _ref: PhantomData<&'a ifaddrs>, 119 | } 120 | 121 | impl IfAddrPtr<'_> { 122 | fn addr(&self) -> sockaddr { 123 | unsafe { *self.ifa_addr } 124 | } 125 | 126 | fn name(&self) -> String { 127 | unsafe { CStr::from_ptr(self.ifa_name).to_string_lossy().to_string() } 128 | } 129 | 130 | fn data(&self) -> Option { 131 | if self.ifa_data.is_null() { 132 | None 133 | } else { 134 | Some(unsafe { self.ifa_data.cast::().read() }) 135 | } 136 | } 137 | } 138 | 139 | impl Deref for IfAddrPtr<'_> { 140 | type Target = ifaddrs; 141 | 142 | fn deref(&self) -> &Self::Target { 143 | unsafe { self.ptr.as_ref().expect("can deref") } 144 | } 145 | } 146 | 147 | impl Iterator for IfAddrPtr<'_> { 148 | type Item = Self; 149 | 150 | fn next(&mut self) -> Option { 151 | ptr::NonNull::new(self.ptr).map(|p| { 152 | self.ptr = unsafe { p.as_ref().ifa_next }; 153 | IfAddrPtr { 154 | ptr: p.as_ptr(), 155 | _ref: PhantomData, 156 | } 157 | }) 158 | } 159 | } 160 | 161 | fn if_name_mtu(idx: u32) -> Result<(String, Option)> { 162 | let mut name = [0; libc::IF_NAMESIZE]; 163 | // if_indextoname writes into the provided buffer. 164 | if unsafe { if_indextoname(idx, name.as_mut_ptr()).is_null() } { 165 | return Err(Error::last_os_error()); 166 | } 167 | // Convert to Rust string. 168 | let name = unsafe { 169 | CStr::from_ptr(name.as_ptr()) 170 | .to_str() 171 | .map_err(Error::other)? 172 | }; 173 | let mtu = IfAddrs::new()? 174 | .iter() 175 | .find(|ifa| ifa.addr().sa_family == AF_LINK && ifa.name() == name) 176 | .and_then(|ifa| ifa.data()) 177 | .and_then(|ifa_data| usize::try_from(ifa_data.ifi_mtu).ok()); 178 | Ok((name.to_string(), mtu)) 179 | } 180 | 181 | #[repr(C)] 182 | union SockaddrStorage { 183 | sin: sockaddr_in, 184 | sin6: sockaddr_in6, 185 | } 186 | 187 | fn sockaddr_len(af: AddressFamily) -> Result { 188 | let sa_len = match af { 189 | AF_INET => size_of::(), 190 | AF_INET6 => size_of::(), 191 | _ => { 192 | return Err(Error::new( 193 | ErrorKind::InvalidInput, 194 | format!("Unsupported address family {af:?}"), 195 | )) 196 | } 197 | }; 198 | Ok(aligned_by(sa_len, ALIGN)) 199 | } 200 | 201 | impl From for SockaddrStorage { 202 | fn from(ip: IpAddr) -> Self { 203 | match ip { 204 | IpAddr::V4(ip) => SockaddrStorage { 205 | sin: sockaddr_in { 206 | #[cfg(not(target_os = "solaris"))] 207 | #[expect( 208 | clippy::cast_possible_truncation, 209 | reason = "`sockaddr_in` len is <= u8::MAX per `const_assert!` above." 210 | )] 211 | sin_len: size_of::() as u8, 212 | sin_family: AF_INET, 213 | sin_addr: in_addr { 214 | s_addr: u32::from_ne_bytes(ip.octets()), 215 | }, 216 | sin_port: 0, 217 | sin_zero: [0; 8], 218 | }, 219 | }, 220 | IpAddr::V6(ip) => SockaddrStorage { 221 | sin6: sockaddr_in6 { 222 | #[cfg(not(target_os = "solaris"))] 223 | #[expect( 224 | clippy::cast_possible_truncation, 225 | reason = "`sockaddr_in6` len is <= u8::MAX per `const_assert!` above." 226 | )] 227 | sin6_len: size_of::() as u8, 228 | sin6_family: AF_INET6, 229 | sin6_addr: in6_addr { 230 | s6_addr: ip.octets(), 231 | }, 232 | sin6_port: 0, 233 | sin6_flowinfo: 0, 234 | sin6_scope_id: 0, 235 | #[cfg(target_os = "solaris")] 236 | __sin6_src_id: 0, 237 | }, 238 | }, 239 | } 240 | } 241 | } 242 | 243 | #[repr(C)] 244 | struct RouteMessage { 245 | rtm: rt_msghdr, 246 | sa: SockaddrStorage, 247 | } 248 | 249 | #[cfg(target_os = "openbsd")] 250 | fn getrtable() -> u16 { 251 | extern "C" { 252 | fn getrtable() -> libc::c_int; 253 | } 254 | #[expect( 255 | clippy::cast_possible_truncation, 256 | clippy::cast_sign_loss, 257 | reason = "`getrtable` returns a `c_int`." 258 | )] 259 | unsafe { 260 | getrtable() as u16 261 | } 262 | } 263 | 264 | impl RouteMessage { 265 | fn new(remote: IpAddr, seq: i32) -> Result { 266 | let sa = SockaddrStorage::from(remote); 267 | let sa_len = sockaddr_len(match remote { 268 | IpAddr::V4(_) => AF_INET, 269 | IpAddr::V6(_) => AF_INET6, 270 | })?; 271 | Ok(Self { 272 | rtm: rt_msghdr { 273 | #[expect( 274 | clippy::cast_possible_truncation, 275 | reason = "`rt_msghdr` len + `ALIGN` is <= u8::MAX per `const_assert!` above." 276 | )] 277 | rtm_msglen: (size_of::() + sa_len) as u16, 278 | rtm_version: RTM_VERSION, 279 | rtm_type: RTM_GET, 280 | rtm_seq: seq, 281 | rtm_addrs: RTM_ADDRS, 282 | #[cfg(target_os = "openbsd")] 283 | rtm_tableid: getrtable(), 284 | ..Default::default() 285 | }, 286 | sa, 287 | }) 288 | } 289 | 290 | const fn version(&self) -> u8 { 291 | self.rtm.rtm_version 292 | } 293 | 294 | const fn kind(&self) -> u8 { 295 | self.rtm.rtm_type 296 | } 297 | 298 | const fn len(&self) -> usize { 299 | self.rtm.rtm_msglen as usize 300 | } 301 | } 302 | 303 | impl From<&RouteMessage> for &[u8] { 304 | fn from(value: &RouteMessage) -> Self { 305 | debug_assert!(value.len() >= size_of::()); 306 | unsafe { slice::from_raw_parts(ptr::from_ref(value).cast(), value.len()) } 307 | } 308 | } 309 | 310 | impl From<&[u8]> for rt_msghdr { 311 | fn from(value: &[u8]) -> Self { 312 | debug_assert!(value.len() >= size_of::()); 313 | unsafe { ptr::read_unaligned(value.as_ptr().cast()) } 314 | } 315 | } 316 | 317 | fn if_index_mtu(remote: IpAddr) -> Result<(u16, Option)> { 318 | // Open route socket. 319 | let mut fd = RouteSocket::new(PF_ROUTE, AF_UNSPEC)?; 320 | 321 | // Send route message. 322 | let query_seq = RouteSocket::new_seq(); 323 | let query = RouteMessage::new(remote, query_seq)?; 324 | let query_version = query.version(); 325 | let query_type = query.kind(); 326 | fd.write_all((&query).into())?; 327 | 328 | // Read route messages. 329 | let pid = unsafe { getpid() }; 330 | loop { 331 | let mut buf = vec![ 332 | 0u8; 333 | size_of::() + 334 | // There will never be `RTAX_MAX` sockaddrs attached, but it's a safe upper bound. 335 | (RTAX_MAX as usize * size_of::()) 336 | ]; 337 | let len = fd.read(&mut buf[..])?; 338 | if len < size_of::() { 339 | return Err(default_err()); 340 | } 341 | let (reply, mut sa) = buf.split_at(size_of::()); 342 | let reply: rt_msghdr = reply.into(); 343 | if !(reply.rtm_version == query_version 344 | && reply.rtm_pid == pid 345 | && reply.rtm_seq == query_seq) 346 | { 347 | continue; 348 | } 349 | if reply.rtm_type != query_type { 350 | return Err(default_err()); 351 | } 352 | 353 | // This is a reply to our query. 354 | // This is the reply we are looking for. 355 | // Some BSDs let us get the interface index and MTU directly from the reply. 356 | let mtu = (reply.rtm_rmx.rmx_mtu != 0) 357 | .then(|| usize::try_from(reply.rtm_rmx.rmx_mtu)) 358 | .transpose() 359 | .map_err(|e: TryFromIntError| unlikely_err(e.to_string()))?; 360 | if reply.rtm_index != 0 { 361 | // Some BSDs return the interface index directly. 362 | return Ok((reply.rtm_index, mtu)); 363 | } 364 | // For others, we need to extract it from the sockaddrs. 365 | for i in 0..RTAX_MAX { 366 | if (reply.rtm_addrs & (1 << i)) == 0 { 367 | continue; 368 | } 369 | let saddr = unsafe { ptr::read_unaligned(sa.as_ptr().cast::()) }; 370 | if saddr.sa_family != AF_LINK { 371 | (_, sa) = sa.split_at(sockaddr_len(saddr.sa_family)?); 372 | continue; 373 | } 374 | let sdl = unsafe { ptr::read_unaligned(sa.as_ptr().cast::()) }; 375 | return Ok((sdl.sdl_index, mtu)); 376 | } 377 | } 378 | } 379 | 380 | pub fn interface_and_mtu_impl(remote: IpAddr) -> Result<(String, usize)> { 381 | let (if_index, mtu1) = if_index_mtu(remote)?; 382 | let (if_name, mtu2) = if_name_mtu(if_index.into())?; 383 | Ok((if_name, mtu1.or(mtu2).ok_or_else(default_err)?)) 384 | } 385 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 or the MIT license 3 | // , at your 4 | // option. This file may not be copied, modified, or distributed 5 | // except according to those terms. 6 | 7 | //! A crate to return the name and maximum transmission unit (MTU) of the local network interface 8 | //! towards a given destination `SocketAddr`, optionally from a given local `SocketAddr`. 9 | //! 10 | //! # Usage 11 | //! 12 | //! This crate exports a single function `interface_and_mtu` that returns the name and 13 | //! [maximum transmission unit (MTU)](https://en.wikipedia.org/wiki/Maximum_transmission_unit) 14 | //! of the outgoing network interface towards a remote destination identified by an `IpAddr`. 15 | //! 16 | //! # Example 17 | //! 18 | //! ``` 19 | //! # use std::net::{IpAddr, Ipv4Addr}; 20 | //! let destination = IpAddr::V4(Ipv4Addr::LOCALHOST); 21 | //! let (name, mtu): (String, usize) = mtu::interface_and_mtu(destination).unwrap(); 22 | //! println!("MTU towards {destination} is {mtu} on {name}"); 23 | //! ``` 24 | //! 25 | //! # Supported Platforms 26 | //! 27 | //! * Linux 28 | //! * Android 29 | //! * macOS 30 | //! * Windows 31 | //! * FreeBSD 32 | //! * NetBSD 33 | //! * OpenBSD 34 | //! * Solaris 35 | //! 36 | //! # Notes 37 | //! 38 | //! The returned MTU may exceed the maximum IP packet size of 65,535 bytes on some platforms for 39 | //! some remote destinations. (For example, loopback destinations on Windows.) 40 | //! 41 | //! The returned interface name is obtained from the operating system. 42 | //! 43 | //! # Contributing 44 | //! 45 | //! We're happy to receive PRs that improve this crate. Please take a look at our [community 46 | //! guidelines](CODE_OF_CONDUCT.md) beforehand. 47 | 48 | use std::{ 49 | io::{Error, ErrorKind, Result}, 50 | net::IpAddr, 51 | }; 52 | 53 | #[cfg(not(target_os = "windows"))] 54 | macro_rules! asserted_const_with_type { 55 | ($name:ident, $t1:ty, $e:expr, $t2:ty) => { 56 | #[allow( 57 | clippy::allow_attributes, 58 | clippy::cast_possible_truncation, 59 | clippy::cast_possible_wrap, 60 | reason = "Guarded by the following `const_assert_eq!`." 61 | )] 62 | const $name: $t1 = $e as $t1; 63 | const_assert_eq!($name as $t2, $e); 64 | }; 65 | } 66 | 67 | #[cfg(any(target_os = "macos", bsd))] 68 | mod bsd; 69 | 70 | #[cfg(any(target_os = "linux", target_os = "android"))] 71 | mod linux; 72 | 73 | #[cfg(target_os = "windows")] 74 | mod windows; 75 | 76 | #[cfg(not(target_os = "windows"))] 77 | mod routesocket; 78 | 79 | #[cfg(any(target_os = "macos", bsd))] 80 | use bsd::interface_and_mtu_impl; 81 | #[cfg(any(target_os = "linux", target_os = "android"))] 82 | use linux::interface_and_mtu_impl; 83 | #[cfg(target_os = "windows")] 84 | use windows::interface_and_mtu_impl; 85 | 86 | /// Prepare a default error. 87 | fn default_err() -> Error { 88 | Error::new(ErrorKind::NotFound, "Local interface MTU not found") 89 | } 90 | 91 | /// Prepare an error for cases that "should never happen". 92 | #[cfg(not(target_os = "windows"))] 93 | fn unlikely_err(msg: String) -> Error { 94 | debug_assert!(false, "{msg}"); 95 | Error::other(msg) 96 | } 97 | 98 | /// Align `size` to the next multiple of `align` (which needs to be a power of two). 99 | #[cfg(not(target_os = "windows"))] 100 | const fn aligned_by(size: usize, align: usize) -> usize { 101 | if size == 0 { 102 | align 103 | } else { 104 | 1 + ((size - 1) | (align - 1)) 105 | } 106 | } 107 | 108 | // Platforms currently not supported. 109 | // 110 | // See . 111 | #[cfg(any(target_os = "ios", target_os = "tvos", target_os = "visionos"))] 112 | pub fn interface_and_mtu_impl(remote: IpAddr) -> Result<(String, usize)> { 113 | return Err(default_err()); 114 | } 115 | 116 | /// Return the name and maximum transmission unit (MTU) of the outgoing network interface towards a 117 | /// remote destination identified by an [`IpAddr`], 118 | /// 119 | /// The returned MTU may exceed the maximum IP packet size of 65,535 bytes on some platforms for 120 | /// some remote destinations. (For example, loopback destinations on Windows.) 121 | /// 122 | /// The returned interface name is obtained from the operating system. 123 | /// 124 | /// # Errors 125 | /// 126 | /// This function returns an error if the local interface MTU cannot be determined. 127 | pub fn interface_and_mtu(remote: IpAddr) -> Result<(String, usize)> { 128 | interface_and_mtu_impl(remote) 129 | } 130 | 131 | #[cfg(test)] 132 | mod test { 133 | use std::{ 134 | env, 135 | net::{IpAddr, Ipv4Addr, Ipv6Addr}, 136 | }; 137 | 138 | use crate::interface_and_mtu; 139 | 140 | #[derive(Debug)] 141 | struct NameMtu<'a>(Option<&'a str>, usize); 142 | 143 | impl PartialEq> for (String, usize) { 144 | fn eq(&self, other: &NameMtu<'_>) -> bool { 145 | other.0.map_or(true, |name| name == self.0) && other.1 == self.1 146 | } 147 | } 148 | 149 | const LOOPBACK: [NameMtu; 2] = // [IPv4, IPv6] 150 | if cfg!(any(target_os = "macos", target_os = "freebsd")) { 151 | [NameMtu(Some("lo0"), 16_384), NameMtu(Some("lo0"), 16_384)] 152 | } else if cfg!(any(target_os = "linux", target_os = "android")) { 153 | [NameMtu(Some("lo"), 65_536), NameMtu(Some("lo"), 65_536)] 154 | } else if cfg!(target_os = "windows") { 155 | [ 156 | NameMtu(Some("loopback_0"), 4_294_967_295), 157 | NameMtu(Some("loopback_0"), 4_294_967_295), 158 | ] 159 | } else if cfg!(target_os = "openbsd") { 160 | [NameMtu(Some("lo0"), 32_768), NameMtu(Some("lo0"), 32_768)] 161 | } else if cfg!(target_os = "netbsd") { 162 | [NameMtu(Some("lo0"), 33_624), NameMtu(Some("lo0"), 33_624)] 163 | } else if cfg!(target_os = "solaris") { 164 | // Note: Different loopback MTUs for IPv4 and IPv6?! 165 | [NameMtu(Some("lo0"), 8_232), NameMtu(Some("lo0"), 8_252)] 166 | } else { 167 | unreachable!(); 168 | }; 169 | 170 | // Non-loopback interface names are unpredictable, so we only check the MTU. 171 | const INET: NameMtu = NameMtu( 172 | None, 173 | if cfg!(target_os = "android") { 174 | 1_440 // At least inside the Android emulator we use in CI. 175 | } else { 176 | 1_500 177 | }, 178 | ); 179 | 180 | #[test] 181 | fn loopback_v4() { 182 | assert_eq!( 183 | interface_and_mtu(IpAddr::V4(Ipv4Addr::LOCALHOST)).unwrap(), 184 | LOOPBACK[0] 185 | ); 186 | } 187 | 188 | #[test] 189 | fn loopback_v6() { 190 | assert_eq!( 191 | interface_and_mtu(IpAddr::V6(Ipv6Addr::LOCALHOST)).unwrap(), 192 | LOOPBACK[1] 193 | ); 194 | } 195 | 196 | #[test] 197 | fn inet_v4() { 198 | assert_eq!( 199 | interface_and_mtu(IpAddr::V4(Ipv4Addr::new( 200 | 104, 16, 132, 229 // cloudflare.com 201 | ))) 202 | .unwrap(), 203 | INET 204 | ); 205 | } 206 | 207 | #[test] 208 | fn inet_v6() { 209 | match interface_and_mtu(IpAddr::V6(Ipv6Addr::new( 210 | 0x2606, 0x4700, 0, 0, 0, 0, 0x6810, 0x84e5, // cloudflare.com 211 | ))) { 212 | Ok(res) => assert_eq!(res, INET), 213 | // The GitHub CI environment does not have IPv6 connectivity. 214 | Err(_) => assert!(env::var("GITHUB_ACTIONS").is_ok()), 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/linux.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 or the MIT license 3 | // , at your 4 | // option. This file may not be copied, modified, or distributed 5 | // except according to those terms. 6 | 7 | use std::{ 8 | ffi::CStr, 9 | io::{Error, Read as _, Result, Write as _}, 10 | net::IpAddr, 11 | num::TryFromIntError, 12 | ptr, slice, 13 | }; 14 | 15 | use libc::{ 16 | c_int, AF_NETLINK, ARPHRD_NONE, IFLA_IFNAME, IFLA_MTU, NETLINK_ROUTE, RTA_DST, RTA_OIF, 17 | RTM_GETLINK, RTM_GETROUTE, RTM_NEWLINK, RTM_NEWROUTE, RTN_UNICAST, RT_SCOPE_UNIVERSE, 18 | RT_TABLE_MAIN, 19 | }; 20 | use static_assertions::{const_assert, const_assert_eq}; 21 | 22 | use crate::{aligned_by, default_err, routesocket::RouteSocket, unlikely_err}; 23 | 24 | #[allow( 25 | clippy::allow_attributes, 26 | clippy::struct_field_names, 27 | non_camel_case_types, 28 | clippy::too_many_lines, 29 | reason = "Bindgen-generated code" 30 | )] 31 | mod bindings { 32 | include!(env!("BINDINGS")); 33 | } 34 | 35 | use bindings::{ifinfomsg, nlmsghdr, rtattr, rtmsg}; 36 | 37 | asserted_const_with_type!(AF_INET, u8, libc::AF_INET, i32); 38 | asserted_const_with_type!(AF_INET6, u8, libc::AF_INET6, i32); 39 | asserted_const_with_type!(AF_UNSPEC, u8, libc::AF_UNSPEC, i32); 40 | asserted_const_with_type!(NLM_F_REQUEST, u16, libc::NLM_F_REQUEST, c_int); 41 | asserted_const_with_type!(NLM_F_ACK, u16, libc::NLM_F_ACK, c_int); 42 | asserted_const_with_type!(NLMSG_ERROR, u16, libc::NLMSG_ERROR, c_int); 43 | 44 | const_assert!(size_of::() <= u8::MAX as usize); 45 | const_assert!(size_of::() <= u8::MAX as usize); 46 | const_assert!(size_of::() <= u8::MAX as usize); 47 | const_assert!(size_of::() <= u8::MAX as usize); 48 | 49 | const NETLINK_BUFFER_SIZE: usize = 8192; // See netlink(7) man page. 50 | 51 | #[repr(C)] 52 | enum AddrBytes { 53 | V4([u8; 4]), 54 | V6([u8; 16]), 55 | } 56 | 57 | impl AddrBytes { 58 | const fn new(ip: IpAddr) -> Self { 59 | match ip { 60 | IpAddr::V4(ip) => Self::V4(ip.octets()), 61 | IpAddr::V6(ip) => Self::V6(ip.octets()), 62 | } 63 | } 64 | 65 | const fn len(&self) -> usize { 66 | match self { 67 | Self::V4(_) => 4, 68 | Self::V6(_) => 16, 69 | } 70 | } 71 | } 72 | 73 | impl From for [u8; 16] { 74 | fn from(addr: AddrBytes) -> Self { 75 | match addr { 76 | AddrBytes::V4(bytes) => { 77 | let mut v6 = [0; 16]; 78 | v6[..4].copy_from_slice(&bytes); 79 | v6 80 | } 81 | AddrBytes::V6(bytes) => bytes, 82 | } 83 | } 84 | } 85 | 86 | #[repr(C)] 87 | #[derive(Default)] 88 | struct IfIndexMsg { 89 | nlmsg: nlmsghdr, 90 | rtm: rtmsg, 91 | rt: rtattr, 92 | addr: [u8; 16], 93 | } 94 | 95 | impl IfIndexMsg { 96 | fn new(remote: IpAddr, nlmsg_seq: u32) -> Self { 97 | let addr = AddrBytes::new(remote); 98 | #[expect( 99 | clippy::cast_possible_truncation, 100 | reason = "Structs lens are <= u8::MAX per `const_assert!`s above; `addr_bytes` is max. 16 for IPv6." 101 | )] 102 | let nlmsg_len = 103 | (size_of::() + size_of::() + size_of::() + addr.len()) as u32; 104 | Self { 105 | nlmsg: nlmsghdr { 106 | nlmsg_len, 107 | nlmsg_type: RTM_GETROUTE, 108 | nlmsg_flags: NLM_F_REQUEST | NLM_F_ACK, 109 | nlmsg_seq, 110 | ..Default::default() 111 | }, 112 | rtm: rtmsg { 113 | rtm_family: match remote { 114 | IpAddr::V4(_) => AF_INET, 115 | IpAddr::V6(_) => AF_INET6, 116 | }, 117 | rtm_dst_len: match remote { 118 | IpAddr::V4(_) => 32, 119 | IpAddr::V6(_) => 128, 120 | }, 121 | rtm_table: RT_TABLE_MAIN, 122 | rtm_scope: RT_SCOPE_UNIVERSE, 123 | rtm_type: RTN_UNICAST, 124 | ..Default::default() 125 | }, 126 | rt: rtattr { 127 | #[expect( 128 | clippy::cast_possible_truncation, 129 | reason = "Structs len is <= u8::MAX per `const_assert!` above; `addr_bytes` is max. 16 for IPv6." 130 | )] 131 | rta_len: (size_of::() + addr.len()) as u16, 132 | rta_type: RTA_DST, 133 | }, 134 | addr: addr.into(), 135 | } 136 | } 137 | 138 | const fn len(&self) -> usize { 139 | let len = self.nlmsg.nlmsg_len as usize; 140 | debug_assert!(len <= size_of::()); 141 | len 142 | } 143 | } 144 | 145 | impl From<&IfIndexMsg> for &[u8] { 146 | fn from(value: &IfIndexMsg) -> Self { 147 | unsafe { slice::from_raw_parts(ptr::from_ref(value).cast(), value.len()) } 148 | } 149 | } 150 | 151 | impl TryFrom<&[u8]> for nlmsghdr { 152 | type Error = Error; 153 | 154 | fn try_from(value: &[u8]) -> Result { 155 | if value.len() < size_of::() { 156 | return Err(default_err()); 157 | } 158 | Ok(unsafe { ptr::read_unaligned(value.as_ptr().cast()) }) 159 | } 160 | } 161 | 162 | fn parse_c_int(buf: &[u8]) -> Result { 163 | let bytes = <&[u8] as TryInto<[u8; size_of::()]>>::try_into(&buf[..size_of::()]) 164 | .map_err(|_| default_err())?; 165 | Ok(c_int::from_ne_bytes(bytes)) 166 | } 167 | 168 | fn read_msg_with_seq(fd: &mut RouteSocket, seq: u32, kind: u16) -> Result<(nlmsghdr, Vec)> { 169 | loop { 170 | let buf = &mut [0u8; NETLINK_BUFFER_SIZE]; 171 | let len = fd.read(buf.as_mut_slice())?; 172 | let mut next = &buf[..len]; 173 | while size_of::() <= next.len() { 174 | let (hdr, mut msg) = next.split_at(size_of::()); 175 | let hdr: nlmsghdr = hdr.try_into()?; 176 | // `msg` has the remainder of this message plus any following messages. 177 | // Strip those it off and assign them to `next`. 178 | debug_assert!(size_of::() <= hdr.nlmsg_len as usize); 179 | (msg, next) = msg.split_at(hdr.nlmsg_len as usize - size_of::()); 180 | 181 | if hdr.nlmsg_seq != seq { 182 | continue; 183 | } 184 | 185 | if hdr.nlmsg_type == NLMSG_ERROR { 186 | // Extract the error code and return it. 187 | let err = parse_c_int(msg)?; 188 | if err != 0 { 189 | return Err(Error::from_raw_os_error(-err)); 190 | } 191 | } else if hdr.nlmsg_type == kind { 192 | // Return the header and the message. 193 | return Ok((hdr, msg.to_vec())); 194 | } 195 | } 196 | } 197 | } 198 | 199 | impl TryFrom<&[u8]> for rtattr { 200 | type Error = Error; 201 | 202 | fn try_from(value: &[u8]) -> Result { 203 | if value.len() < size_of::() { 204 | return Err(default_err()); 205 | } 206 | Ok(unsafe { ptr::read_unaligned(value.as_ptr().cast()) }) 207 | } 208 | } 209 | 210 | struct RtAttr<'a> { 211 | hdr: rtattr, 212 | msg: &'a [u8], 213 | } 214 | 215 | impl<'a> RtAttr<'a> { 216 | fn new(bytes: &'a [u8]) -> Result { 217 | debug_assert!(bytes.len() >= size_of::()); 218 | let (hdr, mut msg) = bytes.split_at(size_of::()); 219 | let hdr: rtattr = hdr.try_into()?; 220 | let aligned_len = aligned_by(hdr.rta_len.into(), 4); 221 | debug_assert!(size_of::() <= aligned_len); 222 | (msg, _) = msg.split_at(aligned_len - size_of::()); 223 | Ok(Self { hdr, msg }) 224 | } 225 | } 226 | 227 | struct RtAttrs<'a>(&'a [u8]); 228 | 229 | impl<'a> Iterator for RtAttrs<'a> { 230 | type Item = RtAttr<'a>; 231 | 232 | fn next(&mut self) -> Option { 233 | if size_of::() <= self.0.len() { 234 | let attr = RtAttr::new(self.0).ok()?; 235 | let aligned_len = aligned_by(attr.hdr.rta_len.into(), 4); 236 | debug_assert!(self.0.len() >= aligned_len); 237 | self.0 = self.0.split_at(aligned_len).1; 238 | Some(attr) 239 | } else { 240 | None 241 | } 242 | } 243 | } 244 | 245 | fn if_index(remote: IpAddr, fd: &mut RouteSocket) -> Result { 246 | // Send RTM_GETROUTE message to get the interface index associated with the destination. 247 | let msg_seq = RouteSocket::new_seq(); 248 | let msg = IfIndexMsg::new(remote, msg_seq); 249 | fd.write_all((&msg).into())?; 250 | 251 | // Receive RTM_GETROUTE response. 252 | let (_hdr, mut buf) = read_msg_with_seq(fd, msg_seq, RTM_NEWROUTE)?; 253 | debug_assert!(size_of::() <= buf.len()); 254 | let buf = buf.split_off(size_of::()); 255 | 256 | // Parse through the attributes to find the interface index. 257 | for attr in RtAttrs(buf.as_slice()).by_ref() { 258 | if attr.hdr.rta_type == RTA_OIF { 259 | // We have our interface index. 260 | return parse_c_int(attr.msg); 261 | } 262 | } 263 | Err(default_err()) 264 | } 265 | 266 | #[repr(C)] 267 | struct IfInfoMsg { 268 | nlmsg: nlmsghdr, 269 | ifim: ifinfomsg, 270 | } 271 | 272 | impl IfInfoMsg { 273 | fn new(if_index: i32, nlmsg_seq: u32) -> Self { 274 | #[expect( 275 | clippy::cast_possible_truncation, 276 | reason = "Structs lens are <= u8::MAX per `const_assert!`s above." 277 | )] 278 | let nlmsg_len = (size_of::() + size_of::()) as u32; 279 | Self { 280 | nlmsg: nlmsghdr { 281 | nlmsg_len, 282 | nlmsg_type: RTM_GETLINK, 283 | nlmsg_flags: NLM_F_REQUEST | NLM_F_ACK, 284 | nlmsg_seq, 285 | ..Default::default() 286 | }, 287 | ifim: ifinfomsg { 288 | ifi_family: AF_UNSPEC, 289 | ifi_type: ARPHRD_NONE, 290 | ifi_index: if_index, 291 | ..Default::default() 292 | }, 293 | } 294 | } 295 | 296 | const fn len(&self) -> usize { 297 | self.nlmsg.nlmsg_len as usize 298 | } 299 | } 300 | 301 | impl From<&IfInfoMsg> for &[u8] { 302 | fn from(value: &IfInfoMsg) -> Self { 303 | debug_assert!(value.len() >= size_of::()); 304 | unsafe { slice::from_raw_parts(ptr::from_ref(value).cast(), value.len()) } 305 | } 306 | } 307 | 308 | fn if_name_mtu(if_index: i32, fd: &mut RouteSocket) -> Result<(String, usize)> { 309 | // Send RTM_GETLINK message to get interface information for the given interface index. 310 | let msg_seq = RouteSocket::new_seq(); 311 | let msg = IfInfoMsg::new(if_index, msg_seq); 312 | fd.write_all((&msg).into())?; 313 | 314 | // Receive RTM_GETLINK response. 315 | let (_hdr, mut buf) = read_msg_with_seq(fd, msg_seq, RTM_NEWLINK)?; 316 | debug_assert!(size_of::() <= buf.len()); 317 | let buf = buf.split_off(size_of::()); 318 | 319 | // Parse through the attributes to find the interface name and MTU. 320 | let mut ifname = None; 321 | let mut mtu = None; 322 | for attr in RtAttrs(buf.as_slice()).by_ref() { 323 | match attr.hdr.rta_type { 324 | IFLA_IFNAME => { 325 | let name = CStr::from_bytes_until_nul(attr.msg).map_err(Error::other)?; 326 | ifname = Some(name.to_str().map_err(Error::other)?.to_string()); 327 | } 328 | IFLA_MTU => { 329 | mtu = Some( 330 | parse_c_int(attr.msg)? 331 | .try_into() 332 | .map_err(|e: TryFromIntError| unlikely_err(e.to_string()))?, 333 | ); 334 | } 335 | _ => (), 336 | } 337 | if let (Some(ifname), Some(mtu)) = (ifname.as_ref(), mtu.as_ref()) { 338 | return Ok((ifname.clone(), *mtu)); 339 | } 340 | } 341 | 342 | Err(default_err()) 343 | } 344 | 345 | pub fn interface_and_mtu_impl(remote: IpAddr) -> Result<(String, usize)> { 346 | // Create a netlink socket. 347 | let mut fd = RouteSocket::new(AF_NETLINK, NETLINK_ROUTE)?; 348 | let if_index = if_index(remote, &mut fd)?; 349 | if_name_mtu(if_index, &mut fd) 350 | } 351 | -------------------------------------------------------------------------------- /src/routesocket.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 or the MIT license 3 | // , at your 4 | // option. This file may not be copied, modified, or distributed 5 | // except according to those terms. 6 | 7 | use std::{ 8 | io::{Error, Read, Result, Write}, 9 | num::TryFromIntError, 10 | os::fd::{AsRawFd, FromRawFd as _, OwnedFd}, 11 | sync::atomic::Ordering, 12 | }; 13 | 14 | use libc::{fsync, read, socket, write, SOCK_RAW}; 15 | 16 | use crate::unlikely_err; 17 | 18 | #[cfg(any(target_os = "linux", target_os = "android"))] 19 | type AtomicRouteSocketSeq = std::sync::atomic::AtomicU32; 20 | #[cfg(any(target_os = "linux", target_os = "android"))] 21 | type RouteSocketSeq = u32; 22 | 23 | #[cfg(not(any(target_os = "linux", target_os = "android")))] 24 | type AtomicRouteSocketSeq = std::sync::atomic::AtomicI32; 25 | #[cfg(not(any(target_os = "linux", target_os = "android")))] 26 | type RouteSocketSeq = i32; 27 | 28 | static SEQ: AtomicRouteSocketSeq = AtomicRouteSocketSeq::new(0); 29 | 30 | pub struct RouteSocket(OwnedFd); 31 | 32 | impl RouteSocket { 33 | pub fn new(domain: libc::c_int, protocol: libc::c_int) -> Result { 34 | let fd = unsafe { socket(domain, SOCK_RAW, protocol) }; 35 | if fd == -1 { 36 | return Err(Error::last_os_error()); 37 | } 38 | Ok(Self(unsafe { OwnedFd::from_raw_fd(fd) })) 39 | } 40 | 41 | pub fn new_seq() -> RouteSocketSeq { 42 | SEQ.fetch_add(1, Ordering::Relaxed) 43 | } 44 | } 45 | 46 | impl AsRawFd for RouteSocket { 47 | fn as_raw_fd(&self) -> i32 { 48 | self.0.as_raw_fd() 49 | } 50 | } 51 | 52 | fn check_result(res: isize) -> Result { 53 | if res == -1 { 54 | Err(Error::last_os_error()) 55 | } else { 56 | Ok(res 57 | .try_into() 58 | .map_err(|e: TryFromIntError| unlikely_err(e.to_string()))?) 59 | } 60 | } 61 | 62 | impl Write for RouteSocket { 63 | fn write(&mut self, buf: &[u8]) -> Result { 64 | let res = unsafe { write(self.as_raw_fd(), buf.as_ptr().cast(), buf.len()) }; 65 | check_result(res) 66 | } 67 | 68 | fn flush(&mut self) -> Result<()> { 69 | let res = unsafe { fsync(self.as_raw_fd()) }; 70 | check_result(res as isize).and(Ok(())) 71 | } 72 | } 73 | 74 | impl Read for RouteSocket { 75 | fn read(&mut self, buf: &mut [u8]) -> Result { 76 | // If we've written a well-formed message into the kernel via `write`, we should be able to 77 | // read a well-formed message back out, and not block. 78 | let res = unsafe { read(self.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) }; 79 | check_result(res) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/windows.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 or the MIT license 3 | // , at your 4 | // option. This file may not be copied, modified, or distributed 5 | // except according to those terms. 6 | 7 | use std::{ 8 | ffi::CStr, 9 | io::{Error, Result}, 10 | net::IpAddr, 11 | ptr, slice, 12 | }; 13 | 14 | use windows::Win32::{ 15 | Foundation::NO_ERROR, 16 | NetworkManagement::{ 17 | IpHelper::{ 18 | if_indextoname, FreeMibTable, GetBestInterfaceEx, GetIpInterfaceTable, 19 | MIB_IPINTERFACE_ROW, MIB_IPINTERFACE_TABLE, 20 | }, 21 | Ndis::IF_MAX_STRING_SIZE, 22 | }, 23 | Networking::WinSock::{ 24 | AF_INET, AF_INET6, IN6_ADDR, IN6_ADDR_0, IN_ADDR, IN_ADDR_0, SOCKADDR, SOCKADDR_IN, 25 | SOCKADDR_IN6, SOCKADDR_INET, 26 | }, 27 | }; 28 | 29 | use crate::default_err; 30 | 31 | struct MibTablePtr(*mut MIB_IPINTERFACE_TABLE); 32 | 33 | impl MibTablePtr { 34 | fn mut_ptr_ptr(&mut self) -> *mut *mut MIB_IPINTERFACE_TABLE { 35 | ptr::from_mut(&mut self.0) 36 | } 37 | } 38 | 39 | impl Default for MibTablePtr { 40 | fn default() -> Self { 41 | Self(ptr::null_mut()) 42 | } 43 | } 44 | 45 | impl Drop for MibTablePtr { 46 | fn drop(&mut self) { 47 | if !self.0.is_null() { 48 | // Free the memory allocated by GetIpInterfaceTable. 49 | unsafe { 50 | FreeMibTable(self.0.cast()); 51 | } 52 | } 53 | } 54 | } 55 | 56 | pub fn interface_and_mtu_impl(remote: IpAddr) -> Result<(String, usize)> { 57 | // Convert remote to Windows SOCKADDR_INET format. The SOCKADDR_INET union contains an IPv4 or 58 | // an IPv6 address. 59 | // 60 | // See https://learn.microsoft.com/en-us/windows/win32/api/ws2ipdef/ns-ws2ipdef-sockaddr_inet 61 | let dst = match remote { 62 | IpAddr::V4(ip) => { 63 | // Initialize the `SOCKADDR_IN` variant of `SOCKADDR_INET` based on `ip`. 64 | SOCKADDR_INET { 65 | Ipv4: SOCKADDR_IN { 66 | sin_family: AF_INET, 67 | sin_addr: IN_ADDR { 68 | S_un: IN_ADDR_0 { 69 | S_addr: u32::to_be(ip.into()), 70 | }, 71 | }, 72 | ..Default::default() 73 | }, 74 | } 75 | } 76 | IpAddr::V6(ip) => { 77 | // Initialize the `SOCKADDR_IN6` variant of `SOCKADDR_INET` based on `ip`. 78 | SOCKADDR_INET { 79 | Ipv6: SOCKADDR_IN6 { 80 | sin6_family: AF_INET6, 81 | sin6_addr: IN6_ADDR { 82 | u: IN6_ADDR_0 { Byte: ip.octets() }, 83 | }, 84 | ..Default::default() 85 | }, 86 | } 87 | } 88 | }; 89 | 90 | // Get the interface index of the best outbound interface towards `dst`. 91 | let mut idx = 0; 92 | let res = unsafe { 93 | // We're now casting `&dst` to a `SOCKADDR` pointer. This is OK based on 94 | // https://learn.microsoft.com/en-us/windows/win32/winsock/sockaddr-2. 95 | // With that, we call `GetBestInterfaceEx` to get the interface index into `idx`. 96 | // See https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getbestinterfaceex 97 | GetBestInterfaceEx( 98 | ptr::from_ref(&dst).cast::(), 99 | ptr::from_mut(&mut idx), 100 | ) 101 | }; 102 | if res != 0 { 103 | return Err(Error::last_os_error()); 104 | } 105 | 106 | // Get a list of all interfaces with associated metadata. 107 | let mut if_table = MibTablePtr::default(); 108 | // GetIpInterfaceTable allocates memory, which MibTablePtr::drop will free. 109 | let family = if remote.is_ipv4() { AF_INET } else { AF_INET6 }; 110 | if unsafe { GetIpInterfaceTable(family, if_table.mut_ptr_ptr()) } != NO_ERROR { 111 | return Err(Error::last_os_error()); 112 | } 113 | // Make a slice 114 | let ifaces = unsafe { 115 | slice::from_raw_parts::( 116 | &(*if_table.0).Table[0], 117 | (*if_table.0).NumEntries as usize, 118 | ) 119 | }; 120 | 121 | // Find the local interface matching `idx`. 122 | for iface in ifaces { 123 | if iface.InterfaceIndex == idx { 124 | // Get the MTU. 125 | let mtu: usize = iface.NlMtu.try_into().map_err(|_| default_err())?; 126 | // Get the interface name. 127 | let mut interfacename = [0u8; IF_MAX_STRING_SIZE as usize]; 128 | // if_indextoname writes into the provided buffer. 129 | if unsafe { if_indextoname(iface.InterfaceIndex, &mut interfacename).is_null() } { 130 | return Err(default_err()); 131 | } 132 | // Convert the interface name to a Rust string. 133 | let name = CStr::from_bytes_until_nul(interfacename.as_ref()) 134 | .map_err(|_| default_err())? 135 | .to_str() 136 | .map_err(Error::other)? 137 | .to_string(); 138 | // We found our interface information. 139 | return Ok((name, mtu)); 140 | } 141 | } 142 | Err(default_err()) 143 | } 144 | --------------------------------------------------------------------------------