你好,我是胜辉。今天我们接着上节课的学习和思考,继续来探讨如何定位防火墙问题。

在上节课里,我们用两侧抓包并对比分析的方法,首先定位到了引发长耗时的数据包,然后对比两侧抓包文件,定位到了包乱序的现象及其原因。最后,我们综合这些有利证据,跟安全部门沟通后,拿到了真正的根因,并彻底解决了问题。

在那个案例中,大量的分析技术是位于传输层,而且要结合应用层(超时的问题)做综合分析。总的来说,难度还是不小的。而且还有一个不可回避的问题:包乱序难道只有防火墙才会引发吗?

其实不是的。包乱序是一种相对来说比较普遍的现象,除了防火墙,还有网络设备、主机本身都可能引起乱序。所以,单纯根据包乱序就断定是防火墙在中间捣鬼,就有点以偏概全了。

那么有没有一种方法,不需要借助那么多的传输层的复杂知识,就可以让我们更加明确地判断出,问题是在防火墙呢?

这节课,我就给你介绍这种方法,即聚焦在网络层的精确打击。这是一种更加直接、更加高效的办法。

你可能又会疑惑了:难道说我们上节课学的东西,其实是多余的吗?那倒不是。这两节讲防火墙的课程,各自有不同的侧重点和不同的适用场景。这次我们介绍的方法,在上节课的案例里就不会起到作用;反过来也是如此。技术上没有“一招鲜”,只是这次课讲的内容相对上节课来说,确实更加直接,这也是它的一大特点。

好了,废话不多说,咱们来看案例吧。

案例1:Web站点访问被reset

几年前我在公有云公司就职,当时公司乔迁入住了新的办公大楼,没过半天,不少同事陆续报告,他们无法访问某个内部Web工具。而且报告的人都有个共同点:他们使用的是二楼的有线网络。

我们这个内部工具是以Web网站形式运行的,报错页面就是类似下面这种(不同浏览器会有不同的错误页面):

补充:不要误会,这可不是我们极客时间App的图片加载的报错,这本身就是当时的报错截图。

我们让二楼同事连接到有线网络并做了抓包,然后传给我们做分析。因为抓包的同事没有做过滤,所以抓上来的包比较大,包含了各种其他无关的数据包。不过没关系,我们可以按下面的顺序来。

在这个案例里面,我们就可以把Web站点的IP作为过滤条件。Web站点的IP是253.61.239.103,所以我们的过滤器就是:

ip.addr == 253.61.239.103

补充:因为信息安全的原因,这里的报文信息我做过脱敏了(也就是修改或者删除了敏感信息),所以IP和载荷等都已经不是原始信息了。

我们得到下面的过滤结果:

从上图中已经能看到RST报文了。当然,在第4讲里,我们也学习了如何在Wireshark里面,用过滤器来找到TCP RST报文。那么这个案例里面,我们同样可以这么做。在过滤输入框里输入:

ip.addr eq 253.61.239.103 and tcp.flags.reset eq 1

我们觉得某个RST包比较可疑,那么还是用Follow -> TCP Stream的方法,找到对应的TCP流:

在我们点击TCP Stream的时候,Wireshark会弹出一个窗口,显示解读好的应用层信息。如果是HTTP明文(非HTTPS加密),那就可以直接看到里面的内容了。

回到主窗口,此时过滤生效,所以只有属于这个TCP流的包才被显示,其他无关的数据包都已经被隐藏了。

可见,TCP三次握手后,客户端发起了一个HTTP请求,即GET /overview HTTP/1.1,但服务端回复的是TCP RST,怪不得访问失败了。我画了下面这张示意图供你参考:

握手能成功,看起来网络连通性没有问题,然后发送HTTP请求后收到RST,感觉服务端的嫌疑很大。

我们找到了负责服务端的同事,不过,他们说完全没有做RST这种设置,而且反问:“不是说Wi-Fi就都正常吗?二楼以外的其他楼层也都可以,不是可以排除服务端原因了吗?”

