avatar

SLHAF's blog

SLHAF的个人博客

  • 首页
  • 分类
  • 标签
  • 归档
  • 友链
主页 简单的ddns解析ipv6 实现思路(包括systemd服务、ddns脚本)
文章

简单的ddns解析ipv6 实现思路(包括systemd服务、ddns脚本)

发表于 2025-07-3 更新于 4天前
作者 slhaf
22~29 分钟 阅读

之前租的云服务器再有几天就要到期了,于是几天前斥巨资买了一台4核8G的香橙派+ups模块来当服务器。正好手里的随身wifi设置好APN接入点后,也能够拿到IPV6,那么再加上一个ddns解析到Cloudflare,再开启Cloudflare的双栈代理,勉强也算是接入公网了() 于是乎就在ChatGPT的帮助下,写了一个ddns脚本,配合systemd服务,持续检测并更新解析的Cloudflare地址。至于为啥不用现成的脚本,一方面是我需要的功能确实并不多,另一方面是,别人的工具日后调整起来也不好弄,就当是愿意折腾吧。

Systemd服务

  1. 通过systemd服务开机启动ddns脚本
  2. 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 动态解析使用。

细节说明

  1. 脚本中判断ipv6解析是否过期,主要是通过查询在env中配置的环境变量CF_DOMAIN对应的ipv6解析是否正常,因此这个脚本应当也只适用于单个二级域名及其子域名的更新了。在判断到解析的ipv6地址已经过期时,将会针对在DNS解析中不包含注释的AAAA记录的域名执行更新操作,就是说如果有的子域名想要手动进行管理解析的话,加上注释就行了。
  2. 由于更新、查询等API操作都用到了Cloudflare提供的Python库,所以需要确保安装了Python环境以及对应的Cloudflare包,执行pip install cloudflare就行。
  3. 脚本会在启动后直接执行一次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电池(平头)
香橙派, Linux
Linux 香橙派 脚本
许可协议:  CC BY 4.0
分享

相关文章

8月 4, 2025

为无头服务器配置带 GUI 的 Clash 客户端:基于 Xvfb + VNC 的解决方案

作者使用香橙派作为服务器,通过`create_ap`共享网络,并尝试在无图形界面的Linux服务器上运行带GUI的Clash客户端。为解决配置不便的问题,采用`xvfb`提供虚拟X环境,配合`x11vnc`实现VNC远程桌面连接,最终通过TigerVNC客户端访问图形化Clash-Verge界面。该方法虽然资源占用较高,但实现了服务器端代理的可视化管理,同时提到轻量级替代方案如clash核心+yacd面板。

7月 3, 2025

简单的ddns解析ipv6 实现思路(包括systemd服务、ddns脚本)

作者将云服务器替换为香橙派搭配UPS模块,利用随身WiFi获取IPv6地址并通过Cloudflare实现动态域名解析。通过systemd服务开机启动自定义的ddns脚本,该脚本结合Shell和Python检测本地IPv6变化并更新Cloudflare解析记录。环境变量配置包括Cloudflare账号信息,脚本定期检查地址变化并自动更新。当前服务器配置为OrangePi 3B(4核8G)搭配Geekworm UPS模块。

下一篇

喜报

上一篇

day_0 分布式入门: 基本知识+Nacos基础

最近更新

  • 为无头服务器配置带 GUI 的 Clash 客户端:基于 Xvfb + VNC 的解决方案
  • Manjaro/X11 环境下通过 bbswitch 关闭 NVIDIA 显卡以延长续航
  • 关于Bottles中同一容器内不同应用需要配置不同显卡方案的解决办法
  • day_3 分布式入门: Gateway
  • day_2 分布式入门: Sentinel

热门标签

Java 日常 分布式 Linux 学习 脚本 智能体 香橙派

目录

©2025 SLHAF's blog. 保留部分权利。

使用 Halo 主题 Chirpy