Florian Westphal 发布的文章

这是有关 网络地址转换 network address translation (NAT)的系列文章中的第二篇。之前的第一篇文章介绍了 如何使用 iptables/nftables 的报文跟踪功能 来定位 NAT 相关的连接问题。作为第二部分,本文介绍 conntrack 命令,它允许你查看和修改被跟踪的连接。

引言

通过 iptables 或 nftables 配置的 NAT 建立在 netfilters 连接跟踪子系统之上。conntrack 命令作为 “conntrack-tools” 软件包的一部分,用于查看和更改连接状态表。

连接跟踪状态表

连接跟踪子系统会跟踪它看到的所有报文流。运行 sudo conntrack -L 可查看其内容:

tcp 6 43184 ESTABLISHED src=192.168.2.5 dst=10.25.39.80 sport=5646 dport=443 src=10.25.39.80 dst=192.168.2.5 sport=443 dport=5646 [ASSURED] mark=0 use=1
tcp 6 26 SYN_SENT src=192.168.2.5 dst=192.168.2.10 sport=35684 dport=443 [UNREPLIED] src=192.168.2.10 dst=192.168.2.5 sport=443 dport=35684 mark=0 use=1
udp 17 29 src=192.168.8.1 dst=239.255.255.250 sport=48169 dport=1900 [UNREPLIED] src=239.255.255.250 dst=192.168.8.1 sport=1900 dport=48169 mark=0 use=1

上述显示结果中,每行表示一个连接跟踪项。你可能会注意到,每行相同的地址和端口号会出现两次,而且第二次出现的源地址/端口对和目标地址/端口对会与第一次正好相反!这是因为每个连接跟踪项会先后两次被插入连接状态表。第一个四元组(源地址、目标地址、源端口、目标端口)记录的是原始方向的连接信息,即发送者发送报文的方向。而第二个四元组则记录的是连接跟踪子系统期望收到的对端回复报文的连接信息。这解决了两个问题:

  1. 如果报文匹配到一个 NAT 规则,例如 IP 地址伪装,相应的映射信息会记录在链接跟踪项的回复方向部分,并自动应用于同一条流的所有后续报文。
  2. 即使一条流经过了地址或端口的转换,也可以成功在连接状态表中查找到回复报文的四元组信息。

原始方向的(第一个显示的)四元组信息永远不会改变:它就是发送者发送的连接信息。NAT 操作只会修改回复方向(第二个)四元组,因为这是接受者看到的连接信息。修改第一个四元组没有意义:netfilter 无法控制发起者的连接状态,它只能在收到/转发报文时对其施加影响。当一个报文未映射到现有连接表项时,连接跟踪可以为其新建一个表项。对于 UDP 报文,该操作会自动进行。对于 TCP 报文,连接跟踪可以配置为只有 TCP 报文设置了 SYN 标志位 才新建表项。默认情况下,连接跟踪会允许从流的中间报文开始创建,这是为了避免对启用连接跟踪之前就存在的流处理出现问题。

连接跟踪状态表和 NAT

如上一节所述,回复方向的四元组包含 NAT 信息。你可以通过命令过滤输出经过源地址 NAT 或目标地址 NAT 的连接跟踪项。通过这种方式可以看到一个指定的流经过了哪种类型的 NAT 转换。例如,运行 sudo conntrack -L -p tcp –src-nat 可显示经过源 NAT 的连接跟踪项,输出结果类似于以下内容:

tcp 6 114 TIME_WAIT src=10.0.0.10 dst=10.8.2.12 sport=5536 dport=80 src=10.8.2.12 dst=192.168.1.2 sport=80 dport=5536 [ASSURED]

这个连接跟踪项表示一条从 10.0.0.10:5536 到 10.8.2.12:80 的连接。与前面示例不同的是,回复方向的四元组不是原始方向四元组的简单翻转:源地址已修改。目标主机(10.8.2.12)将回复数据包发送到 192.168.1.2,而不是 10.0.0.10。每当 10.0.0.10 发送新的报文时,具有此连接跟踪项的路由器会将源地址替换为 192.168.1.2。当 10.8.2.12 发送回复报文时,该路由器将目的地址修改回 10.0.0.10。上述源 NAT 行为源自一条 NFT 伪装#Masquerading) 规则:

inet nat postrouting meta oifname "veth0" masquerade

其他类型的 NAT 规则,例如目标地址 DNAT 规则或重定向规则,其连接跟踪项也会以类似的方式显示,回复方向四元组的远端地址或端口与原始方向四元组的远端地址或端口不同。

连接跟踪扩展

连接跟踪的记帐功能和时间戳功能是两个有用的扩展功能。运行 sudo sysctl net.netfilter.nf_conntrack_acct=1 可以在运行 sudo conntrack -L 时显示每个流经过的字节数和报文数。运行 sudo sysctl net.netfilter.nf_conntrack_timestamp=1 为每个连接记录一个开始时间戳,之后每次运行 sudo conntrack -L 时都可以显示这个流从开始经过了多少秒。在上述命令中增加 –output ktimestamp 选项也可以看到流开始的绝对时间。

插入和更改连接跟踪项

你可以手动为状态表添加连接跟踪项,例如:

sudo conntrack -I -s 192.168.7.10 -d 10.1.1.1 --protonum 17 --timeout 120 --sport 12345 --dport 80

