1/2 part
This commit is contained in:
		@@ -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(())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user