内容简介: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;
,经测试没有任何效果。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 机器学习PAI实践三:雾霾成因分析
- 北京大学专家:软件供应链安全的风险和成因分析
- 利用X-Forwarded-For伪造客户端IP漏洞成因及防范
- 利用X-Forwarded-For伪造客户端IP漏洞成因及防范
- 类初始化导致死锁
- 组合漏洞导致的账号劫持
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。