目录
树莓派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的脚本。当然,你也可以利用这个机制做点其他事情。
1. dhcpcd 客户端会自动与dhcp服务器协商网络地址,并进行相应网络地址配置。在这个过程中,dhcpcd客户端会因为某些特定事件调用执行/usr/lib/dhcpcd/dhcpcd-run-hooks这个脚本,这些事件名称会以reason作为环境变量传入/usr/lib/dhcpcd/dhcpcd-run-hooks中。
2. /usr/lib/dhcpcd/dhcpcd-run-hooks这个脚本又会调用执行 /etc/dhcpcd.enter-hook 和在 /usr/lib/dhcpcd/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/dhcpcd/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
上小节中要执行的一系列 shell脚本并不是单独调用执行的,而是/usr/lib/dhcpcd/dhcpcd-run-hooks脚本中导入到自己的进程中执行。因此,使用shell 内置函数 exit, exec或类似的会导致 dhcpcd-run-hooks退出。
据dhcpcd文档描述,可触发dhcpcd客户端调用 dhcpcd-run-hooks的原因有这些:
序号 | reason值 | 说明 |
1 | PREINIT | dhcpcd 启动,初始化完成。 |
2 | CARRIER | dhcpcd 已检测到网卡已启用。 这通常只是一个通知,无需采取任何行动, |
3 | NOCARRIER | dhcpcd 丢失了载体。 电缆可能已拔下或无线已断开。 |
4 | NOCARRIER_ROAMING | dhcpcd 丢失了载体,但接口配置仍然存在。. |
5 | INFORM | INFORM6 | dhcpcd 地址声明消息,将其地址告知 DHCP 服务器并获取其他 配置细节。. |
6 | BOUND | BOUND6 | dhcpcd 从 DHCP 服务器获得了新的租约。 |
7 | RENEW | RENEW6 | dhcpcd 更新了它的租约。 |
8 | REBIND | REBIND6 | dhcpcd 已重绑定到新的 DHCP 服务器。 |
9 | REBOOT | REBOOT6 | dhcpcd 成功从 DHCP 服务器获得了租约。 |
10 | DELEGATED6 | dhcpcd 为接口分配了一个前缀 |
11 | IPV4LL | dhcpcd 获得了一个 IPV4LL 地址,或者一个地址被删除了。 |
12 | STATIC | dhcpcd 没有从DHCP 服务器获取配置,并且配置了静态地址。 |
13 | 3RDPARTY | dhcpcd 监视到第三方dhcp服务器为接口提供了 IP 地址。 |
14 | TIMEOUT | dhcpcd 无法联系任何 DHCP 服务器,但能够使用旧的租约。 |
15 | EXPIRE | EXPIRE6 | dhcpcd 的租约或状态已过期,无法获得新租约或状态。 |
16 | NAK | dhcpcd 从 DHCP 服务器收到 NAK消息。 这应该被视为到期。 |
17 | RECONFIGURE | dhcpcd 已被指示重新配置接口。 |
18 | ROUTERADVERT | dhcpcd 已收到 IPv6 RA消息,或已过期。 |
19 | STOP | STOP6 | dhcpcd 停止在接口上运行。 |
20 | STOPPED | dhcpcd 已完全停止。 |
21 | DEPARTED | 该接口已被删除。 |
22 | FAIL | dhcpcd 无法在该接口上工作。 这通常发生在 dhcpcd 不支持原始接口,这意味着它不能作为 DHCP 或 ZeroConf 客户端。 静态配置和 DHCP INFORM 仍然是允许。 |
23 | TEST | dhcpcd 从 DHCP 服务器收到一个 OFFER,但不会配置接口。 这主要用于测试变量是否被有效赋值以便脚本来处理它们。 |
dhcpcd将清除除 $PATH环境变量之外的环境变量,然后将设置以下变量,以及支持的协议类型。
其他可用的变量值可能通过在 /etc/dhcpcd.enter-hook 和 /usr/lib/dhcpcd/dhcpcd-hooks 中加入命令:set >>/tmp/1.txt来查看(env,printenv均可)。
序号 | 变量名称 | 说明 |
1 | $interface | 接口的名称。 |
2 | $protocol | 触发事件的协议。 |
3 | $reason | 如第二小节所述。 |
4 | $pid | dhcpcd进程 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 | $profile | dhcpcd.conf中定义的配置文件的名称 |
16 | $new_delegated_dhcp6_prefix | 以空格分隔的委派前缀列表。 |
在DHCPCD的执行的相应脚本文件中加入捕获事件代码即可达到执行我们更新DDNS代码的目的,你可以在以下文件中加入你想执行的代码:
1. 文件:/usr/lib/dhcpcd/dhcpcd-run-hooks
2. 目录:/usr/lib/dhcpcd/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
# ========================================================================
我的需求是更新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
}
1. 在/etc/dhcpcd.exit-hook文件中除了捕获事件代码外,只需要写入这两行业务代码即可更新DDNS。
#导入ardnspod文件位置,注意:前面有一个点,点后面有一个空格。
. /home/pi/ardnspod
# 调用文件中arDdnsCheck函数来更新Ddns,如果Dhcpcd传入的变量获取IPV6失败
# 就调用ip a命令来获取网卡Ipv6地址。
arDdnsCheck "${interface}
2. dhcpcd客户端新配置的IPV6地址默认为/usr/lib/dhcpcd/dhcpcd-run-hooks脚本传入变量$new_dhcp6_ia_na1_ia_addr1的值,如果该变量未定义或其值为null,脚本就调用ip a 命令来解析指定网卡的ipv6地址,保证获取地址的可靠性。
3.个人域名的相关信息填入/home/pi/ardnspod文件最后的ararDdnsCheck()函数头部变量中即可。
最后,要对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后即可。