Fork me on GitHub

从1写TCPIP协议栈4:ARP协议实现

引言

上节提到ARP协议中协议类型字段需要注意大小端的问题。实际数据传输时,只拿到主机的IP地址是不行的,这样数据只能传递至链路层,因此必须还要拿到的MAC地址,ARP就是解决48位的MAC地址和32位的IPV4地址之间的对照和映射问题。ARP的映射是一种动态映射,因为实际网络中的网卡数量不定,部分机器下线后同一IP可能DHCP分配给其他网卡。ARP采用广播包,只适用于IPV4,IPV6使用邻居发现协议。除此之外,反向ARP(RARP)几乎已经被弃用(RFC0903)。

ARP实现:地址-IP映射表的可增删改

ARP报文结构体定义

“引言”中已经提到,ARP映射IP地址和MAC地址时并不是固定的,因此协议栈开发时需要提供ARP映射表的可增/删改-机制,实现可增删改的前提就是需要检查表中无效和错误的表项。本项目中只考虑一个表,基础代码实现为:

typedef union _arp_table_type
{
	uint8_t array[IPV4_ADDR_SIZE];
	uint32_t addr;
}_arp_table_type;
//arp1表项
typedef struct _arp_table
{
	_arp_table_type ipaddr;//两种表示方式
	uint8_t macaddr[LLC_MAC_SIZE];
	uint8_t tablestate;//表状态,这里只有一个表(结构体)
	uint16_t ttl;//生存时间/超时时间
	uint8_t trycount;//错误后重试的次数
}_arp_table;
static _arp_table _arp_table1;//只有一个表项
//地址映射表更新
_type_drv_err _arp_update_maptable(uint8_t* srcip,uint8_t *srcmac)
{
	memcpy(_arp_tableOne.ipaddr.array,srcip,SIZE_IPV4_ADDR);
	memcpy(_arp_tableOne.macaddr, srcmac,SIZE_ETHII_MAC);
	_arp_tableOne.tablestate = XARP_TABLE_OK;
	_arp_tableOne.trycount = XARP_CFG_MAX_RETRIES;//超时时间
	_arp_tableOne.ttl = XARP_CFG_ENTRY_OK_TTL;//生存时间
	printf("[ARP Table Update] MAC: %d  IP:%d ", srcmac,srcip);
	
}

MAC-IP映射表更新

//地址映射表更新
_type_drv_err _arp_update_maptable(uint8_t* srcip,uint8_t *srcmac)
{
    memcpy(_arp_tableOne.ipaddr.array,srcip,SIZE_IPV4_ADDR);
	memcpy(_arp_tableOne.macaddr, srcmac,SIZE_ETHII_MAC);
	_arp_tableOne.tablestate = XARP_TABLE_OK;
	_arp_tableOne.trycount = XARP_CFG_MAX_RETRIES;//超时时间
	_arp_tableOne.ttl = XARP_CFG_ENTRY_OK_TTL;//生存时间
}

我们也可以arp -a查看本机的ARP地址表:

ARP地址表中有动态和静态,动态ARP映射就是可以动态变化的。如前文所述:当动态ARP映射关系产生冲突时,一般都是停止使用此地址或者不理会继续使用;对于静态ARP地址映射,一般我们希望某些端口和IP是固定在某个VLAN下面,此类地址冲突时会发送ARP防御通告,如果冲突继续,继续使用此映射关系,因此静态的ARP映射优先级最高,可以理解为主机初始化阶段自行写入的~

ARP表超时机制

//arp表查询函数
void _arp_poll(void)
{
	if (_eth_check_ttl(&_arp_time, XARP_TABLE_CHECK_TIME))//_eth_check_ttl:运行工程1s后启动ARP表项扫描
	{
		switch (_arp_tableOne.tablestate)//查看表状态
		{
		case XARP_TABLE_STATE_OK://表状态ok
			if ((--_arp_tableOne.ttl) == 0)//ttl-1后跳出继续下一秒的操作
			{
				_arp_make_request(&_arp_tableOne.ipaddr);//查询arp表中的地址是否还存在,存在时在update函数会更新表项,ttl重新回到最大值
				_arp_tableOne.tablestate = XARP_TABLE_STATE_PENDING;//设置当前表状态为查询中,跳转进XARP_TABLE_STATE_PENDING
				_arp_tableOne.ttl = XARP_TABLE_TTL_PENDING;//为跳入case XARP_TABLE_STATE_PENDING 准备
			}
			break;
		case XARP_TABLE_STATE_PENDING:
			if ((--_arp_tableOne.ttl) == 0)//一般配置为1s,也就是紧接case XARP_TABLE_STATE_OK
			{
				if ((_arp_tableOne.trycount--) == 0)//在update函数中如果没有收到arp响应,并且超时次数超时,-释放掉
				{
					_arp_tableOne.trycount = XARP_TABLE_MAX_RETRIES;
					_arp_tableOne.ttl = XARP_TABLE_TTL;//5
					_arp_tableOne.tablestate = XARP_TABLE_Free;//0
				}
				else//继续尝试
				{
					_arp_make_request(&_arp_tableOne.ipaddr);//查询arp表中的地址是否还存在
					_arp_tableOne.tablestate = XARP_TABLE_STATE_PENDING;//设置当前表状态为查询中,跳转进XARP_TABLE_STATE_PENDING
					_arp_tableOne.ttl = XARP_TABLE_TTL_PENDING;//最大查询时间1s
				}
			}
			break;
		}
	}

可能这里大家有点乱,没关系,其实ARP表项的超时机制核心就是:协议栈启动一定时间后启动ARP表项检查,检查时根据表状态分类,表ok状态时,ttl开始定期自减,为0后进入表状态pending发送ARP请求;表状态pending后开始连续数次发送ARP请求等待ARP响应。整个过程中ARP的输入检查一直while(1),这意味着一旦接收到ARP响应,表的各项参数全部恢复默认值:

//地址映射表更新--下文的数据输入中就有
_type_drv_err _arp_update_maptable(uint8_t* srcip,uint8_t *srcmac)
{
	memcpy(_arp_tableOne.ipaddr.array,srcip,SIZE_IPV4_ADDR);
	memcpy(_arp_tableOne.macaddr, srcmac,SIZE_ETHII_MAC);
	_arp_tableOne.tablestate = XARP_TABLE_STATE_OK;
	_arp_tableOne.trycount = XARP_TABLE_MAX_RETRIES;//超时时间
	printf("[ARP Table Update] MAC: %d  IP:%d ", srcmac,srcip);

}

ARP实现:数据输入与输出

数据输出:免费ARP实现

免费ARP指在主机启动时向网络其他主机通告的自己主机地址的行为,报文中发送方和接收方的MAC与IP都是发送方信息,其他主机在收到此ARP广播包后更新自己地址映射表中的IP-MAC映射,理想状态下主机不会收到任何回复。若网络中存在与自己IP重复的主机,此时就会报错。基于此问题RFC5227提出了ACD地址冲突检测机制,地址冲突机制要求ARP报文的目的MAC/IP地址都为0,以免准备使用的正确的IPV4地址在其他主机被更新/污染。因此ACD机制下的ARP也是免费ARP。

如果没有查到冲突,此时主机会间隔2s向网络发送两个完全体的免费ARP报文(目标和源mac/IP都是发送方)来告知其他主机更新自己地址映射表,代码实现如下:

//arp包结构
typedef struct _arp_packet
{
	uint16_t hardtype, prtcltype;
	uint8_t hardsize, prtclsize;
	uint16_t option;
	uint8_t sormac[LLC_MAC_SIZE];
	uint8_t destmac[LLC_MAC_SIZE];
	uint8_t sorIP[IPV4_ADDR_SIZE];
	uint8_t destIP[IPV4_ADDR_SIZE];
}_arp_packet;

int _arp_make_request(const _arp_ip_type ipaddr)
{
	_eth_packet* packet = _eth_packet_tx(sizeof(packet));
	_arp_packet* arppacket = (_arp_packet*)packet->dataptr;
	arppacket->hardtype = swap_order16(XARP_HW_TYPE);//硬件类型
	arppacket->prtcltype = swap_order16(TCPIP_PROTOCOL);//协议类型
	arppacket->hardsize = SIZE_ETHII_MAC;//硬件长度
	arppacket->prtclsize = SIZE_IPV4_ADDR;//协议长度
	arppacket->option = swap_order16(XARP_OP_REAUEST);//请求
	memcpy(arppacket->sormac, _mac_cfg);//源mac
	memcpy(arppacket->sorIP, _ip_cfg.array,SIZE_IPV4_ADDR);//源ip
	memcpy(arppacket->destmac, 0,SIZE_ETHII_MAC);//目标mac,接收方为全0,避免被污染
	memcpy(arppacket->destIP,ipaddr.array,SIZE_IPV4_ADDR);//目标IP,也应该为0
	return _eth_drive_send(ARP_PROTOCOL,_mac_boardcast,packet);
}

本次练习我们不进行完整的冲突检测开发,那实际ACD机制具体如何处理冲突呢?RFC5227表明,如果发送方在其他主机的免费ARP报文中发现了自己的主机IP,视为冲突。地址冲突目前有三种解决方式:停止使用、继续使用、发送防御ARP报文后继续使用此地址。

数据输入:报文检查与响应

除了免费ARP和地址映射表之外,就是对ARP报文做接收和响应,其中接收的实现思想代码体现为:

驱动层收包

	_eth_packet* packet;
	if (_eth_drive_read(&packet) == DRIVE_ERR_OK)
	{
		_ethernet_input(packet);
	}

包检查

static _type_drv_err _ethernet_input(_eth_packet* packet/*发送的包*/)
{
	_ethII_packet* eth_header = NULL;//用于解析mac地址
	if (packet->size <= sizeof(_ethII_packet))//不能比header小
	{
		return;
	}

	eth_header = (_ethII_packet*)packet->dataptr;
	switch (swap_order16(eth_header->Type))
	{
	case ARP_PROTOCOL://ARP协议的话
		_eth_packet_del_header(packet,sizeof(_ethII_packet));//删除链路层包头,
		_arp_check_in(packet);//检查ARP
		break;
	case TCPIP_PROTOCOL:break;//预留接口给TCPIP协议
	}
	return DRIVE_ERR_OK;
}

具体的ARP包检查实现思想为:

void _arp_check_in(_eth_packet* packet)
{
	if (packet->size < sizeof(_arp_packet))
		return;//不做响应
	_arp_packet* arp_packet = (_arp_packet*)packet->dataptr;
	uint16_t opcode = swap_order16(arp_packet->option);
	printf("arp opcode = 0x%llx \n", opcode);
	//判断包字段
	if (swap_order16(arp_packet->hardtype) != XARP_HW_TYPE || arp_packet->hardsize != SIZE_ETHII_MAC ||
		swap_order16(arp_packet->prtcltype) != TCPIP_PROTOCOL || arp_packet->prtclsize != SIZE_IPV4_ADDR ||
		(opcode != (XARP_OP_REPLY || XARP_OP_REAUEST))
		)
	{
        //打印错误的信息
		printf("hardtype = 0x%llx \n", swap_order16(arp_packet->hardtype));
		printf("hardsize = %d  \n", arp_packet->hardsize);
		printf("prtcltype = 0x%llx \n", swap_order16(arp_packet->prtcltype));
		printf("prtclsize = %d \n", arp_packet->prtclsize);
		printf("option = %d", opcode);
		return;//不做响应
	}
    //检查包的目标IP地址是不是本机地址
	if (!IP_equal(&_ip_cfg,arp_packet->destIP))
	{
		return;
	}

    //OP:1-ARP请求,2-ARP响应,3-RARP请求,4-Rarp响应
	switch (opcode)
	{
	case XARP_OP_REAUEST:
		_arp_make_response(arp_packet);//封装响应包
		_arp_update_maptable(arp_packet->sorIP,arp_packet->sormac);//更新地址映射表,对应上文ARP超时机制
		break;
	case XARP_OP_REPLY: 
		_arp_update_maptable(arp_packet->sorIP, arp_packet->sormac);//更新地址映射表
		break;
	}
}

包响应

包响应接口封装一个格式合格的ARP报文并进行发送:

//响应发送
_type_drv_err _arp_make_response(_arp_packet * arp_packet)//传入接收到的ARP报文
{
	_eth_packet* packet = _eth_packet_tx(sizeof(_arp_packet));
	_arp_packet* response_pocket = (_arp_packet*)packet->dataptr;
	response_pocket->hardtype = swap_order16(XARP_HW_TYPE);
	response_pocket->prtcltype = swap_order16(TCPIP_PROTOCOL);
	response_pocket->hardsize = SIZE_ETHII_MAC;
	response_pocket->prtclsize = SIZE_IPV4_ADDR;
	response_pocket->option = swap_order16(XARP_OP_REPLY);//响应
	memcpy(response_pocket->sormac, _mac_cfg, SIZE_ETHII_MAC);
	memcpy(response_pocket->sorIP, _ip_cfg.array, SIZE_IPV4_ADDR);
	memcpy(response_pocket->destmac, arp_packet->sormac, SIZE_ETHII_MAC);//目的MAC为接收到的ARP报文源MAC
	memcpy(response_pocket->destIP, arp_packet->sorIP, SIZE_IPV4_ADDR);//目的IP为接收到的ARP报文源IP
	return _ethernet_send(ARP_PROTOCOL, arp_packet->sormac, packet);//利用驱动层函数发送ARP包,驱动层函数会进行MAC的增加,请参考本白其他文章
}

调试

当你已经实现了上述的几个接口后,便可以试运行,只看ARP协议:

后面的两条ARP是因为ICMP-PING产生的:

当ARP完成之后,我们查看虚拟机的IP-MAC地址映射,可以看出已经更新:

我们再正常进行主机和虚拟机的互PING操作:

可以看出每工程进行一次PING操作,发送方都会向接收方发送自己的ARP报文以求更新和同步MAC地址。(这里可能不对,本白自己抓包了几次看现象都是这样的~)

关于ARP协议的简单思考

经过ARP协议的基础开发,已然能够发觉ARP协议存在的漏洞,那就是容易被欺骗,我只是用C++虚拟了一个主机就可以骗过本地主机和虚拟机参与其中的通讯,因此ARP机制的安全性是非常值得思考的。根据小白之前的所见所闻,可能的的手段有:

1、增添协议的复杂性,比如添加一些签名和校验形成新的ARP协议。
2、在物理层对ARP的各主机进行专网划分并校验VLANID。
3、可以修改ARP报文的传播方式,将广播修改为单播。
4、与DHCP进行集合形成带有某种复杂规律的动态MAC-IP映射关系。
5、将静态IP和实际网络传输的IP进行某种掩码的换算,比如异或算法,又比如使用E2E的DataID,再指定一个中间转换没有规律的映射表。
... ...

在车联网程度越来越高的今天,其实这些安全手段的本质就是为了保护MAC-IP的映射表关系不被外界轻易嗅探,这对以太网的安全来说是极大的挑战,因为通信速率越高,网内交互越复杂,需要辅助的协议就越多,协议越多,能被攻击的点越多,各位刺客的“妙手”可就太多了,哈哈哈。这样看来Flexray还真是挺好用的,除了速率不太行/成本较高,其他的优点真是诀绝子。可能有朝一日,大家就会发现对以太网的维护成本上升到接近甚至超过Flexray的使用成本,可能以太网也会引入TDMA机制,届时车载以太网可能又会提出什么Flex-ETH之类的协议,将车载网络推向新的高度~当然,就目前来说个人觉得采用2+5的方式基本可以实现ARP的保护。

附录

ARP参考文档:RFC826

ADC参考文档:RFC5227

RFC官方文档:网址

posted @ 2023-02-07 22:36  张一默  阅读(178)  评论(0编辑  收藏  举报