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

1 篇文章 0 订阅
1 篇文章 0 订阅

一、背景需求

客户端程通过透明代理访问远程服务器,代理需要以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载荷

IP Datagram
  Bits 0–7 Bits 8–15 Bits 16–23 Bits 24–31
IP Header
(20 bytes)
Version/IHLType of serviceLength
Identificationflags and offset
Time To Live (TTL)ProtocolChecksum
Source IP address
Destination IP address
ICMP Header
(8 bytes)
Type of messageCodeChecksum
Header Data
ICMP Payload
(optional)
Payload Data

我们在ping程序中使用的是ICMP echo request / reply 信息,注意request.type=8,reply.type=0

00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Type = 8Code = 0Header Checksum
IdentifierSequence Number
Payload

00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Type = 0Code = 0Header Checksum
IdentifierSequence Number
Payload
 

三、编程实现

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的时候,是不是也有类似的选路过程?


参考文章:

[1] https://en.wikipedia.org/wiki/Ping_%28networking_utility%29

[2] http://www.cnblogs.com/chengliangsheng/p/3598845.html

[3] http://blog.csdn.net/qy532846454/article/details/5429700


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值