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 <tazjin@tvl.su> Tested-by: BuildkiteCI
This commit is contained in:
parent
8d632af36e
commit
597d1a0aa2
5 changed files with 1640 additions and 0 deletions
1
users/tazjin/yddns/.gitignore
vendored
Normal file
1
users/tazjin/yddns/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target/
|
1474
users/tazjin/yddns/Cargo.lock
generated
Normal file
1474
users/tazjin/yddns/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
users/tazjin/yddns/Cargo.toml
Normal file
13
users/tazjin/yddns/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "yddns"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.71"
|
||||||
|
crimp = "4087.0.0"
|
||||||
|
tokio = "1.28.2"
|
||||||
|
tonic = "0.9.2"
|
||||||
|
yandex-cloud = "2023.5.23"
|
9
users/tazjin/yddns/default.nix
Normal file
9
users/tazjin/yddns/default.nix
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{ depot, pkgs, ... }:
|
||||||
|
|
||||||
|
depot.third_party.naersk.buildPackage {
|
||||||
|
src = ./.;
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
pkg-config
|
||||||
|
openssl
|
||||||
|
];
|
||||||
|
}
|
143
users/tazjin/yddns/src/main.rs
Normal file
143
users/tazjin/yddns/src/main.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
Loading…
Reference in a new issue