+ feat ipv6; + systemd integ

This commit is contained in:
rskntroot
2024-07-29 09:35:50 +00:00
parent bf97e5a498
commit 214e3fabd4
9 changed files with 650 additions and 211 deletions

323
Cargo.lock generated
View File

@@ -17,6 +17,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.15" version = "0.6.15"
@@ -66,6 +75,17 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "async-trait"
version = "0.1.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -519,7 +539,7 @@ version = "4.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
dependencies = [ dependencies = [
"heck", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@@ -572,6 +592,12 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "data-encoding"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@@ -607,6 +633,41 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "enum-as-inner"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "env_filter"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@@ -780,6 +841,12 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -807,6 +874,17 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.12" version = "0.2.12"
@@ -875,6 +953,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.30" version = "0.14.30"
@@ -988,6 +1072,16 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "idna"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@@ -1008,6 +1102,18 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "ipconfig"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2",
"widestring",
"windows-sys 0.48.0",
"winreg 0.50.0",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.9.0" version = "2.9.0"
@@ -1041,6 +1147,12 @@ version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.14" version = "0.4.14"
@@ -1063,6 +1175,21 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lru-cache"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@@ -1275,6 +1402,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.86" version = "1.0.86"
@@ -1284,6 +1417,12 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.36" version = "1.0.36"
@@ -1300,8 +1439,43 @@ dependencies = [
"aws-config", "aws-config",
"aws-sdk-route53", "aws-sdk-route53",
"clap", "clap",
"env_logger",
"log",
"reqwest", "reqwest",
"thiserror",
"tokio", "tokio",
"trust-dns-proto",
"trust-dns-resolver",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
] ]
[[package]] [[package]]
@@ -1313,12 +1487,41 @@ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
] ]
[[package]]
name = "regex"
version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]] [[package]]
name = "regex-lite" name = "regex-lite"
version = "0.1.6" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.5" version = "0.12.5"
@@ -1360,7 +1563,17 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"winreg", "winreg 0.52.0",
]
[[package]]
name = "resolv-conf"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
dependencies = [
"hostname",
"quick-error",
] ]
[[package]] [[package]]
@@ -1705,6 +1918,26 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "thiserror"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.36" version = "0.3.36"
@@ -1881,6 +2114,52 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "trust-dns-proto"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3119112651c157f4488931a01e586aa459736e9d6046d3bd9105ffb69352d374"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna 0.4.0",
"ipnet",
"once_cell",
"rand",
"smallvec",
"thiserror",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "trust-dns-resolver"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a3e6c3aff1718b3c73e395d1f35202ba2ffa847c6a62eea0db8fb4cfe30be6"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lru-cache",
"once_cell",
"parking_lot",
"rand",
"resolv-conf",
"smallvec",
"thiserror",
"tokio",
"tracing",
"trust-dns-proto",
]
[[package]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.5" version = "0.2.5"
@@ -1927,7 +2206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna 0.5.0",
"percent-encoding", "percent-encoding",
] ]
@@ -2058,6 +2337,34 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "widestring"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@@ -2197,6 +2504,16 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.52.0" version = "0.52.0"

View File

@@ -6,6 +6,11 @@ edition = "2021"
[dependencies] [dependencies]
aws-config = { version = "1.1.7", features = ["behavior-version-latest"] } aws-config = { version = "1.1.7", features = ["behavior-version-latest"] }
aws-sdk-route53 = "1.37.0" aws-sdk-route53 = "1.37.0"
tokio = { version = "1", features = ["full"] }
clap = { version = "4.5.11", features = ["derive"] } clap = { version = "4.5.11", features = ["derive"] }
env_logger = "0.11"
log = "0.4"
reqwest = { version = "0.12.5", features = ["blocking", "json"] } reqwest = { version = "0.12.5", features = ["blocking", "json"] }
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
trust-dns-proto = "0.23.2"
trust-dns-resolver = "0.23.2"

106
README.md
View File

