浙江温州,皮鞋,湿,下雨进水,不会,胖!

近日无雨,还像往常一样,周五晚上下班早,写一篇有点意思的文章。今天的还是想写一个Howto,一起来看点实际的东西,不然《闲谈IPv6》系列岂不是成了一部软文集锦了吗?


光说不练假把式,前面讲了很多关于IPv6怎么怎么好的,其中包含IPv6如何支持移动IP的,但那些都是理论,如果不去亲自试一下,总觉得无法令人信服。或者说,即便自己能自圆其说,跟别人要是怼起来,也就是个纸上谈兵。

理论上简单的东西,看似一眨眼搞定的东西,真的动起手来,还真要花时间来处理很多的边边角角,所以,动手很重要,这样才能真正掌握一个理论,同时在这个过程中积累很多troubleshooting的技巧。


如果你已经理解了关于IPv6如何支持移动IP的RFC文档或者至少已经对此特性曾经管中窥豹,那么本文将为你展现一个剥离了复杂的外围东西(比如说IPSec)的纯粹的Howto。本文将展示一个过程,描述 一个节点在改变了其IP地址之后,如何用其原来的IP地址继续通信下去。

觉得我这个不过瘾的,可以去看:
Mobile IPv6 with Linux: https://www.linuxjournal.com/magazine/mobile-ipv6-linux


先给出我的测试拓扑:

我们假设A为移动节点,B为服务器固定节点,A节点起初被分配了唯一的一个IP地址:
240e:918:8003::12
这个地址我们视其为 home地址 。然后它移动了位置,在新的地方又被分配了另外一个IP地址,是为转交地址,即care-of地址:
240e:918:8003::3f1
此时,A节点的home地址并没有因为得到了新的地址而消失(home地址消失与否,全凭自己决定的啊),这样,A节点便同时拥有了两个地址:

  1. home地址: 240e:918:8003::12
  2. care-of地址: 240e:918:8003::3f1

至始至终,B节点的IP地址一直没有变,即:
240e:918:8003::3f0

现在,我们来看一下矛盾之所在。然后能见招拆招描述一个解法。

在应用层看来,一个应用程序丝毫不会管A节点的移动,网络协议栈的分层原则决定底层的移动在应用层看来是透明的,底层保证逐跳寻址可达性即可。然而,由于垃圾TCP协议,以及稍微好一点的UDP这些协议的缺陷,标识一个TCP必须使用一个四元组,甚至还要构造伪三层头来计算校验码,因此里面就牵扯到了源IP地址和目标IP地址,特别地,计算TCP或者UDP的校验和也需要这些IP层的地址信息,因此, 在端到端看来,一个连接从始至终,其五元组是不能改变的!

然而,另一方面,在IP层看来,它只保证任意节点间的可达性,它才不会管什么上层应用。

上述矛盾预示着,一旦IP地址发生了切换,TCP这种连接一定会断开!但是 应用为王! TCP是万万不能断开的!

如何保证在IP地址切换的时候,上层连接被保持而不被断开?! 这便成了一个大问题。

固然,在会话层处理和维持端到端的连接逻辑更加合适和优雅,但是网络协议栈的实现从一开始就很务实和缺乏前瞻,是的,根本就没有一个标准的会话层!如果说为了解决移动节点的问题而引入一个会话层,那么势必要对大量的应用程序和服务器做超出人想象难度的改造!我曾经针对我们的一个产品做过一些这样的事情。参见我的这篇文章:
Open***移动性改造-靠新的session iD而不是IP/Port识别客户端: https://blog.51cto.com/dog250/1423291
事情已经是现在这个样子了,我们没有办法。那么IPv6作为下一版IP协议,在IP层解决了这个问题。

是的,IPv6在IP层轻而易举解决了这个大问题!详情参见:
闲谈IPv6-IPv6对移动性的天然支持 :https://blog.csdn.net/dog250/article/details/88397134
我想这篇文章已经说的比较清楚了。

本文在理论方面不再多说,本文给出一个观感的东西,该观感的东西足够简单,同时也剥离了很多更加复杂但对理解IPv6移动IP并没有多少帮助的细节。请继续阅读。


移动终端离开家乡,重新进入一个新的地方,重新获得一个新的IP地址,这是移动终端普遍存在的必须经历的一个典型的过程,现在假设这个过程已经完成了。也就是说现在假设移动节点已经拥有了两个IP地址,一个home地址,一个care-of地址。

