diff --git a/.github/workflows/cache-factory.yml b/.github/workflows/cache-factory.yml
index 8c49b335f1b..7623b56f450 100644
--- a/.github/workflows/cache-factory.yml
+++ b/.github/workflows/cache-factory.yml
@@ -22,7 +22,7 @@ jobs:
 
       - uses: dtolnay/rust-toolchain@stable
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
         with:
           shared-key: stable-cache
 
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2c1dfc8aaef..aad5b39aec7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,7 +40,7 @@ jobs:
 
       - uses: dtolnay/rust-toolchain@stable
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
         with:
           shared-key: stable-cache
           save-if: false
@@ -149,7 +149,7 @@ jobs:
 
       - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
         with:
           key: ${{ matrix.target }}
           save-if: ${{ github.ref == 'refs/heads/master' }}
@@ -174,7 +174,7 @@ jobs:
 
       - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
         with:
           save-if: ${{ github.ref == 'refs/heads/master' }}
 
@@ -195,7 +195,7 @@ jobs:
 
       - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
         with:
           key: ${{ matrix.features }}
           save-if: ${{ github.ref == 'refs/heads/master' }}
@@ -212,7 +212,7 @@ jobs:
 
       - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
         with:
           save-if: ${{ github.ref == 'refs/heads/master' }}
 
@@ -238,7 +238,7 @@ jobs:
 
       - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
         with:
           save-if: ${{ github.ref == 'refs/heads/master' }}
 
@@ -254,7 +254,7 @@ jobs:
 
       - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
         with:
           save-if: ${{ github.ref == 'refs/heads/master' }}
 
@@ -273,7 +273,7 @@ jobs:
 
       - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
         with:
           shared-key: stable-cache
           save-if: false
@@ -308,7 +308,7 @@ jobs:
       RUSTFLAGS: ''
     steps:
       - uses: actions/checkout@v4
-      - run: wget -q -O- https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.33.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz | tar -xz -C ~/.cargo/bin
+      - run: wget -q -O- https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.36.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz | tar -xz -C ~/.cargo/bin
         shell: bash
       - uses: obi1kenobi/cargo-semver-checks-action@7272cc2caa468d3e009a2b0a9cc366839348237b # v2.6
 
@@ -365,7 +365,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
 
       - run: cargo install --version 0.10.0 pb-rs --locked
 
@@ -391,7 +391,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: Swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2.7.3
+      - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
       - run: cargo metadata --locked --format-version=1 > /dev/null
 
   cargo-deny:
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 21863d0ed39..5cbfc20d69d 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -6,7 +6,6 @@ on:
       - 'master'
     tags:
       - 'libp2p-server-**'
-  pull_request:
 
 jobs:
   server:
@@ -34,11 +33,6 @@ jobs:
         with:
           context: .
           file: ./misc/server/Dockerfile
-          push: ${{ ! github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} # Only push image if we have the required permissions, i.e. not running from a fork
-          cache-from: ${{ ! github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && type=s3,mode=max,bucket=libp2p-by-tf-aws-bootstrap,region=us-east-1,prefix=buildCache,name=rust-libp2p-server }}
-          cache-to:   ${{ ! github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && type=s3,mode=max,bucket=libp2p-by-tf-aws-bootstrap,region=us-east-1,prefix=buildCache,name=rust-libp2p-server }}
+          push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
-        env:
-          AWS_ACCESS_KEY_ID: ${{ vars.TEST_PLANS_BUILD_CACHE_KEY_ID }}
-          AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_PLANS_BUILD_CACHE_KEY }}
diff --git a/.github/workflows/interop-test.yml b/.github/workflows/interop-test.yml
index f3950897089..57d0f1a692d 100644
--- a/.github/workflows/interop-test.yml
+++ b/.github/workflows/interop-test.yml
@@ -1,6 +1,5 @@
 name: Interoperability Testing
 on:
-  pull_request:
   push:
     branches:
       - "master"
@@ -12,6 +11,7 @@ concurrency:
 jobs:
   run-transport-interop:
     name: Run transport interoperability tests
+    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
     runs-on: ${{ fromJSON(github.repository == 'libp2p/rust-libp2p' && '["self-hosted", "linux", "x64", "4xlarge"]' || '"ubuntu-latest"') }}
     strategy:
       matrix:
@@ -24,8 +24,9 @@ jobs:
       - name: Build ${{ matrix.flavour }} image
         run: ./scripts/build-interop-image.sh
         env:
-          AWS_ACCESS_KEY_ID: ${{ vars.TEST_PLANS_BUILD_CACHE_KEY_ID }}
-          AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_PLANS_BUILD_CACHE_KEY }}
+          AWS_BUCKET_NAME: ${{ vars.S3_LIBP2P_BUILD_CACHE_BUCKET_NAME }}
+          AWS_ACCESS_KEY_ID: ${{ vars.S3_LIBP2P_BUILD_CACHE_AWS_ACCESS_KEY_ID }}
+          AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_LIBP2P_BUILD_CACHE_AWS_SECRET_ACCESS_KEY }}
           FLAVOUR: ${{ matrix.flavour }}
 
       - name: Run ${{ matrix.flavour }} tests
@@ -33,12 +34,13 @@ jobs:
         with:
           test-filter: ${{ matrix.flavour }}-rust-libp2p-head
           extra-versions: ${{ github.workspace }}/interop-tests/${{ matrix.flavour }}-ping-version.json
-          s3-cache-bucket: libp2p-by-tf-aws-bootstrap
-          s3-access-key-id: ${{ vars.TEST_PLANS_BUILD_CACHE_KEY_ID }}
-          s3-secret-access-key: ${{ secrets.TEST_PLANS_BUILD_CACHE_KEY }}
+          s3-cache-bucket: ${{ vars.S3_LIBP2P_BUILD_CACHE_BUCKET_NAME }}
+          s3-access-key-id: ${{ vars.S3_LIBP2P_BUILD_CACHE_AWS_ACCESS_KEY_ID }}
+          s3-secret-access-key: ${{ secrets.S3_LIBP2P_BUILD_CACHE_AWS_SECRET_ACCESS_KEY }}
           worker-count: 16
   run-holepunching-interop:
     name: Run hole-punch interoperability tests
+    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
     runs-on: ${{ fromJSON(github.repository == 'libp2p/rust-libp2p' && '["self-hosted", "linux", "x64", "4xlarge"]' || '"ubuntu-latest"') }}
     steps:
       - uses: actions/checkout@v4
@@ -50,7 +52,7 @@ jobs:
         with:
           test-filter: rust-libp2p-head
           extra-versions: ${{ github.workspace }}/hole-punching-tests/version.json
-          s3-cache-bucket: libp2p-by-tf-aws-bootstrap
-          s3-access-key-id: ${{ vars.TEST_PLANS_BUILD_CACHE_KEY_ID }}
-          s3-secret-access-key: ${{ secrets.TEST_PLANS_BUILD_CACHE_KEY }}
+          s3-cache-bucket: ${{ vars.S3_LIBP2P_BUILD_CACHE_BUCKET_NAME }}
+          s3-access-key-id: ${{ vars.S3_LIBP2P_BUILD_CACHE_AWS_ACCESS_KEY_ID }}
+          s3-secret-access-key: ${{ secrets.S3_LIBP2P_BUILD_CACHE_AWS_SECRET_ACCESS_KEY }}
           worker-count: 16
diff --git a/Cargo.lock b/Cargo.lock
index a3105987694..13cbd3622bc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -557,34 +557,6 @@ dependencies = [
  "tracing-subscriber",
 ]
 
-[[package]]
-name = "axum"
-version = "0.6.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
-dependencies = [
- "async-trait",
- "axum-core 0.3.4",
- "bitflags 1.3.2",
- "bytes",
- "futures-util",
- "http 0.2.9",
- "http-body 0.4.5",
- "hyper 0.14.27",
- "itoa",
- "matchit",
- "memchr",
- "mime",
- "percent-encoding",
- "pin-project-lite",
- "rustversion",
- "serde",
- "sync_wrapper 0.1.2",
- "tower",
- "tower-layer",
- "tower-service",
-]
-
 [[package]]
 name = "axum"
 version = "0.7.5"
