目前飞牛的ddns只能更对当前电脑的ip地址做dns解析,有点太弱了,希望可以对局域网内的其它机器也可以做dns解析。支持可配置的地址获取方式,下面以cloudflare为例,说明一下期望的功能。
需要解析的域名配置
下面的数据存储到sqlite中,其中:
cf_identifier: cloudflare中的域名标识符号
hostname: 要获取哪个机器的ip地址
addr_type: 地址类型, A 表示ipv4, AAAA 表示解析ipv6
domain: 完整的域名,如fnos.fnnas.com
ip_addr: 实际获取到的ip地址, 后面通过接口将该地址解析到domain字段的域名
update_time: 记录更新时间
resolve_type: 可以扩展,现在以internet和resolve来举例说明:
- internet 表示获取当前机器的出口公网ip地址
- resolve 表示使用avahi-resolve命令,根据addr_type和hostname解析出ip地址
root@fnos:~# sqlite3 ddns.db
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE ddns (
id integer,
resolve_type text,
hostname text,
addr_type text,
cf_identifier text,
domain text,
ip_addr text,
update_time integer
);
sqlite> select * from ddns;
1|internet||A|8cc34d8d1d8420b48a23f355057698e6|fnos.masked.top|101.71.232.255|1769416996
2|internet||A|b42e573a2d52412513d3b73100fcf4c4|pub.masked.top|101.71.232.255|1769416998
3|resolve|server.local|AAAA|5260599072e57252425f262f249633e0|pc.masked.top|2408:8240:c11:56b0:0000:86ff:0000:f192|1769416999
4|resolve|fnos.local|AAAA|9361dc80027fe287dadd8c86e3604313|fnos.masked.top|fe80::3d45:0000:0000:12f5|1769658837
最终实现的效果就是,根据hostname、addr_type、resolve_type解析出一个ip地址,调用cloudflare接口进行更新。并把解析出来的ip地址,更新到ip_addr字段,下次定时任务执行时,检查是否有变更,只有在有变更时才需要进行更新。
基于脚本实现的方案
以下脚本由千问大模型生成,仅对更新ip_addr和update_time字段部分做了错误修正,实测功能是正常的,使用systemd的定时器,每10分钟检测一次,可以实现预期效果。
root@fnos:~# cat ddns.sh
#!/bin/bash
set -euo pipefail
# ================== 配置区 ==================
CF_ZONE_ID=05049340000000000000000000435093
CF_AUTH_KEY=04bdd80000000000000000000fb9003c3c4a8
CF_AUTH_EMAIL=use@someserver.com
DB_FILE="/root/ddns.db"
ZONE_ID="${CF_ZONE_ID:-}"
AUTH_EMAIL="${CF_AUTH_EMAIL:-}"
AUTH_KEY="${CF_AUTH_KEY:-}"
# 从配置文件加载(可选)
if [ -z "$ZONE_ID" ] || [ -z "$AUTH_EMAIL" ] || [ -z "$AUTH_KEY" ]; then
if [ -f ~/.cf_ddns.conf ]; then
source ~/.cf_ddns.conf
fi
fi
# ================== 函数定义 ==================
# 获取当前公网 IP
get_current_public_ip() {
local ip_type=${1:-ipv4}
local ip=""
if [ "$ip_type" = "ipv4" ]; then
ip=$(curl -s --max-time 10 -4 http://4.ipw.cn || curl -s --max-time 10 -4 https://api.ipify.org)
elif [ "$ip_type" = "ipv6" ]; then
ip=$(curl -s --max-time 10 -6 http://6.ipw.cn || curl -s --max-time 10 -6 https://api6.ipify.org)
else
echo "Unknown IP type: $ip_type" >&2
return 1
fi
if [ -z "$ip" ]; then
echo "Failed to fetch public IP of type $ip_type" >&2
return 1
fi
echo "$ip"
}
# 解析本地主机名 IP(使用 avahi-resolve)
resolve_hostname_to_ip() {
local hostname="$1"
local addr_type="$2" # A or AAAA
# 检查 avahi-resolve 是否可用
if ! command -v avahi-resolve >/dev/null 2>&1; then
echo "Error: avahi-resolve command not found." >&2
return 1
fi
# 构建查询类型参数
local resolve_flags="-4"
if [ "$addr_type" = "AAAA" ]; then
resolve_flags="-6"
fi
# 执行解析
local ip
ip=$(avahi-resolve $resolve_flags -n "$hostname" 2>/dev/null | awk '{print $2}' | head -n1)
if [ -z "$ip" ]; then
echo "Failed to resolve hostname $hostname for $addr_type" >&2
return 1
fi
echo "$ip"
}
# 更新 Cloudflare DNS 记录
update_cloudflare_record() {
local domain="$1"
local identifier="$2"
local new_ip="$3"
local record_type="$4"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Updating $domain ($record_type) to $new_ip..."
local response
response=$(curl -sS --max-time 30 -X PUT \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${identifier}" \
--header "X-Auth-Email: ${AUTH_EMAIL}" \
--header "X-Auth-Key: ${AUTH_KEY}" \
--header "Content-Type: application/json" \
--data "{\"content\":\"${new_ip}\",\"name\":\"${domain}\",\"proxied\":false,\"type\":\"${record_type}\",\"ttl\":120}" \
2>&1)
if [ $? -ne 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Error: curl failed: $response" >&2
return 1
fi
# 检查 API 返回是否成功
if echo "$response" | grep -q '"success":true'; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Successfully updated DNS record for $domain"
return 0
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] API error: $(echo "$response" | jq -r '.errors // empty' 2>/dev/null || echo "$response")" >&2
return 1
fi
}
# 更新数据库中的 ip_addr 和 update_time
update_db_record() {
local id="$1"
local new_ip="$2"
local timestamp="$3"
sqlite3 $DB_FILE "update ddns set ip_addr = '$new_ip', update_time=$timestamp where id = $id;"
#"printf '%s\n%s\n%s' "$new_ip" "$timestamp" "$id" | sqlite3 "$DB_FILE" "UPDATE ddns SET ip_addr = ?, update_time = ? WHERE id = ?;"
}
# ================== 主逻辑 ==================
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting DDNS update process..."
# 从数据库中查询所有记录
while IFS='|' read -r id resolve_type hostname domain addr_type cf_identifier ip_addr _; do
echo "Processing ID $id: $domain ($addr_type)"
# 根据 resolve_type 获取 IP
if [ "$resolve_type" = "internet" ]; then
if [ "$addr_type" = "A" ]; then
current_ip=$(get_current_public_ip ipv4) || continue
elif [ "$addr_type" = "AAAA" ]; then
current_ip=$(get_current_public_ip ipv6) || continue
else
echo "Unsupported addr_type $addr_type for internet resolve_type" >&2
continue
fi
elif [ "$resolve_type" = "resolve" ]; then
if [ -z "$hostname" ]; then
echo "Error: hostname field is empty for resolve_type 'resolve' (ID: $id)" >&2
continue
fi
current_ip=$(resolve_hostname_to_ip "$hostname" "$addr_type") || continue
else
echo "Unknown resolve_type: $resolve_type (ID: $id)" >&2
continue
fi
# 比较新 IP 与数据库中的旧 IP
if [ "x$ip_addr" = "x$current_ip" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] IP for $domain unchanged ($current_ip). Skipping update."
continue
fi
# IP 已改变,调用 Cloudflare API 更新
if update_cloudflare_record "$domain" "$cf_identifier" "$current_ip" "$addr_type"; then
# 更新数据库记录
now_timestamp=$(date +%s)
update_db_record "$id" "$current_ip" "$now_timestamp"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Database record for ID $id updated with IP $current_ip."
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Failed to update DNS for $domain (ID: $id). DB not updated." >&2
fi
done < <(sqlite3 -separator '|' "$DB_FILE" "SELECT id, resolve_type, hostname, domain, addr_type, cf_identifier, ip_addr FROM ddns;")
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DDNS update process completed."
bash
root@fnos:~# systemctl cat ddns.timer
# /lib/systemd/system/ddns.timer
[Unit]
Description=Run DDNS Update
[Timer]
OnActiveSec=10
OnBootSec=30
OnUnitActiveSec=600
Unit=ddns.service
[Install]
WantedBy=multi-user.target
root@fnos:~# systemctl cat ddns.service
# /lib/systemd/system/ddns.service
[Unit]
Description=Cloudflare ddns ipv6 for local nas
[Service]
Type=oneshot
User=root
ExecStart=/root/nas_ddns.sh
[Install]
WantedBy=multi-user.target
Also=ddns.timer
执行日志信息
maskedroot@fnos:~# journalctl -u ddns -n 12
1月 31 13:43:58 fnos nas_ddns.sh[22997]: [2026-01-31 13:43:58] Starting DDNS update process...
1月 31 13:43:58 fnos nas_ddns.sh[22997]: Processing ID 1: fnos.masked.top (A)
1月 31 13:43:58 fnos nas_ddns.sh[22997]: [2026-01-31 13:43:58] IP for fnos.masked.top unchanged (101.71.232.255). Skipping update.
1月 31 13:43:58 fnos nas_ddns.sh[22997]: Processing ID 2: pub.masked.top (A)
1月 31 13:43:58 fnos nas_ddns.sh[22997]: [2026-01-31 13:43:58] IP for pub.masked.top unchanged (101.71.232.255). Skipping update.
1月 31 13:43:58 fnos nas_ddns.sh[22997]: Processing ID 3: pc.masked.top (AAAA)
1月 31 13:44:03 fnos nas_ddns.sh[23011]: Failed to resolve hostname server.local for AAAA
1月 31 13:44:03 fnos nas_ddns.sh[22997]: Processing ID 4: fnos.masked.top (AAAA)
1月 31 13:44:03 fnos nas_ddns.sh[22997]: [2026-01-31 13:44:03] IP for fnos.masked.top unchanged (fe80::3d45:55e9:8cd7:12f5). Skipping update.
1月 31 13:44:03 fnos nas_ddns.sh[22997]: [2026-01-31 13:44:03] DDNS update process completed.
1月 31 13:44:03 fnos systemd[1]: ddns.service: Deactivated successfully.
1月 31 13:44:03 fnos systemd[1]: Finished ddns.service - Cloudflare ddns ipv6 for local nas.
对个人来说,这么处理已经完全可以满足需要了,没必要再要求飞牛实现这样的功能,只是觉得飞牛立志做一个伟大的产品,目前的方案是很大众的解决方式,没有办法解决文中描述的这种需求,完全可以做的更好,如果能集成的官方系统中,感觉肯定会更好。
另外,个人感觉在飞牛中删除域名解析配置时,没必要调用服务商接口把域名也删掉,或者可以加个配置开关,用户来决定是否同步删除。 本来我用脚本做ddns的, 试了一下飞牛的ddns,结果在飞牛中删除后,发现脚本解析也失败了,登录cloudflare一看,发现那条域名解析也被删掉了。