捕获报文

如果你想解析报文,首先要做到的就是捕获到整个报文。而捕获到全部网卡接收到的报文信息,那么就要设置socket的模式。

socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IPV6));

AF_PACKET 定义在 socket.h 中 #define AF_PACKET PF_PACKET 所以说实际使用中,使用AF 和 PF 都是一样的。 SOCK_RAW 定义在 socket_type.h 中, 其含义是创建原始套接字。 socket的第二个参数可以被设置为SOCK_DGRAM,主要区别是当指定SOCK_DGRAM时,获取的数据包是去掉了数据链路层的头(link-layer header),当指定SOCK_RAW时,获取的数据包是一个完整的数据链路层数据包.

#define SOCK_DGRAM SOCK_DGRAM
  SOCK_RAW = 3,            /* Raw protocol interface.  */

第三个参数是指定协议类型,需要注意的是需要使用htos将其转化为网络字节序。 其它数据链路层类型定义的常量参数有 ETH_P_ARP 和 ETH_P_IPV6 等等。使用时需要 #include <linux/if_ether.h> 。 指定协议(protocol)为ETH_P_XXX即告诉数据链路层我只接收该类型的帧(frame),socket会自动过滤。如果数据链路支持混杂模式(promiscuous mode),可以设置socket可选参数PACKET_ADD_MEMBERSHIP选项,使用packet_mreq结构体指定一个以太网接口和混杂模式行为(PACKET_MR_PROMISC)。

经过这一系列的设置我们就可以捕获到IPV6包的原始信息(包含整个数据包).其他还要对这个套接字进行一系列的设置。

使用 BPF 进行过滤

BPF(Berkeley Packet Filter)伯克利包过滤器。最初构想提出于 1992 年,其目的是为了提供一种过滤包的方法,并且要避免从内核空间到用户空间的无用的数据包复制行为。它最初是由从用户空间注入到内核的一个简单的字节码构成,它在那个位置利用一个校验器进行检查 —— 以避免内核崩溃或者安全问题 —— 并附着到一个套接字上,接着在每个接收到的包上运行。几年后它被移植到 Linux 上,并且应用于一小部分应用程序上(例如,tcpdump)。其简化的语言以及存在于内核中的即时编译器(JIT),使 BPF 成为一个性能卓越的工具。

同样使用 setsockopt 函数进行设置 setsockopt(sd, SOL_SOCKET, SO_ATTACH_FILTER, &Filter, sizeof(Filter));
其中 Filter 是一个 sock_fprog 结构。具体结构如下。

struct sock_fprog       /* Required for SO_ATTACH_FILTER. */
{
        unsigned short          len;    /* Number of filter blocks */
        struct sock_filter      *filter;
};

struct sock_filter      /* Filter block */
{
        __u16   code;   /* Actual filter code */
        __u8    jt;     /* Jump true */
        __u8    jf;     /* Jump false */
        __u32   k;      /* Generic multiuse field */
};

实际使用中需要指定匹配规则,下面是过滤取得一个 icmpv6 的例子。里面的一些类似数组一样的东西如何知道怎么填写呢?
这里就用到了 tcpdump 的 -dd 选项

-dd Dump packet-matching code as a C program fragment.
使用 tcpdump icmp6 -dd 。我们就能得到这种过滤器的代码了。

    struct sock_filter filter[] = {
        {0x28, 0, 0, 0x0000000c},
        {0x15, 0, 6, 0x000086dd},
        {0x30, 0, 0, 0x00000014},
        {0x15, 3, 0, 0x0000003a},
        {0x15, 0, 3, 0x0000002c},
        {0x30, 0, 0, 0x00000036},
        {0x15, 0, 0, 0x0000003a},
        {0x06, 0, 0, 0x00000640},
        {0x06, 0, 0, 0x00000000},
    };

    struct sock_fprog sprog = {
        .len = sizeof(filter) / sizeof(struct sock_filter),
        .filter = filter,
    };

    setsockopt(raw_sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &sprog, sizeof(sprog))

Bind socket

如果要获取从指定以太网接口卡上的数据包时,在 struct sockaddr_ll中指定网络接口卡,绑定(bind)数据包到该interface上。只有sll_protocol和 sll_ifindex这两个地址字段是用来bind的。

/* 定义在 if_packet.h 中 */