这项命令通常被 conntrackd 用于状态复制,即将主防火墙的连接跟踪项复制到备用防火墙系统。于是当切换发生的时候,备用系统可以接管已经建立的连接且不会造成中断。连接跟踪还可以存储报文的带外元数据,例如连接跟踪标记和连接跟踪标签。可以用更新选项(-U)来修改它们:

sudo conntrack -U -m 42 -p tcp

这条命令将所有的 TCP 流的连接跟踪标记修改为 42。

删除连接跟踪项

在某些情况下,你可能想从状态表中删除条目。例如,对 NAT 规则的修改不会影响表中已存在流的经过报文。因此对 UDP 长连接(例如像 VXLAN 这样的隧道协议),删除表项可能很有意义,这样新的 NAT 转换规则才能生效。可以通过 sudo conntrack -D 命令附带可选的地址和端口列表选项,来删除相应的表项,如下例所示:

sudo conntrack -D -p udp  --src 10.0.12.4 --dst 10.0.0.1 --sport 1234 --dport 53

连接跟踪错误计数

conntrack 也可以输出统计数字:

# sudo conntrack -S
cpu=0 found=0 invalid=130 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=10
cpu=1 found=0 invalid=0 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=0
cpu=2 found=0 invalid=0 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=1
cpu=3 found=0 invalid=0 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=0

大多数计数器将为 0。Foundinsert 数将始终为 0,它们只是为了后向兼容。其他错误计数包括:

  • invalid:报文既不匹配已有连接跟踪项,也未创建新连接。
  • insert_failed:报文新建了一个连接,但插入状态表时失败。这在 NAT 引擎在伪装时恰好选择了重复的源地址和端口时可能出现。
  • drop:报文新建了一个连接,但是没有可用的内存为其分配新的状态条目。
  • early_drop:连接跟踪表已满。为了接受新的连接,已有的未看到双向报文的连接被丢弃。
  • error:icmp(v6) 收到与已知连接不匹配的 icmp 错误数据包。
  • search_restart:查找过程由于另一个 CPU 的插入或删除操作而中断。
  • clash_resolve:多个 CPU 试图插入相同的连接跟踪条目。

除非经常发生,这些错误条件通常无害。一些错误可以通过针对预期工作负载调整连接跟踪子系统的参数来降低其发生概率,典型的配置包括 net.netfilter.nf_conntrack_bucketsnet.netfilter.nf_conntrack_max 参数。可在 nf\_conntrack-sysctl 文档 中查阅相应配置参数的完整列表。

当报文状态是 invalid 时,请使用 sudo sysctl net.netfilter.nf_conntrack_log_invalid=255 来获取更多信息。例如,当连接跟踪遇到一个所有 TCP 标志位均为 0 的报文时,将记录以下内容:

nf_ct_proto_6: invalid tcp flag combination SRC=10.0.2.1 DST=10.0.96.7 LEN=1040 TOS=0x00 PREC=0x00 TTL=255 ID=0 PROTO=TCP SPT=5723 DPT=443 SEQ=1 ACK=0

总结

本文介绍了如何检查连接跟踪表和存储在跟踪流中的 NAT 信息。本系列的下一部分将延伸讨论连接跟踪工具和连接跟踪事件框架。


via: https://fedoramagazine.org/network-address-translation-part-2-the-conntrack-tool/

作者:Florian Westphal 选题:lujun9972 译者:cooljelly 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这是有关 网络地址转换 network address translation (NAT)的系列文章中的第一篇。这一部分将展示如何使用 iptables/nftables 报文跟踪功能来定位 NAT 相关的连接问题。

引言

网络地址转换(NAT)是一种将容器或虚拟机暴露在互联网中的一种方式。传入的连接请求将其目标地址改写为另一个地址,随后被路由到容器或虚拟机。相同的技术也可用于负载均衡,即传入的连接被分散到不同的服务器上去。

当网络地址转换没有按预期工作时,连接请求将失败,会暴露错误的服务,连接最终出现在错误的容器中,或者请求超时,等等。调试此类问题的一种方法是检查传入请求是否与预期或已配置的转换相匹配。

连接跟踪

NAT 不仅仅是修改 IP 地址或端口号。例如,在将地址 X 映射到 Y 时,无需添加新规则来执行反向转换。一个被称为 “conntrack” 的 netfilter 系统可以识别已有连接的回复报文。每个连接都在 conntrack 系统中有自己的 NAT 状态。反向转换是自动完成的。

规则匹配跟踪

nftables 工具(以及在较小的程度上,iptables)允许针对某个报文检查其处理方式以及该报文匹配规则集合中的哪条规则。为了使用这项特殊的功能,可在合适的位置插入“跟踪规则”。这些规则会选择被跟踪的报文。假设一个来自 IP 地址 C 的主机正在访问一个 IP 地址是 S 以及端口是 P 的服务。我们想知道报文匹配了哪条 NAT 转换规则,系统检查了哪些规则,以及报文是否在哪里被丢弃了。

由于我们要处理的是传入连接,所以我们将规则添加到 prerouting 钩子上。prerouting 意味着内核尚未决定将报文发往何处。修改目标地址通常会使报文被系统转发,而不是由主机自身处理。

初始配置