此时,应用程序对于尚未断开的连接,需要继续使用其原始的home地址来进行通信,对于新建的连接,也希望继续使用之前的home地址来建立通信,怎么办?

以Linux为例,其实,此时Linux内核的XFRM框架会设置一些规则,这样在你发包时,内核会将数据包截获,然后再做封包处理。这里要讲的就是如何处理的细节?

XFRM框架会将该连接的源IP,即socket保留的home地址放在一个叫做DSTOPTS的TLV选项里面,然后用care-of地址替换IPv6头部中本来应该是home地址的source address字段。以此来保证两件事:

  1. 在IP层看来,源地址使用新的care-of地址,以保证节点的可达性。
  2. 在传输层,应用层看来,源地址依然使用老的home地址,以维持端到端连接。

如果你的Home Agent得知你已经获得了一个新的care-of地址并且已经从家乡失联,这会促使该机制被使能,最终当你要发送一个数据包时,就会进入到下面的逻辑中:


XFRM框架支持的安全特性是必须的,这样才能防止安全事件的发生。毕竟在移动环境下,针对IP地址的 认证工作 是百分之一百万不能少的!

然而,我无力去配置复杂的XFRM,我也没有真实的移动环境,我只是想展示一下效果,这个应该不会太难。所以我选择使用RAW套接字去模拟效果。我要模拟的非常简单:

  1. 用care-of地址封装IPv6头部
  2. 用home地址建立四层连接

下面的代码部署在模拟的移动节点A:

