Spurious Retransmit 导致 NAT 设备响应 RST

在一个线上问题排查过程中,出现了一个新的问题:wget 下载文件在连接建立很短的时间内(1 - 2s)就被 RESET 了。 首先先简要说明一下链路:

客户端经过一个SNAT设备出公网,在服务器上下载一个大文件。端到端的 RTT 大约 90 ms。由于最开始并没有怀疑到 SNAT 设备,并且 server 端无法抓包,我们分别在客户端 和 SNAT 设备到 server 间一台网元设备上抓包。 中间网元设备

SNAT 设备

如上,中间网元设备显示客户端(端口 34567)先发送了 RST 报文,随后服务器(端口 443)响应 RST 报文。但是客户端抓包显示它根本没有发送过 RST 报文。

此时,我们再从 SNAT 设备上抓包,发现就是 SNAT 设备首先给服务器发送了 RST 报文。

这里的 SNAT 设备实际上是一台基于 IPtables 的 Linux 设备。

抓包可以看到,RST 是对一个 TCP Spurious Retransmission 的响应。为什么会发出这个 RST 呢?正好网上有一个相似的场景:Add workaround for spurious retransmits leading to connection resets

总的来说,就是 spurious retransmits 报文在序列号超出 TCP 窗口时,会被 conntrack 认为是 invalid 包,从而不再经过反向 SNAT 规则的处理。由于目的地址没有被转换,报文会按照原来的目的地址送往 INPUT,而本地又没有这个 socket,则响应一个 RST 报文。

VRF: An Overview

什么是 VRF(virtual routing forwarding)

VRF是一种实现三层网络隔离的关键技术。它通过创建多个路由表,为不同的网络流量提供独立的转发路径。 这意味着,任何三层网络结构,如接口的IP地址、静态路由的配置,甚至BGP(边界网关协议)会话,都可以被映射到特定的VRF中。这种映射机制就像是为每个VRF构建了一个独立的网络空间,彼此之间相互隔离,极大地增强了网络的安全性和管理的便利性。在MPLS VPN(多协议标签交换虚拟专用网络)等应用场景中,VRF为实现大规模的网络隔离和灵活的路由策略提供了基础框架。就像 VLAN 隔离了二层网络一样,VRF 隔离了三层网络。

5eb20c3e919fe3724b92c2ae7a66a7da_MD5

为什么需要 VRF

在 VRF 出现之前,Linux 用户主要采用两种方式来尝试实现类似的功能:策略路由(policy routing)和网络命名空间(net namespace)。然而,这两种方法都存在明显的局限性。

策略路由虽然能够通过多个路由表和策略规则来模拟 VRF 的部分功能(事实上,在 Linux 中,也是基于策略路由来去对 VRF 做的实现),但它的缺点十分突出。这种方式在配置和管理上非常复杂,难以确保网络隔离的有效性,在面对严格的网络审计时,往往无法通过。其复杂性不仅增加了运维的难度,还可能导致网络故障的风险上升,因此不被推荐使用。

网络命名空间在容器技术兴起后得到了广泛应用,它能够为容器提供全面的网络隔离。但在模拟 VRF 功能时,却显得有些“大材小用”。网络命名空间会对所有网络相关的资源进行完全隔离,包括设备、接口、ARP 表和路由表等。这意味着,即使是一些不需要隔离的服务,也会被隔离在不同的命名空间中。以 LLDP(链路层发现协议)为例,在使用网络命名空间的情况下,若要在不同的网络隔离环境中使用 LLDP,就需要在每个命名空间中单独运行实例,并且由于默认套接字相同,还需要为每个实例创建独特的套接字。这不仅增加了系统的开销,还使得管理变得更加复杂。相比之下,VRF 在隔离三层网络结构的同时,允许全局配置的共享和非三层感知服务的统一运行,大大提高了资源的利用效率。

Policy Routing VRF Net Namespace
隔离路由表 隔离三层网络 整个协议栈从二层到 socket 隔离

a9f7c55f8f39572d339b138fb1e12429_MD5c9b6614c864c7d35a8ef0a4f12ecdbfa_MD59edc8b051f504bf72140d1238513d687_MD5

VRF 配置

在 Linux 系统中配置 VRF,主要借助iproute2包来完成一系列操作。

  • 创建 VRF,并关联到 table 1
test1@test1:~$ ip link add vrf-1 type vrf table 1
test1@test1:~$ ip link set vrf-1 up
  • 添加接口到 VRF,可以看到 wg0 的 master 是 vrf-1,所有 wg0 的流量会使用关联的 vrf-1 路由表进行路由
test1@test1:~$ ip link set wg0 master vrf-1
test1@test1:~$ ip -d link show wg0 
9: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1400 qdisc noqueue master vrf-1 state UNKNOWN mode DEFAULT group default qlen 1000
    link/none  promiscuity 0 minmtu 0 maxmtu 2147483552 
    wireguard 
    vrf_slave table 1 addrgenmode none numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
  • 添加和查看 VRF 静态路由