# nft 'add table inet trace_debug'
# nft 'add chain inet trace_debug trace_pre { type filter hook prerouting priority -200000; }'
# nft "insert rule inet trace_debug trace_pre ip saddr $C ip daddr $S tcp dport $P tcp flags syn limit rate 1/second meta nftrace set 1"

第一条规则添加了一张新的规则表,这使得将来删除和调试规则可以更轻松。一句 nft delete table inet trace_debug 命令就可以删除调试期间临时加入表中的所有规则和链。

第二条规则在系统进行路由选择之前(prerouting 钩子)创建了一个基本钩子,并将其优先级设置为负数,以保证它在连接跟踪流程和 NAT 规则匹配之前被执行。

然而,唯一最重要的部分是第三条规则的最后一段:meta nftrace set 1。这条规则会使系统记录所有匹配这条规则的报文所关联的事件。为了尽可能高效地查看跟踪信息(提高信噪比),考虑对跟踪的事件增加一个速率限制,以保证其数量处于可管理的范围。一个好的选择是限制每秒钟最多一个报文或一分钟最多一个报文。上述案例记录了所有来自终端 $C 且去往终端 $S 的端口 $P 的所有 SYN 报文和 SYN/ACK 报文。限制速率的配置语句可以防范事件过多导致的洪泛风险。事实上,大多数情况下只记录一个报文就足够了。

对于 iptables 用户来讲,配置流程是类似的。等价的配置规则类似于:

# iptables -t raw -I PREROUTING -s $C -d $S -p tcp --tcp-flags SYN SYN  --dport $P  -m limit --limit 1/s -j TRACE

获取跟踪事件

原生 nft 工具的用户可以直接运行 nft 进入 nft 跟踪模式:

# nft monitor trace

这条命令会将收到的报文以及所有匹配该报文的规则打印出来(用 CTRL-C 来停止输出):

trace id f0f627 ip raw prerouting  packet: iif "veth0" ether saddr ..

我们将在下一章详细分析该结果。如果你用的是 iptables,首先通过 iptables –version 命令检查一下已安装的版本。例如:

# iptables --version
iptables v1.8.5 (legacy)

(legacy) 意味着被跟踪的事件会被记录到内核的环形缓冲区中。你可以用 dmesgjournalctl 命令来查看这些事件。这些调试输出缺少一些信息,但和新工具提供的输出从概念上来讲很类似。你将需要首先查看规则被记录下来的行号,并与活跃的 iptables 规则集合手动关联。如果输出显示 (nf_tables),你可以使用 xtables-monitor 工具:

# xtables-monitor --trace

如果上述命令仅显示版本号,你仍然需要查看 dmesg/journalctl 的输出。xtables-monitor 工具和 nft 监控跟踪工具使用相同的内核接口。它们之间唯一的不同点就是,xtables-monitor 工具会用 iptables 的语法打印事件,且如果你同时使用了 iptables-nftnft,它将不能打印那些使用了 maps/sets 或其他只有 nftables 才支持的功能的规则。

示例

我们假设需要调试一个到虚拟机/容器的端口不通的问题。ssh -p 1222 10.1.2.3 命令应该可以远程连接那台服务器上的某个容器,但连接请求超时了。

你拥有运行那台容器的主机的登录权限。现在登录该机器并增加一条跟踪规则。可通过前述案例查看如何增加一个临时的调试规则表。跟踪规则类似于这样:

nft "insert rule inet trace_debug trace_pre ip daddr 10.1.2.3 tcp dport 1222 tcp flags syn limit rate 6/minute meta nftrace set 1"

在添加完上述规则后,运行 nft monitor trace,在跟踪模式下启动 nft,然后重试刚才失败的 ssh 命令。如果规则集较大,会出现大量的输出。不用担心这些输出,下一节我们会做逐行分析。

trace id 9c01f8 inet trace_debug trace_pre packet: iif "enp0" ether saddr .. ip saddr 10.2.1.2 ip daddr 10.1.2.3 ip protocol tcp tcp dport 1222 tcp flags == syn
trace id 9c01f8 inet trace_debug trace_pre rule ip daddr 10.2.1.2 tcp dport 1222 tcp flags syn limit rate 6/minute meta nftrace set 1 (verdict continue)
trace id 9c01f8 inet trace_debug trace_pre verdict continue
trace id 9c01f8 inet trace_debug trace_pre policy accept
trace id 9c01f8 inet nat prerouting packet: iif "enp0" ether saddr .. ip saddr 10.2.1.2 ip daddr 10.1.2.3 ip protocol tcp  tcp dport 1222 tcp flags == syn
trace id 9c01f8 inet nat prerouting rule ip daddr 10.1.2.3  tcp dport 1222 dnat ip to 192.168.70.10:22 (verdict accept)
trace id 9c01f8 inet filter forward packet: iif "enp0" oif "veth21" ether saddr .. ip daddr 192.168.70.10 .. tcp dport 22 tcp flags == syn tcp window 29200
trace id 9c01f8 inet filter forward rule ct status dnat jump allowed_dnats (verdict jump allowed_dnats)
trace id 9c01f8 inet filter allowed_dnats rule drop (verdict drop)
trace id 20a4ef inet trace_debug trace_pre packet: iif "enp0" ether saddr .. ip saddr 10.2.1.2 ip daddr 10.1.2.3 ip protocol tcp tcp dport 1222 tcp flags == syn

