当前位置: 首页 > 工具软件 > dhcpcd > 使用案例 >

利用DHCPCD客户端Hook机制实现DDNS[树莓派][DNSpod][IPV6]

潘志国
2023-12-01

目录

一、系统环境

二、实现功能

三、前提条件

四、实现原理分析

(一)dhcpcd-run-hooks执行过程

(二)DHCPCD的事件reason的值有哪些?

(三)一些可用的环境变量

五、具体实现过程

(一)新建要写入脚本代码的文件

(二) 加入更新DDNS的代码

(三)dhcpcd.exit-hook文件修改说明

六、修改DHCPCD的配置(重要)


一、系统环境

树莓派4B-4GB/raspberrypi OS(32位,内核版本5.10.63)/dhcpcd客户端(8.1.2)

二、实现功能

1.当系统获取到新的IPV6地址时,自动更新地址到DNSpod解析记录。

2.无需添加常驻后台的系统服务或监控IP地址变化的后台程序;不需要使用系统cron定时执行定期执行更新脚本,保证系统轻量化运行。

3.IPV4同样适用。

三、前提条件

1. DHCP服务器必须配置为有状态获取IPV6地址。

路由或光猫的ipv6RA消息配置应打开M位(即真正的dhcpv6),关闭O位(O位用于无状态配置时获取DNS等其他信息),且不能为无状态自动配置(SLAAC)(M位和O位均关闭)。

至于为什么?因为无状态自动配置是客户端自己生成地址,不会触发特定事件,也就不能利用dhcpcd的hook机制。

2. 只能由dhcpcd接管分配系统网络地址

在/etc/network/interfaces网络接口配置文件中不能出现指定静态IP地址的static或dhcp字样。否则dhcpcd就会退出。详见/usr/lib/dhcpcd5/dhcpcd这个文件。其代码实现如下:


#!/bin/sh -e

DHCPCD=/sbin/dhcpcd
INTERFACES=/etc/network/interfaces

if grep -q -E "^[[:space:]]*iface[[:space:]]*.*[[:space:]]*inet[[:space:]]*(dhcp|static)" \
                $INTERFACES; then
        echo "Not running dhcpcd because $INTERFACES"
        echo "defines some interfaces that will use a"
        echo "DHCP client or static address"
        exit 6
fi

exec $DHCPCD $@

四、实现原理分析

在raspberrypi OS系统中,没有使用systemd-networkd,Network-manager,netctl,dhclient (已弃用)等常见客户端来管理网络地址,默认安装并使用了dhcpcd客户端来管理网络地址。

原dhclient 和现在的dhcp客户端dhcpcd 都提供了在发生特定事件时执行任意代码和脚本的选项(hook机制),以帮助用户实现特定需求。

我们就可以通过捕获reason这个变量的值来做一些自己想做的事情 。本文就是利用上述机制,实现触发更新IP地址事件时执行更新DDNS的脚本。当然,你也可以利用这个机制做点其他事情。

(一)dhcpcd-run-hooks执行过程

1.  dhcpcd 客户端会自动与dhcp服务器协商网络地址,并进行相应网络地址配置。在这个过程中,dhcpcd客户端会因为某些特定事件调用执行/usr/lib/dh​​cpcd/dhcpcd-run-hooks这个脚本,这些事件名称会以reason作为环境变量传入/usr/lib/dh​​cpcd/dhcpcd-run-hooks中。

2. /usr/lib/dh​​cpcd/dhcpcd-run-hooks这个脚本又会调用执行 /etc/dhcpcd.enter-hook 和在 /usr/lib/dh​​cpcd/dhcpcd-hooks 中的一系列脚本,最后执行 /etc/dhcpcd.exit-hook这个脚本。 并向 /etc/resolv.conf 写入DNS信息。

3. 每一次 dhcpcd-run-hooks被调用, $interface会被设置为当前接口名称,如:eth0。 dhcpcd会设置$reason 的值说明为什么 dhcpcd-run-hooks会被调用。 要被配置的 DHCP 信息保存在以new_开头的变量中,旧的 DHCP 信息保存在以old_ 开头的变量中。命令dhcpcd -V 可查看支持的完整变量列表 。

/usr/lib/dh​​cpcd/dhcpcd-run-hooks脚本最后的内容如下(前面定义了一些函数):

# 导入下列脚本
for hook in \
	/etc/dhcpcd.enter-hook \
	/lib/dhcpcd/dhcpcd-hooks/* \
	/etc/dhcpcd.exit-hook
do
# 过滤掉一些脚本
	for skip in $skip_hooks; do
		case "$hook" in
			*/*~)				continue 2;;
			*/"$skip")			continue 2;;
			*/[0-9][0-9]"-$skip")		continue 2;;
			*/[0-9][0-9]"-$skip.sh")	continue 2;;
		esac
	done
# 通过. 脚本路径 来执行脚本
	if [ -f "$hook" ]; then
		. "$hook"
	fi
done

(二)DHCPCD的事件reason的值有哪些?

上小节中要执行的一系列 shell脚本并不是单独调用执行的,而是/usr/lib/dh​​cpcd/dhcpcd-run-hooks脚本中导入到自己的进程中执行。因此,使用shell 内置函数 exit, exec或类似的会导致 dhcpcd-run-hooks退出。

据dhcpcd文档描述,可触发dhcpcd客户端调用 dhcpcd-run-hooks的原因有这些:

序号reason值说明

1

PREINITdhcpcd 启动,初始化完成。
2CARRIERdhcpcd 已检测到网卡已启用。 这通常只是一个通知,无需采取任何行动,
3NOCARRIERdhcpcd 丢失了载体。 电缆可能已拔下或无线已断开。
4NOCARRIER_ROAMINGdhcpcd 丢失了载体,但接口配置仍然存在。.
5INFORM | INFORM6dhcpcd 地址声明消息,将其地址告知 DHCP 服务器并获取其他 配置细节。.
6BOUND | BOUND6dhcpcd 从 DHCP 服务器获得了新的租约。
7RENEW | RENEW6dhcpcd 更新了它的租约。
8REBIND | REBIND6dhcpcd 已重绑定到新的 DHCP 服务器。
9REBOOT | REBOOT6dhcpcd 成功从 DHCP 服务器获得了租约。
10DELEGATED6dhcpcd 为接口分配了一个前缀
11IPV4LLdhcpcd 获得了一个 IPV4LL 地址,或者一个地址被删除了。
12STATICdhcpcd 没有从DHCP 服务器获取配置,并且配置了静态地址。
133RDPARTYdhcpcd 监视到第三方dhcp服务器为接口提供了 IP 地址。
14TIMEOUTdhcpcd 无法联系任何 DHCP 服务器,但能够使用旧的租约。
15EXPIRE | EXPIRE6dhcpcd 的租约或状态已过期,无法获得新租约或状态。
16NAKdhcpcd 从 DHCP 服务器收到 NAK消息。 这应该被视为到期。
17RECONFIGUREdhcpcd 已被指示重新配置接口。
18ROUTERADVERTdhcpcd 已收到 IPv6 RA消息,或已过期。
19STOP | STOP6dhcpcd 停止在接口上运行。
20STOPPEDdhcpcd 已完全停止。
21DEPARTED该接口已被删除。
22FAILdhcpcd 无法在该接口上工作。 这通常发生在 dhcpcd 不支持原始接口,这意味着它不能作为 DHCP 或 ZeroConf 客户端。 静态配置和 DHCP INFORM 仍然是允许。
23TESTdhcpcd 从 DHCP 服务器收到一个 OFFER,但不会配置接口。 这主要用于测试变量是否被有效赋值以便脚本来处理它们。

(三)一些可用的环境变量

dhcpcd将清除除 $PATH环境变量之外的环境变量,然后将设置以下变量,以及支持的协议类型。

其他可用的变量值可能通过在 /etc/dhcpcd.enter-hook 和 /usr/lib/dh​​cpcd/dhcpcd-hooks 中加入命令:set >>/tmp/1.txt来查看(env,printenv均可)。

序号变量名称说明

1

$interface接口的名称。
2$protocol触发事件的协议。
3$reason如第二小节所述。
4$piddhcpcd进程 pid 值。
5$ifcarrier当前$interface连接状态: unknown, up或者 down.
6$ifmetric$interface的网关跃点数值,值越低越好。
7$ifwireless$interface是否是无线的,是为1,否则 0.
8$ifflags$interface 标志。
9$ifmtu$interface MTU。
10$ifssid接口已连接到SSID 的名称 。
11$interface_order以空格分隔的接口列表,按优先顺序排列。
12$if_up如果启用了interface为true ,否则 false,不仅仅是IFF_UP,而且可能不相等。
13$if_down和上面值相反
14$af_waiting等待获取地址的协议,在dhcpcd.conf中定义 。
15$profiledhcpcd.conf中定义的配置文件的名称
16$new_delegated_dhcp6_prefix以空格分隔的委派前缀列表。

五、具体实现过程

(一)新建要写入脚本代码的文件

在DHCPCD的执行的相应脚本文件中加入捕获事件代码即可达到执行我们更新DDNS代码的目的,你可以在以下文件中加入你想执行的代码:

1.  文件:/usr/lib/dh​​cpcd/dhcpcd-run-hooks

2.  目录:/usr/lib/dh​​cpcd/dhcpcd-hooks

3. /etc目录下的 dhcpcd.enter-hook 和dhcpcd.exit-hook的这两个脚本文件,没有就创建它(名字要对号入座)。

我采用的是在/etc/dhcpcd.exit-hook文件中添加捕获事件代码的方案,sudo vi /etc/dhcpcd.exit-hook,写入的代码如下:

#!/bin/sh

#sed -i 's/\r//g' ardnspod 用来解决windows编辑文件,行尾结束符导致在linux中脚本运行出错的问题。
# =======================================================================
: ${interface:='eth0'}
: ${reason:=BOUND6}
# 脚本执行时间变量
updatetime=$(date '+%Y-%m-%d %H:%M:%S')


# 事件捕获代码,要捕获的接口名称eth0
if [ "${interface}" = "eth0" ]; then
	# 要捕获的dhcpcd事件码,此处仅适用于IPV6。
    case "${reason}" in BOUND6|RENEW6|REBIND6|REBOOT6)


	   # 下两行是更新DDNS的代码,自行发挥填写。


	   . /home/pi/ardnspod
	   arDdnsCheck "${interface}" 



	   # 执行状态码检测并写入日志文件..
	   errCode=$?
    	   if [ $errCode -eq 0 ]; then
       		 echo "${updatetime}-更新成功..." >>/tmp/ret.txt
	   else
		echo "${updatetime}-更新失败..." >>/tmp/ret.txt
   	   fi
   	   # 调试用的变量值输出代码
	   #echo $updatetime $interface $reason $protocol $new_dhcp6_ia_na1_ia_addr1 >>/tmp/1.txt                                   
	   #echo "${updatetime}-------------------------------------------------set varibles--" >>/tmp/vari.txt                     
	   #set >>/tmp/vari.txt
	   ;;

    esac
fi
# ========================================================================

(二) 加入更新DDNS的代码

我的需求是更新IPV6地址到DNSpod(即腾讯云)。同理,更新到各大运营商DDNS的shell代码,网上非常之多,也不是本文重点。

题外话,更新代码没必要重复造轮子(劳神费力),推荐这个写得非常鲁棒的 (感谢原作者rehiy)。你可以直接使用,也可以根据个人需求进行调整。

mac和linux均可使用的DNSpod解析脚本https://github.com/rehiy/dnspod-shell

腾讯云API接口文档DNS 解析 DNSPod 修改解析记录 - API 文档 - 文档中心 - 腾讯云

个人对原脚本进行简单修改,主要修改内容如下:

1. 去除了获取recordID和domainID的过程函数(执行一次脚本后就可获得recordID和domainID,就没必要再次调用了),改为在脚本中直接填入相应的值。

2. 由于没有IPV4公网地址,直接删除了IPV4相关代码,以提高代码运行速度。

3. 修改了获取ipv6地址函数。

修改的代码如下,保存文件名为/home/pi/ardnspod

#!/bin/sh
#
#sed -i 's/\r//g' ardnspod
# =======================================================================
# AnripDdns v6.1.1
#
# Dynamic DNS using DNSPod API
#
# Author: Rehiy, https://github.com/rehiy
#                https://www.anrip.com/?s=dnspod
# Collaborators: ProfFan, https://github.com/ProfFan
#
# 
# =======================================================================


# Get WAN IPv6

arWanIp6() {

    local hostIp
    hostIp=$(ip a show $1 |grep -Poi "(?<=inet6\s).*?(?=/.+global)"|head -n 1)
    echo $hostIp

}

# Dnspod Bridge
# Args: type data

arDdnsApi() {

    local agent="AnripDdns/6.1.0(wang@rehiy.com)"

    local apiurl="https://dnsapi.cn/${1:?'Info.Version'}"
    local params="login_token=$arToken&format=json&$2"

    if type wget >/dev/null 2>&1; then
        wget -q -O- --no-check-certificate -U $agent --post-data $params $apiurl
    else
        curl -s -A $agent -d $params $apiurl
    fi

}

# Fetch Record Ip
# Args: domainId recordId

arDdnsRecordIp() {

    local errMsg

    local recordIp

    # Get Record Ip
    recordIp=$(arDdnsApi "Record.Info" "domain_id=$1&record_id=$2")
    recordIp=$(echo $recordIp | sed 's/.*,"value":"\([0-9a-fA-F\.\:]*\)".*/\1/')

    # Output Record Ip
    case "$recordIp" in
        [1-9]*)
            echo $recordIp
            return 0
        ;;
        *)
            errMsg=$(echo $recordIp | sed 's/.*"message":"\([^\"]*\)".*/\1/')
            echo "arDdnsRecordIp - $errMsg"
            return 1
        ;;
    esac

}

# Update Record Ip
# Args: domainId recordId subdomain hostIp recordType

arDdnsUpdate() {

    local errMsg

    local recordRs
    local recordIp
    local recordCd

    if [ -z "$5" ]; then
        echo "arDdnsUpdate - Args number error"
        return 1
    fi

    # Update Ip
    recordRs=$(arDdnsApi "Record.Modify" "domain_id=$1&record_id=$2&sub_domain=$3&record_type=$5&value=$4&record_line=%e9%bb%98%e8%ae%a4")
    recordIp=$(echo $recordRs | sed 's/.*,"value":"\([0-9a-fA-F\.\:]*\)".*/\1/')
    recordCd=$(echo $recordRs | sed 's/.*{"code":"\([0-9]*\)".*/\1/')

    # Output Result
    if [ "$recordIp" = "$4" ] && [ "$recordCd" = "1" ]; then
        echo "$arDdnsUpdate - success"
        return 0
    else
        errMsg=$(echo $recordRs | sed 's/.*,"message":"\([^"]*\)".*/\1/')
        echo "$arDdnsUpdate - $errMsg"
        return 1
    fi

}



# DDNS Check
# Args: Main Sub
arDdnsCheck() {
    # 按'TokenID,Token'格式填写
    local arToken="939921,799fsd455sd555d5222c55dsdfs"
    # 域名名称"xx.xx"
    local domain="test.com"
    # 二级域名名称www,@等
    local subdomain="@"
    # 要解析的域名类型,ipv6为AAAA,不用改。
    local recordType=AAAA
    # 按"域名ID 记录ID"填写
    local ddnsIds="12555886 8442221156"
    local lastIp=$(arDdnsRecordIp $ddnsIds)
    local hostIp=${new_dhcp6_ia_na1_ia_addr1:=$(arWanIp6 $1)}
    local postRs
    local errCode

    echo "Fetching Host Ip"
    echo "> Host Ip: $hostIp"
    echo "> Record Type: $recordType"
    echo "Fetching Ids of $subdomain.$domain"
    echo "> Domain Ids: $ddnsIds"
    echo "> Last Ip: $lastIp"
    if [ "$lastIp" = "$hostIp" ]; then
        echo "> Last Ip is the same as host Ip"
        return 1
    fi

    echo "Updating Record for $subdomain.$domain"
    postRs=$(arDdnsUpdate $ddnsIds "$subdomain" "$hostIp" "$recordType")
    errCode=$?
    echo "> $postRs"
    if [ $errCode -eq 0 ]; then
        return 0
    fi


}

(三)dhcpcd.exit-hook文件修改说明

1. 在/etc/dhcpcd.exit-hook文件中除了捕获事件代码外,只需要写入这两行业务代码即可更新DDNS。

#导入ardnspod文件位置,注意:前面有一个点,点后面有一个空格。
 . /home/pi/ardnspod

# 调用文件中arDdnsCheck函数来更新Ddns,如果Dhcpcd传入的变量获取IPV6失败
# 就调用ip a命令来获取网卡Ipv6地址。

arDdnsCheck "${interface}

2. dhcpcd客户端新配置的IPV6地址默认为/usr/lib/dh​​cpcd/dhcpcd-run-hooks脚本传入变量$new_dhcp6_ia_na1_ia_addr1的值,如果该变量未定义或其值为null,脚本就调用ip a 命令来解析指定网卡的ipv6地址,保证获取地址的可靠性。

3.个人域名的相关信息填入/home/pi/ardnspod文件最后的ararDdnsCheck()函数头部变量中即可。


六、修改DHCPCD的配置(重要)

最后,要对DHCPCD的配置文件进行相应的修改,去掉slaac及无状态配置的相关设置,改为dhcpv6,以确保能触发我们需要的与地址更新相关的事件reason。修改内容如下:  

# 要注释掉原有的这行(重要修改)
#slaac private

# 设置拒绝配置的网络接口
denyinterfaces wlan0

# eth0配置段
interface eth0
# 禁用根据ra消息自动配置ipv6 slaac地址(重要修改)。
ipv6ra_noautoconf
# 不要配置IPV4地址
noipv4
noipv4ll

# mac0配置段
interface mac0
# 禁用根据ra消息自动配置ipv6 slaac地址(重要修改)。
ipv6ra_noautoconf
# IPV4使用静态地址
static ip_address=192.168.1.4/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1

  修改完配置文件后,重启DHCPCD,sudo service dhcpcd restart后即可。

   

   

   

 类似资料: