8.1 IPv6/IPsec 的实现
8.1 IPv6/IPsec的实现
本节解释IPv6和IPsec相关的实现内部。 这些功能衍生于 KAME 工程。
8.1.1 IPv6
8.1.1.1 一致性
IPv6 相关的函数与最新的 IPv6 规格一致或努力保持一致。 为了以后的引用,我们将一些相关文档列在下面 (注释: 这不是一个完整的清单 ── 那将太难于维护……)。
详细信息请参考文档中的特定章节、 RFC、手册页或源代码中的注释。
一致性测试由TAHI工程的KAME STABLE kit进行。 结果可在 http://www.tahi.org/report/KAME/查看。 过去我们也用快照参加新罕布什尔大学互操作性实验室(Univ. of New Hampshire IOL) (http://www.iol.unh.edu/)的测试。
RFC1639: 大地址记录上的FTP操作 (FOOBAR)
RFC2428比RFC1639更受欢迎。FTP客户端将 先尝试RFC2428,如果失败了才尝试RFC1639。
RFC1886: 支持IPv6的DNS扩展
RFC1933: IPv6主机和路由器的过渡机制
RFC1981: IPv6的路径最大传输单元的发现
RFC2080: IPv6的RIPng
usr.sbin/route6d支持此功能。
RFC2292: IPv6的高级套接字应用程序接口(API)
对于已被支持的库函数/内核API,参见 sys/netinet6/ADVAPI。
RFC2362: 协议无关的组播稀疏模式(Protocol Independent Multicast-Sparse Mode, PIM-SM)
RFC2362定义PIM-SM的包格式 draft-ietf-pim-ipv6-01.txt据此写成。
RFC2373: IPv6寻址结构
支持结点要求的地址,与作用域要求一致。
RFC2374: 一种IPv6可聚合全局单播地址格式
支持接口标识的64位长度。
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 近邻发现
近邻发现是相当稳定的。当前,地址解析(Address Resolution)、 重复地址侦测(Duplicated Address Detection), 以及近邻不可到达侦测(Neighbor Unreachability Detection)都得到了支持。 在不久的将来,我们将在内核中加入代理近邻公告(Proxy Neighbor Advertisement)支持,并加入自发近邻公告(Unsolicited Neighbor Advertisement)传输命令管理工具。
如果DAD(重复地址侦测)失败,地址将被标记为“重复”, 会有消息产生至syslog中(并且通常会在控制台上有显示)。 “重复”标记可用ifconfig(8)检查。 检查出DAD失败并从失败中恢复是管理员的职责。 这样的行为应该在不久的将来得到改进。
一些网络驱动器将组播包回环至其自身, 即使已被命令不要这样(尤其在混杂模式)。 在这样DAD可能失败的情况中,DAD引擎可以看见进入的NS包 (实际是从结点自身)并且考虑为表明重复的标志。 你可能需要在查看位于sys/netinet6/nd6_nbr.c:nd6_dad_timer()中 被标记为“heuristics”(试探性的)的#if条件作为这种问题的解释。 (注意在“heuristics”节中的代码片段不与规格一致)。
近邻发现(Neighbor Discovery)规格书(RFC2461)没有谈及 在如下情形中的近邻缓存处理:
在没有近邻缓存条目时 结点自发的 RS/NS/NA/重定向 包, 不含数据链路层地址
介质上的近邻缓存处理,不含数据链路层地址 (我们一个近邻缓存条目给IsRouter位)
对于第一种情形,我们在IETF ipngwg信件列表讨论的基础上实际解决方法。 更多详细情况请参见源代码中的注释和从(IPng 7155,日期1999年2月6日) 开始的信件线索。
IPv6的链路上决定规则(RFC2461)与BSD网络代码的假定有很大不同。 在这时,缺省路同器清单为空时,没有链路上决定规则得到支持 (RFC2461,第5.2节,第2段最后一句 ── 注意规格书这一节中有几处错用 词语“host”(主机)和“node”(结点))。
为避免可能的DoS攻击和无限循环,现在只有ND包中的10个选项被接受。 所以,如果你有20个前缀选项附在RA中,只有前10个前缀会被辨认。 如果这使你遇到麻烦,请在FREEBSD-CURRENT信件列表询问, 和/或修改sys/netinet6/nd6.c中的nd6_maxndop。 如果有大的需求,我们可以为这个变量提供sysctl开关。
8.1.1.3 作用域索引
IPv6使用有作用域的地址。所以对IPv6地址指定作用域索引是非常重要的 (对于链路-本地地址,为接口索引;对于站点-本地地址,为站点索引)。 没有作用域索引,有作用域的IPv6地址对于内核是意义不明确的。 内核将无法确定一个包的向外接口。
普通用户级应用程序应当使用高级应用程序接口(API) (RFC2292)来指定作用域索引,或接口索引。与此目的相同的结构体 sockaddr_in6的成员sin6_scope_id被定义在 RFC2553。然而,sin6_scope_id的语义相当含糊。 如果你要照顾到你的应用程序的移植性, 我们建议你使用高级应用程序接口,而不是sin6_scope_id。
在内核中,一个链路-本地有作用域地址的接口索引被嵌入到 IPv6地址中的第2个16位字(第3、4字节)。 例如,你可以看见
fe80:1::200:f8ff:fe01:6317
位于路由表和接口地址结构体(struct in6_ifaddr)之中。上面的地址是一个链路-本地单播地址, 属于接口标识为1的网络接口。 被嵌入的索引允许我们有效的鉴别在多个接口上的链路-本地地址, 仅凭一点小的代码改动。
路由守护程序和配置程序,像 route6d(8) 和 ifconfig(8),将需要使用“被嵌入的”作用域索引。 这些程序使用路由套接字和ioctl(像 SIOCGIFADDR_IN6),内核应用程序接口将返回填入了第2个16位字的IPv6地址。 应用程序接口用来使用内核内部结构体。 总之,使用这些应用程序接口的程序应被准备好应付各种内核的差异。
当你在命令行指定有作用域地址时,绝不要 写成嵌入形式(例如ff02:1::1或fe80:2::fedc)。这恐怕不行。 请一直使用标准形式,像ff02::1或 fe80::fedc,带上指定接口的命令行选项(像 ping6 -I ne0 ff02::1)。通常, 如果一条命令里没有带上指定外发接口的命令行选项, 那条命令就没有准确接受有作用域地址。 这似乎有悖于IPv6支持“牙科医生办公室”况状的承诺。 我们认为规格书需要对此的改进。
用户级工具中有一些支持扩展数字IPv6语法, 就像记述于文档 draft-ietf-ipngwg-scopedaddr-format-00.txt中的。 你能指定向外的链路,通过使用像“fe80::1%ne0”的外发接口名。 用这种方法你将没有多少麻烦就能指定链路-本地有作用域地址。
为了在你的程序中使用这个扩展,你需要使用 getaddrinfo(3),并使用getnameinfo(3)与NI_WITHSCOPEID。 这些实现目前假定一个链路和一个接口的1对1的关系, 这比规格书说的更强。
8.1.1.4 即插即用
无状态地址自动配置大部分被实现在内核中。 近邻发现函数被实现在内核中,并与内核成为一个整体。 主机的路由器公告(Router Advertisement, RA)输入被实现在内核中。 末端主机的路由器请求(Router Solicitation, RS)输出、 路由器的RS输入和路由器的RA输出被实现在用户级。
8.1.1.4.1 链路-本地和特殊地址的指定
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包, 主机可以发送路由器请求。 使用rtsol命令在任何时刻产生路由器请求包。 守护程序rtsold(8)也是可用的。 rtsold(8)在任何必要的时候产生路由器请求, 这在移动使用时(笔记本/膝上型计算机)工作的很好。 如果希望忽略路由器公告,使用sysctl设置 net.inet6.ip6.accept_rtadv为0。
使用守护程序rtadvd(8)产生路由器公告。
注意,IPv6规格假定如下几条成立, 不一致的情形不被规定:
只有主机会监听路由器公告
主机只有一个网络接口(除了回环)
所以,在路由器或多接口主机上使能net.inet6.ip6.accept_rtadv是不明智的。 一个被错误配置的结点可能行为古怪 (不一致的配置为那些要做某些实验的人们而被允许)。
总结sysctl开关:
accept_rtadv 转发 结点的角色 --- --- --- 0 0 主机 (待手工配置) 0 1 路由器 1 0 被自动配置的主机 (规格假定主机只有一个接口, 有多个接口的自动配置的主机 在作用域外) 1 1 非法,或实验性的 (规格中的“作用域外”)
RFC2462有针对输入路由器公告前缀信息选项的合法性规则, 位于 5.5.3 (e)。这是为了保护主机免受恶意的 (或被错误配置的)以很短的前缀周期公告的路由器的侵害。 有一个来自Jim Bound发给ipngwg信件列表 (在该文档中查找“(ipng 6712)”)的更新, 这些合法性规则在Jim的更新中被实现。
参看本文档23.5.1.2 对DAD(重复地址侦测)和自动配置的关系的描述。
8.1.1.5 通用隧道接口
GIF(通用接口, Generic InterFace)是用于可配置隧道的虚接口。 详细情况被描述在gif(4)。目前有
v6 包裹于 v6
v6 包裹于 v4
v4 包裹于 v6
v4 包裹于 v4
可用。使用gifconfig(8)指派物理(外部)源和目的地址给GIF接口。 source and destination address to gif interfaces. Configuration that 对内部和外部IP头部(v4包裹于v4,或v6包裹于v6)使用相同地址族的配置是危险的。 这很容易将接口和路由表配置为无限级隧道。 请警惕。
GIF可被配置为与ECN匹配。参看23.5.4.5中隧道的ECN匹配问题, 以及gif(4)中的配置方法。
如果你要用GIF接口配置一个IPv4-in-IPv6隧道, 请仔细阅读gif(4)。你将需要自动删除指派给GIF接口的链路-本地IPv6地址。
8.1.1.6 源地址选择
当前源选择规则是面向作用域的(有一些例外──见下文)。 对于一个给定的目的,一个源IPv6地址被按如下规则选择:
如果源地址显式的由用户指定 (例如,通过高级API),则使用被指定地址。
如果有一个地址被指派给与目的地址有着相同作用域的外发接口 (常常通过查找路由表决定),则使用这个被指派的地址。
这是最典型的情形。
如果没有地址满足上述条件, 选择一个指派给发送结点上的一个接口的全局地址。
如果没有地址满足上述条件, 并目的地址有着站点-本地作用域, 选择一个指派给发送结点上的一个接口的站点-本地地址。
如果没有地址满足上述条件, 选择与路由表目的对应入口相关联的地址。
例如,对于ff01::1则::1被选择, 对于fe80:1::2a0:24ff:feab:839b则fe80:1::200:f8ff:fe01:6317选择(注意, 被嵌入的地址索引 ── 描述于23.5.1.3 ── 帮助我们选择正确的源地址。那些被嵌入的索引将不会在线 )。如果外发接口对作用域有多个地址, 地址将会被按最长匹配被选择(规则3)。 假想 2001:0DB8:808:1:200:f8ff:fe01:6317 和 2001:0DB8:9:124:200:f8ff:fe01:6317 被给予外发接口。 2001:0DB8:808:1:200:f8ff:fe01:6317 被选择作为目的 2001:0DB8:800::1 的源。
注意,上述规则并未在IPv6规格书中载明。 而是被考虑为“由实现决定的”条目。有些我们不使用上述规则的情况。 一个例子是已连接的TCP会话, 我们使用保存在传输控制块(TCB)中的地址作为源。 另一个例子是近邻公告(Neighbor Advertisement, NA)的源地址。 在规格书(RFC2461 7.2.2)里NA的源应该是对应的NS目标的目标地址。 在这种情形中,我们依照规格书而不是上述最长匹配规则。
对于新连接(此时规则1不适用),如果有其它选择, 不合适的地址(而是首选 lifetime = 0 的地址)将不被选择为源地址。 如果没有其它选择,不合适的地址将被作为最后的选择。 如果不合适的地址有多个选择, 上述的作用域规则将会被用来从那些不合适的地址中做出选择。 如果你要按照某些理由禁用不合适的地址, 配置net.inet6.ip6.use_deprecated为0。 与不合适的地址相关的话题被描述在 RFC2462 5.5.4 (注意: 对于如何使用“不合适的”地址有一些进行中的争论)。
8.1.1.7 超大有效负载
译者注: 此处英文原文“Jumbo Payload”的原义为“巨大有效负载”。 此处的“有效负载”实指报文承载的用户数据。
超长报文的逐跳(hop-by-hop)选项已被实现,并可用于发送带有长于 65536 个八位字节有效负载的 IPv6 包。但是现在没有物理接口的最大传输单元 (MTU) 大于 65536 个八位字节,所以那样的有效负载只能在回环接口(那是指 lo0)上看见。
如果你需要尝试超长报文,你首先要重新配置内核使得回环接口的 最大传输单元大于 65536 字节;将如下内容添加至内核配置文件:
options "LARGE_LOMTU" # 测试超长报文
并重新编译内核。
然后你可以用命令 ping6(8) 并加 -b 和 -s 选项测试超长报文。 选项 -b 必须被指定以增大套接字缓冲区大小; 选项 -s 指定包的长度,应大小 65535。例如,打如下命令:
% ping6 -b 70000 -s 68000 ::1
IPv6 规格要求超长报文不可用于承载分段头部的包。 如果这个条件被打破,一个 ICMPv6 Parameter Problem (参数问题) 报文 必须被发往发送者。(即使)规格被遵守了, 但是你也可能不会总是看见由那个要求而导致的 ICMPv6 错误。
当收到一个 IPv6 包时,帧长度被检查并与 IPv6 头部中指出的有效负载长度 或超大有效负载选项中的值(如果有的话)相比较。 如果前者短于后者,包就被抛弃,相应的统计计量被增加。 你可以在加上选项`-s -p ip6'的命令 netstat(8) 的输出中看见统计数字:
% netstat -s -p ip6 ip6: (略) 1 with data size < data length
所以,除非出错的包是超长有效负载包(包长度超过 65535 字节) 内核不会发送 ICMPv6 出错包。如上所述, 现在还没有物理接口支持超长的最大传输单元 (MTU), 所以很少会返回 ICMPv6 错误信息。
这样的话,超长报文承载的 TCP/UDP 也没有被支持。 这是因为我们没有介质 (除了回环设备) 来进行测试。 如果你需要这些测试,请与我们联系。
IPsec 不能在超长报文上工作。这是因为一些规格歪曲了超长报文的 AH 支持。(AH 头部大小影响有效负载长度, 这使得鉴别既有 AH 又有超长有效负载选项的输入的包变的很困难。)
对于 *BSD 支持超长报文还有一些基本问题。 我们想解决这些问题,不过我们需要更多的时间来完成。 其中的一些问题罗列如下:
mbuf 的域 pkthdr.len 在 4.4BSD 中被定义为“int”, 所以在32位系统结构的CPU上 mbuf 不会承载 len > 2G 的超长报文。 如果我们想要支持超长报文,域 pkthdr.len 必须被扩展以适应 4G + IPv6 头部 + 数据链路层头部。 所以,该域至少被扩展为 int64_t (u_int32_t 是不够的)。
我们在许多地方错误的使用“int”保存包长度。 我们需要把它们转换为更大的整数类型。 在计算包长度时我们可能遇到溢出,这时需要非常小心。
在许多地方我们错误的检查 IPv6 头部的域 ip6_plen 以获知包的有效负载。 相反,我们应该检查 mbuf 的 pkthdr.len 。 ip6_input() 会完善的检查输入中的超长有效负载选项, 在这之后我们就可以安全的使用 mbuf 的 pkthdr.len 了。
当然, TCP 代码的很多地方需要更新。
8.1.1.8 头部处理过程中的防止死循环(loop)
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 应用程序
对于用户级程序的编程,我们提供 IPv6 套接字应用程序接口的支持, 正如 RFC2553、RFC2292 和将要发布的草案 (Internet drafts) 中规定的那样。
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 存储包。 一个典型的旧驱动程序准备两个内部 mbufs 以存储 96 - 204 字节的数据, 然而,现在这样的包数据被存储在一个外部 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 的行为。
监听方 初始化方 (AF_INET6 通配套接字 (连接到 ::ffff:10.1.1.1) 获得 IPv4 连接) --- --- FreeBSD 4.x 可配置缺省:已被允许 已被支持
随后几节将给你更多的详细信息,并告诉你如何配置那些行为。
对于监听方的注释:
似乎 RFC2553 太少提及通配绑定的问题, 尤其是端口空间问题、失败模式和 AF_INET/INET6 通配绑定的关系。 对于这个 RFC ,有几种不同的相符解释,但这些解释却有着不同的行为。 所以,为了实现可执行的应用程序,你不能想当然的假定内核中有关这些行为的一切。 使用 getaddrinfo(3) 是最安全的方式。 端口号空间和通配绑定问题于 1999 年三月中旬在 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),并设置第一个参数 hostname 为 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 目的地,该这个目的地址嵌在 IPv4 映射的 IPv6 地址中。
你可以用下述的 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 连接当且仅当 下列条件被满足:
没有 AF_INET 套接字匹配 IPv4 连接
AF_INET6 套接字被配置接受 IPv4 通信,这是指 getsockopt(IPV6_BINDV6ONLY) 返回 0。
打开/关闭 的顺序不会引起问题。
8.1.1.12.1.2 初始化方
FreeBSD 4.x 支持向外的连接到 IPv4 映射地址 (::ffff:10.1.1.1),前提是结点被配置为支持 IPv4 映射地址。
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; /* 地址长度 */ u_char __ss_family; /* 地址族 */ /* 以及一些填充 */ };
相反,XNET 草案定义如下:
struct sockaddr_storage { u_char ss_len; /* 地址长度 */ u_char ss_family; /* 地址族 */ /* 以及一些填充 */ };
在 1999 年 12 月,一致意见形成了,RFC2553bis 应选择 后一种 (XNET) 定义。
现在的实现符合 XNET 定义, 以 RFC2553bis 的讨论为基础。
如果你看过多种 IPv6 实现,你将可以看到这两种定义。 对于一个用户程序编程者,对此移植性最好的方法是:
保证该平台有 ss_family 和/或 ss_len, 可使用 GNU autoconf,
设置 -Dss_family=__ss_family 以统一所有的相应成员名称 (包括头文件) 为 __ss_family,或
决不要碰 __ss_family。强制转换为 sockaddr * 并这样使用 sa_family:
struct sockaddr_storage ss; family = ((struct sockaddr *)&ss)->sa_family
8.1.2 网络驱动程序
现在如下两项被要求由标准驱动程序支持:
mbuf聚集。在这个稳定发行版中, 我们为所有操作系统将MINCLSIZE改为MHLEN+1, 以便使所有驱动程序按照我们期望的那样工作。
组播。如果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主机成为可能。
A类的TCP延时翻译器已被支持。 这被称为“FAITH”。我们也提供A类的IP头部翻译器。 (后者还没有被放入FreeBSD 4.x。)
8.1.3.1 FAITH TCP 延时翻译器
FAITH系统使用TCP延时守护程序,被称为faithd(8), 由内核支持。FAITH将保留一个IPv6地址前缀, 并且把向该前缀的TCP连接中转到IPv4目的。
例如,如果被保留的IPv6前缀是 2001:0DB8:0200:ffff::,TCP连接的IPv6目的是 2001:0DB8:0200:ffff::163.221.202.12, 连接将被中转到IPv4目的163.221.202.12。
目的 IPv4 结点 (163.221.202.12) ^ | IPv4 tcp toward 163.221.202.12 FAITH-中转 双堆栈结点 ^ | IPv6 TCP toward 2001:0DB8:0200:ffff::163.221.202.12 源 IPv6 结点
faithd(8)必须在FAITH-中转双栈结点上被调用。
更多详细信息,参考 src/usr.sbin/faithd/README 。
8.1.4 IPsec
IPsec主要按三个部分组织。
策略管理
钥匙管理
AH和ESP处理
8.1.4.1 策略管理
内核实现了实验性的策略管理代码。 有两种方法管理安全策略。 一种是使用setsockopt(2)配置每个套接字的策略。这种情况下, 相关策略配置被描述在ipsec_set_policy(3)。 另一种是通过setkey(8)使用PF_KEY接口配置以内核包过滤器为基础的策略。
策略条目不按其索引重排序, 所以在你添加时的条目顺序是非常重要的。
8.1.4.2 钥匙管理
在工具包(sys/netkey)中实现的钥匙管理代码是一个自造的PFKEY第2版的实现。 这符合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()通过检查是否可以找到一个匹配用的SPD (Security Policy Database),来检查是否需要ESP/AH处理。 如果需要ESP/AH,{esp,ah}{4,6}_output()将被调用,mbuf将被随之更新。 当收到一个包时,{esp,ah}4_input()将被按协议号调用,即 (*inetsw[proto])()。 {esp,ah}4_input()将解密/检查包的真实性, 并剥去菊花链头部,ESP/AH的填充。 在包受理时剥去ESP/AH部分是安全的, header on packet reception, since we 因为我们从不受理接收到的“原样的”包。
通过使用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].txt、 rfc241[01].txt、rfc2451.txt 和 draft-mcdonald-simple-ipsec-api-01.txt (草案已过期,但是你可以取自 ftp://ftp.kame.net/pub/internet-drafts/)。 (注意:IKE 规格,rfc241[7-9].txt 在用户级实现,如“racoon”IKE 守护程序)
当然支持的算法是:
旧 IPsec AH
空加密检查和 (无文档, 仅为排错)
加锁的 MD5 带128位加密检查和 (rfc1828.txt)
加锁的 SHA1 带128位加密检查和 (无文档)
HMAC MD5 带128位加密检查和 (rfc2085.txt)
HMAC SHA1 带128位加密检查和 (无文档)
旧 IPsec ESP
无加密 (无文档,相同于 rfc2410.txt)
DES-CBC 模式 (rfc1829.txt)
新 IPsec AH
空加密检查和 (无文档, 仅为排错)
加锁的 MD5 带96位加密检查和 (无文档)
加锁的 SHA1 带96位加密检查和 (无文档)
HMAC MD5 带96位加密检查和 (rfc2403.txt)
HMAC SHA1 带96位加密检查和 (rfc2404.txt)
新 IPsec ESP
无加密 (rfc2410.txt)
DES-CBC 带衍生的 IV (draft-ietf-ipsec-ciph-des-derived-01.txt, 草案已过期)
DES-CBC 带显式的 IV (rfc2405.txt)
3DES-CBC 带显式的 IV (rfc2451.txt)
BLOWFISH CBC (rfc2451.txt)
CAST128 CBC (rfc2451.txt)
RC5 CBC (rfc2451.txt)
上面每种情形可以与下列组合:
ESP HMAC-MD5 认证 (96位)
ESP HMAC-SHA1 认证 (96位)
如下算法不被支持:
旧 IPsec AH
HMAC MD5 带128位加密检查和 + 64位 防重复 (rfc2085.txt)
加锁的 SHA1 带160位加密检查和 + 32位填充 (rfc1852.txt)
IPsec (在内核中) 和 IKE (用户级的“racoon”) 已被在几种互操作测试情形中测试,与许多其它实现互操作良好。 并且,当前的 IPsec 实现对于载于RFC中的加密算法有很大的覆盖面 (我们只覆盖了无智能属性的算法)。
8.1.4.5 IPsec 隧道兼容 ECN
与 ECN 兼容良好的 IPsec 隧道的支持被描述在 draft-ipsec-ecn-00.txt。
普通的 IPsec 隧道被描述在 RFC2401 。加封装时, IPv4 TOS 域 (或 IPv6 交换类域) 将被从内部 IP 头部复制到 外部 IP 头部。 去封装时,外部 IP 头部会被简单的抛弃。 去封装的规则与 ECN 不兼容, 这是因为外部 IP TOS/交换类域中的 ECN 位会被丢失。
为了使 IPsec 隧道与 ECN 配合良好, 我们应该修改加封装和去封装的步骤。 这被描述在 http://http://www.aciri.org/floyd/papers/draft-ipsec-ecn-00.txt, 第 3 章。
IPsec 隧道的实现可以给我们三种选择,这些选择通过设置 net.inet.ipsec.ecn (或 net.inet6.ipsec6.ecn) 为一些特定的值来指定:
RFC2401: 未考虑 ECN (sysctl 项的值为 -1)
ECN 被禁用 (sysctl 项的值为 0)
ECN 被允许 (sysctl 项的值为 1)
注意,以上选项在每个结点都可配置, 而不是按每安全关联 (Security Association, SA) 的方式。 (draft-ipsec-ecn-00 要求按每安全关联进行配置, 但是对于我来说那显得太多了)
各选项总结如下 (详见源代码):
加封装 去封装 --- --- RFC2401 把所有 TOS 位 抛弃外部的 TOS 位 从内部复制到外部 (原样的使用内部 TOS 位) ECN 被禁用 除 ECN (掩码 0xfc) 外将 抛弃外部的 TOS 位 TOS 位从内部复制到外部。 (原样的使用内部 TOS 位) 设置 ECN 位为 0 。 ECN 被允许 除 ECN CE (掩码 0xfe) 外将 使用内部 TOS 位,有一些改变。 TOS 位从内部复制到外部。 如果外部 ECN CE 位是1, 设置 ECN CE 位为 0 。 则在内部使能 ECN CE 位。
通用配置方法如下:
如果两个 IPsec 隧道端点能兼容 ECN 你最好将两个端点配置为 “ECN 被允许” (sysctl 项的值为 1)。
如果另一端对 TOS 位的控制很严格,使用“RFC2401” (sysctl 项的值为 -1)。
在其它情形中,使用“ECN 被禁用” (sysctl 项的值为 0)。
缺省行为是“ECN 被禁用” (sysctl 项的值为 0)。
更多信息请参考:
http://http://www.aciri.org/floyd/papers/draft-ipsec-ecn-00.txt, RFC2481 (显式拥塞通知), src/sys/netinet6/{ah,esp}_input.c
(感谢长·健二朗 <kjc@csl.sony.co.jp>
的详细分析 )
8.1.4.6 互操作性
KAME 的代码已经在一些平台上测试了 IPsec/IKE 的互操作性。 注意,互操作性测试的两边都已修改了它们的实现, 所以如下清单仅供参考。
Altiga, Ashley-laurent (vpcom.com), Data Fellows (F-Secure), Ericsson ACC, FreeS/WAN, 日立, IBM AIX®, IIJ, Intel, Microsoft® Windows NT®, NIST (linux IPsec + plutoplus), Netscreen, OpenBSD, RedCreek, Routerware, SSH, Secure Computing, Soliton, 东芝, VPNet, Yamaha(在日本还用日文假名写作“ヤマハ”,对应汉字为繁体的“山叶”, 为其创始人山叶寅楠的姓氏,但日本人并不习惯于用汉字“山叶”指称该公司) RT100i