对跟踪结果作逐行分析

输出结果的第一行是触发后续输出的报文编号。这一行的语法与 nft 规则语法相同,同时还包括了接收报文的首部字段信息。你也可以在这一行找到接收报文的接口名称(此处为 enp0)、报文的源和目的 MAC 地址、报文的源 IP 地址(可能很重要 - 报告问题的人可能选择了一个错误的或非预期的主机),以及 TCP 的源和目的端口。同时你也可以在这一行的开头看到一个“跟踪编号”。该编号标识了匹配跟踪规则的特定报文。第二行包括了该报文匹配的第一条跟踪规则:

trace id 9c01f8 inet trace_debug trace_pre rule ip daddr 10.2.1.2 tcp dport 1222 tcp flags syn limit rate 6/minute meta nftrace set 1 (verdict continue)

这就是刚添加的跟踪规则。这里显示的第一条规则总是激活报文跟踪的规则。如果在这之前还有其他规则,它们将不会在这里显示。如果没有任何跟踪输出结果,说明没有抵达这条跟踪规则,或者没有匹配成功。下面的两行表明没有后续的匹配规则,且 trace_pre 钩子允许报文继续传输(判定为接受)。

下一条匹配规则是:

trace id 9c01f8 inet nat prerouting rule ip daddr 10.1.2.3  tcp dport 1222 dnat ip to 192.168.70.10:22 (verdict accept)

这条 DNAT 规则设置了一个到其他地址和端口的映射。规则中的参数 192.168.70.10 是需要收包的虚拟机的地址,目前为止没有问题。如果它不是正确的虚拟机地址,说明地址输入错误,或者匹配了错误的 NAT 规则。

IP 转发

通过下面的输出我们可以看到,IP 路由引擎告诉 IP 协议栈,该报文需要被转发到另一个主机:

trace id 9c01f8 inet filter forward packet: iif "enp0" oif "veth21" ether saddr .. ip daddr 192.168.70.10 .. tcp dport 22 tcp flags == syn tcp window 29200

这是接收到的报文的另一种呈现形式,但和之前相比有一些有趣的不同。现在的结果有了一个输出接口集合。这在之前不存在的,因为之前的规则是在路由决策之前(prerouting 钩子)。跟踪编号和之前一样,因此仍然是相同的报文,但目标地址和端口已经被修改。假设现在还有匹配 tcp dport 1222 的规则,它们将不会对现阶段的报文产生任何影响了。

如果该行不包含输出接口(oif),说明路由决策将报文路由到了本机。对路由过程的调试属于另外一个主题,本文不再涉及。

trace id 9c01f8 inet filter forward rule ct status dnat jump allowed_dnats (verdict jump allowed_dnats)

这条输出表明,报文匹配到了一个跳转到 allowed_dnats 链的规则。下一行则说明了连接失败的根本原因:

trace id 9c01f8 inet filter allowed_dnats rule drop (verdict drop)

这条规则无条件地将报文丢弃,因此后续没有关于该报文的日志输出。下一行则是另一个报文的输出结果了:

trace id 20a4ef inet trace_debug trace_pre packet: iif "enp0" ether saddr .. ip saddr 10.2.1.2 ip daddr 10.1.2.3 ip protocol tcp tcp dport 1222 tcp flags == syn

跟踪编号已经和之前不一样,然后报文的内容却和之前是一样的。这是一个重传尝试:第一个报文被丢弃了,因此 TCP 尝试了重传。可以忽略掉剩余的输出结果了,因为它并没有提供新的信息。现在是时候检查那条链了。

规则集合分析

上一节我们发现报文在 inet filter 表中的一个名叫 allowed_dnats 的链中被丢弃。现在我们来查看它:

# nft list chain inet filter allowed_dnats
table inet filter {
 chain allowed_dnats {
  meta nfproto ipv4 ip daddr . tcp dport @allow_in accept
  drop
   }
}

接受 @allow_in 集的数据包的规则没有显示在跟踪日志中。我们通过列出元素的方式,再次检查上述报文的目标地址是否在 @allow_in 集中:

# nft "get element inet filter allow_in { 192.168.70.10 . 22 }"
Error: Could not process rule: No such file or directory

不出所料,地址-服务对并没有出现在集合中。我们将其添加到集合中。

# nft "add element inet filter allow_in { 192.168.70.10 . 22 }"

现在运行查询命令,它将返回新添加的元素。

# nft "get element inet filter allow_in { 192.168.70.10 . 22 }"
table inet filter {
   set allow_in {
      type ipv4_addr . inet_service
      elements = { 192.168.70.10 . 22 }
   }
}

ssh 命令现在应该可以工作,且跟踪结果可以反映出该变化:

trace id 497abf58 inet filter forward rule ct status dnat jump allowed_dnats (verdict jump allowed_dnats)

trace id 497abf58 inet filter allowed_dnats rule meta nfproto ipv4 ip daddr . tcp dport @allow_in accept (verdict accept)

trace id 497abf58 ip postrouting packet: iif "enp0" oif "veth21" ether .. trace id 497abf58 ip postrouting policy accept

这表明报文通过了转发路径中的最后一个钩子 - postrouting

