├── .github └── workflows │ ├── shellcheck.yml │ └── docker-build.yml ├── Dockerfile ├── LICENSE.txt ├── README.md └── ddns-route53 /.github/workflows/shellcheck.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main] 6 | jobs: 7 | shellcheck: 8 | name: shellcheck 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Run Shellcheck 13 | uses: ludeeus/action-shellcheck@master 14 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: docker-build 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main] 6 | jobs: 7 | buildx: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - id: buildx 13 | uses: docker/setup-buildx-action@v1 14 | with: 15 | install: true 16 | - name: Build Docker image 17 | run: | 18 | docker build . # will run buildx 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | LABEL maintainer "Mathias Söderberg " 3 | 4 | ENV AWSCLI_VERSION 1.18.119 5 | 6 | RUN apk --no-cache add bash bind-tools && \ 7 | pip install --no-cache-dir awscli==${AWSCLI_VERSION} 8 | 9 | COPY ddns-route53 /usr/local/bin/ddns-route53 10 | 11 | ARG AWS_ACCESS_KEY_ID 12 | ARG AWS_SECRET_ACCESS_KEY 13 | 14 | ENV AWS_ACCESS_KEY_ID $AWS_ACCESS_KEY_ID 15 | ENV AWS_SECRET_ACCESS_KEY $AWS_SECRET_ACCESS_KEY 16 | 17 | ENTRYPOINT ["/usr/local/bin/ddns-route53"] 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddns-route53 2 | 3 | [![Build Status](https://travis-ci.org/mthssdrbrg/ddns-route53.svg?branch=main)](https://travis-ci.org/mthssdrbrg/ddns-route53) 4 | [![GitHub Release](https://img.shields.io/github/release/mthssdrbrg/ddns-route53.svg)]() 5 | 6 | Simple dynamic DNS updater script using Amazon Route53. 7 | 8 | Source is originally from [Roll your own dynamic DNS service using Amazon Route53](https://willwarren.com/2014/07/03/roll-dynamic-dns-service-using-amazon-route53) 9 | though it's been modified to use OpenDNS for getting the current IP address, as 10 | well as usage of command line arguments and environment variables. 11 | 12 | ## Requirements 13 | 14 | * `bash` 15 | * `awscli` 16 | * `dig` 17 | 18 | ## Installation 19 | 20 | ```shell 21 | curl -sLO https://github.com/mthssdrbrg/ddns-route53/raw/$VERSION/ddns-route53 22 | ``` 23 | 24 | The following IAM policy (or something similar) will have to be applied to an user or role. 25 | Unfortunately it's not possible to restrict access to specific resource record (yet), but 26 | a workaround is to create a "sub" hosted zone for a specific record. 27 | 28 | ```json 29 | { 30 | "Version": "2012-10-17", 31 | "Statement": [ 32 | { 33 | "Action": [ 34 | "route53:ChangeResourceRecordSets", 35 | "route53:ListResourceRecordSets" 36 | ], 37 | "Effect": "Allow", 38 | "Resource": "arn:aws:route53:::hostedzone/EXAMPLE" 39 | } 40 | ] 41 | } 42 | ``` 43 | 44 | ## Usage 45 | 46 | ```shell 47 | $ ddns-route53 --zone-id --record-set 48 | ``` 49 | 50 | > Note: long options on the form `--long-option=` are not supported. 51 | 52 | The above command assumes that the necessary environment variables for `awscli` 53 | are set, an A type record and a TTL of 300 seconds. 54 | 55 | See `ddns-route53 --help` for more information about command line arguments. 56 | 57 | It's possible to set a number of environment variables instead of using command 58 | line arguments, though command line arguments take precedence over environment 59 | variables. 60 | 61 | * `DDNS_ROUTE53_TTL`: TTL for DNS record. 62 | * `DDNS_ROUTE53_TYPE`: DNS record type. 63 | * `DDNS_ROUTE53_COMMENT`: comment set when updating the Route53 record (only 64 | configurable as environment variable). 65 | * `DDNS_ROUTE53_ZONE_ID`: Amazon Route53 hosted zone ID. 66 | * `DDNS_ROUTE53_RECORD_SET`: Amazon Route53 record set name. 67 | * `DDNS_ROUTE53_SCRIPT`: path to script to execute on changes. 68 | 69 | ## Copyright 70 | 71 | This is free and unencumbered software released into the public domain. 72 | 73 | See LICENSE.txt or [unlicense.org](http://unlicense.org) for more information. 74 | -------------------------------------------------------------------------------- /ddns-route53: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | declare -r DATE_FORMAT="%Y-%m-%d %H:%M:%S" 4 | declare -r VERSION="2.1.0" 5 | declare ttl=${DDNS_ROUTE53_TTL:-300} 6 | declare record_type="${DDNS_ROUTE53_TYPE:-A}" 7 | declare comment="${DDNS_ROUTE53_COMMENT:-Updated at $(date +"$DATE_FORMAT")}" 8 | declare zone_id="$DDNS_ROUTE53_ZONE_ID" 9 | declare record_set="$DDNS_ROUTE53_RECORD_SET" 10 | declare handler_script="$DDNS_ROUTE53_SCRIPT" 11 | declare old_ip ip 12 | 13 | function errlog() { 14 | echo "$@" >&2 15 | } 16 | 17 | function log() { 18 | echo "$@" 19 | } 20 | 21 | function usage() { 22 | cat < Amazon Route53 hosted zone ID (required) 29 | -r, --record-set Amazon Route53 record set name (required) 30 | -t, --ttl TTL for DNS record 31 | -y, --type DNS record type 32 | -i, --ip Force usage of IP address 33 | -s, --script Path to script to execute on change 34 | -h, --help You're looking at it 35 | -v, --version Print version and exit 36 | HELP 37 | } 38 | 39 | function valid-ip() { 40 | local ip=$1 octets 41 | if [[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 42 | IFS='.' read -r -a octets <<< "$ip" 43 | for octet in "${octets[@]}"; do 44 | (( octet <= 255 )) || return 1 45 | done 46 | elif [[ "$ip" =~ ^[0-9a-fA-F:]\+$ ]]; then # Let's take the easy way out with validation of IPv6 addresses 47 | return 1 48 | fi 49 | return 0 50 | } 51 | 52 | function fetch-current-ip() { 53 | aws route53 list-resource-record-sets \ 54 | --hosted-zone-id "$zone_id" \ 55 | --output text \ 56 | --query "ResourceRecordSets[?Name == '$record_set.' && Type == '$record_type'].ResourceRecords[0].Value" 57 | } 58 | 59 | function init-args() { 60 | while (( $# > 0 )); do 61 | case "$1" in 62 | -z|--zone-id) zone_id="$2"; shift ;; 63 | -r|--record-set) record_set="$2"; shift ;; 64 | -i|--ip) ip="$2"; shift ;; 65 | -t|--ttl) ttl="$2"; shift ;; 66 | -y|--type) record_type="$2"; shift ;; 67 | -s|--script) handler_script="$2"; shift ;; 68 | -h|--help) usage; exit 0 ;; 69 | -v|--version) echo "$VERSION"; exit 0 ;; 70 | *) errlog "Unknown option: $1"; usage; exit 1 ;; 71 | esac 72 | shift 73 | done 74 | if [[ -z "$ip" ]]; then 75 | if [[ $record_type = "AAAA" ]]; then 76 | ip="$(dig +short -6 myip.opendns.com aaaa @resolver1.opendns.com)" 77 | else 78 | ip="$(dig +short -4 myip.opendns.com @resolver1.opendns.com)" 79 | fi 80 | # shellcheck disable=SC2181 # this is intentional 81 | (( $? != 0 )) && ip="" 82 | fi 83 | old_ip="$(fetch-current-ip 2> /dev/null)" 84 | return 0 85 | } 86 | 87 | function validate-args() { 88 | if [[ -z "$zone_id" ]]; then 89 | errlog "Missing -z or --zone-id or \$DDNS_ROUTE53_ZONE_ID" 90 | return 1 91 | elif [[ -z "$record_set" ]]; then 92 | errlog "Missing -r or --record-set or \$DDNS_ROUTE53_RECORD_SET" 93 | return 1 94 | elif [[ -n "$handler_script" && ! -x "$handler_script" ]]; then 95 | errlog "Script at '$handler_script' is not executable" 96 | return 1 97 | elif [[ -z "$ip" ]]; then 98 | errlog "Unable to determine current IP address" 99 | return 1 100 | fi 101 | return 0 102 | } 103 | 104 | function update-dns-entry() { 105 | local ip=$1 output success batch 106 | read -r -d '' batch << EOF 107 | { 108 | "Comment": "$comment", 109 | "Changes": [ 110 | { 111 | "Action": "UPSERT", 112 | "ResourceRecordSet": { 113 | "ResourceRecords": [{"Value": "$ip"}], 114 | "Name": "$record_set", 115 | "Type": "$record_type", 116 | "TTL": $ttl 117 | } 118 | } 119 | ] 120 | } 121 | EOF 122 | output="$(aws route53 change-resource-record-sets --hosted-zone-id "$zone_id" --change-batch "$batch" 2>&1)" 123 | success=$? 124 | if (( success == 0 )); then 125 | log "$output" 126 | else 127 | errlog "Failed to update DNS entry:" 128 | errlog "$output" 129 | fi 130 | return $success 131 | } 132 | 133 | function run-handler-script() { 134 | local old_ip=$1 new_ip=$2 135 | [[ -z "$handler_script" ]] || "$handler_script" "$old_ip" "$new_ip" 136 | } 137 | 138 | function main() { 139 | init-args "$@" 140 | validate-args || exit 1 141 | if ! valid-ip "$ip"; then 142 | log "Invalid IP address: '$ip'" 143 | exit 1 144 | fi 145 | if [[ "$old_ip" != "$ip" ]]; then 146 | log "IP changed from '$old_ip' to '$ip', updating entry" 147 | update-dns-entry "$ip" && run-handler-script "$old_ip" "$ip" 148 | else 149 | log "Current IP == $ip" 150 | fi 151 | exit $? 152 | } 153 | 154 | main "$@" 155 | --------------------------------------------------------------------------------