当前位置:   article > 正文

闲谈IPv6-v4/v6协议转换报文的checksum无关性_stat-mode v4-v6

stat-mode v4-v6

在IPv6时代,是不是可以用本地链路质量信息编码源地址的主机标识符从而指导服务器端拥塞控制策略呢,是不是也可以把自己是谁编码进去呢?比如自己是Android,自己是一台PC,或者说自己是一双智能皮鞋?以此来指导数据发送端的定制化动作呢?


IPv6的地址空间足够大,且留下了可达64位的主机标识符可供任意发挥,如此长度的主机标识符可以藏匿很多信息啊!

可以先看一下我很久之前在2012年写的一篇文章:
IPv6的NAT原理以及MAP66: https://blog.csdn.net/dog250/article/details/7799398
很有意思。


这种 利用IPv6地址空间远大于IPv4地址空间的特性,在IPv4报文转换为IPv6报文实现IPv4和IPv6之间互访的时候,通过解一个一元一次方程来保证协议checksum无需重新计算 的技术,其实还有很多玩法。

关键不在于什么解一元一次方程,而是在于IPv6的地址空间比IPv4地址空间足够大,在IPv4地址嵌入到IPv6地址中后,剩余的空间仍然可以存储校验码矫正值。

之前说过,IPv4和IPv6之间存在联通的必要,因为要平滑过渡就必然需要某种兼容,那么这种联通就可以分为两类:

  • 横向联通: IPv4海洋中,IPv6孤岛之间的互访,此时需要IPv4隧道,参见6to4以及ISATAP等。
  • 纵向联通: IPv4直接访问IPv6资源或者反过来。此时就需要协议转换,协议转换必然涉及到checksum的重新计算问题。

以上纵向联通方面,有一个超猛支撑技术,那就是DNS64,但是这种DNS技术更加侧重于管理平面和配置技巧,不是我的菜,所以我也不想多聊,分享一篇文章:
支持IPv6 DNS64/NAT64 网络<- 网络概述: https://www.jianshu.com/p/37b8c006cd2d

为了防止链接失效,盗图一张,解释DNS64:
在这里插入图片描述

逻辑是比较简单的,但细节却足够繁琐,超级烦人。

本文不想谈DNS64,本文谈谈当IPv4报文转换为IPv6报文以及IPv6报文转换为IPv4报文时,上层协议checksum的计算问题。


上层协议在计算checksum时早就不需要IP层字段作为伪头参与了,但是不管TCP,UDP还是ICMP都是古老的协议,它们设计时就如此,没有办法,即便是IPv6还是要支持!

如果在协议转换的集中化节点去重新计算上层协议的checksum,那么资源的消耗将会是集中式的,为此,我们希望这些相关的计算尽量在边缘进行。此外,由于IPv6没有NAT或者至少说不提倡NAT,且地址足够长,没有哪台设备有足够的内存可以承受海量的连接状态跟踪的维护,所以需要某种stateless机制去维护conntrack!

就像TCP的Syncookie一样,我们 可以把conntrack信息存放在IPv6报文本身,因为它的地址空间足够大!

先看IPv4报文转换为IPv6报文时,如何保持上层checksum的不变性。

按照RFC6052的规范:
IPv6 Addressing of IPv4/IPv6 Translators: https://tools.ietf.org/html/rfc6052

IPv4地址会嵌入到IPv6地址空间的低位,具体就是下面这个规则了(参见2.2节):
在这里插入图片描述

注意后面的suffix,这些后缀空间是可以供我们自由发挥的。

除却96位的prefix实在是没有空间,其它的情况,至少可以在suffix低位取2个字节来存放checksum矫正值,计算这个checksum矫正值的问题可以描述为:
求解一个16bit的数字,请问它是多少时,当IPv4头按照RFC6052规范换成IPv6头时,TCP的checksum可以保持不变?

这不就是一个一元一次方程嘛…


给出一段代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//以下2个函数就是计算校验码的,具体的原理请参见RFC1071/RFC1624/RFC1141
static inline u_int16_t add16(
    u_int16_t a,
    u_int16_t b)
{
    a += b;
    return a + (a < b);
}

static inline u_int16_t csum16(const u_int16_t *buf, int len)
{
    u_int16_t csum = 0;
    while(len--) csum = add16(csum, *buf++);
    return csum;
}

unsigned char sip[4] = {192, 168, 1, 1};
unsigned char dip[4] = {172, 16, 2, 2};

// 转换后的源IPv6地址,即将源IPv4地址嵌入到IPv6地址后64位的高32位,sip6已经完成嵌入
unsigned char sip6[16] = {0x20, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
						 192, 168, 1, 1, 0x00, 0x00, 0x00, 0x00};
// 1. 转换后的目标IPv6地址,即将目标IPv4地址嵌入到IPv6地址后64位的高32位,sip6已经完成嵌入
// 2. 目标IPv4地址2.2.2.2,是由DNS64机制通告给IPv4-only的客户端的,到达NAT64网关后,将其转换为下面的IPv6地址
unsigned char dip6[16] = {0x20, 0x01, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
						 172, 16, 2, 2, 0x00, 0x00, 0x00, 0x00};

// 简版IPv4头
struct ipv4hdr {
	unsigned char sip[4];
	unsigned char dip[4];
};

// 简版IPv6头
struct ipv6hdr {
	unsigned char sip6[16];
	unsigned char dip6[16];
};

// 报文格式:|0~31 IPv4/6头部|32~45 payload|46~47 校验码|