如果现在仍然无法连接,问题可能处在报文流程的后续阶段,有可能并不在 nftables 的规则集合范围之内。

总结

本文介绍了如何通过 nftables 的跟踪机制检查丢包或其他类型的连接问题。本系列的下一篇文章将展示如何检查连接跟踪系统和可能与连接跟踪流相关的 NAT 信息。


via: https://fedoramagazine.org/network-address-translation-part-1-packet-tracing/

作者:Florian Westphal 选题:lujun9972 译者:cooljelly 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Linux TCP 协议栈具有无数个可以更改其行为的 sysctl 旋钮。 这包括可用于接收或发送操作的内存量、套接字的最大数量、可选的特性和协议扩展。

有很多文章出于各种“性能调优”或“安全性”原因,建议禁用 TCP 扩展,比如时间戳或 选择性确认 Selective ACKnowledgments (SACK)。

本文提供了这些扩展功能的背景,为什么会默认启用,它们之间是如何关联的,以及为什么通常情况下将它们关闭是个坏主意。

TCP 窗口缩放

TCP 可以承受的数据传输速率受到几个因素的限制。其中包括:

  • 往返时间 Round trip time (RTT)。

这是数据包到达目的地并返回回复所花费的时间。越低越好。

  • 所涉及的网络路径的最低链路速度。
  • 丢包频率。
  • 新数据可用于传输的速度。

例如,CPU 需要能够以足够快的速度将数据传递到网络适配器。如果 CPU 需要首先加密数据,则适配器可能必须等待新数据。同样地,如果磁盘存储不能足够快地读取数据,则磁盘存储可能会成为瓶颈。

  • TCP 接收窗口的最大可能大小。

接收窗口决定了 TCP 在必须等待接收方报告接收到该数据之前可以传输多少数据(以字节为单位)。这是由接收方宣布的。接收方将在读取并确认接收到传入数据时不断更新此值。接收窗口的当前值包含在 TCP 报头 中,它是 TCP 发送的每个数据段的一部分。因此,只要发送方接收到来自对等方的确认,它就知道当前的接收窗口。这意味着往返时间(RTT)越长,发送方获得接收窗口更新所需的时间就越长。

TCP 的未确认(正在传输)数据被限制为最多 64KB。在大多数网络场景中,这甚至还不足以维持一个像样的数据速率。让我们看看一些例子。

理论数据速率

在往返时间(RTT)为 100 毫秒的情况下,TCP 每秒最多可以传输 640KB。在延迟为 1 秒的情况下,最大理论数据速率降至只有 64KB/s。

这是因为接收窗口的原因。一旦发送了 64KB 的数据,接收窗口就已经满了。发送方必须等待,直到对等方通知它应用程序已经读取了至少一部分数据。

发送的第一个段会把 TCP 窗口缩减去该段的大小。在接收窗口值的更新信息可用之前,需要往返一次。当更新以 1 秒的延迟到达时,即使链路有足够的可用带宽,也会导致 64KB 的限制。

为了充分利用一个具有几毫秒延迟的快速网络,必须有一个比传统 TCP 支持的窗口更大的窗口。“64KB 限制”是协议规范的产物:TCP 头只为接收窗口大小保留了 16 个位。这允许接收窗口最大为 64KB。在 TCP 协议最初设计时,这个大小并没有被视为一个限制。

不幸的是,想通过仅仅更改 TCP 头来支持更大的最大窗口值是不可能的。如果这样做就意味着 TCP 的所有实现都必须同时更新,否则它们将无法相互理解。为了解决这个问题,我们改变了对接收窗口值的解释。

“窗口缩放选项”允许你改变这个解释,同时保持与现有实现的兼容性。

TCP 选项:向后兼容的协议扩展

TCP 支持可选扩展。这允许使用新特性增强协议,而无需立即更新所有实现。当 TCP 发起方连接到对等方时,它还会发送一个支持的扩展列表。所有扩展都遵循相同的格式:一个唯一的选项号,后跟选项的长度以及选项数据本身。

TCP 响应方检查连接请求中包含的所有选项号。如果它遇到一个不能理解的选项号,则会跳过 该选项号附带的“长度”字节的数据,并检查下一个选项号。响应方忽略了从答复中无法理解的内容。这使发送方和接收方都够理解所支持的公共选项集。

使用窗口缩放时,选项数据总是由单个数字组成。

窗口缩放选项

Window Scale option (WSopt): Kind: 3, Length: 3
    +---------+---------+---------+
    | Kind=3  |Length=3 |shift.cnt|
    +---------+---------+---------+
         1         1         1

窗口缩放 选项告诉对等方,应该使用给定的数字缩放 TCP 标头中的接收窗口值,以获取实际大小。

例如,一个宣告窗口缩放因子为 7 的 TCP 发起方试图指示响应方,任何将来携带接收窗口值为 512 的数据包实际上都会宣告 65536 字节的窗口。增加了 128 倍(2^7)。这将允许最大为 8MB 的 TCP 窗口。

不能理解此选项的 TCP 响应方将会忽略它,为响应连接请求而发送的 TCP 数据包(SYN-ACK)不会包含该窗口缩放选项。在这种情况下,双方只能使用 64k 的窗口大小。幸运的是,默认情况下,几乎每个 TCP 栈都支持并默认启用了此选项,包括 Linux。

