FIN-WAIT-1的问题一例

这是一个早应该知道的事情。但是还是被整了半天。

引子

tcp关闭时有多少个状态?

当当,别数了,应该是6个,不算CLOSED。分别是FIN-WAIT-1/FIN-WAIT-2, CLOSING, TIME_WAIT, CLOSE_WAIT, LAST_ACK。如果不能瞬间想起一个方块来,说明tcp状态不算熟。

问题

今天的故事来自今天BI同事提出的一个问题。在线上,他发现这么一个现象。在一组系统中,客户端全都FIN-WAIT-1了,但是服务器还是ESTAB。

我的第一反应很简单,这明显差了一个FIN包的距离。而且鉴于两者在同一网络中,而且重复出现。建议他首先排查中间的防火墙设备和防火墙设置。

会找上我的问题,当然没这么简单。中间没有任何防火墙或软件防火墙设定。

分析

下一步呢?有点没方向了。抓包分析。发现FIN端向ESTAB端不停的发起ACK,但是看起来和FIN没什么太大关系。

偶然,同事注意到所有出现现象的链接都有写缓冲区数据。这是一个不常见的现象。写缓冲区一般会有点数据,但是应该很快就被消费,而不会长期堆积,更不会长期维持同样的数字。这是写缓冲区满。结合刚刚的ACK,其实本质是对端停止消费数据。

这是一个TCP的边角。当读缓冲区满的时候,tcp协议栈会声明window=0。当读缓冲区恢复的时候,读方会用ack with window来重新宣告可用缓冲区。但是在tcp里,ack是不重传的。所以这个ack会丢失。因此写方有责任定期请求确认读方window,来确定整个过程不会卡死。这就是刚刚看到的不停ACK的现象。

而这里就有个非常重要的可能性——FIN包的处理方式。为此我阅读了源码。源码告诉我们,FIN包被接收到的时候,并不是即时处理的。实际上,在ESTAB状态收到的FIN,正常path下会进入tcp_data_queue。这个函数会将包堆积到队列中,并根据当前seq来处理包。主要包括三种seq,当前包,过去包,未来包。只有在以下两种情况下,fin包才会被处理:

  1. 当前收到一个fin包。
  2. 当前收到一个包,完成处理后out of order队列中有数据,因而进入tcp_ofo_queue。而队列中有fin包。

而不幸的是,当前包处理流程第一步就是判断tcp_receive_window。如果没空间了,会进入out_of_window过程。后者会快速的触发一个ack返回,然后就把包给丢了。

我猜对了开头,可是没有猜到结局。

结论

通过python的快速复现,验证了这个结论。建立一对连接,其中一个不接收任何数据,而其中一个发送足够长的数据。当读缓冲区满后,再去close,出现一端FIN-WAIT-1,一端ESTAB的现象。

因此,结论如下:

当写缓冲区满之后,收到的fin包会被丢弃,而发送端并不会重发。而只要写缓冲还有剩余空间,哪怕一个字节,都可以正常处理。

内核参数

根据文档,可能有几个内核参数与此有关。

  • net.ipv4.tcp_max_orphans
  • net.ipv4.tcp_orphan_retries

测试表明,net.ipv4.tcp_max_orphans可以抑制这个现象。当减低这个数值后,再进入FIN-WAIT-1状态的连接会自动消失。ss -natp不能看到连接。有趣的是,如果进程尚未关闭的话,可以在/proc/[pid]/fd下面看到fd仍然存在,而且还可以读出数据。

抓包表明,连接实际是被一个RST干掉的。阅读源码,在tcp_close的最后部分,可以看到tcp_too_many_orphans被调用了。如果超过限制,就会发出reset,并且关闭连接。

而tcp_orphan_retirs,根据我的理解和测试,和这事没有关系。这主要是指这么一种现象:当对端机器poweroff(而不是shutdown)的情况下,你所发出的报文会丢失。因此理所当然的,写者的写缓冲区会很快充满。此时会发起连接探测,以确定对方是不是掉线了。套在close的这个case上,就是一边是FIN-WAIT-1,另一边死不响应。需要通过多次探测来宣告对方死亡。因此,如果对方机器死机导致不响应你的FIN,才是用tcp_orphan_retirs的场合。

p2p vpn的部署方法

p2p vpn的基本概念

p2p vpn这个概念的提出,是因为openvpn在数据传输上的一个特性——虚拟链路都是从拨入端到服务器的。例如vpn网关在北美,上海电信的两个人要通讯,数据就要从北美绕一圈。这个特性在多节点打通上无疑很扯,于是催生了很多p2p vpn。他们的基本理念是——尽力从端到端,不成再绕。而且为了解决端到端,顺便得解决NAT问题——也就是带有STUN打洞。

tun模式的三层转发表

先说明一点,大部分p2p vpn都是tun模式。这也很正常,tunnel用的么。但是大家在配置openvpn的时候,不知是否注意过iroute这个指令。为什么会有iroute指令的存在?

tun模式是三层模式,相信大家都有数。也就是说,报文传递的时候只带有三层地址,openvpn也凭借三层地址来找到要转发给谁。这里和普通的网络就显示出区别了——普通网络使用ARP协议来自管理转发规则,而openvpn则是凭借内部写好的转发表。

例如vpn gateway的虚地址是192.168.100.1,节点1是100.5,节点2是100.10。那么节点2发送报文给节点1时,报文大约长这个样子。

192.168.100.10 -> 192.168.100.5

在普通网络中,第一步会查路由表,确定是eth0(虚拟网络是tun0)。然后在上面广播ARP请求,获得MAC地址。最后填写MAC地址,发送报文。但是在tun虚拟网络中,仔细看你的路由表,是不是整个虚网络都被交给了一个叫做100.4之类的奇怪gateway转发?甚至如果你没有打开client-to-client,整个虚网络只有一台可见,这台还是交给这个gateway转发的。

这是因为你到这个奇怪的IP之间还是走ARP过程,但是这个奇怪的IP收到你的报文后,就可以是纯三层过程了。你可以把这个IP视为本地openvpn的化身。openvpn会把你的报文发送到openvpn gateway,然后openvpn gateway再转发给正确的机器。也就是说,openvpn gateway必须知道某个目标IP需要转给哪个节点,物理地址多少,对吧?

作为纯虚网络,知道节点的IP很容易——毕竟是openvpn gateway管理的地址分配过程。但是作为tunnel和多地址打通,这里就有点困难了。例如节点1还有个网段是192.168.80.0/24,节点2还有个网段是192.168.60.0/24,那么如下一个报文从节点2中出去,你让openvpn gateway怎么办?

192.168.60.15 -> 192.168.80.15

你也许会说,我当然有配节点1的网关转给openvpn的拨入端。问题是,这个动作,openvpn的拨入端尚且不知,何况openvpn gateway?于是我们派生了route/iroute这两个指令。

route/iroute表示这个地址段归属于这个节点所有,区别在于route同时修改路由表,而iroute不修改路由表。配合push,可以由服务器端下发指令修改客户端路由表。

p2p vpn也有类似的问题。甚至,由于没有统一配置端,因此连每个节点的虚IP都不能很容易的得到。在配置中必须注意这点。

n2n

n2n的模式比较简单,也比较有局限性。基本分为两个端,supernode和edge。supernode类似于hub,把所有edge拉到一起。edge都是对等的。

supernode:
supernode -l port

edge:
edge -a [address] -c [name] -k [password] -l [supernode:port] -u [userid] -g [groupid]

解释一下。开一个supernode,不用做任何设定。反正supernode也不会持有edge别的信息。edge要设定supernode的ip和端口,然后提交name和password。name和password相同的,就进入同一个虚拟网络。然后自己的虚拟ip是address。最后的userid和groupid必须是数字。主要是因为开tun需要root权限,因此可以在获得完权限后退化成普通用户,以防权限太高。

这里好玩的就是,理论上address可以天南海北,完全不用管路由怎么走。甚至172.16.0.1可以和10.0.0.1通讯(我没实际确认)。因为大家都是看彼此的IP是否经过注册,而不是计算路由表。

但是这里就有个缺点,我看到man文档中只提到address,没提到可以提交一个网段。所以无论我怎么设定,使用三层方案做隧道的时候,n2n是转不过去的。因为他不知道这个网段归哪个节点管。

