cloudflare-ddns-service/src/main.rs
Jan Christian Grünhage 5f25f85a5e chore: fork and rename
I'm forking this, with the following changes:
 - daemon instead of CLI tool run via cron
 - mandatory config file
 - use cloudflare crate instead of home grown API client
 - IPv6 support
 - CNPLv6+ licensed instead of MIT licensed
2021-05-23 15:33:28 +02:00

164 lines
4.6 KiB
Rust

mod network;
use anyhow::{Context, Result};
use directories_next::ProjectDirs;
use network::{get_current_ipv4, get_current_ipv6, get_record, get_zone, update_record};
use serde::{Deserialize, Serialize};
use serde_yaml::{from_str, to_writer};
use std::{
fs::{create_dir_all, read_to_string, File},
net::{Ipv4Addr, Ipv6Addr},
path::PathBuf,
time::Duration,
};
use tokio::time::interval;
use cloudflare::{
endpoints::dns::DnsContent,
framework::{
async_api::Client as CfClient, auth::Credentials, Environment, HttpApiClientConfig,
},
};
use reqwest::Client as ReqwClient;
#[derive(Serialize, Deserialize)]
struct Config {
api_token: String,
zone: String,
domain: String,
#[serde(default = "yes")]
ipv4: bool,
#[serde(default = "no")]
ipv6: bool,
#[serde(default = "default_duration")]
interval: u64,
}
#[derive(Serialize, Deserialize, Default)]
struct Cache {
v4: Option<Ipv4Addr>,
v6: Option<Ipv6Addr>,
}
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let dirs = ProjectDirs::from("re", "jcg", "cloudflare-ddns-service")
.context("Couldn't find project directories! Is $HOME set?")?;
let config_string = read_to_string(dirs.config_dir().join("config.yaml"))
.context("couldn't read config file!")?;
let config: Config = from_str(&config_string)?;
let cache_path = dirs.cache_dir().join("cache.yaml");
let mut cache = match read_to_string(&cache_path) {
Ok(cache) => from_str(&cache)?,
Err(_) => {
create_dir_all(dirs.cache_dir())?;
Cache::default()
}
};
let mut interval = interval(Duration::new(config.interval, 0));
let mut reqw_client = ReqwClient::new();
let mut cf_client = CfClient::new(
Credentials::UserAuthToken {
token: config.api_token.clone(),
},
HttpApiClientConfig::default(),
Environment::Production,
)?;
let zone = get_zone(config.zone.clone(), &mut cf_client).await?;
loop {
update(
&config,
&mut cache,
&cache_path,
&zone,
&mut reqw_client,
&mut cf_client,
)
.await?;
interval.tick().await;
}
}
async fn update(
config: &Config,
cache: &mut Cache,
cache_path: &PathBuf,
zone: &str,
reqw_client: &mut ReqwClient,
cf_client: &mut CfClient,
) -> Result<()> {
if config.ipv4 {
let current = get_current_ipv4(reqw_client).await?;
log::debug!("fetched current IP: {}", current.to_string());
match cache.v4 {
Some(old) if old == current => {
log::debug!("ipv4 unchanged, continuing...");
}
_ => {
log::debug!("ipv4 changed, setting record");
let rid = get_record(zone, config.domain.clone(), network::A_RECORD, cf_client)
.await
.context("couldn't find record!")?;
log::debug!("got record ID {}", rid);
update_record(
zone,
&rid,
&config.domain,
DnsContent::A { content: current },
cf_client,
)
.await?;
cache.v4 = Some(current);
write_cache(cache, cache_path)?;
}
}
}
if config.ipv6 {
let current = get_current_ipv6(reqw_client).await?;
log::debug!("fetched current IP: {}", current.to_string());
match cache.v6 {
Some(old) if old == current => {
log::debug!("ipv6 unchanged, continuing...")
}
_ => {
log::debug!("ipv4 changed, setting record");
let rid = get_record(zone, config.domain.clone(), network::AAAA_RECORD, cf_client)
.await
.context("couldn't find record!")?;
log::debug!("got record ID {}", rid);
update_record(
zone,
&rid,
&config.domain,
DnsContent::AAAA { content: current },
cf_client,
)
.await?;
cache.v6 = Some(current);
write_cache(cache, cache_path)?;
}
}
}
Ok(())
}
fn write_cache(cache: &mut Cache, cache_path: &PathBuf) -> Result<()> {
to_writer(File::create(cache_path)?, cache)?;
Ok(())
}
fn yes() -> bool {
true
}
fn no() -> bool {
false
}
fn default_duration() -> u64 {
60
}