#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <linux/in6.h>#define __u8 unsigned char
#define __be16 unsigned short
#define uint16_t unsigned short
#define uint32_t unsigned intstruct ipv6hdr {__u8            priority:4,version:4;__u8            flow_lbl[3];__be16          payload_len;__u8            nexthdr;__u8            hop_limit;struct  in6_addr    saddr;struct  in6_addr    daddr;
};struct padN {__u8     type;__u8       length;char     pad[2];
} __attribute__((packed));struct ipv6_destopt_hao {__u8            type;__u8            length;struct in6_addr     addr;
} __attribute__((packed));struct ipv6_opt_hdr {__u8        nexthdr;__u8        hdrlen;/** TLV encoded option data follows.*/
} __attribute__((packed));// 定义移动节点A的home地址 240e:918:8003::12
__u8     home_addr[16] = {0x24, 0x0e, 0x09, 0x18, 0x80, 0x03, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12};
// 定义移动节点A的care-of地址 240e:918:8003::3f1
__u8     care_of_addr[16] = {0x24, 0x0e, 0x09, 0x18, 0x80, 0x03, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf1};
// 定义固定节点服务器B的地址 240e:918:8003::3f0
__u8     dest_addr[16] = {0x24, 0x0e, 0x09, 0x18, 0x80, 0x03, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0};// 以下计算UDP校验和的代码改自网上的 http://www.pdbuchan.com/rawsock/udp6_cooked.c
uint16_t checksum (uint16_t *addr, int len)
{int count = len;register uint32_t sum = 0;uint16_t answer = 0;while (count > 1) {sum += *(addr++);count -= 2;}if (count > 0) {sum += *(uint8_t *) addr;}while (sum >> 16) {sum = (sum & 0xffff) + (sum >> 16);}answer = ~sum;return (answer);
}uint16_t
udp6_checksum (struct ipv6hdr *iphdr, struct udphdr udphdr, uint8_t *payload, int payloadlen)
{char buf[IP_MAXPACKET];char *ptr;char nxt = SOL_UDP;int chksumlen = 0;int i;ptr = &buf[0];  // ptr points to beginning of buffer bufmemcpy (ptr, &home_addr[0], sizeof (iphdr->saddr));ptr += sizeof (iphdr->saddr);chksumlen += sizeof (iphdr->saddr);memcpy (ptr, &iphdr->daddr, sizeof (iphdr->daddr));ptr += sizeof (iphdr->daddr);chksumlen += sizeof (iphdr->daddr);memcpy (ptr, &udphdr.len, sizeof (udphdr.len));ptr += sizeof (udphdr.len);chksumlen += sizeof (udphdr.len);*ptr = 0; ptr++;*ptr = 0; ptr++;*ptr = 0; ptr++;chksumlen += 3;memcpy (ptr, &nxt, sizeof (iphdr->nexthdr));ptr += sizeof (iphdr->nexthdr);chksumlen += sizeof (iphdr->nexthdr);memcpy (ptr, &udphdr.source, sizeof (udphdr.source));ptr += sizeof (udphdr.source);chksumlen += sizeof (udphdr.source);memcpy (ptr, &udphdr.dest, sizeof (udphdr.dest));ptr += sizeof (udphdr.dest);chksumlen += sizeof (udphdr.dest);memcpy (ptr, &udphdr.len, sizeof (udphdr.len));ptr += sizeof (udphdr.len);chksumlen += sizeof (udphdr.len);*ptr = 0; ptr++;*ptr = 0; ptr++;chksumlen += 2;memcpy (ptr, payload, payloadlen * sizeof (uint8_t));ptr += payloadlen;chksumlen += payloadlen;for (i=0; i<payloadlen%2; i++, ptr++) {*ptr = 0;ptr++;chksumlen++;}return checksum ((uint16_t *) buf, chksumlen);
}int main(int argc, char **argv)
{int            sd;char            *packet;static struct sockaddr_in6 remote;struct ipv6hdr *hdr;struct ipv6_opt_hdr *opthdr;struct padN *padn;struct ipv6_destopt_hao *hao;struct udphdr *udp;int snd;int tot;remote.sin6_family = PF_INET6;remote.sin6_port = htons (0);sd = socket(PF_INET6, SOCK_RAW, IPPROTO_RAW);if (sd < 0) {fprintf(stderr, "Cannot create socket: %s\n", strerror(errno));abort();}// 从IPv6头开始的数据报文总长tot = sizeof(struct ipv6hdr) + sizeof(struct ipv6_opt_hdr) + sizeof(struct padN) + sizeof(struct ipv6_destopt_hao) + sizeof(struct udphdr) + 1;packet = (char *)calloc(1, tot);// IPv6 versionpacket[0] = 0x60;hdr = (struct ipv6hdr *)&packet[0];opthdr = (struct ipv6_opt_hdr *)&packet[sizeof(struct ipv6hdr)];padn = (struct padN *)&packet[sizeof(struct ipv6hdr) + sizeof(struct ipv6_opt_hdr)];hao = (struct ipv6_destopt_hao*)&packet[sizeof(struct ipv6hdr) + sizeof(struct ipv6_opt_hdr) + sizeof(struct padN)];udp = (struct udphdr *)&packet[sizeof(struct ipv6hdr) + sizeof(struct ipv6_opt_hdr) + sizeof(struct padN) + sizeof(struct ipv6_destopt_hao)];// 设置固定简单的IPv6头memset(hdr->flow_lbl, 0, sizeof(hdr->flow_lbl));// 除了IPv6头部以外的所有载荷长度hdr->payload_len = htons(tot - sizeof(struct ipv6hdr));// 下一个头是“地址选项头”hdr->nexthdr = IPPROTO_DSTOPTS;//0x3c; 60hdr->hop_limit = 64;// 注意,源地址是care-of转交地址,而不是home家乡地址memcpy(&hdr->saddr, care_of_addr, 16);// 目标地址没得说,就是目标地址memcpy(&hdr->daddr, dest_addr, 16);// 设置“地址扩展选项头”,其下一个头是UDPopthdr->nexthdr = IPPROTO_UDP;opthdr->hdrlen = 2;// 设置填充padn->type = 1;padn->length = 2;memset(&padn->pad, 0, 2);// 设置home TLV选项,home地址将藏匿于其中hao->type = IPV6_TLV_HAO;//0xc9;hao->length = 16;memcpy(&hao->addr, home_addr, 16);// 这里是UDP头udp->source = htons(1111);udp->dest = htons(12345);udp->len = htons(1 + sizeof(struct udphdr));char *buf = (char *)udp;udp->check = udp6_checksum(hdr, *udp, &buf[sizeof(struct udphdr)] , 1);memcpy(&remote.sin6_addr.s6_addr, &hdr->daddr, sizeof(remote.sin6_addr.s6_addr));// 好了,现在将我们从IPv6头部开始构造的报文,一股脑儿全部发送出去!snd = sendto(sd, packet, tot, 0, (struct sockaddr *)&remote, sizeof(remote));if (snd < 0) {fprintf(stderr, "Cannot send message:  %s\n", strerror(errno));abort();}return 0;
}

