Personal project Platform

N27 Network Lab

A browser-based network lab. Draw a topology, cable ports in a rack, deploy it with Containerlab and configure every device from a browser terminal.

My role
Designed and built it end to end, including the architecture, API, web app, host worker, CI workflow and documentation.
Context
Personal project · 2026
Status
v0.1.0 release candidate
Stack
  • Containerlab
  • Next.js
  • NestJS
  • Keycloak
  • PostgreSQL
  • RabbitMQ
  • Docker Compose
  • FRRouting
  • TypeScript
N27 topology workspace. A canvas shows a router and two Linux clients, the inspector shows client1's ports and addresses, and a browser terminal shows a successful ping.
Screenshot Local test run with sample data (organization, user and lab addresses are test values).

The problem

Tools like GNS3 and EVE-NG prove that browser-driven network labs work. Sharing one across a team usually means hand-written topology files, heavy VM images and a lab host where every user can reach everything.

N27 is a multi-organization lab platform for a single Linux host. People sign in, draw a topology, cable real device ports, deploy it with Containerlab and get real routers in a browser terminal. Nobody writes YAML. The API generates Containerlab topology files from validated, structured topology data. The lab devices themselves cannot reach the platform, the host or the internet.

How it works

  1. Build the topology. Drag FRRouting routers and Linux clients from the approved catalog onto the Canvas. Set addresses, default gateways and static routes in the inspector. Each save is an immutable revision.
  2. Connect devices. Click a port, then a port on another device. The same draft is also available as a 12-unit Rack with front-panel ports and as a keyboard- and phone-friendly List.
  3. Deploy with Containerlab. Deploying pins a saved revision. The API records the desired state and an outbox row in one transaction, and the host worker deploys the lab and reports progress back.
  4. Open terminals. Routers open the FRR CLI (vtysh) and clients open a Linux shell, next to the topology. Router configurations can be saved with checksums and restored on the next deploy.
Rack view: a 12-unit rack with an FRRouting router and two Alpine clients, front-panel ports and two cables, with the port inspector open on r1 eth1.
Screenshot Rack view. Cabling front-panel ports updates the Canvas and List views of the same draft.

Architecture

BrowserCanvas · Rack · List · xterm.js terminals
Control plane · Docker Compose · no Docker socket, nothing privileged
Traefiksingle entry point, static file routing
webNext.js

Backend-for-frontend: OIDC client, server-side sessions, no tokens in the browser

apiNestJS + Prisma

Organizations, roles, labs, immutable revisions, deployments, outbox

terminal-gatewayNestJS

Relays browser terminals with single-use tickets

KeycloakOIDC

Sign-in (code flow + PKCE); permissions stay in N27

PostgreSQLsource of truth

Desired state, revisions, audit log

RabbitMQquorum queues

Jobs to the worker, events back

GarageS3-compatible

Saved device configurations

Execution plane · the Linux host
workerhost process

Validated jobs only, fixed commands with argument arrays, no shell, no user YAML

Containerlabofficial image, pinned by digest

Deploy, destroy, readiness checks

Lab devicesnetwork-mode: none

FRRouting, Alpine, SR Linux; cabled only to each other

DiagramN27 components on one Linux host, derived from the project’s architecture documentation. The control plane never touches the Docker socket; only the host worker drives Containerlab.

The design separates a control plane, which decides what should exist and runs in Docker Compose without access to the Docker socket, from an execution plane: one worker process on the host that has the privileges Containerlab needs. The two meet at a narrow, typed message contract shared as a package (packages/contracts, Zod schemas for topology, jobs, events and terminals).

Decisions and trade-offs

Privileged work stays on the host worker

Containerlab needs root-equivalent Docker access. Giving that to a web-facing container would turn any application bug into a host compromise. The web app, API and terminal gateway never mount the Docker socket or run privileged. The worker accepts only structured, validated jobs and runs fixed commands with argument arrays, never a shell or user-supplied YAML. CI enforces the rule: the build fails if any Compose service mounts the socket or runs privileged.

