5f25f85a5e
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
164 lines
4.6 KiB
Rust
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
|
|
}
|