懒的换 IP 地址了,于是研究了一下 Cloudflare 的 API,用着还可以,做成小脚本。
1 、前言
服务器 IP 总是变,没事就会变个新的,这时候就需要一个 Dynamic Domain Name Server 来保证实时的 DNS 更换。
当然首先这个需要你的 DNS 解析商做配合,本文则采用 Cloudflare+DDNS+Shell
2 、准备
Cloudflare 的 Global API / Token
Cloudflare 解析的域名一个
Curl 已安装
3 、 DDNS 获取新 IP 地址 Shell 脚本
#!/usr/bin/env bash PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH # # Dynamic Domain Name Server (Cloudflare API) # # Author: StarryVoid <[email protected]> # Intro: https://blog.starryvoid.com/archives/313.html # Build: 2020/12/08 Version 2.3.1 # # Select API(1) Or Token(2) SelectAT="2" # CloudFlare API " X-Auth-Email: *** " " X-Auth-Key: *** " XAUTHEMAIL="YOUREMAILADDRESS" XAUTHKEY="YOURCLOUDFLAREAPIKEY" # CloudFlare Token " Authorization: Bearer *** " AuthorizationToken="YOURTOKEN" # Domain Name " example.domain " " ddns.example.domain " ZONENAME="example.domain" DOMAINNAME="ddns.example.domain" DOMAINTTL="1" # Output OUTPUTLOG="$(pwd)/${DOMAINNAME}.log" OUTPUTINFO="/tmp/ddns_${DOMAINNAME}.info" # Time DATETIME=$(date +%Y-%m-%d_%H-%M-%S) # ------------ Start ------------ function check_file_directory() { if [ "$(pwd)" == "/" ]; then if [ -f "${OUTPUTLOG}" ]; then echo "[Warning] The current directory is \"""$(pwd)""\". Please move to another path and delete this log file." > "${OUTPUTLOG}" ; exit 1 else if ! [ -f "$(pwd)/${DOMAINNAME}.log" ]; then echo "[Warning] The current directory is \"""$(pwd)""\". For management reasons, the log file path has been moved to \"/var/log/ddns/\". Remember to delete \"""${OUTPUTLOG}""\" log file" > "$(pwd)"./ddns_readme.log ; fi OUTPUTLOG="/var/log/ddns/${DOMAINNAME}.log" if ! [ -d "/var/log/ddns/" ]; then mkdir "/var/log/ddns/" && touch "${OUTPUTLOG}" ; fi if ! [ -f "${OUTPUTLOG}" ]; then echo "[Error] Could not create log file \"""${OUTPUTLOG}""\"" ; exit 1 ; fi fi fi } function check_environment () { if ! [ "$(command -v pwd)" ]; then echo "[Error] Command not found \"pwd\"" >> "${OUTPUTLOG}" ; exit 1 ; fi if ! [ -x "$(command -v curl)" ]; then echo "[Error] Command not found \"curl\"" >> "${OUTPUTLOG}" ; exit 1 ; fi } function check_selectAT () { if [[ ! "${SelectAT}" = 1 && ! "${SelectAT}" = 2 ]]; then echo "[Error] Failed to Select API(1) Or Token(2), Please check the configuration." >> "${OUTPUTLOG}"; exit 1; fi } function cloudflare_return_log_check() { cloudflare_return_log_check_status=$(echo "$1" | awk BEGIN"{RS=EOF}"'{gsub(/,/,"\n");print}' | sed 's/{/\n{\n/g' | sed 's/}/\n}\n/g' | sed 's/ //g' | grep -v "^$" | grep "$2" | head -1 | awk -F ":" '{print $2}' | sed 's/\"//g' ) echo "${cloudflare_return_log_check_status}" } function get_cloudflare_ipaddress_api() { [ -z "${Data_zone_records}" ] && LOG_get_zone_records_api=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$1" -H "X-Auth-Email: ${XAUTHEMAIL}" -H "X-Auth-Key: ${XAUTHKEY}" -H "Content-Type: application/json" --connect-timeout 30 -m 10 ) [ -z "${Data_zone_records}" ] && if [ ! "$(cloudflare_return_log_check "${LOG_get_zone_records_api}" "success")" == "true" ]; then echo "[Error] Failed to get cloudflare \"$1\" zone_records information" >> "${OUTPUTLOG}" ; exit 1; fi [ -z "${Data_zone_records}" ] && Data_zone_records=$(cloudflare_return_log_check "${LOG_get_zone_records_api}" "id") [ -z "${Data_dns_records}" ] && LOG_get_dns_records_api=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${Data_zone_records}/dns_records?type=A&name=$2" -H "X-Auth-Email: ${XAUTHEMAIL}" -H "X-Auth-Key: ${XAUTHKEY}" -H "Content-Type: application/json" --connect-timeout 30 -m 10 ) [ -z "${Data_dns_records}" ] && if [ ! "$(cloudflare_return_log_check "${LOG_get_dns_records_api}" "success")" == "true" ]; then echo "[Error] Failed to get cloudflare \"$2\" dns_records information" >> "${OUTPUTLOG}" ; exit 1; fi [ -z "${Data_dns_records}" ] && Data_dns_records=$(cloudflare_return_log_check "${LOG_get_dns_records_api}" "id") LOG_get_domain_ip_api=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${Data_zone_records}/dns_records/${Data_dns_records}" -H "X-Auth-Email: ${XAUTHEMAIL}" -H "X-Auth-Key: ${XAUTHKEY}" -H "Content-Type: application/json" --connect-timeout 30 -m 10 ) if [ ! "$(cloudflare_return_log_check "${LOG_get_domain_ip_api}" "success")" == "true" ]; then echo "[Error] Failed to get cloudflare \"$2\" ip_address information" >> "${OUTPUTLOG}" ; exit 1; fi Data_domain_ip=$(cloudflare_return_log_check "${LOG_get_domain_ip_api}" content) } function get_cloudflare_ipaddress_token() { [ -z "${Data_zone_records}" ] && LOG_get_zone_records_token=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$1" -H "Authorization: Bearer ${AuthorizationToken}" -H "Content-Type: application/json" --connect-timeout 30 -m 10 ) [ -z "${Data_zone_records}" ] && if [ ! "$(cloudflare_return_log_check "${LOG_get_zone_records_token}" "success")" == "true" ]; then echo "[Error] Failed to get cloudflare \"$1\" zone_records information" >> "${OUTPUTLOG}" ; exit 1; fi [ -z "${Data_zone_records}" ] && Data_zone_records=$(cloudflare_return_log_check "${LOG_get_zone_records_token}" "id") [ -z "${Data_dns_records}" ] && LOG_get_dns_records_token=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${Data_zone_records}/dns_records?type=A&name=$2" -H "Authorization: Bearer ${AuthorizationToken}" -H "Content-Type: application/json" --connect-timeout 30 -m 10 ) [ -z "${Data_dns_records}" ] && if [ ! "$(cloudflare_return_log_check "${LOG_get_dns_records_token}" "success")" == "true" ]; then echo "[Error] Failed to get cloudflare \"$2\" dns_records information" >> "${OUTPUTLOG}" ; exit 1; fi [ -z "${Data_dns_records}" ] && Data_dns_records=$(cloudflare_return_log_check "${LOG_get_dns_records_token}" "id") LOG_get_domain_ip_token=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${Data_zone_records}/dns_records/${Data_dns_records}" -H "Authorization: Bearer ${AuthorizationToken}" -H "Content-Type: application/json" --connect-timeout 30 -m 10 ) if [ ! "$(cloudflare_return_log_check "${LOG_get_domain_ip_token}" "success")" == "true" ]; then echo "[Error] Failed to get cloudflare \"$2\" ip_address information" >> "${OUTPUTLOG}" ; exit 1; fi Data_domain_ip=$(cloudflare_return_log_check "${LOG_get_domain_ip_token}" content) } function get_server_new_ip() { [ -z "${NEWIPADD}" ] && NEWIPADD=$(curl -s --retry 1 --connect-timeout 2 https://ipv4.icanhazip.com/) [ -z "${NEWIPADD}" ] && NEWIPADD=$(curl -s --retry 1 --connect-timeout 2 https://api.ipify.org/) [ -z "${NEWIPADD}" ] && NEWIPADD=$(curl -s --retry 1 --connect-timeout 2 https://ipinfo.io/ip/) [ -z "${NEWIPADD}" ] && NEWIPADD=$(curl -s --retry 1 --connect-timeout 2 http://ipv4.icanhazip.com/) [ -z "${NEWIPADD}" ] && NEWIPADD=$(curl -s --retry 1 --connect-timeout 2 http://api.ipify.org/) [ -z "${NEWIPADD}" ] && NEWIPADD=$(curl -s --retry 1 --connect-timeout 2 http://ipinfo.io/ip/) if [[ ! "${NEWIPADD}" ]]; then echo "[Error] Failed to obtain the public address of the current network." >> "${OUTPUTLOG}"; exit 1; fi } function update_new_ipaddress_api() { echo "[Info] IP address will been modified from \"""${Data_domain_ip}""\" to \"""${NEWIPADD}""\"." >> "${OUTPUTLOG}" LOG_update_new_ipaddress_api=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${Data_zone_records}/dns_records/${Data_dns_records}" -H "X-Auth-Email: ${XAUTHEMAIL}" -H "X-Auth-Key: ${XAUTHKEY}" -H "Content-Type: application/json" --data "{\"type\":\"A\",\"name\":\"""$1""\",\"content\":\"""$2""\",\"ttl\":""$3"",\"proxied\":false}" --connect-timeout 30 -m 10 ) if [ ! "$(cloudflare_return_log_check "${LOG_update_new_ipaddress_api}" "success")" == "true" ]; then echo "[Error] Failed to update cloudflare address." >> "${OUTPUTLOG}" ; exit 1; fi } function update_new_ipaddress_token() { echo "[Info] IP address will been modified from \"""${Data_domain_ip}""\" to \"""${NEWIPADD}""\"." >> "${OUTPUTLOG}" LOG_update_new_ipaddress_token=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${Data_zone_records}/dns_records/${Data_dns_records}" -H "Authorization: Bearer ${AuthorizationToken}" -H "Content-Type: application/json" --data "{\"type\":\"A\",\"name\":\"""$1""\",\"content\":\"""$2""\",\"ttl\":""$3"",\"proxied\":false}" --connect-timeout 30 -m 10 ) if [ ! "$(cloudflare_return_log_check "${LOG_update_new_ipaddress_token}" "success")" == "true" ]; then echo "[Error] Failed to update cloudflare address." >> "${OUTPUTLOG}" ; exit 1; fi } function update_new_ipaddress() { if [ "${SelectAT}" = 1 ]; then update_new_ipaddress_api "${DOMAINNAME}" "${NEWIPADD}" "${DOMAINTTL}" ; fi if [ "${SelectAT}" = 2 ]; then update_new_ipaddress_token "${DOMAINNAME}" "${NEWIPADD}" "${DOMAINTTL}" ; fi sleep 10s if [[ ! "${Data_domain_ip}" ]]; then Data_domain_ip="" ; fi if [ "${SelectAT}" = 1 ]; then get_cloudflare_ipaddress_api "${ZONENAME}" "${DOMAINNAME}" ; fi if [ "${SelectAT}" = 2 ]; then get_cloudflare_ipaddress_token "${ZONENAME}" "${DOMAINNAME}" ; fi if [[ "${NEWIPADD}" == "${Data_domain_ip}" ]]; then echo "[Info] IP address has been modified to \"""${NEWIPADD}""\"." >> "${OUTPUTLOG}" make_records_file exit 0 else echo "[Error] IP address modification failed." >> "${OUTPUTLOG}" exit 1 fi } function make_records_file() { echo "{\"datatime\":""${DATETIME}"":,\"zone_records\":""${Data_zone_records}"",\"dns_records\":""${Data_dns_records}"",\"ip_address\":""${NEWIPADD}""}" > "${OUTPUTINFO}" echo "[Info] Successfully generated DDNS information file." >> "${OUTPUTLOG}" } function read_records_file() { if [ -f "${OUTPUTINFO}" ]; then Data_file_info=$(cat < "${OUTPUTINFO}") Data_zone_records=$(cloudflare_return_log_check "${Data_file_info}" zone_records) Data_dns_records=$(cloudflare_return_log_check "${Data_file_info}" dns_records) Old_domain_ip=$(cloudflare_return_log_check "${Data_file_info}" ip_address) else echo "[Warning] Could not find local configuration file." >> "${OUTPUTLOG}" fi if ! [[ "${Data_zone_records}" && "${Data_dns_records}" && "${Old_domain_ip}" ]]; then echo "[Warning] Failed to check local configuration file." >> "${OUTPUTLOG}"; fi } function main() { check_file_directory check_environment check_selectAT echo "[Info] Running Time is ${DATETIME}" >> "${OUTPUTLOG}" get_server_new_ip read_records_file if [[ "${NEWIPADD}" != "${Old_domain_ip}" ]]; then if [ "${SelectAT}" = 1 ]; then get_cloudflare_ipaddress_api "${ZONENAME}" "${DOMAINNAME}" ; fi if [ "${SelectAT}" = 2 ]; then get_cloudflare_ipaddress_token "${ZONENAME}" "${DOMAINNAME}" ; fi if [[ "${NEWIPADD}" != "${Data_domain_ip}" ]]; then update_new_ipaddress exit 0 else # echo "[Info] The ip address is the same as the cloudflare record." >> "${OUTPUTLOG}" make_records_file exit 0 fi else # echo "[Info] There is no need to change ip address." >> "${OUTPUTLOG}" exit 0 fi } main
4 、讲解
首先本文制作过程中参考过秋水逸冰的脚本,在此表示感谢
4.1 、脚本配置 Global API 版
我们需要将所需的内容(Cloudflare API 和 DDNS 域名)填入对应位置
# Select API(1) Or Token(2) SelectAT="1" #选择模式 # CloudFlare API " X-Auth-Email: *** " " X-Auth-Key: *** " XAUTHEMAIL="[email protected]" #你的 Cloudflare 邮箱用户名 XAUTHKEY="123123123123" #你的 Cloudflare Global API Key # Domain Name " example.domain " " ddns.example.domain " ZONENAME="example.domain" #你的二级域名 DOMAINNAME="ddns.example.domain" #你的 DDNS 域名 DOMAINTTL="1" #你的域名 TTL 时间,默认 1 为 auto
4.2 、脚本配置 Token 版
我们需要将所需的内容(Cloudflare Token 和 DDNS 域名)填入对应位置
# Select API(1) Or Token(2) SelectAT="2" # CloudFlare Token " Authorization: Bearer *** " AuthorizationToken="YOURTOKEN" #你的 Token 密钥 # Domain Name " example.domain " " ddns.example.domain " ZONENAME="example.domain" #你的二级域名 DOMAINNAME="ddns.example.domain" #你的 DDNS 域名 DOMAINTTL="1" #你的域名 TTL 时间,默认 1 为 auto
4.3 、定时运行 crontab
如果需要定时运行,可以编辑 /etc/crontab 实现定期运行,下例为 10min 运行一次
可以把 DDNS 脚本放在同一个目录下,会自动根据脚本内填写的域名生成对应文件名的日志到本地目录,数据文件存放在 /tmp/ 下。
# Example of job definition: # .---------------- minute (0 - 59) # | .------------- hour (0 - 23) # | | .---------- day of month (1 - 31) # | | | .------- month (1 - 12) OR jan,feb,mar,apr ... # | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat # | | | | | # * * * * * user-name command to be executed */10 * * * * root bash /opt/ddns/cloudflare-ddns.sh
4.4 、定时运行 systemd
定时运行还可以通过 systemd 来进行管理。下例为 10min 运行一次,支持指定用户。
下方的配置文件必须把 DDNS 脚本放在同一个目录下,日志存放在 /var/log/ddns/ 下,数据文件存放在 /tmp/ 下。
vim /etc/systemd/system/ddns_auto_yourdomain.timer
[Unit] Description=Automatically trigger the DDNS service process [Timer] [email protected] #关联单次启动服务名 OnBootSec=10min #关联首次启动命令运行延迟 OnUnitActiveSec=10m #关联单次命令运行周期 [Install] WantedBy=timers.target
vim /etc/systemd/system/[email protected]
[Unit] Description=DDNS service After=network.target nss-lookup.target [Service] Type=oneshot User=root #限定 DDNS 脚本运行用户 NoNewPrivileges=true KillMode=process #WorkingDirectory=/opt/ddns #限制 DDNS 脚本运行相关文件目录,不指定时采用 systemd 管理的脚本,参数文件与日志文件路径会直接参照下方移动。 ExecStart=/bin/bash /opt/ddns/%i.sh #关联 DDNS 脚本存放目录,文件仅需只读属性 StateDirectory=ddns #指定参数文件路径 /var/lib/ddns/ 如指定 WorkingDirectory 则不需要此选项 LogsDirectory=ddns #指定日志文件路径 /var/lib/ddns/ 如指定 WorkingDirectory 则不需要此选项
单次启动方式为 systemctl start [email protected]
对应脚本为 /opt/ddns/yourdomain.sh
循环启动方式为 systemctl start ddns_auto_yourdomain.timer
开机自动启动为 systemctl enable ddns_auto_yourdomain.timer
4.5 、脚本输出
默认会在脚本当前目录下生成一个日志文件 yourdomain.log 和 在 tmp 下存放一个缓存文件 ddns_yourdomain.info ,前者是储存运行日志,后者是储存获取的 API 信息。
[root@linux ddns]# cat /tmp/ddns_yourdomain.info
{"datatime":2020-12-08_18-00:00:,"zone_records":0123456789abcde,"dns_records":0123456789abcde,"ip_address":1.2.3.4}
[root@linux ddns]# cat yourdomain.log
[Info] Running Time is 2020-12-08_18-00-00
[Warning] Could not find local configuration file.
[Warning] Failed to check local configuration file.
[Info] Successfully generated DDNS information file.
如果出现 [Error]
问题,可以在日志中查看问题原因。
4.6 、获取 Cloudflare 的 API Tokens
首先进入 Cloudflare 的个人配置页面 [链接]
找到下面的 API Tokens (Manage access and permissions for your accounts, sites, and products.)
然后点击右侧的 Create Token 创建新的 Token ( 相对 Global API 约束访问权限 )
新的 Token 需要配置权限,本次 DDNS 需要的权限分别为 Account.Dns Firewall.Read 和 Zone.Zone.Read 和 Zone.DNS.Edit 三条。配置好后确认
*** 提醒:2020/03/05 发现 Cloudflare 如果 Token 配置了 Zone Resources 限制区域,会导致无法获取 Zone Record 。
*** 现象:手动执行获取 zone_record 命令返回 "message":"Actor 'com.cloudflare.api.token.***' requires permission 'com.cloudflare.api.account.zone.list' to list zones"
*** 暂时解决方式:配置为 All zones 。
最后核对好权限后再次确认,显示" *** API token was successfully updated ",此时你可以查看你的 Token ,并点击 Copy 即可复制。
4.7 、获取 Cloudflare 的 Global API
首先进入 Cloudflare 的个人配置页面 [链接]
找到下面的 API Keys (Keys used to access Cloudflare APIs.)
然后在 Global API Key 一行点击右侧的 View 查看你的 Global API Key
最后额外注意,Global API 需要搭配你的邮箱账户名才可以使用
5 、后期修订
2019/11/05 、经由 Sion 提醒,换行符在发表文章时丢失,现已提供下载地址。 同时支持 Token 方式管理。
2020/03/05 、发现 Cloudflare 关于 Token 配置疑似 Bug ,已增加额外提醒。
2020/04/26 、发现 Cloudflare 关于 List DNS Records 的返回结果有变化,额外增加判定
2020/12/08 、重新梳理并上传 github 。