DDOS导致Nginx 499状态码成因分析

摘要

Nginx 499状态码的成因分析

27 Apr 2017

Reading time ~1 minute

工作时遇到一个疑似被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请求时的代码:

客户端: DDOS导致Nginx 499状态码成因分析

服务端:

服务端的图就不截了,基本就是客户端的反向,基本没有差别。

从TCP流分析,首先开始是TCP三次握手,然后服务端向客户端发送HTTP请求,发送完HTTP请求之后,立刻发送FIN包,请求关闭TCP连接,开始四次挥手的流程。差不多60ms之后,客户端收到了服务端的发来的ACK包,代表服务端进入CLOSE-WAIT状态,此时TCP四次握手并没有完全关闭,服务端依然会将HTTP请求的数据发送给客户端。所以我们看到了NO.504包和NO.561包,length均为1514,这个包就是服务端把HTTP的返回包发给客户端的包,但是客户端接收到这个包之后,由于客户端本身已经关闭连接不接收数据,所以就会发RST包给服务端,代表接收到了异常数据,连接异常关闭。这也就导致了TCP四次挥手没有全部做完,而是异常关闭的。附一张TCP的连接图如下:

DDOS导致Nginx 499状态码成因分析

回到我们的问题上,我们的猜想也可以合理解释了: 即使我们的SOCK连接是发送了http请求后立马就请求关闭TCP连接,但是TCP四次挥手需要时间,在TCP挥手结束之前,服务端就把数据传输了过来,所以返回包依然是200。

二次测试

这个时候我们还无法验证我们的猜想是否正确,所以我找了一个PHP的站点,又一次进行了上述实验,在Nginx代理PHP的环境下,上述两个测试脚本均造成了大量499状态码的出现。

抓下包,截图如下: DDOS导致Nginx 499状态码成因分析

这是跑第一个sock脚本的结果,这个TCP流就简单多了,先三次握手,再发送HTTP请求包,再四次挥手,可以看到在此时服务端并没有返回数据给客户端,也就是说明服务端在TCP连接关闭之前,没有生成完网页内容并把内容返回给客户端。此时再看一下服务端的状态,Nginx日志中出现了大量499状态。

DDOS导致Nginx 499状态码成因分析

结论分析

经过两次测试,初步可以得出以下结论:

  1. 499状态码产生的原因是在服务端准备好返回内容并发送之前,客户端已经关闭了和服务端的 TCP 连接。而之所以我的博客不会产生是因为静态页面,Nginx处理的很快,而Nginx主要用作反向代理,准备返回包的内容肯定会有一定的延迟,所以499状态码的出现屡见不鲜。
  2. 经抓包分析,python的httplib模块,在发送完HTTP请求,接受完数据后就会主动关闭TCP连接,下次请求时会再新建一个TCP连接,而不是一直保持一个HTTP长连接不断。
  3. 网上流传的解决措施: proxy_ignore_client_abort on; ,经测试没有任何效果。

Nginx 499状态码的成因分析

27 Apr 2017

Reading time ~1 minute

工作时遇到一个疑似被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请求时的代码:

客户端: DDOS导致Nginx 499状态码成因分析

服务端:

服务端的图就不截了,基本就是客户端的反向,基本没有差别。

从TCP流分析,首先开始是TCP三次握手,然后服务端向客户端发送HTTP请求,发送完HTTP请求之后,立刻发送FIN包,请求关闭TCP连接,开始四次挥手的流程。差不多60ms之后,客户端收到了服务端的发来的ACK包,代表服务端进入CLOSE-WAIT状态,此时TCP四次握手并没有完全关闭,服务端依然会将HTTP请求的数据发送给客户端。所以我们看到了NO.504包和NO.561包,length均为1514,这个包就是服务端把HTTP的返回包发给客户端的包,但是客户端接收到这个包之后,由于客户端本身已经关闭连接不接收数据,所以就会发RST包给服务端,代表接收到了异常数据,连接异常关闭。这也就导致了TCP四次挥手没有全部做完,而是异常关闭的。附一张TCP的连接图如下:

DDOS导致Nginx 499状态码成因分析

回到我们的问题上,我们的猜想也可以合理解释了: 即使我们的SOCK连接是发送了http请求后立马就请求关闭TCP连接,但是TCP四次挥手需要时间,在TCP挥手结束之前,服务端就把数据传输了过来,所以返回包依然是200。

二次测试

这个时候我们还无法验证我们的猜想是否正确,所以我找了一个PHP的站点,又一次进行了上述实验,在Nginx代理PHP的环境下,上述两个测试脚本均造成了大量499状态码的出现。

抓下包,截图如下: DDOS导致Nginx 499状态码成因分析

这是跑第一个sock脚本的结果,这个TCP流就简单多了,先三次握手,再发送HTTP请求包,再四次挥手,可以看到在此时服务端并没有返回数据给客户端,也就是说明服务端在TCP连接关闭之前,没有生成完网页内容并把内容返回给客户端。此时再看一下服务端的状态,Nginx日志中出现了大量499状态。

DDOS导致Nginx 499状态码成因分析

结论分析

经过两次测试,初步可以得出以下结论:

  1. 499状态码产生的原因是在服务端准备好返回内容并发送之前,客户端已经关闭了和服务端的 TCP 连接。而之所以我的博客不会产生是因为静态页面,Nginx处理的很快,而Nginx主要用作反向代理,准备返回包的内容肯定会有一定的延迟,所以499状态码的出现屡见不鲜。
  2. 经抓包分析,python的httplib模块,在发送完HTTP请求,接受完数据后就会主动关闭TCP连接,下次请求时会再新建一个TCP连接,而不是一直保持一个HTTP长连接不断。
  3. 网上流传的解决措施: proxy_ignore_client_abort on; ,经测试没有任何效果。

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: