Skip to content

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 definition

Throughout this guide, NN-your-feature and <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:

bash
mkdir docs/challenges/NN-your-feature
  • NN is a two-digit ordinal that determines the ordering of challenges.
  • slug is 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:

yaml
---
layout: challenge
title: "Your Challenge Title"
difficulty: 3
challengeSlug: "<your-slug>"
config: ./config.yaml
---

Frontmatter Fields

FieldTypeRequiredDescription
layoutstringyesMust be challenge; selects the challenge page layout
titlestringyesChallenge title, shown in the nav badge
difficultynumberyesDifficulty rating (1-5), rendered as Lv.N
challengeSlugstringyesUnique challenge identifier; must match the directory name
configstringyesRelative 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:

markdown
## Mission Briefing

Describe the scenario and backstory...

## Objective

Clearly state what learners need to achieve.

## Hints

1. First hint
2. Second hint

Step 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

yaml
# 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:

  1. Single-locale array (treated as English; backward compatible with older configs):

    yaml
    learning_objectives:
      - "Understand the Ethernet broadcast mechanism"
      - "Use a packet-capture tool to observe traffic"
  2. Bilingual locale map (recommended):

    yaml
    learning_objectives:
      en:
        - "Understand the Ethernet broadcast mechanism"
        - "Use a packet-capture tool to observe traffic"
      zh-TW:
        - "了解 Ethernet 廣播機制與封包傳遞過程"
        - "學會使用封包擷取工具觀察網路流量"
    • The map must contain an en key, which acts as fallback.
    • Each locale value must be an array of strings.

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)DescriptionRequired attributesOptional attributes
pcPersonal computerid, type, mac, ipplayer, label, position
switchSwitchid, typelabel, position
serverServerid, type, mac, iplabel, services, position
routerRouterid, type, mac, iplabel, position

Attribute Reference

yaml
- 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: 53

The links array defines physical connections between devices:

yaml
links:
  - from: pc1
    to: sw1
  - from: pc2
    to: sw1
  - from: server
    to: sw1

Each 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

TypeDescriptionKey parameters
periodic_framePeriodically broadcasts an Ethernet framesource, destination, interval_ms, payload
periodic_communicationPeriodic bidirectional communicationsource, destination, interval_ms
periodic_http_requestPeriodic HTTP requestsource, destination, interval_ms
http_responseHTTP response (supports conditional triggers)source, destination, payload, trigger
arp_reply_embedEmbeds extra information in ARP repliessource, arp_note

Event Examples

yaml
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

  1. Author time: inside the config is rewritten during the build.
  2. Frontend validation: When the learner submits a flag, the browser computes its SHA-256 hash using the Web Crypto API.
  3. Comparison: The computed hash is compared against the hash stored in challenge.flag.value.

flags.secret.yaml

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-crypto crate handles the build-time encryption pipeline.
  • flags.secret.yaml is 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

bash
# Make sure WASM is built
pnpm wasm:build

# Start the VitePress dev server
pnpm dev

Verification 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

SymptomLikely causeResolution
Blank page, no simulator UIlayout: challenge is missingCheck the layout field in the frontmatter
WASM fails to loadpnpm wasm:build was not runRun the WASM build command
"Config not found" errorWrong config pathVerify the config relative path in the frontmatter
Topology graph shows no devicesYAML syntax errorValidate config.yaml with a YAML linter
Flag validation always failsHash mismatchConfirm the plaintext in flags.secret.yaml matches the intended value
Page missing after switching localeOnly one sidebar was updatedUpdate both the root and zh-TW sidebars

Step 9: Full Example

A minimal but complete challenge example:

docs/challenges/<your-slug>/index.md

markdown
---
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 hint

docs/challenges/<your-slug>/config.yaml

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:

  1. Register the new locale: add a new key under locales in .vitepress/config.mts.

    ts
    locales: {
      root: { /* English */ },
      'zh-TW': { /* Traditional Chinese */ },
      ja: {
        label: '日本語',
        lang: 'ja-JP',
        link: '/ja/',
        themeConfig: {
          nav: [ /* Japanese nav */ ],
          sidebar: { /* Japanese sidebar */ },
        },
      },
    }
  2. 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 challengeSlug frontmatter stays identical across all locales (it is a logical identifier, not display text), and config still points to the same config.yaml. If that config has a bilingual learning_objectives map, add the new locale key alongside (for example ja: [...]).

Restart pnpm dev and VitePress's built-in locale switcher will automatically offer the new language.

Next Steps

Once your challenge is ready:

  1. Add the plaintext flag to docs/challenges/flags.secret.yaml.
  2. Update both locale sidebars in .vitepress/config.mts.
  3. Run pnpm dev and verify locally.
  4. Open a Pull Request once the verification checklist passes.

For deeper details about how the system works internally, see the Architecture document.