我们模拟一个移动节点A,在获得了一个新的care-of转交地址,同时保有原来的home家乡地址的情况下,用上面的程序发出一个报文,我们来看看这个报文长什么样子。

该报文抓包如下:

从抓包上看,我们已经做到了第一点,即用care-of地址封装IPv6报文头,它确实是如此封装的,然后home地址藏匿于一个DSTOPTS TLV选项里面。那么接下来,我们需要确认在四层看来,它使用的是home地址。

为了让B节点按照IPv6处理DSTOPTS的方式成功接收这个数据包,同样需要绕开XFRM逻辑。如果你忽略了这一点,下面的实验将全部不会成功,你将会发现snmp统计里的XfrmInNoStates计数器字段在升高,这意味着数据包没有通过内核XFRM的检验,毕竟我们什么规则也没有使用,更没有去生成IPSec SA,所以我们要绕开这个复杂的东西。

非常简单,用HOOK机制让XFRM的检查函数永远返回true即可,即:

static int stub_xfrm6_input_addr(struct sk_buff *skb, xfrm_address_t *daddr)
{printk("hook stub \n");return 1;
}static int hook_xfrm6_input_addr(struct sk_buff *skb, xfrm_address_t *daddr)
{printk("hook \n");return 1;
}
...// 嗯,我们就是要hook住xfrm6_input_addr,所以要先找到它!ptr_xfrm6_input_addr = kallsyms_lookup_name("xfrm6_input_addr");if (!ptr_xfrm6_input_addr) {printk("err");return -1;}jump_op[0] = 0xe9;hook_offset = (s32)((long)hook_xfrm6_input_addr - (long)ptr_xfrm6_input_addr - OPTSIZE);(*(s32*)(&jump_op[1])) = hook_offset;saved_op[0] = 0xe9;orig_offset = (s32)((long)ptr_xfrm6_input_addr + OPTSIZE - ((long)stub_xfrm6_input_addr + OPTSIZE));(*(s32*)(&saved_op[1])) = orig_offset;get_online_cpus();// 替换操作!ptr_poke_smp(stub_xfrm6_input_addr, saved_op, OPTSIZE);barrier();ptr_poke_smp(ptr_xfrm6_input_addr, jump_op, OPTSIZE);put_online_cpus();

详情参见:
Linux内核如何替换内核函数并调用原始函数 : https://blog.csdn.net/dog250/article/details/84201114

绕过XFRM之后,IPv6的DSTOPTS扩展头处理逻辑就能正确处理这个选项了:

  1. 从DSTOPTS选项里取出home地址,用其替换IPv6头里面的源地址。
  2. 将替换好的IPv6报文继续处理DSTOPTS的next header,即UDP数据报。

现在在节点B运行的服务进程正等着呢:

import socket# 侦听3f0地址
addr = ("240e:918:8003::3f0", 12345, 0, 0)
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_RECVDSTOPTS, 1)
s.bind(addr)
data, raddr = s.recvfrom(64)
# 打印来源地址,看看数据包的源地址到底是home地址还是care-of地址,你猜猜看。
print str(raddr)
time.sleep(20)

OK,打印结果如下:

('240e:918:8003::12', 1111, 0, 0)

如果我们在B节点看一下netstat显示的连接信息,果不其然,依然也是看到是A节点的home地址240e:918:8003::12在发送数据,而不是抓包里IPv6头部的care-of地址240e:918:8003::3f1在发送,然而,从网络的角度看确实是A节点的care-of地址作为源地址在传输数据报文。

端到端逻辑和网络逻辑区分开了!


也许你会说,这不就是简单的一个SNAT嘛,有什么大惊小怪的,但是非也,非也!

我并没有配置任何NAT规则,所有这一切都是在IPv6的RFC里天然支持的:
Mobility Support in IPv6: https://tools.ietf.org/html/rfc6275

也许你见过,用过,听说过之前淘宝LVS使用的TAO(TCP Option Address):
LVS在淘宝环境中的应用: http://cherishry.github.io/2016-01-16-LVS在淘宝环境中的应用.html

TCP在经过四层以上代理后,请求到达后端时,业务逻辑无法区分独立的客户端,为了能作出区分,TOA被设计了出来。

看样子,其思想和IPv6的这个DESTOPTS非常类似,都是将一个源地址藏匿于选项里面,但是不同的是,TOA是在TCP层本身来处理这个源地址的,如果想实现类似IPv6的这个源地址交换的效果,在取出这个地址之前,检查校验码就已经失败了!

