6.9 KiB
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
- Participants are assigned a unique IPv4 or IPv6 prefix in the lab topology (e.g.
10.0.3.0/24). - 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. - All matching prefixes are merged (least- to most-specific), producing a set of template variables for that request.
- Every file under
docroot/is rendered as a Go template with those variables substituted before being returned astext/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:
- config:/app/config:ro
- docroot:/app/docroot:ro
env:
CLIENT_MAP: /app/config/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 config/client-map.yaml
# Build and run
go build -o clab-webserver .
./clab-webserver -client-map config/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/templateengine before being returned. - The
Content-Typeis alwaystext/plain; charset=utf-8. - Requests to
/are served fromdocroot/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.yamlunder a matching prefix becomes a template variable. - HTTP-derived variables (
remote_address,host,user_agent) are always set and cannot be overridden byclient-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;config/(containingclient-map.yaml) is expected as a bind-mounted directory.- All configuration is available as both CLI flags and environment variables.
- Image:
git.ipng.ch/ipng/clab-webserver:latest