红联Linux门户
Linux帮助

Linux下使用原始套接字实现ping命令

发布时间:2017-03-25 11:25:42来源:linux网站作者:staticnetwind
一、背景需求
客户端程通过透明代理访问远程服务器,代理需要以SNAT去修改源地址源端口,一般写法是Add SNAT、Connect、Del SNAT;
那么问题来了,加SNAT规则时需要 -s $ip --sport $port (避免多个客户端互相混淆),若正好代理机器上存在多个地址时,调用Connect之前Socket并不知道需要绑定哪个出口地址,那怎么获取到$ip、$port呢?
我的思路是需要在Connect动作之前,目的服务器地址是已知的,通过发送ICMP echo 来确定本机的出口地址;
 
二、相关知识
2.1 IP路由选择过程
假设当前Linux具备多个网卡若干个地址,那么在路由表上将存在各个网段的默认路由(route -n / netstat -nr);
《CCNA》P257-261中提到了一个最简单的IP路由选择的过程就是ping操作,大致步骤就是:
1)ICMP创建回应请求数据包,IP协议创建分组;
2)IP协议判断目的IP地址为本地网络还是远程网络;
3)若目的为远程网络,分组需要先发送给默认网关(以默认网关的MAC地址发送,帧的形式);
4)网关收到IP分组,检查IP目的地址是否匹配网关的路由表项,得到下一跳路径,若找不到相关表项则丢弃分组;
5)循环第4步骤,最后服务器收到分组(网络层)完成目的地址的匹配,生成一个新的有效荷载递交到ICMP;
6)上述ICMP需要成功返回到最初的客户端,完成一个PING的过程;
2.2 ICMP结构
ICMP包含在IP分组中,所以整体结构是20字节的IP头+8字节ICMP头+ICMP载荷
Linux下使用原始套接字实现ping命令
我们在ping程序中使用的是ICMP echo request / reply 信息,注意request.type=8,reply.type=0
Linux下使用原始套接字实现ping命令Linux下使用原始套接字实现ping命令
 
三、编程实现
ping的编程发送三层IP分组时,需要用到原始套接字(raw socket),参考《UNP》书中的方法
“socket(PF_INET, SOCK_RAW, IPPROTO_ICMP”
int ping(char *dst_ip)  
{  
int ret = FAILURE;  
int sd = 0;   
char buf[SIZE_LINE_NORMAL] = {0};
struct ip *ip = NULL;  
struct sockaddr_in dst_addr = {0};  
struct icmp icmp_packet = {0};
struct timeval tm = {.tv_sec = 1, .tv_usec = 0};
fd_set rdfds;
if ( !dst_ip ) {  
perror("NULL\n");  
goto _E1;  
}
sd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP);  
if ( sd < 0 ) {  
perror("socket\n");  
goto _E1;  
}
dst_addr.sin_family = AF_INET;  
dst_addr.sin_addr.s_addr = inet_addr(dst_ip);
ret = gen_icmp_packet(&icmp_packet, 8, 1);  
if ( SUCCESS != ret ) {  
goto _E2;  
}
ret = sendto(sd, &icmp_packet, sizeof(struct icmp), 0,  
(struct sockaddr *)&dst_addr, sizeof(struct sockaddr_in));  
if ( ret < 0 ) {  
perror("sendto\n");  
goto _E2;  
}  
printf("Send ping sucess!\n");
/* Timeout 1s to recv icmp */  
FD_ZERO(&rdfds);  
FD_SET(sd, &rdfds);
ret = select(sd + 1, &rdfds, NULL, NULL, &tm);  
if ( -1 == ret && EINTR != errno ) {  
/* if serial error */  
perror("select fail\n");  
goto _E2;  
}  
else if ( 0 == ret ) {  
/* timeout */  
perror("recv timeout\n");  
ret = FAILURE;  
goto _E2;  
if ( FD_ISSET(sd, &rdfds) ) {  
ret = recv(sd, buf, sizeof(buf), 0);  
if ( ret <= 0 ) {  
perror("recv\n");  
goto _E2;  
}
ip = (struct ip *)buf;  
printf("from: %s\n", inet_ntoa(ip->ip_src));  
printf("  to: %s\n", inet_ntoa(ip->ip_dst));  
}
ret = SUCCESS;  
_E2:  
CLOSE_SOCK(sd);  
_E1:  
return ret;  
}
以上该注意的是并不是发送出ICMP echo request 就结束了,别忘了需求是获取出口地址;
所以,又结合 select + recv 的方式,超时1秒去等待 ICMP echo reply,然后再获取出口地址;
由于未使用“setsockopt (..., IPPROTO_IP, IP_HDRINCL, ...);” ,由系统自动填充IP头,所以我们只需要 gen_icmp_packet 去填充 icmp内容即可;
int gen_icmp_packet(struct icmp *icmp_packet, int type, int seq)  
{  
if ( !icmp_packet ) {  
perror("NULL\n");  
return FAILURE;  
}
icmp_packet->icmp_type  = type;  
icmp_packet->icmp_code  = 0;  
icmp_packet->icmp_cksum = 0;  
icmp_packet->icmp_id  = htons(getpid());  
icmp_packet->icmp_seq = htons(seq);
gettimeofday((struct timeval *)icmp_packet->icmp_data, NULL);  
icmp_packet->icmp_cksum = api_checksum16((unsigned short *)icmp_packet, sizeof(struct icmp));
return SUCCESS;  
}  
同时需要进行crc16的校验码
u16 api_checksum16(u16 *buffer, int size)  
{  
u32 cksum = 0;
if ( !buffer ) {   
perror("NULL\n");  
return 0;  
}
while ( size > 1 ) {   
printf("1. Cksum: 0x%08x + 0x%04x\n", cksum, *buffer);  
cksum += *buffer++;  
size -= sizeof(u16);  
}
if ( size ) {   
cksum += *(u8 *)buffer;  
}
printf("2. Cksum: 0x%08x\n", cksum);
/* 32 bit change to 16 bit */  
while ( cksum >> 16 ) {   
cksum = (cksum >> 16) + (cksum & 0xFFFF);  
printf("3. Cksum: 0x%08x\n", cksum);  
}
return (u16)(~cksum);  
}  
所以,一个基础的ping命令就完成了。
 
四、总结
利用ICMP可以获取出口地址,透明代理就可以针对目的地址进行一个出口地址的缓存。
若ICMP不可达,也不一定表示ICMP request 未送达目的,是存在 ICMP reply 回不来的可能性的,那么又如何获取到出口地址呢?
是否有直接查找路由表的编程方法?
平行思考,Nginx upstream的时候,是不是也有类似的选路过程?
 
本文永久更新地址:http://www.linuxdiyf.com/linux/29473.html