标签归档:Nginx

绕过代理服务器获取客户端IP

一般来说,Web应用和客户端之间隔着很多个代理服务器,无论正向代理还是反向代理。这样就给本地资讯类应用(新闻、天气等)或者统计(审计,追踪等)需求带来了麻烦,因为在应用通过request.getRemoteAddr()方法(以Java为例)往往只能拿到与自己直接通信的设备IP。因此,如果用户通过直接或者间接的正向代理(ISP提供的缓存服务器等)上网,Web应用只会取到正向代理服务器地址;如果Web应用前端部署着Nginx或者Apache之类的反向代理,Web应用只会取到反向代理的地址。

面对这样的限制,代理厂商利用HTTP自定义Header规避了问题。比如Nginx,可以添加以下配置:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

它向后端应用传递了两个HTTP自定义Header,X-Real-IP和X-Forwarded-For。这两个Header作用类似,都是向后端透传客户端源IP,但是又有一些不同,主要区别在于:

1. X-Forwarded-For已有标准RFC文档(RFC 7239)定义,而X-Real-IP没有。
2. X-Forwarded-For Header格式为X-Forwarded-For: client1, proxy1, proxy2,每级代理都会将与自己直接通信的对端IP追加在X-Forwarded-For中,因此应用还需要额外解析IP列表以获取所需IP。而X-Real-IP只会记录与自己直接通信的对端IP。

由此,反向代理后面的应用服务器可以通过X-Real-IP或者X-Forwarded-For请求头来获取客户端IP:

String xRealIp = request.getHeader("X-Real-IP");
String xForwardIps = request.getHeader("X-Forwarded-For");

另外需要注意的是,HTTP Header是很容易被伪造的,因此,X-Forwarded-For Header中的首个IP也不意味着就一定是客户端源IP。从安全性方面来说,如果应用前不加反向代理,则request.getRemoteAddr()拿到的IP便是可信的,无法伪造;如果加了反向代理,那么X-Real-IP和X-Forwarded-For IP的最后一个IP是可信的,无法伪造。

--EOF--

博客搬迁至DigitalOcean VPS

0x00: 起因

最近想把博客从国内的虚拟主机升级到VPS,原因有3点:
1. 虚拟主机空间有限,100M的空间大小(含数据库)捉襟见肘。
2. 灵活度太小。后期想新增几个博客,如果用VPS的话可以共用空间,降低成本。
3. 网络环境越来越差了,VPN已经成为必需品。

0x01: 选型

决定了购买国外的VPS以后,接下来的就是简单地评测工作。有两家待选:
1. Linode. 老牌VPS提供商,服务和售后服务稳定,其在东京的机房网络对国内用户非常友好。
2. DigtalOcean. VPS界的后起之秀,凭性价比取胜,它提供的VPS更接近云主机。对国内访问来说,有旧金山和新加坡两大机房可选,虽然物理上新加坡更近,但是种种资料表明旧金山的网络更加稳定,更有说法国内到新加坡的流量是经过旧金山的。

分别选取了两家最小规格VPS,Linode东京机房1024型(24G存储,1VCPU,2T流量),DigtalOcean旧金山机房和新加坡机房的512型(20G SSD存储,1VCPU, 1T流量)。用阿里测跑了下全网ping值,Linode稳定在80-100ms,DigtalOcean新加坡机房能保持在150ms以内,旧金山机房则在200-400ms不等。再在本地用ab跑了下Nginx静态页面测试,三个节点性能差不多,但是不知为何SSH到新加坡节点后操作异常卡顿,感觉其ping值名不副实,故首先排除DigtalOcean新加坡节点。

从测试情况来看,Linode VPS胜出,但是我却选择了DigtalOcean旧金山机房节点,因为它便宜了一半,5刀一个月,一年算下来360RMB。不是有句话说吗,选择恐惧症说到底还是因为穷。Linode给出的2TB流量对我这种个站来说完全多余,而且我相信DigtalOcean的SSD能在磁盘读写方面弥补一些网络延时造成的劣势。

0x02: 部署

1. 软件安装

部署采用广泛的LNMP架构(Linux+Nginx+MySQL+PHP),操作系统Debian7, 所有依赖软件安装用apt-get命令。

1
2
3
4
apt-get update
apt-get install nginx mysql-server-5.5
apt-get install php5 php5-mysql php5-cli php5-common
apt-get install php5-fpm php5-cgi

