从如何获取可信赖的IP地址聊起

作者: joyqi 时间: October 11, 2014 分类: 编程开发,PHP
起因

写这篇文章缘起SF的一个问题 http://segmentfault.com/q/1010000000686700/a-1020000000687155。由此我想到了很多,就和大家随便聊聊吧
在PHP中获取ip地址有一段网上流传甚广的代码,还有它的各种变种

function real_ip()
{
    static $realip = NULL;

    if ($realip !== NULL)
    {
        return $realip;
    }

    if (isset($_SERVER))
    {
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
        {
            $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);

            /* 取X-Forwarded-For中第一个非unknown的有效IP字符串 */
            foreach ($arr AS $ip)
            {
                $ip = trim($ip);

                if ($ip != 'unknown')
                {
                    $realip = $ip;

                    break;
                }
            }
        }
        elseif (isset($_SERVER['HTTP_CLIENT_IP']))
        {
            $realip = $_SERVER['HTTP_CLIENT_IP'];
        }
        else
        {
            if (isset($_SERVER['REMOTE_ADDR']))
            {
                $realip = $_SERVER['REMOTE_ADDR'];
            }
            else
            {
                $realip = '0.0.0.0';
            }
        }
    }
    else
    {
        if (getenv('HTTP_X_FORWARDED_FOR'))
        {
            $realip = getenv('HTTP_X_FORWARDED_FOR');
        }
        elseif (getenv('HTTP_CLIENT_IP'))
        {
            $realip = getenv('HTTP_CLIENT_IP');
        }
        else
        {
            $realip = getenv('REMOTE_ADDR');
        }
    }

    preg_match("/[\d\.]{7,15}/", $realip, $onlineip);
    $realip = !empty($onlineip[0]) ? $onlineip[0] : '0.0.0.0';

    return $realip;
}

这段代码的原理是从HTTP_X_FORWARDED_FOR以及HTTP_CLIENT_IP还有REMOTE_ADDR中获取ip地址。

这段代码有问题吗?如果从功能角度来看,它是没有问题的,因为它保证了你可以尽可能地获取到一个ip地址,如果你地程序不需要验证这个ip地址是否正确,只是为了显示它的来源,那么你可以这样做没问题。

但是很多情况下我们需要确切地知道当前跟服务器连接的是哪个ip,那这段代码就不妥了。

在PHP的$_SERVER变量中,凡是以HTTP_开头的键值都是直接从客户端的HTTP请求头中直接解析出来的。

这句话啥意思,意思就是客户端告诉你ip是多少就是多少,伪造它的成本可以说非常低。除了这两个以HTTP_开头的地址外,还有一个REMOTE_ADDR地址,这个地址是与你的服务器进行实际tcp连接的地址,它是一个可信赖的地址。要伪造这个地址成本很高,这是由于TCP三次握手协议的原理决定的,除非你能骗过服务器端的路由,那这样的成本就太高了,还不如直接黑进你的服务器

衍生

上图是一个经过简化的TCP握手协议,握手就是指在你正式传输信息之前,客户端和服务端确认双方是否可信的过程,这三步可以通俗化成下面的过程

小C (Client) 向小S (Server) 发了一封信(SYN),告诉它我要寄一个包裹 (数据) 给你,但是为了保证这个包裹你能收到,我先发一封信确认下你的地址 (Server IP) 是不是正确的,怎么确认呢?收到这封信后请按信上的地址 (Client IP) 回信就行了。
小S收到了这封信以后一想,我不仅要回信确认我的地址是存在的(ACK),还要问问你的地址是不是正确的,要不然我咋知道你是不是靠谱的人。于是小S给小C发了一封回信,不仅在信里确认自己的地址是正确的(ACK),也要求(SYN)小C在收到信件后给自己发一封回信,以确认他的地址也是真实的。要不然我会拒绝你的连接
小C在收到小S的确认后,马上发了一封回信(ACK)告诉我已经收到信了
从此它们就可以愉快的通信了。

那么问题来了

如果小C没有收到小S的确认信,但他也发了一封回信骗小S说自己已经收到了,怎么破?

这个问题实际上在TCP协议中已经解决了,每个SYN请求都会包含一个随机数字,发送回信ACK的时候,必须一并将这个数字加一再发回去。这样我们收到ACK的时候只要确认这个值是否正确就行了。