客户端没问题,服务端也没问题,是不是就要怀疑是防火墙了?但还是那个困境:我们并没有防护墙的权限,无法证实这件事。

那我们还是回到网络本身来查看,静下心来思考一下。

其实,虽然抓包分析是一个很好的排错方法,但是一般在中间设备(交换机、防火墙等)上不方便抓包,所以主要途径还是在客户端或者服务端的抓包文件里寻找端倪。当然,我们也可以争取条件到服务器上抓个包,然后再结合两侧抓包文件,进行对比分析,会更容易得出结论,这也是上节课我介绍过的方法。

不过,有没有办法只根据客户端的抓包,就能确定这次问题的根因呢?这个比较悬。但是,对于当前这种场景,如果能掌握到一个关键信息,我们就可以直接得出结论。

它就是TTL(Time To Live)。你可能会觉得,这个知识点简直太简单了,真的能解决我们的问题吗?且听我慢慢说。

TTL详解

TTL是IP包(网络层)的一个属性,字面上就差不多是生命长度的意思,每一个三层设备都会把路过的IP包的TTL值减去1。而IP包的归宿,无非以下几种:

RFC791中规定了TTL值为8位,所以取值范围是0~255。

因为TTL是从出发后就开始递减的,那么必然,网络上我们能抓到的包,它的当前TTL一定比出发时的值要小。而且,我们可能也早就知道,TTL从初始值到当前值的差值,就是经过的三层设备的数量。

不同的操作系统其初始TTL值不同,一般来说Windows是128,Linux是64。由此,我们就可以做一些快速的判断了。比如我自己测试ping www.baidu.com,收到的TTL是52,如下图:

这个百度服务端大概率是Linux类系统,因为用Linux类系统的TTL 64减去52,得到12。这意味着这个回包在公网上经过了12跳的路由设备(三层设备),这个数量是符合常识的。

假如百度服务端是Windows,那么Windows类系统一般TTL为128,减去52,得到76。那就意味着这个回包居然要途经76个三层设备,这显然就违背常识了,所以反证这个百度服务端不会是Windows。这就是我想说的第一点:TTL的值是反映了网络路径跳数的,也可以通过它间接推导出对端的OS类型。

补充:现今绝大部分网站的接入环节都是*nix类系统,所以这里只是单纯关于TTL这个知识点的技术讨论,不涉及“哪种OS是服务端主流”这种有争议的话题。

接下来第二点更加关键了。同样两个通信方之间的数据交互,其数据包在公网上容易出现路径不同的情况。就好比你每天开车上班,一般也有不止一条线路,无论走哪一条,只要能到公司就可以了。那么你和百度之间的路径,上午是12跳,下午变成13跳,或者11跳,也都属于正常。

但是内网不同,因为内网路径相对稳定,一般不会变化。如果出现TTL值的波动,特别是当这个波动值比较大(比如超过2)的时候,那几乎就说明这些包的不同寻常了。这就是我想说的第二点:内网同一个连接中的报文,其TTL值一般不会变化。

回到我们这次的案例。我们就按这个思路,来排查下这仅有的5个数据包:

因为TTL并不是默认的展示列,所以我们需要选中报文,然后到IP详情部分去查看,像下面这样:

不过,这样看几个报文的TTL还行,再多点就十分困难了。怎么做才能更有效率呢?答案是借助Wireshark的自定义列。我们可以这样做:

你也能很清楚地看到,同样的服务端,在三次握手中(SYN+ACK报文)的TTL是59,在导致连接中断的RST包里却变成了64!显然,这个RST包并不是跟我们握手的那个服务端发出的,否则TTL值就不会变化。

发出这个包的会是谁呢?其实,一般就是防火墙设备。由于防火墙也遵循IP协议,而这里的TTL值是64,这就说明这个防火墙跟客户端之间没有别的三层环节,或者说是三层直连的。