因此,TOA只适合于传递源IP地址信息,而不能让其参与封包处理。这是TOA的局限。


好了,现在该服务器回包了。我们要看一下服务器B节点是如何做到下面的两点的:

  1. 使用模拟移动节点A的home地址构建端到端连接
  2. 使用模拟移动节点A的care-of地址进行网络层路由

当服务器B节点发现了藏匿于DSTOPTS选项里面的A节点的home地址之后,它会将其保存起来,作为其四层连接的目标地址继续使用,同时,模拟节点A的正向报文的原始IPv6头部的源地址,即其care-of地址,将作为其IP层的目标地址参与IP路由寻址。这个道理和上面描述的是一样的,三层,四层使用不同的目标IP地址,代表不同的含义分别处理。

IPv6的该功能是用 第二类路由头 实现的,所谓的 第二类 其实只是针对第一类做了一些更为严格的约束,即里面只能藏匿一个经由地址,即最终目的地,A节点的home地址!

为了实现这个,这次我不再用RAW套接字,而是直接使用IPv6相关的API来完成,即通过设置sockopt的方式,将这个RTHDR直接设置下去。

当然了,用RAW的话,也是可以,可能还更直接,但是我喜欢一顿饭两个味道,先干吃,然后再放辣椒,还是领略一下不同的味道吧。

这个版本里,只需要现成的setsockopt,简单地将这个头部扩展设置进去即可。当然,我的版本里是自己拼接buffer的,你也可以使用现成的API来完成,大同小异。你可以使用下面的API套件:

     intinet6_opt_init(void *extbuf, socklen_t extlen);intinet6_opt_append(void *extbuf, socklen_t extlen, int offset,u_int8_t type, socklen_t len, u_int8_t align, void **databufp);intinet6_opt_finish(void *extbuf, socklen_t extlen, int offset);intinet6_opt_set_val(void *databuf, int offset, void *val,socklen_t vallen);intinet6_opt_next(void *extbuf, socklen_t extlen, int offset,u_int8_t *typep, socklen_t *lenp, void **databufp);intinet6_opt_find(void *extbuf, socklen_t extlen, int offset, u_int8_t type,socklen_t *lenp, void **databufp);intinet6_opt_get_val(void *databuf, socklen_t offset, void *val,socklen_t vallen);

详情参阅其manual:https://www.unix.com/man-page/netbsd/3/inet6_opt_append/

总之,理解了TLV以及路由头的数值,怎么拼接都可以。

先看服务器节点B上运行的代码,它的目标是:

  1. 用A的care-of地址封装数据报文的IPv6头;
  2. 用A的home地址设置进第二类路由头;

请看代码:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <errno.h>
#include <netinet/ip.h>
#include <netinet/udp.h>#define   __u8 unsigned char
#define __be16 unsigned shortstruct ipv6_rt_hdr {__u8        nexthdr;__u8        hdrlen;__u8        type;__u8        segments_left;/**  type specific data*  variable length field*/
};#define __u32 unsigned intstruct rt2_hdr {struct ipv6_rt_hdr  rt_hdr;__u32           reserved;struct in6_addr     addr;#define rt2_type        rt_hdr.type
};uint8_t   app_addr[16] = {0x24, 0x0e, 0x09, 0x18, 0x80, 0x03, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12};
uint8_t   care_of_addr[16] = {0x24, 0x0e, 0x09, 0x18, 0x80, 0x03, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0};
uint8_t   real_addr[16] = {0x24, 0x0e, 0x09, 0x18, 0x80, 0x03, 0x00, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf1};int main(int argc, char **argv)
{int  num, sd;struct rt2_hdr *ext_buffer;static struct sockaddr_in6 remote;remote.sin6_family = PF_INET6;remote.sin6_port = htons (12345);memcpy(&remote.sin6_addr.s6_addr, &app_addr[0], sizeof(remote.sin6_addr.s6_addr));sd = socket(AF_INET6, SOCK_DGRAM, SOL_UDP);if (sd < 0) {fprintf(stderr, "Cannot create socket: %s\n", strerror(errno));abort();}// 封装第二类路由头,将A节点的home地址藏匿其中ext_buffer = (struct rt2_hdr *)malloc(24);ext_buffer->rt_hdr.nexthdr = 222;ext_buffer->rt_hdr.hdrlen = 2;ext_buffer->rt_hdr.type = 2;ext_buffer->rt_hdr.segments_left = 1;memset(&ext_buffer->reserved, 0, sizeof(ext_buffer->reserved));memcpy(&ext_buffer->addr, real_addr, sizeof(real_addr));if (setsockopt(sd, IPPROTO_IPV6, IPV6_RTHDR, ext_buffer, sizeof(struct rt2_hdr)) == -1) {fprintf(stderr, "setsockopt IPV6_RTHDR: %s\n", strerror(errno));abort();}num = sendto(sd, "1234567890", (size_t) 10, 0, (struct sockaddr *)&remote, sizeof(remote));if (num < 0) {fprintf(stderr, "Cannot send message:  %s\n", strerror(errno));abort();}return (0);
}

