笔者近期在忙 Lilac 五一欢乐赛的事。其中过程百般曲折,不过回顾这一段时间折腾 cdn、全站加速、nginx,感觉收获还是很大的,故写一篇博客记录下来。

  竞赛平台采用了 CTFd(也没有其他选项,只有 CTFd 可以用)。最初以部署在华为云上,带宽只有 1Mbps,非常卡。于是迁移到了 Vultr 东京节点,虽然国际访问非常流畅,但国内访问又断断续续了。套 Cloudflare 加速之后,国内可用性比较高,至少不会 Timeout 了;但是速度还是上不来,与最初的华为云半斤八两。

  最终决定迁移回华为云 VPS,同时采取各种加速手段。本文记载了如何在 VPS 只有 1Mbps 带宽的情况下,把网站加速到用户可以接受的水平。


0x00 安装 CTFd

  这是最不用动脑子的部分。docker 即可安装:

docker run --name ctfd -dit -p 8000:8000 ctfd/ctfd

  于是就起了一个 CTFd 服务,运行在8000端口上。如果想要修改源码,可以搞 volume 挂载网站的代码目录,这样直接在实体机上修改,docker 里面的文件就能更新。如果想让它自动启动,加上  --restart=always 就行。

▲ 安装好之后的 CTFd, 改了一下界面

  另外,由于 VPS 上有很多个容器(pwn 题的和 web 题的),还是需要一个 docker 管理工具。我以前一直用 DaoCloud 来做这件事,这一次采用  portainer

docker volume create portainer_data
docker run -d -p 10000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer
▲ 安装 portainer,Web UI 在 9000 端口

  于是可以方便地管理容器、查看负载:

▲ portainer 管理界面
▲ CTFd 的实时资源监控

  目前为止我们安装好了 CTFd.

0x01 基本配置

  首先我们得把主页给改掉。在 Pages 里面改 index 就行。我们放了一张 logo 在首页。代码如下:

<center><img src="/files/xxxx/lilac-white.png" width="750px"/></center>

  另外,我们把主题色改成像饿了么那样的蓝色。直接去 element ui 抄配色就行了。注入 header 代码如下:

