How to Add a New Challenge
This guide walks you through building a SimNet challenge from scratch. Each challenge consists of one Markdown page and one YAML config file — no Vue components or Rust code required.
Overview
Minimum layout of a challenge:
docs/challenges/NN-your-feature/
├── index.md # Page content + frontmatter
└── config.yaml # Topology, events, flag definitionThroughout this guide,
NN-your-featureand<your-slug>are placeholders used to illustrate file structure — they are not directories that exist in the repo. Substitute your own chosen challenge slug.
Step 1: Create the Directory
Create a new directory under docs/challenges/ using the naming pattern NN-slug:
mkdir docs/challenges/NN-your-featureNNis a two-digit ordinal that determines the ordering of challenges.slugis a lower-case kebab-case identifier that briefly describes the topic.
Step 2: Write index.md
Create docs/challenges/<your-slug>/index.md with the following frontmatter:
---
layout: challenge
title: "Your Challenge Title"
difficulty: 3
challengeSlug: "<your-slug>"
config: ./config.yaml
---Frontmatter Fields
| Field | Type | Required | Description |
|---|---|---|---|
layout | string | yes | Must be challenge; selects the challenge page layout |
title | string | yes | Challenge title, shown in the nav badge |
difficulty | number | yes | Difficulty rating (1-5), rendered as Lv.N |
challengeSlug | string | yes | Unique challenge identifier; must match the directory name |
config | string | yes | Relative path to the config YAML |
Markdown Body
Markdown content below the frontmatter is injected into SimNet's Mission Briefing panel via a Vue slot. A recommended structure:
## Mission Briefing
Describe the scenario and backstory...
## Objective
Clearly state what learners need to achieve.
## Hints
1. First hint
2. Second hintStep 3: Create config.yaml
config.yaml is the core configuration file of a challenge. It defines the network topology, enabled protocols, event system, and win condition.
Full Structure
# Challenge metadata
challenge:
id: "<your-slug>"
title: "Your Challenge Title"
difficulty: 3
flag:
value: "{{FLAG_PLACEHOLDER}}"
validation: sha256
# learning_objectives accepts either a single-locale array or a
# bilingual locale map — see the next section.
learning_objectives:
en:
- "First learning objective"
- "Second learning objective"
zh-TW:
- "第一條學習目標"
- "第二條學習目標"
# Network topology
topology:
nodes:
- id: pc1
type: pc
label: PC1 (You)
ip: 192.168.1.1
mac: "aa:bb:cc:dd:ee:01"
player: true
- id: sw1
type: switch
label: Switch1
- id: server
type: server
label: Example Server
ip: 192.168.1.100
mac: "aa:bb:cc:dd:ee:ff"
links:
- from: pc1
to: sw1
- from: server
to: sw1
# Enabled protocols
protocols:
- ethernet
- arp
- ip
# Event definitions
events:
- type: periodic_communication
source: pc1
destination: server
interval_ms: 5000
description: "PC1 periodically queries the server"
# Win condition
win_condition:
type: capture_payload
contains: "{{FLAG_PLACEHOLDER}}"Bilingual learning_objectives Schema
The learning_objectives field accepts two shapes:
Single-locale array (treated as English; backward compatible with older configs):
yamllearning_objectives: - "Understand the Ethernet broadcast mechanism" - "Use a packet-capture tool to observe traffic"Bilingual locale map (recommended):
yamllearning_objectives: en: - "Understand the Ethernet broadcast mechanism" - "Use a packet-capture tool to observe traffic" zh-TW: - "了解 Ethernet 廣播機制與封包傳遞過程" - "學會使用封包擷取工具觀察網路流量"- The map must contain an
enkey, which acts as fallback. - Each locale value must be an array of strings.
- The map must contain an
See the canonical example at docs/challenges/01-ethernet-basics/config.yaml, which uses the bilingual map form.
Step 4: Device Configuration
Supported Device Types
Type (type) | Description | Required attributes | Optional attributes |
|---|---|---|---|
pc | Personal computer | id, type, mac, ip | player, label, position |
switch | Switch | id, type | label, position |
server | Server | id, type, mac, ip | label, services, position |
router | Router | id, type, mac, ip | label, position |
Attribute Reference
- id: pc1 # Unique identifier (referenced by links and events)
type: pc # Device type
label: "PC1 (You)" # Display name (shown on the topology graph)
ip: 192.168.1.1 # IPv4 address
mac: "aa:bb:cc:dd:ee:01" # MAC address (quote it to avoid YAML parsing issues)
player: true # Marks the player-controlled device (only one per challenge)
position: # Coordinates on the topology graph (optional; auto-layout otherwise)
x: 100
y: 200
services: # Server-only: exposed services
- type: http
port: 80
- type: dns
port: 53Link Definitions
The links array defines physical connections between devices:
links:
- from: pc1
to: sw1
- from: pc2
to: sw1
- from: server
to: sw1Each link represents a bidirectional Ethernet wire. All traffic flowing through a switch is forwarded according to its MAC address table.
Step 5: Event System
Events define behaviour that happens automatically during the simulation. They are the core mechanism that drives a challenge's narrative.
Event Types
| Type | Description | Key parameters |
|---|---|---|
periodic_frame | Periodically broadcasts an Ethernet frame | source, destination, interval_ms, payload |
periodic_communication | Periodic bidirectional communication | source, destination, interval_ms |
periodic_http_request | Periodic HTTP request | source, destination, interval_ms |
http_response | HTTP response (supports conditional triggers) | source, destination, payload, trigger |
arp_reply_embed | Embeds extra information in ARP replies | source, arp_note |
Event Examples
events:
# Broadcast a frame containing the flag every 3 seconds
- type: periodic_frame
source: pc1
destination: broadcast
interval_ms: 3000
payload: "{{FLAG_PLACEHOLDER}}"
description: "PC1 broadcasts a frame containing the flag every 3 seconds"
# HTTP response that only fires after MITM is active
- type: http_response
source: server
destination: pc1
payload: "{{FLAG_PLACEHOLDER}}"
trigger: mitm_active
description: "Server HTTP response contains the flag; only visible when MITM is active"Flag Placeholder
Use as the flag placeholder inside config files. The build pipeline replaces it with the actual encrypted flag value. Plaintext flags live in docs/challenges/flags.secret.yaml, which is excluded from version control.
Step 6: Flag Configuration
How It Works
- Author time:
inside the config is rewritten during the build. - Frontend validation: When the learner submits a flag, the browser computes its SHA-256 hash using the Web Crypto API.
- Comparison: The computed hash is compared against the hash stored in
challenge.flag.value.
flags.secret.yaml
# docs/challenges/flags.secret.yaml
# This file must not be committed to version control
01-ethernet-basics: "FLAG{ethernet_frame_captured}"
02-arp-discovery: "FLAG{arp_table_revealed}"
03-arp-spoofing: "FLAG{mitm_success}"Security Considerations
- Plaintext flags never appear in frontend build artefacts.
- Validation relies on SHA-256 hash comparison; plaintext cannot be recovered from the hash.
- The
simnet-cryptocrate handles the build-time encryption pipeline. flags.secret.yamlis listed in.gitignore.
Step 7: Sidebar Dual-Write Checklist
Important: every new page must be added to both locales' sidebars.
VitePress maintains an independent themeConfig.sidebar per locale. When you add a challenge page or developer document, you must update both entries in .vitepress/config.mts:
config.locales.root.themeConfig.sidebar(English, the default locale)config.locales['zh-TW'].themeConfig.sidebar(Traditional Chinese)
The link field on each side points to the locale-specific path (for example /challenges/... in the English sidebar versus /zh-TW/challenges/... in the Chinese one), and text should be localised accordingly. Updating only one side hides the new entry from users browsing in the other language.
See STYLEGUIDE.md at the repo root for the full localisation conventions and checklist.
Step 8: Local Testing
Start the Dev Server
# Make sure WASM is built
pnpm wasm:build
# Start the VitePress dev server
pnpm devVerification Checklist
Once the dev server is running, browse to your new challenge page and confirm each item:
- [ ] The page loads, shows "Initializing simulation engine...", and then enters the simulator UI.
- [ ] The topology graph renders every node and link correctly.
- [ ] Device labels and type icons appear correctly.
- [ ] Clicking a device opens its DeviceModal showing IP / MAC information.
- [ ] Packets generated by events appear in the Traffic Log.
- [ ] The Terminal executes basic commands.
- [ ] The nav bar shows the correct challenge name and difficulty badge.
- [ ] The layout works at Desktop / Tablet / Mobile breakpoints.
- [ ] The new page entry appears in both the English and Traditional Chinese sidebars.
Troubleshooting
| Symptom | Likely cause | Resolution |
|---|---|---|
| Blank page, no simulator UI | layout: challenge is missing | Check the layout field in the frontmatter |
| WASM fails to load | pnpm wasm:build was not run | Run the WASM build command |
| "Config not found" error | Wrong config path | Verify the config relative path in the frontmatter |
| Topology graph shows no devices | YAML syntax error | Validate config.yaml with a YAML linter |
| Flag validation always fails | Hash mismatch | Confirm the plaintext in flags.secret.yaml matches the intended value |
| Page missing after switching locale | Only one sidebar was updated | Update both the root and zh-TW sidebars |
Step 9: Full Example
A minimal but complete challenge example:
docs/challenges/<your-slug>/index.md
---
layout: challenge
title: "Your Challenge Title"
difficulty: 3
challengeSlug: "<your-slug>"
config: ./config.yaml
---
## Mission Briefing
Describe the scenario and backstory...
## Objective
Clearly state what learners need to achieve.
## Hints
1. First hint
2. Second hint
3. Third hintdocs/challenges/<your-slug>/config.yaml
challenge:
id: "<your-slug>"
title: "Your Challenge Title"
difficulty: 3
flag:
value: "{{FLAG_PLACEHOLDER}}"
validation: sha256
learning_objectives:
en:
- "Observe and analyse packet traffic"
- "Identify anomalous responses"
zh-TW:
- "觀察並分析封包流量"
- "辨識異常回應"
topology:
nodes:
- id: pc1
type: pc
label: PC1 (You)
ip: 192.168.1.1
mac: "aa:bb:cc:dd:ee:01"
player: true
- id: pc2
type: pc
label: PC2
ip: 192.168.1.2
mac: "aa:bb:cc:dd:ee:02"
- id: sw1
type: switch
label: Switch1
- id: server
type: server
label: Example Server
ip: 192.168.1.100
mac: "aa:bb:cc:dd:ee:ff"
links:
- from: pc1
to: sw1
- from: pc2
to: sw1
- from: server
to: sw1
protocols:
- ethernet
- arp
- ip
events:
- type: periodic_communication
source: pc2
destination: server
interval_ms: 4000
description: "PC2 periodically queries the server"
- type: periodic_frame
source: server
destination: broadcast
interval_ms: 6000
payload: "{{FLAG_PLACEHOLDER}}"
description: "Server leaks the flag in a broadcast frame"
win_condition:
type: capture_payload
contains: "{{FLAG_PLACEHOLDER}}"Third Locale Extension
Adding a third language (for example Japanese) to the template requires no runtime code changes — just two steps:
Register the new locale: add a new key under
localesin.vitepress/config.mts.tslocales: { root: { /* English */ }, 'zh-TW': { /* Traditional Chinese */ }, ja: { label: '日本語', lang: 'ja-JP', link: '/ja/', themeConfig: { nav: [ /* Japanese nav */ ], sidebar: { /* Japanese sidebar */ }, }, }, }Create the mirror content tree: under
docs/ja/, mirror the structure of the English tree (docs/ja/guide/,docs/ja/dev/,docs/ja/challenges/...) and translate each Markdown file.The
challengeSlugfrontmatter stays identical across all locales (it is a logical identifier, not display text), andconfigstill points to the sameconfig.yaml. If that config has a bilinguallearning_objectivesmap, add the new locale key alongside (for exampleja: [...]).
Restart pnpm dev and VitePress's built-in locale switcher will automatically offer the new language.
Next Steps
Once your challenge is ready:
- Add the plaintext flag to
docs/challenges/flags.secret.yaml. - Update both locale sidebars in
.vitepress/config.mts. - Run
pnpm devand verify locally. - Open a Pull Request once the verification checklist passes.
For deeper details about how the system works internally, see the Architecture document.