网站被DDoS后制定解决方案
很平常的一天,我在网站主题交流群里,发现有群友网站被打掉线了。刚刚给他发“贺电”,没过多久,我也收到了华为云发来的“贺电”:
尊敬的(Rin):
感谢您使用华为云产品,非常抱歉打扰您。
在北京时间2025/05/22 20:33,华为云监测到您的弹性公网IP(EIP)实例 发生EIP封堵事件。 详情:
节点:亚太-新加坡
实例名称:119.8.?.?
实例ID:88????e7-1??7-4??a-8??d-0??????????5
告警描述:{"reason_zh": 尊敬的Rin:华为云DDoS防护服务在2025-05-22 20:33:05检测到您的EIP 119.8.?.? 异常流量已超过DDoS防护峰值触发封堵,封堵时带宽为 8 Gbps, 您服务器的访问将受到限制,24小时后系统将自动为您解封。您可登陆官网购买DDoS高防服务提升防护能力,保障服务器稳定运行。, "reason_en": Dear Rin: HUAWEI CLOUD Anti-DDoS Service detects that your EIP 119.8.?.? abnormal traffic has exceeded the DDoS protection peak at 2025-05-22 20:33:05 and triggers blocking. Bandwidth when blocking is 8 Gbps, Access to your server will be restricted, and the system will automatically unblock it for you after 24 hours. You can log in to the official website of HUAWEI CLOUD to purchase Advanced Anti-DDoS service to enhance protection capabilities and ensure stable server operation.}
原因分析:尊敬的Rin:华为云DDoS防护服务在2025-05-22 20:33:05检测到您的EIP 119.8.?.? 异常流量已超过DDoS防护峰值触发封堵,封堵时带宽为 8 Gbps您服务器的访问将受到限制,24小时后系统将自动为您解封。您可登陆官网购买DDoS高防服务提升防护能力,保障服务器稳定运行。
如有疑问,您可以登录华为云官网提交工单咨询,会有相应工程师为您核实查看。
而且连着几个群友的网站都被攻击,甚至有人还被短信轰炸了,初步断定就是群里面有人搞鬼,但是没有证据抓现行,只能自己做好预案和防护了。
由于DDoS攻击基本上都会被云服务器提供商黑洞IP,如果只有单台云服务器,这个真的无解。但是,我有不止一台机器,这可就有解决办法了。
我的大体解决思路是这样的:
- 配置服务器访问频率限制,防止高频恶意请求(CC攻击)耗尽服务器资源。
- 设置主服务器定期备份内容到备用服务器。
- 备用服务器部署检测,一分钟一次,判定主服务器状态,如果超过三次无响应,判定为被打挂了。
- 判定打挂后,通过Cloudflare的API,设置域名的A记录IP地址为备用服务器IP地址,并开启Cloudflare的代理防护。
- 当主服务器恢复后,自动将DNS记录切换回主服务器,然后将备份的内容还原到主服务器。
接下来,让我们开始任务吧。这里说一下环境条件:目标操作系统为 Alpine Linux 3.20 ,LNMP环境都为系统默认设置安装的,备份的目标为Typecho博客。
配置服务器访问频率限制
虽然我一开始网站就已经做了缓存配置,是能抗高并发的,不至于像某些人的网站itdog网站测速一测就死。但是,如果是高频的请求,还是会耗尽带宽的资源,导致网络阻塞,访问变慢。
所以,是时候配合iptables
和内核设置来阻挡恶意请求了。
首先是拉黑IP,平时注意留意服务器的网络负载,如果遇到有过高负载历史的时候,善于使用Nginx的日志进行IP追溯,拉黑恶意IP。拉黑命令如下:
iptables -A INPUT -s 45.140.143.77 -j DROP
iptables -A INPUT -s 106.15.224.186 -j DROP
其次,就是设置单个IP的最大连接数,对超出的部分数据包进行丢弃处理(如果是拒绝处理,还是会回传数据包导致上行被占)。这里设置为20:
iptables -A INPUT -p tcp --dport 80 -m connlimit --connlimit-above 20 --connlimit-mask 32 -j DROP
iptables -A INPUT -p tcp --dport 443 -m connlimit --connlimit-above 20 --connlimit-mask 32 -j DROP
一并设置内核参数保证连接处理数量足够:
sysctl -w net.netfilter.nf_conntrack_max=10240
由于直接丢弃数据包会造成半连接状态,仍然会消耗资源,所以要进行一些规则防止资源耗尽。
#防御 SYN Flood
# 启用 SYN Cookies(防御 SYN Flood)
sysctl -w net.ipv4.tcp_syncookies=1
# 限制 SYN 请求速率(每秒最多 20 个)
iptables -A INPUT -p tcp --syn --dport 443 -m connlimit --connlimit-above 20 -j DROP
iptables -A INPUT -p tcp --syn --dport 80 -m connlimit --connlimit-above 20 -j DROP
# 缩短 SYN 重试次数(加速无效连接释放)
sysctl -w net.ipv4.tcp_syn_retries=2
sysctl -w net.ipv4.tcp_synack_retries=2
至此,这个设置已经可以防御常见的CC攻击手段。
不过需要注意的是,以上命令只是临时生效,重启后就会失效,如果需要永久生效,则需要写入配置文件。
不过考虑到云服务器很少重启,问题也不大。
设置定期备份
我打算使用mysqldump
进行数据库导入和导出操作,然后使用rsync
进行内容同步,最后使用chmod
和chown
设置网站目录权限。
配置ssh密钥登陆
首先,如果要自动化,则需要配置ssh密钥登陆。
在客户端生成SSH密钥对:
ssh-keygen -t rsa -b 4096 -C "your_email@nekopara.uk"
执行后一路回车即可。
然后,将公钥复制到服务器:
ssh-copy-id root@server.nekopara.uk
提示输入密码,输入目标服务器密码即可完成操作。
这里your_email@nekopara.uk
请替换为你实际邮箱,server.nekopara.uk
替换为实际服务器的IP或者域名,推荐是IP。
设置自动复制规则
在主服务器上,将以下内容保存为backup_blog.sh
放到root文件夹:
/usr/bin/mysqldump -u typecho_user -p'p@sswd' typecho > "/data/database/typecho_backup.sql"
/usr/bin/rsync -avz --no-perms --delete -e ssh /data/ root@backupserver.nekopara.uk:/data/
/usr/bin/ssh root@backupserver.nekopara.uk /root/set_website.sh
然后赋予执行权限:
chmod +x /root/backup_blog.sh
相应的,在备份服务器上,也要在root文件夹创建一个set_website.sh
文件进行文件权限的设置和数据库导入:
/bin/chown -R nginx:nginx /data/typecho/
/bin/chmod -R 755 /data/typecho/
/usr/bin/mysql -u typecho_user -p'p@sswd' typecho < /data/database/typecho_backup.sql
设置完成后,自己执行一次backup_blog.sh
脚本看看有没有备份成功。
要注意,这里的backupserver.nekopara.uk
请替换为实际备份服务器的IP或者域名,推荐是IP。typecho_user
替换为实际博客数据库的用户名,p@sswd
替换为实际数据库的密码,typecho
替换为实际数据库。
测试成功后,配置crontab自动任务,一个小时执行一次:
0 * * * * "/root"/backup_blog.sh >> /var/log/backup.log 2>&1
设置状态检测和切换策略
首先,这个前提条件是你要去Cloudflare获取区域API令牌:
打开目标站点>点击“获取您的 API 令牌”>点击“创建令牌”>使用“编辑区域 DNS”模板>然后选择特定区域,创建令牌
记录下这个令牌,后面会用到,而且以后不再可以查看了,请妥善保存。
然后,在root文件夹创建一个dns_auto_update.sh
,内容如下:
#!/bin/bash
# 🐱 Cloudflare API Token
CF_API_TOKEN="neko????????????????????????????????para"
# 🐱 域名,例如 nekopara.uk
CF_ZONE_NAME="nekopara.uk"
# 🐱 记录名称列表,例如 "nekopara.uk www.nekopara.uk"
CF_RECORD_NAMES=(nekopara.uk www.nekopara.uk)
# 🐱 目标 IP
TARGET_IP="119.8.?.?"
# 🐱 目标端口
TARGET_PORT="80"
# 🐱 备用 IP
BACKUP_IP="172.16.1.233"
# 🐱 获取 Zone ID
get_zone_id() {
curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${CF_ZONE_NAME}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" | jq -r '.result[0].id'
}
# 🐱 获取 DNS 记录 ID
get_record_id() {
local zone_id="$1"
local record_name="$2"
curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=A&name=${record_name}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" | jq -r '.result[0].id'
}
# 🐱 更新 DNS 记录
update_dns_record() {
local zone_id="$1"
local record_id="$2"
local record_name="$3"
local ip="$4"
local proxied="$5"
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"${record_name}\",\"content\":\"${ip}\",\"ttl\":1,\"proxied\":${proxied}}" | jq
}
# 🐱 检测目标 IP 的 HTTP 状态码
check_target() {
curl -s --connect-timeout 10 "http://${TARGET_IP}:${TARGET_PORT}" -o /dev/null
return $?
}
# 🐱 初始化
zone_id=$(get_zone_id)
declare -A record_ids
for record_name in "${CF_RECORD_NAMES[@]}"; do
record_ids["$record_name"]=$(get_record_id "$zone_id" "$record_name")
done
current_ip="$TARGET_IP"
proxied=false
failure_count=0
success_count=0
# 🐱 监控循环
while true; do
if check_target; then
echo "🐾 目标 IP ${TARGET_IP} 响应正常"
failure_count=0
success_count=$((success_count + 1))
if [ "$current_ip" != "$TARGET_IP" ] && [ "$success_count" -ge 3 ]; then
echo "🐾 切换回原始 IP ${TARGET_IP},关闭代理"
for record_name in "${CF_RECORD_NAMES[@]}"; do
update_dns_record "$zone_id" "${record_ids[$record_name]}" "$record_name" "$TARGET_IP" false
done
current_ip="$TARGET_IP"
proxied=false
success_count=0
fi
else
echo "🐾 目标 IP ${TARGET_IP} 无响应"
failure_count=$((failure_count + 1))
success_count=0
if [ "$current_ip" != "$BACKUP_IP" ] && [ "$failure_count" -ge 3 ]; then
echo "🐾 切换到备用 IP ${BACKUP_IP},开启代理"
for record_name in "${CF_RECORD_NAMES[@]}"; do
update_dns_record "$zone_id" "${record_ids[$record_name]}" "$record_name" "$BACKUP_IP" true
done
current_ip="$BACKUP_IP"
proxied=true
failure_count=0
fi
fi
sleep 60
done
运行这个脚本需要确保jq
和curl
软件包已经安装。
这个shell脚本前面是配置部分,请根据注释的提示替换为实际内容。脚本策略是,每60秒(一分钟)检测一次目标站点的http服务,如果目标站点连续三次异常,则切换到备用服务器并开启Cloudflare的代理防护。当目标服务器恢复正常后,则切换回去。
确认脚本无误后,在备用服务器上面创建一个tmux终端挂着就行了:
tmux new -t dns_auto
./dns_auto_update.sh
总结一下
其实漏了一步——将备份服务器的内容还原回目标服务器。这部分就跟第一部分是同理的,不加crontab计划任务就行了。也可以结合切换脚本自行实现。
由于我网站流量比较小,对内容的及时性没有这么高的要求,在被打期间我也不会写文章。最多就是在被打期间如果有人评论,那这个评论会在主服务器恢复后的数据同步中被清除,无非就是丢一两条评论,而且大部分时间也没人评论,所以无所谓了。
现在想想如果真的搞了,万一恢复的时间赶在同步时间的点子上,冲突搞炸数据库就不好玩了。
其实就算是主服务器备份到备胎服务器,也不一定需要计划任务,每次更新文章后连上去执行一下脚本就行,顺手的事情。
切换期间如果真有评论想要保留的,手动迁移过去也不费什么事,毕竟被打也算是小概率事件,玩网站一年多了才被打这一次。
或许有人会好奇,既然被打就是切Cloudflare,为什么不一直套Cloudflare,这样不就不怕打了吗?其实我不怎么做是有原因的,因为我还是希望网站速度能快一些,而切了Cloudflare,那速度自然就快不起来了。而且,一直套Cloudflare降低网站国内访问速度和质量,和在关键时刻因为DDoS临时切换Cloudflare降低速度和质量,哪个更好还是看得出来的。当然,怎么玩是建立在你有两个服务器到的基础上,如果只有一个,打寄了就寄了,一点办法都没有。
其实之前就已经做好被打的预案了,但是实际发生了被DDoS的时候,还是有点手忙脚乱,这下能让我的网站更加完善了。
最主要还是想保持网站不掉线罢了。