struct sockaddr_ll {
    unsigned short    sll_family;
    __be16        sll_protocol;
    int        sll_ifindex;
    unsigned short    sll_hatype;
    unsigned char    sll_pkttype;
    unsigned char    sll_halen;
    unsigned char    sll_addr[8];
};

sll_protocol : 标准以太网协议类型,按网络字节顺序。定义在 if_ether.h 中。 sll_ifindex: interface索引,0 匹配所有的网络接口卡; sll_hatype: ARP 硬件地址类型(hardware address type) 定义在 if_arp.h 中,常用 ARPHRD_ETHER sll_pkttype: 包含了packet类型。 PACK_HOST 包地址为本地主机地址。 PACK_BROADCAST 物理层广播包。 PACK_MULTICAST 发送到物理层多播地址的包。 PACK_OTHERHOST 发往其它在混杂模式下被设备捕获的主机的包。 PACK_OUTGOING 本地回环包; sll_addr 和 ssl_halen 包含了物理层地址和其长度;

当发送数据包时,指定 sll_family, sll_addr, sll_halen, sll_ifindex, sll_protocol 就足够了。其它字段设置为0; sll_hatype和 sll_pkttype是在接收数据包时使用的;** 如果要bind, 只需要使用 sll_protocol和 sll_ifindex**。

bind(raw_sockfd, (struct sockaddr*)(&ll), sizeof(ll))

解析报文

通过上面的方法,就可以接收到全部的网络报文。要解析报文首先还是要对报文的头部以及内容进行获取。其中头部用来验证报文类型。 头部解析报文可以通过将报文数据移动指针位置之后转换成对应格式的结构就可以了。要注意的是 IPV6 的拓展头部处理。 头部结构可以看后面的 以太网头结构 部分。

static struct ether_header* eth_header(void *pkt, size_t *offset)
{
    *offset = 0;
    return (struct ether_header*)pkt;
}

static struct ip6_hdr* ip6_header(void *pkt, size_t *offset)
{
    *offset += sizeof(struct ether_header);
    return (struct ip6_hdr*)(pkt + (*offset));
}

struct icmp6_hdr* icmp6_header(void *pkt, size_t *offset)
{
    *offset += sizeof(struct ip6_hdr);
    struct ip6_hdr *hdr = (struct ip6_hdr*)pkt;

    if(hdr->ip6_nxt == IPPROTO_ICMPV6)
        return (struct icmp6_hdr*)(hdr+1);

    /* if it have extension header */
    struct ip6_ext *ehdr = (struct ip6_ext*)(hdr + 1);
    *offset += (ehdr->ip6e_len + sizeof(struct ip6_ext));
    while(ehdr->ip6e_nxt != IPPROTO_ICMPV6){
        ehdr = (struct ip6_ext*)((void *)ehdr + ehdr->ip6e_len + sizeof(struct ip6_ext));
        *offset += (ehdr->ip6e_len + sizeof(struct ip6_ext));
    }
    return (struct icmp6_hdr*)((void *)ehdr + ehdr->ip6e_len + sizeof(struct ip6_ext));
}

接下来就可以根据 icmp6_header 获取想要的类型。

这里有一个对 icmp6 比较适合的结构

struct icmp6{
    union
    {
        struct nd_router_advert ra;
        struct nd_router_solicit rs;
        struct nd_neighbor_solicit ns;
        struct nd_neighbor_advert  na;
        struct icmp6_hdr comm;
    };
    size_t len;
    size_t opt_cnt;
    struct ip6_hdr ip6hdr;
    struct ether_header ethaddr;
    union icmp6_opt opt[0];       
};

在 <netinet/in.h> 中有很多对 ipv6 地址进行处理的宏,比如比较是否相等,等。 这个长度为0的数组是 GNU C 的特性,柔性数组。 具体的解析步骤和流程都在下面代码实例部分。

发送 NS 报文

创建套接字

socket(PF_INET6, SOCK_RAW, IPPROTO_ICMPV6);

绑定接口(网卡)

参数 ifname 就是接口的名字的字符串(ifconfig 中的名字)。

setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname)))

指定该数据包不需要路由:

int set_donot_route = 1; setsockopt(sockfd, SOL_SOCKET, SO_DONTROUTE, &set_donot_route, sizeof(set_donot_route)))

指定跳数:

must set to 255 or the LAN/WAN host will not accept this RA packet unsigned hops = 255; setsockopt(port->icmpv6_sock, SOL_IPV6, IPV6_MULTICAST_HOPS, &hops, sizeof(hops)))

设置发送时使用的地址或者网络接口

IPV6_PKTINFO Set the source address and/or interface out which the packet(s) is sent. Takes a struct in6_pktinfo as the parameter.

setsockopt(port->icmpv6_sock, SOL_IPV6, IPV6_PKTINFO, &pktinfo, sizeof(pktinfo))

struct in6_pktinfo {
	struct in6_addr	ipi6_addr;
	int		ipi6_ifindex;
};

其他

发送 NS 报文内容主要包含两个结构,一个是 struct nd_neighbor_solicit NS 本身的结构,还有就是 NS option 结构 struct nd_opt_linkaddr 这样就可以构成一个 NS 报文了。

NS

还有就是要注意, NS 的目标地址(Target Address) 应为前缀(F02::1:f/104) 加上 IPV6 地址的低 16 位。

具体代码可以看发送 NS 报文部分。

代码实例

ICMP 解析

struct icmp6* parse_icmp6(void *pkt, size_t len)
{
    size_t tmp;
    size_t hdrlen;
    size_t offset;
    static uint8_t buffer[1520];
    struct ether_header *ethdr = eth_header(pkt, &offset);
    struct ip6_hdr *ip6hdr = ip6_header(ethdr, &offset);
    struct icmp6_hdr * icmp6hdr = icmp6_header(ip6hdr, &offset);

    switch (icmp6hdr->icmp6_type)
    {
        case ND_ROUTER_ADVERT:
            hdrlen = sizeof(struct nd_router_advert);
            break;
        case ND_ROUTER_SOLICIT:
            hdrlen = sizeof(struct nd_router_solicit);
            break;
        case ND_NEIGHBOR_ADVERT:
            hdrlen = sizeof(struct nd_neighbor_advert);
            break;
        case ND_NEIGHBOR_SOLICIT:
            hdrlen = sizeof(struct nd_neighbor_solicit);
            break;
        default:
            return NULL;
    } 

    offset += hdrlen;
    tmp = offset;
    union icmp6_opt *opt = (union icmp6_opt*)(pkt + offset);
    size_t count = 0;
    while(offset < len && opt->comm.nd_opt_type && opt->comm.nd_opt_len){
        count += 1;
        opt = (union icmp6_opt*)((void*)opt + opt->comm.nd_opt_len * 8);
        offset += opt->comm.nd_opt_len * 8;
    }

    struct icmp6 *icmp6 = (struct icmp6*)buffer;

    icmp6->opt_cnt = count;
    if( !icmp6 ){
        handle_error("can not creat icmp6 obhect");
    }

    offset = tmp;
    memcpy(&icmp6->comm, icmp6hdr, hdrlen);
    for(size_t i=0; i<count; i++){
        opt = (union icmp6_opt*)(pkt + offset);
        memcpy(&icmp6->opt[i], opt, opt->comm.nd_opt_len * 8);
        offset += opt->comm.nd_opt_len;
    }
    memcpy(&icmp6->ethaddr, ethdr, sizeof(*ethdr));
    memcpy(&icmp6->ip6hdr, ip6hdr, sizeof(*ip6hdr));

    return icmp6;
}


static struct ether_header* eth_header(void *pkt, size_t *offset)
{
    *offset = 0;
    return (struct ether_header*)pkt;
}

static struct ip6_hdr* ip6_header(void *pkt, size_t *offset)
{
    *offset += sizeof(struct ether_header);
    return (struct ip6_hdr*)(pkt + (*offset));
}

struct icmp6_hdr* icmp6_header(void *pkt, size_t *offset)
{
    *offset += sizeof(struct ip6_hdr);
    struct ip6_hdr *hdr = (struct ip6_hdr*)pkt;

    if(hdr->ip6_nxt == IPPROTO_ICMPV6)
        return (struct icmp6_hdr*)(hdr+1);