Trade-off: the worker is a second deployment unit (a systemd user service) with its own diagnostics, and this boundary is not strong isolation between hostile tenants on one host.

Lab devices get no network except their cables

Every node runs with Docker network-mode: none, so there is no eth0 and no Docker network. Its only interfaces are Containerlab links to other nodes in the same lab. Consoles, readiness checks and configuration export go through docker exec. The result is isolation without editing the host’s firewall chains, checked by probes from inside lab nodes.

Trade-off: no SSH or web access to devices from outside the lab, and nodes still share the host kernel (FRR needs SYS_ADMIN).

RabbitMQ rather than Kafka, behind a transactional outbox

Lab operations are human-driven and low-volume. What matters is acknowledging a job only after it completes, redelivery and dead-lettering, which are work-queue semantics. Desired state lives in PostgreSQL; messages go out through an outbox, and consumers are idempotent by operation ID. One browser suite kills the worker 300 ms into a deploy to prove the lab still converges.

Trade-off: the broker keeps no replayable history; audit and deployment history live in PostgreSQL.

The worker connects out to the gateway, so it opens no inbound port. It trusts only a local CA created at bootstrap and keeps certificate verification on. Browsers get single-use tickets that last 60 seconds, are hashed at rest and are bound to the user, organization, deployment, node and origin. Membership is checked again every 5 seconds while a terminal is open.

Trade-off: certificate rotation is a documented manual step.

Immutable revisions and optimistic saves

Each save names the revision it started from. If someone saved in the meantime, the API returns 409 stale_revision instead of silently overwriting their work. Deployments reference an exact revision, and device catalog versions are immutable, so a topology saved today deploys the same devices later.

Trade-off: there is no automatic merge. The editor offers to keep the draft or reload.

Sign-in through a backend-for-frontend

Keycloak handles sign-in (authorization code flow with PKCE). The Next.js server is the OIDC client and keeps tokens in a server-side session, so the browser only holds an HTTP-only cookie. The API resolves organization membership and role (Owner, Admin, Operator, Viewer) on every request; organizations and permissions stay in N27’s own database.

Trade-off: sessions live in the web server’s memory, so restarting it signs everyone out.

Device catalog today

DeviceStatus
FRRouting 10.6.2Verified on the lab host
Alpine Linux 3.24.3 clientVerified on the lab host
Nokia SR Linux 26.7.2Verified. Runs privileged as a documented exception, flagged with a warning in the UI
Cisco IOL router and IOL-L2 switch (IOS XE 17.12.01)Defined. Blocked until a licensed image is imported and verified on the host
HPE Aruba AOS-CX 10.14Defined. Cannot run yet: it needs KVM inside the lab node, which the worker does not grant
FirewallsNone yet. An appliance is added only after it boots and passes readiness, console and traffic tests

Commercial images stay on the lab host and are never published.

Device images page listing FRRouting and the Alpine client as Verified, the Cisco IOL router and switch as Missing with import commands, and HPE Aruba AOS-CX as Missing because it needs KVM.
Screenshot Device images: each catalog entry states whether its exact image is present and verified on this host.

Verification

  • Local checks: format, lint, type checks and unit tests (make check), API integration tests on a real PostgreSQL with the restricted application role, and an end-to-end smoke test of the Compose stack.
  • Browser suites with real devices: a release suite that also covers interrupted deploys and destroys, replayed jobs, a degraded runtime, lab isolation, Viewer access and an offline worker. Separate suites cover the Rack, the Audit log and SR Linux (18 of 18 checks).
  • Backup and restore drill: restores a backup into a disposable PostgreSQL and object store, imports every artifact and loads a saved configuration onto a throwaway FRR device.
  • CI workflow: the same checks plus Compose validation for the default stack and every optional profile, the no-socket/no-privileged rule, and image builds without publishing.

Status and limits

v0.1.0 is a single-host, single-worker release candidate. Not implemented yet: live link telemetry, packet capture, VM appliances, per-organization quotas, multi-worker scheduling, pushed progress (pages poll), OpenBao integration and local TLS. After a host reboot, labs come back without cables and are marked for recovery.

Screenshots