所以,n2n的p2p模式很便利,但是没法打tunnel(至少我不知道怎么玩)。

tinc

tinc和n2n一样,也是一种p2p vpn。不过好处在于,tinc允许你在一个节点上配置多个网络,因此可以打tunnel。

在配置之前,我先约定两个词。“配置名”和“节点名”。一个配置是接入同一个网络的多个节点,还有他们如何拓扑。节点名就是一个节点的名字。所以,和配置有关的有以下两个。

/etc/tinc/nets.boot:
这里写上想自动启用的配置的名字

/etc/tinc/[configname]:
配置的根路径,以下路径全是相对路径。

配置

下面就是某个配置中的一堆文件。注意这些配置都是配置自己节点的属性信息。

tinc.conf:

Name = [nodename]
Device = /dev/net/tun
ConnectTo = xxx

tinc-up:

ip link set $INTERFACE up
ip addr add [address/32] dev $INTERFACE
ip route add [network/mask] dev $INTERFACE

tinc-down:

ip route del [network/mask] dev $INTERFACE
ip addr del [address/32] dev $INTERFACE
ip link set $INTERFACE down

其中tinc-up会在tinc启动时执行,tinc-down会在tinc关闭时执行,所以两者必须是+x权限。有趣的是,tinc并不管理你的整个启动过程,tinc-up和tinc-down甚至要亲自来管理接口的up和down过程。但是这也给你足够的额权力去执行任何复杂的路由逻辑指令。

hosts

除去上部分外,还有一个很大的hosts目录。里面放满了节点群信息。

hosts/[nodename]:

Address = external ip address
Port = external port
Subnet = network/mask

其中Address和Port如果没有可以不写。Subnet可以彼此重叠,路由决定逻辑是从小子网到大子网(普通路由表逻辑)

初始化

在配置hosts的时候,我建议每个节点就配置自己的hosts/[nodename]文件,然后执行下面的指令:

tincd -n [name] -K

这个指令会在/etc/tinc/[configname]/rsa_key.priv下生成一个私钥,并且把公钥添加到hosts/[nodename]文件中。最后,在多个node间把彼此的hosts/下的文件一复制,让每个node都拥有其他node的hosts文件,整个网络就完成配置了。

如果你的配置名已经写入nets.boot文件的话,可以用/etc/init.d/tinc restart来重启启动整个网络。

openvpn的几种基本模式

vpn的原始模式

vpn的最简模型,相当于在两台机器上插一块虚拟网卡,然后中间连一根虚拟网线连通。因此vpn才得名vpn(virtual private network)。

其复杂之处在于,这块虚拟网卡如何配置网络,和别的网卡是什么关系。再加上多个节点间如何通讯。种种都够新手喝一壶。

虽然openvpn在科学上网上是废了,但是在不出国的网络上用来保护通讯,还是非常好用的。

tap模式

tap模式的特点是二层打通。典型场景是从外部打一条隧道到本地网络。进来的机器就像本地的机器一样参与通讯,你分毫看不出这些机器是在远程。

优点:

  • 配置简单。
  • 不需要在所有机器上配置或者动网关。

缺点:

  • tap在部分设备上不支持(例如移动设备)。
  • wlan加入网桥后不一定可以工作。
  • 广播包会散发到虚拟网络中,可能极大消耗流量。

特别解说一下wlan。部分AP对一个客户只接受一个MAC地址,因此无法做网桥。这应该是wifi网络的常规问题了。解决方法是换AP,或者做mac-nat。

操作方法:

你需要先在当前网络中,为vpn预留一些地址。这些地址应该足够拨入用户使用,不应和dhcp撞车,不应有其他人使用。

而后,建立一个br,将当前工作的eth迁移过去。(具体细节就不说了,每个系统小有差别)再建立一个tap vpn,在启动脚本中指定加入这个br。

example

假定内网地址为172.19.0.0/24,其中保留172.19.0.16-172.19.0.31供vpn使用。

服务器配置:

