SOCK_RAW

关于 SOCK_RAW 的详细内容,参见man 7 raw,简单来说就是

raw_socket = socket(AF_INET, SOCK_RAW, int protocol);

可以创建一个 raw socket,可以发送纯 IP 报文,如果protocol设置为IPPROTO_RAW,报文所有细节,包括首部都需要自己填写,可以使用netinet/ip.h头文件中的struct ip等结构体,如果要发送 ICMP 报文,可以设置protocolIPPROTO_ICMP,参见netinet/ip_icmp.h

但是,根据man文档所说,IPPROTO_RAW 和 IPPROTO_ICMP 都是只能发送不能接收的,要接收的话,还得使用

sockfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

PA_PACKET

详情参见man 7 packet,下面是我自己写的一个简单抓包程序,为了方便演示,只写出解析 ICMP_ECHO 和 ICMP_ECHOREPLY 的代码,而且只能处理 IPv4:

#include "all.h"

int main(int argc, char **argv)
{
    int n, fd, addr, iphdrlen, ethhdrlen;
    char buf[MAXLINE];
    struct sockaddr_in sin;
    struct icmp *icmp;
    struct ip *ip;

    fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP));
    if (fd < 0)
        errsys("socket error");

    ethhdrlen = sizeof(struct ethhdr);     /* 链路层首部长度 */
    memset(&sin, 0, sizeof(struct sockaddr_in));
    inet_pton(AF_INET, "127.0.0.1", &addr);
    sin.sin_family = AF_PACKET;
    sin.sin_addr.s_addr = addr;

again:
    n = read(fd, buf, MAXLINE);
    if (n < 0)
        errsys("Recvived form ether error\n");
    ip = (struct ip *) (buf + ethhdrlen);   /* 跳过链路层首部 */
    iphdrlen = ip->ip_hl << 2;    /* IP 首部长度字段乘以 4,得到首部字节数 */
    printf("Received %d bytes data, protocol %s\n",
            n - ethhdrlen - iphdrlen,  /* IP 报文承载的长度 */
            (ip->ip_p == IPPROTO_UDP) ? "UDP" :
            (ip->ip_p == IPPROTO_TCP) ? "TCP" :
            (ip->ip_p == IPPROTO_ICMP) ? "ICMP" : "Unknown"
          );

    /* 跳过链路层和 IP 首部,得到 ICMP 报文地址 */
    icmp = (struct icmp *) (buf + ethhdrlen + iphdrlen);
    if ((icmp->icmp_type != ICMP_ECHO) &&
        (icmp->icmp_type != ICMP_ECHOREPLY)) {
        ;
    } else {
        printf("type %d, code %x, checksum %x, id %hu, sequence %hu\n",
               icmp->icmp_type, icmp->icmp_code, icmp->icmp_cksum,
               icmp->icmp_id, icmp->icmp_seq);
    }
    goto again;

    return 0;
}

默认情况下,所有符合protocol的链路层报文都会在传给内核之前传给PF_PACKET,如果设置ETH_P_ALL,就会收到所有的报文了,所以就可以很容易地做一个抓包程序。

然后,是一些解析包必要的头文件,linux/udp.hlinux/tcp.hnetinet/ip.hnetinet/ip_icmp.hlinux/if_ether.h

ETH_P_ALL

ETH_P_ALLETH_P_IP的区别,Google 一下一大堆,就不再赘述了。一个要点就是,一定记得加 htons(),为什么呢?因为 802.2 和 802.3 定义的帧使用两个字节表示协议类型,而 IP 数据报使用一个字节表示上层协议,所以就不用管字节序了。

struct sockaddr_ll

因为我要接收的仅仅是 IP 数据报,所以就使用了标准的 sockaddr_in 结构体,如果要做很强的设备相关的编程,建议详细阅读man文档。