响应方包括了它自己所需的缩放因子。两个对等方可以使用不同的因子。宣布缩放因子为 0 也是合法的。这意味着对等方应该如实处理它接收到的接收窗口值,但它允许应答方向上的缩放值,然后接收方可以使用更大的接收窗口。

与 SACK 或 TCP 时间戳不同,窗口缩放选项仅出现在 TCP 连接的前两个数据包中,之后无法更改。也不可能通过查看不包含初始连接三次握手的连接的数据包捕获来确定缩放因子。

支持的最大缩放因子为 14。这将允许 TCP 窗口的大小高达 1GB。

窗口缩放的缺点

在非常特殊的情况下,它可能导致数据损坏。但在你禁用该选项之前,要知道通常情况下是不可能损坏的。还有一种解决方案可以防止这种情况。不幸的是,有些人在没有意识到它与窗口缩放的关系的情况下禁用了该解决方案。首先,让我们看一下需要解决的实际问题。想象以下事件序列:

  1. 发送方发送段:s\_1、s\_2、s\_3、... s\_n。
  2. 接收方看到:s\_1、s\_3、... s\_n,并发送对 s\_1 的确认。
  3. 发送方认为 s\_2 丢失,然后再次发送。它还发送了段 s\_n+1 中包含的新数据。
  4. 接收方然后看到:s\_2、s\_n+1,s\_2:数据包 s\_2 被接收两次。

当发送方过早触发重新传输时,可能会发生这种情况。在正常情况下,即使使用窗口缩放,这种错误的重传也绝不会成为问题。接收方将只丢弃重复项。

从旧数据到新数据

TCP 序列号最多可以为 4GB。如果它变得大于此值,则该序列会回绕到 0,然后再次增加。这本身不是问题,但是如果这种问题发生得足够快,则上述情况可能会造成歧义。

如果在正确的时刻发生回绕,则序列号 s\_2(重新发送的数据包)可能已经大于 s\_n+1。因此,在最后的步骤(4)中,接收方可以将其解释为:s\_2、s\_n+1、s\_n+m,即它可以将 “旧” 数据包 s\_2 视为包含新数据。

通常,这不会发生,因为即使在高带宽链接上,“回绕”也只会每隔几秒钟或几分钟发生一次。原始数据包和不需要的重传的数据包之间的间隔将小得多。

例如,对于 50MB/s 的传输速度,重复项要迟到一分钟以上才会成为问题。序列号的回绕速度没有快到让小的延迟会导致这个问题。

一旦 TCP 达到 “GB/s” 的吞吐率,序列号的回绕速度就会非常快,以至于即使只有几毫秒的延迟也可能会造成 TCP 无法检测出的重复项。通过解决接收窗口太小的问题,TCP 现在可以用于以前无法实现的网络速度,这会产生一个新的,尽管很少见的问题。为了在 RTT 非常低的环境中安全使用 GB/s 的速度,接收方必须能够检测到这些旧的重复项,而不必仅依赖序列号。

TCP 时间戳

最佳截止日期

用最简单的术语来说,TCP 时间戳只是在数据包上添加时间戳,以解决由非常快速的序列号回绕引起的歧义。如果一个段看起来包含新数据,但其时间戳早于上一个在接收窗口内的数据包,则该序列号已被重新回绕,而“新”数据包实际上是一个较旧的重复项。这解决了即使在极端情况下重传的歧义。

但是,该扩展不仅仅是检测旧数据包。TCP 时间戳的另一个主要功能是更精确的往返时间测量(RTTm)。

需要准确的 RTT 估算

当两个对等方都支持时间戳时,每个 TCP 段都携带两个附加数字:时间戳值和回显时间戳。

TCP Timestamp option (TSopt): Kind: 8, Length: 10
+-------+----+----------------+-----------------+
|Kind=8 | 10 |TS Value (TSval)|EchoReply (TSecr)|
+-------+----+----------------+-----------------+
    1      1         4                4

准确的 RTT 估算对于 TCP 性能至关重要。TCP 会自动重新发送未确认的数据。重传由计时器触发:如果超时,则 TCP 会将尚未收到确认的一个或多个数据包视为丢失。然后再发送一次。

但是,“尚未得到确认” 并不意味着该段已丢失。也有可能是接收方到目前为止没有发送确认,或者确认仍在传输中。这就造成了一个两难的困境:TCP 必须等待足够长的时间,才能让这种轻微的延迟变得无关紧要,但它也不能等待太久。

低网络延迟 VS 高网络延迟

在延迟较高的网络中,如果计时器触发过快,TCP 经常会将时间和带宽浪费在不必要的重发上。

然而,在延迟较低的网络中,等待太长时间会导致真正发生数据包丢失时吞吐量降低。因此,在低延迟网络中,计时器应该比高延迟网络中更早到期。所以,TCP 重传超时不能使用固定常量值作为超时。它需要根据其在网络中所经历的延迟来调整该值。

往返时间的测量

TCP 选择基于预期的往返时间(RTT)的重传超时。RTT 事先是未知的。它是通过测量发送的段与 TCP 接收到该段所承载数据的确认之间的增量来估算的。

