Skip to content

如何新增挑戰關卡

本指南將帶你從零開始建立一個 SimNet 挑戰關卡。每個挑戰關卡由一個 Markdown 頁面和一份 YAML 設定檔組成,不需要撰寫任何 Vue 元件或 Rust 程式碼。

總覽

一個挑戰關卡的最小組成:

docs/challenges/NN-your-feature/
├── index.md        # 頁面內容 + frontmatter 設定
└── config.yaml     # 拓撲(Topology)、事件、Flag 定義

本指南通篇使用 NN-your-feature<your-slug> 作為佔位符,僅用於說明檔案結構,並非實際存在的目錄。請以你自己選定的關卡 slug 取代。

步驟 1:建立目錄

docs/challenges/ 下建立新的目錄,命名規則為 NN-slug

bash
mkdir docs/challenges/NN-your-feature
  • NN 是兩位數編號,決定關卡的順序
  • slug 使用小寫英文加連字號(kebab-case),簡要描述關卡主題

步驟 2:撰寫 index.md

建立 docs/challenges/<your-slug>/index.md,使用以下 frontmatter(前置資料)格式:

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

Frontmatter 欄位說明

欄位類型必填說明
layoutstring固定為 challenge,觸發挑戰版面
titlestring關卡標題,顯示在導覽列 Badge(徽章)中
difficultynumber難度等級(1-5),顯示為 Lv.N
challengeSlugstring關卡唯一識別碼,需與目錄名稱一致
configstringconfig YAML 的相對路徑

Markdown 內容

Frontmatter 下方的 Markdown 內容會透過 Vue slot 機制注入到 SimNet 的 Mission Briefing(任務說明)面板中。建議結構:

markdown
## 任務說明

描述情境與背景故事...

## 目標

明確告訴學生需要達成什麼條件。

## 提示

1. 第一個提示
2. 第二個提示

步驟 3:建立 config.yaml

config.yaml 是挑戰關卡的核心設定檔,定義了網路拓撲、啟用的協定、事件系統和勝利條件。

完整結構

yaml
# 挑戰中繼資料(Metadata)
challenge:
  id: "<your-slug>"
  title: "Your Challenge Title"
  difficulty: 3
  flag:
    value: "{{FLAG_PLACEHOLDER}}"
    validation: sha256
  # learning_objectives 可使用單一語系陣列或雙語對照表,詳見下節
  learning_objectives:
    en:
      - "First learning objective"
      - "Second learning objective"
    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: 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

# 啟用的協定
protocols:
  - ethernet
  - arp
  - ip

# 事件定義
events:
  - type: periodic_communication
    source: pc1
    destination: server
    interval_ms: 5000
    description: "PC1 periodically queries the server"

# 勝利條件
win_condition:
  type: capture_payload
  contains: "{{FLAG_PLACEHOLDER}}"

雙語 learning_objectives Schema

learning_objectives 欄位接受兩種形式:

  1. 單一語系陣列(被視為英文,向下相容舊版設定):

    yaml
    learning_objectives:
      - "Understand the Ethernet broadcast mechanism"
      - "Use a packet-capture tool to observe traffic"
  2. 雙語對照表(建議形式):

    yaml
    learning_objectives:
      en:
        - "Understand the Ethernet broadcast mechanism"
        - "Use a packet-capture tool to observe traffic"
      zh-TW:
        - "了解 Ethernet 廣播機制與封包傳遞過程"
        - "學會使用封包擷取工具觀察網路流量"
    • 對照表必須包含 en 鍵作為 fallback
    • 每個語系的值必須是字串陣列

可參考標準範例:docs/challenges/01-ethernet-basics/config.yaml 採用雙語對照表形式。

步驟 4:裝置設定

支援的裝置類型

