Pandorabox解决IPV6中继失败(OpenWRT可能适用)

时间:2024-03-05 19:02:28

本随笔记录了在Pandorabox固件下(OpenWRT也适用)IPV6中继失效问题的踩坑和解决过程。

路由器:Newifi 3

固件版本:PandoraBox 19.02

零、故障描述

参考配置了dhcp服务器,发现后台设备无法获取到IPV6地址,拓扑如下:

电信光猫——路由器——PC

路由器/etc/config/dhcp内容如下:

 

config dnsmasq
        option domainneeded \'1\'
        option boguspriv \'1\'
        option filterwin2k \'0\'
        option localise_queries \'1\'
        option rebind_protection \'0\'
        option rebind_localhost \'1\'
        option local \'/lan/\'
        option domain \'lan\'
        option expandhosts \'1\'
        option nonegcache \'0\'
        option authoritative \'1\'
        option noping \'0\'
        option readethers \'1\'
        option leasefile \'/tmp/dhcp.leases\'
        option resolvfile \'/tmp/resolv.conf.auto\'
        option localservice \'1\'
        option allservers \'1\'
        option sequential_ip \'1\'
        option redirect_all \'0\'
        option noresolv \'0\'
        option ignore \'1\'
        list server \'127.0.0.1#5333\'

config dhcp \'lan\'
        option interface \'lan\'
        option start \'100\'
        option limit \'150\'
        option leasetime \'12h\'
        option dhcpv6 \'relay\'
        option ndp \'relay\'
        option ra \'relay\'

config dhcp \'wan6\'
        option interface \'wan6\'
        option master \'1\'
        option dhcpv6 \'relay\'
        option ndp \'relay\'
        option ra \'relay\'

config odhcpd \'odhcpd\'
        option maindhcp \'0\'
        option leasefile \'/tmp/hosts/odhcpd\'
        option leasetrigger \'/usr/sbin/odhcpd-update\'
        option loglevel \'4\'
dhcp配置文件

 

一、初查,定位故障点

通过进入光猫后台,发现光猫支持两种方式分配IP地址,DHCPv6和SLACC两种方式,目前使用DHCPv6方式。

调整为SLACC发现可以实现IP地址的获取,遂定位问题点出在DHCPv6报文的中继转发上。

通过路由器安装tcpdump抓包,发现路由器在lan侧收到了报文,在wan侧存在中继请求和中继应答报文,但是lan侧没有转发应答报文,判断中继应答报文的转发出现异常

 lan端捕获到了请求报文

wan端有捕获到中继应答报文

二、编译、调试odhcpd

因为使用的是Pandorabox的固件,需要到官网下载相关的SDK才可以进行编译,所幸下载地址http://downloads.pangubox.com:6380/尚可访问,成功下载到了SDK。

进入对应版本的SDK,替换对应的mirror为前面提到的地址,执行一次make,下载完所需的依赖后,完成了编译前的准备工作。

进入github,找了OpenWRT组织维护的odhcpd代码,下载了一份最新版本,同时从archive路径下扣了package.mk文件放置到SDK的package目录下,参考了一下archive里面UCI的MAKEFILE,结合自己的摸索,改了下MAKEFILE以支持odhcpd的代码编译。

 1 #
 2 # Copyright (C) 2008-2014 OpenWrt.org
 3 #
 4 # This is free software, licensed under the GNU General Public License v2.
 5 # See /LICENSE for more information.
 6 #
 7 
 8 include $(TOPDIR)/rules.mk
 9 
