init commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
24
AllowRoute53RecordUpdate.policy
Normal file
24
AllowRoute53RecordUpdate.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/${zone_id}",
|
||||
"arn:aws:route53:::change/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Sid": "VisualEditor1",
|
||||
"Effect": "Allow",
|
||||
"Action": "route53:TestDNSAnswer",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
2220
Cargo.lock
generated
Normal file
2220
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "r53-ddns"
|
||||
version = "0.1.0"
|
||||
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"] }
|
||||
reqwest = { version = "0.12.5", features = ["blocking", "json"] }
|
||||
88
README.md
Normal file
88
README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# r53-ddns
|
||||
|
||||
## Brief
|
||||
|
||||
this was intended to solve the problem for when your local ISP force renews your PublicIP and no one can reach your might-as-well-be-a-toaster-minecraft server.
|
||||
|
||||
what this ended up as was a mockery of dynamic dns, progam/scripting, error handling, all in memory-safe rust. whatever that means, right?
|
||||
|
||||
> "this is the worst of example working code ive ever seen"
|
||||
> // "ah but you have seen the code working" -- cpt. rskntroot 2024
|
||||
|
||||
## 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 and 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.
|
||||
|
||||
## Setup
|
||||
|
||||
1. use out of below command to create AWS IAM Policy.
|
||||
``` zsh
|
||||
zone_id=<zone_id> envsubst < AllowRoute53RecordUpdate.policy
|
||||
```
|
||||
1. create IAM user, generate access keys for automated service
|
||||
1. login on the machine where you built this binary
|
||||
```
|
||||
aws sso login --profile
|
||||
```
|
||||
1. setup a cron job to poll at your leisure
|
||||
|
||||
## 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>
|
||||
|
||||
Options:
|
||||
-z, --zone-id <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.
|
||||
```
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
## Q&A
|
||||
|
||||
> Why are you doing AWS calls instead of us nslookup and compare that?
|
||||
|
||||
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 0 to deployed tokio async rust binary in less than 8 hours. And thats exactly what I did.
|
||||
|
||||
> wen IPv6?
|
||||
|
||||
Stfu, John. How about, wen PR?
|
||||
|
||||
183
src/main.rs
Normal file
183
src/main.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use aws_config::meta::region::RegionProviderChain;
|
||||
use aws_sdk_route53 as route53;
|
||||
use aws_sdk_route53::types::{
|
||||
Change, ChangeAction, ChangeBatch, ResourceRecord, ResourceRecordSet,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(
|
||||
name = "r53-ddns",
|
||||
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, 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();
|
||||
|
||||
// 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);
|
||||
|
||||
// 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?;
|
||||
|
||||
// check the status
|
||||
if let Some(change_info) = change_response.change_info {
|
||||
println!("Change ID: {}, Status: {:?}", change_id, change_info.status);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_public_ip() -> Result<Ipv4Addr, Box<dyn std::error::Error>> {
|
||||
Ok(reqwest::get("http://ipv4.icanhazip.com")
|
||||
.await?
|
||||
.text()
|
||||
.await?
|
||||
.trim()
|
||||
.to_string()
|
||||
.parse::<Ipv4Addr>()?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::get_public_ip;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_public_ip_works() {
|
||||
dbg!(get_public_ip().await.unwrap());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user