int main(int argc, char **argv)
{

    u_int16_t csum, *c2;
	// 完整的数据报文:0~31bit可以装下简版IPv6头,24~31bit可以装下简版IPv4头.保持payload不变,方便操作
	unsigned char  packet[48] = {0};
	// 暂存IPv6头
	unsigned char  hdr6buf[32] = {0};
	struct ipv4hdr *hdr4;
	struct ipv6hdr *hdr6;

	hdr4 = (struct ipv4hdr *)&packet[24];
	memcpy(hdr4->sip, sip, 4);
	memcpy(hdr4->dip, dip, 4);
	// 10个字节的payload
	memcpy((unsigned char *)hdr4 + sizeof(struct ipv4hdr), "1234567890abcd", 14);
	// 最后2个字节保存校验码
	memset((unsigned char *)hdr4 + sizeof(struct ipv4hdr) + 14, 0, 2);

	// 一共校验23个16位组,总共46个字节
	csum = csum16((u_int16_t *)(&packet[0]), 23);
	c2 = (u_int16_t *)(&packet[46]);
	*c2 = csum;
    printf("IPv4报文数据检验码(可模拟包含伪头的TCP校验码):%.2X\n", csum);

	{
		int i = 0;
		printf("begin IPv4 packet:\n");
		for (i = 24; i < 48; i++) {
			printf ("%.2X ", packet[i]);
		}
		printf("\nend IPv4 packet\n");
	}

	hdr6 = (struct ipv6hdr *)&hdr6buf[0];
	memcpy(hdr6->sip6, sip6, 16);
	memcpy(hdr6->dip6, dip6, 16);
    u_int16_t csum_hdr6 = csum16((u_int16_t *)hdr6, 16);

    //定位固定修改后的动态修改的初始地址,我将IPv6头的源地址hostID的最低2个字节用于检验码矫正!
    u_int16_t* pcsum = (u_int16_t*)(&hdr6buf[14]);

    //计算校验码矫正值!即已知h,p,c,求x:h + p + x = c
    *pcsum = ~add16(
        add16(
            ~(*pcsum),
            ~csum16((u_int16_t*)(&packet[0]), 16)
        ),
        csum_hdr6
    );

    printf("校验码矫正值为:%X\n", *pcsum);
    memcpy(&packet[0], &hdr6buf[0], 32); //完成修改

    printf("当前IPv6报文的校验码(模拟在IPv4头转换为IPv6头之后,TCP协议的校验码不需要改变):%.2X\n", csum16((u_int16_t*)(&packet[0]), 23));

	{
		int i;
		printf("begin IPv6 packet:\n");
		for (i = 0; i < 48; i++) {
			printf ("%.2X ", packet[i]);
		}
		printf("\nend IPv6 packet\n");
	}

	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112

编译执行之:

[root@localhost DESTHDR]# ./a.out
IPv4报文数据检验码(可模拟包含伪头的TCP校验码):883E
begin IPv4 packet:
C0 A8 01 01 AC 10 02 02 31 32 33 34 35 36 37 38 39 30 61 62 63 64 3E 88
end IPv4 packet
校验码矫正值为:EEB0
当前IPv6报文的校验码(模拟在IPv4头转换为IPv6头之后,TCP协议的校验码不需要改变):883E
begin IPv6 packet:
20 01 00 01 02 03 04 05 C0 A8 01 01 00 00 B0 EE 20 01 05 04 03 02 01 00 AC 10 02 02 00 00 00 00 31 32 33 34 35 36 37 38 39 30 61 62 63 64 3E 88
end IPv6 packet
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

是不是很好玩?解一元一次方程也能解出实际用途来。

这可不是我自己说的,这可是RFC上说的,我只是照着试试做一下而已:
https://tools.ietf.org/html/rfc6052#section-4.1


现在反过来,IPv6报文如果转换为IPv4报文呢?


我们知道IPv4报文本身就有一个针对于IPv4协议头的校验码字段,如果数据始发于IPv6栈,那么当它需要转换为IPv4报文时,看样子这个IPv4的校验码计算是躲不过。

确实躲不过,但问题是在哪里进行这个计算。是在边缘节点还是在协议转换的节点来做呢?

我认为可以在边缘节点来做这个计算,然后把值藏匿于IPv6地址的 可以自由发挥的主机标识符里 就是了。

假设从IPv6栈发起一个去往IPv4地址10.18.19.2的数据报文,按照RFC6052的规范,这个IPv4地址肯定被编码进了IPv6栈的源地址的主机标识符里,那么是不是可以在数据始发的时候,就直接按照IPv4地址来计算TCP/UDP/ICMP的校验码呢,然后继续计算IPv4头的校验码,将IPv4头的校验码藏匿于源IPv6地址的suffix即可。

下面是一个例子:

  1. IPv6始发:2001: 1234: 1234: 1234:192.168.12.2::0/64到2001:4321:4321:4321:172.16.12.2::/64。
  2. 始发站直接按照192.168.12.2和172.16.12.2作为源和目标计算4层协议checksum保存在报文checksum字段。
  3. 始发站自行组装源和目标分别为192.168.12.2和172.16.12.2的IPv4报头,计算IPv4的checksum,保存于源IPv6地址2001: 1234: 1234: 1234:192.168.12.2::0/6的最后2个字节。
  4. 协议转换网关收到报文,按照嵌入的IPv4地址组装IPv4头,取出IPv6源地址的低2字节作为checksum装入IPv4头的checksum字段。
  5. IPv4报文发出到IPv4网络。

这便解放了协议转换网关的算力资源。


遗留的问题是,IPv6始发站如何识别一个报文是不是发往IPv4网络的,如何触发它去按照内嵌IPv4地址去生成伪头以及去计算一个IPv4头的校验码,这除了RFC6052之外,就看应用层的配置了,一个sockopt规则灌下去,非常容易做到!


既然IPv4和IPv6要互联互通,肯定是需要协议转换设备了,我本来就是一个设备迷而不是很care什么软件,所以,本文也是看了下面的新闻才有感而发的:
国内首个IPv6翻译设备认证出炉 北京英迪瑞讯IVI通过IPv6 认证: http://www.qianjia.com/zhike/201904/031822517170.html

我可能思想过于老套了,但我依然觉得设备才是重要的,软件灌入设备卖出去才有效。最关键的,我不喜欢互联网软件的原因是,互联网软件的代码一般都很low,毕竟服务器都在这些互联网公司自己的机房,出了问题远程登录即可排错,而设备是卖出去到客户那里的,排错成本高昂,所以必须精雕细琢不断测试。


清明时节,没有雨纷纷,所以皮鞋就不会湿,所以也就更加不会胖。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/261846
推荐阅读
相关标签
  

闽ICP备14008679号