<style id="theme-color">
:root {--theme-color: #409EFF;}
.navbar{background-color: var(--theme-color) !important;}
.jumbotron{background-color: var(--theme-color) !important;}
</style>

  现在我们有了漂亮的首页和 Challenges 界面。

▲ Challenges 界面。没做的题是黑色,成功的题是绿色。

  为了不带端口用域名访问,我们采用 nginx 反代 CTFd. 在  sites-enabled 创建一个 train 文件(这不是个好习惯。正常的做法是在 sites-avaliable 创建配置文件,然后用符号链接放进 sites-enabled,不过我们敏捷开发,就不管那么多了),写进如下内容:

server {
    listen 80;
    server_name self.lilac, train.hitctf.cn, ctf.ruanx.net;
 
    location /{
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:8000;

	}
}
▲ nginx 配置。由 nginx 反代 8000 端口 

  配置好 A 记录之后,访问 train.hitctf.cn 可以访问到 CTFd. 我们基本配置已经完成,这是一个可以拿去用的服务器了。

  本次比赛中,我们需要上传一个 5 MB 左右的文件,但是返回了 413 状态码。需要修改 nginx 配置,允许上传大文件:

# ...

http {

    # ...

    client_max_body_size 1G;

	
    # ...
}
▲ 修改 /etc/nginx/nginx.conf ,文件大小限制改成 1GB

  于是可以顺利上传。来下载一下试试:

▲ 华为云说给你 1Mbps,绝对不会多给

  所以,我们需要专注于加速了。加速!加速!


0x02 nginx 缓存

  首先想到的是用 nginx 来缓存静态文件,分担 CTFd 的服务器负担(CTFd 是 flask 写的)。在 nginx.conf 里面写:

proxy_cache_path /var/cache/nginx/proxycache levels=1:2:2 keys_zone=proxycache:500m inactive=120s max_size=1g;
nginx.conf 缓存配置

  然后在 sites-enabledtrain 改成:

# ...
location /{
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:8000;

        proxy_cache proxycache;
        proxy_cache_key $request_uri;
        proxy_cache_valid 200 302 301 10m;
        proxy_cache_valid any 1m;
}
▲ nginx 配置全站缓存 

  做完了之后,出现了新的问题:在 admin 修改题目之后,迟迟不能更新。从而我们必须只缓存静态文件。规则改为:

location /{
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:8000;
}

location /themes/core/static/ {
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        add_header Lilac-Proxy 'yes';
        proxy_pass http://127.0.0.1:8000;

        proxy_cache proxycache;
        proxy_cache_key $request_uri;
        proxy_cache_valid 200 302 301 10m;
        proxy_cache_valid any 1m;
    }
▲ nginx 只缓存静态文件 

  再尝试访问,发现确实 /themes/core/static 里面的文件会加上 Lilac-Proxy: yes 这个头;其余的页面都没有这个头。我们完成了 nginx 层面的配置。

0x03 cdn 与全站加速

  CTFd 引用了 fontawesome 等一系列外部库。资源都在国外,访问起来很慢。我们可以利用 cdn 加速。

  具体来讲,我是这样干的:在阿里云 OSS 上开一个桶,存下这些信息;建立一个 cdn,源站设为这个 OSS 桶。OSS 本身在国内访问已经很快了,建立 cdn 之后全球访问都很快。于是解决了外部资源问题。

  至于内部资源,我们采用了阿里云的 “全站加速”。把 ctf.ruanx.net CNAME 到阿里云指定的地址,由阿里云申请 https 证书,这样我们的 CTFd 平台就支持了 https.

  全站加速分为动态加速和静态加速两个部分。

  • 静态加速:对于指定的静态文件,由阿里云缓存到边缘网络。
  • 动态加速:直接回源。阿里云会选择最快的路由。

  我们主要是利用静态加速功能。设置如下:

▲ 设置静态文件路径 
▲ 设置 cache 规则,0s 表示不缓存

  现在,静态加速已经做到极致了。直接访问 ctf.ruanx.net,页面可以在 2s 之内加载出来。

0x04 按量计费的带宽

  阿里云的 cdn 和全站加速,虽然帮我们把静态资源加速到飞快,但动态请求还是要回源的。如果阻塞得比较严重,会影响所有人的游戏体验。

  我们在华为云弄一个弹性 IP,选择按量计费(0.64元 / GB),带宽放到 300 Mbps. 接下来把这个 IP 绑定到服务器的网卡上,然后配置路由。官方有教程,直接抄官方教程就行了:

dhclient eth1

ip route flush table net1
ip route add default via 192.168.0.1 dev eth1 src 192.168.0.132 table net1
ip route add 192.168.1.0/24 dev eth1 src 192.168.0.132 table net1
ip rule add from 192.168.0.132 table net1 
▲ 给新加的网卡配置路由

  现在用新网卡的 IP 地址可以访问到我们的平台。换句话讲,我们的平台有了两条路——一条是 1Mbps,流量免费的;另一条是 300Mbps,流量费很贵的。考虑到 CTFd 不是很耗流量的网站,按量计费是没问题的。

  接下来,我们把动态请求交给 300Mbps 的路线。考虑到这条路线的流量费比全站加速更贵,我们仍然由阿里云的全站加速来缓存静态文件(这样也可以降低服务器压力)。

0x05 全站加速的主从切换

  把全站加速的源站信息改为:

  阿里云会每隔几秒发个包检测服务器状态。在主服务器正常工作的情况下,回源到主服务器;否则回源到备用服务器。这样配置之后,如果遭到 DDOS 攻击,我们卸掉 300Mbps 的网卡,全站加速就开始从 1Mbps 的线路回源,减少我们的损失。

  现在 ctf.ruanx.net 高速度、高可用性、全球访问丝滑流畅了。Enjoy the game!


Update 2020-05-01 10:00

▲ 比赛开始后一小时的流量

  我们扛住了大量的访问,而且所有人都很流畅。


Update 2020-05-03 09:00

▲ 开赛当天的 DDoS

  开赛当晚 20:30 ~ 21:30,有神必壬打了两次 DDoS,有代理池,疯狂访问 scoreboard 和《打工是不可能打工的》附件 che.gif . 至于是谁打的我暂且蒙在鼓里。打出了一共 1.2GB 流量,峰值带宽大约 60Mbps,可惜全都打在 cdn 上了,对业务没有任何影响 :)

▲ 回源一共回了 26 MB 的流量
▲ 开赛日当天,全站加速 cdn 的字节命中率