春风得意马蹄疾 记录技术,分享生活
随笔: 34 分类: 15 评论: 1 阅读: 2,189

Linux 服务器 DDNS 实战:Cron + 华为云 DNS 自动更新 IPv6

📅 发表于 2026-06-22 22:59:23 👁 阅读 52 次 💬 评论 0

为什么还需要服务器自己的 DDNS?

虽然云函数会维护 iBMC 的 DNS,但博客域名 xiaoajun.cnwww.xiaoajun.cn 的 IPv6 解析仍然依赖服务器自己来更新。

原因很简单:只有服务器才知道自己 真正能用、能出外网 的 IPv6 地址。云函数只能通过 DNS 间接解析,而 DNS 记录本身可能已经过时。

本文详细记录如何在 Linux 服务器上部署一套健壮的 DDNS 脚本。

整体思路


Cron 定时执行 → 读取网卡 IPv6 → 测试外网连通 → 对比缓存 → 更新 DNS

前提条件

  • 服务器已获取公网 IPv6 地址(通过 SLAAC 或 DHCPv6)
  • 华为云 DNS 已创建 AAAA 记录
  • Python 3 和 pip 已安装

步骤一:安装华为云 Python SDK

SSH 登录服务器(root 用户):

ssh root@你的服务器IP

安装 SDK:

pip3 install huaweicloudsdkdns huaweicloudsdkcore

步骤二:获取 Record Set ID

登录华为云控制台:

  1. 云解析 DNS → 选择域名 → 管理
  2. 点击 xiaoajun.cn 的 AAAA 记录
  3. 浏览器 URL 末尾的 recordset_id=... 就是 Record Set ID
  4. 同理获取 www.xiaoajun.cn 的 Record Set ID

步骤三:编写 DDNS 脚本

保存到 /usr/local/bin/ddns-update.py

#!/usr/bin/env python3
"""
华为云 DDNS - 服务器端脚本
通过本地网卡获取 IPv6,测试连通性后更新 DNS
"""
import os
import re
import subprocess
import sys
import logging
from huaweicloudsdkcore.auth.credentials import BasicCredentials
from huaweicloudsdkdns.v2 import DnsClient
from huaweicloudsdkdns.v2 import UpdateRecordSetRequest, UpdateRecordSetReq
from huaweicloudsdkcore.region.region import Region

# ========== 配置(替换为你的实际值)==========
AK = '你的AK'
SK = '你的SK'
REGION = Region(id='cn-north-4',
                endpoint='dns.cn-north-4.myhuaweicloud.com')
ZONE_ID = '你的ZoneID'

# 需要更新的域名 → Record Set ID
RECORDS = {
    'xiaoajun.cn.':       'RecordSetID-1',
    'www.xiaoajun.cn.':   'RecordSetID-2',
    # 如果需要同时处理 iBMC,取消下面注释
    # 'ibmc.xiaoajun.cn.': 'RecordSetID-3',
}

# 网卡名(用 ip link show 确认)
INTERFACE = 'enp2s0f0'

# 日志和缓存
LOG_FILE = '/var/log/ddns-update.log'
LAST_IP_FILE = '/var/tmp/ddns_last_ipv6.txt'
# ==========================================

logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)


def get_all_ipv6():
    """获取网卡上所有有效的全局 IPv6 地址"""
    candidates = []
    try:
        result = subprocess.run(
            ['ip', '-6', 'addr', 'show', INTERFACE],
            capture_output=True, text=True, timeout=10
        )
        for line in result.stdout.split('\n'):
            # 筛选:global + mngtmpaddr(非临时地址)
            if 'global' in line and 'mngtmpaddr' in line:
                match = re.search(r'inet6\s+([\da-f:]+)/', line)
                if match:
                    addr = match.group(1)
                    # 排除 link-local (fe80) 和 ULA (fd)
                    if not addr.startswith('fd') \
                       and not addr.startswith('fe80'):
                        candidates.append(addr)
    except Exception as e:
        logging.error(f'获取 IPv6 地址失败: {e}')
    return candidates


