about summary refs log tree commit diff
path: root/users/tazjin/yddns/src/main.rs
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2023-06-13T18·05+0300
committertazjin <tazjin@tvl.su>2023-06-14T10·06+0000
commit597d1a0aa28602e31e746408145101363296e731 (patch)
tree576b5ef80b99e76ab577c347745747ecb60d7f7c /users/tazjin/yddns/src/main.rs
parent8d632af36e76df1bfaa7cfb2f04e4ca75a37ee56 (diff)
feat(tazjin/yddns): hack together a quick dyndns update script r/6286
Change-Id: I4114a1b83e5f1edbb04d0ab920f107245ec998bd
Reviewed-on: https://cl.tvl.fyi/c/depot/+/8761
Reviewed-by: tazjin <tazjin@tvl.su>
Tested-by: BuildkiteCI
Diffstat (limited to 'users/tazjin/yddns/src/main.rs')
-rw-r--r--users/tazjin/yddns/src/main.rs143
1 files changed, 143 insertions, 0 deletions
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<T> = DnsZoneServiceClient<InterceptedService<Channel, AuthInterceptor<T>>>;
+
+/// 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<Ipv4Addr> {
+    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<T: TokenProvider>(
+    client: &mut DnsClient<T>,
+    zone_id: &str,
+    record_name: &str,
+) -> Result<Ipv4Addr> {
+    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<T: TokenProvider>(
+    client: &mut DnsClient<T>,
+    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<T: TokenProvider>(
+    client: &mut DnsClient<T>,
+    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(())
+}