clab-webserver

A lightweight, template-driven HTTP server designed to run as a node inside a containerlab topology. It serves personalised plain-text pages to participants based on their source IP address, making it easy to hand out unique tokens, course names, or any per-prefix data during instructor-led lab sessions.


How it works

  1. Participants are assigned a unique IPv4 or IPv6 prefix in the lab topology (e.g. 10.0.3.0/24).
  2. When a participant runs curl http://<webserver-ip>/, the server looks up their source address against a prefix map (client-map.yaml) using longest-prefix match.
  3. All matching prefixes are merged (least- to most-specific), producing a set of template variables for that request.
  4. Every file under docroot/ is rendered as a Go template with those variables substituted before being returned as text/plain.

Catch-all entries (0.0.0.0/0, ::/0) provide defaults for all clients; more-specific prefixes override individual variables for specific participants.


Quickstart — adding to a containerlab topology

Add clab-webserver as a linux node and bind-mount your config files:

# webserver.clab.yml
name: my-lab

topology:
  nodes:
    webserver:
      kind: linux
      image: git.ipng.ch/ipng/clab-webserver:latest
      binds:
        - client-map.yaml:/app/client-map.yaml:ro
        - docroot:/app/docroot:ro
      env:
        CLIENT_MAP: /app/client-map.yaml
        DOCROOT: /app/docroot
        LISTEN: ":80"
      ports:
        - "80:80"

    participant1:
      kind: linux
      image: alpine:latest

  links:
    - endpoints: ["webserver:eth1", "participant1:eth1"]

The server starts automatically as the container entrypoint — no extra configuration needed.


client-map.yaml

This file maps IPv4/IPv6 prefixes to template variables. Prefix matching is longest-match; all matching entries are merged with more-specific values winning.

# Default for all IPv4 clients
0.0.0.0/0:
  course: "My Lab"
  client_map_token: "not set"

# Default for all IPv6 clients
::/0:
  course: "My Lab"
  client_map_token: "not set"

# Per-participant overrides
10.0.1.0/24:
  client_map_token: "swift grey pirate"

2001:db8:1::/48:
  client_map_token: "swift grey pirate"

# A prefix can override any variable, including course
2001:db8:2::/48:
  client_map_token: "brave lime falcon"
  course: "Advanced Track"

A client at 2001:db8:2::1 would receive course = "Advanced Track" and client_map_token = "brave lime falcon", merged from ::/0 and 2001:db8:2::/48.

The file is watched at runtime — edits take effect within 5 seconds without restarting the container.


docroot/

Static files served from this directory are processed as Go templates. Any {{ .variable }} reference is substituted with the value from the merged prefix-match result for that client.

Three variables are always available regardless of client-map.yaml:

Variable Value
{{ .remote_address }} Client IP address
{{ .host }} HTTP Host header
{{ .user_agent }} HTTP User-Agent header

A request to / serves docroot/index.txt. Subdirectories are supported.

If a template references a variable that has no value for a given client, the variable renders as an empty string and a warning is logged.

Example docroot/index.txt:

Welcome, {{ .course }} participant from {{ .remote_address }}!
I see you are using "{{ .user_agent }}".

Your token is "{{ .client_map_token }}"

You may write it in your workbook and give to your teacher. Thank you for
participating in the {{ .course }} session.

Configuration

All options can be set via CLI flag or environment variable. Flags take precedence.

Flag Env var Default Description
-client-map CLIENT_MAP client-map.yaml Path to prefix/variable map
-docroot DOCROOT docroot Directory of template files to serve
-listen LISTEN :80 Listen address

Building and running locally

# Run directly
go run . -client-map client-map.yaml

# Build and run
go build -o clab-webserver .
./clab-webserver -client-map client-map.yaml -listen :8080

# Docker
docker compose build
docker compose up

Logging

All requests are logged to stdout in nginx combined-log format:

10.0.1.5 - - [03/Apr/2026:14:22:01 +0200] "GET /index.txt HTTP/1.1" 200 312 "-" "curl/7.88.1"

Warnings for missing template variables appear as structured log lines:

2026/04/03 14:22:01 WARNING: template variable .client_map_token has no value for client 10.0.3.1

Appendix — Specification

Purpose

A small, portable Go HTTP server for use in distributed containerlab exercises. Each participant is assigned a unique IPv4 and/or IPv6 prefix. When they send a request to the server, they receive a personalised plain-text response identifying them and returning a unique token they can record and submit.

File serving

  • Files are served from a configurable docroot/ directory.
  • All files are rendered through Go's text/template engine before being returned.
  • The Content-Type is always text/plain; charset=utf-8.
  • Requests to / are served from docroot/index.txt.
  • Path traversal attempts are rejected with 403 Forbidden.

Client map

  • Configured via a single YAML file (-client-map / $CLIENT_MAP).
  • Top-level keys are IPv4 or IPv6 CIDR prefixes.
  • Values are dictionaries of variable_name: "string value".
  • On each request, all prefixes that contain the client IP are collected, sorted least- to most-specific, and merged. More-specific entries overwrite less-specific ones for any shared key.
  • Variables produced by the merge are injected into the template alongside the HTTP-derived variables (remote_address, host, user_agent), which always win.
  • IPv4-mapped IPv6 addresses (from dual-stack sockets) are normalised to IPv4 before matching.
  • The file is polled every 5 seconds. A changed file is reloaded atomically; a file with invalid YAML is rejected with a log warning and the previous data is kept in service.

Template variables

  • Any key defined in client-map.yaml under a matching prefix becomes a template variable.
  • HTTP-derived variables (remote_address, host, user_agent) are always set and cannot be overridden by client-map.yaml.
  • Template variables with no value for a given client default to "" and a warning is logged.

Logging

  • All requests are logged to stdout in nginx combined-log format after the response is sent.
  • Warnings and errors are written to stdout via the standard Go logger.

Deployment

  • Packaged as a multi-stage Docker image based on Alpine.
  • docroot/ is baked into the image; client-map.yaml is expected as a bind mount.
  • All configuration is available as both CLI flags and environment variables.
  • Image: git.ipng.ch/ipng/clab-webserver:latest
Description
A lightweight, template-driven HTTP server for containerlab
Readme 44 KiB
Languages
Go 95.2%
Dockerfile 4.8%