類型(type說明必填屬性選填屬性
pc個人電腦id, type, mac, ipplayer, label, position
switch交換器(Switch)id, typelabel, position
server伺服器id, type, mac, iplabel, services, position
router路由器(Router)id, type, mac, iplabel, position

屬性詳解

yaml
- id: pc1                    # 唯一識別碼(在 links 與 events 中引用)
  type: pc                   # 裝置類型
  label: "PC1 (You)"         # 顯示名稱(出現在拓撲圖上)
  ip: 192.168.1.1            # IPv4 位址
  mac: "aa:bb:cc:dd:ee:01"   # MAC 位址(需加引號避免 YAML 解析錯誤)
  player: true               # 標記為玩家控制的裝置(每個關卡僅一台)
  position:                  # 拓撲圖上的位置(選填,未指定則自動排列)
    x: 100
    y: 200
  services:                  # 伺服器專用:開放的服務
    - type: http
      port: 80
    - type: dns
      port: 53

連結定義

links 陣列定義裝置之間的實體連線:

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

每條連結代表一條雙向的 Ethernet 線路。所有經過 Switch 的流量都會依據 MAC 位址表進行轉發。

步驟 5:事件系統

事件(Event)定義了模擬過程中自動發生的行為,是驅動挑戰劇情的核心機制。

事件類型

類型說明關鍵參數
periodic_frame週期性發送 Ethernet 訊框source, destination, interval_ms, payload
periodic_communication週期性雙向通訊source, destination, interval_ms
periodic_http_request週期性 HTTP 請求source, destination, interval_ms
http_responseHTTP 回應(可帶條件觸發)source, destination, payload, trigger
arp_reply_embed在 ARP 回應中嵌入資訊source, arp_note

事件範例

yaml
events:
  # 每 3 秒廣播一次包含 Flag 的訊框
  - type: periodic_frame
    source: pc1
    destination: broadcast
    interval_ms: 3000
    payload: "{{FLAG_PLACEHOLDER}}"
    description: "PC1 broadcasts a frame containing the flag every 3 seconds"

  # 需要 MITM 啟動後才會觸發的 HTTP 回應
  - 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 佔位符

在 config 中使用 作為 Flag 佔位符。建置流程會自動將其替換為實際的加密 Flag 值。明文 Flag 儲存在 docs/challenges/flags.secret.yaml 中(此檔案不應進入版本控制)。

步驟 6:Flag 設定

運作原理

  1. 開發階段:config 中的 會在建置時被替換
  2. 前端驗證:使用者提交 Flag 後,瀏覽器端以 Web Crypto API 計算 SHA-256 hash
  3. 比對機制:將計算出的 hash 與 challenge.flag.value 中儲存的 hash 比對

flags.secret.yaml

yaml
# docs/challenges/flags.secret.yaml
# 此檔案不應提交到版本控制中
01-ethernet-basics: "FLAG{ethernet_frame_captured}"
02-arp-discovery: "FLAG{arp_table_revealed}"
03-arp-spoofing: "FLAG{mitm_success}"

安全性考量

  • Flag 的明文絕不會出現在前端建置產物中
  • 驗證透過 SHA-256 hash 比對完成,無法從 hash 反推明文
  • simnet-crypto crate 處理建置時的加密 Pipeline(加密流程)
  • flags.secret.yaml 已被列入 .gitignore

步驟 7:側邊欄(Sidebar)雙語同步

重要:每次新增頁面都必須同時更新兩個語系的側邊欄設定。

VitePress 為每個 locale 維護獨立的 themeConfig.sidebar。當你新增挑戰頁面或開發文件時,必須在 .vitepress/config.mts同步更新

  • config.locales.root.themeConfig.sidebar(英文,預設語系)
  • config.locales['zh-TW'].themeConfig.sidebar(繁體中文)

兩邊的 link 欄位指向各自 locale 對應的路徑(例如英文版指 /challenges/...,中文版指 /zh-TW/challenges/...),text 欄位則使用對應語系的字串。若只更新其中一邊,會造成另一語系的使用者看不到新頁面入口。

詳細規範與檢核清單請參考專案根目錄的 STYLEGUIDE.md

步驟 8:本地測試

啟動開發伺服器

bash
# 確保 WASM 已建置
pnpm wasm:build

# 啟動 VitePress 開發伺服器
pnpm dev

驗證清單

開發伺服器啟動後,瀏覽你的新關卡頁面並逐項確認:

  • [ ] 頁面載入後顯示「Initializing simulation engine...」然後進入模擬介面
  • [ ] 拓撲圖正確渲染所有裝置與連線
  • [ ] 裝置標籤和類型圖示顯示正確
  • [ ] 點擊裝置可開啟詳情彈窗(DeviceModal),顯示 IP / MAC 資訊
  • [ ] Traffic Log 中能看到事件產生的封包
  • [ ] Terminal 可以執行基本指令
  • [ ] 導覽列顯示正確的關卡名稱與難度等級
  • [ ] 在 Desktop / Tablet / Mobile 三種斷點下版面正常
  • [ ] 英文與繁體中文兩個 locale 的側邊欄都看得到新頁面入口

常見問題排除

問題可能原因解決方式
頁面空白、無模擬介面layout: challenge 未設定檢查 frontmatter 的 layout 欄位
WASM 載入失敗未執行 pnpm wasm:build執行 WASM 建置指令
Config not found 錯誤config 路徑錯誤確認 frontmatter 中的 config 指向正確的相對路徑
拓撲圖沒有裝置YAML 語法錯誤用 YAML 驗證工具檢查 config.yaml
Flag 驗證始終失敗hash 不匹配確認 flags.secret.yaml 中的明文與預期一致
切換語系後找不到頁面只更新單邊側邊欄同步更新 rootzh-TW 兩個 locale 的 sidebar

步驟 9:完整範例

以下是一個最小但完整的挑戰關卡範例。

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}}"

第三語系擴充

若要為模板新增第三種語系(例如日文),不需要修改任何執行階段的程式碼,只需做兩件事:

  1. 註冊新 locale:在 .vitepress/config.mtslocales 物件中新增一個 key。

    ts
    locales: {
      root: { /* English */ },
      'zh-TW': { /* Traditional Chinese */ },
      ja: {
        label: '日本語',
        lang: 'ja-JP',
        link: '/ja/',
        themeConfig: {
          nav: [ /* Japanese nav */ ],
          sidebar: { /* Japanese sidebar */ },
        },
      },
    }
  2. 建立對應的內容目錄:在 docs/ja/ 下建立與英文版相同結構的鏡像樹(docs/ja/guide/docs/ja/dev/docs/ja/challenges/...),把每個 Markdown 翻譯成新語系。

    挑戰頁面的 frontmatter challengeSlug 在所有 locale 中保持相同(這是邏輯識別碼,並非顯示文字),config 仍指向同一份 config.yaml;該設定檔內如有 learning_objectives 雙語對照表,請額外加入新的語系鍵(例如 ja: [...])。

完成後重新執行 pnpm dev,VitePress 內建的 locale 切換器會自動出現新的語系入口。

下一步

建立完新的挑戰關卡後:

  1. 將 Flag 明文新增到 docs/challenges/flags.secret.yaml
  2. .vitepress/config.mts 中同步更新兩個 locale 的側邊欄
  3. 執行 pnpm dev 進行本地測試
  4. 確認所有驗證項目通過後,提交 Pull Request

如需了解更多系統內部運作細節,請參閱系統架構