Compare commits
14 Commits
4f3e689218
...
master
Author | SHA1 | Date | |
---|---|---|---|
64da1e7a2e | |||
60bdb83b6b | |||
1cd57845a1 | |||
787f74e3ec | |||
9b68b32354 | |||
e5c9cb6024 | |||
0cd2d364aa | |||
f34c4609a5 | |||
5466090256 | |||
b6f447e39f | |||
8e5d472018 | |||
b22616e209 | |||
4fd9a9832a | |||
2e2d723b2a |
108
Cargo.lock
generated
108
Cargo.lock
generated
@ -109,9 +109,9 @@ checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.4"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9711f33475c22aab363b05564a17d7b789bf3dfec5ebabb586adee56f0e271b5"
|
||||
checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@ -337,7 +337,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.71",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -705,13 +705,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -816,21 +817,11 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.1"
|
||||
version = "0.36.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
|
||||
checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@ -843,9 +834,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.64"
|
||||
version = "0.10.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
@ -864,7 +855,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.71",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -875,9 +866,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.102"
|
||||
version = "0.9.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
|
||||
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@ -952,7 +943,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.71",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1029,9 +1020,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
|
||||
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
@ -1144,9 +1135,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.11"
|
||||
version = "0.23.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0"
|
||||
checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
@ -1173,9 +1164,9 @@ checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.5"
|
||||
version = "0.102.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78"
|
||||
checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@ -1253,7 +1244,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.71",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1269,9 +1260,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
||||
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@ -1360,9 +1351,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.71"
|
||||
version = "2.0.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
|
||||
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1410,29 +1401,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.62"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.62"
|
||||
version = "1.0.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.71",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread-id"
|
||||
version = "4.2.1"
|
||||
version = "4.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b"
|
||||
checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
@ -1455,32 +1446,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.38.0"
|
||||
version = "1.39.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||
checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.71",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1519,9 +1509,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.14"
|
||||
version = "0.8.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||
checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@ -1531,18 +1521,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.6"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
||||
checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.15"
|
||||
version = "0.22.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1"
|
||||
checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@ -1707,7 +1697,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.71",
|
||||
"syn 2.0.72",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -1741,7 +1731,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.71",
|
||||
"syn 2.0.72",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -1934,9 +1924,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.13"
|
||||
version = "0.6.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
|
||||
checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
24
LICENSE
Normal file
24
LICENSE
Normal file
@ -0,0 +1,24 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2024, Love Billenius
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
80
README.md
Normal file
80
README.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Dynamic IP Updater with Cloudflare DNS
|
||||
|
||||
This project is a dynamic IP updater for Cloudflare DNS records. It listens for changes in the IPv4 address of the network interface and updates the DNS records in Cloudflare accordingly.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically detects changes in the public IP address.
|
||||
- Updates Cloudflare DNS records when the public IP changes.
|
||||
- Listens for network interface events to detect IP changes.
|
||||
- Handles errors and retries updates to Cloudflare.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust and Cargo installed.
|
||||
- Cloudflare account with API token.
|
||||
- Network interface with dynamic IP address.
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository:**
|
||||
|
||||
```sh
|
||||
git clone https://github.com/yourusername/dynip-cloudflare.git
|
||||
cd dynip-cloudflare
|
||||
```
|
||||
|
||||
2. **Build the project:**
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
3. **Create configuration file:**
|
||||
|
||||
Create a `config.toml` file in the current directory or in the configuration directory (typically `~/.config/dynip-cloudflare/`) with the following content:
|
||||
|
||||
```toml
|
||||
zone_id = "your_zone_id"
|
||||
api_key = "your_api_key"
|
||||
domains = ["example.com", "sub.example.com"]
|
||||
max_errors_in_row = 5
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Run the program:**
|
||||
|
||||
```sh
|
||||
./target/release/dynip-cloudflare
|
||||
```
|
||||
|
||||
2. **Logging:**
|
||||
|
||||
The program logs to both stderr and a rotating file located in the `logs` directory. The log level is set to `Info`.
|
||||
|
||||
## Configuration
|
||||
|
||||
### `config.toml`
|
||||
|
||||
- `zone_id`: The Zone ID of your Cloudflare domain.
|
||||
- `api_key`: The API key for your Cloudflare account.
|
||||
- `domains`: A list of domain names to update.
|
||||
- `max_errors_in_row`: Maximum number of consecutive errors before the program stops. Will be set to 10 as default
|
||||
|
||||
### Example
|
||||
|
||||
```toml
|
||||
zone_id = "your_zone_id"
|
||||
api_key = "your_api_key"
|
||||
domains = ["example.com", "sub.example.com"]
|
||||
max_errors_in_row = 5
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The program handles errors gracefully, with retries and logging of error messages. If the number of consecutive errors exceeds `max_errors_in_row`, the program will stop.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the BSD 2-Clause License.
|
@ -1,18 +1,26 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use crate::utils::duration_to_string;
|
||||
use anyhow;
|
||||
use dirs;
|
||||
use log::warn;
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
use std::{env, path::PathBuf};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use tokio::{fs, io::AsyncReadExt};
|
||||
|
||||
use crate::PROGRAM_NAME;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub zone_id: Box<str>,
|
||||
pub api_key: Box<str>,
|
||||
pub zone_id: String,
|
||||
pub api_key: String,
|
||||
pub domains: Vec<Box<str>>,
|
||||
#[serde(default)]
|
||||
pub max_errors_in_row: Option<usize>,
|
||||
#[serde(with = "duration_format", default)]
|
||||
pub max_duration: Option<Duration>,
|
||||
}
|
||||
|
||||
pub async fn get_config_path() -> Result<PathBuf, Vec<PathBuf>> {
|
||||
@ -58,9 +66,64 @@ pub async fn get_config_path() -> Result<PathBuf, Vec<PathBuf>> {
|
||||
Err(tried_paths)
|
||||
}
|
||||
|
||||
pub async fn read_config(path: &PathBuf) -> anyhow::Result<Config> {
|
||||
let mut file = fs::File::open(&path).await?;
|
||||
pub async fn read_config<P: AsRef<Path>>(path: &P) -> anyhow::Result<Config> {
|
||||
let mut file = fs::File::open(path).await?;
|
||||
let mut buf = String::new();
|
||||
file.read_to_string(&mut buf).await?;
|
||||
Ok(toml::from_str(&buf)?)
|
||||
}
|
||||
|
||||
mod duration_format {
|
||||
use super::*;
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn serialize<S>(duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match duration.as_ref() {
|
||||
Some(duration) => serializer.serialize_str(duration_to_string(duration).trim()),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::<Box<str>>::deserialize(deserializer)?.map_or(Ok(None), |s| {
|
||||
parse_duration(&s)
|
||||
.map(Some)
|
||||
.map_err(serde::de::Error::custom)
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_duration(s: &str) -> Result<Duration, String> {
|
||||
let mut total_duration = Duration::new(0, 0);
|
||||
let units = [("d", 86400), ("h", 3600), ("m", 60), ("s", 1)];
|
||||
let mut remainder = s;
|
||||
|
||||
for &(unit, factor) in &units {
|
||||
if let Some(idx) = remainder.find(unit) {
|
||||
let (value, rest) = remainder.split_at(idx);
|
||||
let value: u64 = value.trim().parse().map_err(|_| "Invalid number")?;
|
||||
total_duration += Duration::from_secs(value * factor);
|
||||
remainder = &rest[unit.len()..];
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = remainder.find("ns") {
|
||||
let (value, rest) = remainder.split_at(idx);
|
||||
let value: u32 = value.trim().parse().map_err(|_| "Invalid number")?;
|
||||
total_duration += Duration::new(0, value);
|
||||
remainder = &rest["ns".len()..];
|
||||
}
|
||||
|
||||
if !remainder.trim().is_empty() {
|
||||
return Err("Invalid duration format".to_string());
|
||||
}
|
||||
|
||||
Ok(total_duration)
|
||||
}
|
||||
}
|
||||
|
43
src/exit_listener.rs
Normal file
43
src/exit_listener.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use tokio::{
|
||||
self, signal,
|
||||
sync::{futures::Notified, Notify},
|
||||
};
|
||||
|
||||
pub struct ExitListener {
|
||||
should_exit: Arc<AtomicBool>,
|
||||
notify: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl ExitListener {
|
||||
pub fn new() -> Self {
|
||||
let this = Self {
|
||||
should_exit: Arc::new(AtomicBool::new(false)),
|
||||
notify: Arc::new(Notify::new()),
|
||||
};
|
||||
|
||||
let should_exit = this.should_exit.clone();
|
||||
let notify = this.notify.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to install CTRL+C signal handler");
|
||||
should_exit.store(true, Ordering::SeqCst);
|
||||
notify.notify_one();
|
||||
});
|
||||
this
|
||||
}
|
||||
|
||||
pub fn notified(&self) -> Notified<'_> {
|
||||
self.notify.notified()
|
||||
}
|
||||
|
||||
pub fn should_exit(&self) -> bool {
|
||||
self.should_exit.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
@ -1,49 +1,34 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use log::{error, info};
|
||||
use reqwest::Client;
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
};
|
||||
|
||||
use super::cloudflare_responses::{CloudflareResponse, DnsRecord};
|
||||
use crate::get_current_public_ipv4;
|
||||
|
||||
pub struct CloudflareClient {
|
||||
pub struct CloudflareClient<A, Z>
|
||||
where
|
||||
A: fmt::Display,
|
||||
Z: fmt::Display,
|
||||
{
|
||||
client: Client,
|
||||
domains: Vec<Box<str>>,
|
||||
current_ip: Ipv4Addr,
|
||||
api_key: Box<str>,
|
||||
zone_id: Box<str>,
|
||||
api_key: A,
|
||||
zone_id: Z,
|
||||
}
|
||||
|
||||
#[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<Box<str>, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct CloudflareResponse {
|
||||
success: bool,
|
||||
errors: Vec<HashMap<String, serde_json::Value>>,
|
||||
messages: Vec<HashMap<String, serde_json::Value>>,
|
||||
result: Option<Vec<DnsRecord>>,
|
||||
}
|
||||
|
||||
impl CloudflareClient {
|
||||
pub async fn new(api_key: Box<str>, zone_id: Box<str>, domains: Vec<Box<str>>) -> Result<Self> {
|
||||
impl<Api, Zone> CloudflareClient<Api, Zone>
|
||||
where
|
||||
Api: fmt::Display,
|
||||
Zone: fmt::Display,
|
||||
{
|
||||
pub async fn new(api_key: Api, zone_id: Zone, domains: Vec<Box<str>>) -> Result<Self> {
|
||||
let force_ipv4 = IpAddr::from([0, 0, 0, 0]);
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.local_address(force_ipv4)
|
||||
@ -117,7 +102,7 @@ impl CloudflareClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_dns_records(&self, domain: &str) -> Result<Vec<DnsRecord>> {
|
||||
async fn get_dns_records<D: fmt::Display>(&self, domain: &D) -> Result<Vec<DnsRecord>> {
|
||||
let url = format!(
|
||||
"https://api.cloudflare.com/client/v4/zones/{}/dns_records?type=A&name={}",
|
||||
self.zone_id, domain
|
27
src/internet/cloudflare_responses.rs
Normal file
27
src/internet/cloudflare_responses.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde;
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct DnsRecord {
|
||||
pub id: Box<str>,
|
||||
#[serde(rename = "type")]
|
||||
pub record_type: Box<str>,
|
||||
pub name: Box<str>,
|
||||
pub content: Box<str>,
|
||||
pub ttl: u32,
|
||||
pub proxied: bool,
|
||||
pub locked: bool,
|
||||
pub zone_id: Box<str>,
|
||||
pub zone_name: Box<str>,
|
||||
pub modified_on: Box<str>,
|
||||
pub created_on: Box<str>,
|
||||
pub meta: HashMap<Box<str>, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct CloudflareResponse {
|
||||
pub success: bool,
|
||||
pub errors: Box<[HashMap<Box<str>, serde_json::Value>]>,
|
||||
pub messages: Box<[HashMap<Box<str>, serde_json::Value>]>,
|
||||
pub result: Option<Vec<DnsRecord>>,
|
||||
}
|
3
src/internet/mod.rs
Normal file
3
src/internet/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod cloudflare;
|
||||
pub mod cloudflare_responses;
|
||||
pub use cloudflare::CloudflareClient;
|
14
src/lib.rs
14
src/lib.rs
@ -1,15 +1,21 @@
|
||||
mod cloudflare;
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
mod config;
|
||||
mod exit_listener;
|
||||
mod internet;
|
||||
mod logging;
|
||||
mod public_ip;
|
||||
mod message_handler;
|
||||
mod network_change_listener;
|
||||
mod public_ip;
|
||||
pub mod utils;
|
||||
|
||||
pub use cloudflare::CloudflareClient;
|
||||
pub use config::{get_config_path, read_config, Config};
|
||||
pub use exit_listener::ExitListener;
|
||||
pub use internet::CloudflareClient;
|
||||
pub use logging::init_logger;
|
||||
pub use public_ip::get_current_public_ipv4;
|
||||
pub use message_handler::MessageHandler;
|
||||
pub use network_change_listener::NetworkChangeListener;
|
||||
pub use public_ip::get_current_public_ipv4;
|
||||
|
||||
pub const PROGRAM_NAME: &'static str = "dynip-cloudflare";
|
||||
pub const MAX_ERORS_IN_ROW_DEFAULT: usize = 10;
|
||||
|
@ -1,3 +1,5 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use chrono::Local;
|
||||
use colored::*;
|
||||
use log::LevelFilter;
|
||||
|
96
src/main.rs
96
src/main.rs
@ -1,30 +1,14 @@
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use futures::stream::StreamExt;
|
||||
use futures::future::{self, Either};
|
||||
use log::{error, info};
|
||||
use netlink_sys::{AsyncSocket, SocketAddr};
|
||||
use rtnetlink::new_connection;
|
||||
|
||||
use dynip_cloudflare::{utils, CloudflareClient, MessageHandler, MAX_ERORS_IN_ROW_DEFAULT};
|
||||
use scopeguard::defer;
|
||||
use tokio::{signal, sync::Notify};
|
||||
use tokio::time;
|
||||
|
||||
const RTNLGRP_LINK: u32 = 1;
|
||||
const RTNLGRP_IPV4_IFADDR: u32 = 5;
|
||||
|
||||
const fn nl_mgrp(group: u32) -> u32 {
|
||||
if group > 31 {
|
||||
panic!("use netlink_sys::Socket::add_membership() for this group");
|
||||
}
|
||||
if group == 0 {
|
||||
0
|
||||
} else {
|
||||
1 << (group - 1)
|
||||
}
|
||||
}
|
||||
use dynip_cloudflare::{
|
||||
utils, CloudflareClient, ExitListener, MessageHandler, NetworkChangeListener,
|
||||
MAX_ERORS_IN_ROW_DEFAULT,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@ -32,24 +16,11 @@ async fn main() {
|
||||
defer! {
|
||||
log::logger().flush();
|
||||
}
|
||||
let should_exit = Arc::new(AtomicBool::new(false));
|
||||
let notify = Arc::new(Notify::new());
|
||||
let exit_listener = ExitListener::new();
|
||||
|
||||
let should_exit_clone = should_exit.clone();
|
||||
let notify_clone = notify.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("Failed to install CTRL+C signal handler");
|
||||
should_exit_clone.store(true, Ordering::SeqCst);
|
||||
notify_clone.notify_one();
|
||||
});
|
||||
|
||||
let config = if let Some(aux) = utils::get_config().await {
|
||||
aux
|
||||
} else {
|
||||
return;
|
||||
let config = match utils::get_config().await {
|
||||
Some(aux) => aux,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let mut cloudflare =
|
||||
@ -61,29 +32,48 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
let (mut conn, mut _handle, mut messages) = new_connection().unwrap();
|
||||
let groups = nl_mgrp(RTNLGRP_LINK) | nl_mgrp(RTNLGRP_IPV4_IFADDR);
|
||||
|
||||
let addr = SocketAddr::new(0, groups);
|
||||
|
||||
if let Err(e) = conn.socket_mut().socket_mut().bind(&addr) {
|
||||
error!("Failed to bind to socket: {:?}", &e);
|
||||
let mut network_change_listener = match NetworkChangeListener::new() {
|
||||
Some(aux) => {
|
||||
info!("Listening for IPv4 address changes and interface connect/disconnect events...");
|
||||
aux
|
||||
}
|
||||
None => {
|
||||
error!("Failed to initialize networkchangelistener");
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(conn);
|
||||
info!("Listening for IPv4 address changes and interface connect/disconnect events...");
|
||||
};
|
||||
|
||||
let mut message_handler = MessageHandler::new(
|
||||
&mut cloudflare,
|
||||
config.max_errors_in_row.unwrap_or(MAX_ERORS_IN_ROW_DEFAULT),
|
||||
);
|
||||
|
||||
while !should_exit.load(Ordering::SeqCst) {
|
||||
let mut interval = config.max_duration.map(|duration| {
|
||||
let mut interval = time::interval(duration);
|
||||
interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
|
||||
interval
|
||||
});
|
||||
|
||||
while !exit_listener.should_exit() {
|
||||
let tick_future = match interval.as_mut() {
|
||||
Some(interval) => Either::Left(interval.tick()),
|
||||
None => Either::Right(future::pending::<tokio::time::Instant>()),
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = notify.notified() => break,
|
||||
message = messages.next() => {
|
||||
_ = exit_listener.notified() => break,
|
||||
_ = tick_future => {
|
||||
if let Some(duration) = config.max_duration.as_ref() {
|
||||
let duration_string = utils::duration_to_string(duration);
|
||||
let log_string = format!("{} has passed since last check, checking...", duration_string.trim());
|
||||
message_handler.log_and_check(Some(&log_string), Option::<&&str>::None).await;
|
||||
}
|
||||
}
|
||||
message = network_change_listener.next_message() => {
|
||||
if let Some((message, _)) = message {
|
||||
if let Some(interval) = interval.as_mut() {
|
||||
interval.reset();
|
||||
}
|
||||
message_handler.handle_message(message).await;
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,28 @@
|
||||
use std::fmt;
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use std::fmt;
|
||||
use log::{debug, error, info};
|
||||
use netlink_packet_core::{NetlinkMessage, NetlinkPayload};
|
||||
use netlink_packet_route::RouteNetlinkMessage as RtnlMessage;
|
||||
|
||||
use crate::CloudflareClient;
|
||||
|
||||
pub struct MessageHandler<'a> {
|
||||
cloudflare: &'a mut CloudflareClient,
|
||||
pub struct MessageHandler<'a, Api, Zone>
|
||||
where
|
||||
Api: fmt::Display,
|
||||
Zone: fmt::Display,
|
||||
{
|
||||
cloudflare: &'a mut CloudflareClient<Api, Zone>,
|
||||
errs_counter: usize,
|
||||
errs_max: usize,
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<'a> {
|
||||
pub fn new(cloudflare: &'a mut CloudflareClient, errs_max: usize) -> Self {
|
||||
impl<'a, Api, Zone> MessageHandler<'a, Api, Zone>
|
||||
where
|
||||
Api: fmt::Display,
|
||||
Zone: fmt::Display,
|
||||
{
|
||||
pub fn new(cloudflare: &'a mut CloudflareClient<Api, Zone>, errs_max: usize) -> Self {
|
||||
Self {
|
||||
cloudflare,
|
||||
errs_counter: 0,
|
||||
@ -24,13 +33,14 @@ impl<'a> MessageHandler<'a> {
|
||||
pub async fn handle_message(&mut self, message: NetlinkMessage<RtnlMessage>) -> Option<()> {
|
||||
match message.payload {
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::NewAddress(msg)) => {
|
||||
self.log_and_check("New IPv4 address", &msg).await
|
||||
self.log_and_check(Some("New IPv4 address"), Some(&msg))
|
||||
.await
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::DelAddress(msg)) => {
|
||||
self.log_info("Deleted IPv4 address", &msg).await
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::NewLink(link)) => {
|
||||
self.log_and_check("New link (interface connected)", &link)
|
||||
self.log_and_check(Some("New link (interface connected)"), Some(&link))
|
||||
.await
|
||||
}
|
||||
NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => {
|
||||
@ -44,13 +54,21 @@ impl<'a> MessageHandler<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn log_and_check<D, M>(&mut self, log_msg: &D, msg: &M) -> Option<()>
|
||||
pub async fn log_and_check<D, M>(&mut self, log_msg: Option<&D>, msg: Option<&M>) -> Option<()>
|
||||
where
|
||||
D: fmt::Display + ?Sized,
|
||||
M: fmt::Debug,
|
||||
{
|
||||
info!("{}", log_msg);
|
||||
debug!("{}: {:?}", log_msg, msg);
|
||||
if let Some(s) = log_msg {
|
||||
info!("{}", s);
|
||||
}
|
||||
if let Some(m) = msg {
|
||||
if let Some(lm) = log_msg {
|
||||
debug!("{}: {:?}", lm, m);
|
||||
} else {
|
||||
debug!("{:?}", m);
|
||||
}
|
||||
}
|
||||
if let Err(e) = self.cloudflare.check().await {
|
||||
self.errs_counter += 1;
|
||||
error!(
|
||||
|
60
src/network_change_listener.rs
Normal file
60
src/network_change_listener.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use futures::{
|
||||
channel::mpsc::UnboundedReceiver,
|
||||
stream::{Next, StreamExt},
|
||||
};
|
||||
use log::error;
|
||||
use netlink_packet_core::NetlinkMessage;
|
||||
use netlink_packet_route::RouteNetlinkMessage;
|
||||
use netlink_sys::{AsyncSocket, SocketAddr};
|
||||
use rtnetlink::new_connection;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const RTNLGRP_LINK: u32 = 1;
|
||||
const RTNLGRP_IPV4_IFADDR: u32 = 5;
|
||||
|
||||
const fn nl_mgrp(group: u32) -> u32 {
|
||||
if group > 31 {
|
||||
panic!("use netlink_sys::Socket::add_membership() for this group");
|
||||
}
|
||||
if group == 0 {
|
||||
0
|
||||
} else {
|
||||
1 << (group - 1)
|
||||
}
|
||||
}
|
||||
|
||||
type Messages = UnboundedReceiver<(NetlinkMessage<RouteNetlinkMessage>, SocketAddr)>;
|
||||
pub struct NetworkChangeListener {
|
||||
messages: Messages,
|
||||
thread_handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl NetworkChangeListener {
|
||||
pub fn new() -> Option<Self> {
|
||||
let (mut conn, mut _handle, messages) = new_connection().ok()?;
|
||||
let groups = nl_mgrp(RTNLGRP_LINK) | nl_mgrp(RTNLGRP_IPV4_IFADDR);
|
||||
|
||||
let addr = SocketAddr::new(0, groups);
|
||||
|
||||
if let Err(e) = conn.socket_mut().socket_mut().bind(&addr) {
|
||||
error!("Failed to bind to socket: {:?}", &e);
|
||||
return None;
|
||||
}
|
||||
|
||||
let thread_handle = tokio::spawn(conn);
|
||||
Some(Self {
|
||||
messages,
|
||||
thread_handle,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_message(&mut self) -> Next<'_, Messages> {
|
||||
self.messages.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NetworkChangeListener {
|
||||
fn drop(&mut self) {
|
||||
self.thread_handle.abort();
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use log::error;
|
||||
use reqwest::Client;
|
||||
|
33
src/utils.rs
33
src/utils.rs
@ -1,3 +1,7 @@
|
||||
// SPDX: BSD-2-Clause
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{get_config_path, read_config, Config};
|
||||
use log::error;
|
||||
|
||||
@ -26,3 +30,32 @@ pub async fn get_config() -> Option<Config> {
|
||||
}
|
||||
read_result.ok()
|
||||
}
|
||||
pub fn duration_to_string(duration: &Duration) -> String {
|
||||
let mut secs = duration.as_secs();
|
||||
let nanos = duration.subsec_nanos();
|
||||
|
||||
let days = secs / 86400;
|
||||
secs %= 86400;
|
||||
let hours = secs / 3600;
|
||||
secs %= 3600;
|
||||
let minutes = secs / 60;
|
||||
secs %= 60;
|
||||
|
||||
let mut ret = String::new();
|
||||
if days > 0 {
|
||||
ret.push_str(&format!("{}d ", days));
|
||||
}
|
||||
if hours > 0 {
|
||||
ret.push_str(&format!("{}h ", hours));
|
||||
}
|
||||
if minutes > 0 {
|
||||
ret.push_str(&format!("{}m ", minutes));
|
||||
}
|
||||
if secs > 0 {
|
||||
ret.push_str(&format!("{}s ", secs));
|
||||
}
|
||||
if nanos > 0 {
|
||||
ret.push_str(&format!("{}ns", nanos));
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
59
tests/config_serialization.rs
Normal file
59
tests/config_serialization.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use dynip_cloudflare::Config;
|
||||
use std::time::Duration;
|
||||
use toml;
|
||||
|
||||
const TOML_STR_ONE: &str = r#"
|
||||
zone_id = ""
|
||||
api_key = ""
|
||||
domains = [""]
|
||||
max_duration = "1d 2h 30m 45s 500000000ns"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize() {
|
||||
let config: Config = toml::from_str(TOML_STR_ONE).unwrap();
|
||||
assert_eq!(config.max_duration, Some(Duration::new(95445, 500000000)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize() {
|
||||
let config = Config {
|
||||
zone_id: "".into(),
|
||||
api_key: "".into(),
|
||||
domains: vec!["".into()],
|
||||
max_errors_in_row: None,
|
||||
max_duration: Some(Duration::new(95445, 500000000)),
|
||||
};
|
||||
let toml_str = toml::to_string(&config).unwrap();
|
||||
assert_eq!(TOML_STR_ONE.trim(), toml_str.trim());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_none() {
|
||||
let toml_str = r#"
|
||||
zone_id = ""
|
||||
api_key = ""
|
||||
domains = [""]
|
||||
max_errors_in_row = 5
|
||||
"#;
|
||||
let config: Config = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(config.max_duration, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_none() {
|
||||
let toml_to_be = r#"
|
||||
zone_id = ""
|
||||
api_key = ""
|
||||
domains = [""]
|
||||
"#;
|
||||
let config = Config {
|
||||
zone_id: "".into(),
|
||||
api_key: "".into(),
|
||||
domains: vec!["".into()],
|
||||
max_errors_in_row: None,
|
||||
max_duration: None,
|
||||
};
|
||||
let toml_str = toml::to_string(&config).unwrap();
|
||||
assert_eq!(toml_to_be.trim(), toml_str.trim());
|
||||
}
|
Reference in New Issue
Block a user