Repository: srl-labs/containerlab
Pull request #2652 (2025-07-04 08:30:56)

Creation of bridges within containers and wiring links into container based bridges
Allow for the creation of bridges within containers and also the wiring into these bridges.

```
name: "bridge-ns"
topology:
  nodes: 
     br01:
       kind: bridge
       network-mode: container:bp1
     bp1:
       kind: linux
       image: alpine:latest
     c1:
       kind: linux
       image: alpine:latest
   links:
     - endpoints: ["c1:eth1", "br01:c1eth1"]
```

The bridge `br01` is being created in the `bp1` container. Then the client `c1` is attached to the bridge that resides in container `bp1`.

A limitation that exists with this approach is, that in the nodes section names need to be unique, hence `br01` can only exist once, although it would be possible to have it exist with the same name in different namespaces.

Possible solution to this:
Add a pre or postfix to the bridgenames in the topology whilst everything behind / before the delimiter would be removed on provisioning.
```
  nodes: 
     br01|bp1:
       kind: bridge
       network-mode: container:bp1
```
the part ofter the delimiter is not necessarly the base container name whatever makes it unique should work.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) Report
Attention: Patch coverage is `18.28571%` with `143 lines` in your changes missing coverage. Please review.
> Project coverage is 54.65%. Comparing base [(`792919f`)](https://app.codecov.io/gh/srl-labs/containerlab/commit/792919f95da989644e87724fa1162db0686e9520?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) to head [(`00ebe52`)](https://app.codecov.io/gh/srl-labs/containerlab/commit/00ebe52c26e0e245e93a6ac31d56dbb22881bca9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs).
> Report is 2 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Patch % | Lines |
|---|---|---|
| [nodes/bridge/bridge.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fbridge%2Fbridge.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvYnJpZGdlL2JyaWRnZS5nbw==) | 23.25% | [60 Missing and 6 partials :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [links/link\_bridge.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Flink_bridge.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvbGlua19icmlkZ2UuZ28=) | 0.00% | [43 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [mocks/mocknodes/node.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=mocks%2Fmocknodes%2Fnode.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bW9ja3MvbW9ja25vZGVzL25vZGUuZ28=) | 0.00% | [13 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [mocks/mocknodes/default\_node.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=mocks%2Fmocknodes%2Fdefault_node.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bW9ja3MvbW9ja25vZGVzL2RlZmF1bHRfbm9kZS5nbw==) | 0.00% | [9 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [utils/containers.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=utils%2Fcontainers.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvY29udGFpbmVycy5nbw==) | 0.00% | [6 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [links/endpoint\_bridge.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Fendpoint_bridge.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvZW5kcG9pbnRfYnJpZGdlLmdv) | 50.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [cmd/tools\_veth.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=cmd%2Ftools_veth.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL3Rvb2xzX3ZldGguZ28=) | 33.33% | [2 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [nodes/ovs/ovs.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fovs%2Fovs.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvb3ZzL292cy5nbw==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2652      +/-   ##
==========================================
- Coverage   55.03%   54.65%   -0.39%     
==========================================
  Files         186      187       +1     
  Lines       20454    20727     +273     
==========================================
+ Hits        11257    11328      +71     
- Misses       8032     8207     +175     
- Partials     1165     1192      +27     
```

| [Files with missing lines](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Coverage Δ | |
|---|---|---|
| [clab/config.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=clab%2Fconfig.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jb25maWcuZ28=) | `69.13% <100.00%> (ø)` | |
| [links/endpoint\_raw.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Fendpoint_raw.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvZW5kcG9pbnRfcmF3Lmdv) | `73.33% <100.00%> (ø)` | |
| [links/link.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Flink.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvbGluay5nbw==) | `63.15% <ø> (ø)` | |
| [nodes/default\_node.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fdefault_node.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvZGVmYXVsdF9ub2RlLmdv) | `65.02% <100.00%> (-0.71%)` | :arrow_down: |
| [nodes/node.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fnode.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvbm9kZS5nbw==) | `56.00% <ø> (ø)` | |
| [nodes/srl/srl.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fsrl%2Fsrl.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvc3JsL3NybC5nbw==) | `58.79% <100.00%> (ø)` | |
| [nodes/ovs/ovs.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fovs%2Fovs.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvb3ZzL292cy5nbw==) | `1.33% <0.00%> (ø)` | |
| [cmd/tools\_veth.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=cmd%2Ftools_veth.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL3Rvb2xzX3ZldGguZ28=) | `71.87% <33.33%> (-0.91%)` | :arrow_down: |
| [links/endpoint\_bridge.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Fendpoint_bridge.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvZW5kcG9pbnRfYnJpZGdlLmdv) | `58.13% <50.00%> (+0.99%)` | :arrow_up: |
| [utils/containers.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=utils%2Fcontainers.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvY29udGFpbmVycy5nbw==) | `58.62% <0.00%> (-6.77%)` | :arrow_down: |
| ... and [4 more](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs)

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) Report
Attention: Patch coverage is `20.31250%` with `153 lines` in your changes missing coverage. Please review.
> Project coverage is 54.63%. Comparing base [(`792919f`)](https://app.codecov.io/gh/srl-labs/containerlab/commit/792919f95da989644e87724fa1162db0686e9520?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) to head [(`5f63ceb`)](https://app.codecov.io/gh/srl-labs/containerlab/commit/5f63ceb5c875529ffffda18bc6114ee5120b07ef?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs).
> Report is 2 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Patch % | Lines |
|---|---|---|
| [nodes/bridge/bridge.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fbridge%2Fbridge.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvYnJpZGdlL2JyaWRnZS5nbw==) | 25.49% | [67 Missing and 9 partials :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [links/link\_bridge.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Flink_bridge.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvbGlua19icmlkZ2UuZ28=) | 0.00% | [43 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [mocks/mocknodes/node.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=mocks%2Fmocknodes%2Fnode.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bW9ja3MvbW9ja25vZGVzL25vZGUuZ28=) | 0.00% | [13 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [mocks/mocknodes/default\_node.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=mocks%2Fmocknodes%2Fdefault_node.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bW9ja3MvbW9ja25vZGVzL2RlZmF1bHRfbm9kZS5nbw==) | 0.00% | [9 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [utils/containers.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=utils%2Fcontainers.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvY29udGFpbmVycy5nbw==) | 0.00% | [6 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [links/endpoint\_bridge.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Fendpoint_bridge.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvZW5kcG9pbnRfYnJpZGdlLmdv) | 50.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [cmd/tools\_veth.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=cmd%2Ftools_veth.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL3Rvb2xzX3ZldGguZ28=) | 33.33% | [2 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |
| [nodes/ovs/ovs.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fovs%2Fovs.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvb3ZzL292cy5nbw==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2652      +/-   ##
==========================================
- Coverage   55.03%   54.63%   -0.41%     
==========================================
  Files         186      187       +1     
  Lines       20454    20740     +286     
==========================================
+ Hits        11257    11331      +74     
- Misses       8032     8214     +182     
- Partials     1165     1195      +30     
```

| [Files with missing lines](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Coverage Δ | |
|---|---|---|
| [clab/config.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=clab%2Fconfig.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jb25maWcuZ28=) | `69.13% <100.00%> (ø)` | |
| [links/endpoint\_raw.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Fendpoint_raw.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvZW5kcG9pbnRfcmF3Lmdv) | `73.33% <100.00%> (ø)` | |
| [links/link.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Flink.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvbGluay5nbw==) | `63.15% <ø> (ø)` | |
| [nodes/default\_node.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fdefault_node.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvZGVmYXVsdF9ub2RlLmdv) | `65.02% <100.00%> (-0.71%)` | :arrow_down: |
| [nodes/k8s\_kind/k8s\_kind.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fk8s_kind%2Fk8s_kind.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvazhzX2tpbmQvazhzX2tpbmQuZ28=) | `62.59% <100.00%> (ø)` | |
| [nodes/node.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fnode.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvbm9kZS5nbw==) | `56.00% <ø> (ø)` | |
| [nodes/srl/srl.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fsrl%2Fsrl.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvc3JsL3NybC5nbw==) | `58.79% <100.00%> (ø)` | |
| [nodes/ovs/ovs.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=nodes%2Fovs%2Fovs.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvb3ZzL292cy5nbw==) | `1.33% <0.00%> (ø)` | |
| [cmd/tools\_veth.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=cmd%2Ftools_veth.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL3Rvb2xzX3ZldGguZ28=) | `71.87% <33.33%> (-0.91%)` | :arrow_down: |
| [links/endpoint\_bridge.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree&filepath=links%2Fendpoint_bridge.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bGlua3MvZW5kcG9pbnRfYnJpZGdlLmdv) | `58.13% <50.00%> (+0.99%)` | :arrow_up: |
| ... and [5 more](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/srl-labs/containerlab/pull/2652/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs)

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 445c1ee0..deadc330 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -151,6 +151,7 @@ jobs:
     uses: ./.github/workflows/smoke-tests.yml
     with:
       py_ver: ${{ needs.process-gitref.outputs.py_ver }}
+    secrets: inherit
     needs:
       - file-changes
       - process-gitref
diff --git a/clab/clab.go b/clab/clab.go
index 3886b067..919f2ece 100644
--- a/clab/clab.go
+++ b/clab/clab.go
@@ -271,7 +271,7 @@ func WithTopoFromLab(labName string) ClabOption {
 }
 
 // ProcessTopoPath takes a topology path, which might be the path to a directory or a file
-// or stdin or a URL and returns the topology file name if found.
+// or stdin or a URL (HTTP/HTTPS/S3) and returns the topology file name if found.
 func (c *CLab) ProcessTopoPath(path string) (string, error) {
 	var file string
 	var err error
@@ -290,6 +290,13 @@ func (c *CLab) ProcessTopoPath(path string) (string, error) {
 		if err != nil {
 			return "", err
 		}
+	// if the path is an S3 URL, download the file and store it in the tmp dir
+	case utils.IsS3URL(path):
+		log.Debugf("interpreting topo %q as S3 URL", path)
+		file, err = downloadTopoFile(path, c.TopoPaths.ClabTmpDir())
+		if err != nil {
+			return "", err
+		}
 
 	case path == "":
 		return "", fmt.Errorf("provide a path to the clab topology file")
@@ -705,7 +712,7 @@ func (c *CLab) scheduleNodes(ctx context.Context, maxWorkers int, skipPostDeploy
 				}
 
 				// Deploy
-				err = node.Deploy(ctx, &nodes.DeployParams{})
+				err = node.Deploy(ctx, &nodes.DeployParams{Nodes: c.Nodes})
 				if err != nil {
 					log.Errorf("failed deploy stage for node %q: %v", node.Config().ShortName, err)
 					continue
diff --git a/clab/config.go b/clab/config.go
index 1cc9c340..8782de0e 100644
--- a/clab/config.go
+++ b/clab/config.go
@@ -261,7 +261,7 @@ func (c *CLab) createNodeCfg(nodeName string, nodeDef *types.NodeDefinition, idx
 }
 
 // processStartupConfig processes the raw path of the startup-config as it is defined in the topology file.
-// It handles remote files, local files and embedded configs.
+// It handles remote files (HTTP/HTTPS/S3), local files and embedded configs.
 // As a result the `nodeCfg.StartupConfig` will be set to an absPath of the startup config file.
 func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error {
 	// replace __clabNodeName__ magic var in startup-config path with node short name
@@ -271,8 +271,8 @@ func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error {
 	// embedded config is a config that is defined as a multi-line string in the topology file
 	// it contains at least one newline
 	isEmbeddedConfig := strings.Count(p, "\n") >= 1
-	// downloadable config starts with http(s)://
-	isDownloadableConfig := utils.IsHttpURL(p, false)
+	// downloadable config starts with http(s):// or s3://
+	isDownloadableConfig := utils.IsHttpURL(p, false) || utils.IsS3URL(p)
 
 	if isEmbeddedConfig || isDownloadableConfig {
 		switch {
diff --git a/cmd/tools_veth.go b/cmd/tools_veth.go
index 6ee29ce4..159a9063 100644
--- a/cmd/tools_veth.go
+++ b/cmd/tools_veth.go
@@ -143,7 +143,7 @@ func createNodes(_ context.Context, c *clab.CLab, AEnd, BEnd parsedEndpoint, rt
 				return err
 			}
 
-		case links.LinkEndpointTypeBridge:
+		case links.LinkEndpointTypeBridge, links.LinkEndpointTypeBridgeNS:
 			err := createFakeNode(c, "bridge", &types.NodeConfig{
 				ShortName: epDefinition.Node,
 				LongName:  epDefinition.Node,
@@ -208,6 +208,8 @@ func parseVethEndpoint(s string) (parsedEndpoint, error) {
 		switch arr[0] {
 		case "bridge", "ovs-bridge":
 			kind = links.LinkEndpointTypeBridge
+		case "bridge-ns":
+			kind = links.LinkEndpointTypeBridgeNS
 		default:
 			kind = links.LinkEndpointTypeVeth
 		}
diff --git a/docs/cmd/deploy.md b/docs/cmd/deploy.md
index acfd159c..c4925483 100644
--- a/docs/cmd/deploy.md
+++ b/docs/cmd/deploy.md
@@ -65,6 +65,10 @@ The following URL formats are supported:
 
 Containerlab distinct HTTP URLs from GitHub/GitLab by checking if github.com or gitlab.com is present in the URL. If not, it will treat the URL as a plain HTTP(S) URL.
 
+###### S3
+
+Containerlab supports using S3 URLs to retrieve topology files and startup configurations for network devices. Check out the documentation on [S3 usage](../manual/s3-usage-example.md) for more details.
+
 #### name
 
 With the global `--name | -n` flag a user sets a lab name. This value will override the lab name value passed in the topology definition file.
@@ -150,6 +154,7 @@ The local `--owner` flag allows you to specify a custom owner for the lab. This
 This flag is designed for multi-user environments where you need to track ownership of lab resources. Only users who are members of the `clab_admins` group can set a custom owner. If a non-admin user attempts to set an owner, the flag will be ignored with a warning, and the current user will be used as the owner instead.
 
 Example:
+
 ```bash
 containerlab deploy -t mylab.clab.yml --owner alice
 ```
diff --git a/docs/manual/config-mgmt.md b/docs/manual/config-mgmt.md
index 0ff22aff..8dcd1bfc 100644
--- a/docs/manual/config-mgmt.md
+++ b/docs/manual/config-mgmt.md
@@ -124,7 +124,7 @@ If a config file exists in the lab directory for a given node, then it will take
 
 ### Remote
 
-It is possible to specify a remote `http(s)` location for a startup-config file. Simply provide a URL that can be accessed from the containerlab host.
+It is possible to specify a remote (`http(s)` or [S3](s3-usage-example.md)) location for a startup-config file. Simply provide a URL that can be accessed from the containerlab host.
 
 ```yaml
 topology:
diff --git a/docs/manual/kinds/bridge.md b/docs/manual/kinds/bridge.md
index 0ba71db5..3eaec7e0 100644
--- a/docs/manual/kinds/bridge.md
+++ b/docs/manual/kinds/bridge.md
@@ -56,7 +56,7 @@ When connecting other nodes to a bridge, the bridge endpoint must be present in
 /// admonition
     type: subtle-note
 When choosing names of the interfaces that need to be connected to the bridge make sure that these names are not clashing with existing interfaces.  
-In the example above we named interfaces `eth1`, `eth2`, `eth3` accordingly and ensured that none of these interfaces existed before in the root netns.  
+In the example above we named interfaces `eth1`, `eth2`, `eth3` accordingly and ensured that none of these interfaces existed before in the root netns.
 ///
 
 As a result of such topology definition, you will see bridge `br-clab` with three interfaces attached to it:
@@ -89,3 +89,39 @@ grep "set by containerlab" | awk '{print $1}' \
 ///
 
 Check out ["External bridge"](../../lab-examples/ext-bridge.md) lab for a ready-made example on how to use bridges.
+
+## Bridges in container namespace
+
+It is possible to make Containerlab create bridges inside the container namespace and connect nodes to them. As opposed to the host-bound bridges, these bridges reside in a container namespace and therefore are isolated from the host.
+
+A practical use case for this is to create backplane bridges that are used for internal connectivity between nodes in a lab and should not be part of the host namespace. To defined a namespaced bridge, you need to use a namespace of another node, like this:
+
+```yaml
+name: "bridge-ns"
+topology:
+  nodes:
+     br01:
+       kind: bridge
+       network-mode: container:bp1
+     bp1:
+       kind: linux
+       image: alpine:latest
+     c1:
+       kind: linux
+       image: alpine:latest
+   links:
+     - endpoints: ["c1:eth1", "br01:c1eth1"]
+```
+
+In the example above, the bridge `br01` is created inside the container namespace of the `bp1` node. The bridge will be named `br01` inside the `bp1` container and will have an interface `c1eth1` connected to it from the `c1` node.
+
+Note, that while `br01` is created inside the `bp1` container, containerlab still counts it as a node and will not allow you to create another node with the same name in the same lab even if it is created in a different namespace. To overcome this, you can use a special naming convention for the same-named bridges in different namespaces, like `br01|bp1` and `br01|bp2`, which will allow you to create bridges with the same name in different namespaces.
+
+The extra `|bp1` suffix is used to distinguish the bridges and make them unique for containerlab, but this suffix will be dropped when the bridge is created inside the container namespace, so the bridge will still be named `br01` inside the `bp1` and `bp2` containers.
+
+```yaml
+  nodes:
+     br01|bp1:
+       kind: bridge
+       network-mode: container:bp1
+```
diff --git a/docs/manual/nodes.md b/docs/manual/nodes.md
index 289e3eca..5103844f 100644
--- a/docs/manual/nodes.md
+++ b/docs/manual/nodes.md
@@ -140,11 +140,11 @@ Some containerized NOSes require a license to operate or can leverage a license
 
 ### startup-config
 
-It is possible to provide the startup configuration that the node applies on boot for most Containerlab kinds. The startup config can be provided in two ways:
+It is possible to provide the startup configuration that the node applies on boot for most Containerlab kinds. The startup config can be provided as:
 
-1. As a path to a file that is available on the host machine and contains the config blob that the node understands.
-2. As an embedded config blob that is provided as a multiline string.
-3. As an URL to a file that contains the config blob that the node can apply.
+1. A path to a file that is available on the host machine and contains the config blob that the node understands.
+2. An embedded config blob that is provided as a multiline string.
+3. An URL (http(s) or [S3](s3-usage-example.md)) to a file that contains the config blob that the node can apply.
 
 Read more about the usage of the startup configuration (and other ways to perform configuration management with Containerlab) in the [Configuration Management](config-mgmt.md) section.
 
diff --git a/docs/manual/s3-usage-example.md b/docs/manual/s3-usage-example.md
new file mode 100644
index 00000000..9f16bd1b
--- /dev/null
+++ b/docs/manual/s3-usage-example.md
@@ -0,0 +1,61 @@
+# S3 Support in Containerlab
+
+Containerlab supports using S3 URLs to retrieve topology files and startup configurations for network devices.
+
+## Prerequisites
+
+AWS credentials are automatically discovered using the standard AWS credential chain (in order of precedence):
+
+1. **Environment variables** (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
+2. **Shared credentials file** (`~/.aws/credentials`)
+3. **IAM roles** (EC2 instance profiles)
+
+## Usage Examples
+
+### Topology Files from S3
+
+Deploy a lab using a topology file stored in S3:
+
+```bash
+containerlab deploy -t s3://my-bucket/topologies/my-lab.clab.yml
+```
+
+### Startup Configurations from S3
+
+In your topology file, you can reference startup configurations stored in S3:
+
+```yaml
+name: my-lab
+topology:
+  nodes:
+    router1:
+      kind: srl
+      image: ghcr.io/nokia/srlinux:latest
+      startup-config: s3://my-bucket/configs/router1.cli
+    
+    router2:
+      kind: srl
+      image: ghcr.io/nokia/srlinux:latest
+      startup-config: s3://my-bucket/configs/router2.cli
+```
+
+## Authentication
+
+### Environment Variables
+
+```bash
+export AWS_ACCESS_KEY_ID=your-access-key
+export AWS_SECRET_ACCESS_KEY=your-secret-key
+export AWS_REGION=us-east-1  # Optional, defaults to us-east-1
+```
+
+### AWS Credentials File
+
+Configure in `~/.aws/credentials`:
+
+```ini
+[default]
+aws_access_key_id = your-access-key
+aws_secret_access_key = your-secret-key
+region = us-east-1
+```
diff --git a/go.mod b/go.mod
index bdae8333..1380c9b9 100644
--- a/go.mod
+++ b/go.mod
@@ -28,6 +28,7 @@ require (
 	github.com/kellerza/template v0.0.6
 	github.com/klauspost/cpuid/v2 v2.2.9
 	github.com/mackerelio/go-osstat v0.2.5
+	github.com/minio/minio-go/v7 v7.0.82
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/opencontainers/runtime-spec v1.2.0
 	github.com/pkg/errors v0.9.1
@@ -78,6 +79,7 @@ require (
 	github.com/evanphx/json-patch/v5 v5.6.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
+	github.com/go-ini/ini v1.67.0 // indirect
 	github.com/go-jose/go-jose/v4 v4.0.4 // indirect
 	github.com/go-logfmt/logfmt v0.6.0 // indirect
 	github.com/go-openapi/analysis v0.23.0 // indirect
@@ -90,6 +92,7 @@ require (
 	github.com/go-openapi/strfmt v0.23.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-openapi/validate v0.24.0 // indirect
+	github.com/goccy/go-json v0.10.3 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/go-containerregistry v0.20.2 // indirect
 	github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect
@@ -100,6 +103,7 @@ require (
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-sqlite3 v1.14.24 // indirect
 	github.com/mdlayher/socket v0.5.1 // indirect
+	github.com/minio/md5-simd v1.1.2 // indirect
 	github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect
 	github.com/moby/docker-image-spec v1.3.1 // indirect
 	github.com/moby/sys/capability v0.4.0 // indirect
@@ -118,6 +122,7 @@ require (
 	github.com/pjbgf/sha1cd v0.3.2 // indirect
 	github.com/pkg/sftp v1.13.7 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/rs/xid v1.6.0 // indirect
 	github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
 	github.com/sigstore/fulcio v1.6.4 // indirect
 	github.com/sigstore/rekor v1.3.8 // indirect
diff --git a/go.sum b/go.sum
index 6da01408..49f64715 100644
--- a/go.sum
+++ b/go.sum
@@ -354,6 +354,8 @@ github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJA
 github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
 github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
 github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
 github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
 github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -456,6 +458,8 @@ github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Il
 github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
 github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godbus/dbus/v5 v5.1.1-0.20241109141217-c266b19b28e9 h1:Kzr9J0S0V2PRxiX6B6xw1kWjzsIyjLO2Ibi4fNTaYBM=
@@ -665,6 +669,7 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
 github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
 github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -753,6 +758,10 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
 github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
 github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
 github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.82 h1:tWfICLhmp2aFPXL8Tli0XDTHj2VB/fNf0PC1f/i1gRo=
+github.com/minio/minio-go/v7 v7.0.82/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0=
 github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU=
 github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@@ -932,6 +941,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
diff --git a/links/endpoint_bridge.go b/links/endpoint_bridge.go
index 19e2c110..6531b53e 100644
--- a/links/endpoint_bridge.go
+++ b/links/endpoint_bridge.go
@@ -27,15 +27,19 @@ func (e *EndpointBridge) Verify(ctx context.Context, p *VerifyLinkParams) error
 	if err != nil {
 		errs = append(errs, err)
 	}
-	if p.RunBridgeExistsCheck {
+	// if the BridgeExists check is disabled by config and it is not a Bridge in an Namespace, run the check
+	if p.RunBridgeExistsCheck && e.Node.GetLinkEndpointType() != LinkEndpointTypeBridgeNS {
 		err = CheckBridgeExists(ctx, e.GetNode())
 		if err != nil {
 			errs = append(errs, err)
 		}
 	}
-	err = CheckEndpointDoesNotExistYet(ctx, e)
-	if err != nil {
-		errs = append(errs, err)
+	// if it is supposed to be a bridge in a Namespace, the if exists check is to be skipped.
+	if e.Node.GetLinkEndpointType() != LinkEndpointTypeBridgeNS {
+		err = CheckEndpointDoesNotExistYet(ctx, e)
+		if err != nil {
+			errs = append(errs, err)
+		}
 	}
 	if len(errs) > 0 {
 		return errors.Join(errs...)
diff --git a/links/endpoint_raw.go b/links/endpoint_raw.go
index b1c083fb..22f5398f 100644
--- a/links/endpoint_raw.go
+++ b/links/endpoint_raw.go
@@ -57,7 +57,7 @@ func (er *EndpointRaw) Resolve(params *ResolveParams, l Link) (Endpoint, error)
 	var e Endpoint
 
 	switch node.GetLinkEndpointType() {
-	case LinkEndpointTypeBridge:
+	case LinkEndpointTypeBridge, LinkEndpointTypeBridgeNS:
 		e = NewEndpointBridge(genericEndpoint, false)
 
 	case LinkEndpointTypeHost:
diff --git a/links/link.go b/links/link.go
index 754992fe..42c5c2f5 100644
--- a/links/link.go
+++ b/links/link.go
@@ -57,6 +57,7 @@ const (
 	LinkTypeVxlan       LinkType = "vxlan"
 	LinkTypeVxlanStitch LinkType = "vxlan-stitch"
 	LinkTypeDummy       LinkType = "dummy"
+	LinkTypeBridge      LinkType = "bridge"
 
 	// LinkTypeBrief is a link definition where link types
 	// are encoded in the endpoint definition as string and allow users
@@ -393,9 +394,10 @@ type Node interface {
 type LinkEndpointType string
 
 const (
-	LinkEndpointTypeVeth   = "veth"
-	LinkEndpointTypeBridge = "bridge"
-	LinkEndpointTypeHost   = "host"
+	LinkEndpointTypeVeth     = "veth"
+	LinkEndpointTypeBridge   = "bridge"
+	LinkEndpointTypeBridgeNS = "bridge-ns"
+	LinkEndpointTypeHost     = "host"
 )
 
 // SetNameMACAndUpInterface is a helper function that will bind interface name and Mac
diff --git a/links/link_bridge.go b/links/link_bridge.go
new file mode 100644
index 00000000..b78f8973
--- /dev/null
+++ b/links/link_bridge.go
@@ -0,0 +1,82 @@
+package links
+
+import (
+	"context"
+
+	"github.com/charmbracelet/log"
+	"github.com/containernetworking/plugins/pkg/ns"
+	"github.com/vishvananda/netlink"
+)
+
+type Bridge struct {
+	LinkCommonParams
+	endpoint Endpoint
+}
+
+func NewBridge() *Bridge {
+	return &Bridge{}
+}
+
+func (b *Bridge) SetEndpoint(e Endpoint) {
+	b.endpoint = e
+}
+
+// Deploy deploys the link. Endpoint is the endpoint that triggers the creation of the link.
+func (b *Bridge) Deploy(ctx context.Context, endpoint Endpoint) error {
+	err := endpoint.GetNode().ExecFunction(ctx, func(nn ns.NetNS) error {
+		// add the bridge
+		err := netlink.LinkAdd(&netlink.Bridge{
+			LinkAttrs: netlink.LinkAttrs{
+				Name: b.endpoint.GetIfaceName(),
+			},
+		})
+		if err != nil {
+			return err
+		}
+		// retrieve link ref
+		netlinkLink, err := netlink.LinkByName(b.endpoint.GetIfaceName())
+		if err != nil {
+			return err
+		}
+		// bring the link up
+		err = netlink.LinkSetUp(netlinkLink)
+		if err != nil {
+			return err
+		}
+		return nil
+	})
+
+	return err
+}
+
+// Remove removes the link.
+func (b *Bridge) Remove(ctx context.Context) error {
+	// check Deployment state, if the Link was already
+	// removed via e.g. the peer node
+	if b.DeploymentState == LinkDeploymentStateRemoved {
+		return nil
+	}
+	// trigger link removal via the NodeEndpoint
+	err := b.endpoint.Remove(ctx)
+	if err != nil {
+		log.Debug(err)
+	}
+	// adjust the Deployment status to reflect the removal
+	b.DeploymentState = LinkDeploymentStateRemoved
+	return nil
+}
+
+// GetType returns the type of the link.
+func (b *Bridge) GetType() LinkType {
+	return LinkTypeBridge
+}
+
+// GetEndpoints returns the endpoints of the link.
+func (b *Bridge) GetEndpoints() []Endpoint {
+	return []Endpoint{b.endpoint}
+}
+
+// GetMTU returns the Link MTU.
+func (b *Bridge) GetMTU() int {
+	return b.MTU
+}
diff --git a/mocks/mocknodes/default_node.go b/mocks/mocknodes/default_node.go
index 6acc4530..6fd5776b 100644
--- a/mocks/mocknodes/default_node.go
+++ b/mocks/mocknodes/default_node.go
@@ -129,6 +129,21 @@ func (mr *MockNodeOverwritesMockRecorder) GetMappedInterfaceName(ifName any) *go
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMappedInterfaceName", reflect.TypeOf((*MockNodeOverwrites)(nil).GetMappedInterfaceName), ifName)
 }
 
+// GetNSPath mocks base method.
+func (m *MockNodeOverwrites) GetNSPath(ctx context.Context) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetNSPath", ctx)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetNSPath indicates an expected call of GetNSPath.
+func (mr *MockNodeOverwritesMockRecorder) GetNSPath(ctx any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNSPath", reflect.TypeOf((*MockNodeOverwrites)(nil).GetNSPath), ctx)
+}
+
 // PullImage mocks base method.
 func (m *MockNodeOverwrites) PullImage(ctx context.Context) error {
 	m.ctrl.T.Helper()
diff --git a/mocks/mocknodes/node.go b/mocks/mocknodes/node.go
index aa5c4289..cea5e999 100644
--- a/mocks/mocknodes/node.go
+++ b/mocks/mocknodes/node.go
@@ -92,17 +92,17 @@ func (mr *MockNodeMockRecorder) CalculateInterfaceIndex(ifName any) *gomock.Call
 }
 
 // CheckDeploymentConditions mocks base method.
-func (m *MockNode) CheckDeploymentConditions(arg0 context.Context) error {
+func (m *MockNode) CheckDeploymentConditions(ctx context.Context) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CheckDeploymentConditions", arg0)
+	ret := m.ctrl.Call(m, "CheckDeploymentConditions", ctx)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // CheckDeploymentConditions indicates an expected call of CheckDeploymentConditions.
-func (mr *MockNodeMockRecorder) CheckDeploymentConditions(arg0 any) *gomock.Call {
+func (mr *MockNodeMockRecorder) CheckDeploymentConditions(ctx any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckDeploymentConditions", reflect.TypeOf((*MockNode)(nil).CheckDeploymentConditions), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckDeploymentConditions", reflect.TypeOf((*MockNode)(nil).CheckDeploymentConditions), ctx)
 }
 
 // CheckInterfaceName mocks base method.
@@ -288,6 +288,21 @@ func (mr *MockNodeMockRecorder) GetLinkEndpointType() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLinkEndpointType", reflect.TypeOf((*MockNode)(nil).GetLinkEndpointType))
 }
 
+// GetNSPath mocks base method.
+func (m *MockNode) GetNSPath(ctx context.Context) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetNSPath", ctx)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetNSPath indicates an expected call of GetNSPath.
+func (mr *MockNodeMockRecorder) GetNSPath(ctx any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNSPath", reflect.TypeOf((*MockNode)(nil).GetNSPath), ctx)
+}
+
 // GetRuntime mocks base method.
 func (m *MockNode) GetRuntime() runtime.ContainerRuntime {
 	m.ctrl.T.Helper()
diff --git a/nodes/bridge/bridge.go b/nodes/bridge/bridge.go
index dae21dae..774080dc 100644
--- a/nodes/bridge/bridge.go
+++ b/nodes/bridge/bridge.go
@@ -6,6 +6,8 @@ package bridge
 
 import (
 	"context"
+	"fmt"
+	"strings"
 
 	"github.com/charmbracelet/log"
 	"github.com/containernetworking/plugins/pkg/ns"
@@ -23,6 +25,10 @@ import (
 
 var kindNames = []string{"bridge"}
 
+// bridgeNodeSep is a separator used in the bridge name to distinguish
+// bridges attached to different namespaces in the topology.
+var bridgeNodeSep = "|"
+
 const (
 	generateable     = true
 	generateIfFormat = "eth%d"
@@ -40,6 +46,8 @@ func Register(r *nodes.NodeRegistry) {
 
 type bridge struct {
 	nodes.DefaultNode
+	containerNs string
+	nodesMap    map[string]nodes.Node
 }
 
 func (s *bridge) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error {
@@ -50,11 +58,65 @@ func (s *bridge) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error {
 	for _, o := range opts {
 		o(s)
 	}
-	s.Cfg.IsRootNamespaceBased = true
+	if s.Cfg.NetworkMode == "" || s.Cfg.NetworkMode == "host" {
+		s.Cfg.IsRootNamespaceBased = true
+	} else {
+		var err error
+		s.containerNs, err = utils.ContainerNameFromNetworkMode(s.Config().NetworkMode)
+		if err != nil {
+			return err
+		}
+	}
 	return nil
 }
 
-func (n *bridge) Deploy(_ context.Context, _ *nodes.DeployParams) error {
+// nameWithoutSeparatorSuffix returns the bridge name without the separator suffix
+// used to distinguish bridges attached to different namespaces in the topology.
+// For example, if the bridge name is "br0|ns1", it will return "br0".
+func (n *bridge) nameWithoutSeparatorSuffix() string {
+	s := n.GetShortName()
+	if idx := strings.Index(s, bridgeNodeSep); idx != -1 {
+		return s[:idx]
+	}
+	return s
+}
+
+func (n *bridge) Deploy(ctx context.Context, dp *nodes.DeployParams) error {
+	// store nodes map for later use
+	n.nodesMap = dp.Nodes
+
+	// if the NetworkMode is set, then the bridge is setup within a namespace, so it must be created.
+	if n.Config().NetworkMode != "" {
+		cntName, err := utils.ContainerNameFromNetworkMode(n.Config().NetworkMode)
+		if err != nil {
+			return err
+		}
+		err = dp.Nodes[cntName].ExecFunction(ctx, func(nn ns.NetNS) error {
+			// add the bridge
+			err := netlink.LinkAdd(&netlink.Bridge{
+				LinkAttrs: netlink.LinkAttrs{
+					Name: n.nameWithoutSeparatorSuffix(),
+				},
+			})
+			if err != nil {
+				return err
+			}
+			// retrieve link ref
+			netlinkLink, err := netlink.LinkByName(n.nameWithoutSeparatorSuffix())
+			if err != nil {
+				return err
+			}
+			// bring the link up
+			err = netlink.LinkSetUp(netlinkLink)
+			if err != nil {
+				return err
+			}
+			return nil
+		})
+		if err != nil {
+			return err
+		}
+	}
 	n.SetState(state.Deployed)
 	return nil
 }
@@ -71,20 +133,52 @@ func (*bridge) GetImages(_ context.Context) map[string]string { return map[strin
 func (b *bridge) DeleteNetnsSymlink() (err error) { return nil }
 
 func (b *bridge) PostDeploy(_ context.Context, _ *nodes.PostDeployParams) error {
+	if b.containerNs != "" {
+		return nil
+	}
 	return b.installIPTablesBridgeFwdRule()
 }
 
-func (b *bridge) CheckDeploymentConditions(_ context.Context) error {
-	err := b.VerifyHostRequirements()
+func (b *bridge) GetNSPath(ctx context.Context) (string, error) {
+	if b.containerNs != "" {
+		node, ok := b.nodesMap[b.containerNs]
+		if !ok {
+			return "", fmt.Errorf("unable to find node %s", b.containerNs)
+		}
+		return node.GetNSPath(ctx)
+	}
+	curns, err := ns.GetCurrentNS()
 	if err != nil {
-		return err
+		return "", err
 	}
-	// check bridge exists
-	_, err = utils.BridgeByName(b.Cfg.ShortName)
+	return curns.Path(), nil
+}
+
+func (b *bridge) CheckDeploymentConditions(ctx context.Context) error {
+	err := b.VerifyHostRequirements()
 	if err != nil {
 		return err
 	}
-	return nil
+
+	if strings.HasPrefix(b.Cfg.NetworkMode, "container:") {
+		if b.Cfg.NetworkMode[10:] != b.GetShortName()[len(b.nameWithoutSeparatorSuffix())+1:] {
+			return fmt.Errorf("container based bridge requires container name as suffix %s != %s", b.Cfg.NetworkMode[10:], b.GetShortName()[len(b.nameWithoutSeparatorSuffix())+1:])
+		}
+	}
+
+	// check bridge exists only if host ns
+	if b.containerNs == "" {
+		err = b.ExecFunction(ctx, func(nn ns.NetNS) error {
+			// check bridge exists
+			_, err = utils.BridgeByName(b.nameWithoutSeparatorSuffix())
+			if err != nil {
+				return err
+			}
+			return nil
+		})
+	}
+
+	return err
 }
 
 func (*bridge) PullImage(_ context.Context) error { return nil }
@@ -98,11 +192,41 @@ func (*bridge) GetContainers(_ context.Context) ([]runtime.GenericContainer, err
 // RunExec is a noop for bridge kind.
 func (b *bridge) RunExec(_ context.Context, _ *cExec.ExecCmd) (*cExec.ExecResult, error) {
 	log.Warnf("Exec operation is not implemented for kind %q", b.Config().Kind)
-
 	return nil, cExec.ErrRunExecNotSupported
 }
 
 func (b *bridge) AddLinkToContainer(ctx context.Context, link netlink.Link, f func(ns.NetNS) error) error {
+	if b.Cfg.NetworkMode != "" {
+		return b.addLinkToContainerNamespace(ctx, link, f)
+	}
+	return b.addLinkToContainerHost(ctx, link, f)
+}
+
+func (b *bridge) addLinkToContainerNamespace(ctx context.Context, link netlink.Link, f func(ns.NetNS) error) error {
+	cntName, err := utils.ContainerNameFromNetworkMode(b.Config().NetworkMode)
+	if err != nil {
+		return err
+	}
+	err = b.nodesMap[cntName].AddLinkToContainer(ctx, link, func(nn ns.NetNS) error {
+		// get the bridge as netlink.Link
+		br, err := netlink.LinkByName(b.nameWithoutSeparatorSuffix())
+		if err != nil {
+			return err
+		}
+
+		// assign the bridge to the link as master
+		err = netlink.LinkSetMaster(link, br)
+		if err != nil {
+			return err
+		}
+
+		// execute the given function
+		return f(nn)
+	})
+	return err
+}
+
+func (b *bridge) addLinkToContainerHost(_ context.Context, link netlink.Link, f func(ns.NetNS) error) error {
 	// retrieve the namespace handle
 	ns, err := ns.GetCurrentNS()
 	if err != nil {
@@ -110,7 +234,7 @@ func (b *bridge) AddLinkToContainer(ctx context.Context, link netlink.Link, f fu
 	}
 
 	// get the bridge as netlink.Link
-	br, err := netlink.LinkByName(b.Cfg.ShortName)
+	br, err := netlink.LinkByName(b.nameWithoutSeparatorSuffix())
 	if err != nil {
 		return err
 	}
@@ -126,6 +250,9 @@ func (b *bridge) AddLinkToContainer(ctx context.Context, link netlink.Link, f fu
 }
 
 func (b *bridge) GetLinkEndpointType() links.LinkEndpointType {
+	if b.containerNs != "" {
+		return links.LinkEndpointTypeBridgeNS
+	}
 	return links.LinkEndpointTypeBridge
 }
 
@@ -139,7 +266,7 @@ func (b *bridge) installIPTablesBridgeFwdRule() (err error) {
 	log.Debugf("setting up bridge firewall rules using %s as the firewall interface", f.Name())
 
 	r := definitions.FirewallRule{
-		Interface: b.Cfg.ShortName,
+		Interface: b.nameWithoutSeparatorSuffix(),
 		Direction: definitions.InDirection,
 		Action:    definitions.AcceptAction,
 		Comment:   definitions.ContainerlabComment,
@@ -152,7 +279,7 @@ func (b *bridge) installIPTablesBridgeFwdRule() (err error) {
 	}
 
 	r = definitions.FirewallRule{
-		Interface: b.Cfg.ShortName,
+		Interface: b.nameWithoutSeparatorSuffix(),
 		Direction: definitions.OutDirection,
 		Action:    definitions.AcceptAction,
 		Comment:   definitions.ContainerlabComment,
diff --git a/nodes/default_node.go b/nodes/default_node.go
index 0989e8d6..bf236582 100644
--- a/nodes/default_node.go
+++ b/nodes/default_node.go
@@ -168,7 +168,7 @@ func (d *DefaultNode) Deploy(ctx context.Context, _ *DeployParams) error {
 }
 
 // getNSPath retrieves the nodes nspath.
-func (d *DefaultNode) getNSPath(ctx context.Context) (string, error) {
+func (d *DefaultNode) GetNSPath(ctx context.Context) (string, error) {
 	var err error
 	nsp := ""
 
@@ -412,6 +412,7 @@ type NodeOverwrites interface {
 	GetContainerName() string
 	VerifyLicenseFileExists(context.Context) error
 	RunExec(context.Context, *exec.ExecCmd) (*exec.ExecResult, error)
+	GetNSPath(ctx context.Context) (string, error)
 }
 
 // LoadStartupConfigFileVr templates a startup-config using the file specified for VM-based nodes in the topo
@@ -553,7 +554,7 @@ func (d *DefaultNode) LoadOrGenerateCertificate(certInfra *cert.Cert, topoName s
 
 func (d *DefaultNode) AddLinkToContainer(ctx context.Context, link netlink.Link, f func(ns.NetNS) error) error {
 	// retrieve nodes nspath
-	nsp, err := d.getNSPath(ctx)
+	nsp, err := d.OverwriteNode.GetNSPath(ctx)
 	if err != nil {
 		return err
 	}
@@ -577,7 +578,7 @@ func (d *DefaultNode) AddLinkToContainer(ctx context.Context, link netlink.Link,
 // ExecFunction executes the given function in the nodes network namespace.
 func (d *DefaultNode) ExecFunction(ctx context.Context, f func(ns.NetNS) error) error {
 	// retrieve nodes nspath
-	nspath, err := d.getNSPath(ctx)
+	nspath, err := d.OverwriteNode.GetNSPath(ctx)
 	if err != nil {
 		return err
 	}
diff --git a/nodes/k8s_kind/k8s_kind.go b/nodes/k8s_kind/k8s_kind.go
index ed4c787e..42ee656b 100644
--- a/nodes/k8s_kind/k8s_kind.go
+++ b/nodes/k8s_kind/k8s_kind.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/charmbracelet/log"
 	"github.com/srl-labs/containerlab/clab/exec"
+	"github.com/srl-labs/containerlab/labels"
 	"github.com/srl-labs/containerlab/nodes"
 	"github.com/srl-labs/containerlab/runtime"
 	"github.com/srl-labs/containerlab/runtime/docker"
@@ -125,7 +126,7 @@ func (n *k8s_kind) GetContainers(ctx context.Context) ([]runtime.GenericContaine
 			cnt.Labels[key] = v
 		}
 		// we need to overwrite the nodename label
-		cnt.Labels["clab-node-name"] = cnt.Names[0]
+		cnt.Labels[labels.NodeName] = cnt.Names[0]
 	}
 
 	return containeList, nil
diff --git a/nodes/node.go b/nodes/node.go
index e5cc1eab..5c56738a 100644
--- a/nodes/node.go
+++ b/nodes/node.go
@@ -63,7 +63,9 @@ type PreDeployParams struct {
 }
 
 // DeployParams contains parameters for the Deploy function.
-type DeployParams struct{}
+type DeployParams struct {
+	Nodes map[string]Node
+}
 
 // PostDeployParams contains parameters for the PostDeploy function.
 type PostDeployParams struct {
@@ -78,7 +80,7 @@ type Node interface {
 	DeleteNetnsSymlink() (err error)
 	Config() *types.NodeConfig // Config returns the nodes configuration
 	// CheckDeploymentConditions checks if node-scoped deployment conditions are met.
-	CheckDeploymentConditions(context.Context) error
+	CheckDeploymentConditions(ctx context.Context) error
 	PreDeploy(ctx context.Context, params *PreDeployParams) error
 	Deploy(context.Context, *DeployParams) error // Deploy triggers the deployment of this node
 	PostDeploy(ctx context.Context, params *PostDeployParams) error
@@ -117,6 +119,7 @@ type Node interface {
 	RunExecFromConfig(context.Context, *exec.ExecCollection) error
 	IsHealthy(ctx context.Context) (bool, error)
 	GetContainerStatus(ctx context.Context) runtime.ContainerStatus
+	GetNSPath(ctx context.Context) (string, error)
 }
 
 type NodeOption func(Node)
diff --git a/utils/containers.go b/utils/containers.go
index e88cbe4f..579178a5 100644
--- a/utils/containers.go
+++ b/utils/containers.go
@@ -76,3 +76,12 @@ func DestinationBindMountExists(binds []string, dest string) bool {
 	}
 	return false
 }
+
+// ContainerNameFromNetworkMode takes the NetworkMode config string and returns the container name from it
+func ContainerNameFromNetworkMode(s string) (string, error) {
+	after, found := strings.CutPrefix(s, "container:")
+	if !found {
+		return "", fmt.Errorf("%s not a valid container reference", s)
+	}
+	return after, nil
+}
diff --git a/utils/file.go b/utils/file.go
index af8406af..db4009ea 100644
--- a/utils/file.go
+++ b/utils/file.go
@@ -6,6 +6,7 @@ package utils
 
 import (
 	"bufio"
+	"context"
 	"crypto/tls"
 	"errors"
 	"fmt"
@@ -22,7 +23,10 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
+	"time"
 
+	"github.com/minio/minio-go/v7"
+	"github.com/minio/minio-go/v7/pkg/credentials"
 	"github.com/steiler/acls"
 
 	"github.com/charmbracelet/log"
@@ -31,6 +35,7 @@ import (
 var (
 	errNonRegularFile = errors.New("non-regular file")
 	errHTTPFetch      = errors.New("failed to fetch http(s) resource")
+	errS3Fetch        = errors.New("failed to fetch s3 resource")
 )
 
 // FileExists returns true if a file referenced by filename exists & accessible.
@@ -61,7 +66,7 @@ func DirExists(filename string) bool {
 // mode is the desired target file permissions, e.g. "0644".
 func CopyFile(src, dst string, mode os.FileMode) (err error) {
 	var sfi os.FileInfo
-	if !IsHttpURL(src, false) {
+	if !IsHttpURL(src, false) && !IsS3URL(src) {
 		sfi, err = os.Stat(src)
 		if err != nil {
 			return err
@@ -129,15 +134,42 @@ func IsHttpURL(s string, allowSchemaless bool) bool {
 	return err == nil && u.Host != ""
 }
 
+// IsS3URL checks if the URL is an S3 URL (s3://bucket/key format).
+func IsS3URL(s string) bool {
+	return strings.HasPrefix(s, "s3://")
+}
+
+// ParseS3URL parses an S3 URL and returns the bucket and key.
+func ParseS3URL(s3URL string) (bucket, key string, err error) {
+	if !IsS3URL(s3URL) {
+		return "", "", fmt.Errorf("not an S3 URL: %s", s3URL)
+	}
+
+	u, err := url.Parse(s3URL)
+	if err != nil {
+		return "", "", err
+	}
+
+	bucket = u.Host
+	key = strings.TrimPrefix(u.Path, "/")
+
+	if bucket == "" || key == "" {
+		return "", "", fmt.Errorf("invalid S3 URL format: %s", s3URL)
+	}
+
+	return bucket, key, nil
+}
+
 // CopyFileContents copies the contents of the file named src to the file named
 // by dst. The file will be created if it does not already exist. If the
 // destination file exists, all it's contents will be replaced by the contents
 // of the source file.
-// src can be an http(s) URL as well.
+// src can be an http(s) URL or an S3 URL.
 func CopyFileContents(src, dst string, mode os.FileMode) (err error) {
 	var in io.ReadCloser
 
-	if IsHttpURL(src, false) {
+	switch {
+	case IsHttpURL(src, false):
 		client := NewHTTPClient()
 
 		// download using client
@@ -147,7 +179,52 @@ func CopyFileContents(src, dst string, mode os.FileMode) (err error) {
 		}
 
 		in = resp.Body
-	} else {
+
+	case IsS3URL(src):
+		bucket, key, err := ParseS3URL(src)
+		if err != nil {
+			return err
+		}
+
+		// Get region from environment, default to us-east-1
+		region := os.Getenv("AWS_REGION")
+		if region == "" {
+			region = "us-east-1"
+		}
+
+		// Create credential chain that mimics AWS SDK behavior
+		credProviders := []credentials.Provider{
+			&credentials.EnvAWS{},                                              // 1. Environment variables
+			&credentials.FileAWSCredentials{},                                  // 2. ~/.aws/credentials (default profile)
+			&credentials.IAM{Client: &http.Client{Timeout: 10 * time.Second}}, // 3. IAM role (EC2/ECS/Lambda)
+		}
+
+		// Create MinIO client with chained credentials
+		client, err := minio.New("s3.amazonaws.com", &minio.Options{
+			Creds:  credentials.NewChainCredentials(credProviders),
+			Secure: true,
+			Region: region,
+		})
+		if err != nil {
+			return fmt.Errorf("failed to create S3 client: %w", err)
+		}
+
+		// Get object from S3
+		object, err := client.GetObject(context.TODO(), bucket, key, minio.GetObjectOptions{})
+		if err != nil {
+			return fmt.Errorf("%w: %s: %v", errS3Fetch, src, err)
+		}
+
+		// Verify object exists by reading its stats
+		_, err = object.Stat()
+		if err != nil {
+			object.Close()
+			return fmt.Errorf("%w: %s: object not found or access denied: %v", errS3Fetch, src, err)
+		}
+
+		in = object
+
+	default:
 		in, err = os.Open(src)
 		if err != nil {
 			return err


Test patch:
diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml
index 0183e22c..5d575b6d 100644
--- a/.github/workflows/smoke-tests.yml
+++ b/.github/workflows/smoke-tests.yml
@@ -11,6 +11,9 @@ jobs:
   smoke-tests:
     runs-on: ubuntu-24.04
     timeout-minutes: 5
+    env:
+      AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }}
+      AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET }}
     strategy:
       fail-fast: false
       matrix:
@@ -41,6 +44,7 @@ jobs:
           - "21*.robot"
           - "22*.robot"
           - "23*.robot"
+          - "24*.robot"
 
     # allow podman job to fail, since it started to fail on github actions
     continue-on-error: ${{ matrix.runtime == 'podman' }}
@@ -90,7 +94,7 @@ jobs:
 
       - name: Run smoke tests
         run: |
-          bash ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/01-smoke/${{ matrix.test-suite }}
+          ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/01-smoke/${{ matrix.test-suite }}
 
       # upload test reports as a zip file
       - name: Upload test report
diff --git a/tests/01-smoke/17-cloned-lab.robot b/tests/01-smoke/17-cloned-lab.robot
index bd9c0831..004adeba 100644
--- a/tests/01-smoke/17-cloned-lab.robot
+++ b/tests/01-smoke/17-cloned-lab.robot
@@ -16,6 +16,9 @@ ${lab1-gitlab-url2}     https://github.com/hellt/clab-test-repo/blob/main/lab1.c
 ${lab2-gitlab-url}      https://github.com/hellt/clab-test-repo/tree/branch1
 ${http-lab-url}         https://gist.githubusercontent.com/hellt/66a5d8fca7bf526b46adae9008a5e04b/raw/034a542c3fbb17333afd20e6e7d21869fee6aeb5/linux.clab.yml
 ${single-topo-folder}   tests/01-smoke/single-topo-folder
+
+${s3-url}               s3://clab-integration/srl02-s3.clab.yml
+
 ${runtime}              docker
 
 
@@ -129,6 +132,17 @@ Test lab1 downloaded from https url
 
     Should Contain    ${output.stdout}    clab-alpine-l1
 
+Test lab downloaded from s3 url
+    ${output} =    Process.Run Process
+    ...    ${CLAB_BIN} --runtime ${runtime} deploy -t ${s3-url}
+    ...    shell=True
+
+    Log    ${output.stdout}
+    Log    ${output.stderr}
+
+    Should Be Equal As Integers    ${output.rc}    0
+
+    Should Contain    ${output.stdout}    clab-srl02-srl01
 
 Test deploy referencing folder as topo
     ${output_pre} =    Process.Run Process
diff --git a/tests/01-smoke/24-bridge-in-ns.clab.yaml b/tests/01-smoke/24-bridge-in-ns.clab.yaml
new file mode 100644
index 00000000..eed11f71
--- /dev/null
+++ b/tests/01-smoke/24-bridge-in-ns.clab.yaml
@@ -0,0 +1,18 @@
+name: "bridges-in-ns"
+topology:
+  nodes: 
+    br01|bp1:
+      kind: bridge
+      network-mode: container:bp1
+    bp1:
+      kind: linux
+      image: alpine:latest
+      exec:
+        - "ip address add dev br01 192.168.0.1/24"
+    c1:
+      kind: linux
+      image: alpine:latest
+      exec:
+        - "ip address add dev eth1 192.168.0.2/24"
+  links:
+     - endpoints: ["c1:eth1", "br01|bp1:c1eth1"]
\ No newline at end of file
diff --git a/tests/01-smoke/24-bridge-in-ns.robot b/tests/01-smoke/24-bridge-in-ns.robot
new file mode 100644
index 00000000..875a2a9c
--- /dev/null
+++ b/tests/01-smoke/24-bridge-in-ns.robot
@@ -0,0 +1,85 @@
+*** Settings ***
+Library             OperatingSystem
+Library             String
+Library             Collections
+Library             Process
+Resource            ../common.robot
+Library             Collections
+
+Suite Setup         Setup
+Suite Teardown      Run Keyword    Teardown
+
+
+*** Variables ***
+${lab-file}                     24-bridge-in-ns.clab.yaml
+${lab-name}                     bridges-in-ns
+${runtime}                      docker
+
+
+*** Test Cases ***
+Deploy ${lab-name} lab
+    ${output} =    Process.Run Process
+    ...    ${CLAB_BIN} --runtime ${runtime} deploy -t ${CURDIR}/${lab-file}
+    ...    shell=True
+
+    Log    stdout:${\n}${output.stdout}    console=${True}
+    Log    stderr:${\n}${output.stderr}    console=${True}
+
+    Should Be Equal As Integers    ${output.rc}    0
+
+    Set Suite Variable    ${deploylog}    ${output}
+
+Ensure bridge br01 exists in bp1 container and is up
+    [Documentation]    Ensure br01 is exists in container bp1 
+    ${result} =    Process.Run Process
+    ...    sudo -E docker exec clab-${lab-name}-bp1 ip link show dev br01
+    ...    shell=True
+
+    Log    ${result.stdout}
+    Log    ${result.stderr}
+
+    Should Be Equal As Integers    ${result.rc}    0
+
+    Should Contain    ${result.stdout}    UP,LOWER_UP
+
+Ensure veth of client is attached to br01
+    [Documentation]    Ensure client veth inteface c1eth1 is attached to the bridge br01
+    ${result} =    Process.Run Process
+    ...    sudo -E docker exec clab-${lab-name}-bp1 ip link show dev c1eth1
+    ...    shell=True
+
+    Log    ${result.stdout}
+    Log    ${result.stderr}
+
+    Should Be Equal As Integers    ${result.rc}    0
+
+    Should Contain    ${result.stdout}    state UP
+    Should Contain    ${result.stdout}    master br01
+
+Ensure communication between client c1 and bridge br01 in container bp1
+    [Documentation]    Ensure communication is possible from client to bp1.
+    ${result} =    Process.Run Process
+    ...    sudo -E docker exec clab-${lab-name}-c1 ping 192.168.0.1 -c 1
+    ...    shell=True
+    
+    Log    ${result.stdout}
+    Log    ${result.stderr}
+
+    Should Be Equal As Integers    ${result.rc}    0
+
+    Should Contain    ${result.stdout}    0% packet loss
+
+
+*** Keywords ***
+Teardown
+    Run    ${CLAB_BIN} --runtime ${runtime} destroy -c -t ${CURDIR}/${lab-file}
+
+Setup
+    ${output} =    Process.Run Process
+    ...    ${CLAB_BIN} --runtime ${runtime} destroy -c -t ${CURDIR}/${lab-file}
+    ...    shell=True
+
+    Log    ${output.stdout}
+    Log    ${output.stderr}
+    # skipping this test suite for podman for now
+    Skip If    '${runtime}' == 'podman'
\ No newline at end of file
diff --git a/tests/rf-run.sh b/tests/rf-run.sh
index 355f72cb..1368aa15 100755
--- a/tests/rf-run.sh
+++ b/tests/rf-run.sh
@@ -13,6 +13,9 @@ if [ -z "${CLAB_BIN}" ]; then
   CLAB_BIN=containerlab
 fi
 
+export AWS_ACCESS_KEY_ID
+export AWS_SECRET_ACCESS_KEY
+
 echo "Running tests with containerlab binary at $(which ${CLAB_BIN}) path and selected runtime: $1"
 
 COV_DIR=/tmp/clab-tests/coverage
diff --git a/utils/file_test.go b/utils/file_test.go
index 0dca21c0..1318c49e 100644
--- a/utils/file_test.go
+++ b/utils/file_test.go
@@ -138,3 +138,119 @@ func TestIsHttpURL(t *testing.T) {
 		})
 	}
 }
+
+func TestIsS3URL(t *testing.T) {
+	tests := []struct {
+		name string
+		url  string
+		want bool
+	}{
+		{
+			name: "Valid S3 URL",
+			url:  "s3://bucket/key/to/file.yaml",
+			want: true,
+		},
+		{
+			name: "Valid S3 URL with subdirectories",
+			url:  "s3://my-bucket/path/to/deep/file.cfg",
+			want: true,
+		},
+		{
+			name: "HTTP URL should not match",
+			url:  "https://example.com/file.yaml",
+			want: false,
+		},
+		{
+			name: "Local file path should not match",
+			url:  "/path/to/file.yaml",
+			want: false,
+		},
+		{
+			name: "Empty string should not match",
+			url:  "",
+			want: false,
+		},
+		{
+			name: "S3 without bucket/key should match",
+			url:  "s3://",
+			want: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := IsS3URL(tt.url); got != tt.want {
+				t.Errorf("IsS3URL() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestParseS3URL(t *testing.T) {
+	tests := []struct {
+		name       string
+		s3URL      string
+		wantBucket string
+		wantKey    string
+		wantErr    bool
+	}{
+		{
+			name:       "Valid S3 URL",
+			s3URL:      "s3://my-bucket/path/to/file.yaml",
+			wantBucket: "my-bucket",
+			wantKey:    "path/to/file.yaml",
+			wantErr:    false,
+		},
+		{
+			name:       "Valid S3 URL with single file",
+			s3URL:      "s3://bucket/file.cfg",
+			wantBucket: "bucket",
+			wantKey:    "file.cfg",
+			wantErr:    false,
+		},
+		{
+			name:       "Invalid - not an S3 URL",
+			s3URL:      "https://example.com/file",
+			wantBucket: "",
+			wantKey:    "",
+			wantErr:    true,
+		},
+		{
+			name:       "Invalid - missing bucket",
+			s3URL:      "s3:///file.yaml",
+			wantBucket: "",
+			wantKey:    "",
+			wantErr:    true,
+		},
+		{
+			name:       "Invalid - missing key",
+			s3URL:      "s3://bucket/",
+			wantBucket: "",
+			wantKey:    "",
+			wantErr:    true,
+		},
+		{
+			name:       "Invalid - missing both bucket and key",
+			s3URL:      "s3://",
+			wantBucket: "",
+			wantKey:    "",
+			wantErr:    true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotBucket, gotKey, err := ParseS3URL(tt.s3URL)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ParseS3URL() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if gotBucket != tt.wantBucket {
+				t.Errorf("ParseS3URL() gotBucket = %v, want %v", gotBucket, tt.wantBucket)
+			}
+			if gotKey != tt.wantKey {
+				t.Errorf("ParseS3URL() gotKey = %v, want %v", gotKey, tt.wantKey)
+			}
+		})
+	}
+}