由于多种因素使其而变得复杂。

  • 出于性能原因,TCP 不会为收到的每个数据包生成新的确认。它等待的时间非常短:如果有更多的数据段到达,则可以通过单个 ACK 数据包确认其接收。这称为 “累积确认” cumulative ACK
  • 往返时间并不恒定。这是有多种因素造成的。例如,客户端可能是一部移动电话,随其移动而切换到不同的基站。也可能是当链路或 CPU 的利用率提高时,数据包交换花费了更长的时间。
  • 必须重新发送的数据包在计算过程中必须被忽略。这是因为发送方无法判断重传数据段的 ACK 是在确认原来的传输数据(毕竟已到达)还是在确认重传数据。

最后一点很重要:当 TCP 忙于从丢失中恢复时,它可能仅接收到重传段的 ACK。这样,它就无法在此恢复阶段测量(更新)RTT。所以,它无法调整重传超时,然后超时将以指数级增长。那是一种非常具体的情况(它假设其他机制,如快速重传或 SACK 不起作用)。但是,使用 TCP 时间戳,即使在这种情况下也会进行 RTT 评估。

如果使用了扩展,则对等方将从 TCP 段的扩展空间中读取时间戳值并将其存储在本地。然后,它将该值作为 “回显时间戳” 放入发回的所有数据段中。

因此,该选项带有两个时间戳:它的发送方自己的时间戳和它从对等方收到的最新时间戳。原始发送方使用 “回显时间戳” 来计算 RTT。它是当前时间戳时钟与 “回显时间戳” 中所反映的值之间的增量。

时间戳的其他用途

TCP 时间戳甚至还有除 PAWS( 防止序列号回绕 Protection Against Wrapped Sequences ) 和 RTT 测量以外的其他用途。例如,可以检测是否不需要重发。如果该确认携带较旧的回显时间戳,则该确认针对的是初始数据包,而不是重新发送的数据包。

TCP 时间戳的另一个更晦涩的用例与 TCP syn cookie 功能有关。

在服务器端建立 TCP 连接

当连接请求到达的速度快于服务器应用程序可以接受新的传入连接的速度时,连接积压最终将达到其极限。这可能是由于系统配置错误或应用程序中的错误引起的。当一个或多个客户端发送连接请求而不对 “SYN ACK” 响应做出反应时,也会发生这种情况。这将用不完整的连接填充连接队列。这些条目需要几秒钟才会超时。这被称为 “同步泛洪攻击” syn flood attack

TCP 时间戳和 TCP Syn Cookie

即使队列已满,某些 TCP 协议栈也允许继续接受新连接。发生这种情况时,Linux 内核将在系统日志中打印一条突出的消息:

端口 P 上可能发生 SYN 泛洪。正在发送 Cookie。检查 SNMP 计数器。

此机制将完全绕过连接队列。通常存储在连接队列中的信息被编码到 SYN/ACK 响应 TCP 序列号中。当 ACK 返回时,可以根据序列号重建队列条目。

序列号只有有限的空间来存储信息。因此,使用 “TCP Syn Cookie” 机制建立的连接不能支持 TCP 选项。

但是,对两个对等点都通用的 TCP 选项可以存储在时间戳中。ACK 数据包在回显时间戳字段中反映了该值,这也允许恢复已达成共识的 TCP 选项。否则,cookie 连接受标准的 64KB 接收窗口限制。

常见误区 —— 时间戳不利于性能

不幸的是,一些指南建议禁用 TCP 时间戳,以减少内核访问时间戳时钟来获取当前时间所需的次数。这是不正确的。如前所述,RTT 估算是 TCP 的必要部分。因此,内核在接收/发送数据包时总是采用微秒级的时间戳。

在包处理步骤的其余部分中,Linux 会重用 RTT 估算所需的时钟时间戳。这还避免了将时间戳添加到传出 TCP 数据包的额外时钟访问。

整个时间戳选项在每个数据包中仅需要 10 个字节的 TCP 选项空间,这不会显著减少可用于数据包有效负载的空间。

常见误区 —— 时间戳是个安全问题

一些安全审计工具和(较旧的)博客文章建议禁用 TCP 时间戳,因为据称它们泄露了系统正常运行时间:这样一来,便可以估算系统/内核的补丁级别。这在过去是正确的:时间戳时钟基于不断增加的值,该值在每次系统引导时都以固定值开始。时间戳值可以估计机器已经运行了多长时间(正常运行时间 uptime)。

从 Linux 4.12 开始,TCP 时间戳不再显示正常运行时间。发送的所有时间戳值都使用对等设备特定的偏移量。时间戳值也每 49 天回绕一次。

换句话说,从地址 “A” 出发,或者终到地址 “A” 的连接看到的时间戳与到远程地址 “B” 的连接看到的时间戳不同。

运行 sysctl net.ipv4.tcp_timeamp=2 以禁用随机化偏移。这使得分析由诸如 wiresharktcpdump 之类的工具记录的数据包跟踪变得更容易 —— 从主机发送的数据包在其 TCP 选项时间戳中都具有相同的时钟基准。因此,对于正常操作,默认设置应保持不变。

选择性确认

如果同一数据窗口中的多个数据包丢失了,TCP 将会出现问题。这是因为 TCP 确认是累积的,但仅适用于按顺序到达的数据包。例如:

  • 发送方发送段 s\_1、s\_2、s\_3、... s\_n
  • 发送方收到 s\_2 的 ACK
  • 这意味着 s\_1 和 s\_2 都已收到,并且发送方不再需要保留这些段。
  • s\_3 是否应该重新发送? s\_4 呢? s\_n?