@@ -1,108 +1,88 @@
# r53-ddns # r53-ddns
Route53 Dynamic DNS
## Brief ## Brief
this was intended to solve the problem of when your local ISP force renews your PublicIP and no one can reach your not-sure-if-toaster-or-minecraft-server. Submits a Route53 `ChangeRequest` for updating `A` or `AAAA` records when PublicIP drift is detected.
what it ended up as, was instead a mockery of dynamic dns, progam/scripting, error handling, all in memory-safe rust. whatever that means, right? Drift detection is determined by comparing http request to `icanhazip.com` and a DNS lookup to `cloudflare`.
> "this is the worst example of working code ive ever seen" This is intended to be installed on a public-facing loadbalancer.
> // "ah, but you have seen the code working"
>
> -- Rskntroot 2024
## Assumptions ## Assumptions
1. Your ISP randomly changes your PublicIP and that pisses you off. 1. Your ISP randomly changes your PublicIP and that pisses you off.
1. You have no idea how DDNS is actually supposed to work. 1. You just want something that will curl `ipv4.icanhazip.com`, check 3rd-party dns, and update Route53.
- dns is all smoke and mirrors, confirmed. 1. Your Name records only contain a single IP. (future update maybe).
1. You just want something that will curl `ipv4.icanhazip.com` and push the update to Route53.
1. You plan on handjamming this into a cron job on your webserver/loadbalancer.
1. ...
1. Profit.
Congratulations, this is the package for you. If so, this is for you.
## Setup ## Setup
1. use output of below command to create the IAM policy 1. setup `Route53AllowRecordUpdate.policy`
``` zsh ```zsh
zone_id=<zone_id> envsubst < AllowRoute53RecordUpdate.policy DNS_ZONE_ID=YOURZONEIDHERE \
envsubst < aws.policy > Route53AllowRecordUpdate.policy
``` ```
1. create IAM user, generate access keys for automated service 1. create IAM user, generate access keys for automated service
1. log into aws with the acct on the machine where you install this binary 1. log into aws with the acct on the machine where you install this binary
``` ```
aws sso login --profile aws sso login --profile
``` ```
1. setup a user-level cron job to poll at your leisure 1. setup link in `/usr/bin`
``` zsh
ln -s -f ~/r53-ddns/target/release/r53-ddns /usr/bin/r53-ddns
``` ```
0 * * * * ~/home/lost/.r53-update-dns.sh 1. setup systemd service and then install as normal
```zsh
DNS_ZONE_ID=YOURZONEIDHERE \
DOMAIN_NAME=your.domain.com. \
envsubst < r53-ddns.service | sudo /etc/system/
``` ```
## Usage ## CLI Usage
``` ```
$ r53-ddns -h $ r53-ddns -h
A CLI tool for correcting drift between your PublicIP and Route53 DNS A RECORD A CLI tool for correcting drift between your PublicIP and Route53 DNS A RECORD
Usage: r53-ddns --zone-id <ZONE_ID> --domain-name <DOMAIN_NAME> Usage: r53-ddns --dns-zone-id <DNS_ZONE_ID> --domain-name <DOMAIN_NAME>
Options: Options:
-z, --zone-id <ZONE_ID> DNS ZONE ID (see AWS Console Route53) -z, --dns-zone-id <DNS_ZONE_ID> DNS ZONE ID (see AWS Console Route53)
-d, --domain-name <DOMAIN_NAME> DOMAIN NAME (ex. 'docs.rskio.com.') -d, --domain-name <DOMAIN_NAME> DOMAIN NAME (ex. 'docs.rskio.com.')
-h, --help Print help -h, --help Print help
``` ```
### Drift Detected ### Example
``` zsh
$ r53-ddns -z ${aws_dns_zone_id} -d smp.rskio.com.
```
``` ```
The dynamic IP provided by the ISP has drifted. 10.0.11.201 -> 10.0.88.219 $ systemctl status r53-ddns.service
Requested DNS record update to PublicIP: 10.0.88.219 ● r53-ddns.service - Route53 Dynamic DNS Service
Change ID: /change/C04224022UE1ZQA26RE7O, Status: Pending Loaded: loaded (/etc/systemd/system/r53-ddns.service; enabled; vendor preset: enabled)
Change ID: /change/C04224022UE1ZQA26RE7O, Status: Insync Active: active (running) since Mon 2024-07-29 09:03:40 UTC; 7min ago
The change request has been completed. Main PID: 215630 (r53-ddns)
``` Tasks: 6 (limit: 18886)
Memory: 3.6M
CPU: 389ms
CGroup: /system.slice/r53-ddns.service
└─215630 /usr/bin/r53-ddns -z [##TRUNCATED##] -d rskio.com.
### No Drift Detected Jul 29 09:03:40 hostname systemd[1]: Started Route53 Dynamic DNS Service.
Jul 29 09:03:40 hostname r53-ddns[215630]: [2024-07-29T09:03:40Z INFO r53_ddns] starting with options: -z [##TRUNCATED##] -d rskio.com.
``` zsh Jul 29 09:09:41 hostname r53-ddns[215630]: [2024-07-29T09:09:41Z INFO r53_ddns::dns] dynamic ip drift detected: 10.0.0.1 -> 71.211.88.219
$ r53-ddns -z ${aws_dns_zone_id} -d example.com. Jul 29 09:09:41 hostname r53-ddns[215630]: [2024-07-29T09:09:41Z INFO r53_ddns::route53] requesting update to route53 record for A rskio.com. -> 71.211.88.219
``` Jul 29 09:09:41 hostname r53-ddns[215630]: [2024-07-29T09:09:41Z INFO r53_ddns::route53] change_id: /change/C02168177BNS6R50C32Q has status: Pending
Jul 29 09:10:41 hostname r53-ddns[215630]: [2024-07-29T09:09:41Z INFO r53_ddns::route53] change_id: /change/C02168177BNS6R50C32Q has status: Insync
```
The DNS record is currently up to date with the public IP: 10.0.88.219
```
### Tests
Yeah, I have em! Well... one of them.
```
$ cargo test
Compiling r53-ddns v0.1.0 (~/workspace/r53-ddns)
Finished `test` profile [unoptimized + debuginfo] target(s) in 3.81s
Running unittests src/main.rs (target/debug/deps/r53_ddns-9ff92b89721daeea)
running 1 test
test tests::get_public_ip_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.07s
``` ```
## Q&A ## Q&A
> Why are you doing AWS calls instead of us nslookup and compare that? > Why did you do create this monster in rust?
Why in the world would internal DNS give me a PublicIP? Imagine not implementing internal DNS. To be able to handle errors in the future.
> Why did you do create this monster?
To prove to myself that with the help of LLMs that even I could go from nothing to a deployed tokio async rust binary in less than 8 hours. And thats exactly what I did.
> wen IPv6? > wen IPv6?
Stfu, John. How about, wen PR? It should work with IPv6.

24
aws.policy Normal file
View File

@@ -0,0 +1,24 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets",
"route53:GetChange"
],
"Resource": [
"arn:aws:route53:::hostedzone/${DNS_ZONE_ID}",
"arn:aws:route53:::change/*"
]
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "route53:TestDNSAnswer",
"Resource": "*"
}
]
}

View File

@@ -10,7 +10,7 @@
"route53:GetChange" "route53:GetChange"
], ],
"Resource": [ "Resource": [
"arn:aws:route53:::hostedzone/${zone_id}", "arn:aws:route53:::hostedzone/",
"arn:aws:route53:::change/*" "arn:aws:route53:::change/*"
] ]
}, },

16
r53-ddns.service Normal file
View File

@@ -0,0 +1,16 @@
[Unit]
Description=Route53 Dynamic DNS Service
After=network-online.target
Requires=network-online.target
[Service]
Type=simple
RemainAfterExit=yes
ExecStart=/usr/bin/r53-ddns -z ${DNS_ZONE_ID} -d ${DOMAIN_NAME}
User=${USER}
Restart=always
RestartSec=60
[Install]
WantedBy=multi-user.target

52
src/dns.rs Normal file
View File

@@ -0,0 +1,52 @@
use std::error::Error;
use std::net::IpAddr;
use log::info;
use trust_dns_proto::rr::record_type::RecordType;
use trust_dns_proto::rr::RecordData;
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
use trust_dns_resolver::TokioAsyncResolver;
pub async fn is_addr_current(domain: &str, ip_addr: IpAddr) -> Result<bool, Box<dyn Error>> {
let response = TokioAsyncResolver::tokio(ResolverConfig::cloudflare(), ResolverOpts::default())
.lookup(
domain,
match ip_addr {
IpAddr::V4(_) => RecordType::A,
IpAddr::V6(_) => RecordType::AAAA,
},
)
.await?;
let mut record_ip: Option<IpAddr> = None;
for record in response.into_iter() {
record_ip = record.into_rdata().ip_addr();
if !record_ip.is_none() && record_ip == Some(ip_addr) {
return Ok(true);
}
}
info!(
"dynamic ip drift detected: {} -> {}",
record_ip.unwrap(),
ip_addr
);
Ok(false)
}
#[cfg(test)]
mod unit {
use std::net::IpAddr;
use std::str::FromStr;
#[tokio::test]
async fn test_is_addr_current() {
let domain = "rskio.com";
let ip_addr = IpAddr::from_str("0.0.0.0").unwrap();
assert_eq!(
super::is_addr_current(domain, ip_addr).await.unwrap(),
false
);
}
}

View File

@@ -1,12 +1,12 @@
use aws_config::meta::region::RegionProviderChain; mod dns;
use aws_sdk_route53 as route53; mod route53;
use aws_sdk_route53::types::{
Change, ChangeAction, ChangeBatch, ResourceRecord, ResourceRecordSet,
};
use clap::Parser; use clap::Parser;
use env_logger::Builder;
use std::net::Ipv4Addr; use log::info;
use reqwest::get;
use std::net::IpAddr;
use tokio::time::{sleep, Duration};
#[derive(Parser)] #[derive(Parser)]
#[clap( #[clap(
@@ -14,166 +14,48 @@ use std::net::Ipv4Addr;
about = "A CLI tool for correcting drift between your PublicIP and Route53 DNS A RECORD" about = "A CLI tool for correcting drift between your PublicIP and Route53 DNS A RECORD"
)] )]
struct Args { struct Args {
#[clap(short, long, help = "DNS ZONE ID\t(see AWS Console Route53)")] #[clap(short = 'z', long, help = "DNS ZONE ID\t(see AWS Console Route53)")]
zone_id: String, dns_zone_id: String,
#[clap(short, long, help = "DOMAIN NAME\t(ex. 'docs.rskio.com.')")] #[clap(short, long, help = "DOMAIN NAME\t(ex. 'docs.rskio.com.')")]
domain_name: String, domain_name: String,
} }
const RECORD_TYPE: &'static str = "A";
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse(); let args = Args::parse();
Builder::new().filter(None, log::LevelFilter::Info).init();
// get aws r53 client info!(
let region_provider = RegionProviderChain::default_provider(); "starting with options: -z {} -d {}",
let config = aws_config::from_env().region(region_provider).load().await; &args.dns_zone_id, &args.domain_name,
let client = route53::Client::new(&config);
// get a list of resource_record_sets
let list_resource_record_sets = client
.list_resource_record_sets()
.hosted_zone_id(&args.zone_id)
.start_record_name(&args.domain_name)
.send()
.await?;
// match a single resource record_set
let mut resource_record_set: Option<ResourceRecordSet> = None;
for rrs in list_resource_record_sets.resource_record_sets {
if rrs.name.as_str() == &args.domain_name && rrs.r#type.as_str() == "A" {
resource_record_set = Some(rrs);
break;
}
}
// if record is none: exit
// else shadow resource_record_set with a safe unwrap
let resource_record_set = match resource_record_set.is_none() {
true => {
println!(
"No ResourceRecordSet found in Zone: {} for Record like: {} {}",
&args.zone_id, RECORD_TYPE, &args.domain_name,
); );
std::process::exit(1);
}
false => resource_record_set.unwrap(),
};
// if record contains empty resource_records, exit
// else shadow resource_records with a safe unwrap
let resource_records = match resource_record_set.resource_records.is_none() {
true => {
println!(
"No ResourceRecord found Zone: {} for Record like: {} {}",
&args.zone_id, RECORD_TYPE, &args.domain_name,
);
std::process::exit(1);
}
false => &resource_record_set.resource_records.unwrap(),
};
// get the first ip in the DNS record
let record_ip = resource_records[0]
.value
.parse::<Ipv4Addr>()
.expect("Failed to parse IP address");
let public_ip: Ipv4Addr = get_public_ip().await?;
// if no drift detected, exit
if record_ip == public_ip {
println!(
"The DNS record is currently up to date with the public IP: {}",
record_ip
);
return Ok(());
}
let msg: String = format!("The dynamic IP provided by the ISP has drifted.",);
println!("{} {} -> {}", msg, record_ip, public_ip);
// prepare aws r53 change request
let change = Change::builder()
.action(ChangeAction::Upsert)
.resource_record_set(
ResourceRecordSet::builder()
.name(resource_record_set.name.clone())
.r#type(resource_record_set.r#type.clone())
.ttl(resource_record_set.ttl.unwrap())
.resource_records(
ResourceRecord::builder()
.set_value(Some(public_ip.to_string()))
.build()
.unwrap(),
)
.build()
.unwrap(),
) // Build the ResourceRecordSet
.build()
.unwrap(); // Build the Change
// Change the resource record set
let response = client
.change_resource_record_sets()
.hosted_zone_id(&args.zone_id)
.change_batch(
ChangeBatch::builder()
.set_changes(Some(vec![change]))
.set_comment(Some(msg))
.build()
.unwrap(),
)
.send()
.await?;
println!("Requested DNS record update to PublicIP: {}", public_ip);
// Get the change ID from the response
let change_id = response.change_info.unwrap().id;
// Check the status of the change request every 60 seconds
loop { loop {
let change_response = client.get_change().id(&change_id).send().await?; let public_ip = get_public_ip().await?;
// check the status if !dns::is_addr_current(&args.domain_name, public_ip).await? {
if let Some(change_info) = change_response.change_info { route53::update_record(&args.dns_zone_id, &args.domain_name, public_ip).await?;
println!("Change ID: {}, Status: {:?}", change_id, change_info.status); };
// break loop if the change is insync sleep(Duration::from_secs(60)).await;
if change_info.status == route53::types::ChangeStatus::Insync {
println!("The change request has been completed.");
break;
} }
}
// sleep for 60 seconds before checking again...
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
}
Ok(())
} }
async fn get_public_ip() -> Result<Ipv4Addr, Box<dyn std::error::Error>> { async fn get_public_ip() -> Result<IpAddr, Box<dyn std::error::Error>> {
Ok(reqwest::get("http://ipv4.icanhazip.com") Ok(get("http://icanhazip.com")
.await? .await?
.text() .text()
.await? .await?
.trim() .trim()
.to_string() .to_string()
.parse::<Ipv4Addr>()?) .parse::<IpAddr>()?)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod unit {
use super::get_public_ip;
#[tokio::test] #[tokio::test]
async fn get_public_ip_works() { async fn test_get_public_ip() {
dbg!(get_public_ip().await.unwrap()); dbg!(super::get_public_ip().await.unwrap());
} }
} }

163
src/route53.rs Normal file
View File

@@ -0,0 +1,163 @@
use std::error::Error;
use std::fmt;
use std::net::IpAddr;
use aws_config::meta::region::RegionProviderChain;
use aws_sdk_route53 as r53;
use aws_sdk_route53::types::{
Change, ChangeAction, ChangeBatch, ResourceRecord, ResourceRecordSet,
};
use log::info;
use thiserror::Error;
use tokio::time::{sleep, Duration};
pub async fn update_record(
dns_zone_id: &str,
domain_name: &str,
public_ip: IpAddr,
) -> Result<(), Box<dyn Error>> {
let record_type: RecordType = match public_ip {
IpAddr::V4(_) => RecordType::A,
IpAddr::V6(_) => RecordType::AAAA,
};
let client: r53::Client = get_client().await?;
let resource_record_set: Option<ResourceRecordSet> =
get_single_record_set(&client, &dns_zone_id, &domain_name, &record_type).await?;
match resource_record_set.is_none() {
true => return Err(Box::new(Route53UpdateError::NoRecordAvailable)),
false => {
info!(
"requesting update to route53 record for {} {} -> {}",
record_type, domain_name, public_ip
);
return Ok(submit_single_change_request(
&client,
resource_record_set.unwrap(),
&public_ip,
&dns_zone_id,
)
.await?);
}
}
}
pub async fn get_client() -> Result<aws_sdk_route53::Client, Box<dyn Error>> {
// get aws r53 client
Ok(r53::Client::new(
&aws_config::from_env()
.region(RegionProviderChain::default_provider())
.load()
.await,
))
}
pub async fn get_single_record_set(
client: &r53::Client,
dns_zone_id: &str,
domain_name: &str,
record_type: &RecordType,
) -> Result<Option<ResourceRecordSet>, Box<dyn Error>> {
// get a list of resource_record_sets
let list_resource_record_sets = client
.list_resource_record_sets()
.hosted_zone_id(dns_zone_id.to_string())
.start_record_name(domain_name.to_string())
.send()
.await?;
// match a single resource record_set
let mut resource_record_set: Option<ResourceRecordSet> = None;
for rrs in list_resource_record_sets.resource_record_sets {
if rrs.name.as_str() == domain_name && rrs.r#type.as_str() == record_type.to_string() {
resource_record_set = Some(rrs);
break;
}
}
Ok(resource_record_set)
}
pub async fn submit_single_change_request(
client: &r53::Client,
resource_record_set: ResourceRecordSet,
public_ip: &IpAddr,
dns_zone_id: &str,
) -> Result<(), Box<dyn Error>> {
let change = Change::builder()
.action(ChangeAction::Upsert)
.resource_record_set(
ResourceRecordSet::builder()
.name(resource_record_set.name.clone())
.r#type(resource_record_set.r#type.clone())
.ttl(resource_record_set.ttl.clone().unwrap())
.resource_records(
ResourceRecord::builder()
.set_value(Some(public_ip.to_string()))
.build()
.unwrap(),
)
.build()
.unwrap(),
)
.build()
.unwrap();
let msg: String = format!("ISP provided dynamic IP has drifted.");
// submit batch change request of the resource record set
let response = client
.change_resource_record_sets()
.hosted_zone_id(dns_zone_id)
.change_batch(
ChangeBatch::builder()
.set_changes(Some(vec![change]))
.set_comment(Some(msg))
.build()
.unwrap(),
)
.send()
.await?;
let change_id = response.change_info.unwrap().id;
// check change request status every 60 seconds
loop {
let change_response = client.get_change().id(&change_id).send().await?;
if let Some(change_info) = change_response.change_info {
info!(
"change_id: {} has status: {:?}",
change_id, change_info.status
);
// break loop if the change is insync
if change_info.status == r53::types::ChangeStatus::Insync {
return Ok(());
}
}
sleep(Duration::from_secs(180)).await;
}
}
#[derive(Error, Debug)]
pub enum Route53UpdateError {
#[error("zone does not contain requested name record")]
NoRecordAvailable,
}
#[derive(Debug, PartialEq)]
pub enum RecordType {
A,
AAAA,
}
impl fmt::Display for RecordType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RecordType::A => write!(f, "A"),
RecordType::AAAA => write!(f, "AAAA"),
}
}
}