D^3CTF 2026 运维小记
一年一度的 D^3CTF 也是已经接近尾声了,而似乎我负责 Vidar 的运维工作起也过去快 1 年了的样子(虽然感觉自己没做什么事情)。
于是很自然地,D^3 似乎没有足够的人手(指抓不到其他小登),我就来帮忙做今年 D^3CTF 的运维了,本篇以我的视角记录一下今年运维的基本情况。
基本架构
今年 @shiori 师傅为我们争取到了咕咕噜的赞助,有一张 500$ 的 coupon,对于一个 24 小时的比赛来说,是非常充足的了。事前我还在尽量控制成本,后面发现根本用不完。
我们全部服务都打算部署在 GCP 上,平台单独运行在一台 VM 上,然后使用 Kubernetes Engine 自动分配好一个 k8s 集群。不过,前期资源划分、平台部署的工作都是 rx 师傅做的(伟大!),毕竟办赛经验充足许多,我刚开始的时候被 gcp 一大堆菜单整头晕了(比起网页,其实 gcloud cli 可能会更好用)。
与往年一样,平台使用的是「高性能ですから!(∠・ω<)⌒☆」的 ret2shell,平台机器在准备阶段使用 2c4g,赛时 8c8g 就足够满足需求了。
一台 Golang、Rune、TLS 上的灵车
由于 ret2shell 已经是一个开源项目了,所以我想我们可以详细的说说今年整的一些活。
众所周知,传统的远程环境一般都是平台去运行一个容器,然后将服务暴露到公网 ip 的某个端口上。很明显,我们在一个 ip 上很可能有多个暴露的服务,这就会导致选手有意或无意地访问其他选手的服务,甚至获取不属于自己的 flag。
由于 ret2shell 具备强大的反作弊机制,这种行为很容易被办赛方认为相关选手产生了一些不明的 py 交易。(事实上,扫描平台本身也应当是不被允许的)
为了防止这种情况出现,我们想到的一个方案是尝试把端口编码 / 加密在域名中,直接扫描别人的服务几乎就不会发生了。问题来了,域名和端口要怎么放在一起呢,DNS 肯定不能帮我们搞定这些脏活,所以只能单独起一个转发服务去负责这个工作。
对于不同题目的环境,比较常见的有裸 TCP、HTTPS 和 UDP,而 UDP 题我们今年是没有的。那看起来比较可行的方案是,让所有连接都基于 TLS,确保请求中携带了端口相关的信息,这里指 SNI。
本来我们是想尽量依赖可靠的开源库去完成连接的转发,例如 nginx stream,不过怪异的是它的转发对 TCP 半关闭的支持并不是很好,而刚好我们的题目有一些 fin 的需求,遂放弃。对网络实现比较完备,io 高性能的语言可能首先想到的就是 golang 了,后面也确实证明了这一点,我们最终基于 go 搓了一个自己的转发器跑在 VM 上,用来接受公网传入连接,并且转发到内网的集群上。
把端口编码在域名中听起来很好,转发时只要解密出对应的端口就行,但是还需要考虑一些低概率的重入问题。后面我又看了一下平台的环境路由、生命周期功能,感觉倒不如直接基于生命周期(创建、续期和销毁)维护一套路由表,动态地维护每个实例对应的域名和生命周期,域名中包含的信息是一个唯一、随机的 ID。基于 Rune 的环境路由与生命周期定义帮我们完成了平台侧的所有操作。平台所做的事情就是和转发服务的 API 交互,并且拿到域名返回给选手;转发服务所做的事情就是查表和转发。
资源分配
使用咕咕噜的集群管理可以方便地调整节点容量,比赛前一个小时,我们直接开满了 8 台 8c32g,按 24 小时的成本计算并不算太贵。即使在比赛的高峰期间(开赛结束前后的约五小时内),也只有最高 30% 的 CPU 占用,所以在预算紧张的情况下我们也可以做进一步的调整。
而转发服务使用的 2c8g 机器,在整个比赛过程中 CPU 也只去到 40%。
一些小插曲
由于吸取了各位学长的经验,比如尽早扩容
quota、做好充分压测等等,比赛期间服务稳定性还是相当可观的。少数几个观测到的问题还是偶发的 cluster internal error,由于对比赛并没有造成影响,我决定还是等待赛后再去排查。后面发现其实也和当时推测的类似,是 API 交互方面产生了 Err,不过并不是遇到性能瓶颈了,只是一些错误处理没写好,可能有选手遇到了这种情况,希望没有造成不良好的体验 (╥﹏╥)