2. 数据导入

把原先博客的数据导出成sql文件,安装phpMyAdmin工具,phpMyAdmin是一个PHP编写的MySQL Web客户端管理软件,去官网下个最新版解压到Nginx vhost根目录下即可,安装成功后用http://ip/phpMyAdmin/index.php访问,根据提示导入sql数据。

3. 程序迁移
把原先博客的WordPress目录打包后解压至Nginx vhost根目录下,修改wp-config.php文件中的数据库用户名和密码,修改Nginx vhost配置后reload,理论上此时博客应该迁移成功了。Nginx的关键配置如下:

1
2
3
4
5
6
7
8
9
10
root /usr/share/nginx/www;
index index.php;
 
server_name fengchj.com *.fengchj.com localhost;
location ~ \.php$ {
  fastcgi_split_path_info ^(.+\.php)(/.+)$;
  fastcgi_pass unix:/var/run/php5-fpm.sock;
  fastcgi_index index.php;
  include fastcgi_params;
}

4. 修改DNS解析
去DNSPod上把域名的CNAME记录改成A记录,指到DigtalOcean VPS,保存后本地测试发现实时生效。

至此,博客迁移完毕。期间配置方面的问题可以查看Nginx日志(/var/log/nginx/access.log和error.log),比如Nginx worker进程跑在www-data用户下,而程序文件可能对其不可读写或执行时会报Permission Deny等等,这类问题通过Google很好解决。部署方面不想折腾的话可以使用LNMP一键安装包一键搞定必需软件的依赖和配置,不过我不喜欢这种定制化的方式,过程不可控。

0x03: 优化
1. 修改MySQL密码为强密码。混淆phpMyAdmin的访问路径,避免猜测攻击。
2. 安装七牛镜像存储WordPress插件。它支持博客静态文件CDN加速,免费的,绝对是WordPress必装插件之一。
3. 安装Disable Google Fonts插件。WordPress的Twenty Twelve主题在管理后台不知道什么原因依赖了Google的Open Sans字体,这在Google被禁的地方非常不友好,每次页面加载都被这个请求阻塞,安装这个插件后后台打开速度会提升不少。
4. 优化php-fpm和MySQL的内存使用策略,避免OOM Killer问题,参考『小内存VPS站点内存使用调优』一文。(2015-04-11更新)
以上就是本次博客迁移的全部过程,历时一天。按照惯例放出我的DigtalOcean邀请链接,点此连接注册可以获取10刀优惠,这可是相当于免费体验两个月的最小规格VPS了。

--EOF--

多级反向代理下的维护页面配置

Nginx的error_page指令可以用来在服务器临时维护时呈现静态维护页面,增强用户体验,但是在稍微复杂的反向代理部署架构中,只有error_page还是不够的。比如:
proxy
Tomcat前端接Apache,Apache前端接Nginx,流量入口在Nginx。这样一个两级的反向代理结构下,如果服务器程序升级,Tomcat重启,Apache检测到Tomcat服务不可用时,向上级返回503,那么用户通过Nginx看到的,也是丑陋的503 Service Temporarily Unavailable页面,试图在Nginx配置中加上error_page 503 /50x.html页面也不会起作用。探其原因,在于error_page指令是用来处理由Nginx产生的错误,在Nginx看来,Apache工作正常,且能返回响应码和正确页面,只不过返回的页面对用户来说是个服务不可用页面。如果是Apache停掉,那么Nginx是能够正确呈现error_page指定的维护页面的。

所以,需要另外在Nginx配置中加入proxy_intercept_errors指令,官方说法是:

Determines whether proxied responses with codes greater than or equal to 300 
should be passed to a client or be redirected to nginx for processing with the 
error_page directive.

设置proxy_intercept_errors为on后,Nginx会解析被代理服务器的响应码,如果出现大于300的响应码,就会调用error_page指令处理,如果error_page指令中没有指定返回的错误码,那么Nginx仍会依样返回被代理服务器的响应。

以下为简化的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
 server_name domain.com;
 location / {
  ...
  proxy_pass ...;
  proxy_intercept_errors on;
 } 
 
 error_page 503 /50x.html;
 location = /50x.html {
  root /etc/nginx/maintein;
 }
 #50x.html中引入了同级目录下的static目录中的图片文件。
 location ~ /static {
  root /etc/nginx/maintein;
  rewrite ^(.*)/(static/.*)$ /$2 break;
 }
}