10 PKG_NAME:=odhcpd
11 PKG_VERSION:=1.11-3
12 PKG_BUILD_PARALLEL:=0
13 
14 include $(INCLUDE_DIR)/package.mk
15 include $(INCLUDE_DIR)/cmake.mk
16 
17 
18 CMAKE_OPTIONS = \
19         -DUBUS=1 \
20         -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON
21 
22 define Package/odhcpd
23   SECTION:=base
24   CATEGORY:=Base system
25   DEPENDS:=+libnl-tiny +libubox +libuci +libubus
26   TITLE:=Odhcpd for OpenWRT
27 endef
28 
29 TARGET_CFLAGS += -I$(STAGING_DIR)/usr/include
30 TARGET_LDFLAGS += -L$(STAGING_DIR)/usr/lib
31 
32 define Package/odhcpd/install
33         $(INSTALL_DIR) $(1)/sbin
34         $(INSTALL_BIN) $(PKG_BUILD_DIR)/odhcpd $(1)/sbin/
35 endef
36 
37 $(eval $(call BuildPackage,odhcpd))
Makefile文件

很明显的,编译不报错,是不可能的。编译报错发现缺少了mkdir_p的函数实现,参考了1.11版本的odhcpd代码,移植了相关实现,解决了此问题,移植代码如下:

 1 static int mkdir_p(char *dir, mode_t mask)
 2 {
 3         char *l = strrchr(dir, \'/\');
 4         int ret;
 5 
 6         if (!l)
 7                 return 0;
 8 
 9         *l = \'\0\';
10 
11         if (mkdir_p(dir, mask))
12                 return -1;
13 
14         *l = \'/\';
15 
16         ret = mkdir(dir, mask);
17         if (ret && errno == EEXIST)
18                 return 0;
19 
20         if (ret)
21                 syslog(LOG_ERR, "mkdir(%s, %d) failed: %m\n", dir, mask);
22 
23         return ret;
24 }
View Code

编译后调试没有坑也是不可能的,调试打了断点结果发现我竟然没法进入函数,参考代码发现程序文件中充斥着大量的static声明,参考GNU GCC手冊通过增加一个编译选项(-fno-keep-static-consts)解决了问题,所以对CMakeLists.txt进行了改动,改动如下:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -O0 -Wall -ggdb -fno-keep-static-consts -std=gnu99")

编译完成后,得到了一个可调试的,版本还很新的odhcp服务器程序。

通过断点调试,发现在eth0.2接口上没有收到报文,特别奇怪,另外一边的br-lan可以收到报文,结合tcpdump的抓包结果,怀疑套接字收包存在异常。

三、编写测试程序,验证并锁定原因

阅读了odhcpd有关代码,参考网上的udp socket编程实现(不好意思,我真的太菜了),从dhcpv6_setup_interface函数中抽取相关的socket创建和bind代码,组成了调试小程序,代码如下:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdbool.h>
#include <netinet/ether.h>
#include <net/if.h>

#define DHCPV6_CLIENT_PORT 546
#define DHCPV6_SERVER_PORT 547
#define DHCPV6_HOP_COUNT_LIMIT 32

#define ALL_DHCPV6_RELAYS "ff02::1:2"
#define ALL_DHCPV6_SERVERS "ff05::1:3"

#define BUFF_LEN 9000

struct socket_st {
        int fd;
        char ifname[20];
};

int socket_create(struct socket_st *sock)
{
        int ret = 0;
        int ifindex;

        if (sock->fd >= 0) {
                printf("close socket %d\n", sock->fd);
                close(sock->fd);
                sock->fd = -1;
        }

        /* Configure multicast settings */
        struct sockaddr_in6 bind_addr = {AF_INET6, htons(DHCPV6_SERVER_PORT),
                                0, IN6ADDR_ANY_INIT, 0};
        struct ipv6_mreq mreq;
        int val = 1;

        sock->fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_UDP);
        if (sock->fd < 0) {
                printf("socket(AF_INET6): %m\n");
                ret = -1;
                goto out;
        }

        /* Basic IPv6 configuration */
        if (setsockopt(sock->fd, SOL_SOCKET, SO_BINDTODEVICE,
                                sock->ifname, strlen(sock->ifname)) < 0) {
                printf("setsockopt(SO_BINDTODEVICE): %m\n");
                ret = -1;
                goto out;
        }

        if (setsockopt(sock->fd, IPPROTO_IPV6, IPV6_V6ONLY,
                                &val, sizeof(val)) < 0) {
                printf("setsockopt(IPV6_V6ONLY): %m\n");
                ret = -1;
                goto out;
        }

        if (setsockopt(sock->fd, SOL_SOCKET, SO_REUSEADDR,
                                &val, sizeof(val)) < 0) {
                printf("setsockopt(SO_REUSEADDR): %m\n");
                ret = -1;
                goto out;
        }

        if (setsockopt(sock->fd, IPPROTO_IPV6, IPV6_RECVPKTINFO,
                                &val, sizeof(val)) < 0) {
                printf("setsockopt(IPV6_RECVPKTINFO): %m\n");
                ret = -1;
                goto out;
        }

        val = DHCPV6_HOP_COUNT_LIMIT;
        if (setsockopt(sock->fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS,
                                &val, sizeof(val)) < 0) {
                printf("setsockopt(IPV6_MULTICAST_HOPS): %m\n");
                ret = -1;
                goto out;
        }

        val = 0;
        if (setsockopt(sock->fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP,
                                &val, sizeof(val)) < 0) {
                printf("setsockopt(IPV6_MULTICAST_LOOP): %m\n");
                ret = -1;
                goto out;
        }

        if (bind(sock->fd, (struct sockaddr*)&bind_addr,
                                sizeof(bind_addr)) < 0) {
                printf("bind(): %m\n");
                ret = -1;
                goto out;
        }

        ifindex = if_nametoindex(sock->ifname);
        if (ifindex) {
                memset(&mreq, 0, sizeof(mreq));
                inet_pton(AF_INET6, ALL_DHCPV6_RELAYS, &mreq.ipv6mr_multiaddr);
                mreq.ipv6mr_interface = ifindex;

                if (setsockopt(sock->fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP,
                                        &mreq, sizeof(mreq)) < 0) {
                        printf("setsockopt(IPV6_ADD_MEMBERSHIP): %m\n");
                        ret = -1;
                        goto out;
                }
        } else {
                printf("get ifindex failed! ifname:%s\n", sock->ifname);
        }


out:
        if (ret < 0 && sock->fd >= 0) {
                close(sock->fd);
                sock->fd = -1;
        }

        return ret;
}

