内容简介:DDOS导致Nginx 499状态码成因分析
工作时遇到一个疑似被DDOS的情况,查看网站日志发现Nginx有大量的499状态码,但是正常用浏览器访问网站却能访问,没有任何问题。所以搜索了一些资料,找到了原因并复现了该现象。
猜想
499 状态码是 Nginx 自己定义的特殊返回码,查看资料,Nginx源码中是如下规定的:
ngx_string(ngx_http_error_495_page), /* 495, https certificate error */ ngx_string(ngx_http_error_496_page), /* 496, https no certificate */ ngx_string(ngx_http_error_497_page), /* 497, http to https */ ngx_string(ngx_http_error_404_page), /* 498, canceled */ ngx_null_string, /* 499, client has closed connection */
字面上的意思是客户端关闭了连接导致的。我的理解是客户端在发送HTTP请求后,不接收返回包,也不等服务器响应,立即关闭HTTP通道,或者强行关闭tcp连接。这样就会导致服务器在想发送response包给客户端时,找不到客户端的连接,所以提示499的状态码。
测试
针对以上的猜想,我对我自己的博客网站做了测试。用sock和httplib分别写了两段代码如下:
sock:
#encoding:utf-8
#created by noble
import sys
import socket
from multiprocessing import Process as pro
from multiprocessing.dummy import Process as thr
def sendsock(target, url, i):
obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
obj.settimeout(0.8)
obj.connect((target, 80))
print i
obj.send("GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36\r\nAccept: */*\r\n\r\n" %(url, target))
obj.close()
print i
if __name__ == '__main__':
target = sys.argv[1]
url = "/goods.php?id=51"
for i in range(1000):
t=pro(target=sendsock, args=(target, url, i))
t.start()
httplib:
#encoding:utf-8
#created by noble
import sys
import socket
import gevent
import httplib
from multiprocessing import Process as pro
from multiprocessing.dummy import Process as thr
def sendhttp(target, url, i, obj):
print i
obj.request('GET', url)
obj.getresponse()
def run(target, url, i, obj):
jobs=[gevent.spawn(sendhttp,target, url, i, obj) for i in range(10)]
gevent.joinall(jobs)
for i in jobs:
i.join()
if __name__ == '__main__':
target = sys.argv[1]
url = "/goods.php?id=51"
obj = httplib.HTTPConnection(target, 80, timeout=0.5)
for i in range(1000):
t=pro(target=run, args=(target, url, i, obj))
t.start()
第一段sock的代码全用的多进程,第二段httplib的用多进程和协程结合,然而这两种方法对于我的博客并发4-5千没有任何影响,没有一个499状态码出现。按照网络上文章的解释,这里我猜测是无论是sock还是http,在关闭请求时都没有关闭TCP连接,所以导致了客户端其实没有断开连接,所以服务端依然返回200。所以抓包分析了一下在做sock请求时的代码:
客户端:
服务端:
服务端的图就不截了,基本就是客户端的反向,基本没有差别。
从TCP流分析,首先开始是TCP三次握手,然后服务端向客户端发送HTTP请求,发送完HTTP请求之后,立刻发送FIN包,请求关闭TCP连接,开始四次挥手的流程。差不多60ms之后,客户端收到了服务端的发来的ACK包,代表服务端进入CLOSE-WAIT状态,此时TCP四次握手并没有完全关闭,服务端依然会将HTTP请求的数据发送给客户端。所以我们看到了NO.504包和NO.561包,length均为1514,这个包就是服务端把HTTP的返回包发给客户端的包,但是客户端接收到这个包之后,由于客户端本身已经关闭连接不接收数据,所以就会发RST包给服务端,代表接收到了异常数据,连接异常关闭。这也就导致了TCP四次挥手没有全部做完,而是异常关闭的。附一张TCP的连接图如下:
回到我们的问题上,我们的猜想也可以合理解释了: 即使我们的SOCK连接是发送了http请求后立马就请求关闭TCP连接,但是TCP四次挥手需要时间,在TCP挥手结束之前,服务端就把数据传输了过来,所以返回包依然是200。
二次测试
这个时候我们还无法验证我们的猜想是否正确,所以我找了一个 PHP 的站点,又一次进行了上述实验,在Nginx代理PHP的环境下,上述两个测试脚本均造成了大量499状态码的出现。
抓下包,截图如下:
这是跑第一个sock脚本的结果,这个TCP流就简单多了,先三次握手,再发送HTTP请求包,再四次挥手,可以看到在此时服务端并没有返回数据给客户端,也就是说明服务端在TCP连接关闭之前,没有生成完网页内容并把内容返回给客户端。此时再看一下服务端的状态,Nginx日志中出现了大量499状态。
结论分析
经过两次测试,初步可以得出以下结论:
- 499状态码产生的原因是在服务端准备好返回内容并发送之前,客户端已经关闭了和服务端的 TCP 连接。而之所以我的博客不会产生是因为静态页面,Nginx处理的很快,而Nginx主要用作反向代理,准备返回包的内容肯定会有一定的延迟,所以499状态码的出现屡见不鲜。
- 经抓包分析,python的httplib模块,在发送完HTTP请求,接受完数据后就会主动关闭TCP连接,下次请求时会再新建一个TCP连接,而不是一直保持一个HTTP长连接不断。
- 网上流传的解决措施:
proxy_ignore_client_abort on;,经测试没有任何效果。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Django 1.0 Template Development
Scott Newman / Packt / 2008 / 24.99
Django is a high-level Python web application framework designed to support the rapid development of dynamic websites, web applications, and web services. Getting the most out of its template system a......一起来看看 《Django 1.0 Template Development》 这本书的介绍吧!