--EOF--

多Region应用系统设计和部署

有一个Web应用系统,需要跨Region部署,每个Region依赖的底层服务各不相同,但是要求是入口统一、单点登录(SSO)。一些较大的业务平台(商务领航)、监控平台、云计算平台(aws)应该都有类似需求。整体结构如下图所示:
multi-regions

用户通过mydomain.com入口进入系统,登陆后可选择进入不同Region,使用不同的后端服务(Service1,Service2...),并且可以随时通过页面选择进入其他Region。除非用户明确切换Region,否则每次请求总是落到相同Region上。

在设计方面,要实现上述系统,需关心以下几点:
1. 单点登陆。在没有可靠的集中式session服务器存在的情况下,将用户认证信息放在Cookie中是个较好的选择,去掉应用系统的状态,方便扩展,应用可以以集群的方式部署在任一Region里。这里意味着,当用户在任一Region登陆成功后,应用系统要向浏览器写认证Cookie,此后,用户无论在Region内部或者Region间跳转都可以不必重新登陆。

2. 底层服务去耦合。应用系统作为业务聚合的地方,需与底层服务尽量松耦合。一般底层服务以RESTful API的形式对外暴露接口,应用系统对其的依赖可以删减至最少,只需将其服务地址(IP+端口号)加入配置项即可。相同Region中的Tomcat集群使用相同配置文件,不同Region使用不同的配置文件。

3. 反向代理标记。架构图中可以看出前端有个Nginx作为反向代理,将请求代理到某个Region下Tomcat集群中的一个节点。Nginx实际上是个透明代理,应用系统必须通过一种方式将用户所选择的Region信息告知Nginx,才能保证每次用户请求落到相同Region上。最简单的做法当然是由应用系统将当前Region的id写入到REGION Cookie里,Nginx根据此Cookie值进行反向代理。也可以选择不侵入系统,应用系统只需给出第一推动力,比如给个参数,此后Nginx会一直将该参数传递下去,实现上述Cookie实现的功能。

在部署方面,主要涉及应用系统部署和Nginx部署:
1. 应用系统。应用系统只要做到了无状态,就可以很简单的以Tomcat集群方式对外提供服务。注意每个集群下的节点配置文件不同,主要是RegionId, 底层服务地址等有所区别。

2. Nginx。各个Tomcat集群中的节点负载均衡交给Nginx upstream模块实现,一个集群对应一段upstream配置,均衡策略根据需要选择。例如:

1
2
3
4
5
6
7
8
9
10
upstream region1 {
  server 10.10.1.1;
  server 10.10.1.2;
  server 10.10.1.3;
}
 
upstream region2 {
  server 10.10.2.1;
  server 10.10.2.2;
}

多Region的配置方面,要实现Region间切换,Nginx需支持根据regionid参数反向代理,要实现Region内停留,需支持根据Cookie值反向代理。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#省略部分配置
location / {
  if ($args ~ regionid=1){
    proxy_pass http://region1;
    break;
  }
  if ($args ~ regionid=2){
    proxy_pass http://region2;
    break;
  }
  if ($http_cookie ~* "REGION=1" ){
    proxy_pass http://region1;
    break;
  }
  if ($http_cookie ~* "REGION=2" ){
    proxy_pass http://region2;
    break;
  }
  proxy_pass http://region1;
 
  #省略部分配置
}

上述配置的含义是,当url中携带显式的regionid参数时,根据参数值代理到相应Region,该参数只有用户选择切换Region时才会携带。当url中不带regionid参数,则根据REGION Cookie的值进行反向代理。否则,Nginx默认将请求代理到默认Region上。

--EOF--

keepalived vip漂移基本原理及选举算法

keepalived可以将多个无状态的单点通过虚拟IP(以下称为VIP)漂移的方式搭建成一个高可用服务,常用组合比如keepalived+nginx,lvs,haproxy和memcached等。它的实现基础是VRRP协议,包括核心的MASTER竞选机制都是在VRRP协议所约定的。

一、配置说明:
keepalived的配置位于/etc/keepalived/keepalived.conf,配置文件格式包含多个必填/可选的配置段,部分重要配置含义如下:
global_defs: 全局定义块,定义主从切换时通知邮件的SMTP配置。
vrrp_instance: vrrp实例配置。
vrrp_script: 健康检查脚本配置。

