Skip to content

xsen84/orion

Repository files navigation

orion

A 100-node enterprise network lab running on a single MacBook M-series, with no hypervisor and no dedicated server.

  • 40+ Cisco IOL devices (L2/L3, real IOS 17.x): routers, distribution switches, access switches across 10 sites.
  • 60+ Linux endpoints (alpine containers): workstations, servers, NAS, printers, cameras, IP phones, video conf units, DNS, monitoring.
  • Real routing: OSPF area 0 inside each site, eBGP between sites, iBGP inside hubs, single NAT'd internet egress.
  • One YAML file (sites.yaml) drives the whole topology: add a site, change an ASN, scale endpoints — re-run the generator, redeploy.

Topology overview

Built and tested on a MacBook M4 inside an OrbStack Ubuntu machine. Should work on any Apple Silicon Mac with enough RAM; resource overhead is comparable to running ~100 small Docker containers (the IOL ones are the heavyweights at ~150 MB RSS each).


Why this stack

EVE-NG and GNS3 both struggle on Apple Silicon. Hypervisor mode chews RAM/CPU, bare-metal mode has its own driver headaches, and either way you hit a wall around 10–15 nodes before the VM becomes unusable. The combination that scales well past 100 nodes on a laptop:

Component Role
OrbStack Lightweight Linux VM on macOS. Replaces Docker Desktop; runs an Ubuntu machine with native-feeling overhead on M-series.
Containerlab Topology orchestrator. Wires nodes into a Docker bridge management network, brings them up in parallel.
vrnetlab cisco_iol Wraps the Cisco IOL binary in a container so containerlab can drive it.
Cisco IOL Real IOS 17.x as a userland binary. No KVM, no nested virtualization.
alpine-ssh Tiny endpoint image (~17 MB) with sshd + dnsmasq + bind-tools. One container per endpoint.

Prerequisites

A Mac with Apple Silicon (M1+), 16 GB RAM minimum (32 GB recommended for the full topology), and a few hours the first time you set things up.

1. OrbStack + Ubuntu machine

Install OrbStack: https://orbstack.dev/download.

Create a Linux machine inside it (from your Mac terminal):

orb create ubuntu lab

Drop into it whenever you need a shell:

orb shell lab

Everything below runs inside that Ubuntu machine, not on your Mac.

2. SSH into the Ubuntu machine (optional but convenient)

orb shell is enough for most things, but having real SSH makes the dashboard and NetBox easier to reach from your Mac browser.

# Inside the Ubuntu machine
sudo apt-get update
sudo apt-get install -y openssh-server
sudo systemctl enable --now ssh
sudo passwd $USER

# Find the machine's IP:
ip -4 addr show eth0 | awk '/inet /{print $2}' | cut -d/ -f1

From your Mac: ssh <user>@<ip>, or use the OrbStack alias ssh <user>@lab.orb.local.

3. Base packages

Python 3 is preinstalled but python3-venv and python3-pip aren't:

sudo apt-get install -y make git curl ca-certificates python3-venv python3-pip

4. Docker + buildx + compose

sudo apt-get install -y docker.io docker-buildx docker-compose-v2
sudo usermod -aG docker $USER
# log out and back in (or `newgrp docker`) so the group membership takes effect
docker run --rm hello-world      # smoke test
docker compose version           # should print "Docker Compose v2.x"

docker-buildx is needed for the vrnetlab cross-arch build in step 7. docker-compose-v2 is the docker compose (with a space) plugin; NetBox uses it.

5. Containerlab

bash -c "$(curl -sL https://get.containerlab.dev)"
containerlab version

6. Cisco IOL binaries

Obtain these yourself — they're Cisco proprietary and not redistributed by this repo:

  • x86_64_crb_linux-adventerprisek9-ms.iol (L3 / router)
  • x86_64_crb_linux_l2-adventerprisek9-ms.iol (L2 / switch)

If you have a Cisco Modeling Labs license, the binaries are in the CML refplat under iol-xe-x.y.z/ and ioll2-xe-x.y.z/.

7. Build the vrnetlab cisco_iol images

git clone https://github.com/hellt/vrnetlab.git
cd vrnetlab/cisco/iol

