1.尴尬的arp
很早以前就知道,三层的协议包括IP,ARP,ICMP,ARP在IP之下,ICMP在IP之上。看看arp协议格式,却发现它根本就没有用IP协议进行封包,看样子好象和IP协议是并列的关系。仅仅是因为这种arp是专门为IP服务的,就把它看作是IP协议的一部分,这也太不妥当了。
由于arp处在的位置,以及它上接标准的IP协议,下接杂乱的各种链路层协议,因此它的位置很尴尬,这是它的这种不三不二的位置,使得它的实现很复杂,既要有针对IP协议相同的部分,又要有针对MAC不同的部分以可以处理不同的链路层,换句话说,每一种链路层都需要一个arp协议。另外,arp在BMA网络和NBMA网络的实现又有不同,在BMA网络比如以太网中,一次地址解析要惊动整个广播域,这不能不说是一个副作用。
IPv6彻底解决了这个问题。IPv6使用ND的邻接点请求报文和邻接点公告报文来代替arp作为地址解析方案,ND报文全部是承载在ICMPv6中的,而ICMPv6本身使用IP承载,这样所有和IP地址相关的东西就集成在ICMPv6中了,不需要再像IPv4那样单独开发一个位置很尴尬的arp协议了。
IPv6的地址解析使用组播技术,地址解析的过程最小化了对整个链路的影响。之所以可以这样,也是IPv6的标准规定的。标准规定,每一个接口都要自动生成一个链路本地地址(详细解释见下文),可选的有若干其它类型的地址,IPv6协议栈必须再为接口的每一个IPv6地址生成一个“请求节点”组播地址并侦听之-IP地址和生成的组播地址有特定的对应关系,每次需要解析IP地址的MAC时,只需要将邻接点请求发往需要解析的IP地址对应的请求节点组播地址即可。
2.尴尬的地址配置
曾几何时,我一直都想写一个小程序,把它带到旅店等有网线或者wifi但是不一定有DHCP的地方,它能发现接入网络的地址段,然后为我选择一个没有冲突的IP地址。程序写了不少,也能满足这样的需求,但是还是觉得麻烦,不能指望一个不懂网络的人也能做到这一点,本质上这个需求可以使用arping以及nmap来完成...
IPv6针对主机可以使用自动配置。这样就真正做到了“即插即用”,只要你的电脑接入网络,对应的网口就会按照一定的规则生成了一个链路本地地址,该规则就是FE80+(MAC地址-EUI64映射),这样就可以使用这个地址和本链路的其它节点通信了,如果链路上有路由器,当然可以和路由器通信,路由器会把可以在公网上跑的全球地址配置给你的机器的接口,还可以把路由帮你推送下来,更加让人高兴的是,上述的诸多的通信竟然还都是全自动的,这体现在了ND的几个报文中,比如路由器请求/公告,邻接点请求/公告等报文。
玩过OpenVPN的应该对这一切再熟悉不过了吧,其实OpenVPN中也有很多让标准指定者们跃跃欲试的想法!
3.尴尬的NAT
IPv4的有状态NAT彻底取消了全球互联,这种单边的保护主义像关税一样让人厌烦,不但客户端用SNAT做了保护,服务端同样也用DNAT作了映射...使用无状态NAT可好?非也!无状态NAT前后的互联性完全掌握在配置NAT的网管的顶头上司手里,当然这也和网管是否失误有着密切的关系。
IPv6不再需要NAT了,当然如果你想搞保护主义,你可以仍然使用NAT。每一个接口配置了多个地址,协议栈知道在和谁通信的时候使用哪一个地址,和本链路通信,那就用链路本地地址,和本机构通信,那就用本地站点地址,和外界通信,那就用全球地址,如果你不想为节点分配全球地址,也还是可以将机构站点地址NAT成全球地址的...
4.尴尬的地址规划
上大学时,就知道A,B,C类地址,可是后来又学习了CIDR,以至于很多路由器的配置界面中都有一个有类配置和无类配置的选项,在摸索各个路由器的实现时,常常为之头大,有类地址的路由的路由查找逻辑和无类地址的路由查找路由是不同的,最简单的例子,那就是有类查找就不适合使用动态Trie树来进行。IPv4的分类地址使得IP地址的地区分布很不均匀,美国占据了大量的A类地址,如果运营商允许,孩子们可以自由的p2p,而中国的大量网民却只能躲在NAT后浏览web。
IPv6的规划取消了这种地区性,完全根据固定长度的前缀来分类,就连私有地址(链路本地地址/站点地址,见下文)和组播地址也是这么分类的。
a.全球地址固定48位前缀,接下来16位供机构划分子网,最后的64位用来标识节点。
b.私有地址比如链路本地地址由FE80开头,固定前缀位数。
c.其它你能想到的地址类型都是这样。 5.尴尬的私有IP地址
外网口到达的流量的源IP地址绝不能是私有地址,这一直都是几乎每一个网管员都配置过的一条ACL,然而也常常因为遗漏了比如内网口的目标不允许是私有地址这么一条规则而留下隐患,这些规则都是教训产生的经验,久而久之,出现了一系列的所谓经典ACL规则,不管是网管还是网络方面的程序员,都必须将这些规则牢记在心。
如果我们反思一下,这些规则之所以产生,大部分原因正是由于IPv4规定了几个私有地址段,却没有规定路由器的实现该如何对待这些私有地址,一切全部都得靠人工配置而成,这不得不说是一个尴尬。
私有地址绝不应该在公网上出现,问题是谁来定义“公网”。IPv6不光对地址本身进行了层次类别划分,还对某一个基本类别的地址出现的范围进行了约束,也因此将各种约束内置于路由器的实现中。IPv6对地址进行了限域。
总的来讲,单播IPv6地址被分为3类:
全球可达地址:可以在全球范围内路由的地址,类似IPv4的非私有地址,该地址可以配置在任何网口上,可以手工配置,可以使用DHCPv6,也可以使用IPv6的自动配置。
机构可达地址:某个机构内可以路由的地址,不能无限制的在任何地方被路由,仅限于机构内使用。机构可以是一个国家,一个公司,一个部落,总之和人的规划参与有关,这就使得该定义很不明确。
链路可达地址:仅在一条二层链路上有效的地址,不能跨越三层设备。该类地址每个接口上必须配置一个,FE80为其前缀标示。该地址必须是自动配置的,只要接口使能,该地址便生成,生成规则为FE80::+EUI-64标示,EUI-64由接口MAC地址和一系列规则自动生成。
三类地址的区分在于其作用域,路由器根据地址类型完成地址的隔离:
a.不允许链路本地地址跨越路由器;
b.不允许机构地址跨越机构出口路由器;
c.一个接口必然拥有一个链路本地地址,可以再有一个全球地址,发生通信时,根据路由结果和目标地址作用域来选择源地址。 来看一下IPv6对IPv4的改进。
| IPv4 | IPv6 |
同一链路的通信 | 必须使用DHCP或者手工指定同一网段的IP地址 | 可以直接使用自动配置的链路本地地址 |
同一机构内通信 | 人工配置IP地址,如果使用全局的公网地址,时刻注意着不能有地址冲突。 | 使用本地唯一地址 |
互联网通信 | 需要在出口路由器或者防火墙上配置多条策略,防止私有地址的外泄。 | 私有的链路本地地址和机构地址跑不出出口路由器 |
关于私有地址 | 针对ABC类的每一类地址都有一个私有段,私有段不连续。私有段和地址分类有交集,必须显式进行区分 | 直接针对限域来规定是否私有,私有段连续。可以直接解析地址本身得到地址的限域。 |
这些特性的背后是IPv6的实现机理。在进行通信的时候,IPv6根据目标地址总是选择满足可达性前提下作用域最小的本机配置的IP地址来使用,比如它发现目标地址是FE80开头的,那么就会使用一个链路本地地址,如果目标是一个机构内地址,那么就是使用一个机构内地址作为源而不使用全球单播地址,即使它有这个地址也不会使用。
Linux有以下的宏定义:
#define IFA_HOST IPV6_ADDR_LOOPBACK
#define IFA_LINK IPV6_ADDR_LINKLOCAL
#define IFA_SITE IPV6_ADDR_SITELOCAL
#define IFA_GLOBAL 0x0000U
源地址选择逻辑如下:
int ipv6_dev_get_saddr(struct net_device *dev,
struct in6_addr *daddr, struct in6_addr *saddr, int onlink)
{
struct inet6_ifaddr *ifp = NULL;
struct inet6_ifaddr *match = NULL;
struct inet6_dev *idev;
int scope;
int err;
int hiscore = -1, score;
if (!onlink)
scope = ipv6_addr_scope(daddr);
else
scope = IFA_LINK;
/*
* known dev
* search dev and walk through dev addresses
*/
//首先在路由结果的出口网卡上选择源IP地址
if (dev) {
if (dev->flags & IFF_LOOPBACK)
scope = IFA_HOST;
read_lock(&addrconf_lock);
idev = __in6_dev_get(dev);
if (idev) {
read_lock_bh(&idev->lock);
for (ifp=idev->addr_list; ifp; ifp=ifp->if_next) {
if (ifp->scope == scope) {
if (ifp->flags&IFA_F_TENTATIVE)
continue;
score = ipv6_saddr_pref(ifp, 0);
//按照地址状态确定选中比率
if (score <= hiscore)
continue;
if (match)
in6_ifa_put(match);
match = ifp;
hiscore = score;
in6_ifa_hold(ifp);
...
}
}
read_unlock_bh(&idev->lock);
}
read_unlock(&addrconf_lock);
}
if (scope == IFA_LINK)
goto out;
/*
* dev == NULL or search failed for specified dev
*/
//如果没有找到则在全局选择
read_lock(&dev_base_lock);
read_lock(&addrconf_lock);
for (dev = dev_base; dev; dev=dev->next) {
//重复“路由结果的出口网卡上选择源IP地址”
}
...
out:
err = -EADDRNOTAVAIL;
if (match) {
ipv6_addr_copy(saddr, &match->addr);
err = 0;
in6_ifa_put(match);
}
return err;
}
其实,Linux的IPv4实现早已有scope的概念了。IPv4的scope分为路由scope和地址scope,路由scope表示目标距离本机器的距离,地址scope表示该地址的作用域。之所以IPv4有路由scope,是因为IPv4地址不能根据地址本身来确定目标的范围,只能在配置路由的时候指定,IPv4路由的scope并不是必须的,其目的在于确保数据包在转发过程中距离目标越来越近,因此一个数据包的路由scope一定要比该到达路由的下一跳的路由scope更广才行。IPv4的地址scope的作用类似IPv6的限域,但是其用法却鲜为人知,它实际上也是为了限制地址到达的范围,比如发往本链路的通信流就尽量选择link scope的地址,而跨越路由器的通信就不能使用link scope地址作为源。
对于IPv6,由于地址本身便能得到地址的scope信息,因此也就不需要再为地址或者路由项维护一个scope属性字段,不信的话,你使用ip route ls table all看看,凡是IPv4的路由都有scope,IPv6的都没有scope。ip address ls显示出的IPv6的scope并不是地址的一个外在配置的属性,而是地址内在的性质。IPv6地址的scope并不根据你在ip addr add命令中的scope参数而改变,而是根据地址本身算出来的。
6.尴尬的MTU
我曾经实现了一个Linux上不重组分段就能NAT的模块,其动机在于为了在一个中间节点做NAT,先将分片重组,然后在NAT完成后再将其分片,这太麻烦了。这个实现中最烦人的地方在于校验码的重新计算,不光要计算IP头的校验码,还要重新计算TCP/UDP伪头,进而影响上层的校验码。罪魁祸首就是NAT,分片/重组仅仅是迎合而已。如果我们再找一下根源,假设NAT的问题已经解决了(事实上,IPv6中它就是解决了),那么罪魁祸首就是MTU值对于各个链路差异太大了。
IPv6做了硬性规定,中间路由器一律不做IP分片。这么做主要基于两点,第一,目前的网络介质已经不像N年前了,大家完全可以默认一个最小的MTU值(其实也不算小),如果遇到更小的,那算倒霉;第二,很多事情都可以放在端到端的端节点来做,类似MTU发现之类的,如果需要分片,为何上层协议不发小一点的包呢,或者在端的IP层分片。路由器就是高速转发的,分片不是它的职能范围。这非常类似快递, 中间装箱人员是无权拆包的,一般都是直接退回。
7.尴尬的流识别
曾几何时,不管为公还是为私,我一直期待得到一块网络加速卡,使之完成Linux的ip_conntrack的功能,因为协议栈的conntrack效率实在太低了,不光要解析到第四层协议,还要为流保持conntrack结构进而占用内存,还要面临大量连接的情况下连接表爆满的情形。之所以要有conntrack,有两个作用,第一是基于效率考虑对一个流做必须的某种处理,比如NAT,状态匹配等,第二是为了识别某些流从而进行特殊处理,比如优先处理音视频流等。实际上,第二种要求根本就不应该由中间节点负责,而应该由端节点自报,这同时也是TOS字段的原始含义。然而由于额外的conntrack处理,本来应该优待的音视频流反而由于conntrack增加了延迟。
IPv6将上述的第二个需求彻底交给了端节点,端节点填充在IPv6报头中包含了的流标记字段,用来让中间节点识别和处理该流,同属于一个源/目标的通信流量可以通过流标记进行区分。即便如此,IPv6并没有强制中间节点取消conntrack,毕竟如果端节点不设置流标记的话,中间节点还是需要额外的逻辑计算来识别流的。比如针对有状态NAT,conntrack就是必须的。
8.尴尬的集大成
IPv4太过复杂,以至于很多逻辑都必须在协议栈内部计算后确定,各个操作系统的协议栈实现也是一个赛一个的复杂,IPv4几乎做了全部的事情,而大多数都会降低IP网络的性能,在链路带宽越来越便宜,处理器资源越来越贵的情况下,将这些处理将给端节点更加妥当。
9.令人不愉快的IPv4协议栈代码
ip_rcv和ip_rcv_finish是Linux协议栈实现关于IP数据报接收的函数,里面处理了大量的信息,十分繁杂,且不说路由查找有多麻烦,光那种校验和验证,包检查,选项处理,分片,重组等就够复杂了。路由查找本身很简单,然而正是由于IPv4的路由项中有很多人为定义的属性使得代码量不止一倍的增加,比如要判断scope的匹配,邻居解析以更新路由cache等操作。
IPv6的处理非常简单,简单的难以想象,裁减掉的20%的功能砍掉了80%的代码。
10.高速链路需要的是默契而不是智能
如果很多东西是确定的,那就不需要去计算了,而计算本身是消耗计算机软件资源以及电力能源的第一大户,如果事情确定了,那么生搬硬套即可,剩下来的一次计算的能量可以进行多次生搬硬套。虽然智能很伟大,但是对于协议来讲,规则和规定能使计算更高效。这种简化可以从IPv6的接收函数中看到:
static inline int ip6_rcv_finish( struct sk_buff *skb)
{
if (skb->dst == NULL)
ip6_route_input(skb);
return dst_input(skb);
}
fib6_lookup被ip6_route_input调用,作为路由查找的核心,比IPv4的路由查找瘦身了不少。它本质上是用二叉查找树实现的,查找很直接,去掉了scope等人为属性的匹配。
IPv6的报头固定且灵活,便于快速处理,附加的选项大多数仅仅在端系统被处理,即使中间处理也可以链式处理,这正是报头中“下一个头”的精髓所在。
作者:dog250 发表于2012-11-10 20:48:16
原文链接