细分下去,vrrp_instance配置段包括:
state: 实例角色。分为一个MASTER和一(多)个BACKUP。
virtual_router_id: 标识该虚拟路由器的ID,有效范围为0-255。
priority: 优先级初始值,竞选MASTER用到,有效范围为0-255。
advert_int: VRRP协议通告间隔。
interface: VIP所绑定的网卡,指定处理VRRP多播协议包的网卡。
mcast_src_ip: 指定发送VRRP协议通告的本机IP地址。
authentication: 认证方式。
virtual_ipaddress: VIP。
track_script: 健康检查脚本。

vrrp_script配置段包括:
script: 一句指令或者一个脚本文件,需返回0(成功)或非0(失败),keepalived以此为依据判断其监控的服务状态。
interval: 健康检查周期。
weight: 优先级变化幅度。
fall: 判定服务异常的检查次数。
rise: 判定服务正常的检查次数。

这里有MASTERBACKUP的参考配置。

二、选举算法
keepalived中优先级高的节点为MASTER。MASTER其中一个职责就是响应VIP的arp包,将VIP和mac地址映射关系告诉局域网内其他主机,同时,它还会以多播的形式(目的地址224.0.0.18)向局域网中发送VRRP通告,告知自己的优先级。网络中的所有BACKUP节点只负责处理MASTER发出的多播包,当发现MASTER的优先级没自己高,或者没收到MASTER的VRRP通告时,BACKUP将自己切换到MASTER状态,然后做MASTER该做的事:1.响应arp包,2.发送VRRP通告。

MASTER和BACKUP节点的优先级如何调整?
首先,每个节点有一个初始优先级,由配置文件中的priority配置项指定,MASTER节点的priority应比BAKCUP高。运行过程中keepalived根据vrrp_script的weight设定,增加或减小节点优先级。规则如下:

1. 当weight > 0时,vrrp_script script脚本执行返回0(成功)时优先级为priority + weight, 否则为priority。当BACKUP发现自己的优先级大于MASTER通告的优先级时,进行主从切换。
2. 当weight < 0时,vrrp_script script脚本执行返回非0(失败)时优先级为priority + weight, 否则为priority。当BACKUP发现自己的优先级大于MASTER通告的优先级时,进行主从切换。 3. 当两个节点的优先级相同时,以节点发送VRRP通告的IP作为比较对象,IP较大者为MASTER。 以上文中的配置为例: HOST1: 10.15.8.100, priority=91, MASTER(default) HOST2: 10.15.8.101, priority=90, BACKUP VIP: 10.15.8.102 weight = 2 抓包命令: tcpdump -nn vrrp 示例一:HOST1和HOST2上keepalived和nginx均正常。

1
2
3
4
16:33:07.697281 IP 10.15.8.100 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 93, authtype simple, intvl 1s, length 20
16:33:08.697588 IP 10.15.8.100 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 93, authtype simple, intvl 1s, length 20

此时HOST1优先级为priority + weight = 93,HOST2优先级为priority + weight = 92,HOST1仍为MASTER。

示例二:关闭HOST1上的nginx。

1
2
3
4
5
6
7
8
16:33:09.697928 IP 10.15.8.100 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 93, authtype simple, intvl 1s, length 20
16:33:10.698285 IP 10.15.8.100 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 91, authtype simple, intvl 1s, length 20
16:33:10.698482 IP 10.15.8.101 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 92, authtype simple, intvl 1s, length 20
16:33:11.699441 IP 10.15.8.101 > 224.0.0.18: VRRPv2, Advertisement, vrid 102, 
prio 92, authtype simple, intvl 1s, length 20

HOST1上的nginx关闭后,killall -0 nginx返回非0,HOST1通告的优先级为priority = 91,HOST2的优先级为priority + weight = 92,HOST2抢占成功,被选举为MASTER。相关日志可tail /var/log/messages。

由此可见,主从的优先级初始值priority和变化量weight设置非常关键,配错的话会导致无法进行主从切换。比如,当MASTER初始值定得太高,即使script脚本执行失败,也比BACKUP的priority + weight大,就没法进行VIP漂移了。所以priority和weight值的设定应遵循: abs(MASTER priority - BAKCUP priority) < abs(weight)。 另外,当网络中不支持多播(例如某些云环境),或者出现网络分区的情况,keepalived BACKUP节点收不到MASTER的VRRP通告,就会出现脑裂(split brain)现象,此时集群中会存在多个MASTER节点。 --EOF--