    /* if it have extension header */
    struct ip6_ext *ehdr = (struct ip6_ext*)(hdr + 1);
    *offset += (ehdr->ip6e_len + sizeof(struct ip6_ext));
    while(ehdr->ip6e_nxt != IPPROTO_ICMPV6){
        ehdr = (struct ip6_ext*)((void *)ehdr + ehdr->ip6e_len + sizeof(struct ip6_ext));
        *offset += (ehdr->ip6e_len + sizeof(struct ip6_ext));
    }
    return (struct icmp6_hdr*)((void *)ehdr + ehdr->ip6e_len + sizeof(struct ip6_ext));
}

以太网头结构

内核中网络各个部分结构,与用户空间库提供的列举

Ether header

Kernel

#define	ETHER_ADDR_LEN		6	/* length of an Ethernet address */
struct	ether_header {
	u8 ether_dhost[ETHER_ADDR_LEN];
	u8 ether_shost[ETHER_ADDR_LEN];
	u16 ether_type;
} __attribute__((packed));

userspace

#define ETH_ALEN	6		/* Octets in one ethernet addr	 */
struct ethhdr {
	unsigned char	h_dest[ETH_ALEN];	/* destination eth addr	*/
	unsigned char	h_source[ETH_ALEN];	/* source ether addr	*/
	__be16		h_proto;		/* packet type ID field	*/
} __attribute__((packed));

Ip header

Kernel #include <linux/ip.h>

struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
	__u8	ihl:4,
		version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
	__u8	version:4,
  		ihl:4;
#else
#error	"Please fix <asm/byteorder.h>"
#endif
	__u8	tos;
	__be16	tot_len;
	__be16	id;
	__be16	frag_off;
	__u8	ttl;
	__u8	protocol;
	__sum16	check;
	__be32	saddr;
	__be32	daddr;
	/*The options start here. */
};

#include <linux/ipv6.h>

struct ipv6hdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
	__u8			priority:4,
				version:4;
#elif defined(__BIG_ENDIAN_BITFIELD)
	__u8			version:4,
				priority:4;
#else
#error	"Please fix <asm/byteorder.h>"
#endif
	__u8			flow_lbl[3];

	__be16			payload_len;
	__u8			nexthdr;
	__u8			hop_limit;

	struct	in6_addr	saddr;
	struct	in6_addr	daddr;
};

Usersapce

#include <netinet/ip.h>

struct iphdr
  {
#if __BYTE_ORDER == __LITTLE_ENDIAN
    unsigned int ihl:4;
    unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
    unsigned int version:4;
    unsigned int ihl:4;
#else
# error	"Please fix <bits/endian.h>"
#endif
    uint8_t tos;
    uint16_t tot_len;
    uint16_t id;
    uint16_t frag_off;
    uint8_t ttl;
    uint8_t protocol;
    uint16_t check;
    uint32_t saddr;
    uint32_t daddr;
    /*The options start here. */
  };

#include <netinet/ip6.h>

struct ip6_hdr
  {
    union
      {
	struct ip6_hdrctl
	  {
	    uint32_t ip6_un1_flow;   /* 4 bits version, 8 bits TC,
					20 bits flow-ID */
	    uint16_t ip6_un1_plen;   /* payload length */
	    uint8_t  ip6_un1_nxt;    /* next header */
	    uint8_t  ip6_un1_hlim;   /* hop limit */
	  } ip6_un1;
	uint8_t ip6_un2_vfc;       /* 4 bits version, top 4 bits tclass */
      } ip6_ctlun;
    struct in6_addr ip6_src;      /* source address */
    struct in6_addr ip6_dst;      /* destination address */
  };

#define ip6_vfc   ip6_ctlun.ip6_un2_vfc
#define ip6_flow  ip6_ctlun.ip6_un1.ip6_un1_flow
#define ip6_plen  ip6_ctlun.ip6_un1.ip6_un1_plen
#define ip6_nxt   ip6_ctlun.ip6_un1.ip6_un1_nxt
#define ip6_hlim  ip6_ctlun.ip6_un1.ip6_un1_hlim
#define ip6_hops  ip6_ctlun.ip6_un1.ip6_un1_hlim

ICMP header

Userspace

struct icmp6_hdr
  {
    uint8_t     icmp6_type;   /* type field */
    uint8_t     icmp6_code;   /* code field */
    uint16_t    icmp6_cksum;  /* checksum field */
    union
      {
	uint32_t  icmp6_un_data32[1]; /* type-specific field */
	uint16_t  icmp6_un_data16[2]; /* type-specific field */
	uint8_t   icmp6_un_data8[4];  /* type-specific field */
      } icmp6_dataun;
  };

