好久没写博文了,昨晚睡了不足4个小时结果还没睡好,原因是女朋友跟我分手,这辈子最真爱的一个。
不说了那还得继续生活奋斗,最近解决公司CC攻击问题,头大的要死。
因为架构原因要在前端nginx代理解决这个问题,没有硬件只能软件的解决这个问题。
Nginx 有 2 个模块用于控制访问“数量”和“速度”,简单的说,控制你最多同时有 多少个访问,并且控制你每秒钟最多访问多少次, 你的同时并发访问不能太多,也不能太快,不然就“杀无赦”。
HttpLimitZoneModule 限制同时并发访问的数量
HttpLimitReqModule 限制访问数据,每秒内最多几个请求
limit_conn_zone
语法: limit_conn_zone $variable zone=name:size;
默认值: none
配置段: http
该指令描述会话状态存储区域。键的状态中保存了当前连接数,键的值可以是特定变量的任何非空值(空值将不会被考虑)。$variable定义键,zone=name定义区域名称,后面的limit_conn指令会用到的。size定义各个键共享内存空间大小。如:
limit_conn_zone $binary_remote_addr zone=addr:10m;
注释:客户端的IP地址作为键。注意,这里使用的是$binary_remote_addr变量,而不是$remote_addr变量。
$remote_addr变量的长度为7字节到15字节,而存储状态在32位平台中占用32字节或64字节,在64位平台中占用64字节。
$binary_remote_addr变量的长度是固定的4字节,存储状态在32位平台中占用32字节或64字节,在64位平台中占用64字节。
1M共享空间可以保存3.2万个32位的状态,1.6万个64位的状态。
如果共享内存空间被耗尽,服务器将会对后续所有的请求返回 503 (Service Temporarily Unavailable) 错误。
limit_zone 指令和limit_conn_zone指令同等意思,已经被弃用,就不再做说明了。
limit_conn_log_level
语法:limit_conn_log_level info | notice | warn | error
默认值:error
配置段:http, server, location
当达到最大限制连接数后,记录日志的等级。
limit_conn
语法:limit_conn zone_name number
默认值:none
配置段:http, server, location
指定每个给定键值的最大同时连接数,当超过这个数字时被返回503 (Service Temporarily Unavailable)错误。如:
limit_conn_zone $binary_remote_addr zone=addr:10m; |
server { |
location /www.hzcsky.com/ { |
limit_conn addr 1; |
} |
} |
同一IP同一时间只允许有一个连接。
当多个 limit_conn 指令被配置时,所有的连接数限制都会生效。比如,下面配置不仅会限制单一IP来源的连接数,同时也会限制单一虚拟服务器的总连接数:
limit_conn_zone $binary_remote_addr zone=perip:10m; |
limit_conn_zone $server_name zone=perserver:10m; |
server { |
limit_conn perip 10; |
limit_conn perserver 100; |
} |
[warning]limit_conn指令可以从上级继承下来。[/warning]
limit_conn_status
语法: limit_conn_status code;
默认值: limit_conn_status 503;
配置段: http, server, location
该指定在1.3.15版本引入的。指定当超过限制时,返回的状态码。默认是503。
limit_rate
语法:limit_rate rate
默认值:0
配置段:http, server, location, if in location
对每个连接的速率限制。参数rate的单位是字节/秒,设置为0将关闭限速。 按连接限速而不是按IP限制,因此如果某个客户端同时开启了两个连接,那么客户端的整体速率是这条指令设置值的2倍。
----------------------------------------------------------------------------
limit_req_zone
语法: limit_req_zone $variable zone=name:size rate=rate;
默认值: none
配置段: http
设置一块共享内存限制域用来保存键值的状态参数。 特别是保存了当前超出请求的数量。 键的值就是指定的变量(空值不会被计算)。如
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; |
说明:区域名称为one,大小为10m,平均处理的请求频率不能超过每秒一次。
键值是客户端IP。
使用$binary_remote_addr变量, 可以将每条状态记录的大小减少到64个字节,这样1M的内存可以保存大约1万6千个64字节的记录。
如果限制域的存储空间耗尽了,对于后续所有请求,服务器都会返回 503 (Service Temporarily Unavailable)错误。
速度可以设置为每秒处理请求数和每分钟处理请求数,其值必须是整数,所以如果你需要指定每秒处理少于1个的请求,2秒处理一个请求,可以使用 “30r/m”。
limit_req_log_level
语法: limit_req_log_level info | notice | warn | error;
默认值: limit_req_log_level error;
配置段: http, server, location
设置你所希望的日志级别,当服务器因为频率过高拒绝或者延迟处理请求时可以记下相应级别的日志。 延迟记录的日志级别比拒绝的低一个级别;比如, 如果设置“limit_req_log_level notice”, 延迟的日志就是info级别。
limit_req_status
语法: limit_req_status code;
默认值: limit_req_status 503;
配置段: http, server, location
该指令在1.3.15版本引入。设置拒绝请求的响应状态码。
limit_req
语法: limit_req zone=name [burst=number] [nodelay];
默认值: ―
配置段: http, server, location
设置对应的共享内存限制域和允许被处理的最大请求数阈值。 如果请求的频率超过了限制域配置的值,请求处理会被延迟,所以所有的请求都是以定义的频率被处理的。 超过频率限制的请求会被延迟,直到被延迟的请求数超过了定义的阈值,这时,这个请求会被终止,并返回503 (Service Temporarily Unavailable) 错误。这个阈值的默认值为0。如:
limit_req_zone $binary_remote_addr zone=limit_com:10m rate=1r/s; |
server { |
location /www.hzcsky.com.com/ { |
limit_req zone=limit_com burst=5; |
} |
} |
限制平均每秒不超过一个请求,同时允许超过频率限制的请求数不多于5个。
如果不希望超过的请求被延迟,可以用nodelay参数,如:
limit_req zone=ttlsa_com burst=5 nodelay; |
--------完成配置事例--------------------------------------------
## 用户的 IP 地址 $limit 作为 Key,每个 IP 地址最多有 50 个并发连接
## 你想开 几千个连接 刷死我? 超过 50 个连接,直接返回 503 错误给你,根本不处理你的请求了
当,然这是都是ngin来处理,不会影响后端的tomcat等WEB 应用 ,如果nginx网卡流量堵塞和单台压力问题就的想别的办法了。后面会说在解决的
limit_req_zone $limit zone=tlcy_com:10m rate=10r/s;
limit_req_log_level info;
limit_conn_zone $limit zone=addr:10m;
limit_conn_log_level info;
## 用户的 IP 地址 $limit 作为 Key,每个 IP 地址每秒处理 10 个请求
## 你想用程序每秒几百次的刷我,没戏,再快了就不处理了,直接返回 503 错误给你
## 具体服务器配置
http{.... limit_req_zone $limit zone=tlcy_com:10m rate=10r/s; limit_req_log_level info; limit_conn_zone $limit zone=addr:10m; limit_conn_log_level info; server { listen 80; server_name www.hzcsky.com; if ($http_user_agent ~* LWP::Simple|BBBike|wget|Sosospider|YodaoBot) { return 403; } ## root /data/www/; ## index hou.txt; location /mp4/ { if ($request_method !~ ^(GET|HEAD|POST)$ ) { return 444; } } location / { if ($request_method !~ ^(GET|HEAD)$ ) { return 444; } proxy_next_upstream http_502 http_504 error timeout invalid_header; proxy_pass http://tlcy; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; allow all; } ## 最多 5 个排队, 由于每秒处理 10 个请求 + 5个排队,你一秒最多发送 15 个请求过来, 再多就直接返回 503 错误给你了 limit_req zone=tlcy_com burst=5 nodelay; limit_conn addr 10; location ~* \.(gif|jpg|png|swf|flv)$ { valid_referers none blocked www.hzcsky.com ; if ($invalid_referer) { rewrite ^/ http://www.hzcsky.com/403.html; #return 404; } } }
事务都具有两面性的。ngx_http_limit_conn_module 模块虽说可以解决当前面临的并发问题,但是会引入另外一些问题的。如前端如果有做LVS或反代,而我们后端启用了该模块功能,那不是非常多503错误了? 这样的话,可以在前端启用该模块,要么就是设置白名单。
------------------------------白名单设置-----------------------------------------
## 具体服务器配置 http{.... geo $white_ip { default 1; 127.0.0.1 0; 10.0.0.0/8 0; } #白名单 map $white_ip $limit { 1 $binary_remote_addr; 0 ""; } limit_req_zone $limit zone=tlcy_com:10m rate=10r/s; limit_req_log_level info; limit_conn_zone $limit zone=addr:10m; limit_conn_log_level info; server { listen 80; server_name www.hzcsky.com; if ($http_user_agent ~* LWP::Simple|BBBike|wget|Sosospider|YodaoBot) { return 403; } ## root /data/www/; ## index hou.txt; location /mp4/ { if ($request_method !~ ^(GET|HEAD|POST)$ ) { return 444; } } location / { if ($request_method !~ ^(GET|HEAD)$ ) { return 444; } proxy_next_upstream http_502 http_504 error timeout invalid_header; proxy_pass http://tlcy; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; allow all; } ## 最多 5 个排队, 由于每秒处理 10 个请求 + 5个排队,你一秒最多发送 15 个请求过来, 再多就直接返回 503 错误给你了 limit_req zone=tlcy_com burst=5 nodelay; limit_conn addr 10; location ~* \.(gif|jpg|png|swf|flv)$ { valid_referers none blocked www.hzcsky.com ; if ($invalid_referer) { rewrite ^/ http://www.hzcsky.com/403.html; #return 404; } } }
#################如果 单台nginx解决不了前面需要LVS或者haproxy 了做4层 然后nginx多个 来解决这个问题,如果多台的 因为过来的全是LVS 的IP 这需要很麻烦的解决了。
(这里只说明结果,不了解 Http 协议的人请自行 Google 或者 Wikipedia http://zh.wikipedia.org/zh-cn/X-Forwarded-For )
当一个 CDN 或者透明代理服务器把用户的请求转到后面服务器的时候,这个 CDN 服务器会在 Http 的头中加入 一个记录
X-Forwarded-For : 用户IP, 代理服务器IP
如果中间经历了不止一个 代理服务器,像 www.bzfshop.net 中间建立多层代理之后,这个 记录会是这样
X-Forwarded-For : 用户IP, 代理服务器1-IP, 代理服务器2-IP, 代理服务器3-IP, ….
可以看到经过好多层代理之后, 用户的真实IP 在第一个位置, 后面会跟一串 中间代理服务器的IP地址,从这里取到用户真实的IP地址,针对这个 IP 地址做限制就可以了,
取得用户的原始地址
日志开启显示 :
log_format main '$http_x_forwarded_for $remote_addr - - $time_iso8601 "$request_method $scheme://$host$request_uri $server_protocol" $status $bytes_sent "$http_refe
rer" "$http_user_agent" $request_time $upstream_cache_status:TCP';
1 2 3 4 5 6 7 8 9 10 11 |
map $http_x_forwarded_for $clientRealIp { ## 没有通过代理,直接用 remote_addr "" $remote_addr; ## 用正则匹配,从 x_forwarded_for 中取得用户的原始IP ## 例如 X-Forwarded-For: 202.123.123.11, 208.22.22.234, 192.168.2.100,... ## 这里第一个 202.123.123.11 是用户的真实 IP,后面其它都是经过的 CDN 服务器 ~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr; }
## 通过 map 指令,我们为 nginx 创建了一个变量 $clientRealIp ,这个就是 原始用户的真实 IP 地址, ## 不论用户是直接访问,还是通过一串 CDN 之后的访问,我们都能取得正确的原始IP地址 |
完整配置事例:
## 具体服务器配置 http{ map $http_x_forwarded_for $limit { "" $remote_addr; ~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr; } # map $white_ip $limit { # 1 $clientRealIp; # 0 ""; # } limit_req_zone $limit zone=tlcy_com:10m rate=5r/s; limit_req_log_level info; limit_conn_zone $limit zone=addr:10m; limit_conn_log_level info; server { listen 80; server_name www.hzcsky.com; if ($http_user_agent ~* LWP::Simple|BBBike|wget|Sosospider|YodaoBot) { return 403; } ## root /data/www/; ## index hou.txt; location /mp4/ { if ($request_method !~ ^(GET|HEAD|POST)$ ) { return 444; } } location / { if ($request_method !~ ^(GET|HEAD)$ ) { return 444; } proxy_next_upstream http_502 http_504 error timeout invalid_header; proxy_pass http://tlcy; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; allow all; } ## 最多 5 个排队, 由于每秒处理 10 个请求 + 5个排队,你一秒最多发送 15 个请求过来, 再多就直接返回 503 错误给你了 limit_req zone=tlcy_com burst=5 nodelay; limit_conn addr 10; location ~* \.(gif|jpg|png|swf|flv)$ { valid_referers none blocked www.hzcsky.com ; if ($invalid_referer) { rewrite ^/ http://www.hzcsky.com/403.html; #return 404; } } }
因为他限制的是 原始IP 不用设置白名单之类了就。自己人可以直接访问nginx层,是原始IP地址没有限制就。
还要合理的封掉IP 就需要日志配合脚本了:
#!/bin/bash #取得参数$1为并发阈值,若留空则默认允许单IP最大200并发! if [[ -z $1 ]];then num=200 else num=$1 fi LOG=/root/log/nginx/sns_access.log STATUS=503 #请求检查、判断及拉黑主功能函数 function check(){ iplist=`cat $LOG |grep -i $STATUS |grep -i "," |awk '{print $2}' |grep -v "-" |sed "s#,##g" |sort |uniq -cd |sort -rn| awk -v str=$num '{if ($1>str){print $2}}'` if [[ ! -z $iplist ]]; then >/data/shell/black_ip.txt for black_ip in $iplist do #白名单过滤中已取消IP段的判断功能,可根据需要自行修改以下代码 #exclude_ip=`echo $black_ip | awk -F"." '{print $1"."$2"."$3}'` #grep -q $exclude_ip ./white_ip.txt grep -q $black_ip /data/shell/white_ip.txt if [[ $? -eq 0 ]];then echo "$black_ip (white_ip)" >>/data/shell/black_ip.txt else echo $black_ip >> /data/shell/black_ip.txt # iptables -nL | grep $black_ip ||(iptables -I INPUT -s $black_ip -j DROP & echo "$black_ip `date +%Y-%m-%H:%M:%S`">>/data/shell/denylog.txt ) fi done #存在并发超过阈值的单IP就发送邮件 # if [[ `cat ./sendmail` == 1 ]];then sendmsg;fi fi } function checka(){ iplist=`cat $LOG |grep -i $STATUS |grep -v "," |awk '{print $1}' |sort |uniq -cd |sort -rn | awk -v str=$num '{if ($1>str){print $2}}'` if [[ ! -z $iplist ]]; then >/data/shell/black_ip.txt for black_ip in $iplist do #白名单过滤中已取消IP段的判断功能,可根据需要自行修改以下代码 #exclude_ip=`echo $black_ip | awk -F"." '{print $1"."$2"."$3}'` #grep -q $exclude_ip ./white_ip.txt grep -q $black_ip /data/shell/white_ip.txt if [[ $? -eq 0 ]];then echo "$black_ip (white_ip)" >>/data/shell/black_ip.txt else echo $black_ip >> /data/shell/black_ip.txt # iptables -nL | grep $black_ip ||(iptables -I INPUT -s $black_ip -j DROP & echo "$black_ip `date +%Y-%m-%H:%M:%S`">>/data/shell #/denylog.txt ) fi done #存在并发超过阈值的单IP就发送邮件 # if [[ `cat ./sendmail` == 1 ]];then sendmsg;fi fi } #发邮件函数 function sendmsg(){ netstat -nutlp | grep "sendmail" >/dev/null 2>&1 || /etc/init.d/sendmail start >/dev/null 2>&1 echo -e "From: 发邮件地址@qq.com\nTo:收邮件地址@qq.com\nSubject:Someone Attacking your system!!\nIts Ip is" >./message cat ./black_ip.txt >>./message /usr/sbin/sendmail -f 发邮件地址@qq.com -t 收邮件地址@qq.com -i <./message >./sendmail } ##间隔10s无限循环检查函数 #while true #do # check # #每隔10s检查一次,时间可根据需要自定义 # sleep 10 #done # check #处理没有代理的IP checka #处理多层的IP
我们日志是5分钟切割一次。所以就检查5分钟内非法的IP 给封掉,在写个2小时重置IPtables的计划任务 就行了。