我们可以用一张简单的图来概括这个案子:

这样,我们底气大增!根据我们提供的信息,负责防火墙的同事就去复查了下,果然有发现:防火墙上对二楼有线网络有一条可疑的策略,跟其他线路不同。这条策略的出发点是:每个网络协议规定了协议数据格式以及标准端口号,所以协议数据跟端口号不匹配的话,就可以认为是“有害”流量。因为HTTP协议标准端口是TCP 80,但是我们这个Web站点是3001端口的,被防火墙认为不一致,所以就拒掉了。

我们来看一下当时的防火墙的配置:

这里的application-default就是说,端口需要跟协议匹配。要不然就会被禁止,也就是回复RST给客户端,终止这条连接。这个防火墙策略被修正后,问题也立刻被解决了。

案例2:访问LDAPS服务报connection reset by peer

案例1中,我们在有权限的客户端抓了包。那么,如果两端都做了抓包,那么是否可以看到更多的风景呢?

这是我们最近遇到的一个例子。eBay内部有一个用户报告,他的服务到LDAPS服务失败了(这个LDAPS在我们维护的Windows AD域控上),遇到了connection reset by peer的报错。LDAPS就是LDAP的TLS版本,它监听的端口是636。正好两端的操作权限我们都有,就可以做两侧抓包了。

我们先看一下客户端这边的抓包文件,确实有看到服务端返回了TCP RST包:

图中可见,TCP三次握手正常完成了,随后的TLS握手Client Hello包也发送出来了,但是服务端回复的是RST包,这也就是引起客户端显示connection reset by peer报错的原因。

接着,我们再来看下服务端抓包文件的情况:

显然,这里也有一个从客户端回复的TCP RST。

不过,再仔细对比服务端抓包和客户端抓包,是不是有些异样呢?为什么客户端抓包里有TLS Client Hello报文,但在服务端抓包里却没有呢?是因为Client Hello报文延迟了吗?

如果是延迟,那么这个Hello报文还是会出现在服务端抓包文件里,比如出现在RST报文的后面。但事实上,服务端抓包里并没有这个Hello报文的存在。所以这就反证了:这个Hello不是延迟,而是确实就没到服务端

那么问题来了:

这个现象足以让我们怀疑:两者之间存在着一个“看不见的东西”,这个东西把Client Hello报文给“吃了”。

我们通过案例1的学习,已经了解到TTL是判断“是否有防火墙”的非常直接方便的方法。那么对于当前这个案例,我们也一样检查一下TTL。

从服务端视角来看,它收到的报文只有三个:SYN包、ACK包、最后的RST包。我们选择SYN和RST,来对比看下它们的TTL是多少。下图是SYN包:

下图是RST包:

显然,跟之前的案例类似,这里的TTL也发生了明显的变化。你应该也明白了,这两个包并不是同一个设备发出的

因为这个案例里,客户端我们也抓包了,所以可以来看看客户端抓包里,是否也有TTL不同的现象呢?

客户端收到的SYN+ACK包的TTL是110:

客户端收到的RST包的TTL是54:

终于,结合两侧的抓包,我们就可以把这个拼图给拼完整了,而Client Hello报文丢失之谜,也将揭晓。更新一下前面的示意图,会变成下面这样。显然,Client Hello报文就是被防火墙丢弃了。

可见,防火墙的拦截行为可能出现在多个方向上(面对客户端时代表服务端,面对服务端时代表客户端),毕竟报文都要经过它,它如果想乱来,通信两端还真的无法控制它。从上图来看,防火墙两边“截胡”,两边拒绝,两边还都只好乖乖地听话,结束了连接。你都不能说它“没有武德”,因为整个过程都是完全遵照了TCP规范的,防火墙做得不可谓不周到。

不过百密一疏,它偏偏在TTL上露出了马脚,被我们抓了个现行。