发送方等待 “重传超时” 或 “重复 ACK” 以使 s\_2 到达。如果发生重传超时或到达了 s\_2 的多个重复 ACK,则发送方再次发送 s\_3。

如果发送方收到对 s\_n 的确认,则 s\_3 是唯一丢失的数据包。这是理想的情况。仅发送单个丢失的数据包。

如果发送方收到的确认段小于 s\_n,例如 s\_4,则意味着丢失了多个数据包。发送方也需要重传下一个数据段。

重传策略

可能只是重复相同的序列:重新发送下一个数据包,直到接收方指示它已处理了直至 s\_n 的所有数据包为止。这种方法的问题在于,它需要一个 RTT,直到发送方知道接下来必须重新发送的数据包为止。尽管这种策略可以避免不必要的重传,但要等到 TCP 重新发送整个数据窗口后,它可能要花几秒钟甚至更长的时间。

另一种方法是一次重新发送几个数据包。当丢失了几个数据包时,此方法可使 TCP 恢复更快。在上面的示例中,TCP 重新发送了 s\_3、s\_4、s\_5、...,但是只能确保已丢失 s\_3。

从延迟的角度来看,这两种策略都不是最佳的。如果只有一个数据包需要重新发送,第一种策略是快速的,但是当多个数据包丢失时,它花费的时间太长。

即使必须重新发送多个数据包,第二个也是快速的,但是以浪费带宽为代价。此外,这样的 TCP 发送方在进行不必要的重传时可能已经发送了新数据。

通过可用信息,TCP 无法知道丢失了哪些数据包。这就是 TCP 选择性确认(SACK)的用武之地了。就像窗口缩放和时间戳一样,它是另一个可选的但非常有用的 TCP 特性。

SACK 选项

   TCP Sack-Permitted Option: Kind: 4, Length 2
   +---------+---------+
   | Kind=4  | Length=2|
   +---------+---------+

支持此扩展的发送方在连接请求中包括 “允许 SACK” 选项。如果两个端点都支持该扩展,则检测到数据流中丢失数据包的对等方可以将此信息通知发送方。

   TCP SACK Option: Kind: 5, Length: Variable
                     +--------+--------+
                     | Kind=5 | Length |
   +--------+--------+--------+--------+
   |      Left Edge of 1st Block       |
   +--------+--------+--------+--------+
   |      Right Edge of 1st Block      |
   +--------+--------+--------+--------+
   |                                   |
   /            . . .                  /
   |                                   |
   +--------+--------+--------+--------+
   |      Left Edge of nth Block       |
   +--------+--------+--------+--------+
   |      Right Edge of nth Block      |
   +--------+--------+--------+--------+

接收方遇到 s\_2 后跟 s\_5 ... s\_n,则在发送对 s\_2 的确认时将包括一个 SACK 块:

                +--------+-------+
                | Kind=5 |   10  |
+--------+------+--------+-------+
| Left edge: s_5                 |
+--------+--------+-------+------+
| Right edge: s_n                |
+--------+-------+-------+-------+

这告诉发送方到 s\_2 的段都是按顺序到达的,但也让发送方知道段 s\_5 至 s\_n 也已收到。然后,发送方可以重新发送那两个数据包(s\_3、s\_4),并继续发送新数据。

神话般的无损网络

从理论上讲,如果连接不会丢包,那么 SACK 就没有任何优势。或者连接具有如此低的延迟,甚至等待一个完整的 RTT 都无关紧要。

在实践中,无损行为几乎是不可能保证的。即使网络及其所有交换机和路由器具有足够的带宽和缓冲区空间,数据包仍然可能丢失:

  • 主机操作系统可能面临内存压力并丢弃数据包。请记住,一台主机可能同时处理数万个数据包流。
  • CPU 可能无法足够快地消耗掉来自网络接口的传入数据包。这会导致网络适配器本身中的数据包丢失。
  • 如果 TCP 时间戳不可用,即使一个非常小的 RTT 的连接也可能在丢失恢复期间暂时停止。

使用 SACK 不会增加 TCP 数据包的大小,除非连接遇到数据包丢失。因此,几乎没有理由禁用此功能。几乎所有的 TCP 协议栈都支持 SACK —— 它通常只在不进行 TCP 批量数据传输的低功耗 IOT 类的设备上才不存在。

当 Linux 系统接受来自此类设备的连接时,TCP 会自动为受影响的连接禁用 SACK。

总结

本文中研究的三个 TCP 扩展都与 TCP 性能有关,最好都保留其默认设置:启用。

TCP 握手可确保仅使用双方都可以理解的扩展,因此,永远不需因为对等方可能不支持而全局禁用该扩展。

关闭这些扩展会导致严重的性能损失,尤其是 TCP 窗口缩放和 SACK。可以禁用 TCP 时间戳而不会立即造成不利影响,但是现在没有令人信服的理由这样做了。启用它们还可以支持 TCP 选项,即使在 SYN cookie 生效时也是如此。


via: https://fedoramagazine.org/tcp-window-scaling-timestamps-and-sack/

作者:Florian Westphal 选题:lujun9972 译者:gxlct008 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出