port [port num]
proto udp ; 参考我上一篇vpn不要走tcp协议
dev tap
ca ca.crt
cert server.crt
key server.key
server-bridge 172.19.0.16 255.255.255.0 172.19.0.17 172.19.0.31
; 或者可以采用这句
; server 172.19.0.16 255.255.255.240
; 注意掩码实际上等于/28,做掩码运算后,这段地址和上面的保留地址重合
script-security 2
up vpn-start ; 建议使用绝对路径,避免版本坑
down vpn-stop

vpn-start:

brctl add br0 $dev

vpn-stop:

brctl del br0 $dev

客户端配置:

client
dev tap
proto udp
remote [server ip] [port num]
ca ca.crt
cert client.crt
key client.key

测试:

直接ping任何一台机器,通了就是通了。没通在网关上抓包,看看断哪了。注意关闭防火墙。

PS:

上面的配置我配过,但是没有经过实际测试,所以可能有问题。有问题请联系我,我马上改。

dh和tls-auth可以配,个人每次都是配的。但是懒的话也可以不搞。

user nobody和group nobody强烈建议配,注意debian上是nogroup。这个配置可以将openvpn的执行权搞低,如果openssl再出什么执行任意代码漏洞,那么问题就不是立刻致命。

注意使用了user和group后,要配置persist-key和persist-tun,避免出错。

tun模式

tun模式的特征是三层打通,你可以当作没有二层数据。因此从拨入用户那里去问内网IP的mac是多少,根本没人理你。你必须将包发到vpn网关上,交由网关转交。目标服务器还得知道回这个数据的时候,网关是vpn网关,而不是默认网关。当然,有的时候两者其实是一个,例如vpn网关在默认网关上。或者不修改每台机器配置,直接在网关上做第二跳指向。

典型场景是多个网段打通(所以才叫tun——tunnel)。

优点:

  • 基本在所有设备上都支持。
  • 可以透过wlan。
  • 不会在所有网段上广播报文(广播风暴不过网关,这应该是常识了)。

缺点:

  • 需要修改每台机器,或者网关。

操作方法:

非常灵活,几乎无法总结。简单说说从外网拨入内网,打通两者间互访的配法吧。

配置一个tun vpn,连接到vpn网关上。配置中下发内网网段,走vpn。内网下发vpn路由,指向vpn网关。

example

假定内网地址为172.19.0.0/24,其中vpn网关在内网的地址为172.19.0.100。虚拟网络为172.19.1.0/24,其中vpn网管在虚拟网络的地址为172.19.0.1(默认)。

服务器配置:

port [port num]
proto udp
dev tun
ca ca.crt
cert server.crt
key server.key
server 172.19.1.0 255.255.255.0
push “route 172.19.0.0 255.255.255.0″

在服务器上一定要打开:

sysctl -w ‘net.ipv4.ip_forward=1′

在所有服务器上执行/在网关上执行:

ip route add 172.19.1.0/24 via 172.19.0.100

route add -net 172.19.1.0 netmask 255.255.255.0 gateway 172.19.0.100

客户端配置:

client
dev tun
proto udp
remote [server ip] [port num]
ca ca.crt
cert client.crt
key client.key

测试:

在拨入设备上,使用mtr 172.19.0.1(内网网关),来查看是否通过172.19.1.1和172.19.0.1。如果都到了,说明整个配置成功。否则看是否到vpn网关,再在网关上抓包。

多节点通过虚拟骨干网打通

这个模式在很多地方很有用,例如多个办公室互通,多个机房互通,等等。当然,这也是有前提的,这些节点的网段不能出现互相重叠,不然路由表这一段该指给谁?

另一点细节在于,最好每个节点都在网关上做。不然如上面所说,在每台服务器上做一次配置,非常复杂。

方案基本和tun方案一致,但是在不同的客户端上,将其他节点的网段全部上行到vpn骨干网,分别交由这个内网对应的网关去路由。做这点在客户端配置上做可能会有麻烦,可以考虑使用ccd(client-config-dir ccd)。这样可以在服务器上设定,在客户端拨入的时候下发部分网络配置。

更详细可以看这篇文档

vpn不要走tcp协议

和大家唠叨一件小事。

vpn不要走tcp协议。

我原本以为这是个常识。因为当网络发生丢包时,vpn的那个tcp会等到超时,然后重发。但是vpn里面封装的tcp链路多半也超时了,也要重发。所以一旦发生丢包,整个链路上就会突然一下子变拥堵了。

