Compare commits

...

11 commits
v0.5.0 ... main

Author SHA1 Message Date
Jan Christian Grünhage be17cc5234
chore: use patch section for pulling in upstream forks 2023-01-16 11:25:33 +01:00
Jan Christian Grünhage 84db7292a6
chore: bump version and update changelog 2023-01-15 13:37:32 +01:00
Jan Christian Grünhage 2c1f19d647
chore: update dependencies 2023-01-15 13:37:32 +01:00
Jan Christian Grünhage 8b906b777a
chore: replace homegrown public IP lookup with library 2023-01-15 13:37:31 +01:00
Jan Christian Grünhage 9a7fefc16c
chore: bump version and update changelog 2023-01-14 21:55:23 +01:00
Jan Christian Grünhage 96aa83dbfc
fix: switch to cloudflare fork that actually parses API responses 2023-01-14 21:52:51 +01:00
Jan Christian Grünhage 8cd963ad59
chore: fix clippy lints 2023-01-14 21:48:07 +01:00
Jan Christian Grünhage 3e1152edfe
chore: improve error handling 2023-01-14 21:46:14 +01:00
Jan Christian Grünhage d715d3d408
chore: bump version and update changelog 2023-01-02 00:49:01 +01:00
Jan Christian Grünhage 276a22e7f6
chore: improve logging granularity 2023-01-02 00:48:07 +01:00
Jan Christian Grünhage 4eb137fed0
fix: make container image actually usable 2023-01-02 00:47:40 +01:00
6 changed files with 615 additions and 207 deletions

View file

@ -2,6 +2,37 @@
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
### Bug Fixes
- Make container image actually usable
### Miscellaneous Tasks
- Improve logging granularity
- Bump version and update changelog
## [0.5.0] - 2023-01-01 ## [0.5.0] - 2023-01-01
### Bug Fixes ### Bug Fixes

651
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "cloudflare-ddns-service" name = "cloudflare-ddns-service"
version = "0.5.0" version = "0.5.3"
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,11 +13,15 @@ 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.23.0", features = ["time", "macros", "rt-multi-thread"] } tokio = { version = "1.24.1", 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" }

View file

@ -1,6 +1,6 @@
FROM docker.io/rust:alpine3.17 as builder FROM docker.io/rust:bullseye as builder
RUN apk add musl-dev openssl-dev pkgconf RUN apt update && apt install libssl-dev pkg-config
RUN cargo install cargo-auditable RUN cargo install cargo-auditable
COPY . /app COPY . /app
@ -8,6 +8,10 @@ WORKDIR /app
RUN cargo auditable build --release RUN cargo auditable build --release
FROM docker.io/alpine:3.17 FROM docker.io/debian:bullseye-slim
RUN apt update && apt install openssl ca-certificates
COPY --from=builder /app/target/release/cloudflare-ddns-service /usr/local/bin COPY --from=builder /app/target/release/cloudflare-ddns-service /usr/local/bin
CMD /usr/local/bin/cloudflare-ddns-service

View file

@ -12,7 +12,7 @@
mod network; mod network;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use network::{get_current_ipv4, get_current_ipv6, get_record, get_zone, update_record}; use network::{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,11 +25,8 @@ use tokio::time::interval;
use cloudflare::{ use cloudflare::{
endpoints::dns::DnsContent, endpoints::dns::DnsContent,
framework::{ framework::{async_api::Client, auth::Credentials, Environment, HttpApiClientConfig},
async_api::Client as CfClient, auth::Credentials, Environment, HttpApiClientConfig,
},
}; };
use reqwest::Client as ReqwClient;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct Config { struct Config {
@ -56,37 +53,33 @@ 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)?; let config: Config = from_str(&config_string).context("Failed to parse config file")?;
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) { let mut cache = match read_to_string(&cache_path).map(|str| from_str(&str)) {
Ok(cache) => from_str(&cache)?, Ok(Ok(cache)) => 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 reqw_client = ReqwClient::new(); let mut client = Client::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,
)?; )
let zone = get_zone(config.zone.clone(), &mut cf_client).await?; .context("Failed to initiate cloudflare API client")?;
let zone = get_zone(config.zone.clone(), &mut client)
.await
.context("Failed to get zone")?;
loop { loop {
update( if let Err(error) = update(&config, &mut cache, &cache_path, &zone, &mut client).await {
&config, log::error!("Failed to update record: {}", error);
&mut cache, }
&cache_path,
&zone,
&mut reqw_client,
&mut cf_client,
)
.await?;
interval.tick().await; interval.tick().await;
} }
} }
@ -96,19 +89,20 @@ async fn update(
cache: &mut Cache, cache: &mut Cache,
cache_path: &PathBuf, cache_path: &PathBuf,
zone: &str, zone: &str,
reqw_client: &mut ReqwClient, client: &mut Client,
cf_client: &mut CfClient,
) -> Result<()> { ) -> Result<()> {
if config.ipv4 { if config.ipv4 {
let current = get_current_ipv4(reqw_client).await?; let current = public_ip::addr_v4()
.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 => {
log::debug!("ipv4 unchanged, continuing..."); log::debug!("ipv4 unchanged, continuing...");
} }
_ => { _ => {
log::debug!("ipv4 changed, setting record"); log::info!("ipv4 changed, setting record");
let rid = get_record(zone, config.domain.clone(), network::A_RECORD, cf_client) let rid = get_record(zone, config.domain.clone(), network::A_RECORD, 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);
@ -117,24 +111,28 @@ async fn update(
&rid, &rid,
&config.domain, &config.domain,
DnsContent::A { content: current }, DnsContent::A { content: current },
cf_client, 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 = get_current_ipv6(reqw_client).await?; let current = public_ip::addr_v6()
.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 => {
log::debug!("ipv6 unchanged, continuing...") log::debug!("ipv6 unchanged, continuing...")
} }
_ => { _ => {
log::debug!("ipv4 changed, setting record"); log::info!("ipv6 changed, setting record");
let rid = get_record(zone, config.domain.clone(), network::AAAA_RECORD, cf_client) let rid = get_record(zone, config.domain.clone(), network::AAAA_RECORD, 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);
@ -143,11 +141,13 @@ async fn update(
&rid, &rid,
&config.domain, &config.domain,
DnsContent::AAAA { content: current }, DnsContent::AAAA { content: current },
cf_client, 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,7 +155,11 @@ 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(File::create(cache_path)?, cache)?; to_writer(
File::create(cache_path).context("Failed to open cache file for writing")?,
cache,
)
.context("Failed to serialize cache into file")?;
Ok(()) Ok(())
} }

View file

@ -20,9 +20,8 @@ use cloudflare::{
}, },
zone::{ListZones, ListZonesParams}, zone::{ListZones, ListZonesParams},
}, },
framework::async_api::Client as CfClient, framework::async_api::Client,
}; };
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,
@ -31,30 +30,8 @@ pub const AAAA_RECORD: DnsContent = DnsContent::AAAA {
content: Ipv6Addr::UNSPECIFIED, content: Ipv6Addr::UNSPECIFIED,
}; };
pub async fn get_current_ipv4(client: &mut ReqwClient) -> Result<Ipv4Addr> { pub async fn get_zone(domain: String, client: &mut Client) -> Result<String> {
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),
@ -66,7 +43,8 @@ pub async fn get_zone(domain: String, cf_client: &mut CfClient) -> 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())
@ -76,9 +54,9 @@ pub async fn get_record(
zone_identifier: &str, zone_identifier: &str,
domain: String, domain: String,
r#type: DnsContent, r#type: DnsContent,
cf_client: &mut CfClient, client: &mut Client,
) -> Result<String> { ) -> Result<String> {
Ok(cf_client Ok(client
.request_handle(&ListDnsRecords { .request_handle(&ListDnsRecords {
zone_identifier, zone_identifier,
params: ListDnsRecordsParams { params: ListDnsRecordsParams {
@ -95,9 +73,7 @@ pub async fn get_record(
.context("Couldn't fetch record")? .context("Couldn't fetch record")?
.result .result
.iter() .iter()
.find(|record| { .find(|record| std::mem::discriminant(&record.content) == std::mem::discriminant(&r#type))
std::mem::discriminant(&record.content) == std::mem::discriminant(&r#type)
})
.context("No matching record found")? .context("No matching record found")?
.id .id
.clone()) .clone())
@ -108,9 +84,9 @@ pub async fn update_record(
identifier: &str, identifier: &str,
name: &str, name: &str,
content: DnsContent, content: DnsContent,
cf_client: &mut CfClient, client: &mut Client,
) -> Result<()> { ) -> Result<()> {
cf_client client
.request_handle(&UpdateDnsRecord { .request_handle(&UpdateDnsRecord {
zone_identifier, zone_identifier,
identifier, identifier,