# clab-webserver A lightweight, template-driven HTTP server designed to run as a node inside a [containerlab](https://containerlab.dev) 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:///`, 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: ```yaml # webserver.clab.yml name: my-lab topology: nodes: webserver: kind: linux image: git.ipng.ch/ipng-priv/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. ```yaml # 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 ```sh # 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-priv/clab-webserver:latest`