Compare commits
No commits in common. "main" and "v0.5.1" have entirely different histories.
5 changed files with 201 additions and 594 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -2,26 +2,6 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [0.5.3] - 2023-01-15
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Replace homegrown public IP lookup with library
|
|
||||||
- Update dependencies
|
|
||||||
- Bump version and update changelog
|
|
||||||
|
|
||||||
## [0.5.2] - 2023-01-14
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Switch to cloudflare fork that actually parses API responses
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Improve error handling
|
|
||||||
- Fix clippy lints
|
|
||||||
- Bump version and update changelog
|
|
||||||
|
|
||||||
## [0.5.1] - 2023-01-01
|
## [0.5.1] - 2023-01-01
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
649
Cargo.lock
generated
649
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cloudflare-ddns-service"
|
name = "cloudflare-ddns-service"
|
||||||
version = "0.5.3"
|
version = "0.5.1"
|
||||||
authors = ["Jan Christian Grünhage <jan.christian@gruenhage.xyz>"]
|
authors = ["Jan Christian Grünhage <jan.christian@gruenhage.xyz>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "A daemon to use Cloudflare as a DDNS provider"
|
description = "A daemon to use Cloudflare as a DDNS provider"
|
||||||
|
@ -13,15 +13,11 @@ documentation = "https://git.jcg.re/jcgruenhage/cloudflare-ddns-service"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
reqwest= { version = "0.11.13", features = ["blocking", "json"] }
|
||||||
serde = { version = "1.0.152", features = ["derive"] }
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
anyhow = "1.0.68"
|
anyhow = "1.0.68"
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
tokio = { version = "1.24.1", features = ["time", "macros", "rt-multi-thread"] }
|
tokio = { version = "1.23.0", features = ["time", "macros", "rt-multi-thread"] }
|
||||||
serde_yaml = "0.9.16"
|
serde_yaml = "0.9.16"
|
||||||
cloudflare = "0.10.1"
|
cloudflare = "0.10.1"
|
||||||
public-ip = "0.2.2"
|
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
cloudflare = { git = "https://github.com/jcgruenhage/cloudflare-rs.git", branch = "make-owner-fields-optional" }
|
|
||||||
public-ip = { git = "https://github.com/jcgruenhage/rust-public-ip.git", branch = "cloudflare-provider" }
|
|
||||||
|
|
74
src/main.rs
74
src/main.rs
|
@ -12,7 +12,7 @@
|
||||||
mod network;
|
mod network;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use network::{get_record, get_zone, update_record};
|
use network::{get_current_ipv4, get_current_ipv6, get_record, get_zone, update_record};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::{from_str, to_writer};
|
use serde_yaml::{from_str, to_writer};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -25,8 +25,11 @@ use tokio::time::interval;
|
||||||
|
|
||||||
use cloudflare::{
|
use cloudflare::{
|
||||||
endpoints::dns::DnsContent,
|
endpoints::dns::DnsContent,
|
||||||
framework::{async_api::Client, auth::Credentials, Environment, HttpApiClientConfig},
|
framework::{
|
||||||
|
async_api::Client as CfClient, auth::Credentials, Environment, HttpApiClientConfig,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
use reqwest::Client as ReqwClient;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct Config {
|
struct Config {
|
||||||
|
@ -53,33 +56,37 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let config_string = read_to_string("/etc/cloudflare-ddns-service/config.yaml")
|
let config_string = read_to_string("/etc/cloudflare-ddns-service/config.yaml")
|
||||||
.context("couldn't read config file!")?;
|
.context("couldn't read config file!")?;
|
||||||
let config: Config = from_str(&config_string).context("Failed to parse config file")?;
|
let config: Config = from_str(&config_string)?;
|
||||||
let cache_dir = PathBuf::from("/var/cache/cloudflare-ddns-service");
|
let cache_dir = PathBuf::from("/var/cache/cloudflare-ddns-service");
|
||||||
let cache_path = cache_dir.join("cache.yaml");
|
let cache_path = cache_dir.join("cache.yaml");
|
||||||
let mut cache = match read_to_string(&cache_path).map(|str| from_str(&str)) {
|
let mut cache = match read_to_string(&cache_path) {
|
||||||
Ok(Ok(cache)) => cache,
|
Ok(cache) => from_str(&cache)?,
|
||||||
_ => {
|
Err(_) => {
|
||||||
create_dir_all(cache_dir)?;
|
create_dir_all(cache_dir)?;
|
||||||
Cache::default()
|
Cache::default()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut interval = interval(Duration::new(config.interval, 0));
|
let mut interval = interval(Duration::new(config.interval, 0));
|
||||||
let mut client = Client::new(
|
let mut reqw_client = ReqwClient::new();
|
||||||
|
let mut cf_client = CfClient::new(
|
||||||
Credentials::UserAuthToken {
|
Credentials::UserAuthToken {
|
||||||
token: config.api_token.clone(),
|
token: config.api_token.clone(),
|
||||||
},
|
},
|
||||||
HttpApiClientConfig::default(),
|
HttpApiClientConfig::default(),
|
||||||
Environment::Production,
|
Environment::Production,
|
||||||
)
|
)?;
|
||||||
.context("Failed to initiate cloudflare API client")?;
|
let zone = get_zone(config.zone.clone(), &mut cf_client).await?;
|
||||||
let zone = get_zone(config.zone.clone(), &mut client)
|
|
||||||
.await
|
|
||||||
.context("Failed to get zone")?;
|
|
||||||
loop {
|
loop {
|
||||||
if let Err(error) = update(&config, &mut cache, &cache_path, &zone, &mut client).await {
|
update(
|
||||||
log::error!("Failed to update record: {}", error);
|
&config,
|
||||||
}
|
&mut cache,
|
||||||
|
&cache_path,
|
||||||
|
&zone,
|
||||||
|
&mut reqw_client,
|
||||||
|
&mut cf_client,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,12 +96,11 @@ async fn update(
|
||||||
cache: &mut Cache,
|
cache: &mut Cache,
|
||||||
cache_path: &PathBuf,
|
cache_path: &PathBuf,
|
||||||
zone: &str,
|
zone: &str,
|
||||||
client: &mut Client,
|
reqw_client: &mut ReqwClient,
|
||||||
|
cf_client: &mut CfClient,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if config.ipv4 {
|
if config.ipv4 {
|
||||||
let current = public_ip::addr_v4()
|
let current = get_current_ipv4(reqw_client).await?;
|
||||||
.await
|
|
||||||
.context("Failed to query current IPv4 address")?;
|
|
||||||
log::debug!("fetched current IP: {}", current.to_string());
|
log::debug!("fetched current IP: {}", current.to_string());
|
||||||
match cache.v4 {
|
match cache.v4 {
|
||||||
Some(old) if old == current => {
|
Some(old) if old == current => {
|
||||||
|
@ -102,7 +108,7 @@ async fn update(
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::info!("ipv4 changed, setting record");
|
log::info!("ipv4 changed, setting record");
|
||||||
let rid = get_record(zone, config.domain.clone(), network::A_RECORD, client)
|
let rid = get_record(zone, config.domain.clone(), network::A_RECORD, cf_client)
|
||||||
.await
|
.await
|
||||||
.context("couldn't find record!")?;
|
.context("couldn't find record!")?;
|
||||||
log::debug!("got record ID {}", rid);
|
log::debug!("got record ID {}", rid);
|
||||||
|
@ -111,20 +117,16 @@ async fn update(
|
||||||
&rid,
|
&rid,
|
||||||
&config.domain,
|
&config.domain,
|
||||||
DnsContent::A { content: current },
|
DnsContent::A { content: current },
|
||||||
client,
|
cf_client,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.context("Failed to set DNS record")?;
|
|
||||||
cache.v4 = Some(current);
|
cache.v4 = Some(current);
|
||||||
write_cache(cache, cache_path)
|
write_cache(cache, cache_path)?;
|
||||||
.context("Failed to write current IPv4 address to cache")?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config.ipv6 {
|
if config.ipv6 {
|
||||||
let current = public_ip::addr_v6()
|
let current = get_current_ipv6(reqw_client).await?;
|
||||||
.await
|
|
||||||
.context("Failed to query current IPv4 address")?;
|
|
||||||
log::debug!("fetched current IP: {}", current.to_string());
|
log::debug!("fetched current IP: {}", current.to_string());
|
||||||
match cache.v6 {
|
match cache.v6 {
|
||||||
Some(old) if old == current => {
|
Some(old) if old == current => {
|
||||||
|
@ -132,7 +134,7 @@ async fn update(
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::info!("ipv6 changed, setting record");
|
log::info!("ipv6 changed, setting record");
|
||||||
let rid = get_record(zone, config.domain.clone(), network::AAAA_RECORD, client)
|
let rid = get_record(zone, config.domain.clone(), network::AAAA_RECORD, cf_client)
|
||||||
.await
|
.await
|
||||||
.context("couldn't find record!")?;
|
.context("couldn't find record!")?;
|
||||||
log::debug!("got record ID {}", rid);
|
log::debug!("got record ID {}", rid);
|
||||||
|
@ -141,13 +143,11 @@ async fn update(
|
||||||
&rid,
|
&rid,
|
||||||
&config.domain,
|
&config.domain,
|
||||||
DnsContent::AAAA { content: current },
|
DnsContent::AAAA { content: current },
|
||||||
client,
|
cf_client,
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
.context("Failed to set DNS record")?;
|
|
||||||
cache.v6 = Some(current);
|
cache.v6 = Some(current);
|
||||||
write_cache(cache, cache_path)
|
write_cache(cache, cache_path)?;
|
||||||
.context("Failed to write current IPv4 address to cache")?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,11 +155,7 @@ async fn update(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_cache(cache: &mut Cache, cache_path: &PathBuf) -> Result<()> {
|
fn write_cache(cache: &mut Cache, cache_path: &PathBuf) -> Result<()> {
|
||||||
to_writer(
|
to_writer(File::create(cache_path)?, cache)?;
|
||||||
File::create(cache_path).context("Failed to open cache file for writing")?,
|
|
||||||
cache,
|
|
||||||
)
|
|
||||||
.context("Failed to serialize cache into file")?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,9 @@ use cloudflare::{
|
||||||
},
|
},
|
||||||
zone::{ListZones, ListZonesParams},
|
zone::{ListZones, ListZonesParams},
|
||||||
},
|
},
|
||||||
framework::async_api::Client,
|
framework::async_api::Client as CfClient,
|
||||||
};
|
};
|
||||||
|
use reqwest::Client as ReqwClient;
|
||||||
|
|
||||||
pub const A_RECORD: DnsContent = DnsContent::A {
|
pub const A_RECORD: DnsContent = DnsContent::A {
|
||||||
content: Ipv4Addr::UNSPECIFIED,
|
content: Ipv4Addr::UNSPECIFIED,
|
||||||
|
@ -30,8 +31,30 @@ pub const AAAA_RECORD: DnsContent = DnsContent::AAAA {
|
||||||
content: Ipv6Addr::UNSPECIFIED,
|
content: Ipv6Addr::UNSPECIFIED,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_zone(domain: String, client: &mut Client) -> Result<String> {
|
pub async fn get_current_ipv4(client: &mut ReqwClient) -> Result<Ipv4Addr> {
|
||||||
Ok(client
|
Ok(client
|
||||||
|
.get("https://ipv4.icanhazip.com")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?
|
||||||
|
.trim()
|
||||||
|
.parse()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_current_ipv6(client: &mut ReqwClient) -> Result<Ipv6Addr> {
|
||||||
|
Ok(client
|
||||||
|
.get("https://ipv6.icanhazip.com")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?
|
||||||
|
.trim()
|
||||||
|
.parse()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_zone(domain: String, cf_client: &mut CfClient) -> Result<String> {
|
||||||
|
Ok(cf_client
|
||||||
.request_handle(&ListZones {
|
.request_handle(&ListZones {
|
||||||
params: ListZonesParams {
|
params: ListZonesParams {
|
||||||
name: Some(domain),
|
name: Some(domain),
|
||||||
|
@ -43,8 +66,7 @@ pub async fn get_zone(domain: String, client: &mut Client) -> Result<String> {
|
||||||
search_match: None,
|
search_match: None,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.await
|
.await?
|
||||||
.context("Failed to query zone from cf_client")?
|
|
||||||
.result[0]
|
.result[0]
|
||||||
.id
|
.id
|
||||||
.clone())
|
.clone())
|
||||||
|
@ -54,9 +76,9 @@ pub async fn get_record(
|
||||||
zone_identifier: &str,
|
zone_identifier: &str,
|
||||||
domain: String,
|
domain: String,
|
||||||
r#type: DnsContent,
|
r#type: DnsContent,
|
||||||
client: &mut Client,
|
cf_client: &mut CfClient,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
Ok(client
|
Ok(cf_client
|
||||||
.request_handle(&ListDnsRecords {
|
.request_handle(&ListDnsRecords {
|
||||||
zone_identifier,
|
zone_identifier,
|
||||||
params: ListDnsRecordsParams {
|
params: ListDnsRecordsParams {
|
||||||
|
@ -73,7 +95,9 @@ pub async fn get_record(
|
||||||
.context("Couldn't fetch record")?
|
.context("Couldn't fetch record")?
|
||||||
.result
|
.result
|
||||||
.iter()
|
.iter()
|
||||||
.find(|record| std::mem::discriminant(&record.content) == std::mem::discriminant(&r#type))
|
.find(|record| {
|
||||||
|
std::mem::discriminant(&record.content) == std::mem::discriminant(&r#type)
|
||||||
|
})
|
||||||
.context("No matching record found")?
|
.context("No matching record found")?
|
||||||
.id
|
.id
|
||||||
.clone())
|
.clone())
|
||||||
|
@ -84,9 +108,9 @@ pub async fn update_record(
|
||||||
identifier: &str,
|
identifier: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
content: DnsContent,
|
content: DnsContent,
|
||||||
client: &mut Client,
|
cf_client: &mut CfClient,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
client
|
cf_client
|
||||||
.request_handle(&UpdateDnsRecord {
|
.request_handle(&UpdateDnsRecord {
|
||||||
zone_identifier,
|
zone_identifier,
|
||||||
identifier,
|
identifier,
|
||||||
|
|
Loading…
Reference in a new issue