root@test1:~# ip route add default via 10.1.0.10 vrf vrf-1
root@test1:~# ip route show table 1 
default via 10.1.0.10 dev wg0 
local 10.1.0.10 dev wg0 proto kernel scope host src 10.1.0.10 
root@test1:~# ip route show vrf vrf-1 
default via 10.1.0.10 dev wg0 

VRF 之间路由

有两种方法可以执行跨 VRF 路由。第一种方法涉及一个 VRF 的表中配置的路由,指向绑定到不同 VRF 的设备。

C³: Common Coding Conventions

C³: Common Coding Conventions

https://google.github.io/eng-practices/

https://github.com/tum-esi/common-coding-conventions

The goal of these conventions is to be concise, universal, and remarkable. It targets emerging code enthusiasts under time pressure and covers 7 topics:

  1. General Clarifications,
  2. Architecture,
  3. Implementation,
  4. Naming,
  5. Code Layout,
  6. Documentation, and
  7. Languages.

To follow this guide, you should already have heard about Object Oriented Programming and know basic programming rules, such as writing loops and meaningful functions instead of copy pasting instructions. In this Readme, we will shortly summarize the most important rules for every topic.

eBPF 指令集规范

1 BPF Instruction Set Specification, v1.0 — The Linux Kernel documentation

寄存器和调用约定

eBPF有10个通用寄存器和一个只读的帧指针寄存器,所有的寄存器都是64bits位长。

eBPF的调用约定如下:

  • R0: 函数调用的返回值和eBPF程序的退出值
  • R1 - R5: 函数调用的参数
  • R6 - R9: callee saved registers,由函数调用负责保存到栈上
  • R10: 只读的帧指针,用于访问栈 R0 - R5是临时寄存器,eBPF程序在函数调用时如需使用需要将它们保存/填充。

指令编码

eBPF使用64bit指令,编码如下:

32位(最高有效位) 16位 4位 4位 8位(最低有效位)
immediate offset source register dest register opcode
  • imm: 有符号整型立即数
  • offset: 有符号整型数,用于指针算术
  • src_reg: 除开特殊指令的编码外(如64位立即数指令)指的是源寄存器(R0 - R10)
  • dst_reg: 目标寄存器(R0 - R10)
  • opcode: 操作码

绝大多数指令不会使用所有的字段。没有使用的字段应当被置0。多字节的字段(如imm和offset)在大端BPF中以大端字节序存储,小端BPF中以小端字节序存储。

opcode                  offset imm          assembly
       src_reg dst_reg
07     0       1        00 00  44 33 22 11  r1 += 0x11223344 // little
       dst_reg src_reg
07     1       0        00 00  11 22 33 44  r1 += 0x11223344 // big

除了这些基本指令编码外,eBPF还有一种宽指令编码,在基本指令之后使用第2个64位立即数做扩展。第2个立即数包括一个伪指令,和基本指令的格式一样,不过所有的字段(除了imm)都置为0。它的imm作为整个宽指令中的imm64值的高32位:

eBPF 全局变量的实现

Linux内核在v5.2支持了eBPF全局变量(见v5.2)。

全局变量会被Clang编译器放入.bss, .data, .rodata sections。那么支持全局变量即如何把这些sections中的数据加载进内核,并且在relocation时,给访问这些变量的指令正确的地址。

早期 workaround

关于 eBPF 静态变量支持的讨论在Linux Plumbers Conference 2018 由Cilium提出。早期的 workaround 如下:

#include <linux/bpf.h>

typedef unsigned int __u32;
typedef long long unsigned int __64;

#ifndef __section
# define __section(NAME)				\
	__attribute__((section(NAME), used))
#endif
#ifndef __fetch
# define __fetch(x) (__u32)(__u64)(&(x))
#endif

__u32 foo = 42; 			// .data section
// __u32 foo;   			// .bss section
// const __u32 foo = 42; 	// .rodata section

int __main(struct __sk_buff *skb)
{
    skb->mark = __fetch(foo);
    return 0;
}

char __license[] __section("license") = "";

编译后,foo变量存储在.data section内。

# llvm-readelf -S test.o
There are 8 section headers, starting at offset 0x148:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .strtab           STRTAB          0000000000000000 0000fa 00004b 00      0   0  1
  [ 2] .text             PROGBITS        0000000000000000 000040 000028 00  AX  0   0  8
  [ 3] .rel.text         REL             0000000000000000 0000e8 000010 10      7   2  8
  [ 4] .data             PROGBITS        0000000000000000 000068 000004 00  WA  0   0  4
  [ 5] license           PROGBITS        0000000000000000 00006c 000001 00  WA  0   0  1
  [ 6] .llvm_addrsig     LLVM_ADDRSIG    0000000000000000 0000f8 000002 00   E  7   0  1
  [ 7] .symtab           SYMTAB          0000000000000000 000070 000078 18      1   2  8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

.rel.text section中也存在foo变量的relocation信息: