DDNS dynv6 自动更新IPv6地址 Python脚本

现在大部分地区宽带和手机热点都支持ipv6,相比起公网ipv4地址,个人获取公网ipv6地址还是挺容易的,不过大部分情况都难以获取到固定的ipv6地址,需要利用DDNS事实更新。本文主要介绍如何利用dynv6的http接口进行ipv6的实施更新。

1. 准备

1.1 创建dynv6账号和域名

网上教程很多,不赘述。

1.2 创建dynv6 token

需要创建token作为更新ipv6地址的凭据
在这里插入图片描述

然后点击+Add Http Token,随便起个名字或者不写,Zone选择All(可以更新所有创建的域名)或者选择自己要更新的那个域名。

1.3 确认已开启IPv6

windows的cmd(或power shell)中执行ipconfig,查看ipv6地址,如果没有IPv6地址,则说明相关功能未开启,先确认网卡IPv6是否开启,再确认上级路由/入户光猫是否开启了相关功能,可以和运行商沟通。
在这里插入图片描述

2. 脚本内容

脚本整体思路是

  • 解析域名地址对应的IPv6

  • 获取本地IPv6,通过cmd运行ipconfig返回内容中的IPv6 地址...来解析获取

  • 若本地IPv6和解析域名得到的IPv6不一致,则更新IP地址

为了便于移植,把token/域名等写入配置文件,直接修改配置文件就可以用到其他地方了。

import urllib.request
import subprocess 
import re 
import sys 
import time
import json
import socket
import threading
import datetime

def getIpv6Addr(interface_name):
    output = subprocess.check_output(["ipconfig", ], 
                        stderr=subprocess.STDOUT)
    output = output.decode("gbk",  errors="ignore")

    #print("output type is "+str(type(output)))
    outlines = output.split('\n')

    # 找到对应网络适配器的起始位置
    for idx in range(0, len(outlines)):
        if (-1 != outlines[idx].find(interface_name)):
            break
    #print("start line is "+str(idx)+", content:")
    #print(outlines[idx])

    # 继续向下找ipv6地址
    while (idx < len(outlines)):
        if (-1 != outlines[idx].find("IPv6 地址") and 
            -1 == outlines[idx].find("临时") and 
            -1 == outlines[idx].find("本地链接")):
            #print("get ipv6 addr: "+outlines[idx])
            start = outlines[idx].find(": ") + 2
            end = len(outlines[idx]) - 1
            return outlines[idx][start:end], ""
        elif(-1 != outlines[idx].find("默认网关")):
            break

        idx += 1

    return None, "Error: no ipv6 address."

def getValueByKeyFromJsonFile(key):
    try:
        with open("F:\\share\\SourceCode\\AutoUpdateIPV6\\Dynv6Config.json", "r") as f:
            data = json.load(f)
            if(not key in data):
                return "", "No such key: " + key
            return data[key], ""
    except Exception as e:
        return None, "系统错误: " + str(e)

def resolveDomain2Ipv6(domain):
    """
    解析域名的IPv6地址(AAAA记录)

    参数:
        domain: 要解析的域名(如"example.com" )

    返回:
        tuple: (有效IPv6地址列表, 错误信息)
    """
    try:
        subprocess.run(['ipconfig', '/flushdns'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True)
        #print("DNS 缓存清理成功!")
    except Exception as e:
        #print(f"清理 DNS 缓存失败: {e}")
        return (None, "系统错误: " + str(e))

    try:
        # 设置地址族为IPv6,类型为流式TCP 
        results = socket.getaddrinfo( 
            domain,
            None,
            family=socket.AF_INET6,
            type=socket.SOCK_STREAM 
        )

        # 提取唯一地址并标准化格式 
        ipv6_addresses = list({addr[4][0] for addr in results})

        # 按RFC5952标准排序地址 
        ipv6_addresses.sort(key=lambda  x: socket.inet_pton(socket.AF_INET6,  x))

        return (ipv6_addresses, "")

    except socket.gaierror  as e:
        error_map = {
            socket.EAI_NONAME: "域名不存在",
            socket.EAI_AGAIN: "DNS查询超时",
            socket.EAI_FAIL: "DNS服务器拒绝查询"
        }
        return (None, "解析失败: " + error_map.get(e.errno,  '未知错误'))
    except Exception as e:
        return (None, "系统错误: " + str(e))

if __name__ == "__main__":

    dynv6_server = "dynv6.com"
    dynv6_port = 80

    httpRequestHeader = ""

    interface, errStr = getValueByKeyFromJsonFile("interface")
    if (0 != len(errStr)):
        print(str(datetime.datetime.now()) + "err: " + errStr)

    hostname , errStr = getValueByKeyFromJsonFile("hostname")
    if (0 != len(errStr)):
        print(str(datetime.datetime.now()) + "err: " + errStr)

    token , errStr = getValueByKeyFromJsonFile("token")
    if (0 != len(errStr)):
        print(str(datetime.datetime.now()) + "err: " + errStr)

    print("\n" + str(datetime.datetime.now()) + ": " + "network interface: " + interface)
    print(str(datetime.datetime.now()) + ": " + "update hostname: " + hostname)
    print(str(datetime.datetime.now()) + ": " + "dynv6 token: " + token + "\n")

    while (True):
        time.sleep(0.5)

        # 获取当前域名对应的ipv6地址和本机ipv6地址
        nowIpv6, errStr = resolveDomain2Ipv6(hostname)
        if (0 != len(errStr)):
            print(str(datetime.datetime.now()) + "resolve ipv6 failed: " + errStr)
            continue

        localIp, errStr = getIpv6Addr(interface)
        if(0 != len(errStr)):
            print(str(datetime.datetime.now()) + "get local ipv6 failed: " + errStr)
            continue

        # ip没变时,不更新ip
        if(localIp in nowIpv6):
            #print("ip not update, continue")
            continue

        print(str(datetime.datetime.now()) + ": " + "ip change, " + localIp +" is not in " + str(nowIpv6) + ", update it!")

        # 构建http请求
        requestMethod = "GET"
        requestAPI = "/api/update?hostname=" + hostname + "&ipv6=" + localIp + "&token=" + token
        requestHttpVer = "HTTP/1.1"
        requestOther = "User-Agent: PostmanRuntime/7.43.0\r\n" + "Accept: */*\r\n" + "Host: dynv6.com\r\n" + \
            "Accept-Encoding: gzip, deflate, br\r\n" + "Connection: close\r\n\r\n"
        requestContent = requestMethod + " " + requestAPI + " " + requestHttpVer + "\r\n" + requestOther
        print("request: \n" + requestContent)

        # 建立tcp连接
        try:
            dynv6_sock = socket.socket(socket.AF_INET,  socket.SOCK_STREAM)
            dynv6_sock.settimeout(3.0)
            dynv6_sock.connect((dynv6_server, dynv6_port))
        except Exception as e:
            print("Error: " + str(e))
            dynv6_sock.close()
            continue

        # 发送数据
        try:
            dynv6_sock.sendall(requestContent.encode("utf-8"))
        except Exception as e:
            print("Error: " + str(e))
            dynv6_sock.close()
            continue

        # 接受返回(也不是很必要,只是更新失败的时候可以从这里排查一下)
        try:
            responseContent = dynv6_sock.recv(1024)
            print(str(datetime.datetime.now()) + ": " + "dynv6 server response:\n" + str(responseContent,  encoding = "utf-8"))
        except Exception as e:
            print("Error: " + str(e))
            dynv6_sock.close()
            continue
        dynv6_sock.close()

        time.sleep(300)  # 发送过一次请求至少60s后再发,避免频繁连接被拉黑

token配置文件格式参考如下

{
    "hostname" : "luoxian****.v6.rocks",
    "token" : "yU7VN8yeY6ceLS-pVbyy**********",
    "interface" : "WLAN"
}

脚本执行后会刷类似下面的log

F:\share\SourceCode\AutoUpdateIPV6>python autoUpdateIpv6_v2.py

2025-06-15 01:56:18.400754: network interface: WLAN
2025-06-15 01:56:18.401254: update hostname: luoxian****.v6.rocks
2025-06-15 01:56:18.401254: dynv6 token: yU7VN8yeY6ceLS-pVbyyx*******

2025-06-15 01:56:19.010917resolve ipv6 failed: 解析失败: 域名不存在
2025-06-15 01:56:19.588773: ip change, 2408:8956:600:***** is not in ['2408:8956:7c40:*****'], update it!
request:
GET /api/update?hostname=luoxian****.v6.rocks&ipv6=2408:8956:600:*****&token=yU7VN8yeY6ceLS-pVbyyx******* HTTP/1.1
User-Agent: PostmanRuntime/7.43.0
Accept: */*
Host: dynv6.com
Accept-Encoding: gzip, deflate, br
Connection: close


2025-06-15 01:56:20.837319: dynv6 server response:
HTTP/1.1 200 OK
Content-Length: 17
Content-Type: text/plain; charset=UTF-8
Date: Sat, 14 Jun 2025 17:56:17 GMT
Vary: Origin
Via: 1.1 Caddy
Connection: close

addresses updated
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值