+ feat ipv6; + systemd integ
This commit is contained in:
323
Cargo.lock
generated
323
Cargo.lock
generated
@@ -17,6 +17,15 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
@@ -66,6 +75,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@@ -519,7 +539,7 @@ version = "4.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -572,6 +592,12 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
@@ -607,6 +633,41 @@ dependencies = [
|
||||
"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]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@@ -780,6 +841,12 @@ version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -807,6 +874,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "http"
|
||||
version = "0.2.12"
|
||||
@@ -875,6 +953,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.30"
|
||||
@@ -988,6 +1072,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
@@ -1008,6 +1102,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ipnet"
|
||||
version = "2.9.0"
|
||||
@@ -1041,6 +1147,12 @@ version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.14"
|
||||
@@ -1063,6 +1175,21 @@ version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
@@ -1275,6 +1402,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
@@ -1284,6 +1417,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
@@ -1300,8 +1439,43 @@ dependencies = [
|
||||
"aws-config",
|
||||
"aws-sdk-route53",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"log",
|
||||
"reqwest",
|
||||
"thiserror",
|
||||
"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]]
|
||||
@@ -1313,12 +1487,41 @@ dependencies = [
|
||||
"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]]
|
||||
name = "regex-lite"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.5"
|
||||
@@ -1360,7 +1563,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"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]]
|
||||
@@ -1705,6 +1918,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "time"
|
||||
version = "0.3.36"
|
||||
@@ -1881,6 +2114,52 @@ dependencies = [
|
||||
"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]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@@ -1927,7 +2206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"idna 0.5.0",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
@@ -2058,6 +2337,34 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
@@ -2197,6 +2504,16 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
|
||||
@@ -6,6 +6,11 @@ edition = "2021"
|
||||
[dependencies]
|
||||
aws-config = { version = "1.1.7", features = ["behavior-version-latest"] }
|
||||
aws-sdk-route53 = "1.37.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
clap = { version = "4.5.11", features = ["derive"] }
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
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
106
README.md
@@ -1,108 +1,88 @@
|
||||
# r53-ddns
|
||||
|
||||
Route53 Dynamic DNS
|
||||
|
||||
## 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"
|
||||
> // "ah, but you have seen the code working"
|
||||
>
|
||||
> -- Rskntroot 2024
|
||||
This is intended to be installed on a public-facing loadbalancer.
|
||||
|
||||
## Assumptions
|
||||
|
||||
1. Your ISP randomly changes your PublicIP and that pisses you off.
|
||||
1. You have no idea how DDNS is actually supposed to work.
|
||||
- dns is all smoke and mirrors, confirmed.
|
||||
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.
|
||||
1. You just want something that will curl `ipv4.icanhazip.com`, check 3rd-party dns, and update Route53.
|
||||
1. Your Name records only contain a single IP. (future update maybe).
|
||||
|
||||
Congratulations, this is the package for you.
|
||||
If so, this is for you.
|
||||
|
||||
## Setup
|
||||
|
||||
1. use output of below command to create the IAM policy
|
||||
``` zsh
|
||||
zone_id=<zone_id> envsubst < AllowRoute53RecordUpdate.policy
|
||||
1. setup `Route53AllowRecordUpdate.policy`
|
||||
```zsh
|
||||
DNS_ZONE_ID=YOURZONEIDHERE \
|
||||
envsubst < aws.policy > Route53AllowRecordUpdate.policy
|
||||
```
|
||||
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
|
||||
```
|
||||
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
|
||||
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:
|
||||
-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.')
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### Drift Detected
|
||||
|
||||
``` zsh
|
||||
$ r53-ddns -z ${aws_dns_zone_id} -d smp.rskio.com.
|
||||
```
|
||||
### Example
|
||||
|
||||
```
|
||||
The dynamic IP provided by the ISP has drifted. 10.0.11.201 -> 10.0.88.219
|
||||
Requested DNS record update to PublicIP: 10.0.88.219
|
||||
Change ID: /change/C04224022UE1ZQA26RE7O, Status: Pending
|
||||
Change ID: /change/C04224022UE1ZQA26RE7O, Status: Insync
|
||||
The change request has been completed.
|
||||
```
|
||||
$ systemctl status r53-ddns.service
|
||||
● r53-ddns.service - Route53 Dynamic DNS Service
|
||||
Loaded: loaded (/etc/systemd/system/r53-ddns.service; enabled; vendor preset: enabled)
|
||||
Active: active (running) since Mon 2024-07-29 09:03:40 UTC; 7min ago
|
||||
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
|
||||
|
||||
``` zsh
|
||||
$ r53-ddns -z ${aws_dns_zone_id} -d example.com.
|
||||
```
|
||||
|
||||
```
|
||||
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
|
||||
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.
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
> 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.
|
||||
To be able to handle errors in the future.
|
||||
|
||||
> wen IPv6?
|
||||
|
||||
Stfu, John. How about, wen PR?
|
||||
It should work with IPv6.
|
||||
|
||||
|
||||
24
aws.policy
Normal file
24
aws.policy
Normal 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": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
"route53:GetChange"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:route53:::hostedzone/${zone_id}",
|
||||
"arn:aws:route53:::hostedzone/",
|
||||
"arn:aws:route53:::change/*"
|
||||
]
|
||||
},
|
||||
16
r53-ddns.service
Normal file
16
r53-ddns.service
Normal 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
52
src/dns.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
168
src/main.rs
168
src/main.rs
@@ -1,12 +1,12 @@
|
||||
use aws_config::meta::region::RegionProviderChain;
|
||||
use aws_sdk_route53 as route53;
|
||||
use aws_sdk_route53::types::{
|
||||
Change, ChangeAction, ChangeBatch, ResourceRecord, ResourceRecordSet,
|
||||
};
|
||||
mod dns;
|
||||
mod route53;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use env_logger::Builder;
|
||||
use log::info;
|
||||
use reqwest::get;
|
||||
use std::net::IpAddr;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(
|
||||
@@ -14,166 +14,48 @@ use std::net::Ipv4Addr;
|
||||
about = "A CLI tool for correcting drift between your PublicIP and Route53 DNS A RECORD"
|
||||
)]
|
||||
struct Args {
|
||||
#[clap(short, long, help = "DNS ZONE ID\t(see AWS Console Route53)")]
|
||||
zone_id: String,
|
||||
#[clap(short = 'z', long, help = "DNS ZONE ID\t(see AWS Console Route53)")]
|
||||
dns_zone_id: String,
|
||||
|
||||
#[clap(short, long, help = "DOMAIN NAME\t(ex. 'docs.rskio.com.')")]
|
||||
domain_name: String,
|
||||
}
|
||||
|
||||
const RECORD_TYPE: &'static str = "A";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
Builder::new().filter(None, log::LevelFilter::Info).init();
|
||||
|
||||
// get aws r53 client
|
||||
let region_provider = RegionProviderChain::default_provider();
|
||||
let config = aws_config::from_env().region(region_provider).load().await;
|
||||
let client = route53::Client::new(&config);
|
||||
info!(
|
||||
"starting with options: -z {} -d {}",
|
||||
&args.dns_zone_id, &args.domain_name,
|
||||
);
|
||||
|
||||
// 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 {
|
||||
let change_response = client.get_change().id(&change_id).send().await?;
|
||||
let public_ip = get_public_ip().await?;
|
||||
|
||||
// check the status
|
||||
if let Some(change_info) = change_response.change_info {
|
||||
println!("Change ID: {}, Status: {:?}", change_id, change_info.status);
|
||||
if !dns::is_addr_current(&args.domain_name, public_ip).await? {
|
||||
route53::update_record(&args.dns_zone_id, &args.domain_name, public_ip).await?;
|
||||
};
|
||||
|
||||
// break loop if the change is insync
|
||||
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;
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_public_ip() -> Result<Ipv4Addr, Box<dyn std::error::Error>> {
|
||||
Ok(reqwest::get("http://ipv4.icanhazip.com")
|
||||
async fn get_public_ip() -> Result<IpAddr, Box<dyn std::error::Error>> {
|
||||
Ok(get("http://icanhazip.com")
|
||||
.await?
|
||||
.text()
|
||||
.await?
|
||||
.trim()
|
||||
.to_string()
|
||||
.parse::<Ipv4Addr>()?)
|
||||
.parse::<IpAddr>()?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::get_public_ip;
|
||||
|
||||
mod unit {
|
||||
#[tokio::test]
|
||||
async fn get_public_ip_works() {
|
||||
dbg!(get_public_ip().await.unwrap());
|
||||
async fn test_get_public_ip() {
|
||||
dbg!(super::get_public_ip().await.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
163
src/route53.rs
Normal file
163
src/route53.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user