你看,小小一个TTL,在这里能起到如此关键的作用,真是不可小视。connection reset by peer的问题,我们在第4讲专门讲TCP挥手的时候就仔细分析过了。不过,你还记得为什么那次的情况跟这次不同吗?对,那次的场景里,RST真的是对端发出来的,并不是防火墙从中作梗,所以TTL也都正常。

那么,今天的课程给你介绍的就是connection reset by peer的另外一种可能。你可以仿照我这里介绍的排查过程,然后拿证据去跟网络安全团队沟通就行了。相信此时的你,说服力远远超过单纯抱怨而没有实质证据的你。

如何应对?

防火墙简单插入一个RST,就可以终止连接,确实令人无奈。当然,这对网络安全来说,也许就是一种特性(Feature)了。正所谓“他人的美味,可能是你的毒药”。

我们通过上面介绍的排查过程,确认了防火墙的存在。那么,你可能会问了:接下来我们有什么办法可以规避这个问题呢?

这个问题的核心,就是既然我们可以准确地定义什么是防火墙插入的RST报文,那是否意味着我们就可以避开它了?比如你有没有想到:

干脆直接丢弃这个报文!

这个想法很大胆,好像也挺合理。但稍一细想觉得有几个现实问题得先解决:

对于第一个问题,我想如果你熟悉Linux网络的话,可能第一时间就会想到 iptables 了。是的,我们就是可以利用它来达到“丢弃RST报文”的目的。当然,你做这个事情的时候必须十分小心,毕竟RST也是TCP协议里定义的标准,无论你是否喜欢它,RST在很多场景里是必需的,一股脑地丢弃一定会引来难以预料的麻烦。

所以,我们可以对这条iptables规则设定精确的限定条件,使得它既能帮助我们丢弃“有害”的RST报文,同时也不影响到其他正常连接的交互。

因为我们很难有条件拥有一台真实的防火墙设备来帮助我们做这个实验,所以只能找一个办法来模拟防火墙。怎么做呢?我们还是借助iptables。

这里,真是要感谢创造了iptables的内核子模块——Netfilter的内核开发团队了,有了Netfilter,基于它的iptables等工具在我们日常工作中起到了很大的作用。

对于第二个问题,其实不用担心,在报文进来的方向,报文会经过这样的处理流程:

PREROUTING -> INPUT -> 本地处理 -> OUTPUT -> POSTROUTING

所以,当我们在INPUT链上创建了丢弃RST报文的规则,那么当这个RST报文进入到机器时,会被这条规则丢弃,进不到本地处理,也就是不会有被内核协议栈处理的机会!那么,我们的TCP连接就不会被“毒害”了。还好,我们还有这张王牌可以打,只要对系统和网络足够熟悉,总还是有一线生机。

动手实践

现在“理都懂”了,让我们来动手实操一下。我们需要搭建这么一个测试环境:

实验1:telnet第三方站点

在1上,直接telnet www.baidu.com 443,可以成功。

然后我们需要配置一下,让1访问www.baidu.com的流量强制经过2,这样后续我们就可以让2来操控1和baidu之间的连接了。接下来步骤稍多,感谢你的耐心。

在虚拟机1上,我们需要完成这么几件事。

创建隧道,隧道另一头就是虚拟机2,我们将在那里模拟一个“防火墙”。在上节课里,我们了解了ipip隧道,这里的GRE隧道也是类似的工作原理。

ip tun add tun0 mode gre remote 172.17.158.46 local 172.17.158.48 ttl 64
ip link set tun0 up
ip addr add 100.64.0.1 peer 100.64.0.2 dev tun0

添加路由项,使得本地去往第三方站点的流量,都走这条路由,也就是通过隧道到达虚拟机2,然后2来转发报文。

ip route add 110.242.68.0/24 via 100.64.0.2 dev tun0

当然了,虚拟机2上面也需要做对等的隧道配置:

ip tun add tun0 mode gre remote 172.17.158.48 local 172.17.158.46 ttl 64
ip link set tun0 up
ip addr add 100.64.0.2 peer 100.64.0.1 dev tun0