那么能不能调整链路超时重发机制呢?这个比较困难。链路的超时重发是靠RTT(round-trip time)来工作的,因此VPN的RTT一定小于里面所封装tcp的RTT。如果你的vpn工作在一根很好的链路上,而tcp链路的对端要通过一个延迟非常高的网络。那么vpn的重发对tcp链路的影响并不大。但是我看到的大部分情况下,vpn是穿越互联网(而且大多是跨国或者跨洲网络)的,而vpn的双端都落在内部网络里。因此vpn构成延迟主体。这时候,vpn的RTT和里面所封装tcp的RTT几乎相差无几,两者会几乎同时超时。

什么时候会发生丢包呢?最常见的理由是带宽跑满。当路由器来不及处理数据的时候,就只有丢包了。例如某出国网络丢包三成,这就是拥塞爆了。当然我们也不否认其他理由,例如无线,天生就是有丢包的。即便没有上述理由,有的时候就是运气不好,无理由的发生一下丢包,也完全不需要奇怪。

所以,在大部分网络上,丢包是常态。而丢包后,内外两层tcp同时超时会引起严重的重传问题。所以tcp协议不是特别适合做vpn。

结果某vpn居然被配置到了tcp模式,而且还不能改。。。郁闷啊。。。

Charlie and Dave

公司希望弄一套双授权的安全系统,老大提供了一套算法,求大家review。如果这个方案确实可行,那么我们会做完然后开源出来给大家用。

Author and License

Author: 韩拓
保留所有权利
All Rights Reserved
以下内容不以cc-by-sa3.0发布。(因为根本不是我的创作)

场景

  1. Alice希望登录到Bob上执行操作。
  2. 两者的基本控制协议为ssh。

假定

  1. 攻击者名叫Mallory。
  2. 如果Alice的私钥泄漏,管理者必须有权停止Alice到Bob的访问而不需要更换所有Bob的公钥。
  3. 除去Alice和Bob外,参与通讯过程的所有机器(即下文中的Charlie和Dave)中可能随机被攻破一台。
  4. 服务都在内网,但是如果网关和被攻破的机器是同类系统,Mallory即可具有内网监听和伪造数据报文的权限。
  5. Alice不会利用获得的Bob的权限故意执行危害性指令(但是可能被诱骗)。
  6. Alice和Bob不会被攻破。

方案

假定有两台机器,Charlie和Dave,Dave和网关不得是同类系统。根据假定4,两台机器不会同时被攻破。

  1. Alice通过SSL和Dave建立连接,上报自己的用户名,需要访问的设备和帐号,并提交一个临时生成的ssh pubkey(username, account, host, pubkey)。
  2. Dave根据预先设的IP-username-sslkey验证用户身份为Alice,并且根据ACL确认其具有访问权限。
  3. 如果通过验证,那么Dave用自己的key,通过SSL联系Bob上的某个程序,将Alice的pubkey提交到Bob的合适帐号上(account, pubkey)。
  4. Bob通过sslkey验证提交者确系Dave,将pubkey临时加入account中。
  5. Bob完成此事后,通过Dave向Alice返回成功。
  6. Alice通过SSL和Charlie联系,上报自己的(username, account, host)。
  7. Charlie根据预设的IP-username-sslkey验证用户身份为Alice,并且根据ACL确认其具有访问权限。
  8. 如果通过验证,那么Charlie用自己的key,通过SSL联系Bob上某个程序,为Alice开通到Bob的22端口的tcp盲转发。
  9. Alice利用开启的tcp通道,和自己的临时ssh private key验证登录Bob。
  10. 在Alice连接Bob上的程序后,删除alice留在Bob上的临时pubkey。

验证

假定Charlie被攻破。

  1. 方案1-5没有影响。
  2. Charlie拥有能够在任意一台机器上开启盲转发的权限。
  3. 但是Charlie并不能影响Dave去添加pubkey。

假定Dave被攻破。

  1. Dave拥有在任意一台机器上添加pubkey的权限。
  2. 但是Dave并不具有打开到任意一台机器ssh端口的权限。