int socket_recv(struct socket_st *sock)
{
        struct sockaddr_in6 send_addr6;
        char buf[BUFF_LEN], str[INET6_ADDRSTRLEN];;
        int recv_size, buf_len;
        while(1) {
                memset(&send_addr6, 0, sizeof(send_addr6));
                recv_size=recvfrom(sock->fd, buf, BUFF_LEN, 0, (struct sockaddr *)&send_addr6, &buf_len);
                if(recv_size < 0){
                        printf("recvfrom error!\n");
                        return -1;
                }
                if(inet_ntop(AF_INET6, &send_addr6, str, INET6_ADDRSTRLEN) == NULL){
                        perror("inet ntop/n");
                        printf("error\n");
                }
                printf("send_addr6=%s\n", str);
        }
        return 0;
}

int main()
{
        struct socket_st sock;
        sock.fd = -1;
        strcpy(sock.ifname, "eth0.2");
        socket_create(&sock);

        socket_recv(&sock);

        return 0;
}
View Code

通过更换接口以及xcap构包发送,发现了很有意思的现象,就是其余变量不变,lan口怎么试都有报文,但是wan口死活收不到报文

突然一阵念头飘过,防火墙!!!

将防火墙wan区域入站改为允许后,捕获到了报文,并且PC成功获取到了地址,这下原因水落石出。

处于安全考量,将入站流量重新设置为拒绝,需要修改防火墙配置/etc/config/firewall,参考报文结构,最终拟定放通目的为本机的,访问udp6 547端口的报文入站

新增配置如下:

1 config rule
2         option target \'ACCEPT\'
3         option src \'wan\'
4         option name \'Allow-DHCPv6_Reply\'
5         option family \'ipv6\'
6         option proto \'udp\'
7         option dest_port \'547\'
8         option limit \'100/sec\'
新增DHCPv6-Relay放通规则

 测试,通过!!!