@@ -592,13 +564,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
 dependencies = [
  "async-trait",
- "axum-core 0.4.3",
+ "axum-core",
  "bytes",
  "futures-util",
- "http 1.0.0",
- "http-body 1.0.0",
+ "http 1.1.0",
+ "http-body",
  "http-body-util",
- "hyper 1.1.0",
+ "hyper",
  "hyper-util",
  "itoa",
  "matchit",
@@ -619,23 +591,6 @@ dependencies = [
  "tracing",
 ]
 
-[[package]]
-name = "axum-core"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
-dependencies = [
- "async-trait",
- "bytes",
- "futures-util",
- "http 0.2.9",
- "http-body 0.4.5",
- "mime",
- "rustversion",
- "tower-layer",
- "tower-service",
-]
-
 [[package]]
 name = "axum-core"
 version = "0.4.3"
@@ -645,8 +600,8 @@ dependencies = [
  "async-trait",
  "bytes",
  "futures-util",
- "http 1.0.0",
- "http-body 1.0.0",
+ "http 1.1.0",
+ "http-body",
  "http-body-util",
  "mime",
  "pin-project-lite",
@@ -791,7 +746,7 @@ name = "browser-webrtc-example"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "axum 0.7.5",
+ "axum",
  "futures",
  "js-sys",
  "libp2p",
@@ -845,9 +800,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.6.0"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
 dependencies = [
  "serde",
 ]
@@ -1418,10 +1373,10 @@ dependencies = [
 name = "distributed-key-value-store-example"
 version = "0.1.0"
 dependencies = [
- "async-std",
  "async-trait",
  "futures",
  "libp2p",
+ "tokio",
  "tracing",
  "tracing-subscriber",
 ]
@@ -1943,25 +1898,6 @@ dependencies = [
  "subtle",
 ]
 
-[[package]]
-name = "h2"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
-dependencies = [
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "futures-util",
- "http 0.2.9",
- "indexmap 2.2.1",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
 [[package]]
 name = "h2"
 version = "0.4.4"
@@ -1973,7 +1909,7 @@ dependencies = [
  "futures-core",
  "futures-sink",
  "futures-util",
- "http 1.0.0",
+ "http 1.1.0",
  "indexmap 2.2.1",
  "slab",
  "tokio",
@@ -2164,26 +2100,15 @@ dependencies = [
 
 [[package]]
 name = "http"
-version = "1.0.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
 dependencies = [
  "bytes",
  "fnv",
  "itoa",
 ]
 
-[[package]]
-name = "http-body"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
-dependencies = [
- "bytes",
- "http 0.2.9",
- "pin-project-lite",
-]
-
 [[package]]
 name = "http-body"
 version = "1.0.0"
@@ -2191,7 +2116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
 dependencies = [
  "bytes",
- "http 1.0.0",
+ "http 1.1.0",
 ]
 
 [[package]]
@@ -2202,8 +2127,8 @@ checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
 dependencies = [
  "bytes",
  "futures-util",
- "http 1.0.0",
- "http-body 1.0.0",
+ "http 1.1.0",
+ "http-body",
  "pin-project-lite",
 ]
 
@@ -2233,44 +2158,21 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
 [[package]]
 name = "hyper"
-version = "0.14.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-core",
- "futures-util",
- "h2 0.3.26",
- "http 0.2.9",
- "http-body 0.4.5",
- "httparse",
- "httpdate",
- "itoa",
- "pin-project-lite",
- "socket2 0.4.9",
- "tokio",
- "tower-service",
- "tracing",
- "want",
-]
-
-[[package]]
-name = "hyper"
-version = "1.1.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75"
+checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
 dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "h2 0.4.4",
- "http 1.0.0",
- "http-body 1.0.0",
+ "h2",
+ "http 1.1.0",
+ "http-body",
  "httparse",
  "httpdate",
  "itoa",
  "pin-project-lite",
+ "smallvec",
  "tokio",
  "want",
 ]
@@ -2282,8 +2184,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
 dependencies = [
  "futures-util",
- "http 1.0.0",
- "hyper 1.1.0",
+ "http 1.1.0",
+ "hyper",
  "hyper-util",
  "rustls 0.22.4",
  "rustls-pki-types",
@@ -2294,14 +2196,15 @@ dependencies = [
 
 [[package]]
 name = "hyper-timeout"
-version = "0.4.1"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
+checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793"
 dependencies = [
- "hyper 0.14.27",
+ "hyper",
+ "hyper-util",
  "pin-project-lite",
  "tokio",
- "tokio-io-timeout",
+ "tower-service",
 ]
 
 [[package]]
@@ -2312,7 +2215,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
 dependencies = [
  "bytes",
  "http-body-util",
- "hyper 1.1.0",
+ "hyper",
  "hyper-util",
  "native-tls",
  "tokio",
@@ -2322,20 +2225,19 @@ dependencies = [
 
 [[package]]
 name = "hyper-util"
-version = "0.1.3"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
+checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
 dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "http 1.0.0",
- "http-body 1.0.0",
- "hyper 1.1.0",
+ "http 1.1.0",
+ "http-body",
+ "hyper",
  "pin-project-lite",
  "socket2 0.5.7",
  "tokio",
- "tower",
  "tower-service",
  "tracing",
 ]
@@ -2403,16 +2305,18 @@ dependencies = [
 
 [[package]]
 name = "igd-next"
-version = "0.14.3"
+version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "064d90fec10d541084e7b39ead8875a5a80d9114a2b18791565253bae25f49e4"
+checksum = "76b0d7d4541def58a37bf8efc559683f21edce7c82f0d866c93ac21f7e098f93"
 dependencies = [
  "async-trait",
  "attohttpc",
  "bytes",
  "futures",
- "http 0.2.9",
- "hyper 0.14.27",
+ "http 1.1.0",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
  "log",
  "rand 0.8.5",
  "tokio",
@@ -2489,7 +2393,7 @@ name = "interop-tests"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "axum 0.7.5",
+ "axum",
  "console_error_panic_hook",
  "either",
  "futures",
@@ -2720,7 +2624,7 @@ dependencies = [
 
 [[package]]
 name = "libp2p-autonat"
-version = "0.13.0"
+version = "0.13.1"
 dependencies = [
  "async-std",
  "async-trait",
@@ -2870,7 +2774,7 @@ dependencies = [
 
 [[package]]
 name = "libp2p-gossipsub"
-version = "0.47.1"
+version = "0.48.0"
 dependencies = [
  "async-channel 2.3.1",
  "async-std",
@@ -3154,7 +3058,6 @@ dependencies = [
 name = "libp2p-ping"
 version = "0.45.0"
 dependencies = [
- "async-std",
  "either",
  "futures",
  "futures-timer",
@@ -3164,6 +3067,7 @@ dependencies = [
  "libp2p-swarm-test",
  "quickcheck-ext",
  "rand 0.8.5",
+ "tokio",
  "tracing",
  "tracing-subscriber",
  "void",
@@ -3328,9 +3232,9 @@ dependencies = [
 
 [[package]]
 name = "libp2p-server"
-version = "0.12.7"
+version = "0.12.8"
 dependencies = [
- "axum 0.7.5",
+ "axum",
  "base64 0.22.1",
  "clap",
  "futures",
@@ -3364,7 +3268,7 @@ dependencies = [
 
 [[package]]
 name = "libp2p-swarm"
-version = "0.45.1"
+version = "0.45.2"
 dependencies = [
  "async-std",
  "criterion",
@@ -3409,7 +3313,7 @@ dependencies = [
 
 [[package]]
 name = "libp2p-swarm-test"
-version = "0.4.0"
+version = "0.5.0"
 dependencies = [
  "async-trait",
  "futures",
@@ -3478,7 +3382,7 @@ dependencies = [
 
 [[package]]
 name = "libp2p-upnp"
-version = "0.3.0"
+version = "0.3.1"
 dependencies = [
  "futures",
  "futures-timer",
@@ -3615,6 +3519,7 @@ dependencies = [
  "multiaddr",
  "multibase",
  "multihash",
+ "once_cell",
  "send_wrapper 0.6.0",
  "thiserror",
  "tracing",
@@ -3809,16 +3714,16 @@ dependencies = [
 name = "metrics-example"
 version = "0.1.0"
 dependencies = [
- "axum 0.7.5",
+ "axum",
  "futures",
  "libp2p",
- "opentelemetry 0.23.0",
+ "opentelemetry 0.25.0",
  "opentelemetry-otlp",
- "opentelemetry_sdk 0.23.0",
+ "opentelemetry_sdk 0.25.0",
  "prometheus-client",
  "tokio",
  "tracing",
- "tracing-opentelemetry 0.24.0",
+ "tracing-opentelemetry 0.26.0",
  "tracing-subscriber",
 ]
 
@@ -4233,9 +4138,9 @@ dependencies = [
 
 [[package]]
 name = "opentelemetry"
-version = "0.23.0"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76"
+checksum = "803801d3d3b71cd026851a53f974ea03df3d179cb758b260136a6c9e22e196af"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -4263,16 +4168,16 @@ dependencies = [
 
 [[package]]
 name = "opentelemetry-otlp"
-version = "0.16.0"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a94c69209c05319cdf7460c6d4c055ed102be242a0a6245835d7bc42c6ec7f54"
+checksum = "596b1719b3cab83addb20bcbffdf21575279d9436d9ccccfe651a3bf0ab5ab06"
 dependencies = [
  "async-trait",
  "futures-core",
- "http 0.2.9",
- "opentelemetry 0.23.0",
+ "http 1.1.0",
+ "opentelemetry 0.25.0",
  "opentelemetry-proto",
- "opentelemetry_sdk 0.23.0",
+ "opentelemetry_sdk 0.25.0",
  "prost",
  "thiserror",
  "tokio",
@@ -4281,12 +4186,12 @@ dependencies = [
 
 [[package]]
 name = "opentelemetry-proto"
-version = "0.6.0"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "984806e6cf27f2b49282e2a05e288f30594f3dbc74eb7a6e99422bc48ed78162"
+checksum = "2c43620e8f93359eb7e627a3b16ee92d8585774986f24f2ab010817426c5ce61"
 dependencies = [
- "opentelemetry 0.23.0",
- "opentelemetry_sdk 0.23.0",
+ "opentelemetry 0.25.0",
+ "opentelemetry_sdk 0.25.0",
  "prost",
  "tonic",
 ]
@@ -4324,21 +4229,20 @@ dependencies = [
 
 [[package]]
 name = "opentelemetry_sdk"
-version = "0.23.0"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd"
+checksum = "e0da0d6b47a3dbc6e9c9e36a0520e25cf943e046843818faaa3f87365a548c82"
 dependencies = [
  "async-trait",
  "futures-channel",
  "futures-executor",
  "futures-util",
  "glob",
- "lazy_static",
  "once_cell",
- "opentelemetry 0.23.0",
- "ordered-float 4.2.0",
+ "opentelemetry 0.25.0",
  "percent-encoding",
  "rand 0.8.5",
+ "serde_json",
  "thiserror",
  "tokio",
  "tokio-stream",
@@ -4619,30 +4523,6 @@ dependencies = [
  "elliptic-curve",
 ]
 
-[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
-dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
-]
-
 [[package]]
 name = "proc-macro2"
 version = "1.0.85"
@@ -4677,9 +4557,9 @@ dependencies = [
 
 [[package]]
 name = "prost"
-version = "0.12.3"
+version = "0.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a"
+checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f"
 dependencies = [
  "bytes",
  "prost-derive",
@@ -4687,9 +4567,9 @@ dependencies = [
 
 [[package]]
 name = "prost-derive"
-version = "0.12.3"
+version = "0.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
+checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5"
 dependencies = [
  "anyhow",
  "itertools",
@@ -4759,7 +4639,7 @@ dependencies = [
  "pin-project-lite",
  "quinn-proto",
  "quinn-udp",
- "rustc-hash",
+ "rustc-hash 1.1.0",
  "rustls 0.23.11",
  "thiserror",
  "tokio",
@@ -4768,14 +4648,14 @@ dependencies = [
 
 [[package]]
 name = "quinn-proto"
-version = "0.11.2"
+version = "0.11.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e974563a4b1c2206bbc61191ca4da9c22e4308b4c455e8906751cc7828393f08"
+checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
 dependencies = [
  "bytes",
  "rand 0.8.5",
  "ring 0.17.8",
- "rustc-hash",
+ "rustc-hash 2.0.0",
  "rustls 0.23.11",
  "slab",
  "thiserror",
@@ -5005,11 +4885,10 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
 name = "relay-server-example"
 version = "0.1.0"
 dependencies = [
- "async-std",
- "async-trait",
  "clap",
  "futures",
  "libp2p",
+ "tokio",
  "tracing",
  "tracing-subscriber",
 ]
@@ -5036,11 +4915,11 @@ dependencies = [
  "encoding_rs",
  "futures-core",
  "futures-util",
- "h2 0.4.4",
- "http 1.0.0",
- "http-body 1.0.0",
+ "h2",
+ "http 1.1.0",
+ "http-body",
  "http-body-util",
- "hyper 1.1.0",
+ "hyper",
  "hyper-rustls",
  "hyper-tls",
  "hyper-util",
@@ -5241,6 +5120,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
+[[package]]
+name = "rustc-hash"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
+
 [[package]]
 name = "rustc_version"
 version = "0.4.0"
@@ -5506,18 +5391,18 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.203"
+version = "1.0.210"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
+checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.203"
+version = "1.0.210"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
+checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5548,9 +5433,9 @@ dependencies = [
 
 [[package]]
 name = "serde_repr"
-version = "0.1.18"
+version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
+checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6018,7 +5903,7 @@ dependencies = [
  "async-trait",
  "base64 0.22.1",
  "futures",
- "http 1.0.0",
+ "http 1.1.0",
  "indexmap 2.2.1",
  "parking_lot",
  "paste",
@@ -6037,30 +5922,29 @@ dependencies = [
 
 [[package]]
 name = "thirtyfour-macros"
-version = "0.1.1"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9cae91d1c7c61ec65817f1064954640ee350a50ae6548ff9a1bdd2489d6ffbb0"
+checksum = "b72d056365e368fc57a56d0cec9e41b02fb4a3474a61c8735262b1cfebe67425"
 dependencies = [
- "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.66",
 ]
 
 [[package]]
 name = "thiserror"
-version = "1.0.61"
+version = "1.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
+checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.61"
+version = "1.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
+checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -6174,16 +6058,6 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
-[[package]]
-name = "tokio-io-timeout"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
-dependencies = [
- "pin-project-lite",
- "tokio",
-]
-
 [[package]]
 name = "tokio-macros"
 version = "2.3.0"
@@ -6218,9 +6092,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-stream"
-version = "0.1.14"
+version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
 dependencies = [
  "futures-core",
  "pin-project-lite",
@@ -6277,23 +6151,26 @@ dependencies = [
 
 [[package]]
 name = "tonic"
-version = "0.11.0"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13"
+checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
 dependencies = [
  "async-stream",
  "async-trait",
- "axum 0.6.20",
- "base64 0.21.7",
+ "axum",
+ "base64 0.22.1",
  "bytes",
- "h2 0.3.26",
- "http 0.2.9",
- "http-body 0.4.5",
- "hyper 0.14.27",
+ "h2",
+ "http 1.1.0",
+ "http-body",
+ "http-body-util",
+ "hyper",
  "hyper-timeout",
+ "hyper-util",
  "percent-encoding",
  "pin-project",
  "prost",
+ "socket2 0.5.7",
  "tokio",
  "tokio-stream",
  "tower",
@@ -6331,8 +6208,8 @@ dependencies = [
  "bitflags 2.4.1",
  "bytes",
  "futures-util",
- "http 1.0.0",
- "http-body 1.0.0",
+ "http 1.1.0",
+ "http-body",
  "http-body-util",
  "http-range-header",
  "httpdate",
@@ -6423,14 +6300,14 @@ dependencies = [
 
 [[package]]
 name = "tracing-opentelemetry"
-version = "0.24.0"
+version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f68803492bf28ab40aeccaecc7021096bd256baf7ca77c3d425d89b35a7be4e4"
+checksum = "5eabc56d23707ad55ba2a0750fc24767125d5a0f51993ba41ad2c441cc7b8dea"
 dependencies = [
  "js-sys",
  "once_cell",
- "opentelemetry 0.23.0",
- "opentelemetry_sdk 0.23.0",
+ "opentelemetry 0.25.0",
+ "opentelemetry_sdk 0.25.0",
  "smallvec",
  "tracing",
  "tracing-core",
@@ -6611,9 +6488,9 @@ dependencies = [
 
 [[package]]
 name = "url"
-version = "2.5.0"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
 dependencies = [
  "form_urlencoded",
  "idna 0.5.0",
diff --git a/Cargo.toml b/Cargo.toml
index da8d32e1a4a..8869505921d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -77,13 +77,13 @@ futures-bounded = { version = "0.2.4" }
 futures-rustls = { version = "0.26.0", default-features = false }
 libp2p = { version = "0.54.1", path = "libp2p" }
 libp2p-allow-block-list = { version = "0.4.1", path = "misc/allow-block-list" }
-libp2p-autonat = { version = "0.13.0", path = "protocols/autonat" }
+libp2p-autonat = { version = "0.13.1", path = "protocols/autonat" }
 libp2p-connection-limits = { version = "0.4.0", path = "misc/connection-limits" }
 libp2p-core = { version = "0.42.0", path = "core" }
 libp2p-dcutr = { version = "0.12.0", path = "protocols/dcutr" }
 libp2p-dns = { version = "0.42.0", path = "transports/dns" }
 libp2p-floodsub = { version = "0.45.0", path = "protocols/floodsub" }
-libp2p-gossipsub = { version = "0.47.1", path = "protocols/gossipsub" }
+libp2p-gossipsub = { version = "0.48.0", path = "protocols/gossipsub" }
 libp2p-identify = { version = "0.45.1", path = "protocols/identify" }
 libp2p-identity = { version = "0.2.9" }
 libp2p-kad = { version = "0.47.0", path = "protocols/kad" }
@@ -100,15 +100,15 @@ libp2p-quic = { version = "0.11.1", path = "transports/quic" }
 libp2p-relay = { version = "0.18.0", path = "protocols/relay" }
 libp2p-rendezvous = { version = "0.15.0", path = "protocols/rendezvous" }
 libp2p-request-response = { version = "0.27.0", path = "protocols/request-response" }
-libp2p-server = { version = "0.12.7", path = "misc/server" }
+libp2p-server = { version = "0.12.8", path = "misc/server" }
 libp2p-stream = { version = "0.2.0-alpha", path = "protocols/stream" }
-libp2p-swarm = { version = "0.45.1", path = "swarm" }
+libp2p-swarm = { version = "0.45.2", path = "swarm" }
 libp2p-swarm-derive = { version = "=0.35.0", path = "swarm-derive" } # `libp2p-swarm-derive` may not be compatible with different `libp2p-swarm` non-breaking releases. E.g. `libp2p-swarm` might introduce a new enum variant `FromSwarm` (which is `#[non-exhaustive]`) in a non-breaking release. Older versions of `libp2p-swarm-derive` would not forward this enum variant within the `NetworkBehaviour` hierarchy. Thus the version pinning is required.
-libp2p-swarm-test = { version = "0.4.0", path = "swarm-test" }
+libp2p-swarm-test = { version = "0.5.0", path = "swarm-test" }
 libp2p-tcp = { version = "0.42.0", path = "transports/tcp" }
 libp2p-tls = { version = "0.5.0", path = "transports/tls" }
 libp2p-uds = { version = "0.41.0", path = "transports/uds" }
-libp2p-upnp = { version = "0.3.0", path = "protocols/upnp" }
+libp2p-upnp = { version = "0.3.1", path = "protocols/upnp" }
 libp2p-webrtc = { version = "0.8.0-alpha", path = "transports/webrtc" }
 libp2p-webrtc-utils = { version = "0.3.0", path = "misc/webrtc-utils" }
 libp2p-webrtc-websys = { version = "0.4.0-alpha.2", path = "transports/webrtc-websys" }
@@ -151,4 +151,8 @@ clippy.manual_let_else = "warn"
 clippy.dbg_macro = "warn"
 
 [workspace.metadata.release]
-pre-release-hook = ["/bin/sh", '-c', '/bin/sh $WORKSPACE_ROOT/scripts/add-changelog-header.sh'] # Nested use of shell to expand variables.
+pre-release-hook = [
+    "/bin/sh",
+    '-c',
+    '/bin/sh $WORKSPACE_ROOT/scripts/add-changelog-header.sh',
+] # Nested use of shell to expand variables.
diff --git a/examples/autonatv2/Dockerfile b/examples/autonatv2/Dockerfile
index 5a523649d80..6bc92e4d11b 100644
--- a/examples/autonatv2/Dockerfile
+++ b/examples/autonatv2/Dockerfile
@@ -1,4 +1,4 @@
-FROM rust:1.75-alpine as builder
+FROM rust:1.81-alpine as builder
 
 RUN apk add musl-dev
 
diff --git a/examples/distributed-key-value-store/Cargo.toml b/examples/distributed-key-value-store/Cargo.toml
index 9c2e2bce5c9..3846e54c8d3 100644
--- a/examples/distributed-key-value-store/Cargo.toml
+++ b/examples/distributed-key-value-store/Cargo.toml
@@ -9,10 +9,10 @@ license = "MIT"
 release = false
 
 [dependencies]
-async-std = { version = "1.12", features = ["attributes"] }
+tokio = { workspace = true, features = ["full"] }
 async-trait = "0.1"
 futures = { workspace = true }
-libp2p = { path = "../../libp2p", features = [ "async-std", "dns", "kad", "mdns", "noise", "macros", "tcp", "yamux"] }
+libp2p = { path = "../../libp2p", features = [ "tokio", "dns", "kad", "mdns", "noise", "macros", "tcp", "yamux"] }
 tracing = { workspace = true }
 tracing-subscriber = { workspace = true, features = ["env-filter"] }
 
diff --git a/examples/distributed-key-value-store/src/main.rs b/examples/distributed-key-value-store/src/main.rs
index 404333f3d20..6b7947b7eb3 100644
--- a/examples/distributed-key-value-store/src/main.rs
+++ b/examples/distributed-key-value-store/src/main.rs
@@ -20,8 +20,7 @@
 
 #![doc = include_str!("../README.md")]
 
-use async_std::io;
-use futures::{prelude::*, select};
+use futures::stream::StreamExt;
 use libp2p::kad;
 use libp2p::kad::store::MemoryStore;
 use libp2p::kad::Mode;
@@ -32,9 +31,13 @@ use libp2p::{
 };
 use std::error::Error;
 use std::time::Duration;
+use tokio::{
+    io::{self, AsyncBufReadExt},
+    select,
+};
 use tracing_subscriber::EnvFilter;
 
-#[async_std::main]
+#[tokio::main]
 async fn main() -> Result<(), Box<dyn Error>> {
     let _ = tracing_subscriber::fmt()
         .with_env_filter(EnvFilter::from_default_env())
@@ -44,11 +47,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
     #[derive(NetworkBehaviour)]
     struct Behaviour {
         kademlia: kad::Behaviour<MemoryStore>,
-        mdns: mdns::async_io::Behaviour,
+        mdns: mdns::tokio::Behaviour,
     }
 
     let mut swarm = libp2p::SwarmBuilder::with_new_identity()
-        .with_async_std()
+        .with_tokio()
         .with_tcp(
             tcp::Config::default(),
             noise::Config::new,
@@ -60,7 +63,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
                     key.public().to_peer_id(),
                     MemoryStore::new(key.public().to_peer_id()),
                 ),
-                mdns: mdns::async_io::Behaviour::new(
+                mdns: mdns::tokio::Behaviour::new(
                     mdns::Config::default(),
                     key.public().to_peer_id(),
                 )?,
@@ -72,7 +75,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
     swarm.behaviour_mut().kademlia.set_mode(Some(Mode::Server));
 
     // Read full lines from stdin
-    let mut stdin = io::BufReader::new(io::stdin()).lines().fuse();
+    let mut stdin = io::BufReader::new(io::stdin()).lines();
 
     // Listen on all interfaces and whatever port the OS assigns.
     swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;
@@ -80,7 +83,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
     // Kick it off.
     loop {
         select! {
-        line = stdin.select_next_some() => handle_input_line(&mut swarm.behaviour_mut().kademlia, line.expect("Stdin not to close")),
+        Ok(Some(line)) = stdin.next_line() => {
+            handle_input_line(&mut swarm.behaviour_mut().kademlia, line);
+        }
         event = swarm.select_next_some() => match event {
             SwarmEvent::NewListenAddr { address, .. } => {
                 println!("Listening in {address:?}");
diff --git a/examples/metrics/Cargo.toml b/examples/metrics/Cargo.toml
index 2b82668f52a..129b1abb1f3 100644
--- a/examples/metrics/Cargo.toml
+++ b/examples/metrics/Cargo.toml
@@ -12,13 +12,13 @@ release = false
 futures = { workspace = true }
 axum = "0.7"
 libp2p = { path = "../../libp2p", features = ["tokio", "metrics", "ping", "noise", "identify", "tcp", "yamux", "macros"] }
-opentelemetry = { version = "0.23.0", features = ["metrics"] }
-opentelemetry-otlp = { version = "0.16.0", features = ["metrics"] }
-opentelemetry_sdk = { version = "0.23.0", features = ["rt-tokio", "metrics"] }
+opentelemetry = { version = "0.25.0", features = ["metrics"] }
+opentelemetry-otlp = { version = "0.25.0", features = ["metrics"] }
+opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio", "metrics"] }
 prometheus-client = { workspace = true }
 tokio = { workspace = true, features = ["full"] }
 tracing = { workspace = true }
-tracing-opentelemetry = "0.24.0"
+tracing-opentelemetry = "0.26.0"
 tracing-subscriber = { workspace = true, features = ["env-filter"] }
 
 [lints]
diff --git a/examples/metrics/src/main.rs b/examples/metrics/src/main.rs
index 99a9ca66aaf..1755c769053 100644
--- a/examples/metrics/src/main.rs
+++ b/examples/metrics/src/main.rs
@@ -25,7 +25,7 @@ use libp2p::core::Multiaddr;
 use libp2p::metrics::{Metrics, Recorder};
 use libp2p::swarm::{NetworkBehaviour, SwarmEvent};
 use libp2p::{identify, identity, noise, ping, tcp, yamux};
-use opentelemetry::KeyValue;
+use opentelemetry::{trace::TracerProvider, KeyValue};
 use prometheus_client::registry::Registry;
 use std::error::Error;
 use std::time::Duration;
@@ -90,7 +90,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
 }
 
 fn setup_tracing() -> Result<(), Box<dyn Error>> {
-    let tracer = opentelemetry_otlp::new_pipeline()
+    let provider = opentelemetry_otlp::new_pipeline()
         .tracing()
         .with_exporter(opentelemetry_otlp::new_exporter().tonic())
         .with_trace_config(opentelemetry_sdk::trace::Config::default().with_resource(
@@ -102,10 +102,10 @@ fn setup_tracing() -> Result<(), Box<dyn Error>> {
         .with(tracing_subscriber::fmt::layer().with_filter(EnvFilter::from_default_env()))
         .with(
             tracing_opentelemetry::layer()
-                .with_tracer(tracer)
+                .with_tracer(provider.tracer("libp2p-subscriber"))
                 .with_filter(EnvFilter::from_default_env()),
         )
-        .try_init()?;
+        .init();
 
     Ok(())
 }
diff --git a/examples/relay-server/Cargo.toml b/examples/relay-server/Cargo.toml
index 12d3e2467ce..7385cf6c033 100644
--- a/examples/relay-server/Cargo.toml
+++ b/examples/relay-server/Cargo.toml
@@ -10,10 +10,9 @@ release = false
 
 [dependencies]
 clap = { version = "4.5.6", features = ["derive"] }
-async-std = { version = "1.12", features = ["attributes"] }
-async-trait = "0.1"
+tokio = { version = "1.37.0", features = ["full"] }
 futures = { workspace = true }
-libp2p = { path = "../../libp2p", features = [ "async-std", "noise", "macros", "ping", "tcp", "identify", "yamux", "relay", "quic"] }
+libp2p = { path = "../../libp2p", features = ["tokio", "noise", "macros", "ping", "tcp", "identify", "yamux", "relay", "quic"] }
 tracing = { workspace = true }
 tracing-subscriber = { workspace = true, features = ["env-filter"] }
 
diff --git a/examples/relay-server/src/main.rs b/examples/relay-server/src/main.rs
index bf5817454f8..46a122d0717 100644
--- a/examples/relay-server/src/main.rs
+++ b/examples/relay-server/src/main.rs
@@ -22,8 +22,7 @@
 #![doc = include_str!("../README.md")]
 
 use clap::Parser;
-use futures::executor::block_on;
-use futures::stream::StreamExt;
+use futures::StreamExt;
 use libp2p::{
     core::multiaddr::Protocol,
     core::Multiaddr,
@@ -35,7 +34,8 @@ use std::error::Error;
 use std::net::{Ipv4Addr, Ipv6Addr};
 use tracing_subscriber::EnvFilter;
 
-fn main() -> Result<(), Box<dyn Error>> {
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn Error>> {
     let _ = tracing_subscriber::fmt()
         .with_env_filter(EnvFilter::from_default_env())
         .try_init();
@@ -46,7 +46,7 @@ fn main() -> Result<(), Box<dyn Error>> {
     let local_key: identity::Keypair = generate_ed25519(opt.secret_key_seed);
 
     let mut swarm = libp2p::SwarmBuilder::with_existing_identity(local_key)
-        .with_async_std()
+        .with_tokio()
         .with_tcp(
             tcp::Config::default(),
             noise::Config::new,
@@ -81,27 +81,25 @@ fn main() -> Result<(), Box<dyn Error>> {
         .with(Protocol::QuicV1);
     swarm.listen_on(listen_addr_quic)?;
 
-    block_on(async {
-        loop {
-            match swarm.next().await.expect("Infinite Stream.") {
-                SwarmEvent::Behaviour(event) => {
-                    if let BehaviourEvent::Identify(identify::Event::Received {
-                        info: identify::Info { observed_addr, .. },
-                        ..
-                    }) = &event
-                    {
-                        swarm.add_external_address(observed_addr.clone());
-                    }
-
-                    println!("{event:?}")
+    loop {
+        match swarm.next().await.expect("Infinite Stream.") {
+            SwarmEvent::Behaviour(event) => {
+                if let BehaviourEvent::Identify(identify::Event::Received {
+                    info: identify::Info { observed_addr, .. },
+                    ..
+                }) = &event
+                {
+                    swarm.add_external_address(observed_addr.clone());
                 }
-                SwarmEvent::NewListenAddr { address, .. } => {
-                    println!("Listening on {address:?}");
-                }
-                _ => {}
+
+                println!("{event:?}")
+            }
+            SwarmEvent::NewListenAddr { address, .. } => {
+                println!("Listening on {address:?}");
             }
+            _ => {}
         }
-    })
+    }
 }
 
 #[derive(NetworkBehaviour)]
diff --git a/hole-punching-tests/Dockerfile b/hole-punching-tests/Dockerfile
index af00ef2272f..403cc301fc6 100644
--- a/hole-punching-tests/Dockerfile
+++ b/hole-punching-tests/Dockerfile
@@ -1,5 +1,5 @@
 # syntax=docker/dockerfile:1.5-labs
-FROM rust:1.75.0 as builder
+FROM rust:1.81.0 as builder
 
 # Run with access to the target cache to speed up builds
 WORKDIR /workspace
diff --git a/identity/src/peer_id.rs b/identity/src/peer_id.rs
index 7b3f799f612..8ae6d99ae32 100644
--- a/identity/src/peer_id.rs
+++ b/identity/src/peer_id.rs
@@ -191,7 +191,7 @@ impl<'de> Deserialize<'de> for PeerId {
 
         struct PeerIdVisitor;
 
-        impl<'de> Visitor<'de> for PeerIdVisitor {
+        impl Visitor<'_> for PeerIdVisitor {
             type Value = PeerId;
 
             fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
diff --git a/identity/src/rsa.rs b/identity/src/rsa.rs
index cbfe3c1b919..5eb78a4af75 100644
--- a/identity/src/rsa.rs
+++ b/identity/src/rsa.rs
@@ -149,7 +149,7 @@ struct Asn1RawOid<'a> {
     object: DerObject<'a>,
 }
 
-impl<'a> Asn1RawOid<'a> {
+impl Asn1RawOid<'_> {
     /// The underlying OID as byte literal.
     pub(crate) fn oid(&self) -> &[u8] {
         self.object.value()
@@ -169,7 +169,7 @@ impl<'a> DerTypeView<'a> for Asn1RawOid<'a> {
     }
 }
 
-impl<'a> DerEncodable for Asn1RawOid<'a> {
+impl DerEncodable for Asn1RawOid<'_> {
     fn encode<S: Sink>(&self, sink: &mut S) -> Result<(), Asn1DerError> {
         self.object.encode(sink)
     }
diff --git a/interop-tests/Dockerfile.chromium b/interop-tests/Dockerfile.chromium
index a6b0fc89e82..86edbc5b9d2 100644
--- a/interop-tests/Dockerfile.chromium
+++ b/interop-tests/Dockerfile.chromium
@@ -1,5 +1,5 @@
 # syntax=docker/dockerfile:1.5-labs
-FROM rust:1.75.0 as chef
+FROM rust:1.81 as chef
 RUN rustup target add wasm32-unknown-unknown
 RUN wget -q -O- https://github.com/rustwasm/wasm-pack/releases/download/v0.12.1/wasm-pack-v0.12.1-x86_64-unknown-linux-musl.tar.gz | tar -zx -C /usr/local/bin --strip-components 1 --wildcards "wasm-pack-*/wasm-pack"
 RUN wget -q -O- https://github.com/WebAssembly/binaryen/releases/download/version_115/binaryen-version_115-x86_64-linux.tar.gz | tar -zx -C /usr/local/bin --strip-components 2 --wildcards "binaryen-version_*/bin/wasm-opt"
diff --git a/interop-tests/Dockerfile.native b/interop-tests/Dockerfile.native
index b122ac72991..499c73437fc 100644
--- a/interop-tests/Dockerfile.native
+++ b/interop-tests/Dockerfile.native
@@ -1,5 +1,5 @@
 # syntax=docker/dockerfile:1.5-labs
-FROM lukemathwalker/cargo-chef:0.1.62-rust-1.75.0 as chef
+FROM lukemathwalker/cargo-chef:0.1.67-rust-bullseye as chef
 WORKDIR /app
 
 FROM chef AS planner
diff --git a/misc/allow-block-list/src/lib.rs b/misc/allow-block-list/src/lib.rs
index 7646638a651..56de29d1985 100644
--- a/misc/allow-block-list/src/lib.rs
+++ b/misc/allow-block-list/src/lib.rs
@@ -271,6 +271,8 @@ where
         _: ConnectionId,
         event: THandlerOutEvent<Self>,
     ) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(event)
     }
 
diff --git a/misc/connection-limits/src/lib.rs b/misc/connection-limits/src/lib.rs
index b02e52f25a1..05a9b639f26 100644
--- a/misc/connection-limits/src/lib.rs
+++ b/misc/connection-limits/src/lib.rs
@@ -355,6 +355,8 @@ impl NetworkBehaviour for Behaviour {
         _: ConnectionId,
         event: THandlerOutEvent<Self>,
     ) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(event)
     }
 
@@ -586,6 +588,8 @@ mod tests {
             _connection_id: ConnectionId,
             event: THandlerOutEvent<Self>,
         ) {
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             void::unreachable(event)
         }
 
diff --git a/misc/memory-connection-limits/src/lib.rs b/misc/memory-connection-limits/src/lib.rs
index 7b5803a61aa..757ff770487 100644
--- a/misc/memory-connection-limits/src/lib.rs
+++ b/misc/memory-connection-limits/src/lib.rs
@@ -190,6 +190,8 @@ impl NetworkBehaviour for Behaviour {
         _: ConnectionId,
         event: THandlerOutEvent<Self>,
     ) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(event)
     }
 
diff --git a/misc/memory-connection-limits/tests/util/mod.rs b/misc/memory-connection-limits/tests/util/mod.rs
index d18aa78fd22..01e8cd9f655 100644
--- a/misc/memory-connection-limits/tests/util/mod.rs
+++ b/misc/memory-connection-limits/tests/util/mod.rs
@@ -116,6 +116,8 @@ impl<const MEM_PENDING: usize, const MEM_ESTABLISHED: usize> NetworkBehaviour
         _: ConnectionId,
         event: THandlerOutEvent<Self>,
     ) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(event)
     }
 
diff --git a/misc/quick-protobuf-codec/src/lib.rs b/misc/quick-protobuf-codec/src/lib.rs
index 166ee82ff08..c57b7da7db8 100644
--- a/misc/quick-protobuf-codec/src/lib.rs
+++ b/misc/quick-protobuf-codec/src/lib.rs
@@ -12,6 +12,7 @@ mod generated;
 pub use generated::test as proto;
 
 /// [`Codec`] implements [`Encoder`] and [`Decoder`], uses [`unsigned_varint`]
+///
 /// to prefix messages with their length and uses [`quick_protobuf`] and a provided
 /// `struct` implementing [`MessageRead`] and [`MessageWrite`] to do the encoding.
 pub struct Codec<In, Out = In> {
@@ -119,7 +120,7 @@ impl<'a> BytesMutWriterBackend<'a> {
     }
 }
 
-impl<'a> WriterBackend for BytesMutWriterBackend<'a> {
+impl WriterBackend for BytesMutWriterBackend<'_> {
     fn pb_write_u8(&mut self, x: u8) -> quick_protobuf::Result<()> {
         self.dst.put_u8(x);
 
diff --git a/misc/server/CHANGELOG.md b/misc/server/CHANGELOG.md
index 5369163460c..fe48de0f553 100644
--- a/misc/server/CHANGELOG.md
+++ b/misc/server/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 0.12.8
+
+### Changed
+
+- Remove deprecated [`libp2p-lookup`](https://github.com/mxinden/libp2p-lookup) from Dockerfile.
+  See [PR 5610](https://github.com/libp2p/rust-libp2p/pull/5610).
+
 ## 0.12.7
 
 ### Changed
@@ -31,6 +38,7 @@
 ## 0.12.3
 
 ### Changed
+
 - Add libp2p-lookup to Dockerfile to enable healthchecks.
 
 ### Fixed
@@ -42,14 +50,18 @@
 [PR 4467]: https://github.com/libp2p/rust-libp2p/pull/4467
 
 ## 0.12.2
+
 ### Fixed
+
 - Adhere to `--metrics-path` flag and listen on `0.0.0.0:8888` (default IPFS metrics port).
   [PR 4392]
 
 [PR 4392]: https://github.com/libp2p/rust-libp2p/pull/4392
 
 ## 0.12.1
+
 ### Changed
+
 - Move to tokio and hyper.
   See [PR 4311].
 - Move to distroless Docker base image.
@@ -58,39 +70,57 @@
 [PR 4311]: https://github.com/libp2p/rust-libp2p/pull/4311
 
 ## 0.8.0
+
 ### Changed
+
 - Remove mplex support.
 
 ## 0.7.0
+
 ### Changed
+
 - Update to libp2p v0.47.0.
 
 ## 0.6.0 - 2022-05-05
+
 ### Changed
+
 - Update to libp2p v0.44.0.
 
 ## 0.5.4 - 2022-01-11
+
 ### Changed
+
 - Pull latest autonat changes.
 
 ## 0.5.3 - 2021-12-25
+
 ### Changed
+
 - Update dependencies.
 - Pull in autonat fixes.
 
 ## 0.5.2 - 2021-12-20
+
 ### Added
+
 - Add support for libp2p autonat protocol via `--enable-autonat`.
 
 ## 0.5.1 - 2021-12-20
+
 ### Fixed
+
 - Update dependencies.
 - Fix typo in command line flag `--enable-kademlia`.
 
 ## 0.5.0 - 2021-11-18
+
 ### Changed
+
 - Disable Kademlia protocol by default.
 
 ## 0.4.0 - 2021-11-18
+
 ### Fixed
+
 - Update dependencies.
diff --git a/misc/server/Cargo.toml b/misc/server/Cargo.toml
index 798ecfa07a9..0954e2f38d8 100644
--- a/misc/server/Cargo.toml
+++ b/misc/server/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "libp2p-server"
-version = "0.12.7"
+version = "0.12.8"
 authors = ["Max Inden <mail@max-inden.de>"]
 edition = "2021"
 repository = "https://github.com/libp2p/rust-libp2p"
@@ -16,7 +16,23 @@ clap = { version = "4.5.6", features = ["derive"] }
 futures = { workspace = true }
 futures-timer = "3"
 axum = "0.7"
-libp2p = { workspace = true, features = ["autonat", "dns", "tokio", "noise", "tcp", "yamux", "identify", "kad", "ping", "relay", "metrics", "rsa", "macros", "quic", "websocket"] }
+libp2p = { workspace = true, features = [
+    "autonat",
+    "dns",
+    "tokio",
+    "noise",
+    "tcp",
+    "yamux",
+    "identify",
+    "kad",
+    "ping",
+    "relay",
+    "metrics",
+    "rsa",
+    "macros",
+    "quic",
+    "websocket",
+] }
 prometheus-client = { workspace = true }
 serde = "1.0.203"
 serde_derive = "1.0.125"
diff --git a/misc/server/Dockerfile b/misc/server/Dockerfile
index 1583fba6bef..12a8982eb3f 100644
--- a/misc/server/Dockerfile
+++ b/misc/server/Dockerfile
@@ -1,7 +1,6 @@
 # syntax=docker/dockerfile:1.5-labs
-FROM rust:1.75.0 as chef
+FROM rust:1.81.0 as chef
 RUN wget -q -O- https://github.com/LukeMathWalker/cargo-chef/releases/download/v0.1.62/cargo-chef-x86_64-unknown-linux-gnu.tar.gz | tar -zx -C /usr/local/bin
-RUN cargo install --locked --root /usr/local libp2p-lookup --version 0.6.4
 WORKDIR /app
 
 FROM chef AS planner
@@ -17,5 +16,4 @@ COPY . .
 RUN cargo build --release --package libp2p-server
 
 FROM gcr.io/distroless/cc
-COPY --from=builder /usr/local/bin/libp2p-server /usr/local/bin/libp2p-lookup /usr/local/bin/
 CMD ["libp2p-server"]
diff --git a/misc/server/README.md b/misc/server/README.md
index 0da1bd8abd9..f9a5d65124a 100644
--- a/misc/server/README.md
+++ b/misc/server/README.md
@@ -25,7 +25,6 @@ Options:
   -h, --help                         Print help
 ```
 
-
 ```
 cargo run -- --config ~/.ipfs/config
 
@@ -33,9 +32,3 @@ Local peer id: PeerId("12D3KooWSa1YEeQVSwvoqAMhwjKQ6kqZQckhWPb3RWEGV3sZGU6Z")
 Listening on "/ip4/127.0.0.1/udp/4001/quic"
 [...]
 ```
-
-The Docker container includes [libp2-lookup](https://github.com/mxinden/libp2p-lookup/) to enable adding a proper healthcheck for container startup, e.g.
-
-``` shell
-docker run --health-cmd 'libp2p-lookup direct --address /ip4/127.0.0.1/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa' /home/ipfs/.ipfs:/ipfs ghcr.io/libp2p/rust-libp2p-server --config /ipfs/config
-```
diff --git a/muxers/test-harness/src/lib.rs b/muxers/test-harness/src/lib.rs
index 16c71f414f0..d03bdbdfed7 100644
--- a/muxers/test-harness/src/lib.rs
+++ b/muxers/test-harness/src/lib.rs
@@ -206,7 +206,7 @@ enum Event {
     ProtocolComplete,
 }
 
-impl<'m, M> Stream for Harness<'m, M>
+impl<M> Stream for Harness<'_, M>
 where
     M: StreamMuxer + Unpin,
 {
diff --git a/protocols/autonat/CHANGELOG.md b/protocols/autonat/CHANGELOG.md
index e171412aa58..f1aeda6ac18 100644
--- a/protocols/autonat/CHANGELOG.md
+++ b/protocols/autonat/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 0.13.1
+- Verify that an incoming AutoNAT dial comes from a connected peer. See [PR 5597](https://github.com/libp2p/rust-libp2p/pull/5597).
+
 ## 0.13.0
 
 - Due to the refactor of `Transport` it's no longer required to create a seperate transport for
diff --git a/protocols/autonat/Cargo.toml b/protocols/autonat/Cargo.toml
index 2c01d18dceb..0c0e757641d 100644
--- a/protocols/autonat/Cargo.toml
+++ b/protocols/autonat/Cargo.toml
@@ -3,7 +3,7 @@ name = "libp2p-autonat"
 edition = "2021"
 rust-version = { workspace = true }
 description = "NAT and firewall detection for libp2p"
-version = "0.13.0"
+version = "0.13.1"
 authors = ["David Craven <david@craven.ch>", "Elena Frank <elena.frank@protonmail.com>", "Hannes Furmans <hannes@umgefahren.xyz>"]
 license = "MIT"
 repository = "https://github.com/libp2p/rust-libp2p"
diff --git a/protocols/autonat/src/v1/behaviour/as_client.rs b/protocols/autonat/src/v1/behaviour/as_client.rs
index 8960163ccb3..385dee50ee1 100644
--- a/protocols/autonat/src/v1/behaviour/as_client.rs
+++ b/protocols/autonat/src/v1/behaviour/as_client.rs
@@ -98,7 +98,7 @@ pub(crate) struct AsClient<'a> {
     pub(crate) other_candidates: &'a HashSet<Multiaddr>,
 }
 
-impl<'a> HandleInnerEvent for AsClient<'a> {
+impl HandleInnerEvent for AsClient<'_> {
     fn handle_event(
         &mut self,
         event: request_response::Event<DialRequest, DialResponse>,
@@ -179,7 +179,7 @@ impl<'a> HandleInnerEvent for AsClient<'a> {
     }
 }
 
-impl<'a> AsClient<'a> {
+impl AsClient<'_> {
     pub(crate) fn poll_auto_probe(&mut self, cx: &mut Context<'_>) -> Poll<OutboundProbeEvent> {
         match self.schedule_probe.poll_unpin(cx) {
             Poll::Ready(()) => {
diff --git a/protocols/autonat/src/v1/behaviour/as_server.rs b/protocols/autonat/src/v1/behaviour/as_server.rs
index 3ecdd3ac26e..01148add6e8 100644
--- a/protocols/autonat/src/v1/behaviour/as_server.rs
+++ b/protocols/autonat/src/v1/behaviour/as_server.rs
@@ -91,7 +91,7 @@ pub(crate) struct AsServer<'a> {
     >,
 }
 
-impl<'a> HandleInnerEvent for AsServer<'a> {
+impl HandleInnerEvent for AsServer<'_> {
     fn handle_event(
         &mut self,
         event: request_response::Event<DialRequest, DialResponse>,
@@ -107,6 +107,21 @@ impl<'a> HandleInnerEvent for AsServer<'a> {
                     },
             } => {
                 let probe_id = self.probe_id.next();
+                if !self.connected.contains_key(&peer) {
+                    tracing::debug!(
+                        %peer,
+                        "Reject inbound dial request from peer since it is not connected"
+                    );
+
+                    return VecDeque::from([ToSwarm::GenerateEvent(Event::InboundProbe(
+                        InboundProbeEvent::Error {
+                            probe_id,
+                            peer,
+                            error: InboundProbeError::Response(ResponseError::DialRefused),
+                        },
+                    ))]);
+                }
+
                 match self.resolve_inbound_request(peer, request) {
                     Ok(addrs) => {
                         tracing::debug!(
@@ -193,7 +208,7 @@ impl<'a> HandleInnerEvent for AsServer<'a> {
     }
 }
 
-impl<'a> AsServer<'a> {
+impl AsServer<'_> {
     pub(crate) fn on_outbound_connection(
         &mut self,
         peer: &PeerId,
diff --git a/protocols/autonat/src/v2/client/handler/dial_back.rs b/protocols/autonat/src/v2/client/handler/dial_back.rs
index b94580e69ba..98a41a82504 100644
--- a/protocols/autonat/src/v2/client/handler/dial_back.rs
+++ b/protocols/autonat/src/v2/client/handler/dial_back.rs
@@ -83,6 +83,8 @@ impl ConnectionHandler for Handler {
                     tracing::warn!("Dial back request dropped, too many requests in flight");
                 }
             }
+            // TODO: remove when Rust 1.82 is MSRVprotocols/autonat/src/v2/client/handler/dial_back.rs
+            #[allow(unreachable_patterns)]
             ConnectionEvent::ListenUpgradeError(ListenUpgradeError { error, .. }) => {
                 void::unreachable(error);
             }
diff --git a/protocols/autonat/src/v2/client/handler/dial_request.rs b/protocols/autonat/src/v2/client/handler/dial_request.rs
index 9d2df8ee6b4..85ad176ec30 100644
--- a/protocols/autonat/src/v2/client/handler/dial_request.rs
+++ b/protocols/autonat/src/v2/client/handler/dial_request.rs
@@ -216,6 +216,8 @@ async fn start_stream_handle(
         .map_err(|e| match e {
             StreamUpgradeError::NegotiationFailed => Error::UnsupportedProtocol,
             StreamUpgradeError::Timeout => Error::Io(io::ErrorKind::TimedOut.into()),
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             StreamUpgradeError::Apply(v) => void::unreachable(v),
             StreamUpgradeError::Io(e) => Error::Io(e),
         })?;
diff --git a/protocols/autonat/src/v2/server/behaviour.rs b/protocols/autonat/src/v2/server/behaviour.rs
index 5f7b21d165b..9264c728fe4 100644
--- a/protocols/autonat/src/v2/server/behaviour.rs
+++ b/protocols/autonat/src/v2/server/behaviour.rs
@@ -112,6 +112,8 @@ where
             Either::Left(Either::Left(Err(e))) => {
                 tracing::debug!("dial back error: {e:?}");
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             Either::Left(Either::Right(v)) => void::unreachable(v),
             Either::Right(Either::Left(cmd)) => {
                 let addr = cmd.addr.clone();
diff --git a/protocols/autonat/src/v2/server/handler/dial_request.rs b/protocols/autonat/src/v2/server/handler/dial_request.rs
index 9a3729d4ccf..14ddb153416 100644
--- a/protocols/autonat/src/v2/server/handler/dial_request.rs
+++ b/protocols/autonat/src/v2/server/handler/dial_request.rs
@@ -143,6 +143,8 @@ where
                     );
                 }
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::ListenUpgradeError(ListenUpgradeError { error, .. }) => {
                 tracing::debug!("inbound request failed: {:?}", error);
             }
diff --git a/protocols/autonat/tests/autonatv2.rs b/protocols/autonat/tests/autonatv2.rs
index abd0c4bd8eb..f22a2e51470 100644
--- a/protocols/autonat/tests/autonatv2.rs
+++ b/protocols/autonat/tests/autonatv2.rs
@@ -1,5 +1,6 @@
 use libp2p_autonat::v2::client::{self, Config};
 use libp2p_autonat::v2::server;
+use libp2p_core::multiaddr::Protocol;
 use libp2p_core::transport::TransportError;
 use libp2p_core::Multiaddr;
 use libp2p_swarm::{
@@ -21,17 +22,10 @@ async fn confirm_successful() {
 
     let cor_server_peer = *alice.local_peer_id();
     let cor_client_peer = *bob.local_peer_id();
-    let bob_external_addrs = Arc::new(bob.external_addresses().cloned().collect::<Vec<_>>());
-    let alice_bob_external_addrs = bob_external_addrs.clone();
+    let bob_tcp_listeners = Arc::new(tcp_listeners(&bob));
+    let alice_bob_tcp_listeners = bob_tcp_listeners.clone();
 
     let alice_task = async {
-        let _ = alice
-            .wait(|event| match event {
-                SwarmEvent::NewExternalAddrCandidate { .. } => Some(()),
-                _ => None,
-            })
-            .await;
-
         let (dialed_peer_id, dialed_connection_id) = alice
             .wait(|event| match event {
                 SwarmEvent::Dialing {
@@ -76,10 +70,10 @@ async fn confirm_successful() {
             })
             .await;
 
-        assert_eq!(tested_addr, bob_external_addrs.first().cloned().unwrap());
+        assert_eq!(tested_addr, bob_tcp_listeners.first().cloned().unwrap());
         assert_eq!(data_amount, 0);
         assert_eq!(client, cor_client_peer);
-        assert_eq!(&all_addrs[..], &bob_external_addrs[..]);
+        assert_eq!(&all_addrs[..], &bob_tcp_listeners[..]);
         assert!(result.is_ok(), "Result: {result:?}");
     };
 
@@ -122,7 +116,7 @@ async fn confirm_successful() {
             .await;
         assert_eq!(
             tested_addr,
-            alice_bob_external_addrs.first().cloned().unwrap()
+            alice_bob_tcp_listeners.first().cloned().unwrap()
         );
         assert_eq!(bytes_sent, 0);
         assert_eq!(server, cor_server_peer);
@@ -238,87 +232,84 @@ async fn dial_back_to_non_libp2p() {
     let (mut alice, mut bob) = bootstrap().await;
     let alice_peer_id = *alice.local_peer_id();
 
-    for addr_str in ["/ip4/169.150.247.38/tcp/32", "/ip6/::1/tcp/1000"] {
-        let addr: Multiaddr = addr_str.parse().unwrap();
-        let bob_addr = addr.clone();
-        bob.behaviour_mut()
-            .autonat
-            .on_swarm_event(FromSwarm::NewExternalAddrCandidate(
-                NewExternalAddrCandidate { addr: &addr },
-            ));
-
-        let alice_task = async {
-            let (alice_dialing_peer, alice_conn_id) = alice
-                .wait(|event| match event {
-                    SwarmEvent::Dialing {
-                        peer_id,
-                        connection_id,
-                    } => peer_id.map(|p| (p, connection_id)),
-                    _ => None,
-                })
-                .await;
-            let mut outgoing_conn_error = alice
-                .wait(|event| match event {
-                    SwarmEvent::OutgoingConnectionError {
-                        connection_id,
-                        peer_id: Some(peer_id),
-                        error: DialError::Transport(peers),
-                    } if connection_id == alice_conn_id && peer_id == alice_dialing_peer => {
-                        Some(peers)
-                    }
-                    _ => None,
-                })
-                .await;
-
-            if let Some((multiaddr, TransportError::Other(o))) = outgoing_conn_error.pop() {
-                assert_eq!(
-                    multiaddr,
-                    addr.clone().with_p2p(alice_dialing_peer).unwrap()
-                );
-                let error_string = o.to_string();
-                assert!(
-                    error_string.contains("Connection refused"),
-                    "Correct error string: {error_string} for {addr_str}"
-                );
-            } else {
-                panic!("No outgoing connection errors");
-            }
+    let addr_str = "/ip6/::1/tcp/1000";
+    let addr: Multiaddr = addr_str.parse().unwrap();
+    let bob_addr = addr.clone();
+    bob.behaviour_mut()
+        .autonat
+        .on_swarm_event(FromSwarm::NewExternalAddrCandidate(
+            NewExternalAddrCandidate { addr: &addr },
+        ));
 
-            alice
-                .wait(|event| match event {
-                    SwarmEvent::Behaviour(CombinedServerEvent::Autonat(server::Event {
-                        all_addrs,
-                        tested_addr,
-                        client,
-                        data_amount,
-                        result: Ok(()),
-                    })) if all_addrs == vec![addr.clone()]
-                        && tested_addr == addr
-                        && alice_dialing_peer == client =>
-                    {
-                        Some(data_amount)
-                    }
-                    _ => None,
-                })
-                .await
-        };
-        let bob_task = async {
-            bob.wait(|event| match event {
-                SwarmEvent::Behaviour(CombinedClientEvent::Autonat(client::Event {
+    let alice_task = async {
+        let (alice_dialing_peer, alice_conn_id) = alice
+            .wait(|event| match event {
+                SwarmEvent::Dialing {
+                    peer_id,
+                    connection_id,
+                } => peer_id.map(|p| (p, connection_id)),
+                _ => None,
+            })
+            .await;
+        let mut outgoing_conn_error = alice
+            .wait(|event| match event {
+                SwarmEvent::OutgoingConnectionError {
+                    connection_id,
+                    peer_id: Some(peer_id),
+                    error: DialError::Transport(peers),
+                } if connection_id == alice_conn_id && peer_id == alice_dialing_peer => Some(peers),
+                _ => None,
+            })
+            .await;
+
+        if let Some((multiaddr, TransportError::Other(o))) = outgoing_conn_error.pop() {
+            assert_eq!(
+                multiaddr,
+                addr.clone().with_p2p(alice_dialing_peer).unwrap()
+            );
+            let error_string = o.to_string();
+            assert!(
+                error_string.contains("Connection refused"),
+                "Correct error string: {error_string} for {addr_str}"
+            );
+        } else {
+            panic!("No outgoing connection errors");
+        }
+
+        alice
+            .wait(|event| match event {
+                SwarmEvent::Behaviour(CombinedServerEvent::Autonat(server::Event {
+                    all_addrs,
                     tested_addr,
-                    bytes_sent,
-                    server,
-                    result: Err(_),
-                })) if tested_addr == bob_addr && server == alice_peer_id => Some(bytes_sent),
+                    client,
+                    data_amount,
+                    result: Ok(()),
+                })) if all_addrs == vec![addr.clone()]
+                    && tested_addr == addr
+                    && alice_dialing_peer == client =>
+                {
+                    Some(data_amount)
+                }
                 _ => None,
             })
             .await
-        };
+    };
+    let bob_task = async {
+        bob.wait(|event| match event {
+            SwarmEvent::Behaviour(CombinedClientEvent::Autonat(client::Event {
+                tested_addr,
+                bytes_sent,
+                server,
+                result: Err(_),
+            })) if tested_addr == bob_addr && server == alice_peer_id => Some(bytes_sent),
+            _ => None,
+        })
+        .await
+    };
 
-        let (alice_bytes_sent, bob_bytes_sent) = tokio::join!(alice_task, bob_task);
-        assert_eq!(alice_bytes_sent, bob_bytes_sent);
-        bob.behaviour_mut().autonat.validate_addr(&addr);
-    }
+    let (alice_bytes_sent, bob_bytes_sent) = tokio::join!(alice_task, bob_task);
+    assert_eq!(alice_bytes_sent, bob_bytes_sent);
+    bob.behaviour_mut().autonat.validate_addr(&addr);
 }
 
 #[tokio::test]
@@ -446,7 +437,7 @@ async fn new_client() -> Swarm<CombinedClient> {
             identity.public().clone(),
         )),
     });
-    node.listen().with_tcp_addr_external().await;
+    node.listen().await;
     node
 }
 
@@ -490,13 +481,6 @@ async fn bootstrap() -> (Swarm<CombinedServer>, Swarm<CombinedClient>) {
     let cor_client_peer = *bob.local_peer_id();
 
     let alice_task = async {
-        let _ = alice
-            .wait(|event| match event {
-                SwarmEvent::NewExternalAddrCandidate { .. } => Some(()),
-                _ => None,
-            })
-            .await;
-
         let (dialed_peer_id, dialed_connection_id) = alice
             .wait(|event| match event {
                 SwarmEvent::Dialing {
@@ -566,3 +550,14 @@ async fn bootstrap() -> (Swarm<CombinedServer>, Swarm<CombinedClient>) {
     tokio::join!(alice_task, bob_task);
     (alice, bob)
 }
+
+fn tcp_listeners<T: NetworkBehaviour>(swarm: &Swarm<T>) -> Vec<Multiaddr> {
+    swarm
+        .listeners()
+        .filter(|addr| {
+            addr.iter()
+                .any(|protocol| matches!(protocol, Protocol::Tcp(_)))
+        })
+        .cloned()
+        .collect::<Vec<_>>()
+}
diff --git a/protocols/dcutr/src/behaviour.rs b/protocols/dcutr/src/behaviour.rs
index 574c96205fa..babd56bd28e 100644
--- a/protocols/dcutr/src/behaviour.rs
+++ b/protocols/dcutr/src/behaviour.rs
@@ -314,6 +314,8 @@ impl NetworkBehaviour for Behaviour {
                     .or_default() += 1;
                 self.queued_events.push_back(ToSwarm::Dial { opts });
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             Either::Right(never) => void::unreachable(never),
         };
     }
diff --git a/protocols/dcutr/src/handler/relayed.rs b/protocols/dcutr/src/handler/relayed.rs
index eba58f89313..72af9fec264 100644
--- a/protocols/dcutr/src/handler/relayed.rs
+++ b/protocols/dcutr/src/handler/relayed.rs
@@ -115,6 +115,8 @@ impl Handler {
                 self.attempts += 1;
             }
             // A connection listener denies all incoming substreams, thus none can ever be fully negotiated.
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             future::Either::Right(output) => void::unreachable(output),
         }
     }
@@ -153,6 +155,8 @@ impl Handler {
             <Self as ConnectionHandler>::InboundProtocol,
         >,
     ) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(error.into_inner());
     }
 
@@ -164,6 +168,8 @@ impl Handler {
         >,
     ) {
         let error = match error {
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             StreamUpgradeError::Apply(v) => void::unreachable(v),
             StreamUpgradeError::NegotiationFailed => outbound::Error::Unsupported,
             StreamUpgradeError::Io(e) => outbound::Error::Io(e),
@@ -298,6 +304,8 @@ impl ConnectionHandler for Handler {
             ConnectionEvent::FullyNegotiatedOutbound(fully_negotiated_outbound) => {
                 self.on_fully_negotiated_outbound(fully_negotiated_outbound)
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::ListenUpgradeError(listen_upgrade_error) => {
                 self.on_listen_upgrade_error(listen_upgrade_error)
             }
diff --git a/protocols/gossipsub/CHANGELOG.md b/protocols/gossipsub/CHANGELOG.md
index 2cb25e604da..67662a35979 100644
--- a/protocols/gossipsub/CHANGELOG.md
+++ b/protocols/gossipsub/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.48.0
+
+- Apply `max_transmit_size` to the inner message instead of the final payload.
+  See [PR 5642](https://github.com/libp2p/rust-libp2p/pull/5642).
+
 ## 0.47.1
 
 - Attempt to publish to at least mesh_n peers when flood publish is disabled.
diff --git a/protocols/gossipsub/Cargo.toml b/protocols/gossipsub/Cargo.toml
index 0f8cda8eee5..dbde81eeee0 100644
--- a/protocols/gossipsub/Cargo.toml
+++ b/protocols/gossipsub/Cargo.toml
@@ -3,7 +3,7 @@ name = "libp2p-gossipsub"
 edition = "2021"
 rust-version = { workspace = true }
 description = "Gossipsub protocol for libp2p"
-version = "0.47.1"
+version = "0.48.0"
 authors = ["Age Manning <Age@AgeManning.com>"]
 license = "MIT"
 repository = "https://github.com/libp2p/rust-libp2p"
diff --git a/protocols/gossipsub/src/backoff.rs b/protocols/gossipsub/src/backoff.rs
index b24da318582..4414ffb00e6 100644
--- a/protocols/gossipsub/src/backoff.rs
+++ b/protocols/gossipsub/src/backoff.rs
@@ -48,8 +48,7 @@ pub(crate) struct BackoffStorage {
 
 impl BackoffStorage {
     fn heartbeats(d: &Duration, heartbeat_interval: &Duration) -> usize {
-        ((d.as_nanos() + heartbeat_interval.as_nanos() - 1) / heartbeat_interval.as_nanos())
-            as usize
+        d.as_nanos().div_ceil(heartbeat_interval.as_nanos()) as usize
     }
 
     pub(crate) fn new(
diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs
index f3229c680a2..dc828e00aff 100644
--- a/protocols/gossipsub/src/behaviour.rs
+++ b/protocols/gossipsub/src/behaviour.rs
@@ -591,6 +591,11 @@ where
             .data_transform
             .outbound_transform(&topic, data.clone())?;
 
+        // check that the size doesn't exceed the max transmission size.
+        if transformed_data.len() > self.config.max_transmit_size() {
+            return Err(PublishError::MessageTooLarge);
+        }
+
         let raw_message = self.build_raw_message(topic, transformed_data)?;
 
         // calculate the message id from the un-transformed data
@@ -601,11 +606,6 @@ where
             topic: raw_message.topic.clone(),
         });
 
-        // check that the size doesn't exceed the max transmission size
-        if raw_message.raw_protobuf_len() > self.config.max_transmit_size() {
-            return Err(PublishError::MessageTooLarge);
-        }
-
         // Check the if the message has been published before
         if self.duplicate_cache.contains(&msg_id) {
             // This message has already been seen. We don't re-publish messages that have already
diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs
index 37448a587c0..3c59b6ba7b2 100644
--- a/protocols/gossipsub/src/config.rs
+++ b/protocols/gossipsub/src/config.rs
@@ -177,7 +177,8 @@ impl Config {
 
     /// The maximum byte size for each gossipsub RPC (default is 65536 bytes).
     ///
-    /// This represents the maximum size of the entire protobuf payload. It must be at least
+    /// This represents the maximum size of the published message. It is additionally wrapped
+    /// in a protobuf struct, so the actual wire size may be a bit larger. It must be at least
     /// large enough to support basic control messages. If Peer eXchange is enabled, this
     /// must be large enough to transmit the desired peer information on pruning. It must be at
     /// least 100 bytes. Default is 65536 bytes.
diff --git a/protocols/gossipsub/src/handler.rs b/protocols/gossipsub/src/handler.rs
index c7c3eb2ebe1..7f312355cf7 100644
--- a/protocols/gossipsub/src/handler.rs
+++ b/protocols/gossipsub/src/handler.rs
@@ -520,6 +520,8 @@ impl ConnectionHandler for Handler {
                         ..
                     }) => match protocol {
                         Either::Left(protocol) => handler.on_fully_negotiated_inbound(protocol),
+                        // TODO: remove when Rust 1.82 is MSRV
+                        #[allow(unreachable_patterns)]
                         Either::Right(v) => void::unreachable(v),
                     },
                     ConnectionEvent::FullyNegotiatedOutbound(fully_negotiated_outbound) => {
@@ -531,6 +533,8 @@ impl ConnectionHandler for Handler {
                     }) => {
                         tracing::debug!("Dial upgrade error: Protocol negotiation timeout");
                     }
+                    // TODO: remove when Rust 1.82 is MSRV
+                    #[allow(unreachable_patterns)]
                     ConnectionEvent::DialUpgradeError(DialUpgradeError {
                         error: StreamUpgradeError::Apply(e),
                         ..
diff --git a/protocols/gossipsub/src/transform.rs b/protocols/gossipsub/src/transform.rs
index 6f57d9fc46b..4831f9781b0 100644
--- a/protocols/gossipsub/src/transform.rs
+++ b/protocols/gossipsub/src/transform.rs
@@ -28,6 +28,7 @@
 use crate::{Message, RawMessage, TopicHash};
 
 /// A general trait of transforming a [`RawMessage`] into a [`Message`]. The
+///
 /// [`RawMessage`] is obtained from the wire and the [`Message`] is used to
 /// calculate the [`crate::MessageId`] of the message and is what is sent to the application.
 ///
diff --git a/protocols/kad/CHANGELOG.md b/protocols/kad/CHANGELOG.md
index d0ab7986aad..55d269bf98f 100644
--- a/protocols/kad/CHANGELOG.md
+++ b/protocols/kad/CHANGELOG.md
@@ -1,10 +1,11 @@
 ## 0.47.0
 
-- Expose a kad query facility allowing specify num_results dynamicly.
+- Expose a kad query facility allowing specify num_results dynamicaly.
   See [PR 5555](https://github.com/libp2p/rust-libp2p/pull/5555).
 - Add `mode` getter on `Behaviour`.
   See [PR 5573](https://github.com/libp2p/rust-libp2p/pull/5573).
-  
+- Add `Behavior::find_closest_local_peers()`.
+  See [PR 5645](https://github.com/libp2p/rust-libp2p/pull/5645).
 
 ## 0.46.2
 
diff --git a/protocols/kad/src/behaviour.rs b/protocols/kad/src/behaviour.rs
index 0b15e507ba4..f577971167f 100644
--- a/protocols/kad/src/behaviour.rs
+++ b/protocols/kad/src/behaviour.rs
@@ -771,7 +771,8 @@ where
         self.queries.add_iter_closest(target, peer_keys, info)
     }
 
-    /// Returns closest peers to the given key; takes peers from local routing table only.
+    /// Returns all peers ordered by distance to the given key; takes peers from local routing table
+    /// only.
     pub fn get_closest_local_peers<'a, K: Clone>(
         &'a mut self,
         key: &'a kbucket::Key<K>,
@@ -779,6 +780,23 @@ where
         self.kbuckets.closest_keys(key)
     }
 
+    /// Finds the closest peers to a `key` in the context of a request by the `source` peer, such
+    /// that the `source` peer is never included in the result.
+    ///
+    /// Takes peers from local routing table only. Only returns number of peers equal to configured
+    /// replication factor.
+    pub fn find_closest_local_peers<'a, K: Clone>(
+        &'a mut self,
+        key: &'a kbucket::Key<K>,
+        source: &'a PeerId,
+    ) -> impl Iterator<Item = KadPeer> + 'a {
+        self.kbuckets
+            .closest(key)
+            .filter(move |e| e.node.key.preimage() != source)
+            .take(self.queries.config().replication_factor.get())
+            .map(KadPeer::from)
+    }
+
     /// Performs a lookup for a record in the DHT.
     ///
     /// The result of this operation is delivered in a
@@ -1212,22 +1230,6 @@ where
         }
     }
 
-    /// Finds the closest peers to a `target` in the context of a request by
-    /// the `source` peer, such that the `source` peer is never included in the
-    /// result.
-    fn find_closest<T: Clone>(
-        &mut self,
-        target: &kbucket::Key<T>,
-        source: &PeerId,
-    ) -> Vec<KadPeer> {
-        self.kbuckets
-            .closest(target)
-            .filter(|e| e.node.key.preimage() != source)
-            .take(self.queries.config().replication_factor.get())
-            .map(KadPeer::from)
-            .collect()
-    }
-
     /// Collects all peers who are known to be providers of the value for a given `Multihash`.
     fn provider_peers(&mut self, key: &record::Key, source: &PeerId) -> Vec<KadPeer> {
         let kbuckets = &mut self.kbuckets;
@@ -2300,7 +2302,9 @@ where
             }
 
             HandlerEvent::FindNodeReq { key, request_id } => {
-                let closer_peers = self.find_closest(&kbucket::Key::new(key), &source);
+                let closer_peers = self
+                    .find_closest_local_peers(&kbucket::Key::new(key), &source)
+                    .collect::<Vec<_>>();
 
                 self.queued_events
                     .push_back(ToSwarm::GenerateEvent(Event::InboundRequest {
@@ -2328,7 +2332,9 @@ where
 
             HandlerEvent::GetProvidersReq { key, request_id } => {
                 let provider_peers = self.provider_peers(&key, &source);
-                let closer_peers = self.find_closest(&kbucket::Key::new(key), &source);
+                let closer_peers = self
+                    .find_closest_local_peers(&kbucket::Key::new(key), &source)
+                    .collect::<Vec<_>>();
 
                 self.queued_events
                     .push_back(ToSwarm::GenerateEvent(Event::InboundRequest {
@@ -2422,7 +2428,9 @@ where
                     None => None,
                 };
 
-                let closer_peers = self.find_closest(&kbucket::Key::new(key), &source);
+                let closer_peers = self
+                    .find_closest_local_peers(&kbucket::Key::new(key), &source)
+                    .collect::<Vec<_>>();
 
                 self.queued_events
                     .push_back(ToSwarm::GenerateEvent(Event::InboundRequest {
@@ -3353,7 +3361,7 @@ pub struct QueryMut<'a> {
     query: &'a mut Query,
 }
 
-impl<'a> QueryMut<'a> {
+impl QueryMut<'_> {
     pub fn id(&self) -> QueryId {
         self.query.id()
     }
@@ -3383,7 +3391,7 @@ pub struct QueryRef<'a> {
     query: &'a Query,
 }
 
-impl<'a> QueryRef<'a> {
+impl QueryRef<'_> {
     pub fn id(&self) -> QueryId {
         self.query.id()
     }
diff --git a/protocols/kad/src/handler.rs b/protocols/kad/src/handler.rs
index 5e7c2e21b8b..17c483da709 100644
--- a/protocols/kad/src/handler.rs
+++ b/protocols/kad/src/handler.rs
@@ -501,6 +501,8 @@ impl Handler {
         // is a `Void`.
         let protocol = match protocol {
             future::Either::Left(p) => p,
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             future::Either::Right(p) => void::unreachable(p),
         };
 
diff --git a/protocols/kad/src/lib.rs b/protocols/kad/src/lib.rs
index 681d135f79b..060bfc518e4 100644
--- a/protocols/kad/src/lib.rs
+++ b/protocols/kad/src/lib.rs
@@ -69,7 +69,7 @@ pub use behaviour::{
 pub use kbucket::{
     Distance as KBucketDistance, EntryView, KBucketRef, Key as KBucketKey, NodeStatus,
 };
-pub use protocol::ConnectionType;
+pub use protocol::{ConnectionType, KadPeer};
 pub use query::QueryId;
 pub use record::{store, Key as RecordKey, ProviderRecord, Record};
 
diff --git a/protocols/kad/src/query/peers/fixed.rs b/protocols/kad/src/query/peers/fixed.rs
index b34f7516801..2d0b312454d 100644
--- a/protocols/kad/src/query/peers/fixed.rs
+++ b/protocols/kad/src/query/peers/fixed.rs
@@ -25,7 +25,7 @@ use std::{collections::hash_map::Entry, num::NonZeroUsize, vec};
 
 /// A peer iterator for a fixed set of peers.
 pub(crate) struct FixedPeersIter {
-    /// Ther permitted parallelism, i.e. number of pending results.
+    /// The permitted parallelism, i.e. number of pending results.
     parallelism: NonZeroUsize,
 
     /// The state of peers emitted by the iterator.
diff --git a/protocols/perf/Dockerfile b/protocols/perf/Dockerfile
index 1bd846cc228..f68ea6ef211 100644
--- a/protocols/perf/Dockerfile
+++ b/protocols/perf/Dockerfile
@@ -1,5 +1,5 @@
 # syntax=docker/dockerfile:1.5-labs
-FROM rust:1.75.0 as builder
+FROM rust:1.81.0 as builder
 
 # Run with access to the target cache to speed up builds
 WORKDIR /workspace
diff --git a/protocols/perf/src/client/handler.rs b/protocols/perf/src/client/handler.rs
index 2a2c5499fc2..55fafad7fcc 100644
--- a/protocols/perf/src/client/handler.rs
+++ b/protocols/perf/src/client/handler.rs
@@ -112,6 +112,8 @@ impl ConnectionHandler for Handler {
         >,
     ) {
         match event {
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound {
                 protocol, ..
             }) => void::unreachable(protocol),
@@ -144,6 +146,8 @@ impl ConnectionHandler for Handler {
                         result: Err(error.into()),
                     }));
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::ListenUpgradeError(ListenUpgradeError { info: (), error }) => {
                 void::unreachable(error)
             }
diff --git a/protocols/perf/src/server/handler.rs b/protocols/perf/src/server/handler.rs
index ddfe8f881e5..4cb535a452c 100644
--- a/protocols/perf/src/server/handler.rs
+++ b/protocols/perf/src/server/handler.rs
@@ -73,6 +73,8 @@ impl ConnectionHandler for Handler {
     }
 
     fn on_behaviour_event(&mut self, v: Self::FromBehaviour) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(v)
     }
 
@@ -98,16 +100,22 @@ impl ConnectionHandler for Handler {
                     tracing::warn!("Dropping inbound stream because we are at capacity");
                 }
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::FullyNegotiatedOutbound(FullyNegotiatedOutbound { info, .. }) => {
                 void::unreachable(info)
             }
 
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::DialUpgradeError(DialUpgradeError { info, .. }) => {
                 void::unreachable(info)
             }
             ConnectionEvent::AddressChange(_)
             | ConnectionEvent::LocalProtocolsChange(_)
             | ConnectionEvent::RemoteProtocolsChange(_) => {}
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::ListenUpgradeError(ListenUpgradeError { info: (), error }) => {
                 void::unreachable(error)
             }
diff --git a/protocols/ping/Cargo.toml b/protocols/ping/Cargo.toml
index 66775d3ba8d..794ab54ba42 100644
--- a/protocols/ping/Cargo.toml
+++ b/protocols/ping/Cargo.toml
@@ -23,11 +23,11 @@ tracing = { workspace = true }
 void = "1.0"
 
 [dev-dependencies]
-async-std = "1.6.2"
 libp2p-swarm = { workspace = true, features = ["macros"] }
 libp2p-swarm-test = { path = "../../swarm-test" }
 quickcheck = { workspace = true }
 tracing-subscriber = { workspace = true, features = ["env-filter"] }
+tokio = {workspace = true, features = ["rt", "macros"]}
 
 # Passing arguments to the docsrs builder in order to properly document cfg's.
 # More information: https://docs.rs/about/builds#cross-compiling
diff --git a/protocols/ping/src/handler.rs b/protocols/ping/src/handler.rs
index 2816cdc4048..7b36b2d4b3d 100644
--- a/protocols/ping/src/handler.rs
+++ b/protocols/ping/src/handler.rs
@@ -210,6 +210,8 @@ impl Handler {
                     "ping protocol negotiation timed out",
                 )),
             },
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             StreamUpgradeError::Apply(e) => void::unreachable(e),
             StreamUpgradeError::Io(e) => Failure::Other { error: Box::new(e) },
         };
diff --git a/protocols/ping/src/protocol.rs b/protocols/ping/src/protocol.rs
index 566e5e258e2..101c219aac4 100644
--- a/protocols/ping/src/protocol.rs
+++ b/protocols/ping/src/protocol.rs
@@ -44,7 +44,6 @@ pub const PROTOCOL_NAME: StreamProtocol = StreamProtocol::new("/ipfs/ping/1.0.0"
 /// >           Nagle's algorithm, delayed acks and similar configuration options
 /// >           which can affect latencies especially on otherwise low-volume
 /// >           connections.
-
 const PING_SIZE: usize = 32;
 
 /// Sends a ping and waits for the pong.
@@ -90,8 +89,8 @@ mod tests {
         Endpoint,
     };
 
-    #[test]
-    fn ping_pong() {
+    #[tokio::test]
+    async fn ping_pong() {
         let mem_addr = multiaddr![Memory(thread_rng().gen::<u64>())];
         let mut transport = MemoryTransport::new().boxed();
         transport.listen_on(ListenerId::next(), mem_addr).unwrap();
@@ -102,27 +101,25 @@ mod tests {
             .and_then(|ev| ev.into_new_address())
             .expect("MemoryTransport not listening on an address!");
 
-        async_std::task::spawn(async move {
+        tokio::spawn(async move {
             let transport_event = transport.next().await.unwrap();
             let (listener_upgrade, _) = transport_event.into_incoming().unwrap();
             let conn = listener_upgrade.await.unwrap();
             recv_ping(conn).await.unwrap();
         });
 
-        async_std::task::block_on(async move {
-            let c = MemoryTransport::new()
-                .dial(
-                    listener_addr,
-                    DialOpts {
-                        role: Endpoint::Dialer,
-                        port_use: PortUse::Reuse,
-                    },
-                )
-                .unwrap()
-                .await
-                .unwrap();
-            let (_, rtt) = send_ping(c).await.unwrap();
-            assert!(rtt > Duration::from_secs(0));
-        });
+        let c = MemoryTransport::new()
+            .dial(
+                listener_addr,
+                DialOpts {
+                    role: Endpoint::Dialer,
+                    port_use: PortUse::Reuse,
+                },
+            )
+            .unwrap()
+            .await
+            .unwrap();
+        let (_, rtt) = send_ping(c).await.unwrap();
+        assert!(rtt > Duration::from_secs(0));
     }
 }
diff --git a/protocols/ping/tests/ping.rs b/protocols/ping/tests/ping.rs
index 3ca469f16a8..0752b1fced9 100644
--- a/protocols/ping/tests/ping.rs
+++ b/protocols/ping/tests/ping.rs
@@ -27,15 +27,15 @@ use libp2p_swarm_test::SwarmExt;
 use quickcheck::*;
 use std::{num::NonZeroU8, time::Duration};
 
-#[test]
-fn ping_pong() {
+#[tokio::test]
+async fn ping_pong() {
     fn prop(count: NonZeroU8) {
         let cfg = ping::Config::new().with_interval(Duration::from_millis(10));
 
         let mut swarm1 = Swarm::new_ephemeral(|_| ping::Behaviour::new(cfg.clone()));
         let mut swarm2 = Swarm::new_ephemeral(|_| ping::Behaviour::new(cfg.clone()));
 
-        async_std::task::block_on(async {
+        tokio::spawn(async move {
             swarm1.listen().with_memory_addr_external().await;
             swarm2.connect(&mut swarm1).await;
 
@@ -61,16 +61,16 @@ fn assert_ping_rtt_less_than_50ms(e: ping::Event) {
     assert!(rtt < Duration::from_millis(50))
 }
 
-#[test]
-fn unsupported_doesnt_fail() {
+#[tokio::test]
+async fn unsupported_doesnt_fail() {
     let mut swarm1 = Swarm::new_ephemeral(|_| dummy::Behaviour);
     let mut swarm2 = Swarm::new_ephemeral(|_| ping::Behaviour::new(ping::Config::new()));
 
-    let result = async_std::task::block_on(async {
+    let result = {
         swarm1.listen().with_memory_addr_external().await;
         swarm2.connect(&mut swarm1).await;
         let swarm1_peer_id = *swarm1.local_peer_id();
-        async_std::task::spawn(swarm1.loop_on_next());
+        tokio::spawn(swarm1.loop_on_next());
 
         loop {
             match swarm2.next_swarm_event().await {
@@ -89,7 +89,7 @@ fn unsupported_doesnt_fail() {
                 _ => {}
             }
         }
-    });
+    };
 
     result.expect("node with ping should not fail connection due to unsupported protocol");
 }
diff --git a/protocols/relay/src/behaviour.rs b/protocols/relay/src/behaviour.rs
index 463febf9f2f..46419ae64e3 100644
--- a/protocols/relay/src/behaviour.rs
+++ b/protocols/relay/src/behaviour.rs
@@ -366,6 +366,8 @@ impl NetworkBehaviour for Behaviour {
     ) {
         let event = match event {
             Either::Left(e) => e,
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             Either::Right(v) => void::unreachable(v),
         };
 
diff --git a/protocols/relay/src/behaviour/handler.rs b/protocols/relay/src/behaviour/handler.rs
index 92557287099..23e90f4b3f8 100644
--- a/protocols/relay/src/behaviour/handler.rs
+++ b/protocols/relay/src/behaviour/handler.rs
@@ -449,6 +449,8 @@ impl Handler {
             StreamUpgradeError::Timeout => outbound_stop::Error::Io(io::ErrorKind::TimedOut.into()),
             StreamUpgradeError::NegotiationFailed => outbound_stop::Error::Unsupported,
             StreamUpgradeError::Io(e) => outbound_stop::Error::Io(e),
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             StreamUpgradeError::Apply(v) => void::unreachable(v),
         };
 
diff --git a/protocols/relay/src/priv_client.rs b/protocols/relay/src/priv_client.rs
index f8d1d9c9eb2..8bbc813ec4c 100644
--- a/protocols/relay/src/priv_client.rs
+++ b/protocols/relay/src/priv_client.rs
@@ -236,6 +236,8 @@ impl NetworkBehaviour for Behaviour {
     ) {
         let handler_event = match handler_event {
             Either::Left(e) => e,
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             Either::Right(v) => void::unreachable(v),
         };
 
diff --git a/protocols/relay/src/priv_client/handler.rs b/protocols/relay/src/priv_client/handler.rs
index 662d63cc742..05fdd5673ae 100644
--- a/protocols/relay/src/priv_client/handler.rs
+++ b/protocols/relay/src/priv_client/handler.rs
@@ -445,6 +445,8 @@ impl ConnectionHandler for Handler {
                     let _ = next.send(Ok(ev.protocol));
                 }
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::ListenUpgradeError(ev) => void::unreachable(ev.error),
             ConnectionEvent::DialUpgradeError(ev) => {
                 if let Some(next) = self.pending_streams.pop_front() {
@@ -583,6 +585,8 @@ fn into_reserve_error(e: StreamUpgradeError<Void>) -> outbound_hop::ReserveError
         StreamUpgradeError::Timeout => {
             outbound_hop::ReserveError::Io(io::ErrorKind::TimedOut.into())
         }
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         StreamUpgradeError::Apply(never) => void::unreachable(never),
         StreamUpgradeError::NegotiationFailed => outbound_hop::ReserveError::Unsupported,
         StreamUpgradeError::Io(e) => outbound_hop::ReserveError::Io(e),
@@ -594,6 +598,8 @@ fn into_connect_error(e: StreamUpgradeError<Void>) -> outbound_hop::ConnectError
         StreamUpgradeError::Timeout => {
             outbound_hop::ConnectError::Io(io::ErrorKind::TimedOut.into())
         }
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         StreamUpgradeError::Apply(never) => void::unreachable(never),
         StreamUpgradeError::NegotiationFailed => outbound_hop::ConnectError::Unsupported,
         StreamUpgradeError::Io(e) => outbound_hop::ConnectError::Io(e),
diff --git a/protocols/request-response/src/cbor.rs b/protocols/request-response/src/cbor.rs
index 44d82be2630..a27d069e758 100644
--- a/protocols/request-response/src/cbor.rs
+++ b/protocols/request-response/src/cbor.rs
@@ -143,6 +143,8 @@ mod codec {
 
     fn decode_into_io_error(err: cbor4ii::serde::DecodeError<Infallible>) -> io::Error {
         match err {
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             cbor4ii::serde::DecodeError::Core(DecodeError::Read(e)) => {
                 io::Error::new(io::ErrorKind::Other, e)
             }
diff --git a/protocols/request-response/src/handler.rs b/protocols/request-response/src/handler.rs
index f0467593f85..0591b37dc30 100644
--- a/protocols/request-response/src/handler.rs
+++ b/protocols/request-response/src/handler.rs
@@ -240,6 +240,8 @@ where
                 self.pending_events
                     .push_back(Event::OutboundUnsupportedProtocols(message.request_id));
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             StreamUpgradeError::Apply(e) => void::unreachable(e),
             StreamUpgradeError::Io(e) => {
                 self.pending_events.push_back(Event::OutboundStreamFailed {
@@ -256,6 +258,8 @@ where
             <Self as ConnectionHandler>::InboundProtocol,
         >,
     ) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(error)
     }
 }
@@ -484,6 +488,8 @@ where
             ConnectionEvent::DialUpgradeError(dial_upgrade_error) => {
                 self.on_dial_upgrade_error(dial_upgrade_error)
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::ListenUpgradeError(listen_upgrade_error) => {
                 self.on_listen_upgrade_error(listen_upgrade_error)
             }
diff --git a/protocols/stream/src/handler.rs b/protocols/stream/src/handler.rs
index f63b93c1761..bf80e30c3c6 100644
--- a/protocols/stream/src/handler.rs
+++ b/protocols/stream/src/handler.rs
@@ -96,6 +96,8 @@ impl ConnectionHandler for Handler {
     }
 
     fn on_behaviour_event(&mut self, event: Self::FromBehaviour) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(event)
     }
 
@@ -143,6 +145,8 @@ impl ConnectionHandler for Handler {
                     swarm::StreamUpgradeError::Timeout => {
                         OpenStreamError::Io(io::Error::from(io::ErrorKind::TimedOut))
                     }
+                    // TODO: remove when Rust 1.82 is MSRV
+                    #[allow(unreachable_patterns)]
                     swarm::StreamUpgradeError::Apply(v) => void::unreachable(v),
                     swarm::StreamUpgradeError::NegotiationFailed => {
                         OpenStreamError::UnsupportedProtocol(p)
diff --git a/protocols/upnp/CHANGELOG.md b/protocols/upnp/CHANGELOG.md
index 21e90f9534b..d9c24f8efcc 100644
--- a/protocols/upnp/CHANGELOG.md
+++ b/protocols/upnp/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.3.1
+- update igd-next to 0.15.1.
+  See [PR XXXX](https://github.com/libp2p/rust-libp2p/pull/XXXX).
+
 ## 0.3.0
 
 <!-- Update to libp2p-swarm v0.45.0 -->
diff --git a/protocols/upnp/Cargo.toml b/protocols/upnp/Cargo.toml
index e9c7414236d..209733f53e6 100644
--- a/protocols/upnp/Cargo.toml
+++ b/protocols/upnp/Cargo.toml
@@ -3,7 +3,7 @@ name = "libp2p-upnp"
 edition = "2021"
 rust-version = "1.60.0"
 description = "UPnP support for libp2p transports"
-version = "0.3.0"
+version = "0.3.1"
 license = "MIT"
 repository = "https://github.com/libp2p/rust-libp2p"
 keywords = ["peer-to-peer", "libp2p", "networking"]
@@ -13,7 +13,7 @@ publish = true
 [dependencies]
 futures = { workspace = true }
 futures-timer = "3.0.3"
-igd-next = "0.14.3"
+igd-next = "0.15.1"
 libp2p-core = { workspace = true }
 libp2p-swarm = { workspace = true }
 tokio = { workspace = true, default-features = false, features = ["rt"], optional = true }
diff --git a/scripts/build-interop-image.sh b/scripts/build-interop-image.sh
index 28a8db9188d..ad6ef78b153 100755
--- a/scripts/build-interop-image.sh
+++ b/scripts/build-interop-image.sh
@@ -6,13 +6,13 @@ CACHE_TO=""
 
 # If we have credentials, write to cache
 if [[ -n "${AWS_SECRET_ACCESS_KEY}" ]]; then
-  CACHE_TO="--cache-to   type=s3,mode=max,bucket=libp2p-by-tf-aws-bootstrap,region=us-east-1,prefix=buildCache,name=${FLAVOUR}-rust-libp2p-head"
+  CACHE_TO="--cache-to   type=s3,mode=max,bucket=${AWS_BUCKET_NAME},region=us-east-1,prefix=buildCache,name=${FLAVOUR}-rust-libp2p-head"
 fi
 
 docker buildx build \
   --load \
   $CACHE_TO \
-  --cache-from type=s3,mode=max,bucket=libp2p-by-tf-aws-bootstrap,region=us-east-1,prefix=buildCache,name=${FLAVOUR}-rust-libp2p-head \
+  --cache-from type=s3,mode=max,bucket=${AWS_BUCKET_NAME},region=us-east-1,prefix=buildCache,name=${FLAVOUR}-rust-libp2p-head \
   -t ${FLAVOUR}-rust-libp2p-head \
   . \
   -f interop-tests/Dockerfile.${FLAVOUR}
diff --git a/swarm-test/CHANGELOG.md b/swarm-test/CHANGELOG.md
index 98027fcbea2..5700460b3a6 100644
--- a/swarm-test/CHANGELOG.md
+++ b/swarm-test/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 0.5.0
+
+- Add `tokio` runtime support and make `tokio` and `async-std` runtimes optional behind features.
+  See [PR 5551].
+
+[PR 5551]: https://github.com/libp2p/rust-libp2p/pull/5551
+
 ## 0.4.0
 
 <!-- Update to libp2p-swarm v0.45.0 -->
diff --git a/swarm-test/Cargo.toml b/swarm-test/Cargo.toml
index b285da34f87..7ac7c900deb 100644
--- a/swarm-test/Cargo.toml
+++ b/swarm-test/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "libp2p-swarm-test"
-version = "0.4.0"
+version = "0.5.0"
 edition = "2021"
 rust-version = { workspace = true }
 license = "MIT"
@@ -16,13 +16,19 @@ async-trait = "0.1.80"
 libp2p-core = { workspace = true }
 libp2p-identity = { workspace = true, features = ["rand"] }
 libp2p-plaintext = { workspace = true }
-libp2p-swarm = { workspace = true, features = ["async-std"] }
-libp2p-tcp = { workspace = true, features = ["async-io"] }
+libp2p-swarm = { workspace = true }
+libp2p-tcp = { workspace = true }
 libp2p-yamux = { workspace = true }
 futures = { workspace = true }
 rand = "0.8.5"
 tracing = { workspace = true }
 futures-timer = "3.0.3"
 
+[features]
+default = ["async-std"]
+
+async-std = ["libp2p-swarm/async-std", "libp2p-tcp/async-io"]
+tokio = ["libp2p-swarm/tokio", "libp2p-tcp/tokio"]
+
 [lints]
 workspace = true
diff --git a/swarm-test/src/lib.rs b/swarm-test/src/lib.rs
index 48f5bcbf4ef..bcab6e5b700 100644
--- a/swarm-test/src/lib.rs
+++ b/swarm-test/src/lib.rs
@@ -21,14 +21,10 @@
 use async_trait::async_trait;
 use futures::future::{BoxFuture, Either};
 use futures::{FutureExt, StreamExt};
-use libp2p_core::{
-    multiaddr::Protocol, transport::MemoryTransport, upgrade::Version, Multiaddr, Transport,
-};
-use libp2p_identity::{Keypair, PeerId};
-use libp2p_plaintext as plaintext;
+use libp2p_core::{multiaddr::Protocol, Multiaddr};
+use libp2p_identity::PeerId;
 use libp2p_swarm::dial_opts::PeerCondition;
-use libp2p_swarm::{self as swarm, dial_opts::DialOpts, NetworkBehaviour, Swarm, SwarmEvent};
-use libp2p_yamux as yamux;
+use libp2p_swarm::{dial_opts::DialOpts, NetworkBehaviour, Swarm, SwarmEvent};
 use std::fmt::Debug;
 use std::future::IntoFuture;
 use std::time::Duration;
@@ -38,12 +34,23 @@ use std::time::Duration;
 pub trait SwarmExt {
     type NB: NetworkBehaviour;
 
-    /// Create a new [`Swarm`] with an ephemeral identity.
+    /// Create a new [`Swarm`] with an ephemeral identity and the `async-std` runtime.
     ///
-    /// The swarm will use a [`MemoryTransport`] together with a [`plaintext::Config`] authentication layer and
-    /// [`yamux::Config`] as the multiplexer. However, these details should not be relied upon by the test
+    /// The swarm will use a [`libp2p_core::transport::MemoryTransport`] together with a [`libp2p_plaintext::Config`] authentication layer and
+    /// [`libp2p_yamux::Config`] as the multiplexer. However, these details should not be relied upon by the test
     /// and may change at any time.
-    fn new_ephemeral(behaviour_fn: impl FnOnce(Keypair) -> Self::NB) -> Self
+    #[cfg(feature = "async-std")]
+    fn new_ephemeral(behaviour_fn: impl FnOnce(libp2p_identity::Keypair) -> Self::NB) -> Self
+    where
+        Self: Sized;
+
+    /// Create a new [`Swarm`] with an ephemeral identity and the `tokio` runtime.
+    ///
+    /// The swarm will use a [`libp2p_core::transport::MemoryTransport`] together with a [`libp2p_plaintext::Config`] authentication layer and
+    /// [`libp2p_yamux::Config`] as the multiplexer. However, these details should not be relied upon by the test
+    /// and may change at any time.
+    #[cfg(feature = "tokio")]
+    fn new_ephemeral_tokio(behaviour_fn: impl FnOnce(libp2p_identity::Keypair) -> Self::NB) -> Self
     where
         Self: Sized;
 
@@ -200,18 +207,50 @@ where
 {
     type NB = B;
 
-    fn new_ephemeral(behaviour_fn: impl FnOnce(Keypair) -> Self::NB) -> Self
+    #[cfg(feature = "async-std")]
+    fn new_ephemeral(behaviour_fn: impl FnOnce(libp2p_identity::Keypair) -> Self::NB) -> Self
     where
         Self: Sized,
     {
+        use libp2p_core::{transport::MemoryTransport, upgrade::Version, Transport as _};
+        use libp2p_identity::Keypair;
+
         let identity = Keypair::generate_ed25519();
         let peer_id = PeerId::from(identity.public());
 
         let transport = MemoryTransport::default()
             .or_transport(libp2p_tcp::async_io::Transport::default())
             .upgrade(Version::V1)
-            .authenticate(plaintext::Config::new(&identity))
-            .multiplex(yamux::Config::default())
+            .authenticate(libp2p_plaintext::Config::new(&identity))
+            .multiplex(libp2p_yamux::Config::default())
+            .timeout(Duration::from_secs(20))
+            .boxed();
+
+        Swarm::new(
+            transport,
+            behaviour_fn(identity),
+            peer_id,
+            libp2p_swarm::Config::with_async_std_executor()
+                .with_idle_connection_timeout(Duration::from_secs(5)), // Some tests need connections to be kept alive beyond what the individual behaviour configures.,
+        )
+    }
+
+    #[cfg(feature = "tokio")]
+    fn new_ephemeral_tokio(behaviour_fn: impl FnOnce(libp2p_identity::Keypair) -> Self::NB) -> Self
+    where
+        Self: Sized,
+    {
+        use libp2p_core::{transport::MemoryTransport, upgrade::Version, Transport as _};
+        use libp2p_identity::Keypair;
+
+        let identity = Keypair::generate_ed25519();
+        let peer_id = PeerId::from(identity.public());
+
+        let transport = MemoryTransport::default()
+            .or_transport(libp2p_tcp::tokio::Transport::default())
+            .upgrade(Version::V1)
+            .authenticate(libp2p_plaintext::Config::new(&identity))
+            .multiplex(libp2p_yamux::Config::default())
             .timeout(Duration::from_secs(20))
             .boxed();
 
@@ -219,7 +258,7 @@ where
             transport,
             behaviour_fn(identity),
             peer_id,
-            swarm::Config::with_async_std_executor()
+            libp2p_swarm::Config::with_tokio_executor()
                 .with_idle_connection_timeout(Duration::from_secs(5)), // Some tests need connections to be kept alive beyond what the individual behaviour configures.,
         )
     }
diff --git a/swarm/CHANGELOG.md b/swarm/CHANGELOG.md
index e7931a60de2..c5d10872d40 100644
--- a/swarm/CHANGELOG.md
+++ b/swarm/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.45.2
+
+- Don't report `NewExternalAddrCandidate` for confirmed external addresses.
+  See [PR 5582](https://github.com/libp2p/rust-libp2p/pull/5582).
+
 ## 0.45.1
 
 - Update `libp2p-swarm-derive` to version `0.35.0`, see [PR 5545]
diff --git a/swarm/Cargo.toml b/swarm/Cargo.toml
index 3d0b1a84eee..cdee67f3fb3 100644
--- a/swarm/Cargo.toml
+++ b/swarm/Cargo.toml
@@ -3,7 +3,7 @@ name = "libp2p-swarm"
 edition = "2021"
 rust-version = { workspace = true }
 description = "The libp2p swarm"
-version = "0.45.1"
+version = "0.45.2"
 authors = ["Parity Technologies <admin@parity.io>"]
 license = "MIT"
 repository = "https://github.com/libp2p/rust-libp2p"
diff --git a/swarm/src/behaviour/toggle.rs b/swarm/src/behaviour/toggle.rs
index 398c919ae86..5d72534c91e 100644
--- a/swarm/src/behaviour/toggle.rs
+++ b/swarm/src/behaviour/toggle.rs
@@ -210,6 +210,8 @@ where
     ) {
         let out = match out {
             future::Either::Left(out) => out,
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             future::Either::Right(v) => void::unreachable(v),
         };
 
@@ -251,6 +253,8 @@ where
 
         let err = match err {
             Either::Left(e) => e,
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             Either::Right(v) => void::unreachable(v),
         };
 
diff --git a/swarm/src/connection.rs b/swarm/src/connection.rs
index 603a5b0d7c4..859d138b83a 100644
--- a/swarm/src/connection.rs
+++ b/swarm/src/connection.rs
@@ -516,7 +516,7 @@ pub(crate) struct IncomingInfo<'a> {
     pub(crate) send_back_addr: &'a Multiaddr,
 }
 
-impl<'a> IncomingInfo<'a> {
+impl IncomingInfo<'_> {
     /// Builds the [`ConnectedPoint`] corresponding to the incoming connection.
     pub(crate) fn create_connected_point(&self) -> ConnectedPoint {
         ConnectedPoint::Listener {
@@ -1189,10 +1189,14 @@ mod tests {
             >,
         ) {
             match event {
+                // TODO: remove when Rust 1.82 is MSRV
+                #[allow(unreachable_patterns)]
                 ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound {
                     protocol,
                     ..
                 }) => void::unreachable(protocol),
+                // TODO: remove when Rust 1.82 is MSRV
+                #[allow(unreachable_patterns)]
                 ConnectionEvent::FullyNegotiatedOutbound(FullyNegotiatedOutbound {
                     protocol,
                     ..
@@ -1200,6 +1204,8 @@ mod tests {
                 ConnectionEvent::DialUpgradeError(DialUpgradeError { error, .. }) => {
                     self.error = Some(error)
                 }
+                // TODO: remove when Rust 1.82 is MSRV
+                #[allow(unreachable_patterns)]
                 ConnectionEvent::AddressChange(_)
                 | ConnectionEvent::ListenUpgradeError(_)
                 | ConnectionEvent::LocalProtocolsChange(_)
@@ -1208,6 +1214,8 @@ mod tests {
         }
 
         fn on_behaviour_event(&mut self, event: Self::FromBehaviour) {
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             void::unreachable(event)
         }
 
@@ -1283,6 +1291,8 @@ mod tests {
         }
 
         fn on_behaviour_event(&mut self, event: Self::FromBehaviour) {
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             void::unreachable(event)
         }
 
diff --git a/swarm/src/connection/pool/task.rs b/swarm/src/connection/pool/task.rs
index 08674fd2ee5..13977a17b85 100644
--- a/swarm/src/connection/pool/task.rs
+++ b/swarm/src/connection/pool/task.rs
@@ -105,6 +105,8 @@ pub(crate) async fn new_for_pending_outgoing_connection(
                 })
                 .await;
         }
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         Either::Left((Ok(v), _)) => void::unreachable(v),
         Either::Right((Ok((address, output, errors)), _)) => {
             let _ = events
@@ -143,6 +145,8 @@ pub(crate) async fn new_for_pending_incoming_connection<TFut>(
                 })
                 .await;
         }
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         Either::Left((Ok(v), _)) => void::unreachable(v),
         Either::Right((Ok(output), _)) => {
             let _ = events
diff --git a/swarm/src/dummy.rs b/swarm/src/dummy.rs
index 6e1b4d56eb9..0bd8c06862d 100644
--- a/swarm/src/dummy.rs
+++ b/swarm/src/dummy.rs
@@ -49,6 +49,8 @@ impl NetworkBehaviour for Behaviour {
         _: ConnectionId,
         event: THandlerOutEvent<Self>,
     ) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(event)
     }
 
@@ -76,6 +78,8 @@ impl crate::handler::ConnectionHandler for ConnectionHandler {
     }
 
     fn on_behaviour_event(&mut self, event: Self::FromBehaviour) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(event)
     }
 
@@ -98,19 +102,29 @@ impl crate::handler::ConnectionHandler for ConnectionHandler {
         >,
     ) {
         match event {
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound {
                 protocol, ..
             }) => void::unreachable(protocol),
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::FullyNegotiatedOutbound(FullyNegotiatedOutbound {
                 protocol, ..
             }) => void::unreachable(protocol),
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::DialUpgradeError(DialUpgradeError { info: _, error }) => match error {
+                // TODO: remove when Rust 1.82 is MSRV
+                #[allow(unreachable_patterns)]
                 StreamUpgradeError::Timeout => unreachable!(),
                 StreamUpgradeError::Apply(e) => void::unreachable(e),
                 StreamUpgradeError::NegotiationFailed | StreamUpgradeError::Io(_) => {
                     unreachable!("Denied upgrade does not support any protocols")
                 }
             },
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::AddressChange(_)
             | ConnectionEvent::ListenUpgradeError(_)
             | ConnectionEvent::LocalProtocolsChange(_)
diff --git a/swarm/src/handler.rs b/swarm/src/handler.rs
index 610b95b8cf1..9e31592d68d 100644
--- a/swarm/src/handler.rs
+++ b/swarm/src/handler.rs
@@ -226,7 +226,7 @@ pub enum ConnectionEvent<'a, IP: InboundUpgradeSend, OP: OutboundUpgradeSend, IO
     RemoteProtocolsChange(ProtocolsChange<'a>),
 }
 
-impl<'a, IP, OP, IOI, OOI> fmt::Debug for ConnectionEvent<'a, IP, OP, IOI, OOI>
+impl<IP, OP, IOI, OOI> fmt::Debug for ConnectionEvent<'_, IP, OP, IOI, OOI>
 where
     IP: InboundUpgradeSend + fmt::Debug,
     IP::Output: fmt::Debug,
@@ -262,8 +262,8 @@ where
     }
 }
 
-impl<'a, IP: InboundUpgradeSend, OP: OutboundUpgradeSend, IOI, OOI>
-    ConnectionEvent<'a, IP, OP, IOI, OOI>
+impl<IP: InboundUpgradeSend, OP: OutboundUpgradeSend, IOI, OOI>
+    ConnectionEvent<'_, IP, OP, IOI, OOI>
 {
     /// Whether the event concerns an outbound stream.
     pub fn is_outbound(&self) -> bool {
diff --git a/swarm/src/handler/pending.rs b/swarm/src/handler/pending.rs
index 23b9adcfd90..9601f5cf78b 100644
--- a/swarm/src/handler/pending.rs
+++ b/swarm/src/handler/pending.rs
@@ -52,6 +52,8 @@ impl ConnectionHandler for PendingConnectionHandler {
     }
 
     fn on_behaviour_event(&mut self, v: Self::FromBehaviour) {
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         void::unreachable(v)
     }
 
@@ -74,9 +76,13 @@ impl ConnectionHandler for PendingConnectionHandler {
         >,
     ) {
         match event {
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound {
                 protocol, ..
             }) => void::unreachable(protocol),
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::FullyNegotiatedOutbound(FullyNegotiatedOutbound {
                 protocol,
                 info: _info,
@@ -87,6 +93,8 @@ impl ConnectionHandler for PendingConnectionHandler {
                     void::unreachable(_info);
                 }
             }
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             ConnectionEvent::AddressChange(_)
             | ConnectionEvent::DialUpgradeError(_)
             | ConnectionEvent::ListenUpgradeError(_)
diff --git a/swarm/src/lib.rs b/swarm/src/lib.rs
index 81b1ca1a68d..12280e99f07 100644
--- a/swarm/src/lib.rs
+++ b/swarm/src/lib.rs
@@ -1140,12 +1140,14 @@ where
                 self.pending_handler_event = Some((peer_id, handler, event));
             }
             ToSwarm::NewExternalAddrCandidate(addr) => {
-                self.behaviour
-                    .on_swarm_event(FromSwarm::NewExternalAddrCandidate(
-                        NewExternalAddrCandidate { addr: &addr },
-                    ));
-                self.pending_swarm_events
-                    .push_back(SwarmEvent::NewExternalAddrCandidate { address: addr });
+                if !self.confirmed_external_addr.contains(&addr) {
+                    self.behaviour
+                        .on_swarm_event(FromSwarm::NewExternalAddrCandidate(
+                            NewExternalAddrCandidate { addr: &addr },
+                        ));
+                    self.pending_swarm_events
+                        .push_back(SwarmEvent::NewExternalAddrCandidate { address: addr });
+                }
             }
             ToSwarm::ExternalAddrConfirmed(addr) => {
                 self.add_external_address(addr.clone());
diff --git a/swarm/src/upgrade.rs b/swarm/src/upgrade.rs
index 53b627458c9..f6c6648a373 100644
--- a/swarm/src/upgrade.rs
+++ b/swarm/src/upgrade.rs
@@ -121,6 +121,7 @@ where
 }
 
 /// Wraps around a type that implements [`OutboundUpgradeSend`], [`InboundUpgradeSend`], or
+///
 /// both, and implements [`OutboundUpgrade`](upgrade::OutboundUpgrade) and/or
 /// [`InboundUpgrade`](upgrade::InboundUpgrade).
 ///
diff --git a/swarm/tests/swarm_derive.rs b/swarm/tests/swarm_derive.rs
index 919ed0cab7f..667f68408cf 100644
--- a/swarm/tests/swarm_derive.rs
+++ b/swarm/tests/swarm_derive.rs
@@ -577,6 +577,8 @@ fn custom_out_event_no_type_parameters() {
             _connection: ConnectionId,
             message: THandlerOutEvent<Self>,
         ) {
+            // TODO: remove when Rust 1.82 is MSRV
+            #[allow(unreachable_patterns)]
             void::unreachable(message);
         }
 
diff --git a/transports/noise/src/io/handshake.rs b/transports/noise/src/io/handshake.rs
index 5c1fa806b6d..8993a5795b6 100644
--- a/transports/noise/src/io/handshake.rs
+++ b/transports/noise/src/io/handshake.rs
@@ -73,7 +73,6 @@ where
     /// will be sent and received on the given I/O resource and using the
     /// provided session for cryptographic operations according to the chosen
     /// Noise handshake pattern.
-
     pub(crate) fn new(
         io: T,
         session: snow::HandshakeState,
diff --git a/transports/quic/src/hole_punching.rs b/transports/quic/src/hole_punching.rs
index 605799af5e1..a38d123a6a4 100644
--- a/transports/quic/src/hole_punching.rs
+++ b/transports/quic/src/hole_punching.rs
@@ -20,6 +20,8 @@ pub(crate) async fn hole_puncher<P: Provider>(
     match futures::future::select(P::sleep(timeout_duration), punch_holes_future).await {
         Either::Left(_) => Error::HandshakeTimedOut,
         Either::Right((Err(hole_punch_err), _)) => hole_punch_err,
+        // TODO: remove when Rust 1.82 is MSRV
+        #[allow(unreachable_patterns)]
         Either::Right((Ok(never), _)) => match never {},
     }
 }
diff --git a/transports/websocket-websys/src/lib.rs b/transports/websocket-websys/src/lib.rs
index f353d92b204..3467e802bc5 100644
--- a/transports/websocket-websys/src/lib.rs
+++ b/transports/websocket-websys/src/lib.rs
@@ -96,8 +96,8 @@ impl libp2p_core::Transport for Transport {
             return Err(TransportError::MultiaddrNotSupported(addr));
         }
 
-        let url = extract_websocket_url(&addr)
-            .ok_or_else(|| TransportError::MultiaddrNotSupported(addr))?;
+        let url =
+            extract_websocket_url(&addr).ok_or(TransportError::MultiaddrNotSupported(addr))?;
 
         Ok(async move {
             let socket = match WebSocket::new(&url) {
diff --git a/transports/websocket/src/framed.rs b/transports/websocket/src/framed.rs
index a547aea21ef..198443508d9 100644
--- a/transports/websocket/src/framed.rs
+++ b/transports/websocket/src/framed.rs
@@ -442,7 +442,7 @@ pub(crate) enum WsListenProto<'a> {
     TlsWs(Cow<'a, str>),
 }
 
-impl<'a> WsListenProto<'a> {
+impl WsListenProto<'_> {
     pub(crate) fn append_on_addr(&self, addr: &mut Multiaddr) {
         match self {
             WsListenProto::Ws(path) => {
diff --git a/transports/webtransport-websys/Cargo.toml b/transports/webtransport-websys/Cargo.toml
index 9541c49b737..eeb474d4a63 100644
--- a/transports/webtransport-websys/Cargo.toml
+++ b/transports/webtransport-websys/Cargo.toml
@@ -21,6 +21,7 @@ libp2p-identity = { workspace = true }
 libp2p-noise = { workspace = true }
 multiaddr = { workspace = true }
 multihash = { workspace = true }
+once_cell = "1.19.0"
 send_wrapper = { version = "0.6.0", features = ["futures"] }
 thiserror = "1.0.61"
 tracing = { workspace = true }
diff --git a/transports/webtransport-websys/src/utils.rs b/transports/webtransport-websys/src/utils.rs
index 55bad08e00c..0b3550e5b5b 100644
--- a/transports/webtransport-websys/src/utils.rs
+++ b/transports/webtransport-websys/src/utils.rs
@@ -1,10 +1,17 @@
 use js_sys::{Promise, Reflect};
+use once_cell::sync::Lazy;
 use send_wrapper::SendWrapper;
 use std::io;
 use wasm_bindgen::{JsCast, JsValue};
 
 use crate::Error;
 
+type Closure = wasm_bindgen::closure::Closure<dyn FnMut(JsValue)>;
+static DO_NOTHING: Lazy<SendWrapper<Closure>> = Lazy::new(|| {
+    let cb = Closure::new(|_| {});
+    SendWrapper::new(cb)
+});
+
 /// Properly detach a promise.
 ///
 /// A promise always runs in the background, however if you don't await it,
@@ -13,22 +20,9 @@ use crate::Error;
 //
 // Ref: https://github.com/typescript-eslint/typescript-eslint/blob/391a6702c0a9b5b3874a7a27047f2a721f090fb6/packages/eslint-plugin/docs/rules/no-floating-promises.md
 pub(crate) fn detach_promise(promise: Promise) {
-    type Closure = wasm_bindgen::closure::Closure<dyn FnMut(JsValue)>;
-    static mut DO_NOTHING: Option<SendWrapper<Closure>> = None;
-
-    // Allocate Closure only once and reuse it
-    let do_nothing = unsafe {
-        if DO_NOTHING.is_none() {
-            let cb = Closure::new(|_| {});
-            DO_NOTHING = Some(SendWrapper::new(cb));
-        }
-
-        DO_NOTHING.as_deref().unwrap()
-    };
-
     // Avoid having "floating" promise and ignore any errors.
     // After `catch` promise is allowed to be dropped.
-    let _ = promise.catch(do_nothing);
+    let _ = promise.catch(&DO_NOTHING);
 }
 
 /// Typecasts a JavaScript type.