# 一种DDOS的应对方式 (May 18, 2023)

最近网站遭到了DDOS，流量飙得老高，100GB的流量包半周就能耗完。发现攻击集中在 376 KB 的最大的bundle文件上。由于攻击来自多个IP，且（由于我比较菜）没有什么特征可以分辨，逐url逐IP的令牌桶没效果，所以一直都没有实质性的办法。

> 说起来，这人一开始还通过尾随 queryString 的方式击穿我CDN的缓存，[跑了我源站几十GB的流量](https://web.archive.org/web/20230519041743im_/https://cdn.muspimerol.site/anti-dos-05-04)直接给我干欠费了 哈哈哈
>

今天突然想到了一种办法通过cookies的方式，让浏览器访问的友好用户可以正常访问，而恶意攻击不会占用我太大流量：

我先是创建了一个自动刷新的HTML：

```html
<html>

<head>
    <meta http-equiv="refresh" content="0">
</head>

</html>
```

还可以更短：

```html
<meta http-equiv="refresh" content="0">
```

我的思路是，让所有请求都解析出这个HTML，但响应投中包含一个set-cookies，刷新后的请求发现cookies对上了就返回原始数据。这个方案的优势在于：

1. 可以完全作为中间件存在，无需改动服务源码
2. 用户无感，因为这个刷新实测非常快

> 事实上，这其实很容易被破解，攻击者只需要模仿浏览器执行set-cookies的行为即可。如果他确实这么做了，那我觉得只能把set-cookies放在`<script>`中实现，然后结合混淆的方式来改进了。毕竟要是要执行js的话，基本上就得用Puppeteer、Selinium之类的麻烦玩意儿了
>

欢迎大家开无痕打开Devtools去尝试尝试我[部署了这个中间件的网页](https://chat.0.bnu120.space/)

### 效果
<img src="https://picgo.cdn.muspimerol.site/20230519002351.png" width="1025" title="" crop="0,0,1,1" id="scrvT" class="ne-image">

### 附录
源码如下：

```python
import aiohttp
from sanic import Request, Sanic
from sanic import __version__ as sanic_version
from sanic.response import empty, file, raw

app = Sanic(__name__)

@app.route("/<path:path>")
async def index(request, path):
    async with aiohttp.ClientSession() as session:
        async with session.get(
            f"<protocol>://<host>:<port>/{path}", headers=request.headers
        ) as resp:
            headers = resp.headers.copy()
            headers.pop("Transfer-Encoding", None)
            headers.pop("Content-Encoding", None)

            return raw(await resp.read(), status=resp.status, headers=headers)

CHECK_KEY = "human"
CHECK_VAL = "Hi!"

@app.on_request
async def check_not_first(request: Request):
    if CHECK_KEY in request.cookies:
        if request.cookies.get(CHECK_KEY) == CHECK_VAL:
            return

    if "text/html" in request.headers.get("accept", ""):
        response = await file("./redirect.html")
        response.add_cookie(CHECK_KEY, CHECK_VAL)
        return response
    else:
        return empty()
```