如果邮政局被收买了怎么?

这确实是一个风险,这就是我刚才提到的,路由器被黑掉的可能。这也就是很多人为什么说互联网的基础是非常脆弱的,只要公网上的其中一个路由器说了假话,所有经过这个路由器的数据都会变成不可信的。

但同时这个成本非常高,因为路由器的安全级别通常是很高的,而且一般的路由器管理端口也不会向普通访问者开放。所以能做到这一点的黑客,通常会选择直接去黑你的服务器。

这就好比我为了寄点东西噁心你一下,还要去贿赂一整个邮政局,显然不划算。还不如我直接买张火车票,赶到你的住处,把你门撬了,放你桌上。

再发散下

SYN FLOOD攻击

也就是SYN洪水攻击,看了上面的解释你应该大概知道SYN是啥意思了,就是一个询问包。所谓SYN洪水攻击就是指Client发送第一个SYN包的时候,告诉Server的源地址是一个假的,当Server发送回执时会进入到一个half-open(半开)状态。

为啥叫半开,因为这个端口只开了一半,握手过程已经开始,但还有一半没完成。这个半开连接大家应该都听过,早先玩迅雷下载或者bt的时候都需要打一个windows半开连接数补丁,就是因为桌面操作系统的计算资源是优先倾向于GUI的,当系统处于半开连接时因为处于等待状态,是会消耗内存和计算资源的,所以操作系统会默认把这个值调低,限制你能接受的连接。而我们p2p下载又需要比较高的连接量,所以就有了这个补丁,把它的限制打开。

我花这段篇幅解释半开连接数并不是没事找事,这其实就是SYN FLOOD攻击的原理。因为当系统处于半开状态时要消耗资源,而服务器往往半开连接数限制都比较大(或者干脆没限制),因此接第一段的话,因为服务器得到的源地址是个假的,发送回执后肯定会收不到确认,因此就会进入到漫长的等待过程(相对于响应时间)。攻击者通过伪造大量的这种无效请求,使服务器端等待大量连接,从而耗尽服务器资源,以达到使其瘫痪的目的。

想看看你是不是受到了此种攻击,只需要在netstat时看看是不是有大量的SYN_RCVD连接即可。SYN_RCVD表示服务器处于握手的第二步。

随机数的漏洞

我先将刚刚讲过的一个问题引用下

如果小C没有收到小S的确认信,但他也发了一封回信骗小S说自己已经收到了,怎么破?
这个问题实际上在TCP协议中已经解决了,每个SYN请求都会包含一个随机数字,发送回信ACK的时候,必须一并将这个数字加一再发回去。
这段话段的关键在于“随机数”这三个字,因为我们这个验证过程的一大基础是Server生成的随机数是不可能被Client端提前知道的,这就好比打扑克时你不可能知道其他人的牌是什么,一个道理。

但如果这个Client是个赌神呢?

http://www.securityfocus.com/bid/25348/discuss
根据安全机构的研究,Linux内核中包含了一个可导致Dos和权限提升的漏洞,可以被黑客利用来运行攻击代码。这是一个核心内存的堆栈溢出问题,可以导致系统的崩溃,再特定的环境下,还可以提升权限。这个漏洞影响到了Linux内核2.6.22.3前的版本。
这是一个被爆出来的随机数漏洞,如果我知道了服务端的随机数漏洞,那么完全可以利用这一点来伪造出一个完美的假ip。

最后弱弱地总结下

那我们到底该相信哪个ip呢?

首选REMOTE_ADDR,因为虽然有如此多地伪造方法,但在语言层面你只能选一个最可靠地。
如果你地服务隐藏在负载均衡或者缓存系统后面,它通常会给你发一个Client-Ip或者X-Forwarded-For的HTTP头,告诉你跟它连接的客户端ip,这时的HTTP_CLIENT_IP和HTTP_X_FORWARDED_FOR是可信的,因为它是从前端服务器上的直接传递过来的,当然你必须在程序中指定只信任这一个来源,而不要像最开始的代码那样每个都检测一遍

使用UPYUN加速静态资源

不过貌似在我采用https的情况下用http的静态资源会提示不安全。采用https的upyun空间解决了。

Windows下的Go编译

