简单的ddns解析ipv6 实现思路(包括systemd服务、ddns脚本)
之前租的云服务器再有几天就要到期了,于是几天前斥巨资买了一台4核8G的香橙派+ups模块来当服务器。正好手里的随身wifi设置好APN接入点后,也能够拿到IPV6,那么再加上一个ddns解析到Cloudflare,再开启Cloudflare的双栈代理,勉强也算是接入公网了() 于是乎就在ChatGPT的帮助下,写了一个ddns脚本,配合systemd服务,持续检测并更新解析的Cloudflare地址。至于为啥不用现成的脚本,一方面是我需要的功能确实并不多,另一方面是,别人的工具日后调整起来也不好弄,就当是愿意折腾吧。
Systemd服务
- 通过systemd服务开机启动ddns脚本
- ddns脚本中用到的环境变量通过位于
/etc/cloudflare/ddns.env
的环境变量文件传入
文件内容
[Unit]
Description=Cloudflare IPv6 DDNS Update Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/ddns.sh
EnvironmentFile=/etc/cloudflare/ddns.env
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
ddns环境变量
由于用到的是Cloudflare提供的dns解析,所以需要在配置以下几个变量供脚本文件调用:
- CF_EMAIL: cloudflare账号邮箱
- CF_API_KEY: cloudflare的apikey,注意,不是global key
- CF_DOMAIN: 托管解析的域名,用于查询当前域名的解析结果
- CF_ZONE_ID: Cloudflare区域ID,在云解析中的概述一栏可以找到
ddns脚本
这里脚本本体只是shell脚本,但通过here-doc将python脚本嵌入到了shell脚本中,用来执行那些网络请求或者Cloudflare库提供的API,要问为啥不直接写成python脚本或者将ddns脚本中的python段落抽取出来?问就是懒(毕竟一开始也是用shell脚本写的,但写着写着发现这shell脚本解析返回的JSON数据也太麻烦了,干脆就将Python嵌进去吧。
脚本内容
#! /bin/bash
dev=$(ip -o link show | awk -F': ' '/enx/ {print $2}')
address_v6=$(ip -6 addr show dev "$dev" | grep mngtmpaddr | awk '{split($2,res,"/"); print res[1]}')
readonly base_url="https://api.cloudflare.com/client/v4"
readonly log_file_path="$HOME/.cache/ddns"
readonly interval=10
log(){
path="$log_file_path/$(date '+%Y-%m-%d').log"
echo "[$(date '+%Y-%m-%d %H:%M:%S')]: $*" >> "$path"
}
get_remote_address(){
log "读取远程域名地址中..."
python <<EOF
from cloudflare import Cloudflare
client = Cloudflare(
api_token="$CF_API_KEY", # This is the default and can be omitted
)
response = client.dns.records.list(
zone_id="$CF_ZONE_ID",
name={"exact": "$CF_DOMAIN"},
type="AAAA"
)
print(response.result.pop().content)
EOF
}
update_remote_address(){
log "更新远程域名地址"
python <<EOF
from cloudflare import Cloudflare
import asyncio
client = Cloudflare(
api_token="$CF_API_KEY", # This is the default and can be omitted
)
async def update_dns_record(name, content, a_type, dns_record_id):
try:
client.dns.records.edit(
dns_record_id=dns_record_id,
zone_id="$CF_ZONE_ID",
name=name,
ttl=1,
type=a_type,
content=content
)
return "ok", name
except Exception as e:
return "error", name, str(e)
async def run():
response = client.dns.records.list(
zone_id="$CF_ZONE_ID",
comment={"absent": "1"}, # 实际脚本应当改为absent
type="AAAA",
)
address_v6 = "$address_v6"
tasks = [update_dns_record(e.name, address_v6, "AAAA", e.id) for e in response.result]
results = await asyncio.gather(*tasks)
for r in results:
print(r)
asyncio.run(run())
EOF
}
check_and_update_remote(){
if [[ -z $address_v6 ]]; then
log "未检测到ipv6地址"
fi
address_v6_remote=$(get_remote_address)
if [[ "$address_v6" != "$address_v6_remote" ]]; then
log "地址发生变化: 当前ipv6地址: $address_v6 ; 解析的ipv6地址: $address_v6_remote"
update_remote_address
else
log "地址未发生变化"
fi
}
launch_ddns(){
echo "ddns已启动"
mkdir -p "$log_file_path"
check_and_update_remote
while true; do
#循环读取,和address_v6比较,如果不同,则更新其值,且更新远程解析结果
address_v6_temp=$(ip -6 addr show dev "$dev" | grep mngtmpaddr | awk '{split($2,res,"/"); print res[1]}')
if [[ -z $address_v6_temp ]]; then
log "未检测到ipv6地址"
sleep $interval
continue
fi
address_v6_current=$(ip -6 addr show dev "$dev" | grep mngtmpaddr | awk '{split($2,res,"/"); print res[1]}')
if [[ "$address_v6_current" != "$address_v6" ]]; then
log "ipv6地址发生变化, 当前ipv6地址: $address_v6 ; 新的ipv6地址: $address_v6_current"
address_v6=$address_v6_current
update_remote_address
log "地址更新完毕"
fi
sleep $interval
done
}
launch_ddns
在脚本开头出现了这两段代码:
dev=$(ip -o link show | awk -F': ' '/enx/ {print $2}')
这个主要是用来获取提供ipv6的设备名称,因为我用到的是随身WIFI提供的IPV6地址,它的设备名称也挺固定的,一直都是enx开头,只要匹配这个开头就行,可以按需修改address_v6=$(ip -6 addr show dev "$dev" | grep mngtmpaddr | awk '{split($2,res,"/"); print res[1]}')
这一句在脚本中出现挺多次的,按说抽取成函数更合适点。它主要用来获取设备提供的ipv6地址,提取的地址所在行需要包括mngtmpaddr
这串文字。按照GPT给出的解释:mngtmpaddr 对应的地址 是更稳定的,相比 temporary,更适合做 DDNS 动态解析使用。
细节说明
- 脚本中判断ipv6解析是否过期,主要是通过查询在env中配置的环境变量
CF_DOMAIN
对应的ipv6解析是否正常,因此这个脚本应当也只适用于单个二级域名及其子域名的更新了。在判断到解析的ipv6地址已经过期时,将会针对在DNS解析中不包含注释的AAAA记录的域名执行更新操作,就是说如果有的子域名想要手动进行管理解析的话,加上注释就行了。 - 由于更新、查询等API操作都用到了Cloudflare提供的Python库,所以需要确保安装了Python环境以及对应的Cloudflare包,执行
pip install cloudflare
就行。 - 脚本会在启动后直接执行一次
check_and_update_remote
,在这个操作中会对比远程的ipv6地址与当前设备的ipv6地址是否相同,如果不相同则会触发更新操作,此时的远程ipv6地址(address_v6_remote
)应当与本地的ipv6地址(address_v6
)保持同步,之后会以10s的间隔循环检测address_v6
与address_v6_current
是否一致,如果不一致,则触发更新,并更新address_v6
的值。
当前的服务器配置
- OrangePi_3B 4核8G, 64GB eMMc模块
- Geekworm X-USP1模块+4节松下18650电池(平头)
许可协议:
CC BY 4.0