1#!/bin/sh 2 3self='hetzner_ddns' 4 5# Read variabels from configuration file 6if test -G "/usr/local/etc/$self.conf"; then 7 . "/usr/local/etc/$self.conf" 8else 9 >&2 echo 'unable to read configuration file' 10 exit 78 11fi 12 13# Check dependencies 14if ! command -v curl > /dev/null || \ 15 ! command -v awk > /dev/null || \ 16 ! command -v jq > /dev/null 17then 18 >&2 echo 'missing dependency' 19 exit 1 20fi 21 22# Check logging support 23if ! touch "/var/log/$self.log"; 24then 25 >&2 echo 'unable to open logfile' 26 exit 2 27fi 28 29get_zone() { 30 # Get zone ID 31 zone="$( 32 curl "https://dns.hetzner.com/api/v1/zones" \ 33 -H "Auth-API-Token: $key" 2>/dev/null | \ 34 jq -r '.zones[] | .name + " " + .id' | \ 35 awk "\$1==\"$domain\" {print \$2}" 36 )" 37 if [ -z "$zone" ]; then 38 return 1 39 else 40 printf '[%s] Zone for %s: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" \ 41 "$domain" "$zone" >> "/var/log/$self.log" 42 fi 43} 44 45get_record() { 46 # Get record IDs 47 if [ -n "$zone" ]; then 48 record_ipv4="$( 49 curl "https://dns.hetzner.com/api/v1/records?zone_id=$zone" \ 50 -H "Auth-API-Token: $key" 2>/dev/null | \ 51 jq -r '.records[] | .name + " " + .type + " " + .id' | \ 52 awk "\$1==\"$1\" && \$2==\"A\" {print \$3}" 53 )" 54 record_ipv6="$( 55 curl "https://dns.hetzner.com/api/v1/records?zone_id=$zone" \ 56 -H "Auth-API-Token: $key" 2>/dev/null | \ 57 jq -r '.records[] | .name + " " + .type + " " + .id' | \ 58 awk "\$1==\"$1\" && \$2==\"AAAA\" {print \$3}" 59 )" 60 fi 61 if [ -z "$record_ipv4" ] && [ -z "$record_ipv6" ]; then 62 return 1 63 else 64 printf '[%s] IPv4 record for %s: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1.$domain" \ 65 "${record_ipv4:-(missing)}" >> "/var/log/$self.log" 66 printf '[%s] IPv6 record for %s: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1.$domain" \ 67 "${record_ipv6:-(missing)}" >> "/var/log/$self.log" 68 fi 69} 70 71get_records() { 72 # Get all record IDs 73 for n in $records; do 74 if get_record "$n"; then 75 records_ipv4="$records_ipv4$n=$record_ipv4 " 76 records_ipv6="$records_ipv6$n=$record_ipv6 " 77 else 78 printf '[%s] Missing both records for %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" \ 79 "$n.$domain" >> "/var/log/$self.log" 80 fi 81 done 82} 83 84get_record_ip_addr() { 85 # Get record's IP address 86 if [ -n "$record_ipv4" ]; then 87 ipv4_rec="$( 88 curl "https://dns.hetzner.com/api/v1/records/$record_ipv4" \ 89 -H "Auth-API-Token: $key" 2>/dev/null | \ 90 jq -r '.record.value' 91 )" 92 fi 93 if [ -n "$record_ipv6" ]; then 94 ipv6_rec="$( 95 curl "https://dns.hetzner.com/api/v1/records/$record_ipv6" \ 96 -H "Auth-API-Token: $key" 2>/dev/null | \ 97 jq -r '.record.value' 98 )" 99 fi 100 if [ -z "$ipv4_rec" ] && [ -z "$ipv6_rec" ]; then 101 return 1 102 fi 103} 104 105get_my_ip_addr() { 106 # Get current public IP address 107 ipv4_cur="$( 108 curl 'http://ipv4.whatismyip.akamai.com/' 2>/dev/null 109 )" 110 ipv6_cur="$( 111 curl 'http://ipv6.whatismyip.akamai.com/' 2>/dev/null 112 )" 113 if [ -z "$ipv4_cur" ] && [ -z "$ipv6_cur" ]; then 114 return 1 115 fi 116} 117 118set_record() { 119 # Update record if IP address has changed 120 if [ -n "$record_ipv4" ] && [ -n "$ipv4_cur" ] && [ "$ipv4_cur" != "$ipv4_rec" ]; then 121 curl -X "PUT" "https://dns.hetzner.com/api/v1/records/$record_ipv4" \ 122 -H 'Content-Type: application/json' \ 123 -H "Auth-API-Token: $key" \ 124 -d "{ 125 \"value\": \"$ipv4_cur\", 126 \"ttl\": $interval, 127 \"type\": \"A\", 128 \"name\": \"$n\", 129 \"zone_id\": \"$zone\" 130 }" 1>/dev/null 2>/dev/null && 131 printf "[%s] Update IPv4 for %s: %s => %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" \ 132 "$n.$domain" "$ipv4_rec" "$ipv4_cur" >> "/var/log/$self.log" 133 fi 134 if [ -n "$record_ipv6" ] && [ -n "$ipv6_cur" ] && [ "$ipv6_cur" != "$ipv6_rec" ]; then 135 curl -X "PUT" "https://dns.hetzner.com/api/v1/records/$record_ipv6" \ 136 -H 'Content-Type: application/json' \ 137 -H "Auth-API-Token: $key" \ 138 -d "{ 139 \"value\": \"$ipv6_cur\", 140 \"ttl\": $interval, 141 \"type\": \"AAAA\", 142 \"name\": \"$n\", 143 \"zone_id\": \"$zone\" 144 }" 1>/dev/null 2>/dev/null && 145 printf "[%s] Update IPv6 for %s: %s => %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" \ 146 "$n.$domain" "$ipv6_rec" "$ipv6_cur" >> "/var/log/$self.log" 147 fi 148} 149 150pick_record() { 151 # Get record ID from array 152 echo "$2" | \ 153 awk "{ 154 for(i=1;i<=NF;i++){ 155 n=\$i;gsub(/=.*/,\"\",n); 156 r=\$i;gsub(/.*=/,\"\",r); 157 if(n==\"$1\"){ 158 print r;break 159 } 160 }}" 161} 162 163set_records() { 164 # Get my public IP address 165 if get_my_ip_addr; then 166 # Update all records if possible 167 for n in $records; do 168 record_ipv4="$(pick_record "$n" "$records_ipv4")" 169 record_ipv6="$(pick_record "$n" "$records_ipv6")" 170 if [ -n "$record_ipv4" ] || [ -n "$record_ipv6" ]; then 171 get_record_ip_addr && set_record 172 fi 173 done 174 fi 175} 176 177run_ddns() { 178 printf '[%s] Started Hetzner DDNS daemon\n' "$(date '+%Y-%m-%d %H:%M:%S')" \ 179 >> "/var/log/$self.log" 180 181 while ! get_zone || ! get_records; do 182 sleep $((interval/2+1)) 183 done 184 185 while true; do 186 set_records 187 sleep "$interval" 188 done 189} 190 191if [ "$1" = '--daemon' ]; then 192 # Deamonize and write PID to file 193 if touch "/var/run/$self.pid"; 194 then 195 run_ddns & 196 echo $! > "/var/run/$self.pid" 197 else 198 >&2 echo 'unable to daemonize' 199 exit 2 200 fi 201else 202 # Run in foreground 203 run_ddns 204fi 205