继续折腾多用户SS
由于客户端需要在win下使用,于是要在win下编译go语言的local客户端
去golang.org下载了官方的安装包,安装完成后配置了系统变量
GOROOT为安装目录
PATH变量加入GOROOT\bin
GOPATH为某个编译安装目录
然后进入使用go version查看版本

确定没问题了使用go build
编译local.go文件为local.exe
中间遇到了很多依赖包的问题,一一下载他们放到相应的目录里。
终于编译成功
然后配合shadowsocks-QT5版本做后端使用,速度棒棒的。

利用Redis作为前端静态缓存加速网站访问

本想做Mysql的缓存来加速读取,但是为了练手,先用Redis做HTML页面缓存好了。

先介绍一个Credis的PHP库,官方介绍为Credis_Client, a lightweight Redis PHP standalone client and phpredis wrapper。基本操作函数都有,用起来也是很顺手的,再介绍一个名为RedisDesktopManager的跨平台redis桌面管理软件,类似navicat吧。看redis的内容和修改也是挺好用的。说起redis,前几天装了redis来练手nosql,安装过程略简单,就不写文了。
用Redis作为页面缓存无非是把不太变化的PHP页面静态化然后写入redis,然后时常检测页面更新或者设置TTL来自动更新,也可以手动更新,比如这个首页就经常采用手动更新的方式。
主要用到的就是
ob_start();
require 'index.php';
$html = ob_get_contents();
ob_end_clean();
来把本来index.php的内容静态化写入redis,这里采用Hash的数据方式用hSet(string $key, string $field, string $value)函数来写入数据库,用域名做key,用网址做field参数,value就是页面内容的html代码。虽然是一种比较简单粗暴的办法,但也是极为好用的。

当页面被访问时首先会用$redis->hexists(域名, 网址)来判断Redis内是否存在页面缓存,如果存在则直接用$redis->hget(域名, 网址);来调用内容显示出来。

如果页面不存在会做一次是否需要把该页面缓存的判断,这里会把不需要缓存的页面,例如:搜索页,404页之类的来进行一次判断。
如果该页面是需要缓存的页面,则调用上面的函数进行缓存,然后用hSet写入redis,再用$redis->expire(域名, 86400);来控制整体的刷新时间。

同时为了可以手动清除缓存,这里我进行了一次$_get['clean'],如果该值为all,则将会执行$redis->del(域名);把整个hash数据清空,即清除整个网站的页面缓存,如果该值为page,则执行$redis->hdel(域名,网址);来清空本页面的缓存。

至此,利用Redis来缓存页面内容已经全部搞定了,测试结果显示缓存之前每个页面的执行时间是0.05403 seconds,缓存之后的页面执行时间(仅相当于一次redis读取)为0.00068 seconds,为原来的百分之一,可见效果还是很明显的。

注:以上采用域名和网址作为key和field要进行md5之类的hash转换。

部署SSl证书

下午拿到了
AddTrustExternalCARoot.crt
COMODORSAAddTrustCA.crt
COMODORSADomainValidationSecureServerCA.crt
xtms_me.crt
四个证书文件,用cat命令或者文本编辑器把证书合成一个文件,命名xtms.crt,然后上传服务器。
刚开始想用http+https并存的方式,配置了80端口和443端口同时存在,然后443去反代80端口的内容,后来一想,干脆全部https算了,也显得有逼格一点,于是在nginx的配置里让80的请求全部301到https
server
{
listen 443; #监听 SSL端口
server_name xtms.me www.xtms.me;
index index.html index.htm index.php default.html default.htm default.php;
root 网站目录;
ssl on;
ssl_certificate 证书目录;
ssl_certificate_key key目录;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers “ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4”;
ssl_prefer_server_ciphers on;
#error_page 404 /404.html;
location / {
index index.html index.php;
if (-f $request_filename/index.html){
rewrite (.) $1/index.html break;
}
if (-f $request_filename/index.php){
rewrite (.
) $1/index.php;
}
if (!-f $request_filename){
rewrite (.) /index.php;
}
}
location ~ .php$ {
fastcgi_pass unix:/tmp/php-cgi.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ .
.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 30d;
}
location ~ .*.(js|css)?$
{
expires 12h;
}
access_log off;
}
server{
listen 80;
server_name xtms.me www.xtms.me;
if ($ssl_protocol = “”) {
return 301 https://$server_name$request_uri;
}
}

重启nginx生效