1/2 part
This commit is contained in:
parent
1262510ab9
commit
09afecf396
@ -12,7 +12,7 @@ log = "0.4.22"
|
|||||||
netlink-packet-core = "0.7.0"
|
netlink-packet-core = "0.7.0"
|
||||||
netlink-packet-route = "0.19.0"
|
netlink-packet-route = "0.19.0"
|
||||||
netlink-sys = { version = "0.8.6", features = ["tokio"] }
|
netlink-sys = { version = "0.8.6", features = ["tokio"] }
|
||||||
reqwest = "0.12.5"
|
reqwest = { version = "0.12.5", features = ["json"] }
|
||||||
rtnetlink = "0.14.1" # Updated to a version compatible with netlink-packet-route v0.20.1
|
rtnetlink = "0.14.1" # Updated to a version compatible with netlink-packet-route v0.20.1
|
||||||
serde = { version = "1.0.204", features = ["rc", "derive"] }
|
serde = { version = "1.0.204", features = ["rc", "derive"] }
|
||||||
serde_json = "1.0.120"
|
serde_json = "1.0.120"
|
||||||
|
@ -0,0 +1,151 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use log::{error, info};
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::{self, Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
net::{IpAddr, Ipv4Addr},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct CloudflareClient {
|
||||||
|
client: Client,
|
||||||
|
domains: Vec<Arc<str>>,
|
||||||
|
current_ip: Ipv4Addr,
|
||||||
|
api_key: Box<str>,
|
||||||
|
zone_id: Box<str>,
|
||||||
|
}
|
||||||
|
// Some external site to check this
|
||||||
|
async fn get_current_public_ipv4(client: &Client) -> Result<Ipv4Addr> {
|
||||||
|
let response = client
|
||||||
|
.get("https://api.ipify.org?format=json")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<HashMap<String, String>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response
|
||||||
|
.get("ip")
|
||||||
|
.context("Field 'ip' wasn't found")?
|
||||||
|
.parse()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
struct DnsRecord {
|
||||||
|
id: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
record_type: Box<str>,
|
||||||
|
name: Box<str>,
|
||||||
|
content: Box<str>,
|
||||||
|
ttl: u32,
|
||||||
|
proxied: bool,
|
||||||
|
locked: bool,
|
||||||
|
zone_id: Box<str>,
|
||||||
|
zone_name: Box<str>,
|
||||||
|
modified_on: Box<str>,
|
||||||
|
created_on: Box<str>,
|
||||||
|
meta: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloudflareClient {
|
||||||
|
pub async fn new(api_key: Box<str>, zone_id: Box<str>, domains: Vec<Arc<str>>) -> Result<Self> {
|
||||||
|
let force_ipv4 = IpAddr::from([0, 0, 0, 0]);
|
||||||
|
let client = reqwest::ClientBuilder::new()
|
||||||
|
.local_address(force_ipv4)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let current_ip = get_current_public_ipv4(&client).await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
domains,
|
||||||
|
current_ip,
|
||||||
|
api_key,
|
||||||
|
zone_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check(&mut self) -> Result<()> {
|
||||||
|
let new_ip = get_current_public_ipv4(&self.client).await?;
|
||||||
|
if new_ip == self.current_ip {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.update_dns_records(new_ip).await;
|
||||||
|
self.current_ip = new_ip;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_dns_records(&self, new_ip: Ipv4Addr) {
|
||||||
|
for domain in &self.domains {
|
||||||
|
let records = match self.get_dns_records(domain).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Could not getch dns records for domain '{}': {:?}",
|
||||||
|
&domain, &e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_ip_s = new_ip.to_string().into_boxed_str();
|
||||||
|
for mut record in records.into_iter() {
|
||||||
|
if record.content == new_ip_s {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
"On {}, updating {}: '{}' -> '{}'",
|
||||||
|
&domain, record.name, record.content, &new_ip_s
|
||||||
|
);
|
||||||
|
if let Err(e) = self.update_dns_record(&mut record, new_ip_s.clone()).await {
|
||||||
|
error!(
|
||||||
|
"On {}, failed to update {}: '{}' -> '{}': {:?}",
|
||||||
|
&domain, record.name, record.content, &new_ip_s, &e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_dns_records(&self, domain: &str) -> Result<Vec<DnsRecord>> {
|
||||||
|
let url = format!(
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A&name={}",
|
||||||
|
self.zone_id, domain
|
||||||
|
);
|
||||||
|
let mut response = self
|
||||||
|
.client
|
||||||
|
.get(&url)
|
||||||
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<HashMap<String, Vec<DnsRecord>>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response
|
||||||
|
.remove("result")
|
||||||
|
.context("Key result not in return")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_dns_record(&self, record: &mut DnsRecord, ip_content: Box<str>) -> Result<()> {
|
||||||
|
let url = format!(
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}",
|
||||||
|
self.zone_id, record.id
|
||||||
|
);
|
||||||
|
|
||||||
|
record.content = ip_content;
|
||||||
|
self.client
|
||||||
|
.put(&url)
|
||||||
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&record)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
|
use anyhow;
|
||||||
use dirs;
|
use dirs;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{env, fs, path::PathBuf};
|
use std::{env, path::PathBuf};
|
||||||
|
use tokio::{fs, io::AsyncReadExt};
|
||||||
|
|
||||||
use crate::PROGRAM_NAME;
|
use crate::PROGRAM_NAME;
|
||||||
|
|
||||||
@ -11,11 +13,11 @@ pub struct Config {
|
|||||||
pub cloudflare_api_key: Box<str>,
|
pub cloudflare_api_key: Box<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_config_path() -> Option<PathBuf> {
|
pub async fn get_config_path() -> Option<PathBuf> {
|
||||||
match env::current_dir() {
|
match env::current_dir() {
|
||||||
Ok(current_dir) => {
|
Ok(current_dir) => {
|
||||||
let cwd_config = current_dir.join(format!("{}.toml", PROGRAM_NAME));
|
let cwd_config = current_dir.join(format!("{}.toml", PROGRAM_NAME));
|
||||||
if let Ok(meta) = fs::metadata(&cwd_config) {
|
if let Ok(meta) = fs::metadata(&cwd_config).await {
|
||||||
if meta.is_file() {
|
if meta.is_file() {
|
||||||
return Some(cwd_config);
|
return Some(cwd_config);
|
||||||
}
|
}
|
||||||
@ -29,7 +31,7 @@ pub fn get_config_path() -> Option<PathBuf> {
|
|||||||
match dirs::config_dir() {
|
match dirs::config_dir() {
|
||||||
Some(config_dir) => {
|
Some(config_dir) => {
|
||||||
let config_file = config_dir.join(PROGRAM_NAME).join("config.toml");
|
let config_file = config_dir.join(PROGRAM_NAME).join("config.toml");
|
||||||
if let Ok(meta) = fs::metadata(&config_file) {
|
if let Ok(meta) = fs::metadata(&config_file).await {
|
||||||
if meta.is_file() {
|
if meta.is_file() {
|
||||||
return Some(config_file);
|
return Some(config_file);
|
||||||
}
|
}
|
||||||
@ -41,8 +43,9 @@ pub fn get_config_path() -> Option<PathBuf> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_config(path: PathBuf) -> Option<Config>{
|
pub async fn read_config(path: &PathBuf) -> anyhow::Result<Config> {
|
||||||
|
let mut file = fs::File::open(&path).await?;
|
||||||
|
let mut buf = String::new();
|
||||||
None
|
file.read_to_string(&mut buf).await?;
|
||||||
|
Ok(toml::from_str(&buf)?)
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
mod cloudflare;
|
mod cloudflare;
|
||||||
mod config;
|
mod config;
|
||||||
pub use config::{Config, get_config_path};
|
pub use config::{Config, get_config_path, read_config};
|
||||||
|
|
||||||
|
|
||||||
pub const PROGRAM_NAME: &'static str = "dynip-cloudflare";
|
pub const PROGRAM_NAME: &'static str = "dynip-cloudflare";
|
||||||
|
22
src/main.rs
22
src/main.rs
@ -1,4 +1,3 @@
|
|||||||
use anyhow::{anyhow, Result};
|
|
||||||
use env_logger;
|
use env_logger;
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use log::{debug, error, info, log_enabled, Level};
|
use log::{debug, error, info, log_enabled, Level};
|
||||||
@ -7,7 +6,7 @@ use netlink_packet_route::RouteNetlinkMessage as RtnlMessage;
|
|||||||
use netlink_sys::{AsyncSocket, SocketAddr};
|
use netlink_sys::{AsyncSocket, SocketAddr};
|
||||||
use rtnetlink::new_connection;
|
use rtnetlink::new_connection;
|
||||||
|
|
||||||
use dynip_cloudflare::{get_config_path, Config};
|
use dynip_cloudflare::{get_config_path, read_config, Config};
|
||||||
|
|
||||||
const RTNLGRP_LINK: u32 = 1;
|
const RTNLGRP_LINK: u32 = 1;
|
||||||
const RTNLGRP_IPV4_IFADDR: u32 = 5;
|
const RTNLGRP_IPV4_IFADDR: u32 = 5;
|
||||||
@ -24,24 +23,31 @@ const fn nl_mgrp(group: u32) -> u32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let config_path = match get_config_path() {
|
let config_path = match get_config_path().await {
|
||||||
Some(cp) => cp,
|
Some(cp) => cp,
|
||||||
None => {
|
None => {
|
||||||
error!("Failed to find any config file");
|
error!("Failed to find any config file");
|
||||||
return Ok(());
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let config = match read_config(&config_path).await {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to read and parse config file {:?}", &e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mut conn, mut _handle, mut messages) = new_connection().map_err(|e| anyhow!(e))?;
|
let (mut conn, mut _handle, mut messages) = new_connection().unwrap();
|
||||||
let groups = nl_mgrp(RTNLGRP_LINK) | nl_mgrp(RTNLGRP_IPV4_IFADDR);
|
let groups = nl_mgrp(RTNLGRP_LINK) | nl_mgrp(RTNLGRP_IPV4_IFADDR);
|
||||||
|
|
||||||
let addr = SocketAddr::new(0, groups);
|
let addr = SocketAddr::new(0, groups);
|
||||||
|
|
||||||
if let Err(e) = conn.socket_mut().socket_mut().bind(&addr) {
|
if let Err(e) = conn.socket_mut().socket_mut().bind(&addr) {
|
||||||
error!("Failed to bind to socket: {:?}", &e);
|
error!("Failed to bind to socket: {:?}", &e);
|
||||||
return Err(anyhow!(e));
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::spawn(conn);
|
tokio::spawn(conn);
|
||||||
@ -84,6 +90,4 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user