抓包试试看呗:

B节点的输出程序如下:

import socketaddr = ("240e:918:8003::12", 12345, 0, 0)
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.bind(addr)
data, raddr = s.recvfrom(64)
print str(raddr)time.sleep(30)

来自240e:918:8003::3f0的数据:

('240e:918:8003::3f0', 48571, 0, 0)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
udp6       0      0 240e:918:8003::12:12345 :::*                                11638/python

看到了吧,和抓包显示的完全不同,在模拟移动节点A的四层看来,数据包的目标地址是自己的home地址,但是我们也已经看到,在三层以及以下看来,数据包的目标地址却是A节点的care-of转交地址。

两个地址的任务不同,三层及以下,需要确保可达性,毕竟节点A已经离开家乡,在三层看来,家乡地址是不可达的,但是在四层看来,家乡地址只是为了维持端到端连接的一个标识手段而已。

接下来,换另一种口味。把上面的代码用RAW来再演示一把。

先看模拟服务器B上的代码:

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <linux/in6.h>#define __u8 unsigned char
#define __be16 unsigned short
#define __u32 unsigned intstruct ipv6hdr {__u8            priority:4,version:4;__u8            flow_lbl[3];__be16          payload_len;__u8            nexthdr;__u8            hop_limit;struct  in6_addr    saddr;struct  in6_addr    daddr;
};struct ipv6_rt_hdr {__u8        nexthdr;__u8        hdrlen;__u8        type;__u8        segments_left;/**  type specific data*  variable length field*/
};struct rt2_hdr {struct ipv6_rt_hdr  rt_hdr;__u32           reserved;struct in6_addr     addr;#define rt2_type        rt_hdr.type
};uint8_t         home_addr[16] = {0x24, 0x0e, 0x09, 0x18, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12};
uint8_t         source_addr[16] = {0x24, 0x0e, 0x09, 0x18, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0};
uint8_t         care_of_addr[16] = {0x24, 0x0e, 0x09, 0x18, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf1};
int main(int argc, char **argv)
{int            sd;char            packet[128] = {0};static struct sockaddr_in6 remote;struct ipv6hdr *hdr;struct ipv6_rt_hdr *rthdr;struct rt2_hdr *rt2;int num;remote.sin6_family = PF_INET6;remote.sin6_port = htons (0);sd = socket(PF_INET6, SOCK_RAW, IPPROTO_RAW);if (sd < 0) {fprintf(stderr, "Cannot create socket: %s\n", strerror(errno));abort();}packet[0] = 0x60;hdr = (struct ipv6hdr *)&packet[0];rthdr = (struct ipv6_rt_hdr *)&packet[sizeof(struct ipv6hdr)];rt2 = (struct rt2_hdr *)rthdr;memset(hdr->flow_lbl, 0, sizeof(hdr->flow_lbl));hdr->payload_len = htons(sizeof(packet) - sizeof(struct ipv6hdr));hdr->nexthdr = IPPROTO_ROUTING;hdr->hop_limit = 64;memcpy(&hdr->saddr, source_addr, 16);// 使用care-of地址封装IPv6地址头的目标地址memcpy(&hdr->daddr, care_of_addr, 16);#define IPPROTO_MYTEST   222rt2->rt_hdr.nexthdr = IPPROTO_MYTEST;rt2->rt_hdr.hdrlen = 2;rt2->rt_hdr.type = 2;rt2->rt_hdr.segments_left = 1;memset(&rt2->reserved, 0, sizeof(rt2->reserved));// home地址藏匿于第二类路由头里面memcpy(&rt2->addr, home_addr, sizeof(home_addr));memcpy(&remote.sin6_addr.s6_addr, &hdr->daddr, sizeof(remote.sin6_addr.s6_addr));num = sendto(sd, packet, sizeof(packet), 0, (struct sockaddr *)&remote, sizeof(remote));if (num < 0) {fprintf(stderr, "Cannot send message:  %s\n", strerror(errno));abort();}return (0);
}