虚拟机1把报文发到虚拟机2,但是如果后者不做配置,默认是会丢弃这些报文的,所以还需要在2上开启ip_forward:

sysctl net.ipv4.ip_forward=1

我们在2上运行tcpdump port 443,然后1上运行telnet www.baidu.com 443。在2的tcpdump窗口里,已经可以看到从1过来的流量了!

root@server$tcpdump port 443
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
16:53:36.054124 IP 100.64.0.1.34396 > 110.242.68.3.https: Flags [S], seq 3364415625, win 64620, options [mss 1436,sackOK,TS val 2049305210 ecr 0,nop,wscale 7], length 0
16:53:37.084514 IP 100.64.0.1.34396 > 110.242.68.3.https: Flags [S], seq 3364415625, win 64620, options [mss 1436,sackOK,TS val 2049306241 ecr 0,nop,wscale 7], length 0
16:53:39.100482 IP 100.64.0.1.34396 > 110.242.68.3.https: Flags [S], seq 3364415625, win 64620, options [mss 1436,sackOK,TS val 2049308257 ecr 0,nop,wscale 7], length 0

咦?抓包里只有SYN包而没有SYN+ACK,1这头的telnet也挂起,没响应了,这是怎么回事?

原来,我们还需要设置一下NAT,要不然出去的报文源IP是100.64.0.1,回包也会回这个地址,显然回不到2了。

所以还需要在2上启用SNAT:

iptables -t nat -A POSTROUTING -d 110.242.68.0/24 -j MASQUERADE

我们再试试在1上发起telnet www.baidu.com 443,果然成功了。

2上的tcpdump也抓取到了正常连接的报文(这里就不贴了)。

实验2:插入RST报文,连接失败

现在,我们需要在2上配置一个“插入RST报文”的动作,这样就可以模拟“防火墙阻隔TCP连接”的效果了。

我们可以在2上运行这条iptables命令:

iptables -I FORWARD -p tcp -m tcp --tcp-flags SYN SYN -j REJECT --reject-with tcp-reset

有了这条命令,2就用TCP RST拒绝了转发链(也就是命令中的FORWARD链)上的SYN报文。1上的telnet立刻收到了拒绝:

2上的tcpdump抓包窗口里也看到了握手和拒绝的报文:

root@server$tcpdump -i any port 443
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
17:02:16.623480 IP 100.64.0.1.34428 > 110.242.68.3.https: Flags [S], seq 3314573698, win 64620, options [mss 1436,sackOK,TS val 2049825780 ecr 0,nop,wscale 7], length 0
17:02:16.623518 IP 110.242.68.3.https > 100.64.0.1.34428: Flags [R.], seq 0, ack 3314573699, win 0, length 0

可见,这个RST实实在在地起到了类似防火墙的作用,让你的连接无法建立。你看,其实防火墙也没那么神秘,我们也可以实现。可以小小地鼓励一下你自己!

实验3:丢弃RST报文,连接成功

这套实验的核心目标是实现对RST干扰报文的规避,也就是丢弃这类报文。让我们继续实验,在1上添加这么一条iptables规则:

iptables -I INPUT -s 110.242.68.0/24 -p tcp --sport 443 -m tcp --tcp-flags RST RST -m ttl --ttl-eq 64 -j DROP

有了这条规则,我们就对符合条件的TCP报文进行了丢弃,这个条件就是“来自110.242.68.0/24网段的TCP源端口为443的,带RST标志位的,TTL等于64的报文”。

这里的TTL条件就是关键了。在实际场景下,你就可以根据防火墙插入的RST报文的TTL的实际特征,写一条精确匹配的规则,把它跟正常报文区分开,进行精准的丢弃。

我们还是在1上telnet,然后发现这次不再被reset,而是挂起了。在1的tcpdump中,也看到SYN发出了,对方也回复了RST,但是我们并没有被真的被reset。这里,正是这条丢弃RST报文的iptables规则起到了效果。

