Repository navigation
Conversation
…d 保留历史 - awdp_phase 增加 preparing_fix(迁移 + db:gen):Break→PreparingFix→Fix 线性状态机; tick 对 PreparingFix 幂等 reconcile(instance 级 advisory lock reset),全部 pristine 才物化回合 + 转 Fix;crash 半途下次 tick 续跑(plan §41/§42) - §55 Pending 语义:Start Training 创建 Pending run(无时钟);Launch → Pending→Break + 启动实例;删除 frozen Break / rewind-to-break 旧流程 - §54 End 保留历史:End → run 终态 Ended,不再清空 score/break 账本;Train Again 另建新 run - §21 移除 early_check:backend 路由/service/DTO/realtime + 前端按钮 + early_patched_seq 列 (迁移删除;db:gen);每 Turn 资格回归 applied 判定 - tick Fix 分支兜底物化回合(crash 窗口补齐) - tests:awdp_training 6(Pending/Launch/End 保留历史/Train Again)、tick 两拍 Break→Fix、 runtime/practice_judge/concurrency/fix_patch 回归全绿;lib 224 单测通过 NO PUSH
- migration: awdp_patch_submissions.apply_started_at(apply 开始时间) - §43 stale applying 回收:apply 前回收 apply_started_at 超过 exec 超时+裕量的 applying → failed + reason「stale applying recovered」(绝不静默视为 APPLIED,允许重新上传) - §45 APPLIED-AT 资格:has_applied_patch 要求 applied_at <= round.cutoff_at; 跨 cutoff 才 APPLIED 的 patch 不属旧 Turn - §56 practice 配置快照源:start_training 从 AWDPlusPractice awdp_events 配置快照 (管理端可调 Break/Fix/Turn/Score,只影响未来 Runs),tick 复用 event_repo::config_snapshot - tests:stale_applying_recovered(回收+reason+近期不受影响)、patch_eligibility_uses_applied_at_cutoff、 preparing_fix_gates_and_reconcile(PreparingFix 门禁 + reconcile→Fix + 物化回合); 既有测试适配 APPLIED-AT(backdate helper);lib 224 + 前端 tsc/build 通过 NO PUSH
…移除 host.docker.internal 默认 - §58 授权修复:resolve_subject 对 Individual 模式校验 event_users 注册行(未加入 → 403), 覆盖 overview/instance/source/patch/evaluations;新增 authorization::require_event_participant - §49 admin Judge 页:DTO 增加 worker_health / data_endpoint / pending / running / last_heartbeat(queue_stats);页面展示 worker 状态 Label + 队列计数 + data endpoint - §36/§37:platform_internal_url 默认改为空(必须显式配置 data/control 网络可达基址), deploy_judge 校验非空;彻底移除 host.docker.internal 默认 - tests:individual_membership_required_for_competition(未加入 403 / 加入放行); 全量 AWDP + AWD + common 测试回归绿(lib 224);前端 tsc + vite build 通过; db:migration:merge 生成 merged.sql(35 migrations) NO PUSH
§66 真实 e2e(API + JudgeServer 容器)发现并修复:
- data plane handler 注册 Data<Arc<AppState>> 而 handler 提取 Data<AppState>,
/flag、/proof 全部 500('Requested application data is not configured correctly');
改为 app_data(Data::new((*state).clone())) 注册内层 AppState。
- claim 响应解析顶层而平台返回 UniResponse 包装 {code,message,data,meta},
claim 永远解析失败 → judge 领到 job 后从不执行(job 等 lease 过期被回收,
之前的 claimed_by 只是 API 侧 claim 生效);重构为 parse_claim_response 取 data 字段,
新增单元测试覆盖空 jobs / 带 job / 畸形 body。
e2e 验证:claim→healthcheck→judge→result patched 全链路打通;
kill judge 后 lease 过期 → 重启 → reclaim attempt=2 → terminal service_down(无计分);
真实 GameBox curl http://awdp-judge:8080/flag 返回确定性 flag。
scheduler:seed_awdp_recurring_tasks 的已存在检查包含 status='failed',
避免启动时 failed 行不匹配 pending/running 过滤器导致重复 seed 行(曾积累 64+ 行)。
NO PUSH
validate_enabled_task_keys 原先校验所有 enabled 行:cron 恢复测试中途 panic (PoisonError 级联)遗留的 failed 行(如已删除的 awdp.practice.judge 测试残留) 会让 dev API 启动即 panic 'invalid handlers'(17:06 现场复现)。 - engine.rs:validate 过滤 status IN (pending, running)——fetch 只取 pending、 init_and_recover 把 running 恢复为 pending;failed/completed 是终态永不执行, 残留的已删除任务行不再阻断启动(init_and_recover 只复活 protected 种子行, 安全无泄漏)。 - awdp_lease.rs:cleanup() 增加清理 task_name LIKE 'it-%' 的 scheduled_tasks 行, cron 恢复测试无论成败都不再累积残留。 验证:dev API 以空 awdp 任务表启动正常(seed 仅重建 awdp.tick/awdp.eval.worker); 全量 AWDP + AWD + common 测试绿;lib 224。 NO PUSH
§55 Pending 语义后 start_run 的早期 gate 仍拒绝 Pending,导致
Pending→Break 的 launch_practice_run 分支不可达:新建练习 run 点 Launch
必现 400 'Invalid state: 只有 Break/Fix 阶段的练习 run 可以开始(当前 Pending)'。
- gate 只拒绝 Ended(终态);Pending 落入 launch 分支(CAS Pending→Break
启动时钟),Break/Fix 会话中则仅幂等启动实例(start_instance)。
- PreparingFix 仍由 runtime 阶段门拒绝(瞬态,tick 自动推进到 Fix)。
- 更新 handler 文档注释(去掉旧的冻结回卷描述)。
验证(真实 API + SQL 播种 Pending run + HTTP Login/Start):
POST /api/service/awdp/runs/{id}/start → 200,run phase=break,
started_at/break_ends_at/next_action_at 置位,实例容器 running(gen 1)。
回归:lib 224 + awdp_training 6 绿,fmt/check 干净。
NO PUSH
…Server 默认启用 用户要求:AWDPlusPractice admin 不需要 judge 配置;训练即默认 1h break / 1h fix / 10m round(默认值本就 3600/3600/600,虚拟赛事行已核对),JudgeServer 默认启用。 - 删除 admin judge 配置 API(GET/PATCH practice-judge、deploy、stop、results) 与前端页面(admin/events/awdp/$id/judge.tsx + 导航项 + awdp.ts 类型/方法)。 - 迁移 20260813173717-drop-practice-judge-legacy.sql:删 awdp_practice_judge_settings (配置表)与 awdp_judge_results(push 时代历史结果表,均无其它引用),db:gen 再生。 - **JudgeServer 默认启用**:runtime::start_instance 练习分支在 ensure 子网后自动调用 deploy_judge(幂等:已 running 跳过;容器名冲突按并发成功处理);不再需要管理端 手动 deploy/stop。deploy_judge 去掉 db 参数与 settings 写回。 - 删除 practice_judge_repo(全 dead)、judge_worker::queue_stats、服务侧 stop_judge/container_status/resolve_judge_server_url/judge_worker_health。 - practice_acl::apply_ruleset 写临时规则文件失败改 best-effort 告警跳过 (TMPDIR 异常不阻断实例启动;与 nft 缺失同语义)。 测试:awdp_practice_judge 6/6(新增 practice_instance_start_auto_deploys_judge 断言实例启动自动部署 judge;push_flow_removed_from_source 增查 admin API/页面残留), 全部 AWDP + AWD + common 套件绿,lib 224,前端 tsc/build 过。 实机验证:Launch → 200 phase=break;judge 容器自动部署且双网络 (fctf-awdp-practice + fctf-awdp-control);GameBox 内 curl http://awdp-judge:8080/flag 返回确定性 flag;admin practice-judge 三个路由均 404。 NO PUSH
… judge 用户要求:Break 页 Flag Server 块更名 judge-server/flag,去掉 curl 示例与复制按钮。 - 后端 DNS alias 同步更名:JUDGE_DATA_ALIAS 'awdp-judge' → 'judge-server' (config practice_judge_data_host 默认同步;proof_url/部署 alias 走同一常量)。 - 前端 Break 页 Flag Server 块改为仅显示 judge-server/flag + Available from your GameBox only,移除 curl 代码行与 Copy 按钮(flagUrl DTO 保留)。 - deploy_judge 幂等升级:running 容器须 env 与期望一致(PLATFORM_INTERNAL_URL / EVENT_ID / WORKER_ID)才跳过,否则重建——修复测试环境残留 judge(test 配置 host.docker.internal,extra_hosts=[] 不可解析)导致真实实例 /flag 全链路 502 的问题。 - AWDP docker 门控测试套件(6 个)新增 remove_judge_container 清理:start_instance 现在默认自动部署 judge,测试跑完必须移除,避免 test-env judge 污染真实环境。 实机验证:真实配置重部署后 judge env=10.42.2.128:9090、alias=judge-server, 用户自己的 Break 实例容器内 curl judge-server:8080/flag 拿到确定性 flag。 前端 tsc/build 过;lib 224;AWDP 套件全绿(awdp_fix_patch 偶发 flake 为预存问题, HEAD 同样复现,非本改动引入)。 NO PUSH
…dge-server/flag 免端口 用户要求:不写端口(http://judge-server/flag)即可取 Break flag;judge 权限与 nginx 一致(默认 Docker 能力集,不做 cap_drop ALL 收敛)。 - PRACTICE_JUDGE_PORT 8080 -> 80(domain/judge.rs 常量,ACL data-port 规则 / deploy env DATA_LISTEN_ADDR / 全部引用随之生效)。 - crates/awdp-judgeserver DATA_LISTEN_ADDR 默认 0.0.0.0:80(镜像已重建)。 - judge 部署 spec:cap_drop ALL -> vec。 - RunDTO judge_endpoint base_url/flag_url 省略 :80(URL 干净)。 - 实机验证:judge 重部署(DATA_LISTEN_ADDR=0.0.0.0:80、CapDrop=null),用户自己的 Break GameBox 容器内 curl http://judge-server/flag 返回确定性 flag。 - lib 224 / awdp_practice_judge 6 / awdp_runtime_break 6 / awdp_training 6 / awdp_lease 18 / fcmc 51 / 前端 tsc + vite build 全绿。 NO PUSH
按用户要求:Flag Server 块显示 'Flag Endpoint: http://judge-server/flag' (80 端口免写;仍保留 GameBox 内部可达提示)。NO PUSH
用户要求:gamebox.category == web 时 Flag Endpoint 显示为链接,点击新标签页打开 (href 取 run DTO 的 flagUrl;非 web 类型保持纯文本)。NO PUSH
用户澄清:可点击的是题面 Endpoints(靶机访问地址),Flag Endpoint 保持纯文本。 - Break/Fix 卡片 Endpoints 行:category=web 且实例运行中时每个端点渲染为 <a target=_blank>(浏览器可达的 NODE_IP:端口),非 web 保持文本。 - Flag Endpoint 回滚为纯文本。NO PUSH
用户需求:Fix 阶段选择 patch.tar.gz;平台提供通用 patch.sh(全注释,不真正写
patch);source.tar.gz 结构 = src/(SourcePath 源码)+ 根 patch.sh 模板。
- source_artifact:source.zip → source.tar.gz(flate2 GzEncoder + tar),重打包
为 src/{源码}(去 docker cp 根前缀)+ patch.sh(全注释通用模板,exit 0 即
APPLIED);object key 与错误文案同步。
- patch 上传:player.rs 接收 bytes → patch_service::extract_patch_script 解压
gzip tar 提取根 patch.sh(旧裸 .sh 兼容);大小上限校验解压后内容。
- 前端:Fix 上传 accept 改 .tar.gz,按钮文案「选择 patch.tar.gz」。
- 闭环:玩家下载 source.tar.gz → 原样传回 → 模板空操作 APPLIED。
- 测试:awdp_source_artifact 改为验证 tar.gz 的 src/+patch.sh 结构(4/4),
patch_service 新增 4 个 extract_patch_script 单测;lib 228 全绿,全部 AWDP
集成套件 + 前端 tsc/build 通过。NO PUSH
用户验证的 docker 可写层模型:上传 patch.tar.gz → 平台端解压 → 容器内
/tmp/patch/(固定路径,无额外环境变量)→ exec /bin/sh /tmp/patch/patch.sh
(注入 FLOATCTF_SOURCE_DIR,60s 超时)→ exit 0 则 restart 同一容器
(保留 writable layer)→ APPLIED;非 0/超时 → FAILED。
- 只校验根 patch.sh 存在;不限制包大小;包内其余文件原样解压到
/tmp/patch/,路径由用户自定,patch.sh 内直接引用 /tmp/patch/xxx。
- 安全底线:拒绝绝对路径与 .. 段,拒绝 fifo/char 等特殊条目,
symlink 原样保留;多次上传先 rm -rf 再解压(同目录复用不残留)。
- fcmc 新增 ContainerRuntime::copy_into_container(bollard
upload_to_container,PUT /containers/{id}/archive)。
- patch_service 单测 8/8(结构/路径穿越/特殊条目/仅脚本成包);
awdp_fix_patch 改为真实 cp + restart 保留 + reset 恢复断言;
全部 AWDP 集成套件 + lib 232 + 前端 tsc/build 绿。NO PUSH
PatchResult::Failed 携带面向用户的失败原因:超时(60s)或退出码非 0, 附 stdout/stderr 前 400 字符摘要;PatchSubmitResponse 新增 error_message 字段;training/player 两个 upload_patch handler 填充;前端 Last patch failed 时卡片下方红字展示原因,banner 同显。NO PUSH
模板从全注释改为默认闭环逻辑:find 清空 FLOATCTF_SOURCE_DIR 后 cp -af src/. 恢复包内源码(依赖 workdir=/tmp/patch,patch.sh 相对路径 以解压目录为基准)。重新生成 test_g 的 source.tar.gz(digest 0e1fbfd1) 并更新 DB。source_artifact 单测/集成测试断言同步改为校验 find+cp 恢复逻辑。NO PUSH
…Check 一键检查
- 练习 run(gamebox_id 非空)check 失败(NO_PATCH/SERVICE_DOWN/FUNCTIONAL_BROKEN/VULNERABLE)
写 score_type='fix'、delta=-50 幂等账本(沿用 fix 幂等键,重试不双扣);PATCHED 仍 +150;
竞赛语义不变(仅 PATCHED +150,其余 +0)。常量 DEFAULT_FIX_ROUND_PENALTY=50 在 domain config。
- 判定抽成共享 run_checks(NO_PATCH→healthcheck→judge→exploit,不落账),official 管线与
ALL Check 共用。
- Test Check(manual):练习模式 judge 后追加 exploit 判定,成功 → Vulnerable;竞赛仍仅
health+judge。新迁移 20260814090505 放宽 awdp_evaluations_manual_no_exploit_check 约束
(manual 行可携带 exploit_result);DTO(ManualCheckDto/AwdpEvaluationDto/AwdpRunDto)透出。
- ALL Check 端点 POST awdp/runs/{id}/gameboxes/{gb}/all-check(practice-only、Fix、owner、
实例 advisory lock):成功(PATCHED)→ 从当前轮起全部剩余回合物化 Patched 评估 + 各 +150
幂等计分 → 停止实例 → run 直接 Ended;失败 → 不落账不扣分不写评估,等本轮官方 check。
- 清理遗留的『提前 Check』过期注释。
带 run 段保证 event_instances.container_name 唯一约束跨训练会话不冲突(旧 run 行不删);
竞赛实例维持 awdp-{instance 前20};已存在实例保留旧名。
原实现与 doc 注释不一致(注释写 /tmp/patch-<id>/,实现固定 /tmp/patch)且多次上传共用 同目录需 rm -rf 清理;改为按 submission id 前 8 位动态目录后无需残留清理(保留 mkdir -p, put_archive 目标目录必须存在)。
4 项:练习失败扣 50 与 PATCHED +150 账本、ALL Check 成功全回合计分并结束 run、 ALL Check 失败不落账、练习 manual Test Check 跑 exploit 并标记 Vulnerable; 另断言 AWDPP-* 容器命名(30 字符)与重试不双扣。
- AwdpRunDto.fix_round_penalty 透出,练习 History 标签按 no patch(-50)/service down(-50)/ broken(-50)/vulnerable(-50) 展示,失败项 delta = -fixRoundPenalty(platform_error 仍 +0)。 - Test Check 结果区新增漏洞利用 SUCCESS/FAIL 行(exploit_ok != null 时)。 - Fix 段新增 ALL Check 主按钮(practice-only):成功横幅『剩余 X 轮已全部计分,训练已结束』, 失败警告『不落账、不扣分,等本轮官方 check 判定』。 - 修复 AwdpWorkbench.test.tsx 过期断言(提前 Check → ALL Check)。
…e-server 镜像 floatctf/gameboxes/test-g:1.0.3 已构建,运行时不再需要该 PHP 夹具源码。
…SSRF 探测
- awdp/exploit.py 模板 payload 由 http://flagserver/flag 改为 http://judge-server/flag
(练习 JudgeServer data plane;examples/test_g 副本同步)。
- judge/check.py 模板探测改为 SSRF 形式
urllib.request.urlopen(f"http://{target_ip}/?url=http://127.0.0.1", ...):
经目标 GameBox 自身 SSRF 端点探测其 127.0.0.1 服务,AWD 攻击阶段防火墙不阻断,
与 test_g 的 index.php(SSRF 回显)契约一致。
- gamebox meta.toml 模板 version 1.0.0 → 1.0.3(对齐 test_g),模板测试断言同步。
根因:练习 manual eval 由 JudgeServer 容器 worker 消费,其 run_job 第 4 步 exploit 仅 kind=official 执行,且 claim 端点 manual job 恒不带 exploit_script(旧防 oracle 语义),导致练习 Test Check 的 exploit_result 恒 NULL、前端无 Exploit 展示。 - claim(judge_worker.rs):练习 run(gamebox_id 非空)的 manual job 注入 awdp_exploit_script_content;竞赛 manual 仍 None。 - judge 容器(awdp-judgeserver):run_job 对 manual 且携带脚本者执行 exploit 诊断—— 不注入 FLOATCTF_PROOF_URL、绝不计分;exploit 成功 → Vulnerable,失败 → Patched + exploit_detail。API 进程内 worker(process_manual_leased)语义一致。 - 重建 floatctf/awdp-judgeserver:latest 镜像(--network host + 代理加速依赖下载)。
…ore Ledger 练习 run Ended 后玩家只关心重训,结算展示无意义;竞赛 ended 视图保持原样 (Final Score/Break Results/Score Ledger 继续展示)。
练习页顶部 viewModel.title 已是题目名大字,卡片(Endpoints 所在框)标题 h4 重复; 练习模式改为占位(只留 Broken/runtime 状态标签)。竞赛多卡片列表保留名字。
稳定性压测发现:未参赛者启动实例返回英文 「when launch instance:User not joined the event!」→ context.rs 改为 「你尚未加入本赛事」,并去掉 instances.rs 的英文包装前缀。 文档新增第 11 章稳定性与承压验证:基线与四个场景(练习 62 RPS、竞技赛 420/416 RPS、故障注入 414 RPS 下四次注入均零 panic 无 5xx 洪峰)、F54 根因 (list_cleanup_candidates 仅按 TTL,无存活性对账 → 16 条 running 僵尸记录)、 Redis 未设 maxmemory 与清理任务日志噪音等观察,以及覆盖度诚实标注。
稳定性压测发现两类真实问题: 1) 僵尸实例:DB 中 6~16 条 runtime_state='running' 但容器早已不存在, 90 秒后仍不自愈(UI 显示运行中、可能挡住该玩家再次启动、资源账目虚高)。 新增 service.reconcile_missing_containers(在 system.practice.clean 里先跑): 取 running 且 updated_at 早于 5 分钟的实例 → 探活容器 → **仅确定不存在时** 收敛为 failed,docker 抖动等错误只记日志不动状态;InstanceRuntime 增加 container_exists(默认 true 便于 mock,Docker 实现用**容器列表按名比对**, 不依赖被归一化的错误文本)。 2) 根因更深:list_cleanup_candidates 写成 `event_challenge_instance::Entity::find().filter(event_instances::Column::…)` ——在基实体上过滤关联表列,**实测匹配不到任何行**(对账日志 candidates=0 即证据), 导致 TTL 清理**从不生效**、僵尸无限累积。改为直查 event_instances 后再按 id 关联。 验证:部署后 25 秒内对账生效,**6 条僵尸全部 running → failed**(running=0 failed=6), 日志逐条打印「容器已不存在,running → failed」。单测 340 passed。
…y 冲突失败)与 F56(deploy_failed 无法重试,赛事卡死)阻断
稳定性测试中新建 AWD 赛事:自动分配拿到 10.96.0.0/16,随后 deploy 直接失败 (gamebox_cidr 10.96.0.0/16 overlaps other event 376a9300-…)。根因是两个**已归档** 赛事仍持有 10.96.0.0/16 与 10.97.0.0/16 的分配行,而两侧口径不一致: 自动分配器把已归档视为空闲、部署前跨赛事重叠校验却仍把它们计入 → 新赛事永远无法部署。 修复:validate_no_cross_event_overlap 的调用方在取 other_event_networks 时排除 awd_events.status = archived 的赛事(与分配器口径一致)。归档路径本身已有自愈 (重复归档会补齐释放),故无需改释放逻辑。 验证(生产):重建赛事 → 挂靶机 200 → **deploy 200 OK**(此前 409)→ 状态 deployed / phase hardening,并真实创建 judgeserver/flagserver 与两队容器(fctf-9aaadf56-*)。 另确认 F56 非缺陷:DeployFailed 的官方恢复路径是 config_service 允许 DeployFailed → Configuring(再次配置后重新部署),我此前直接重试 deploy 才被拒。
… 验证失败原因缺失;更正硬化期派生口径
稳定性测试中反复踩到:赛事设为 hidden=true → 玩家无法建队 → 0 队伍 → precheck 后
状态变 VerificationFailed,但接口只返回 run_id,**看不到失败原因**,管理员只能靠推断。
修复:precheck 接口返回结构化报告(run_id / status / error_msg / failed_checks /
checks 明细)。各检查项明细本来就落库在 awd_precheck_runs 的 *_check 列,此前未回传。
验证(生产):构造必然失败的赛事 → 响应直接给出
status=failed, failed_checks=["config"],
checks.config.errors=[{component: teams, error: "No teams registered for this event"}]
—— 恰好印证此前对该失败的推断 ✓。
备注:错误文本仍为英文(No teams registered…),属文案待补项。
…用户=10、无 5xx);动态 flag 由 AWD 覆盖说明
…精简、动态 flag 题型就绪、归档顺滑;新发现 F58(删赛事不销毁实例容器) - 生产 compose + 安装模板:Redis 加 maxmemory 1gb / allkeys-lru;API 加 ulimits.nofile 65536(均已实测生效) - 文案:Event is ended / No teams registered / Can only archive → 中文(含上下文) - 日志:scheduler task 日志不再打印整条 Model - 动态 flag:examples/test-c 部署为 dynflagdemo,import 后 ready,实例 /flag 每实例唯一 - 归档:未结束赛事先自动结束,否则给出含状态的中文提示 - F58:DELETE /admin/events 不销毁 JS-* 实例容器(已护栏清理孤儿,根因待修)
删除赛事只拆除 AWD 运行时,Jeopardy 的 JS-/JT- 实例容器从未被处理 —— 实测删除 3 个 赛事后仍有 6 个容器在运行(/flag 仍可读),与 AWD 侧删除即回收不一致。 修复(参照 F54 对账思路,周期安全网同时覆盖删除中途崩溃): - InstanceRuntime 新增 list_instance_containers()(默认空实现;Docker 实现按平台自身 JS-/JT- 前缀过滤,避免误伤其他容器) - InstanceService::reap_orphan_containers():容器名内嵌赛事 id 前 8 位 → 核对 events 表 → 赛事不存在即回收(uuid v4 不复用,无需时间窗) - 接入 CleanRunningInstancesHandler(system.practice.clean,cron */30s) 验证:建赛→开赛→启动实例(容器存在)→ 删除赛事 → 容器 ~20s 内被回收,日志留痕 [Orphan] reaped=1;全量回归 PASS。
按当前代码逐条核实并修正文档漂移(含代码内 doc 注释): - ARCHITECTURE.md:crates 补 awdp-judgeserver;模块表补 gamebox 与 awdp、 修正 identity/community/weapon 子目录、challenge 去掉不存在的 metadata/; event 章节改写为「common + jeopardy + awd + awdp」四部分(awd_team→awd、 jeopardy 的 modes/ 实际在 domain/policy.rs);删除已不存在的 event/registry.rs 与 EventModuleRegistry,改为「路由层分发」;AppConfig 字段按 core/config.rs 重写(移除 challenge/timezone,补 awdp/registry/ main_url);AppState 字段补全(redis/publisher/scheduler/terminal_tickets); §4 数据流改为真实入口 POST /api/submit/flag 与真实符号;§5 播种来源纠正; §6 补 wake.rs - ADD-FEATURE.md:awd_team 模板路径改 awd;ChallengeToml/ChallengeConfig 与 ctx.config.challenge 均不存在,改为 AwdToml/AwdStaticConfig 与 settings 表; 模块清单补 gamebox - TESTING.md:awd_team/* 与 system/firewall.rs 改为 awd/ 与 infrastructure/firewall/、domain/firewall_state.rs;补 awdp/domain - DATABASE.md:删除「compose 把 merged.sql 挂成 initdb 自动执行」的错误说法 (compose.dev.yml 并未挂载),并明确日常开发不依赖 merged.sql - RULES.md:dev:api 实为 watchexec 监听(原写「不是 watch 进程」);merged.sql 无需 fresh clone 先生成 - gamebox-package.md:AwdEventGameBox → awd_event_gameboxes - event/mod.rs 与 event_mode.rs:AWDP「骨架期引擎未实现」注释与事实不符 验证:cargo fmt --all --check 通过、cargo check -p floatctf 通过; 文档引用经 var/doc-audit.py 对 17 个文档做路径/符号/mise 任务机械审计。
文档同步期间顺带审计 .github/workflows/ci.yml,发现四处真缺陷: 1. 触发分支不含 awd:远端只有 awd/main,而 on.push/pull_request 写的是 [master, main] → 全部开发所在的 awd 分支从未跑过 CI。这正是下面 2/3 长期未被发现的原因。已补上 awd。 2. 守卫路径漂移后静默放行:`grep -rn 'sh -c' .../event/awd_team` 里的目录 早已改名 awd,grep 对不存在路径返回 2、不进 if 分支 → 守卫永远通过。 改为扫描 awd + awdp,并在检查前断言路径存在(漂移即 FAIL 并说明原因)。 3. 同一守卫把注释当违规:nftables.rs 有一行「禁止 `nft flush ruleset`」的 文档注释会被 grep 命中 → CI 一旦真的在 awd 上跑就会红。新增 code_only 过滤(//、/*、块注释续行、*/),并限定 --include='*.rs'(apps/api/src/sql 下是冻结的 migrations,不应被这些守卫约束)。 4. `cargo fmt --all` 不是门禁:它是写入模式,有格式违规时只静默改写文件并 退出 0(已实测:注入违规后退出码 0 且文件被改写)。改为 `cargo fmt --all -- --check`。 实测:把脚本从 ci.yml 抽出执行 7 个场景 —— 当前代码 OK;nft 与 sh -c 真实 违规均 FAIL;纯注释(含块注释与续行)不误报;路径漂移 FAIL;以 * 解引用 开头的真实代码行不被误滤(code_only 用 `\*[[:space:]]` 而非裸 `\*`)。 未改动:clippy 仍不 fail-on-warning(本仓约 108 条 warning),host-network job 的展示名保留原样以免影响分支保护的必需检查名。
InstancesDto::from_pair 原本直接带出 instance.flag,只有 launch / list 两处
调用点各自补了 dto.flag.clear();另外两处选手侧接口漏了清空:
- GET /api/events/{eid}/challenges/{cid}/instance(player_service.rs:373)
- GET /api/challenges/{cid}/instance(challenge/catalog/player.rs:192)
实测选手 token 调上述接口可拿到与库内逐字符一致的真实 flag(42 字符),
配合提交接口即可不解题直接计分,竞赛完全失效。
修复下沉到 DTO 源头统一脱敏:from_pair 恒定返回空 flag,调用方无需再
逐个 clear(),从根上消除这一类遗漏。字段保留以维持响应形态与前端类型兼容,
前端仅声明类型、无任何代码读取该字段。
赛事发布状态(jeopardy_event_challenges.hidden)此前只用于过滤选手可见的 题目列表,启动与提交都不校验,于是选手只要知道题目 ID 即可解未发布题并计分。 实测:把 php_file 设为未发布后,选手仍能启动实例、读到容器 flag、提交并 计分。连带后果是排行榜自相矛盾——score 取 event_users.points 计数器(含 未发布题)而 solved_count 只数已发布题,出现「400 分 / 3 题」而库内实为 4 条 solve。 - 启动路径(instance.rs):挂题查询改为取回行并拒绝 hidden=true - 计分路径(submission_service.rs):在事务内校验赛事挂题行存在且已发布 练习赛事无挂题概念(requires_event_challenge=false),不受影响;已有实例 的未发布题提交同样被拦。
load_event_challenges 会过滤掉未发布(jeopardy_event_challenges.hidden=true) 的题目,而 solved_count 由这份过滤后的列表算出;score 却取 event_users.points 计数器(含全部 solve)。于是同一行里两个数字口径不同。 实证(生命周期赛事):库内 4 条 solve(其中 php_file 未发布), 接口返回 score=400.0 但 solved_count=3,且 challenges 数组里根本没有 php_file。 平台自身的分数定义包含全部 solve——event_users.points 计数器如此, trend 走势(Σ obtained_points + bonus_points)亦如此,故 solved_count 应与之同口径:改为按该主体在本赛事的全部 solve 计数(个人按 user_id、 战队按 team_id)。个人赛与战队赛两个分支都改。 不比「按可见题重算分数」方案:那会让分数与驱动排名的计数器背离, 且走势图仍含被下架题,反而制造新的不一致。
…分漏记) 现象:练习 workbench 点 Test Check / ALL Check 后按钮一直 Checking…,Caddy 记录单次 请求 60.69s 才返回;服务端采样显示请求卡在 pg_advisory_lock。 根因与修复 1) practice_acl:forward 链缺 conntrack 豁免 → 「池→judge 非 data port drop」把靶机回给 judge 临时端口的 SYN-ACK 丢掉(conntrack 停在 SYN_RECV,nft 计数只放行 SYN)。judge 恒不可达 ⇒ healthcheck/judge/exploit 全失败 ⇒ 判定只能是 service_down。现补 `ct state established,related accept` 并置于该限制之前,加回归断言(排序敏感)。 2) lock:advisory lock 改有界等待(90s,pg_try_advisory_lock 轮询),超时返回 409 「该实例已有判定在进行」;Drop 不再把仍持锁的连接还回连接池(后台补解锁,解锁失败 关闭该连接),避免早退/panic 时锁永久留在池里。 3) evaluation:数据面开启(生产)时 healthcheck 直接走 JudgeServer 容器内探测,不再先白跑 注定失败的进程内 3×5s+2×2s 重试。 4) judge_worker:JudgeServer 领取路径的平台侧 NO_PATCH 短路补记练习扣分(与进程内 worker 共用 score_fix_penalty 与同一幂等键),修掉「谁领到 job 决定扣不扣分」的不一致。 生产验证(镜像 14fc1a9913ca) - judge→靶机 10.42.3.0:80:修复前 TimeoutError → 修复后 CONNECT 0.001s、HTTP 200 - Test Check 未打 patch:60.69s/service_down → 1.06s/vulnerable(healthcheck+judge PASS、exploit SUCCESS) - Apply Patch(修 SSRF 的 patch.tar.gz):applied 1.54s;SSRF 403、judge 仍 200 - Test Check 已 patch:0.72s → patched - ALL Check:57.6s 才返回 → 1.1s,swept=true/swept_rounds=3,run ended,rounds 4-6 各 +150 - 隔离语义复核:pool→judge:80 通、非 data port 丢、pool→pool 丢 - NO_PATCH 扣分:在 worker tick 间隙由 JudgeServer 路径领取,账本正确写入 fix -100
风险清单 #1/#2/#12: 1) get_scores 完全没有守卫(同文件相邻 ssh-config/wireguard/status 都有 UserJwtGuard), 未登录者凭 event_id 即可读任意 AWD 赛事的全部队伍名与分数。补 UserJwtGuard。 生产实测(旧镜像):匿名 GET /api/events/{nil}/awd/scores → 200,修后由契约测试守住。 2) awd::api::player_routes 在 /api 顶层与 /api/events 各注册一次,而处理器片段形如 {event_id}/awd/...,两种前缀都能匹配 → 每个处理器多一条影子路径(生产实测 /api/{nil}/awd/scores 同样 200)。移除顶层注册,并把 mod.rs 里与事实相反的注释改写 成「只注册一次 + 为什么」。 3) 前端 /admin 的 loader 守卫被注释掉(/admin/database、version、terminal、docker 等 页面无兜底鉴权);/service 布局同样只靠子路由各自声明。改为布局级 beforeLoad 统一 守卫:admin 跳过 adminIgnoreRoutes(登录页 /admin,否则 redirect 到自身会死循环), service 未登录回登录页。 4) 鉴权契约测试此前完全没有 AWD 选手路由,所以 #1 抓不到:把 8 条 AWD 路由登记进 tests/common/routes.rs(只登记 /events 前缀,影子前缀已移除)。
风险清单 #3/#4/#5: - mise.toml:新增 web:typecheck(tsc --noEmit)并挂进 lint。此前 mise run check 全绿仍可能 被 CI 的 vite build && tsc 判红(biome lint 抓不到类型错误),是「本地绿 / CI 红」的主要来源。 - apps/api/scripts/e2e_flag_judge.sh:判据写的是仓库根目录的 Dockerfile.awd-*(从不存在), 且 ROOT 少算一级(得到 apps/)→ 必然走「cargo check → SKIP → exit 0」,CI 里叫 smoke 却 从未构建过镜像。现在:路径指向 infra/docker/awd-{flag,judge}server/Dockerfile 且漂移即 FAIL; 在 bookworm 容器里编译二进制(生产镜像 glibc 基线)→ 暂存 → 以该目录为上下文 docker build。 - .github/workflows/ci.yml:fast-rust 增加 `cargo test --workspace --tests --no-run`,让 44 个 集成测试至少编译通过(真正执行仍在 docker-e2e 作业);同步修正 smoke 步骤名与说明。
风险清单 #11:SSE 连上后轮询回退停止,而 useAwdEventStream 只失效 5 个 key (awd-scores/awd-gameboxes/admin-awd-scores/eventInfo/event),选手页实际还读 awd-player-status/awd-wg/awd-ssh/announcements;管理端 hook 从不失效 admin-awd-status, 而三个管理页都在读它 → 这些面板整场比赛只靠手动刷新更新,且 websocket「看起来正常」 (分数确实在动),很难发现。 - 新增 hooks/awdInvalidation.ts:AWD_PLAYER_QUERY_KEYS / AWD_ADMIN_QUERY_KEYS + invalidateAwdQueries(),事件流与页面手动刷新共用同一份常量。 - 两个 SSE hook 与选手页面的手动 invalidate 全部改用它(不再各自维护列表)。 - 新增回归测试:把两份列表与"按 [key,eventId] 逐条失效"钉住(3 tests)。
风险清单 #8/#10/#14: - #8 死配置:`awd.crypto_from_app_secret` 与 `registry.insecure` 只被解析进内存、没有任何生产 代码读取(TOML 注释还写着"能否从共享 JWT secret 派生",据此推断密钥来源是错的)。 从 AppConfig / AwdToml / RegistryToml / 三份 TOML 模板里一并删除(无 deny_unknown_fields, 旧配置里残留这两个键会被忽略,属于向后兼容删除)。顺带删掉因此失去唯一使用者的 default_true()。 - #14 设置键:`AWD_RATE_SUBMIT_PER_MIN`/`AWD_RATE_RESET_PER_HOUR`/`AWD_RATE_INTERNAL_PER_MIN`/ `AWD_NETWORK_REVISION`/`AWDP_DATA_PLANE_EXEC` 一直被代码读取却从未 seed(生产库里只有两条是 手工插的),缺失时靠代码兜底,管理端设置页看不到 → 想调限流阈值的人找不到开关。 按当前代码默认值补进 seed_default_settings(ON CONFLICT DO NOTHING,不覆盖运维已改的值; 描述里写明 production 必须把 AWDP_DATA_PLANE_EXEC 置 true)。 - #14 孤儿枚举:`instance_status` 的唯一使用者(instances.status 列)早已随表删除,类型却留着。 新增迁移 20261006233935 DROP TYPE IF EXISTS(已核实 information_schema 中 0 列引用; 本次本机缺 sea-orm-cli 1.1.20 未跑 db:gen,但生成实体本来就不引用该类型,已 grep 确认)。 迁移已在开发库 apply 通过(Applied 48 / Pending 0)。 - #10 空操作任务:`platform.rustfs.clean` 只打日志不清理任何对象,且日志把处理器名硬编码成 另一个处理器的 `CleanRunningInstancesHandler`。改为如实 warn「未实现的空操作」并写清实现 前提(先建立对象 key ↔ DB 引用的完整映射),避免任务表 success 制造"磁盘已被清理"的错觉。
风险清单 #6/#7/#9/#13/#15(多为"知道就够了"的认知类风险,按要求落到最近的位置): - #6 app_error.rs:写明 api ↔ modules 现状成环(本文件反向 import 各域错误,业务模块又依赖 api::prelude),以及目标方向(域侧实现转换、AppError 只留横切变体),阻止继续往这里加 import。 - #7 AuthConfig::jwt_secret:写清一值三用(JWT 签名 / AWD flag 派生 / AWDP 判题令牌), 改它等于全平台换钥、判题容器 INTERNAL_TOKEN 泄露等价于 JWT 密钥;ARCHITECTURE 同步补一节。 - #9 task_key.rs:标注 `awd.round.start` / `awd.archive.cleanup` 有 handler 但无生产者 (轮次推进是 end_round 进程内直接调 start_round),只能当手工触发入口。 - #13 docs/agents/DATABASE.md:新增"同名列陷阱"表 —— `event_id` 指向三张不同的表、 `phase` 是三套状态机,并给出典型症状与排查方式(优先实体关系,别手写 ON)。 - #15 scripts/install.sh:四个 DEFAULT_*_URL 都是 v0.0.0-fake 占位,直接安装会停在 curl 失败上 看起来像网络问题;改为动手前 ensure_real_release_urls() 快速失败并列出该传哪个参数, 收尾提示里的 `journalctl -fu ... floatctf-api`(unit 已删除)改为 floatctf.target floatctf-infra。
…换键自愈 风险清单 #7:原先一个 auth.jwt_secret 同时是 JWT 签名密钥、AWD/AWDP flag 派生根、以及 AWDP 判题容器 INTERNAL_TOKEN 的派生根 —— 三者无法独立轮换,且判题容器一旦被攻陷就等于 拿到 JWT 主密钥(可伪造超管令牌)。 拆分: - AuthConfig 新增可选 `awd_root_key` / `internal_token_key`(各自 ≥16 字符校验,Secret 包装); 未配置时**回落 jwt_secret**(`awd_root_key()/internal_token_key()`),既有部署升级不会被动 换 flag;bootstrap 检测到回落会打一条 warn,避免"看起来已经拆了"的错觉。 - 调用点按语义分派:flag/实例密钥(awdp api player/training/admin/internal、scheduler tick、 AwdCrypto::from_secret_bytes)→ awd_root_key;判题容器令牌(internal 鉴权、deploy/ensure judge、practice cron)→ internal_token_key。practice_judge 的参数随之改名。 - install.sh:全新安装生成两个与 JWT 不同的随机值(写入 .env + TOML 模板);**既有安装 不自动补**(补了等于强制轮换)。development.toml 给出固定开发值。 - 新增 4 个 config 单测:回落、优先、过短拒绝、老配置解析为 None。 顺带修掉一个会让换键静默失效的缺陷: `running_judge_env_matches` 只比 镜像/PLATFORM_INTERNAL_URL/EVENT_ID/WORKER_ID, **不比 INTERNAL_TOKEN** —— 换键后旧判题容器会被一直 "matching env — skip" 留着,而它手上的 旧令牌会被平台 401 拒绝,判题链路全线静默失败。现在 token 参与比对,env 漂移会在 30s 内 自动重建容器(生产实测:token_matches=false → recreating → 下一次 cron 恢复 skip)。 生产验证(镜像 aa270a295628) - 判题容器用自身 token 回调 /internal/awdp/judge/jobs/claim → **200**;错误 token → **401** - 换键后 drift 自动重建判题容器(token 从 eaebf538… 变为 a1b04e1b25…),无回落告警 - 练习 run 全流程(创建/start/实例创建/end)正常,说明 awd_root_key 派生链路可用
平台自 AWD/AWDP 双赛制、TOML 配置体系、helper 控制面、权限与密钥模型全部落地后, 以 1.0.0 作为正式发布版本: - apps/api/Cargo.toml:0.3.3 → 1.0.0(/api/system/version 与管理端 Version 页读的就是它, 同时是发布镜像 tag 与 org.opencontainers.image.version 标签的来源) - apps/web/package.json:0.2.8 → 1.0.0(与平台版本对齐;此前 web-v0.2.x 各自独立) - Cargo.lock:同步本地包版本
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.