第 8 章. IPv6 内部

8.1. IPv6/IPsec 实现

本节应解释 IPv6 和 IPsec 相关的实现内部。这些功能源自 KAME 项目

8.1.1. IPv6

8.1.1.1. 符合性

与 IPv6 相关的功能符合或试图符合最新的 IPv6 规范集。为了便于将来参考,我们在下面列出了一些相关文档(注意:这不是完整列表 - 这太难维护了……)。

有关详细信息,请参阅文档中的特定章节、RFC、手册页或源代码中的注释。

符合性测试已在 TAHI 项目的 KAME STABLE 套件上进行。结果可在 http://www.tahi.org/report/KAME/ 查看。我们过去也参加了新罕布什尔大学 IOL 测试 (http://www.iol.unh.edu/),使用我们过去的快照。

  • RFC1639: FTP 操作通过大地址记录 (FOOBAR)

    • RFC2428 比 RFC1639 更受欢迎。FTP 客户端将首先尝试 RFC2428,如果失败则尝试 RFC1639。

  • RFC1886: 支持 IPv6 的 DNS 扩展

  • RFC1933: IPv6 主机和路由器的过渡机制

    • 不支持 IPv4 兼容地址。

    • 不支持自动隧道(在 RFC 的 4.3 中描述)。

    • gif(4) 接口以通用方式实现 IPv[46]-over-IPv[46] 隧道,它涵盖规范中描述的“配置隧道”。有关详细信息,请参见本文档中的 23.5.1.5

  • RFC1981: IPv6 的路径 MTU 发现

  • RFC2080: IPv6 的 RIPng

    • usr.sbin/route6d 支持此功能。

  • RFC2292: IPv6 的高级套接字 API

    • 有关支持的库函数/内核 API,请参见 sys/netinet6/ADVAPI.

  • RFC2362: 协议无关组播 - 稀疏模式 (PIM-SM)

    • RFC2362 定义了 PIM-SM 的数据包格式。draft-ietf-pim-ipv6-01.txt 是基于此编写的。

  • RFC2373: IPv6 地址架构

    • 支持节点必需地址,并符合范围要求。

  • RFC2374: IPv6 可聚合全局单播地址格式

    • 支持 64 位长度的接口 ID。

  • RFC2375: IPv6 组播地址分配

    • 用户空间应用程序使用 RFC 中分配的知名地址。

  • RFC2428: IPv6 和 NAT 的 FTP 扩展

    • RFC2428 比 RFC1639 更受欢迎。FTP 客户端将首先尝试 RFC2428,如果失败则尝试 RFC1639。

  • RFC2460: IPv6 规范

  • RFC2461: IPv6 的邻居发现

    • 有关详细信息,请参见本文档中的 23.5.1.2

  • RFC2462: IPv6 无状态地址自动配置

    • 有关详细信息,请参见本文档中的 23.5.1.4

  • RFC2463: IPv6 规范的 ICMPv6

    • 有关详细信息,请参见本文档中的 23.5.1.9

  • RFC2464: 以太网网络上的 IPv6 数据包传输

  • RFC2465: IPv6 的 MIB:文本约定和一般组

    • 内核会收集必要的统计信息。实际的 IPv6 MIB 支持作为 ucd-snmp 的补丁包提供。

  • RFC2466: IPv6 的 MIB:ICMPv6 组

    • 内核会收集必要的统计信息。实际的 IPv6 MIB 支持作为 ucd-snmp 的补丁包提供。

  • RFC2467: FDDI 网络上的 IPv6 数据包传输

  • RFC2497: ARCnet 网络上的 IPv6 数据包传输

  • RFC2553: IPv6 的基本套接字接口扩展

    • 支持 IPv4 映射地址 (3.7) 和 IPv6 通配符绑定套接字 (3.8) 的特殊行为。有关详细信息,请参见本文档中的 23.5.1.12

  • RFC2675: IPv6 巨型帧

    • 有关详细信息,请参见本文档中的 23.5.1.7

  • RFC2710: IPv6 的组播监听器发现

  • RFC2711: IPv6 路由器警报选项

  • draft-ietf-ipngwg-router-renum-08: IPv6 的路由器重编号

  • draft-ietf-ipngwg-icmp-namelookups-02: 通过 ICMP 进行 IPv6 名称查找

  • draft-ietf-ipngwg-icmp-name-lookups-03: 通过 ICMP 进行 IPv6 名称查找

  • draft-ietf-pim-ipv6-01.txt: IPv6 的 PIM

  • draft-itojun-ipv6-tcp-to-anycast-00: 断开到 IPv6 任意播地址的 TCP 连接

  • draft-yamamoto-wideipv6-comm-model-00

    • 有关详细信息,请参见本文档中的 23.5.1.6

  • draft-ietf-ipngwg-scopedaddr-format-00.txt: IPv6 范围地址格式的扩展

8.1.1.2. 邻居发现

邻居发现相当稳定。目前支持地址解析、重复地址检测和邻居不可达检测。在不久的将来,我们将在内核中添加代理邻居通告支持,以及作为管理工具的未经请求邻居通告传输命令。

如果 DAD 失败,该地址将被标记为“重复”,并将向 syslog(通常也向控制台)生成消息。可以通过 ifconfig(8) 检查“重复”标记。管理员有责任检查和恢复 DAD 失败。该行为将在不久的将来得到改进。

一些网络驱动程序会将组播数据包循环回自身,即使被指示不要这样做(尤其是在混杂模式下)。在这种情况下,DAD 可能会失败,因为 DAD 引擎会看到入站 NS 数据包(实际上来自节点本身)并将其视为重复的标志。您可以查看 sys/netinet6/nd6_nbr.c:nd6_dad_timer() 中标记为“启发式”的 #if 条件作为解决方法(请注意,“启发式”部分中的代码片段不符合规范)。

邻居发现规范 (RFC2461) 没有讨论以下情况下的邻居缓存处理

  1. 当没有邻居缓存条目时,节点在没有链路层地址的情况下收到未经请求的 RS/NS/NA/重定向数据包

  2. 没有链路层地址的介质上的邻居缓存处理(我们需要一个邻居缓存条目来获取 IsRouter 位)

对于第一种情况,我们基于 IETF ipngwg 邮件列表上的讨论实施了解决方案。有关详细信息,请参见源代码中的注释以及从(IPng 7155)开始的电子邮件线程,日期为 1999 年 2 月 6 日。

IPv6 的链路内确定规则 (RFC2461) 与 BSD 网络代码中的假设有很大不同。目前,不支持在默认路由器列表为空的情况下进行链路内确定规则(RFC2461,第 5.2 节,第二段中的最后一句话 - 请注意,该规范在该节中错误地使用了“主机”和“节点”这两个词)。

为了避免可能的 DoS 攻击和无限循环,现在只接受 ND 数据包上的 10 个选项。因此,如果您在 RA 上附加了 20 个前缀选项,则只有前 10 个前缀会被识别。如果您对此感到困扰,请在 FREEBSD-CURRENT 邮件列表中询问或修改 sys/netinet6/nd6.c 中的 nd6_maxndopt。如果有高需求,我们可能会为该变量提供 sysctl 旋钮。

8.1.1.3. 范围索引

IPv6 使用范围地址。因此,使用 IPv6 地址指定范围索引(对于链路本地地址,为接口索引;对于站点本地地址,为站点索引)非常重要。如果没有范围索引,范围内的 IPv6 地址对内核来说是模棱两可的,内核将无法确定数据包的出站接口。

普通用户空间应用程序应该使用高级 API (RFC2292) 来指定范围索引或接口索引。出于类似目的,sockaddr_in6 结构中的 sin6_scope_id 成员在 RFC2553 中定义。但是,sin6_scope_id 的语义相当含糊。如果您关心应用程序的可移植性,我们建议您使用高级 API 而不是 sin6_scope_id。

在内核中,链路本地范围地址的接口索引被嵌入到 IPv6 地址中的第二个 16 位字(第三和第四个字节)中。例如,您可能会看到类似以下内容

	fe80:1::200:f8ff:fe01:6317

在路由表和接口地址结构(struct in6_ifaddr)中。上面的地址是一个链接本地单播地址,属于接口标识符为 1 的网络接口。嵌入式索引使我们能够有效地识别跨多个接口的 IPv6 链接本地地址,并且只需少量代码更改。

路由守护进程和配置程序,如 route6d(8)ifconfig(8),需要操作“嵌入式”范围索引。这些程序使用路由套接字和 ioctl(如 SIOCGIFADDR_IN6),内核 API 将返回带有填充的第二个 16 位字的 IPv6 地址。这些 API 用于操作内核内部结构。使用这些 API 的程序必须做好应对内核差异的准备。

在命令行中指定范围地址时,切勿写入嵌入式形式(例如 ff02:1::1 或 fe80:2::fedc)。这不能正常工作。始终使用标准形式,例如 ff02::1 或 fe80::fedc,并使用命令行选项指定接口(例如 ping -6 -I ne0 ff02::1)。一般来说,如果命令没有命令行选项来指定传出接口,那么该命令还没有准备好接受范围地址。这似乎与 IPv6 支持“牙医诊所”情况的前提相反。我们认为规范需要针对此进行一些改进。

一些用户空间工具支持扩展的数字 IPv6 语法,如 draft-ietf-ipngwg-scopedaddr-format-00.txt 中所述。可以使用传出接口的名称(如“fe80::1%ne0”)来指定传出链接。这样就可以在不麻烦的情况下指定链接本地范围地址。

要在您的程序中使用此扩展,您需要使用 getaddrinfo(3)getnameinfo(3) 以及 NI_WITHSCOPEID。当前的实现假设链接和接口之间存在一对一关系,这比规范中规定的更严格。

8.1.1.4. 即插即用

大多数 IPv6 无状态地址自动配置是在内核中实现的。邻居发现功能在整个内核中实现。主机上的路由器通告 (RA) 输入在内核中实现。最终主机上的路由器请求 (RS) 输出、路由器上的 RS 输入和路由器上的 RA 输出在用户空间实现。

IPv6 链接本地地址从 IEEE802 地址(以太网 MAC 地址)生成。当接口变为活动状态 (IFF_UP) 时,每个接口都会自动分配一个 IPv6 链接本地地址。此外,将为链接本地地址添加路由表中的直接路由。

以下是 netstat 命令的输出

Internet6:
Destination                   Gateway                   Flags      Netif Expire
fe80:1::%ed0/64               link#1                    UC          ed0
fe80:2::%ep0/64               link#2                    UC          ep0

没有 IEEE802 地址的接口(伪接口,如隧道接口或 ppp 接口)会在可能的情况下从其他接口(如以太网接口)借用 IEEE802 地址。如果没有连接 IEEE802 硬件,将使用最后一个手段伪随机值 MD5(hostname) 作为链接本地地址的来源。如果这并不适合您的使用,则需要手动配置链接本地地址。

如果接口无法处理 IPv6(例如缺少多播支持),则不会将链接本地地址分配给该接口。有关详细信息,请参阅第 2 节。

每个接口都加入了请求的多播地址和链接本地所有节点多播地址(例如,接口连接的链接上的 fe80::1:ff01:6317 和 ff02::1)。除了链接本地地址之外,回环地址 (::1) 还将分配给回环接口。此外,::1/128 和 ff01::/32 会自动添加到路由表中,回环接口加入节点本地多播组 ff01::1。

8.1.1.4.2. 主机上的无状态地址自动配置

在 IPv6 规范中,节点分为两类:路由器主机。路由器转发发送到其他节点的包,主机不转发包。net.inet6.ip6.forwarding 定义了此节点是路由器还是主机(如果为 1 则为路由器,如果为 0 则为主机)。

当主机从路由器收到路由器通告时,主机可以通过无状态地址自动配置来自动配置自身。此行为可以通过 net.inet6.ip6.accept_rtadv 控制(如果设置为 1,主机会自动配置自身)。通过自动配置,将添加接收接口的网络地址前缀(通常是全局地址前缀)。还会配置默认路由。路由器会定期生成路由器通告包。要请求相邻路由器生成 RA 包,主机可以发送路由器请求。要随时生成 RS 包,请使用 rtsol 命令。 rtsold(8) 守护进程也可以使用。 rtsold(8) 会在需要时生成路由器请求,非常适合移动使用(笔记本电脑/笔记本电脑)。如果希望忽略路由器通告,请使用 sysctl 将 net.inet6.ip6.accept_rtadv 设置为 0。

要从路由器生成路由器通告,请使用 rtadvd(8) 守护进程。

请注意,IPv6 规范假设以下内容,并且未指定不符合情况

  • 只有主机才会监听路由器通告

  • 主机只有一个网络接口(回环接口除外)

因此,在路由器或多接口主机上启用 net.inet6.ip6.accept_rtadv 不明智。配置错误的节点可能会出现异常行为(允许进行不符合配置,以便进行一些实验)。

总结 sysctl 旋钮

	accept_rtadv	forwarding	role of the node
	---		---		---
	0		0		host (to be manually configured)
	0		1		router
	1		0		autoconfigured host
					(spec assumes that host has single
					interface only, autoconfigured host
					with multiple interface is
					out-of-scope)
	1		1		invalid, or experimental
					(out-of-scope of spec)

RFC2462 在 5.5.3 (e) 中包含针对传入 RA 前缀信息选项的验证规则。这是为了保护主机免受发布非常短前缀生命周期的恶意(或配置错误)路由器的影响。Jim Bound 针对 ipngwg 邮件列表进行了更新(在档案中查找“(ipng 6712)”),并实现了 Jim 的更新。

有关 DAD 和自动配置之间的关系,请参阅文档中的 23.5.1.2

8.1.1.5. 通用隧道接口

GIF(通用接口)是为已配置隧道设置的伪接口。详细信息在 gif(4) 中描述。目前

  • v6 in v6

  • v6 in v4

  • v4 in v6

  • v4 in v4

可用。使用 gifconfig(8) 为 gif 接口分配物理(外部)源地址和目标地址。使用内外部 IP 标头相同地址族(v4 in v4 或 v6 in v6)的配置存在危险。配置接口和路由表以执行无限级隧道非常容易。请注意

gif 可以配置为支持 ECN。有关隧道的 ECN 支持,请参阅 23.5.4.5,有关配置方法,请参阅 gif(4)

如果要使用 gif 接口配置 IPv4-in-IPv6 隧道,请仔细阅读 gif(4)。您需要删除自动分配给 gif 接口的 IPv6 链接本地地址。

8.1.1.6. 源地址选择

当前的源地址选择规则是面向范围的(有一些例外情况 - 如下所示)。对于给定的目标地址,源 IPv6 地址通过以下规则选择

  1. 如果用户显式指定源地址(例如,通过高级 API),则使用指定的地址。

  2. 如果为传出接口分配了具有与目标地址相同范围的地址(通常通过查找路由表确定),则使用该地址。

    这是最典型的情况。

  3. 如果没有满足上述条件的地址,请选择分配给发送节点上其中一个接口的全局地址。

  4. 如果没有满足上述条件的地址,并且目标地址是站点本地范围,请选择分配给发送节点上其中一个接口的站点本地地址。

  5. 如果没有满足上述条件的地址,请选择与目标地址的路由表条目关联的地址。这是最后的解决办法,可能会导致范围违规。

例如,::1 用于 ff01::1,fe80:1::200:f8ff:fe01:6317 用于 fe80:1::2a0:24ff:feab:839b(请注意,嵌入式接口索引 - 在 23.5.1.3 中描述 - 有助于我们选择正确的源地址。这些嵌入式索引不会出现在线路上)。如果传出接口对范围有多个地址,则根据最长匹配原则选择源地址(规则 3)。假设 2001:0DB8:808:1:200:f8ff:fe01:6317 和 2001:0DB8:9:124:200:f8ff:fe01:6317 被分配给传出接口。对于目标地址 2001:0DB8:800::1,选择 2001:0DB8:808:1:200:f8ff:fe01:6317 作为源地址。

请注意,上述规则没有在 IPv6 规范中记录。这被认为是“取决于实现”的内容。在某些情况下,我们不使用上述规则。一个示例是已连接的 TCP 会话,我们使用 tcb 中保存的地址作为源地址。另一个示例是邻居通告的源地址。根据规范(RFC2461 7.2.2),NA 的源地址应为相应 NS 的目标地址。在这种情况下,我们遵循规范而不是上述最长匹配规则。

对于新连接(规则 1 不适用时),如果存在其他选择,则不会选择已弃用地址(首选生命周期 = 0 的地址)作为源地址。如果没有其他选择,则将使用已弃用地址作为最后的解决办法。如果有多个已弃用地址可供选择,则将使用上述范围规则从这些已弃用地址中进行选择。如果由于某些原因需要禁止使用已弃用地址,请将 net.inet6.ip6.use_deprecated 配置为 0。与已弃用地址相关的议题在 RFC2462 5.5.4 中进行了描述(注意:IETF ipngwg 目前正在讨论如何使用“已弃用”地址)。

8.1.1.7. 超大型有效负载

Jumbo Payload 的逐跳选项已实现,可用于发送有效负载长度超过 65,535 字节的 IPv6 数据包。但目前不支持 MTU 超过 65,535 的物理接口,因此此类有效负载只能在环回接口(即 lo0)上看到。

如果您想尝试 Jumbo Payload,首先需要重新配置内核,使环回接口的 MTU 超过 65,535 字节;在内核配置文件中添加以下内容

options "LARGE_LOMTU" # 用于测试 Jumbo Payload

并重新编译新内核。

然后,您可以使用 ping(8) 命令以及 -6、-b 和 -s 选项测试 Jumbo Payload。必须指定 -b 选项来扩大套接字缓冲区的大小,而 -s 选项指定数据包的长度,该长度应大于 65,535。例如,输入以下内容

% ping -6 -b 70000 -s 68000 ::1

IPv6 规范要求 Jumbo Payload 选项不得用于携带碎片报头的分组。如果违反了此条件,则必须向发送方发送 ICMPv6 参数问题消息。虽然规范得到了遵循,但通常您看不到由此要求导致的 ICMPv6 错误。

当接收到 IPv6 数据包时,会检查帧长度并将其与 IPv6 报头中的有效负载长度字段中指定的长度或 Jumbo Payload 选项的值(如果有)进行比较。如果前者短于后者,则丢弃数据包并增加统计信息。您可以看到使用 `-s -p ip6` 选项的 netstat(8) 命令的输出中的统计信息。

% netstat -s -p ip6
	  ip6:
		(snip)
		1 with data size < data length

因此,内核不会发送 ICMPv6 错误,除非错误数据包是实际的 Jumbo Payload,即其数据包大小大于 65,535 字节。如上所述,目前不支持具有如此巨大 MTU 的物理接口,因此它很少返回 ICMPv6 错误。

目前不支持 Jumbo 协议上的 TCP/UDP。这是因为我们没有介质(除了环回)来测试它。如果您需要,请联系我们。

IPsec 不适用于 Jumbo 协议。这是因为在支持 Jumbo 协议中的 AH 时存在一些规范上的问题(AH 报头大小会影响有效负载长度,这使得使用 Jumbo Payload 选项以及 AH 来验证入站数据包变得非常困难)。

*BSD 对 Jumbo 协议的支持存在一些基本问题。我们希望解决这些问题,但需要更多时间来最终确定它们。举几个例子

  • mbuf pkthdr.len 字段在 4.4BSD 中被类型化为 "int",因此在 32 位架构 CPU 上,它无法容纳长度大于 2G 的 Jumbo 协议。如果我们要正确支持 Jumbo 协议,则该字段必须扩展以容纳 4G + IPv6 报头 + 链路层报头。因此,它必须扩展到至少 int64_t(u_int32_t 不足以)。

  • 我们在许多地方错误地使用 "int" 来保存数据包长度。我们需要将它们转换为更大的整数类型。这需要小心,因为我们在计算数据包长度时可能会遇到溢出。

  • 我们在各种地方错误地检查 IPv6 报头的 ip6_plen 字段以获取数据包有效负载长度。我们应该检查 mbuf pkthdr.len 而不是。ip6_input() 将在输入时对 Jumbo Payload 选项执行完整性检查,之后我们可以安全地使用 mbuf pkthdr.len。

  • 当然,TCP 代码需要在许多地方进行仔细的更新。

8.1.1.8. 报头处理中的循环预防

IPv6 规范允许将任意数量的扩展报头放置到数据包中。如果我们以 BSD IPv4 代码实现的方式来实现 IPv6 数据包处理代码,则由于长函数调用链,内核堆栈可能会溢出。sys/netinet6 代码经过精心设计以避免内核堆栈溢出,因此 sys/netinet6 代码定义了自己的协议切换结构,称为 "struct ip6protosw"(参见 netinet6/ip6protosw.h)。出于兼容性考虑,IPv4 部分(sys/netinet)没有进行此类更新,但在其 pr_input() 原型中添加了少量更改。因此,也定义了 "struct ipprotosw"。结果,如果您收到带有大量 IPsec 报头的 IPsec-over-IPv4 数据包,内核堆栈可能会崩溃。IPsec-over-IPv6 则可以正常工作。(当然,为了处理所有这些 IPsec 报头,每个 IPsec 报头都必须通过每个 IPsec 检查。因此,匿名攻击者将无法进行此类攻击。)

8.1.1.9. ICMPv6

在 RFC2463 发布后,IETF ipngwg 决定禁止针对 ICMPv6 重定向的 ICMPv6 错误数据包,以防止网络介质上的 ICMPv6 风暴。这已在内核中实现。

8.1.1.10. 应用程序

对于用户空间编程,我们支持 RFC2553、RFC2292 和即将推出的互联网草案中指定的 IPv6 套接字 API。

IPv6 上的 TCP/UDP 可用且相当稳定。您可以使用 telnet(1)ftp(1)rlogin(1)rsh(1)ssh(1) 等。这些应用程序与协议无关。也就是说,它们会根据 DNS 自动选择 IPv4 或 IPv6。

8.1.1.11. 内核内部结构

虽然 ip_forward() 调用 ip_output(),但 ip6_forward() 直接调用 if_output(),因为路由器不得将 IPv6 数据包分成碎片。

ICMPv6 应尽可能长时间地包含原始数据包,最长可达 1280 字节。例如,UDP6/IP6 端口不可达应该包含所有扩展报头以及未更改的 UDP6 和 IP6 报头。因此,除了 TCP 之外的所有 IP6 函数都不会将网络字节序转换为主机字节序,以保存原始数据包。

tcp_input()、udp6_input() 和 icmp6_input() 无法假设 IP6 报头位于传输报头之前,因为存在扩展报头。因此,in6_cksum() 被实现来处理 IP6 报头和传输报头不连续的数据包。TCP/IP6 或 UDP6/IP6 报头结构不存在用于校验和计算。

为了轻松处理 IP6 报头、扩展报头和传输报头,网络驱动程序现在需要将数据包存储在一个内部 mbuf 或一个或多个外部 mbuf 中。一个典型的旧驱动程序会为 96-204 字节数据准备两个内部 mbuf,但是现在此类数据包数据存储在一个外部 mbuf 中。

netstat -s -p ip6 会告诉您您的驱动程序是否符合此要求。在以下示例中,"cce0" 违反了要求。(有关更多信息,请参阅第 2 节。)

Mbuf statistics:
                317 one mbuf
                two or more mbuf::
                        lo0 = 8
			cce0 = 10
                3282 one ext mbuf
                0 two or more ext mbuf

每个输入函数在开始时都会调用 IP6_EXTHDR_CHECK 来检查 IP6 与其报头之间的区域是否连续。IP6_EXTHDR_CHECK 仅当 mbuf 具有 M_LOOP 标志时才会调用 m_pullup(),即数据包来自环回接口。对于来自物理网络接口的数据包,永远不会调用 m_pullup()。

IP 和 IP6 重新组装函数都不会调用 m_pullup()。

8.1.1.12. IPv4 映射地址和 IPv6 通配符套接字

RFC2553 描述了 IPv4 映射地址 (3.7) 和 IPv6 通配符绑定套接字的特殊行为 (3.8)。该规范允许您

  • 通过 AF_INET6 通配符绑定套接字接受 IPv4 连接。

  • 通过使用地址的特殊形式(如 ::ffff:10.1.1.1)在 AF_INET6 套接字上发送 IPv4 数据包。

但该规范本身非常复杂,并没有指定套接字层应该如何表现。在这里,我们将前者称为 "监听端",将后者称为 "发起端",以便于参考。

您可以在同一个端口上对两个地址族执行通配符绑定。

下表显示了 FreeBSD 4.x 的行为。

listening side          initiating side
                (AF_INET6 wildcard      (connection to ::ffff:10.1.1.1)
                socket gets IPv4 conn.)
                ---                     ---
FreeBSD 4.x     configurable            supported
                default: enabled

以下部分将提供更多详细信息以及如何配置行为。

关于监听端

RFC2553 对通配符绑定问题,尤其是端口空间问题、故障模式以及 AF_INET/INET6 通配符绑定之间的关系的讨论过于简略。对于此 RFC,可能存在几种不同的解释,它们符合 RFC,但行为却有所不同。因此,为了实现可移植的应用程序,您应该假设内核的行为不确定。使用 getaddrinfo(3) 是最安全的方式。端口号空间问题和通配符绑定问题在 1999 年 3 月中旬的 ipv6imp 邮件列表中进行了详细讨论,看起来没有达成明确的共识(这意味着,要由实现者决定)。您可能需要检查邮件列表存档。

如果服务器应用程序希望接受 IPv4 和 IPv6 连接,则会有两种选择。

一种是使用 AF_INET 和 AF_INET6 套接字(您需要两个套接字)。使用 getaddrinfo(3) 将 AI_PASSIVE 设置为 ai_flags,以及 socket(2)bind(2) 来绑定返回的所有地址。通过打开多个套接字,您可以使用适当的地址族在套接字上接受连接。IPv4 连接将由 AF_INET 套接字接受,而 IPv6 连接将由 AF_INET6 套接字接受。

另一种方法是使用一个 AF_INET6 通配符绑定套接字。使用 getaddrinfo(3) 将 AI_PASSIVE 设置为 ai_flags,将 AF_INET6 设置为 ai_family,并将第一个参数主机名设置为 NULL。然后使用 socket(2)bind(2) 绑定返回的地址。(应该是 IPv6 未指定地址)。您可以通过此单个套接字接受 IPv4 和 IPv6 数据包。

为了便携地支持 AF_INET6 通配符绑定套接字上的 IPv6 流量,在对 AF_INET6 监听套接字进行连接时,始终检查对端地址。如果地址是 IPv4 映射地址,您可能需要拒绝连接。您可以使用 IN6_IS_ADDR_V4MAPPED() 宏来检查此条件。

为了更轻松地解决此问题,有一个系统依赖的 setsockopt(2) 选项 IPV6_BINDV6ONLY,使用方法如下所示。

	int on;

	setsockopt(s, IPPROTO_IPV6, IPV6_BINDV6ONLY,
		   (char *)&on, sizeof (on)) < 0));

当此调用成功时,该套接字只接收 IPv6 数据包。

关于发起端

建议应用程序实现者:为了实现一个可移植的 IPv6 应用程序(可以在多个 IPv6 内核上运行),我们认为以下做法是成功的关键

  • 决不要硬编码 AF_INET 或 AF_INET6。

  • 在整个系统中使用 getaddrinfo(3)getnameinfo(3)。决不要使用 gethostby*()、getaddrby*()、inet_*() 或 getipnodeby*()。(为了轻松地将现有应用程序更新为支持 IPv6,有时 getipnodeby*() 会很有用。但如果可能,请尝试将代码重写为使用 getaddrinfo(3)getnameinfo(3)。)

  • 如果您想连接到目标,请使用 getaddrinfo(3) 并尝试返回的所有目标,就像 telnet(1) 所做的那样。

  • 一些 IPv6 堆栈附带了有问题的 getaddrinfo(3)。将一个最小工作版本与您的应用程序一起发布,并将该版本作为最后的手段使用。

如果您想使用 AF_INET6 套接字进行 IPv4 和 IPv6 出站连接,则需要使用 getipnodebyname(3)。当您想以最少的努力将现有应用程序更新为支持 IPv6 时,可以选择此方法。但请注意,这是一个临时的解决方案,因为 getipnodebyname(3) 本身并不推荐使用,因为它根本无法处理有范围的 IPv6 地址。对于 IPv6 名称解析,getaddrinfo(3) 是首选的 API。因此,当您有时间时,应该将应用程序重写为使用 getaddrinfo(3)

在编写进行出站连接的应用程序时,如果您将 AF_INET 和 AF_INET6 视为完全独立的地址族,则故事会变得简单很多。{set,get}sockopt 问题会变得简单,DNS 问题也会变得简单。我们不建议您依赖 IPv4 映射地址。

8.1.1.12.1. 统一的 tcp 和 inpcb 代码

FreeBSD 4.x 在 IPv4 和 IPv6 之间使用共享的 tcp 代码(来自 sys/netinet/tcp*)和独立的 udp4/6 代码。它使用统一的 inpcb 结构。

该平台可以配置为支持 IPv4 映射地址。内核配置总结如下:

  • 默认情况下,AF_INET6 套接字将在某些情况下获取 IPv4 连接,并且可以发起连接到嵌入在 IPv4 映射 IPv6 地址中的 IPv4 目标。

  • 您可以使用以下 sysctl 在整个系统上禁用它。

    sysctl net.inet6.ip6.mapped_addr=0

8.1.1.12.1.1. 监听方

每个套接字都可以配置为支持特殊的 AF_INET6 通配符绑定(默认情况下启用)。您可以在每个套接字的基础上使用 setsockopt(2) 禁用它,如下所示。

	int on;

	setsockopt(s, IPPROTO_IPV6, IPV6_BINDV6ONLY,
		   (char *)&on, sizeof (on)) < 0));

通配符 AF_INET6 套接字仅当满足以下条件时才会获取 IPv4 连接:

  • 没有匹配 IPv4 连接的 AF_INET 套接字

  • AF_INET6 套接字被配置为接受 IPv4 流量,即 getsockopt(IPV6_BINDV6ONLY) 返回 0。

打开/关闭顺序没有问题。

8.1.1.12.1.2. 发起方

如果节点配置为支持 IPv4 映射地址,FreeBSD 4.x 支持对 IPv4 映射地址 (::ffff:10.1.1.1) 的传出连接。

8.1.1.13. sockaddr_storage

当 RFC2553 即将最终确定时,关于如何命名 struct sockaddr_storage 成员进行了讨论。一项提议是在成员前面加上 ""(例如 "ss_len"),因为它们不应该被触碰。另一项提议是不加前缀(例如 "ss_len"),因为我们需要直接触碰这些成员。对此没有明确的共识。

结果,RFC2553 将 struct sockaddr_storage 定义如下:

	struct sockaddr_storage {
		u_char	__ss_len;	/* address length */
		u_char	__ss_family;	/* address family */
		/* and bunch of padding */
	};

相反,XNET 草案定义如下:

	struct sockaddr_storage {
		u_char	ss_len;		/* address length */
		u_char	ss_family;	/* address family */
		/* and bunch of padding */
	};

1999 年 12 月,人们一致同意 RFC2553bis 应该选择后一种(XNET)定义。

当前实现符合 XNET 定义,基于 RFC2553bis 的讨论。

如果您查看多个 IPv6 实现,您将能够看到这两个定义。作为用户空间程序员,处理它的最便携方式是:

  1. 使用 GNU autoconf 确保 ss_family 和/或 ss_len 在平台上可用。

  2. 有 -Dss_family=ss_family 将所有出现(包括头文件)统一到 ss_family,或者

  3. 不要触碰 __ss_family。强制转换为 sockaddr * 并使用 sa_family,例如:

    	struct sockaddr_storage ss;
    	family = ((struct sockaddr *)&ss)->sa_family

8.1.2. 网络驱动程序

现在,标准驱动程序需要支持以下两项:

  1. mbuf 集群要求。在这个稳定版本中,我们将 MINCLSIZE 更改为 MHLEN+1,以使所有操作系统的驱动程序都能按预期运行。

  2. 组播。如果 ifmcstat(8) 为接口生成没有组播组,则必须修补该接口。

如果任何驱动程序不支持这些要求,则这些驱动程序不能用于 IPv6 和/或 IPsec 通信。如果您发现您的卡在使用 IPv6/IPsec 时遇到任何问题,请将其报告给 FreeBSD 问题报告邮件列表

(注意:过去,我们要求所有 PCMCIA 驱动程序调用 in6_ifattach()。我们不再有这样的要求。)

8.1.3. 翻译器

我们将 IPv4/IPv6 翻译器分为 4 种类型:

  • 翻译器 A --- 它用于过渡的早期阶段,以使 IPv6 岛屿中的 IPv6 主机能够与 IPv4 海洋中的 IPv4 主机建立连接。

  • 翻译器 B --- 它用于过渡的早期阶段,以使 IPv4 海洋中的 IPv4 主机能够与 IPv6 岛屿中的 IPv6 主机建立连接。

  • 翻译器 C --- 它用于过渡的后期阶段,以使 IPv4 岛屿中的 IPv4 主机能够与 IPv6 海洋中的 IPv6 主机建立连接。

  • 翻译器 D --- 它用于过渡的后期阶段,以使 IPv6 海洋中的 IPv6 主机能够与 IPv4 岛屿中的 IPv4 主机建立连接。

8.1.4. IPsec

IPsec 主要由三个组件组成。

  1. 策略管理

  2. 密钥管理

  3. AH 和 ESP 处理

8.1.4.1. 策略管理

内核实现了实验性的策略管理代码。有两种方法可以管理安全策略。一种是使用 setsockopt(2) 配置每个套接字的策略。在这种情况下,策略配置在 ipsec_set_policy(3) 中描述。另一种是使用 PF_KEY 接口,通过 setkey(8) 配置基于内核数据包过滤器的策略。

策略条目不会按其索引重新排序,因此添加条目时的顺序非常重要。

8.1.4.2. 密钥管理

此套件(sys/netkey)中实现的密钥管理代码是自制的 PFKEY v2 实现。这符合 RFC2367。

自制的 IKE 守护程序 "racoon" 包含在套件中(kame/kame/racoon)。基本上,您需要以守护程序身份运行 racoon,然后设置一个策略来要求密钥(例如 ping -P 'out ipsec esp/transport//use')。内核将在需要时与 racoon 守护程序联系以交换密钥。

8.1.4.3. AH 和 ESP 处理

IPsec 模块被实现为标准 IPv4/IPv6 处理的 "钩子"。在发送数据包时,ip{,6}_output() 会检查是否需要 ESP/AH 处理,方法是检查是否找到了匹配的 SPD(安全策略数据库)。如果需要 ESP/AH,将调用 {esp,ah}{4,6}_output() 并且会相应地更新 mbuf。当接收到数据包时,将根据协议号调用 {esp,ah}4_input(),即 (*inetsw[proto])()。{esp,ah}4_input() 将对数据包进行解密/验证其真实性,并剥离 ESP/AH 的串联头和填充。在接收数据包时剥离 ESP/AH 头是安全的,因为我们永远不会以 "按原样" 形式使用接收到的数据包。

通过使用 ESP/AH,TCP4/6 有效数据段大小将受到 ESP/AH 插入的额外串联头的影响。我们的代码处理这种情况。

基本密码函数可以在目录 "sys/crypto" 中找到。ESP/AH 变换列在 {esp,ah}_core.c 中,带有包装函数。如果您想添加一些算法,请在 {esp,ah}_core.c 中添加包装函数,并将您的密码算法代码添加到 sys/crypto 中。

隧道模式在这个版本中部分支持,具有以下限制:

  • IPsec 隧道没有与 GIF 通用隧道接口结合。这需要谨慎,因为我们可能会在 ip_output() 和 tunnelifp→if_output() 之间创建一个无限循环。关于是否最好将它们统一起来,意见不一。

  • MTU 和 Don’t Fragment 位(IPv4)的考虑需要更多检查,但基本上运行良好。

  • AH 隧道的身份验证模型必须重新考虑。我们最终需要改进策略管理引擎。

8.1.4.4. 符合 RFC 和 ID

内核中的 IPsec 代码符合(或者,试图符合)以下标准:

"旧 IPsec" 规范记录在 rfc182[5-9].txt

"新 IPsec" 规范记录在 rfc240[1-6].txtrfc241[01].txtrfc2451.txtdraft-mcdonald-simple-ipsec-api-01.txt(草案已过期,但您可以从 ftp://ftp.kame.net/pub/internet-drafts/ 获取)。(注意:IKE 规范,rfc241[7-9].txt 在用户空间实现,作为 "racoon" IKE 守护程序)

当前支持的算法是:

  • 旧 IPsec AH

    • 空密码校验和(无文档,仅用于调试)

    • 带 128 位密码校验和的带密钥 MD5 (rfc1828.txt)

    • 带 128 位密码校验和的带密钥 SHA1(无文档)

    • 带 128 位密码校验和的 HMAC MD5 (rfc2085.txt)

    • 带 128 位密码校验和的 HMAC SHA1(无文档)

  • 旧 IPsec ESP

    • 空加密(无文档,类似于 rfc2410.txt

    • DES-CBC 模式 (rfc1829.txt)

  • 新 IPsec AH

    • 空密码校验和(无文档,仅用于调试)

    • 带 96 位密码校验和的带密钥 MD5(无文档)

    • 带 96 位密码校验和的带密钥 SHA1(无文档)

    • 带 96 位密码校验和的 HMAC MD5 (rfc2403.txt)

    • 带 96 位密码校验和的 HMAC SHA1 (rfc2404.txt)

  • 新 IPsec ESP

    • 空加密 (rfc2410.txt)

    • 带导出 IV 的 DES-CBC (draft-ietf-ipsec-ciph-des-derived-01.txt,草案已过期)

    • 带显式 IV 的 DES-CBC (rfc2405.txt)

    • 带显式 IV 的 3DES-CBC (rfc2451.txt)

    • BLOWFISH CBC (rfc2451.txt)

    • CAST128 CBC (rfc2451.txt)

    • RC5 CBC (rfc2451.txt)

    • 以上每个都可以与以下结合:

      • 带 HMAC-MD5(96 位) 的 ESP 身份验证

      • 带 HMAC-SHA1(96 位) 的 ESP 身份验证

以下算法不受支持:

  • 旧 IPsec AH

    • 带 128 位密码校验和 + 64 位重放保护的 HMAC MD5 (rfc2085.txt)

    • 带 160 位密码校验和 + 32 位填充的带密钥 SHA1 (rfc1852.txt)

IPsec(在内核中)和 IKE(在用户空间作为 "racoon")已在多个互操作性测试活动中进行过测试,已知可以与许多其他实现良好互操作。此外,当前的 IPsec 实现涵盖了 RFC 中记录的 IPsec 密码算法的相当广泛范围(我们只涵盖没有知识产权问题的算法)。

8.1.4.5. IPsec 隧道上的 ECN 考虑

draft-ipsec-ecn-00.txt 中所述,支持 ECN 友好的 IPsec 隧道。

RFC2401 中描述了正常的 IPsec 隧道。在封装时,IPv4 TOS 字段(或 IPv6 流量类别字段)将从内部 IP 头复制到外部 IP 头。在解封装时,外部 IP 头将被简单地丢弃。解封装规则与 ECN 不兼容,因为外部 IP TOS/流量类别字段上的 ECN 位将丢失。

为了使 IPsec 隧道 ECN 友好,我们应该修改封装和解封装过程。这在 http://www.aciri.org/floyd/papers/draft-ipsec-ecn-00.txt 的第 3 章中进行了描述。

通过将 net.inet.ipsec.ecn(或 net.inet6.ipsec6.ecn)设置为某个值,IPsec 隧道实现可以提供三种行为:

  • RFC2401:不考虑 ECN(sysctl 值 -1)

  • ECN 被禁止(sysctl 值 0)

  • ECN 被允许(sysctl 值 1)

请注意,行为是在每个节点的方式进行配置的,而不是在每个 SA 的方式进行配置的(draft-ipsec-ecn-00 希望每个 SA 的配置,但对我来说这有点太多了)。

行为总结如下(有关更多详细信息,请参阅源代码):

encapsulate                     decapsulate
                ---                             ---
RFC2401         copy all TOS bits               drop TOS bits on outer
                from inner to outer.            (use inner TOS bits as is)

ECN forbidden   copy TOS bits except for ECN    drop TOS bits on outer
                (masked with 0xfc) from inner   (use inner TOS bits as is)
                to outer.  set ECN bits to 0.

ECN allowed     copy TOS bits except for ECN    use inner TOS bits with some
                CE (masked with 0xfe) from      change.  if outer ECN CE bit
                inner to outer.                 is 1, enable ECN CE bit on
                set ECN CE bit to 0.            the inner.

配置的一般策略如下:

  • 如果 IPsec 隧道的两个端点都能够以 ECN 友好的方式运行,则最好将两个端点都配置为 "ECN 被允许"(sysctl 值 1)。

  • 如果另一端对 TOS 位非常严格,请使用 "RFC2401"(sysctl 值 -1)。

  • 在其他情况下,请使用 "ECN 被禁止"(sysctl 值 0)。

默认行为是 "ECN 被禁止"(sysctl 值 0)。

有关更多信息,请参阅:

http://www.aciri.org/floyd/papers/draft-ipsec-ecn-00.txt、RFC2481(显式拥塞通知)、src/sys/netinet6/{ah,esp}_input.c

(感谢 Kenjiro Cho [email protected] 的详细分析)

8.1.4.6. 互操作性

以下是过去 KAME 代码测试 IPsec/IKE 互操作性的(一些)平台。请注意,两端可能都修改了其实现,因此请将以下列表仅供参考。

Altiga、Ashley-laurent (vpcom.com)、Data Fellows (F-Secure)、爱立信 ACC、FreeS/WAN、日立、IBM AIX®、IIJ、英特尔、微软® Windows NT®、NIST (linux IPsec + plutoplus)、网屏、OpenBSD、RedCreek、Routerware、SSH、Secure Computing、Soliton、东芝、VPNet、雅马哈 RT100i


最后修改时间:2024 年 8 月 11 日 由 Fernando Apesteguía