如何新增挑戰關卡
本指南將帶你從零開始建立一個 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:
mkdir docs/challenges/NN-your-featureNN是兩位數編號,決定關卡的順序slug使用小寫英文加連字號(kebab-case),簡要描述關卡主題
步驟 2:撰寫 index.md
建立 docs/challenges/<your-slug>/index.md,使用以下 frontmatter(前置資料)格式:
---
layout: challenge
title: "Your Challenge Title"
difficulty: 3
challengeSlug: "<your-slug>"
config: ./config.yaml
---Frontmatter 欄位說明
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
layout | string | 是 | 固定為 challenge,觸發挑戰版面 |
title | string | 是 | 關卡標題,顯示在導覽列 Badge(徽章)中 |
difficulty | number | 是 | 難度等級(1-5),顯示為 Lv.N |
challengeSlug | string | 是 | 關卡唯一識別碼,需與目錄名稱一致 |
config | string | 是 | config YAML 的相對路徑 |
Markdown 內容
Frontmatter 下方的 Markdown 內容會透過 Vue slot 機制注入到 SimNet 的 Mission Briefing(任務說明)面板中。建議結構:
## 任務說明
描述情境與背景故事...
## 目標
明確告訴學生需要達成什麼條件。
## 提示
1. 第一個提示
2. 第二個提示步驟 3:建立 config.yaml
config.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 欄位接受兩種形式:
單一語系陣列(被視為英文,向下相容舊版設定):
yamllearning_objectives: - "Understand the Ethernet broadcast mechanism" - "Use a packet-capture tool to observe traffic"雙語對照表(建議形式):
yamllearning_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, ip | player, label, position |
switch | 交換器(Switch) | id, type | label, position |
server | 伺服器 | id, type, mac, ip | label, services, position |
router | 路由器(Router) | id, type, mac, ip | label, position |
屬性詳解
- 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 陣列定義裝置之間的實體連線:
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_response | HTTP 回應(可帶條件觸發) | source, destination, payload, trigger |
arp_reply_embed | 在 ARP 回應中嵌入資訊 | source, arp_note |
事件範例
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 設定
運作原理
- 開發階段:config 中的
會在建置時被替換 - 前端驗證:使用者提交 Flag 後,瀏覽器端以 Web Crypto API 計算 SHA-256 hash
- 比對機制:將計算出的 hash 與
challenge.flag.value中儲存的 hash 比對
flags.secret.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-cryptocrate 處理建置時的加密 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:本地測試
啟動開發伺服器
# 確保 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 中的明文與預期一致 |
| 切換語系後找不到頁面 | 只更新單邊側邊欄 | 同步更新 root 與 zh-TW 兩個 locale 的 sidebar |
步驟 9:完整範例
以下是一個最小但完整的挑戰關卡範例。
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}}"第三語系擴充
若要為模板新增第三種語系(例如日文),不需要修改任何執行階段的程式碼,只需做兩件事:
註冊新 locale:在
.vitepress/config.mts的locales物件中新增一個 key。tslocales: { root: { /* English */ }, 'zh-TW': { /* Traditional Chinese */ }, ja: { label: '日本語', lang: 'ja-JP', link: '/ja/', themeConfig: { nav: [ /* Japanese nav */ ], sidebar: { /* Japanese sidebar */ }, }, }, }建立對應的內容目錄:在
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 切換器會自動出現新的語系入口。
下一步
建立完新的挑戰關卡後:
- 將 Flag 明文新增到
docs/challenges/flags.secret.yaml - 在
.vitepress/config.mts中同步更新兩個 locale 的側邊欄 - 執行
pnpm dev進行本地測試 - 確認所有驗證項目通過後,提交 Pull Request
如需了解更多系統內部運作細節,請參閱系統架構。