def test_ipv6_outbound(addr, target='2400:3200::1'):
    """
    测试 IPv6 地址能否出外网
    target=2400:3200::1 是阿里 DNS,也可以用 2400:da00::6666
    """
    try:
        result = subprocess.run(
            ['ping', '-6', '-c', '1', '-W', '2',
             '-I', addr, target],
            capture_output=True, text=True, timeout=5
        )
        return result.returncode == 0
    except Exception:
        return False


def get_best_ipv6():
    """选出最佳 IPv6 地址:外网可达 > 第一个 mngtmpaddr"""
    candidates = get_all_ipv6()
    logging.info(f'候选 IPv6 地址: {candidates}')

    if not candidates:
        return None

    # 优先选能 ping 通外网的
    for addr in candidates:
        if test_ipv6_outbound(addr):
            logging.info(f'选中(外网可达): {addr}')
            return addr

    # 都不通,取第一个
    logging.warning('所有地址外网测试失败,使用第一个')
    return candidates[0]


def update_record(client, domain, record_id, ipv6):
    """更新单条 DNS 记录"""
    try:
        request = UpdateRecordSetRequest()
        request.zone_id = ZONE_ID
        request.recordset_id = record_id

        body = UpdateRecordSetReq()
        body.records = [ipv6]
        body.ttl = 600  # TTL 10 分钟

        request.body = body
        response = client.update_record_set(request)

        success = response.status in (
            'ACTIVE', 'PENDING_UPDATE',
            'PENDING_CREATE', 'PENDING_DELETE'
        )
        if success:
            logging.info(f'[OK] {domain} → {ipv6}')
        else:
            logging.error(f'[FAIL] {domain}: status={response.status}')
        return success
    except Exception as e:
        logging.error(f'[ERROR] {domain}: {e}')
        return False


def main():
    ipv6 = get_best_ipv6()
    if not ipv6:
        logging.error('无法获取有效 IPv6 地址')
        sys.exit(1)

    # 检查是否变化(没变化就跳过 API 调用,省资源)
    last_ip = ''
    if os.path.exists(LAST_IP_FILE):
        with open(LAST_IP_FILE) as f:
            last_ip = f.read().strip()

    if last_ip == ipv6:
        logging.info(f'IPv6 未变化: {ipv6}')
        return

    logging.info(f'IPv6 变化: {last_ip} → {ipv6}')

    # 创建 DNS 客户端
    credentials = BasicCredentials(AK, SK)
    client = DnsClient.new_builder() \
        .with_credentials(credentials) \
        .with_region(REGION) \
        .build()

    # 更新所有 DNS 记录
    all_ok = True
    for domain, record_id in RECORDS.items():
        if not update_record(client, domain, record_id, ipv6):
            all_ok = False

    # 保存最新 IP(只有全部成功才保存)
    if all_ok:
        os.makedirs(os.path.dirname(LAST_IP_FILE), exist_ok=True)
        with open(LAST_IP_FILE, 'w') as f:
            f.write(ipv6)
        print(f'DDNS 更新成功: {ipv6}')
    else:
        sys.exit(1)


if __name__ == '__main__':
    main()

步骤四:设置权限

chmod +x /usr/local/bin/ddns-update.py

步骤五:手动测试

python3 /usr/local/bin/ddns-update.py

正常输出:

DDNS 更新成功: 2408:8270:1034:a370:5cd8:9e55:c6b3:3

查看日志:

cat /var/log/ddns-update.log

步骤六:设置 Cron 定时任务

crontab -e

添加(每 5 分钟执行一次):

*/5 * * * * /usr/bin/python3 /usr/local/bin/ddns-update.py >> /var/log/ddns-cron.log 2>&1

推荐用 */5(每 5 分钟)而不是 */10,因为服务器开机后能更快更新 DNS。

Cron 格式说明:


分  时   日   月  周
*/5 *    *    *   *     ← 每 5 分钟
*/10 *   *    *   *     ← 每 10 分钟
0   */1  *    *   *     ← 每小时

步骤七:验证 Cron 是否生效

# 等待 5 分钟后检查日志
tail -20 /var/log/ddns-update.log

# 查看 cron 运行日志
tail -20 /var/log/ddns-cron.log

完整部署一键脚本

如果你是全新部署,可以用这个一键脚本:

#!/bin/bash
# 一键部署服务器 DDNS
set -e

echo "=== 安装 Python SDK ==="
pip3 install huaweicloudsdkdns huaweicloudsdkcore

echo "=== 部署脚本 ==="
cp ddns-update.py /usr/local/bin/ddns-update.py
chmod +x /usr/local/bin/ddns-update.py

echo "=== 设置 Cron ==="
(crontab -l 2>/dev/null; \
 echo "*/5 * * * * /usr/bin/python3 /usr/local/bin/ddns-update.py >> /var/log/ddns-cron.log 2>&1") \
 | crontab -

echo "=== 手动测试 ==="
python3 /usr/local/bin/ddns-update.py

echo "=== 完成!Cron 已设置:每 5 分钟执行 ==="
crontab -l | grep ddns

地址筛选逻辑详解

服务器上的 IPv6 地址通常有多个,脚本用以下逻辑选出最合适的:

第一步:网卡过滤

ip -6 addr show enp2s0f0

只看唯一的物理网卡,忽略虚拟网卡。

第二步:类型过滤

只取 mngtmpaddr 类型的全局地址:

  • mngtmpaddr:永久地址,由 SLAAC 生成,稳定不变
  • tempaddr:临时隐私地址,定期轮换,不适合做 DDNS

第三步:排除无效前缀

  • fe80:* → link-local,只在局域网通信
  • fd**:** → ULA 唯一本地地址,不能出公网

第四步:连通性测试

ping -6 -c 1 -W 2 -I 候选地址 2400:3200::1

绑定候选地址 ping 公网 DNS,确保这个地址真的能出外网。

第五步:本地缓存

如果 IP 没变就不调 API,节省调用次数(虽然免费额度根本用不完)。

监控与维护

查看最近更新

tail -30 /var/log/ddns-update.log

查看 Cron 执行状态

grep CRON /var/log/syslog | grep ddns    # Debian/Ubuntu
grep CRON /var/log/messages | grep ddns  # CentOS/RHEL

手动强制更新

# 删除缓存文件即可强制下次更新
rm /var/tmp/ddns_last_ipv6.txt
python3 /usr/local/bin/ddns-update.py

停止 DDNS

crontab -e
# 删除或注释掉 ddns 那行
# */5 * * * * /usr/bin/python3 /usr/local/bin/ddns-update.py ...

架构对比

云函数(iBMC) 服务器 Cron(博客)
运行位置 华为云 FunctionGraph 服务器本地
10.10.10.42
获取 IP 方式 DNS 解析
xiaoajun.cn
本地网卡直接读取
更新域名 ibmc.xiaoajun.cn xiaoajun.cn
www.xiaoajun.cn
执行频率 每 10 分钟 每 5 分钟
依赖服务器 不需要 必须
费用 免费 免费
(服务器电费)

常见问题

Q: 获取到的地址是 fe80 开头的?

A: 检查过滤条件。fe80 开头的地址是 link-local,被脚本排除了。如果 `get_all_ipv6()` 返回空,说明网卡没有获取到公网 IPv6 地址。

Q: ping 外网失败怎么办?

A: 检查 IPv6 是否真的通了:

ping -6 2400:3200::1

如果不通,检查 ip6tables 和路由:

ip -6 route show
ip6tables -L -n

Q: AK/SK 泄露了怎么办?

A: 去华为云「我的凭证」删除旧密钥、创建新的,然后更新脚本中的 AK/SK。

Q: 可以不用华为云 DNS 吗?

A: 原理一样,换成 Cloudflare、阿里云 DNS 等,改 API 调用部分即可。

总结

现在你的服务器有了一套完整的 DDNS 方案:

  1. 服务器端(本文):Cron 每 5 分钟主动更新博客域名的 IPv6
  2. 云端(另一篇博客):FunctionGraph 每 10 分钟维护 iBMC 域名
  3. 互不干扰:两套系统独立运行,各管各的域名

即使服务器关机 10 分钟,博客域名的 TTL 也是 10 分钟;等服务器重新开机,5 分钟内 Cron 就会把 DNS 更新过来。iBMC 域名则由云函数独立保障,不受任何设备状态影响。

📂 分类:服务器维护
评论 (0)
技术分享者 · 记录成长的每一步
✨ 特效 ON