Cloudflare+DDNS+Shell

2018-12-02 2192点热度 3人点赞

懒的换 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 。

StarryVoid

Have a good time