【openwrt学习笔记】IPV6 ND协议学习和socket编程

目录

    • 一、参考链接
    • 二、学习目标
    • 三、代码解析
      • [3.1 仅解析NA报文保存设备mac和ipv6地址信息](#3.1 仅解析NA报文保存设备mac和ipv6地址信息)
        • [3.1.1 open_ns_socket](#3.1.1 open_ns_socket)
        • [3.1.2 recv_ns_pack](#3.1.2 recv_ns_pack)
      • [3.2 解析NA和NS报文中DAD报文保存设备mac和ipv6地址信息](#3.2 解析NA和NS报文中DAD报文保存设备mac和ipv6地址信息)
        • [3.2.1 open_ns_na_socket](#3.2.1 open_ns_na_socket)
        • [3.2.2 recv_ns_na_pack](#3.2.2 recv_ns_na_pack)
    • 四、代码优化
      • [4.1 BPF参考学习资料](#4.1 BPF参考学习资料)
      • [4.2 代码实现](#4.2 代码实现)
        • [4.2.1 方式一:使用指令直接编写BPF程序](#4.2.1 方式一:使用指令直接编写BPF程序)
        • [4.2.1 方式二:使用 tcpdump -dd 命令生成BPF字节码](#4.2.1 方式二:使用 tcpdump -dd 命令生成BPF字节码)
      • [4.3 二者优缺点](#4.3 二者优缺点)

一、参考链接

IPv6知识 - ND协议【一文通透】
IPV6 ND协议--源码解析【根源分析】
Raw Socket 接收和发送数据包

二、学习目标

(1)使用socket进行网络编程,创建并接受icmpv6中的NS和NA报文;

(2)要解析出NS中的DAD报文和NA报文,需要保存其源mac地址和和请求ipv6地址,在路由器中可用于存储设备的mac地址

(说明:本笔记主要是实现从DAD报文中解析出源mac地址和和请求ipv6地址,原来的程序实现只是过滤NA报文,然后解析数据,但是经常会出现无法及时解析出设备ipv6地址,甚至长时间获取不到的情况,这里增加DAD检测,一开始就保存设备的所有ipv6地址,后续如果更新在将不使用的ipv6地址老化掉。)

(3)socket网络编程实战,之前没怎么实操过,这一次正好复习巩固。

三、代码解析

3.1 仅解析NA报文保存设备mac和ipv6地址信息

3.1.1 open_ns_socket
c 复制代码
int open_ns_socket(int idx) {
    struct icmp6_filter filt;  // ICMPv6消息过滤器
    int val = 0;               // 用于设置套接字选项的临时变量
    int sock = -1;             // 套接字描述符初始化为-1
    int ret = -1;              // 存储返回值的变量
    int buffersize = 100 * 1024;  // 接收缓冲区大小
    char *ifname = brname[idx];  // 网络接口名称

    // 创建用于ICMPv6通信的原始套接字
    sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
    if (sock < 0) {
        return -1;  // 若套接字创建失败,直接返回-1
    }

    // 设置ICMPv6过滤器,阻止所有ICMPv6消息并仅允许邻居通告消息通过
    ICMP6_FILTER_SETBLOCKALL(&filt);
    ICMP6_FILTER_SETPASS(ND_NEIGHBOR_ADVERT, &filt);
    ret = setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, &filt, sizeof(filt));
    if (ret < 0) {
        close(sock);  // 若设置失败,关闭套接字并返回-1
        return -1;
    }

    // 设置套接字接收缓冲区的大小
    ret = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &buffersize, sizeof(buffersize));
    if (ret < 0) {
        close(sock);  // 若设置失败,关闭套接字并返回-1
        return -1;
    }

    // 启用对特定IPv6多播消息的监听
    val = 1;
    ret = setsockopt(sock, IPPROTO_IPV6, IPV6_MDMAC, &val, sizeof(val));
    if (ret < 0) {
        close(sock);  // 若设置失败,关闭套接字并返回-1
        return -1;
    }

    // 绑定套接字到具体网络接口
    ret=setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname));
	if (ret < 0) //Dana
	{
		close(sock);
		return -1;
	}
	
	return sock;
}
3.1.2 recv_ns_pack
c 复制代码
int recv_ns_pack(int sock) {
    uint8_t buf[1024], cmsg_buf[64];  // 分别用于存储接收数据和控制消息的缓冲区
    struct cmsghdr *ch = NULL;  // 指向cmsghdr结构的指针
    ssize_t len = -1;  // 接收到的数据长度
    uint8 mac[ETH_ALEN];  // 用于存储MAC地址的数组
    struct sockaddr_in6 from;  // 存储源IPv6地址的结构
    struct iovec iov = {buf, sizeof(buf)};  // iov结构,指向数据缓冲区
    struct msghdr msg = {
        .msg_name = (void *) &from,  // 指向存放源地址的结构体
        .msg_namelen = sizeof(from),  // 地址结构体的长度
        .msg_iov = &iov,  // 指向iovec结构数组的指针
        .msg_iovlen = 1,  // iovec结构数组的长度
        .msg_control = cmsg_buf,  // 指向辅助数据的缓冲区
        .msg_controllen = sizeof(cmsg_buf),  // 辅助数据缓冲区的长度
        .msg_flags = 0  // 接收消息的标志位(未设置)
    };

    // 使用recvmsg非阻塞地接收数据
    len = recvmsg(sock, &msg, MSG_DONTWAIT);
    if (len <= 0)  // 如果读取失败或无数据可读,则返回-1
        return -1;

    // 遍历所有控制消息
    for (ch = CMSG_FIRSTHDR(&msg); ch != NULL; ch = CMSG_NXTHDR(&msg, ch)) {
        // 查找IPV6层级的控制消息,类型为MAC地址
        if (ch->cmsg_level == IPPROTO_IPV6 && ch->cmsg_type == IPV6_MDMAC) {
            // 将MAC地址复制到mac数组
            memcpy(mac, CMSG_DATA(ch), ETH_ALEN);
            break;
        }
    }

    // 调用函数使用接收到的数据重建ARP表
    rebuild_arp_table(mac, from.sin6_addr);

    // 返回接收到的数据长度
    return len;
}

3.2 解析NA和NS报文中DAD报文保存设备mac和ipv6地址信息

3.2.1 open_ns_na_socket
c 复制代码
int open_ns_na_socket(int idx) {
    int sock; // 套接字描述符
    int buffersize = 100 * 1024; // 接收缓冲区大小
    int ret = -1; // 用于存储返回值
    struct sockaddr_ll addr; // 低级别的地址定义
    struct ifreq ifr; // 接口请求结构体
    char *ifname = netscan.brname[idx]; // 网络接口名称

    // 创建一个原始套接字用于接收IPv6数据包
    if ((sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IPV6))) < 0) {
        return -1; // 如果创建失败,返回-1
    }

    // 设置套接字选项,增大接收缓冲区以避免数据包丢失
    ret = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &buffersize, sizeof(buffersize));
    if (ret < 0) // 如果设置失败
    {
        close(sock); // 关闭套接字
        return -1; // 返回-1
    }

    // 将网络接口名称复制到ifr结构体中,以便获取接口索引
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
    if (ioctl(sock, SIOCGIFINDEX, &ifr) < 0) { // 使用ioctl获取接口索引
        close(sock); // 如果失败,则关闭套接字
        return -1; // 返回-1
    }

    // 设置地址结构体
    memset(&addr, 0, sizeof(addr)); // 地址结构体清零
    addr.sll_family = AF_PACKET; // 协议族为AF_PACKET
    addr.sll_protocol = htons(ETH_P_IPV6); // 设置协议类型为IPv6
    addr.sll_ifindex = ifr.ifr_ifindex; // 设置网络接口索引
    if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) { // 绑定套接字到指定的网络接口
        close(sock); // 如果绑定失败,则关闭套接字
        return -1; // 返回-1
    }

    return sock; // 绑定成功,返回套接字描述符
}
3.2.2 recv_ns_na_pack
c 复制代码
int recv_ns_na_pack(int sock) {
    char buf[2048];  // 缓冲区,用于存放接收的数据包
    struct ip6_hdr *ipv6_hdr;  // 指向IPv6头部的指针
    struct icmp6_hdr *icmp6_hdr;  // 指向ICMPv6头部的指针
    struct sockaddr_ll addr;  // 用于存储发送方地址信息的结构体
    socklen_t addr_len = sizeof(addr);  // 发送方地址信息结构体的大小
    ssize_t numbytes;  // 接收到的字节数
    uint8_t src_mac[ETH_ALEN];  // 用于存储源MAC地址的数组

    // 从套接字接收数据,并填充发送方地址信息
    numbytes = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&addr, &addr_len);
    if (numbytes > 0) {
        // 获取IPv6头部,并跳过以太网头
        ipv6_hdr = (struct ip6_hdr *)(buf + sizeof(struct ethhdr));
        // 检查下一个头部是否为ICMPv6
        if (ipv6_hdr->ip6_nxt == IPPROTO_ICMPV6) {
            // 从以太网帧中提取源MAC地址
            memcpy(src_mac, buf + 6, ETH_ALEN);
            // 获取ICMPv6头部
            icmp6_hdr = (struct icmp6_hdr *)(buf + sizeof(struct ethhdr) + sizeof(struct ip6_hdr));
            // 如果是邻居请求
            if (icmp6_hdr->icmp6_type == ND_NEIGHBOR_SOLICIT) {
                // 如果源IPv6地址是未指定地址
                if (IN6_IS_ADDR_UNSPECIFIED(&ipv6_hdr->ip6_src)) {
                    // 转换为邻居请求结构体
                    struct nd_neighbor_solicit *ns = (struct nd_neighbor_solicit *)icmp6_hdr;
                    // 使用目标地址和源MAC地址更新ARP表
                    rebuild_arp_table(src_mac, ns->nd_ns_target);
                }
            }
            // 如果是邻居广告
            else if (icmp6_hdr->icmp6_type == ND_NEIGHBOR_ADVERT) {
                // 转换为邻居广告结构体
                struct nd_neighbor_advert *na = (struct nd_neighbor_advert *)icmp6_hdr;
                // 使用目标地址和源MAC地址更新ARP表
                rebuild_arp_table(src_mac, na->nd_na_target);
            }
        }
    }
    // 返回接收到的字节数
    return numbytes;
}

说明:

  • 使用AF_PACKET和SOCK_RAW创建的套接字允许你在更低的层级上操作,直接处理硬件发送和接收的以太网帧,这通常用于实现底层网络协议或进行网络数据包的捕获。
  • 使用AF_INET6和SOCK_RAW创建的套接字让你可以处理ICMPv6数据包,同时自动处理IPv6的数据链路层细节。你将接收到的是从IPv6头部开始的数据包,无需自己解析以太网头部。

由于需要获取DAD报文的mac,只能从eth层获取,所以这里才AF_PACKET创建套接字。

四、代码优化

上述修改后的socket使用AF_PACKET和SOCK_RAW创建的套接字,将接收所有的ipv6报文,并未进行过滤,如果在跑流或者组播测试时,一旦有大量的ipv6报文,会很大的占用资源,造成浪费和严重后果。

所以这里可以使用BPF(Berkeley Packet Filter)伯克利包过滤器进行过滤。

4.1 BPF参考学习资料

  1. Linux网络编程:原始套接字--包过滤器BPF
  2. linux网络和BPF
  3. Linux bpf 3.1、Berkeley Packet Filter (BPF) (Kernel Document)

4.2 代码实现

4.2.1 方式一:使用指令直接编写BPF程序
c 复制代码
struct sock_filter bpf_code[] = {
    // Load Ethernet Protocol Type into the BPF accumulator from the Ethernet header
    {BPF_LD + BPF_H + BPF_ABS, 0, 0, offsetof(struct ethhdr, h_proto)},
    // Jump to next instruction if Protocol Type is IPv6
    {BPF_JMP + BPF_JEQ + BPF_K, 0, 1, htons(ETH_P_IPV6)},
    // Go to reject packet
    {BPF_JMP + BPF_JA, 0, 0, 6},
    // Load the Next Header field from the IPv6 header
    {BPF_LD + BPF_B + BPF_ABS, 0, 0, ETH_HLEN + 6},
    // Jump to next instruction if Next Header is ICMPv6
    {BPF_JMP + BPF_JEQ + BPF_K, 0, 1, IPPROTO_ICMPV6},
    // Go to reject packet
    {BPF_JMP + BPF_JA, 0, 0, 4},
    // Load the ICMPv6 message type
    {BPF_LD + BPF_B + BPF_ABS, 0, 0, ETH_HLEN + 40},
    // Check if it's a Neighbor Solicitation message
    {BPF_JMP + BPF_JEQ + BPF_K, 0, 1, ND_NEIGHBOR_SOLICIT},
    // Check if it's a Neighbor Advertisement message
    {BPF_JMP + BPF_JEQ + BPF_K, 0, 0, ND_NEIGHBOR_ADVERT},
    // Reject packet
    {BPF_RET + BPF_K, 0, 0, 0},
    // Accept packet
    {BPF_RET + BPF_K, 0, 0, 0xffffffff},
};


struct sock_fprog bpf = {
    .len = ARRAY_SIZE(bpf_code),
    .filter = bpf_code
};

// 将BPF过滤器附加到套接字
ret = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
if (ret < 0) {
    close(sock);
    return -1;
}
4.2.1 方式二:使用 tcpdump -dd 命令生成BPF字节码

使用下面这种方式可以进行更好的扩展并指定接口,然后生成BPF字节码进行使用

c 复制代码
#define MAX_FILTERS 256 // 假设一个最大的过滤器数量
#define COMMAND_SIZE 256 // 命令字符串的最大长度

int get_BPF_bytecode(char *ifname) {
    FILE *fp;
    char path[1035];
    struct sock_filter bpf_code[MAX_FILTERS];
    int bpf_code_size = 0;

    // 使用snprintf构建tcpdump命令
    snprintf(command, COMMAND_SIZE, "tcpdump -dd -i %s 'icmp6 && ip6[40] == 135 || ip6[40] == 136'", ifname);

    // 执行tcpdump命令
    fp = popen(command, "r");
    if (fp == NULL) {
        printf("Failed to run command\n" );
        exit(1);
    }

    // 读取输出并解析
    while (fgets(path, sizeof(path), fp) != NULL) {
        struct sock_filter filter;
        if (sscanf(path, "{ 0x%x, %d, %d, 0x%x },", &filter.code, &filter.jt, &filter.jf, &filter.k) == 4) {
            bpf_code[bpf_code_size++] = filter;
            if(bpf_code_size >= MAX_FILTERS) {
                fprintf(stderr, "Too many filters, max is %d\n", MAX_FILTERS);
                break; // 防止数组溢出
            }
        }
    }
    return 0;
}

当然也可以直接使用命令进行生成,然后复制过去使用,这种方式就比较局限无法扩展,并且不易于维护。

4.3 二者优缺点

使用 tcpdump -dd 命令生成BPF字节码和直接编写一个BPF程序本质上是两个不同的操作层级,它们各自有优势和劣势:

一、使用 tcpdump -dd 生成BPF字节码:

优点:

  • 简单易用:对非专家用户而言,使用 tcpdump -dd 可以非常简单快速地生成复杂过滤逻辑的字节码,无需深入了解BPF的内部语言和结构。
  • 快速迭代:可以通过修改 tcpdump 的表达式快速更改过滤器的逻辑,并重新生成字节码。
  • 广泛支持:tcpdump 表达式被广泛使用和支持,有许多文档和社区可以提供帮助。

缺点:

  • 灵活性有限:受限于 tcpdump 表达式的能力,可能无法实现一些更复杂或特定需求的BPF程序逻辑。
  • 外部依赖:需要在系统上安装 tcpdump 工具,对于嵌入式系统或严格的生产环境可能不是最优选择。

二、 直接编写BPF程序:

优点:

  • 更灵活:可以编写任何复杂度的BPF程序,不受 tcpdump 表达式语法的限制。
  • 性能优化:专业的BPF开发者可以精细调整每条指令,优化性能和资源使用。
  • 深度集成:对于需要在运行时动态生成或修改BPF程序的应用,直接编程提供了更高的控制精度。

缺点:

  • 复杂性高:编写原始BPF程序需要对BPF虚拟机的工作方式有深入理解,对于初学者来说门槛较高。
  • 调试困难:BPF程序的调试通常比较困难,尤其是在高级的优化和调整阶段。
相关推荐
Logic1018 小时前
C程序设计(第五版)谭浩强 第七章课后习题优化算法与核心步骤解析
c语言·visualstudio·程序员·学习笔记·软件开发·编程基础·c语言入门
青春pig头少年9 小时前
决战408:计网大题我啃啃啃
计算机网络·学习笔记·408
map_3d_vis9 小时前
JSAPIThree 加载单体三维模型学习笔记:SimpleModel 简易加载方式
学习笔记·three.js·gltf·glb·初学者·三维模型·mapvthree·jsapithree·simplemodel
青春pig头少年12 小时前
决战408:OS大题我拿拿拿(非PV)
操作系统·学习笔记·408
Logic1011 天前
《Mysql数据库应用》 第2版 郭文明 实验5 存储过程与函数的构建与使用核心操作与思路解析
数据库·sql·mysql·学习笔记·计算机网络技术·形考作业·国家开放大学
Logic1012 天前
《Mysql数据库应用》 第2版 郭文明 实验1 在MySQL中创建数据库和表核心操作与思路解析
数据库·sql·mysql·学习笔记·计算机网络技术·形考作业·国家开放大学
一马平川的大草原2 天前
AI Agent常见问题和核心术语
人工智能·学习笔记·agent
Logic1013 天前
《Mysql数据库应用》 第2版 郭文明 实验6 数据库系统维护核心操作与思路解析
数据库·sql·mysql·学习笔记·计算机网络技术·形考作业·国家开放大学
Logic1014 天前
《数据库运维》 郭文明 实验4 数据库备份与恢复实验核心操作与思路解析
运维·数据库·sql·mysql·学习笔记·形考作业·国家开放大学
Logic1014 天前
《数据库运维》 郭文明 实验2 MySQL数据库对象管理核心操作与思路解析
运维·数据库·mysql·学习笔记·计算机网络技术·形考作业·国家开放大学