#define icmp6_data32    icmp6_dataun.icmp6_un_data32
#define icmp6_data16    icmp6_dataun.icmp6_un_data16
#define icmp6_data8     icmp6_dataun.icmp6_un_data8
#define icmp6_pptr      icmp6_data32[0]  /* parameter prob */
#define icmp6_mtu       icmp6_data32[0]  /* packet too big */
#define icmp6_id        icmp6_data16[0]  /* echo request/reply */
#define icmp6_seq       icmp6_data16[1]  /* echo request/reply */
#define icmp6_maxdelay  icmp6_data16[0]  /* mcast group membership */

发送 NS 报文

int send_ns(struct port_t* port, struct icmp6 *icmp6, struct in6_addr *target)
{
    struct nd_neighbor_solicit ns;
    struct nd_opt_linkaddr opt_linkaddr;

    ns.nd_ns_type  = ND_NEIGHBOR_SOLICIT;
    ns.nd_ns_code  = 0;
    ns.nd_ns_cksum = 0;
    ns.nd_ns_reserved = 0;
    memcpy(&ns.nd_ns_target, target, sizeof(*target));
    opt_linkaddr.nd_opt_type = ND_OPT_SOURCE_LINKADDR;
    opt_linkaddr.nd_opt_len  = sizeof(opt_linkaddr) >> 3;
    memcpy(&opt_linkaddr.addr, &port->ethaddr, sizeof(port->ethaddr));

    struct in6_addr dstaddr;
    inet_pton(PF_INET6, "ff02::01:ff00:00", &dstaddr);
    memcpy(dstaddr.s6_addr + 13, target->s6_addr + 13, 3);
    
    char addr[60] = {0};
    char macaddr[20] = "";
    inet_ntop(AF_INET6, &dstaddr, addr, sizeof(addr));
    ether_ntoa_r((struct ether_addr*)&opt_linkaddr.addr, macaddr);
    debug("addr = %s\t macaddr = %s\n", addr, macaddr);

    int ret = send_icmp6_pkt(port, &dstaddr, 2, &ns, sizeof(ns), &opt_linkaddr, sizeof(opt_linkaddr));
    debug("send - ns!\n");
    return 0;  
}


ssize_t send_icmp6_pkt(struct port_t *port, struct in6_addr* to, ssize_t iovec_count, ...)
{
    ssize_t ret;
    va_list val;
    struct in6_pktinfo pktinfo;
    struct msghdr msghdr;
    struct sockaddr_in6 si6;
    struct iovec iovec[iovec_count];

    va_start(val, iovec_count);
    for(size_t i=0; i < iovec_count; i++){
        iovec[i].iov_base = va_arg(val, void*);
        iovec[i].iov_len  = va_arg(val, size_t);
    }
    va_end(val);

    /* set destination address */
    memset(&si6, 0, sizeof(si6));
    si6.sin6_family = PF_INET6;
    memcpy(&si6.sin6_addr, to, sizeof(si6.sin6_addr));

    /* set source address */
    if(0x001 == (to->s6_addr[0]>>5)){
        /*if destination is gloabal addree, use global address as source address*/
        memcpy(&pktinfo.ipi6_addr, &port->globalip6addr, sizeof(struct in6_addr));
        pktinfo.ipi6_ifindex = port->ifindex;
    }else{
        memcpy(&pktinfo.ipi6_addr, &port->localip6addr, sizeof(struct in6_addr));
        pktinfo.ipi6_ifindex = port->ifindex;
    }
    if(0 > setsockopt(port->icmpv6_sock, SOL_IPV6, IPV6_PKTINFO, &pktinfo, sizeof(pktinfo)))
        handle_error("failed setsockopt");

    memset(&msghdr, 0, sizeof(msghdr));
    msghdr.msg_iov      = iovec;
    msghdr.msg_iovlen   = iovec_count;
    msghdr.msg_name     = &si6;
    msghdr.msg_namelen  = sizeof(si6);

    do{
        ret = sendmsg(port->icmpv6_sock, &msghdr, 0);
    }while(ret < 0 && errno == EINTR);

    return ret;
}

参考

https://www.cnblogs.com/rollenholt/articles/2585517.html

https://linux.cn/article-9507-1.html