From 597d1a0aa28602e31e746408145101363296e731 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Tue, 13 Jun 2023 21:05:27 +0300 Subject: feat(tazjin/yddns): hack together a quick dyndns update script Change-Id: I4114a1b83e5f1edbb04d0ab920f107245ec998bd Reviewed-on: https://cl.tvl.fyi/c/depot/+/8761 Reviewed-by: tazjin Tested-by: BuildkiteCI --- users/tazjin/yddns/src/main.rs | 143 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 users/tazjin/yddns/src/main.rs (limited to 'users/tazjin/yddns/src/main.rs') diff --git a/users/tazjin/yddns/src/main.rs b/users/tazjin/yddns/src/main.rs new file mode 100644 index 000000000000..5799b59dafa0 --- /dev/null +++ b/users/tazjin/yddns/src/main.rs @@ -0,0 +1,143 @@ +use anyhow::{anyhow, bail, Context, Result}; +use crimp::Request; +use std::env; +use std::net::Ipv4Addr; +use tokio::runtime; +use tonic::codegen::InterceptedService; +use tonic::transport::channel::Channel; +use tonic::transport::Endpoint; +use yandex_cloud::yandex::cloud::dns::v1 as dns; +use yandex_cloud::yandex::cloud::dns::v1::dns_zone_service_client::DnsZoneServiceClient; +use yandex_cloud::{AuthInterceptor, TokenProvider}; + +type DnsClient = DnsZoneServiceClient>>; + +/// Fetch the current IP from the given URL. It should be the URL of a +/// site that responds only with the IP in plain text, and nothing else. +fn get_current_ip(source: &str) -> Result { + let response = Request::get(source) + .send() + .context("failed to fetch current IP")? + .error_for_status(|resp| anyhow!("error response ({})", resp.status)) + .context("received error response for IP")? + .as_string()? + .body; + + Ok(response.trim().parse().with_context(|| { + format!( + "failed to parse IP address from response body: {}", + response + ) + })?) +} + +/// Fetch the current address of the target record. +async fn fetch_current_record_addr( + client: &mut DnsClient, + zone_id: &str, + record_name: &str, +) -> Result { + let req = dns::GetDnsZoneRecordSetRequest { + dns_zone_id: zone_id.into(), + name: record_name.into(), + r#type: "A".into(), + }; + + let response = client + .get_record_set(req) + .await + .context("failed to fetch current record set")? + .into_inner(); + + if response.data.len() != 1 { + bail!( + "expected exactly one record for 'A {}', but found {}", + record_name, + response.data.len() + ); + } + + Ok(response.data[0] + .parse() + .context("failed to parse returned record")?) +} + +/// Update the record with the new address, if required. +async fn update_record( + client: &mut DnsClient, + zone_id: &str, + record_name: &str, + new_address: Ipv4Addr, +) -> Result<()> { + let request = dns::UpsertRecordSetsRequest { + dns_zone_id: zone_id.into(), + replacements: vec![dns::RecordSet { + name: record_name.into(), + r#type: "A".into(), + ttl: 3600, // 1 hour + data: vec![new_address.to_string()], + }], + ..Default::default() + }; + + client + .upsert_record_sets(request) + .await + .context("failed to update record")?; + + Ok(()) +} + +/// Compare the record with the expected value, and issue an update if +/// necessary. +async fn compare_update_record( + client: &mut DnsClient, + zone_id: &str, + record_name: &str, + new_ip: Ipv4Addr, +) -> Result<()> { + let old_ip = fetch_current_record_addr(client, zone_id, record_name).await?; + + if old_ip == new_ip { + println!("IP address unchanged ({})", old_ip); + return Ok(()); + } + + println!( + "IP address changed: current record points to {}, but address is {}", + old_ip, new_ip + ); + + update_record(client, zone_id, record_name, new_ip).await?; + println!("successfully updated '{}' to 'A {}'", record_name, new_ip); + + Ok(()) +} + +fn main() -> Result<()> { + let token = format!("Bearer {}", env::var("YANDEX_CLOUD_TOKEN")?); + let target_zone_id = + env::var("TARGET_ZONE").unwrap_or_else(|_| "dnsd0tif5mokfu0mg8i5".to_string()); + let target_record = env::var("TARGET_RECORD").unwrap_or_else(|_| "khtrsk".to_string()); + + let current_ip = get_current_ip("http://ifconfig.me")?; + println!("current IP address is '{}'", current_ip); + + let rt = runtime::Builder::new_current_thread() + .enable_time() + .enable_io() + .build()?; + + rt.block_on(async move { + let channel = Endpoint::from_static("https://dns.api.cloud.yandex.net") + .connect() + .await?; + + let mut client = + DnsZoneServiceClient::with_interceptor(channel, AuthInterceptor::new(token)); + + compare_update_record(&mut client, &target_zone_id, &target_record, current_ip).await + })?; + + Ok(()) +} -- cgit 1.4.1