接下来给出模拟节点A上的接收代码:

import socket# 侦听的是home地址
addr = ("240e:918:8003::12", 1245, 0, 0)
# 创建处理自定义协议号为222的RAW socket
s = socket.socket(socket.AF_INET6, socket.SOCK_RAW, 222) #
s.bind(addr)
data, raddr = s.recvfrom(64)
print str(raddr)

代码就不再解释了。

我也不是很会编程,代码,就给到这里。


当然了,以上这些,IPv4也能做到,但是无外乎就是配置复杂的规则,而不是IP协议内在原生的特性。使用IPv6,几乎什么都不用做,想达到以往NAT才能实现的效果,一眨眼就OK了。这就是所谓的 天然支持某种特性 的威力!

好了,现在说一下IPv4如何不用NAT,不用Netfilter,甚至不基于Linux系统内核实现上述这些。

非常简单!

只需要在IP协议和TCP/UDP协议之间插入一层新的协议即可,类似隧道封装那般,分层协议结构无所不能,any over any。我们只需要将IPv4协议头里的proto字段改成一个新的协议,比如:

#define IPPROTO_V4RTHDR

然后实现 IPPROTO_V4RTHDR 的协议逻辑:

struct v4rthdr{u8    l4proto;u32 home_addr;
} __attribute__((packed));

发送端只需要将这个协议push到TCP/UDP前面即可:

struct v4rthdr *rthdr = ......
// 使用home地址建立TCP连接并且计算TCP伪头。
...
rthdr->l4proto = IPPROTO_TCP; // or IPPROTO_UDP
rthdr->home_addr = 移动节点的home地址
iphdr->daddr = 移动节点的care-of地址
...

接收逻辑的handler如下:

...
iphdr->daddr = rthdr->home_addr;
...

是不是很简单呢?

但是问题是,这一切都不是标准化的!!而在IPv6中,这些功能是每一个宣称自己支持IPv6的节点都必须要实现的,这些都是RFC里面的MUST,SHOULD…

我们讲,如果仅仅是实现某个东西,那是简单的,特别是网络协议栈方面,我此前很久都觉得以此为荣,但是 网络协议栈方面的这些仅仅实现出来的东西都是trick, 网络协议栈方面,最重要的就是 标准化!!标准化!!标准化!!

我们看 协议 字面的意思,就有标准化的意思,大家都公认,都遵守的细则,这就是协议。

如果我想用home地址进行移动节点的支持,建立TCP连接,按照上述的IPv4的yy做法,我必须同时在所有参与者设备上部署我的trick代码,这不是标准的,这是私有的东西,同时这也意味着巨大的工作量。

然而如果是使用IPv6,只要对方声称 自己支持IPv6 就这么简单一句话,我就默认当我发送一个第二类路由头过去时,相信它就一定会按照RFC里面说的那样去处理。这就是协议。但是IPv4做不到这种相信,你必须自己去做去实现它,而不能只是相信。


浙江温州皮鞋湿,下雨进水不会胖。