# Drop your .bin files in with the naming the makefile expects:
cp /path/to/x86_64_crb_linux-adventerprisek9-ms.iol     cisco_iol-17.16.01.bin
cp /path/to/x86_64_crb_linux_l2-adventerprisek9-ms.iol  cisco_iol-L2-17.16.01.bin

# Build (the platform override is critical on Apple Silicon):
DOCKER_PLATFORM=linux/amd64 make docker-image

After ~3 minutes:

$ docker images | grep cisco_iol
vrnetlab/cisco_iol   17.16.01      ...
vrnetlab/cisco_iol   L2-17.16.01   ...

Why DOCKER_PLATFORM=linux/amd64: the vrnetlab Dockerfile installs iouyap from an apt repo that only publishes amd64. A plain make docker-image on Apple Silicon runs the build as arm64 and apt fails with E: Unable to locate package iouyap. The override routes the build through docker buildx build --platform=linux/amd64 (hence step 4's docker-buildx). The IOL binary itself is x86_64, so the resulting container runs amd64 via OrbStack's Rosetta translation.

8. Build the alpine-ssh endpoint image

Used for every Linux endpoint and the two DNS containers (they need dnsmasq in the same image):

cat > /tmp/Dockerfile <<'EOF'
FROM alpine:latest
RUN apk add --no-cache openssh dnsmasq bind-tools tcpdump iputils \
    && ssh-keygen -A \
    && echo 'PermitRootLogin yes'        >> /etc/ssh/sshd_config \
    && echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config \
    && adduser -D admin && echo 'admin:admin' | chpasswd
EOF
docker build -t alpine-ssh:latest -f /tmp/Dockerfile /tmp

bind-tools gives you dig/nslookup/host; tcpdump and iputils round out the troubleshooting kit on every endpoint.


Quick start

All commands run inside the Ubuntu machine from Prerequisites step 1.

# 1. Clone
git clone https://github.com/xsen84/orion.git
cd orion

# 2. Generate the host-specific IOL license file
python3 scripts/gen-iourc.py
# -> writes ./iourc  (gitignored)

# 3. Bring up the lab (3–5 min for IOS to boot and BGP to converge)
sudo containerlab deploy --topo topology.clab.yml

# 4. Smoke-test reachability
./tests/connectivity.sh

# 5. Start the dashboard
cd dashboard && ./run.sh
# Then open http://<ubuntu-vm-ip>:5001 in your Mac browser.
# (or http://lab.orb.local:5001 via the OrbStack hostname alias)

Tear it down:

sudo containerlab destroy --topo topology.clab.yml --cleanup

Verifying the deployment

After the deploy finishes, the connectivity script should report 0 failures:

$ ./tests/connectivity.sh | tail -3
DUB-WS-1               172.20.20.138    | OK         | OK
DUB-WS-2               172.20.20.139    | OK         | OK
DUB-SRV-1              172.20.20.140    | OK         | OK

OK: 100   FAIL: 0

A few one-liners that exercise the real routing/DNS stack:

# BGP summary on the egress router — 4 eBGP peers (one per spoke) + 1 iBGP
$ ssh admin@172.20.20.11 'show ip bgp summary' | tail -6
Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
10.0.0.2        4        65011      14      13       26    0    0 00:01:48       11
10.0.1.2        4        65012       9      13       26    0    0 00:01:46        3
10.0.1.6        4        65013       8      13       26    0    0 00:01:42        3
10.0.1.10       4        65014       8      13       26    0    0 00:01:15        3
10.0.3.2        4        65010      12      12       26    0    0 00:01:46       10

# Cross-site path — Tokyo workstation reaches Dubai server through 5 hops
$ docker exec -u root clab-orion-enterprise-tyo-ws-1 traceroute -n 10.110.10.151
 1  10.70.0.1                  ← tyo-r1 (Tokyo LAN gateway)
 2  10.0.1.17                  ← buc-r2 (Tokyo's BGP peer at the hub)
 3  10.0.3.1                   ← buc-r1 (iBGP within Bucharest)
 4  10.0.0.2                   ← fra-r1 (BUC↔FRA hub interconnect)
 5  10.0.2.10                  ← dub-r1 (Frankfurt's spoke to Dubai)
 6  10.110.10.151              ← dub-srv-1 (destination)

# DNS resolution works across sites
$ docker exec clab-orion-enterprise-tyo-ws-1 dig +short @10.10.100.53 buc-srv-1.orion.lab
10.10.10.151

# NAT on the egress router — workstation IP rewritten to the mgmt IP
$ docker exec clab-orion-enterprise-tyo-ws-1 ping -c1 1.1.1.1 > /dev/null
$ ssh admin@172.20.20.11 'show ip nat translations'
Pro Inside global      Inside local       Outside local      Outside global
icmp 172.20.20.11:1024 10.70.10.101:11552 1.1.1.1:11552      1.1.1.1:1024

If you see those, the lab is fully operational. If not, jump to Troubleshooting.


What gets deployed

10 sites, 100 containers:

Site Code Role AS LAN Routers Dist Access Endpoints
Bucharest BUC Hub (primary) 65010 10.10.0.0/16 2 2 3 9 + DNS + monitor
Frankfurt FRA Hub (secondary) 65011 10.20.0.0/16 2 2 3 9 + DNS
Pune PNQ Regional 65012 10.30.0.0/16 1 1 2 5
Denver DEN Regional 65013 10.40.0.0/16 1 1 2 5
Paris PAR Regional 65014 10.50.0.0/16 1 1 2 5
Singapore SIN Regional 65015 10.60.0.0/16 1 1 2 5
Tokyo TYO Regional 65016 10.70.0.0/16 1 1 2 5
Stockholm STO Branch 65020 10.90.0.0/16 1 1 1 3
Toronto TOR Branch 65021 10.100.0.0/16 1 1 1 3
Dubai DUB Branch 65022 10.110.0.0/16 1 1 1 3
                        ┌─────────────────────────────────────────┐
                        │           WAN BACKBONE (eBGP)           │
                        └─────────────────────────────────────────┘
                                │                   │
                    ┌───────────┴────────────┐ ┌────┴────────────────┐
                    │  BUCHAREST (Hub)       │ │  FRANKFURT (Hub)    │
                    │  AS 65010              │ │  AS 65011           │
                    │  R1 ◀── Internet egress│ │  R1, R2 (iBGP)      │
                    │  R2  (iBGP)            │ │  DSW1, DSW2         │
                    │  DSW1, DSW2            │ │  ASW1, ASW2, ASW3   │
                    │  ASW1, ASW2, ASW3      │ │  DNS Secondary      │
                    │  DNS Primary, Monitor  │ │                     │
                    └──┬──────┬──────┬───────┘ └──┬──────┬──────┬───┘
                       │      │      │            │      │      │
                   ┌───┘  ┌───┘   ┌──┘         ┌──┘   ┌──┘   ┌──┘
            ┌──────┴─┐┌───┴──┐┌───┴───┐┌───────┴┐┌────┴──┐┌──┴───┐
            │ Pune   ││Denver││ Paris ││Stockh. ││Toronto││ Dubai│
            │ AS65012││65013 ││ 65014 ││ 65020  ││ 65021 ││65022 │
            └────────┘└──────┘└───────┘└────────┘└───────┘└──────┘
            + Singapore (AS 65015) via BUC-R2
            + Tokyo     (AS 65016) via BUC-R2

Routing model

Two planes, kept deliberately separate.

Management plane — 172.20.20.0/24. Every node has an eth0 here. It's a flat L2 segment provided by containerlab. You use it for SSH into routers/switches and for the dashboard to ping devices. It is never used for transit by lab traffic. Every router does have 172.20.20.0/24 as a directly-connected route on its own mgmt interface (clab attaches that interface, you can't hide it), but the interface is passive-interface in OSPF so no hellos leak there, and the only static default that exists anywhere is on the single egress router. A spoke router has no path to the mgmt bridge from the data plane — only the egress can NAT out via eth0.

Data plane — 10.0.0.0/8.

  • Endpoints' default route is via <site-router-LAN-IP> dev eth1. Everything except management traffic enters the lab fabric.
  • Inside a site: OSPF area 0 between site router(s), distribution switches, and (via SVI) access switches.
  • Between sites: eBGP on /30 WAN links.
  • Inside a hub: iBGP between the two routers.
  • The data plane has no internet route of its own. Internet egress is single-pointed (see below).

Single internet egress

defaults.internet_egress_router in sites.yaml names the one router that NATs lab traffic to the outside world. buc-r1 by default.

The generator emits on the egress router only:

ip access-list standard LAB-NAT
 permit 10.0.0.0 0.255.255.255
!
ip nat inside source list LAB-NAT interface Ethernet0/0 overload
!
interface Ethernet0/0          ! management bridge — outside
 ip nat outside
interface Ethernet0/1          ! site LAN — inside
 ip nat inside
interface Ethernet0/2..N       ! every WAN link — inside
 ip nat inside
!
router bgp 65010
 address-family ipv4 unicast
  neighbor <each-eBGP-peer> default-originate
!
ip route 0.0.0.0 0.0.0.0 172.20.20.1

Every other router has no static default and learns 0.0.0.0/0 via eBGP from the egress router. No site can shortcut to the mgmt bridge — all internet traffic transits Bucharest.

A trace from a Tokyo workstation:

$ docker exec -u root clab-orion-enterprise-tyo-ws-1 traceroute -n 1.1.1.1
 1  10.70.0.1     ← tyo-r1 LAN gateway (eth1)
 2  10.0.1.17     ← buc-r2 WAN end of the TYO↔BUC link
 3  10.0.3.1      ← buc-r1 iBGP end of the BUC R1↔R2 link
 4  172.20.20.1   ← clab mgmt bridge (post-NAT egress)
 5+ ...           ← VM, host, internet

To shift the egress (failover testing, or to land traffic in Europe): set internet_egress_router: fra-r1 in sites.yaml, regenerate, redeploy.


Dashboard

dashboard/ is a small Flask + vanilla JS app that auto-parses topology.clab.yml, pings every node every few seconds via the mgmt bridge, opens a TCP connection to port 22 to check SSH, and reads the sibling sites.yaml for site names, roles, and ASNs.

cd dashboard
./run.sh                          # creates .venv, installs deps, runs app.py
# -> http://<ubuntu-vm-ip>:5001

It needs network access to the clab mgmt bridge (172.20.20.0/24), so run it from the same Ubuntu machine that hosts the lab.

Per-site device list

Dashboard against a different topology

CLAB_FILE overrides which topology the dashboard reads:

CLAB_FILE=$PWD/../examples/triangle/topology.clab.yml ./run.sh

The dashboard picks up the topology name and per-site metadata automatically:

Dashboard rendering the triangle example


NetBox (optional)

NetBox runs as a separate Docker stack — it's not part of the clab topology. Useful for browsing the deployed inventory, looking up IPs, or plugging into automation tooling.

cd netbox
docker compose up -d
sleep 60                  # first-run migrations
./populate-netbox.sh      # pushes deployed inventory into NetBox

Open NetBox in your Mac browser:

From URL
Mac → VM (IP) http://<ubuntu-vm-ip>:8000
Mac → VM (alias) http://lab.orb.local:8000
Inside the VM http://localhost:8000

Login: admin / admin (from netbox/env/netbox.env).

NetBox sites view

netbox/env/netbox.env and netbox/env/postgres.env ship with working lab credentials. They're committed on purpose so the stack just runs — change them if you ever expose this lab beyond your machine.

populate-netbox.py reads the runtime clab artifacts and pushes sites, device types, devices, interfaces, and IPs into NetBox via the API. Re-run it after every redeploy. It's idempotent across runs and topologies — point it at any topology.clab.yml via CLAB_YAML=....

docker compose up -d only prints container names, no URL — that's compose v2's behaviour. Use docker compose ps to confirm all six services (netbox, netbox-worker, netbox-housekeeping, postgres, redis, redis-cache) are Up. The netbox container shows health: starting for the first minute while migrations run.


DNS

Two dnsmasq containers serve the orion.lab zone:

  • Primarybuc-dns-1 at 10.10.100.53. Authoritative for orion.lab; forwards everything else to 8.8.8.8.
  • Secondaryfra-dns-1 at 10.20.100.53. Same zone; forwards non-zone queries to the primary.

Every Linux endpoint gets nameserver 10.10.100.53 and search orion.lab written into /etc/resolv.conf at boot (via the containerlab dns: field — not the exec block, which Docker overrides). Routers and switches get ip name-server 10.10.100.53 in their config, so ping buc-srv-1.orion.lab from a router CLI resolves.

# From a router:
ssh admin@172.20.20.11
> ping buc-srv-1.orion.lab

# From any endpoint:
docker exec -it clab-orion-enterprise-buc-ws-1 nslookup fra-srv-1.orion.lab

The host map is dns/hosts.orion. Regenerate it after editing sites.yaml:

python3 scripts/generate.py --target dns

Default credentials

Where User Password How to change
IOS devices (routers + switches) admin admin defaults.admin_password in sites.yaml, then regenerate
Linux endpoints (alpine-ssh) admin admin Rebuild alpine-ssh:latest with a different chpasswd line
NetBox web UI admin admin Edit netbox/env/netbox.env
NetBox API token 0123456789abcdef0123456789abcdef01234567 Same file (SUPERUSER_API_TOKEN)

Lab defaults. Change them before exposing the lab to anything beyond your machine.


Testing multiple configurations — the IaC loop

This is the workflow the lab is built around. Edit one YAML file, regenerate, redeploy.

# 1. Edit sites.yaml — add a site, change an ASN, scale endpoints, change links.
$EDITOR sites.yaml

# 2. Regenerate configs + topology + DNS + connectivity test
python3 scripts/generate.py

# 3. See what changed
git diff configs/ topology.clab.yml dns/ tests/

# 4. Redeploy
sudo containerlab destroy --topo topology.clab.yml --cleanup
sudo containerlab deploy  --topo topology.clab.yml

# 5. Re-test
./tests/connectivity.sh

scripts/generate.py --check renders in memory and reports which files would change (no writes). --target configs topology dns tests limits which artifact groups are written.

What you can change in sites.yaml without touching any code

  • Add a new site — append to sites: with a role (hub/regional/branch), asn, router_id, lan prefix, mgmt_start IP. List its access switches and endpoint counts. Add wan_links entries to attach it.
  • Resize a site — bump endpoints counts under each access switch.
  • Reshape the WAN — edit wan_links; every link is a from / to / subnet triple.
  • Move internet egress — change defaults.internet_egress_router (e.g., buc-r1fra-r1). NAT and BGP default-originate move with it.
  • Change credentialsdefaults.admin_user / defaults.admin_password.

What's hardcoded in scripts/generate.py (by design)

  • Single OSPF area 0 within each site.
  • eBGP between sites, iBGP inside hubs, redistribution connected + ospf 1.
  • Interface numbering: e0/0 mgmt, e0/1 LAN/uplink, e0/2+ WAN.
  • Endpoint LAN host octets per type (workstations .10x, printers .11x, …) — see ENDPOINT_CATALOG at the top of scripts/generate.py.

Change those if you need to — the generator is ~500 lines and intentionally straightforward.


Smaller example topologies

The full 100-node lab is the default. For something quicker to boot, or to try a different network shape, examples/ ships four self-contained sites.yaml files:

Example Sites Containers Shape
two-site 2 ~13 Minimal HQ ↔ Remote, single WAN link
hub-spoke 4 ~28 HQ + 3 branches, no inter-branch links
triangle 3 ~19 Full mesh between 3 sites
chain 3 ~19 Linear west ↔ center ↔ east (transit through center)

Each example has its own sites.yaml plus generated topology.clab.yml, configs/, dns/, and tests/. To use one:

# Destroy the main lab first if it's up — they share the mgmt /24
sudo containerlab destroy --topo topology.clab.yml --cleanup

# Pick one and deploy
cd examples/two-site
sudo containerlab deploy --topo topology.clab.yml

# Dashboard against this topology
cd ../../dashboard
CLAB_FILE=$PWD/../examples/two-site/topology.clab.yml ./run.sh

# NetBox populate against this topology
cd ../netbox
CLAB_YAML=$PWD/../examples/two-site/topology.clab.yml ./populate-netbox.sh --populate

Edit + regenerate an example:

python3 scripts/generate.py --sites examples/two-site/sites.yaml \
                            --out   examples/two-site/

All examples set defaults.iourc_path: ../../iourc so they pick up the repo-root iourc when deployed from the example directory.


Repo layout

orion/
├── sites.yaml              # The source of truth — edit this
├── topology.clab.yml       # Generated. Don't edit by hand.
├── configs/                # Generated IOS configs (router, dist, access)
├── dns/
│   ├── dnsmasq.conf            # Generated
│   ├── dnsmasq-secondary.conf  # Generated
│   └── hosts.orion             # Generated
├── tests/                  # Generated reachability tests + curated extras
│   ├── connectivity.sh
│   ├── routing.sh
│   ├── dns.sh
│   └── failover.sh
├── scripts/
│   ├── generate.py         # sites.yaml -> all generated artifacts
│   └── gen-iourc.py        # produces ./iourc bound to your host
├── examples/               # Smaller alternative topologies (see above)
│   ├── two-site/
│   ├── hub-spoke/
│   ├── triangle/
│   └── chain/
├── dashboard/              # Flask + JS dashboard (independent of clab)
├── netbox/                 # Optional NetBox docker-compose stack
└── docs/screenshots/       # README images

iourc and clab-orion-enterprise/ are gitignored (host-specific license file + clab runtime state).


Troubleshooting

  • IOL ERROR: License entry missingiourc doesn't match the host's hostname/hostid hash. Regenerate: python3 scripts/gen-iourc.py.
  • Nodes stuck in health: starting — IOL takes 60–120 s per device to boot. Watch with sudo containerlab inspect --name orion-enterprise.
  • BGP not converging — give it 2–3 minutes after deploy. Check with ssh admin@172.20.20.11 'show ip bgp summary'.
  • No such image: vrnetlab/cisco_iol:17.16.01 — see Prerequisites step 7.
  • E: Unable to locate package iouyap during the vrnetlab build — you forgot DOCKER_PLATFORM=linux/amd64. See step 7.
  • unknown flag: --platform during the vrnetlab build — docker-buildx plugin missing. sudo apt-get install -y docker-buildx.
  • alpine-ssh:latest not found — see Prerequisites step 8.
  • permission denied while trying to connect to the Docker daemon socket — shell hasn't picked up the docker group. newgrp docker or new shell.
  • ensurepip is not available when dashboard/run.sh runs — sudo apt-get install -y python3-venv python3-pip.
  • unknown shorthand flag: 'd' in -d from docker compose — the compose v2 plugin isn't installed. sudo apt-get install -y docker-compose-v2.
  • Dashboard shows everything but can't reach nodes — make sure you're running it inside the same Ubuntu machine that hosts clab. The mgmt bridge isn't reachable from your Mac directly.
  • Endpoint ping google.com returns IPv6 then 100% loss — busybox ping prefers AAAA, and the lab has no routable IPv6. Either ping -4 google.com per-shot, or add filter-AAAA to dns/dnsmasq.conf.
  • Endpoint traceroute fails with Operation not permitted — needs CAP_NET_RAW. Run as root: docker exec -u root <container> traceroute -n <target>. Or rebuild alpine-ssh adding setcap cap_net_raw+ep /usr/bin/traceroute.
  • Endpoint can't reach the internet — check the egress router is up (docker ps | grep buc-r1), then verify a spoke has a BGP-learned default: ssh admin@172.20.20.50 'show ip route 0.0.0.0' should show 0.0.0.0/0 [20/0] via <BGP-peer> (B = BGP route).

License

The lab code (configs, generator, dashboard, docker-compose, docs) is MIT licensed — see LICENSE.

Third-party components keep their own licenses:

  • Cisco IOL — Cisco proprietary. You must obtain the binary yourself; it is not redistributed by this repo.
  • vrnetlab — see https://github.com/hellt/vrnetlab.
  • Containerlab — Apache-2.0.
  • NetBox — Apache-2.0.

The scripts/gen-iourc.py algorithm is a publicly documented Cisco IOL license computation, included for convenience for users who already possess a valid IOL binary.

About

Multi-site network lab for Apple Silicon: real Cisco IOS, full OSPF/BGP, NAT, DNS, NetBox. Built on containerlab + vrnetlab + OrbStack.

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors