第8章  IPv6内部

8.1 IPv6/IPsec的实现

供稿:井上 良信. 翻译:intron@intron.ac.

  本节解释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主机和路由器的过渡机制

    • IPv4兼容地址不被支持。

    • 自动管道(在这个RFC的4.3节中描述)不支持。

    • gif(4) 接口以一种通用的方法实现 IPv[46]-over-IPv[46] 管理,并且覆盖了规格书中描述的“可配置管道”。 详细情况请参见这篇文档中的23.5.1.5节。

  • 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)没有谈及 在如下情形中的近邻缓存处理:

  1. 在没有近邻缓存条目时 结点自发的 RS/NS/NA/重定向 包, 不含数据链路层地址

  2. 介质上的近邻缓存处理,不含数据链路层地址 (我们一个近邻缓存条目给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地址被按如下规则选择:

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

  2. 如果有一个地址被指派给与目的地址有着相同作用域的外发接口 (常常通过查找路由表决定),则使用这个被指派的地址。

    这是最典型的情形。

  3. 如果没有地址满足上述条件, 选择一个指派给发送结点上的一个接口的全局地址。

  4. 如果没有地址满足上述条件, 并目的地址有着站点-本地作用域, 选择一个指派给发送结点上的一个接口的站点-本地地址。

  5. 如果没有地址满足上述条件, 选择与路由表目的对应入口相关联的地址。

  例如,对于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 实现,你将可以看到这两种定义。 对于一个用户程序编程者,对此移植性最好的方法是:

  1. 保证该平台有 ss_family 和/或 ss_len, 可使用 GNU autoconf,

  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主机成为可能。

  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主要按三个部分组织。

  1. 策略管理

  2. 钥匙管理

  3. 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].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

    • 空加密检查和 (无文档, 仅为排错)

    • 加锁的 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://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://www.aciri.org/floyd/papers/draft-ipsec-ecn-00.txt, RFC2481 (显式拥塞通知), src/sys/netinet6/{ah,esp}_input.c

  (感谢长·健二朗 的详细分析 )

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

本文档和其它文档可从这里下载:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.

如果对于FreeBSD有问题,请先阅读文档,如不能解决再联系<questions@FreeBSD.org>.
关于本文档的问题请发信联系 <doc@FreeBSD.org>.