root@client2:~# tcpdump -i any host 110.242.68.4 and port 443
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked v1), capture size 262144 bytes
17:27:50.748194 IP client2.53438 > 110.242.68.4.https: Flags [S], seq 1164905016, win 64620, options [mss 1436,sackOK,TS val 2201853747 ecr 0,nop,wscale 7], length 0
17:27:50.748433 IP 110.242.68.4.https > client2.53438: Flags [R.], seq 0, ack 1164905017, win 0, length 0

你可能这里稍有疑惑:1持续发SYN,2持续回复RST,那我们这个连接不是依然没建立起来吗?

其实,一般防火墙工作模式跟实验里的稍有不同。这次两个案例中的防火墙都运行在应用层模式,也就是会先让TCP连接建立,然后检查后续的报文,发现不符合安全策略的时候,就插入一个RST报文,终止这条连接。这种动作一般是一次性的。

而在这个实验中,我们只要让iptables的REJECT只生效一两次(比如用下面的复合命令),TCP连接就只是在最初几秒被短暂干扰,之后就依然能成功建立。

iptables -I FORWARD -p tcp -m tcp --tcp-flags SYN SYN -j REJECT --reject-with tcp-reset;sleep 5;iptables -D FORWARD -p tcp -m tcp --tcp-flags SYN SYN -j REJECT --reject-with tcp-reset

在实际场景中,只要设置前面提到的iptables丢弃特定RST报文的规则,就还有很大的几率能让这条连接继续保持下去,应用也运行下去。防火墙居然对你无效了,你除了长舒一口气,会不会心里也冒出“终于翻身当主人”的感觉?

拓展思考

那么,除了这种丢弃有害RST的办法,还有没有别的办法呢?

就上面探讨的丢弃RST的方法来说,这是一个“应对式”的策略,也就是有人要“害我”,那我把“毒药”给扔了。但仍然是“被动”的方法。如果思考得更进一步,我们有没有办法,使得别人都没有机会来害我呢?就是你连“下毒”的机会也没有?这就是“主动”的策略了。

我个人看法是,可以到网络层(IP层)去寻找机会。利用IPSec(比如IPv6默认启用了IPsec),我们就获得了在第三层加密的能力。因为就连IP报文本身都是加密的,那么即使防火墙要插入报文,因为它不具备密钥,所以这个报文会被接收端认为非法而被丢弃。这样就有希望真正摆脱防火墙对传输层(TCP/UDP)的这种控制。

小结

上节课我们采用的方法,是通过两端抓包后进行网络包的对比分析,排查定位到防火墙的存在,这种方法对于丢包、乱序等场景特别有用。而这节课的方法呢,是通过分析TTL值的变化,快速定位到防火墙的存在。这种方法,对于连接被重置(RST)的场景,十分有效。

那么在学完这节课之后,你需要记住以下几个关键要点:

另外,在这节课的最后,我们也通过一系列实验,再一次深入理解了RST报文的作用,以及可能的规避方法。在这个过程中,我们学习了:

iptables -I INPUT -s 110.242.68.0/24 -p tcp --sport 443 -m tcp --tcp-flags RST RST -m ttl --ttl-eq 64 -j DROP
iptables -t nat -A POSTROUTING -d 110.242.68.0/24 -j MASQUERADE
sysctl net.ipv4.ip_forward=1

思考题

这节课,我给你介绍了利用防火墙的TTL跟原先的正常报文的TTL不一致的特点,从而识别出防火墙的方法。

那么,假设有一天,防火墙公司把这个特性也完善了,我们再也不能仅仅凭TTL的突变而发现防火墙了,你觉得还有什么办法可以识别出防火墙吗?这是一个开放式的话题,欢迎你把答案写在留言区,与同学们一同分享。

附录

抓包文件:https://gitee.com/steelvictor/network-analysis/tree/master/06

评论