闲谈IPv6-体会一下移动IP路由扩展头以及地址选项头的实际操作(Howto)相关推荐

  1. linux使用虚拟ip路由问题,linux – 来自主机的虚拟机的IPv6公共路由

    所以我有一个来自OVH的专用服务器.有了这个,我得到了64位IPv6地址和1个公共IPv4地址.我购买了第二个IPv4地址作为后备. 主机正在运行Xen管理程序,我已经设置了桥 bridge name ...

  2. IP路由故障关于BGP的疑问解答

    IP路由故障关于BGP的疑问解答 为什么BGP不能与其聚合路由所属网段的设备建立邻居? 答:如果BGP与其聚合路由所属网段的设备建立邻居,当目标网段的设备或者链路出现故障时,通过聚合路由是无法感知的, ...

  3. 71张图详解IP 地址、IP 路由、分片和重组、三层转发、ARP、ICMP

    目录 有小伙伴问:为什么没有配置 IP 地址就无法上网?IP 协议又是啥? 这要从 TCP/IP 协议说起,互联网使用的是 TCP/IP 协议,其中 IP 协议又是最重要的协议之一.IP 协议是基于  ...

  4. LinuxC下获取UDP包中的路由目的IP地址和头标识目的地址

    在接受到UDP包后,有时候我们需要根据所接收到得UDP包,获取它的路由目的IP地址和头标识目的地址. (一)主要的步骤: 在setsockopt中设置IP_PKTINFO,然后通过recvmsg来获取 ...

  5. 互联网IP路由的逐跳全局最优化原则-Dijkstra算法证明

    把周末写了一半的东西继续补齐了,实现了完美的一天. 我们知道的一个事实就是IP地址实在太多了,根本就不可能统一的管理起来,无论从数据平面还 是从控制/管理平面上说都是这样.所以,IP协议被设计出来就是 ...

  6. Linux 基础命令:IP 路由操作 -ip命令

    转自Linux爱好者:Linux 基础命令:IP 路由操作 Table of Contents ip 1.语法 2.选项列表 3.ip link---网络设备配置 4.ip address---协议地 ...

  7. LINUX IP 路由实现

    摘自:http://www.cnblogs.com/super-king/p/3296091.html 以下代码取自 kernel 2.6.24. [数据结构] 该结构被基于路由表的classifie ...

  8. 71张图详解IP地址、IP 路由、三层转发、ARP、ICMP

    71张图详解IP地址.IP 路由.三层转发.ARP.ICMP 架构师之道2021-04-07 13:51:24 https://www.toutiao.com/i6948285918986027531 ...

  9. 【最全面的】71张图详解IP 地址、IP 路由、分片和重组、三层转发、ARP、ICMP

    转发自: Original Fox 网络技术平台 目录 有小伙伴问:为什么没有配置 IP 地址就无法上网?IP 协议又是啥? 这要从 TCP/IP 协议说起,互联网使用的是 TCP/IP 协议,其中 ...

  10. IP路由基础、路由器静态路由配置方法、自治系统、缺省路由的配置方法、路由选路规则、缺省路由、备份路由、等价路由、三种查询路由表命令

    目录 路由器特点: 网络IP地址规划 网络间的特性: 基本路由思想: ​编辑 静态路由部分: 查询设备整个路由表: 查看特定的路由协议时使用: 查询目的地址2.2.2.2的路由条目: IP路由表代码写 ...

最新文章

  1. java反射详解 (一)
  2. JAVA51报错,好象是占溢出错误,不知道怎么改
  3. boost::core::typeinfo的用法实例
  4. Ember.js 入门指南——路由切换的终止和回跳
  5. Algorithm——1.排序.md
  6. java面试设计模式
  7. 任务管理器启动资源管理器
  8. 动态滤波网络论文解读
  9. tesseract-orc 合并识别结果
  10. 中切片工具怎么使用_技巧|Excel中切片器的2个使用方法!
  11. Android Thing专题5 I2C
  12. oneday2mybatis下载
  13. Django新手入门(三)——使用PyCharm创建Django项目
  14. 北京邮电计算机课程表,(北邮通信工程本科专业课程表.doc
  15. 网站小图标制作及配置
  16. postman前置脚本密码MD5加密以及转大写
  17. 小米,红米 root Magisk(面具)安装教程
  18. bzoj2555 SubString (SAM+LCT维护子树大小/ETT)
  19. 电子工程师自学成才pdf_给新开发人员的最佳建议:自学成才的软件工程师的建议...
  20. Activity毛玻璃背景效果

热门文章

  1. matlab画一条平滑曲线,Matlab画平滑曲线的两种方法( 拟合或插值后再用plot即可)...
  2. 大数据集群资源监控Zabbix
  3. oracle全量增量_oracle增量和全量备份方案
  4. 【推荐】实现跟随鼠标移动的浮动提示框、气泡框、Tip效果
  5. 华为eNSP静态路由下一跳实验
  6. 大学生计算机基础实验文库,大学计算机基础实验指导书(ecxel)
  7. 在线3D大脑建模网站分享
  8. 应该根据哪些判断云服务器的好坏
  9. 加拿大布兰登大学计算机专业,名校大揭底:布兰登大学到底怎么样?
  10. ref获取元素 vue 删除子元素_vue 添加删除子元素