noip-duc

一个noip.com的动态更新客户端(DUC)的python脚本,适用于部分无法安装官方DUC的环境。同时支持IPv4和IPv6地址

注意

你需要在 https://my.noip.com/dynamic-dns 创建一个AAAA记录的hostname,才能实现这个hostname的ipv4与ipv6同时更新。否则noip.com只会更新ipv4地址。

V2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
NoIP DDNS 自动更新客户端
创建日期: 2024-01-22
更新日期: 2025-05-23
"""

import base64
import logging
import os
import sys
import time
from typing import Optional, Tuple, List, Union

import numpy as np
import requests
from requests.exceptions import ConnectionError, Timeout, RequestException

logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('noip_ddns_updater.log')
]
)
logger = logging.getLogger('noip_ddns_updater')

# 配置信息
CONFIG = {
'username': 'your Enail', # NoIP 账户邮箱
'password': 'your password', # NoIP 账户密码
'hostname': 'your noip.com domain', # DDNS 主机名
'time_interval': 30, # 检查时间间隔(分钟)
'user_agent': 'no-ip shell script/1.0 mail@mail.com', # 用户代理
'connection_timeout': 10, # 连接超时时间(秒)
'retry_interval': 10, # 重试间隔(分钟)
'max_retries': 3 # 最大重试次数
}

# API 和探针地址
API_ENDPOINTS = {
'noip_update': 'https://dynupdate.no-ip.com/nic/update',
'ipv4_probes': [
'https://ipv4.icanhazip.com/',
'https://4.ipw.cn/'
],
'ipv6_probes': [
'https://ipv6.icanhazip.com/',
'https://6.ipw.cn/'
]
}

ERROR_STATUSES = ['nohost', 'badauth', 'badagent', '!donator', 'abuse', '911']


def get_ip_address() -> str:
"""
获取当前的 IP 地址(IPv4 和/或 IPv6)

返回:
str: IP 地址字符串
"""
ip_v4 = ip_v6 = None
np.random.seed(int(time.time()) % 10000)
ipv4_probe = API_ENDPOINTS['ipv4_probes'][np.random.randint(0, len(API_ENDPOINTS['ipv4_probes']))]
ipv6_probe = API_ENDPOINTS['ipv6_probes'][np.random.randint(0, len(API_ENDPOINTS['ipv6_probes']))]

try:
response = requests.get(ipv4_probe, timeout=CONFIG['connection_timeout'])
if response.status_code == 200:
ip_v4 = response.text.strip()
logger.debug(f"获取到 IPv4 地址: {ip_v4}")
except ConnectionError:
logger.warning("IPv4 连接失败")
except Timeout:
logger.warning("IPv4 请求超时")
except RequestException as e:
logger.error(f"IPv4 请求异常: {e}")

try:
response = requests.get(ipv6_probe, timeout=CONFIG['connection_timeout'])
if response.status_code == 200:
ip_v6 = response.text.strip()
logger.debug(f"获取到 IPv6 地址: {ip_v6}")
except ConnectionError:
logger.warning("IPv6 连接失败")
except Timeout:
logger.warning("IPv6 请求超时")
except RequestException as e:
logger.error(f"IPv6 请求异常: {e}")

ip_addresses = list(filter(None, [ip_v4, ip_v6]))
if not ip_addresses:
logger.error("无法获取任何 IP 地址")
return ""

return ",".join(ip_addresses)


def update_ip_address(ip_address: str, retry_count: int = 0) -> bool:
"""
更新 NoIP DDNS 的 IP 地址

参数:
ip_address (str): 要更新的 IP 地址
retry_count (int): 当前重试次数

