Files
clab-webserver/README.md

227 lines
6.9 KiB
Markdown

# 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://<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:
```yaml
# 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.
```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/clab-webserver:latest`