返回:
bool: 更新是否成功
"""
if retry_count >= CONFIG['max_retries']:
logger.error(f"达到最大重试次数 {CONFIG['max_retries']},放弃更新")
return False

if not ip_address:
logger.error("IP 地址为空,无法更新")
return False

auth_string = f"{CONFIG['username']}:{CONFIG['password']}"
base64_auth = base64.b64encode(auth_string.encode()).decode()

headers = {
'Authorization': f"Basic {base64_auth}",
'User-Agent': CONFIG['user_agent']
}

update_url = f"{API_ENDPOINTS['noip_update']}?hostname={CONFIG['hostname']}&myip={ip_address}"

try:
logger.info(f"正在更新 DDNS 记录为: {ip_address}")
response = requests.get(update_url, headers=headers, timeout=CONFIG['connection_timeout'])
result = response.text.strip()
if result in ERROR_STATUSES:
if result == '911':
logger.warning("NoIP 服务器暂时不可用 (911),30 分钟后重试")
time.sleep(30 * 60)
return update_ip_address(ip_address, retry_count + 1)
else:
logger.error(f"更新失败: {result}")
return False
else:
logger.info(f"更新成功: {result}")
return True

except ConnectionError:
logger.error("与 NoIP 服务器连接失败")
time.sleep(CONFIG['retry_interval'] * 60)
return update_ip_address(ip_address, retry_count + 1)
except Timeout:
logger.error("连接 NoIP 服务器超时")
time.sleep(CONFIG['retry_interval'] * 60)
return update_ip_address(ip_address, retry_count + 1)
except Exception as e:
logger.error(f"更新过程中发生未知异常: {e}")
return False


def main():
logger.info("NoIP DDNS 更新客户端已启动")
current_ip = ""

try:
while True:
try:
new_ip = get_ip_address()
if not new_ip:
logger.warning("无法获取 IP 地址,将在下次检查时重试")
time.sleep(CONFIG['time_interval'] * 60)
continue

if current_ip != new_ip:
logger.info(f"检测到 IP 变化: {current_ip} -> {new_ip}")
current_ip = new_ip
result = update_ip_address(new_ip)

if not result:
logger.error("更新失败,程序将退出")
break
else:
logger.info("IP 地址未发生变化,无需更新")

logger.info(f"等待 {CONFIG['time_interval']} 分钟后再次检查")
time.sleep(CONFIG['time_interval'] * 60)

except KeyboardInterrupt:
logger.info("接收到中断信号,程序将退出")
break
except Exception as e:
logger.error(f"发生未处理的异常: {e}")
time.sleep(CONFIG['retry_interval'] * 60)
except KeyboardInterrupt:
logger.info("程序被用户中断")
finally:
logger.info("NoIP DDNS 更新客户端已停止")


if __name__ == '__main__':
main()

V1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env python

# coding:utf-8
# 20240122
import base64
import requests
import numpy as np
import time

username = 'your Enail'
password = 'your password'
time_interval = 30 # 时间间隔(分钟)
hostname = 'your noip.com domain' # DDNS主机名
user_agent = 'no-ip shell script/1.0 mail@mail.com'
noip_host = 'https://dynupdate.no-ip.com/nic/update?' # noip接口地址
icanhaz_probe_v4 = 'https://ipv4.icanhazip.com/' # ipv4探针地址
icanhaz_probe_v6 = 'https://ipv6.icanhazip.com/' # ipv6探针地址
ipw_probe_v4 = 'https://4.ipw.cn/' # ipv4探针地址
ipw_probe_v6 = 'https://6.ipw.cn/' # ipv6探针地址
np.random.seed(2024) # 随机数种子


def getIP():
ip_v4 = ip_v6 = None
probes = [(icanhaz_probe_v4, ipw_probe_v6), (ipw_probe_v4, icanhaz_probe_v6)]
probe_v4, probe_v6 = probes[np.random.randint(0, 2)]
try:
ip_v4 = requests.get(probe_v4).text
except requests.exceptions.ConnectionError:
ip_v4 = None
except requests.exceptions.URLRequired:
print('ip探针地址设置错误')

try:
ip_v6 = requests.get(probe_v6).text
except requests.exceptions.ConnectionError:
ip_v6 = None
except requests.exceptions.URLRequired:
print('ip探针地址设置错误')

return ','.join(filter(None, [ip_v4, ip_v6])).replace("\n", "")


def updateIP(my_ip):
base64_encoded_auth_string = base64.b64encode(f"{username}:{password}".encode()).decode()
headers = {
'Authorization': f"Basic {base64_encoded_auth_string}",
'User-Agent': user_agent
}
try:
res = requests.get(noip_host + 'hostname=' + hostname + '&myip=' + my_ip, headers=headers)
error_status = ['nohost', 'badauth', 'badagent', '!donator', 'abuse']
if res.text in error_status:
print(res.text)
return False
elif res.text == '911':
print('noip 911,30分钟后重试')
time.sleep(1801)
return updateIP(my_ip)
else:
print(res.text)
return True
except requests.exceptions.ConnectionError:
print('与noip连接失败')
time.sleep(600)
return updateIP(my_ip)
except requests.exceptions.Timeout:
print('连接超时')
time.sleep(600)
return updateIP(my_ip)
except Exception as e:
print(f'未知异常{e}')
return False


if __name__ == '__main__':
current_ip = ''
while True:
try:
new_ip = getIP()
except Exception as e:
print(f'获取ip失败{e}')
continue
if current_ip != new_ip:
print(f'新ip:{new_ip}, 正在更新ddns')
current_ip = new_ip
result = updateIP(new_ip)
if result is False:
break
else:
print('ip未发生变化')
time.sleep(time_interval*60)