Compare commits

..

No commits in common. "main" and "1.1.1-rc.1" have entirely different histories.

12 changed files with 254 additions and 348 deletions

View file

@ -1,4 +1,4 @@
name: 'Build and release binary file and packages' name: 'Build and Release Binary File'
author: 'Neshura' author: 'Neshura'
on: on:
@ -51,6 +51,13 @@ jobs:
- -
name: Checking Out Repository Code name: Checking Out Repository Code
uses: https://code.forgejo.org/actions/checkout@v3 uses: https://code.forgejo.org/actions/checkout@v3
-
name: Installing cargo-deb dependencies
run: apt install -y liblzma-dev
-
name: Installing cargo-deb
run: |
cargo install cargo-deb
- -
name: Prepare build environment name: Prepare build environment
run: mkdir dist run: mkdir dist
@ -63,11 +70,8 @@ jobs:
name: Bundle .deb package name: Bundle .deb package
run: | run: |
cargo deb cargo deb
DEBIAN_REF=$(cat Cargo.toml | grep -E "(^|\|)version =" | cut -f2- -d= | tr -d \" | tr -d " " | tr - \~) DEBIAN_REF=$(echo ${{ github.ref_name }} | tr - \~)
echo "DEBIAN_REF=$DEBIAN_REF" >> dist/build.env mv target/debian/${{ github.event.repository.name }}_$DEBIAN_REF-1_amd64.deb dist/${{ github.event.repository.name }}_$DEBIAN_REF-1_amd64.deb
DEBIAN_REV=-$(cat Cargo.toml | grep -E "(^|\|)revision =" | cut -f2- -d= | tr -d \" | tr -d " ")
echo "DEBIAN_REV=$DEBIAN_REV" >> dist/build.env
mv target/debian/${{ github.event.repository.name }}_"$DEBIAN_REF""$DEBIAN_REV"_amd64.deb dist/${{ github.event.repository.name }}_"$DEBIAN_REF""$DEBIAN_REV"_amd64.deb
- -
name: Uploading Build Artifact name: Uploading Build Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
@ -76,7 +80,7 @@ jobs:
path: dist path: dist
if-no-files-found: error if-no-files-found: error
upload-generic-package: upload-release:
needs: build needs: build
if: success() if: success()
runs-on: docker runs-on: docker
@ -89,55 +93,23 @@ jobs:
run: | run: |
echo 'curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \ echo 'curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \
--upload-file release_blobs/${{ github.event.repository.name }}-linux-amd64 \ --upload-file release_blobs/${{ github.event.repository.name }}-linux-amd64 \
https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/generic/${{ github.event.repository.name }}/${{ github.ref_name }}/${{ github.event.repository.name }}-linux-amd64' https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/generic/${{ github.event.repository.name }}/${{ github.ref_name }}/chellaris-rust-api-linux-amd64'
curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \ curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \
--upload-file release_blobs/${{ github.event.repository.name }}-linux-amd64 \ --upload-file release_blobs/${{ github.event.repository.name }}-linux-amd64 \
https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/generic/${{ github.event.repository.name }}/${{ github.ref_name }}/${{ github.event.repository.name }}-linux-amd64 https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/generic/${{ github.event.repository.name }}/${{ github.ref_name }}/chellaris-rust-api-linux-amd64
upload-debian-package:
needs: build
if: success()
runs-on: docker
steps:
- -
name: Downloading All Build Artifacts name: Upload Debian Package
uses: actions/download-artifact@v3
-
name: Upload Debian Package to staging
run: | run: |
source release_blobs/build.env DEBIAN_REF=$(echo ${{ github.ref_name }} | tr - \~)
echo 'curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \ echo 'curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \
--upload-file release_blobs/${{ github.event.repository.name }}_'"$DEBIAN_REF""$DEBIAN_REV"'_amd64.deb \ --upload-file release_blobs/${{ github.event.repository.name }}_$DEBIAN_REF-1_amd64.deb \
https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/debian/pool/bookworm/staging/upload'
curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \
--upload-file release_blobs/${{ github.event.repository.name }}_"$DEBIAN_REF""$DEBIAN_REV"_amd64.deb \
https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/debian/pool/bookworm/staging/upload
-
name: Upload Debian Package to main
if: (! contains(github.ref_name, '-rc'))
run: |
source release_blobs/build.env
echo 'curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \
--upload-file release_blobs/${{ github.event.repository.name }}_'"$DEBIAN_REF""$DEBIAN_REV"'_amd64.deb \
https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/debian/pool/bookworm/main/upload' https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/debian/pool/bookworm/main/upload'
curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \ curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \
--upload-file release_blobs/${{ github.event.repository.name }}_"$DEBIAN_REF""$DEBIAN_REV"_amd64.deb \ --upload-file release_blobs/${{ github.event.repository.name }}_$DEBIAN_REF-1_amd64.deb \
https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/debian/pool/bookworm/main/upload https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/debian/pool/bookworm/main/upload
create-release:
needs: build
if: success()
runs-on: docker
steps:
-
name: Downloading All Build Artifacts
uses: actions/download-artifact@v3
-
name: Filter out env files
run: rm release_blobs/build.env
- -
name: Release New Version name: Release New Version
uses: actions/forgejo-release@v2 uses: actions/forgejo-release@v1
with: with:
direction: upload direction: upload
url: https://forgejo.neshweb.net url: https://forgejo.neshweb.net

View file

@ -1,67 +0,0 @@
name: 'Build binary file and bundle packages'
author: 'Neshura'
on:
pull_request:
branches:
- main
jobs:
test:
runs-on: docker
container: forgejo.neshweb.net/ci-docker-images/rust-node:latest
steps:
-
name: Add Clippy
run: rustup component add clippy
-
name: Checking Out Repository Code
uses: https://code.forgejo.org/actions/checkout@v3
-
name: Set Up Cargo Cache
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
-
name: Run Clippy
run: cargo clippy
build:
needs: test
if: success()
runs-on: docker
container: forgejo.neshweb.net/ci-docker-images/rust-node:latest
steps:
-
name: Checking Out Repository Code
uses: https://code.forgejo.org/actions/checkout@v3
-
name: Prepare build environment
run: mkdir dist
-
name: Compiling To Linux Target
run: |
cargo build -r
mv target/release/${{ github.event.repository.name }} dist/${{ github.event.repository.name }}-linux-amd64
-
name: Bundle .deb package
run: |
cargo deb
DEBIAN_REF=$(cat Cargo.toml | grep -E "(^|\|)version =" | cut -f2- -d= | tr -d \" | tr -d " " | tr - \~)
echo "DEBIAN_REF=$DEBIAN_REF" >> dist/build.env
DEBIAN_REV=-$(cat Cargo.toml | grep -E "(^|\|)revision =" | cut -f2- -d= | tr -d \" | tr -d " ")
echo "DEBIAN_REV=$DEBIAN_REV" >> dist/build.env
mv target/debian/${{ github.event.repository.name }}_"$DEBIAN_REF""$DEBIAN_REV"_amd64.deb dist/${{ github.event.repository.name }}_"$DEBIAN_REF""$DEBIAN_REV"_amd64.deb
-
name: Uploading Build Artifact
uses: actions/upload-artifact@v3
with:
name: release_blobs
path: dist
if-no-files-found: error

1
.gitignore vendored
View file

@ -4,5 +4,6 @@ venv/
.idea/ .idea/
.vscode/ .vscode/
/.env
/interfaces.toml /interfaces.toml
/zones.d /zones.d

36
Cargo.lock generated
View file

@ -114,10 +114,11 @@ dependencies = [
[[package]] [[package]]
name = "cloudflare-dns-updater" name = "cloudflare-dns-updater"
version = "1.1.9" version = "1.1.1-rc.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"confy", "confy",
"dotenv",
"ipnet", "ipnet",
"log", "log",
"reqwest", "reqwest",
@ -177,6 +178,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.33"
@ -620,7 +627,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.43",
] ]
[[package]] [[package]]
@ -821,7 +828,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.43",
] ]
[[package]] [[package]]
@ -868,15 +875,26 @@ dependencies = [
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
version = "0.25.3" version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn", "syn 1.0.109",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
] ]
[[package]] [[package]]
@ -951,7 +969,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.43",
] ]
[[package]] [[package]]
@ -1129,7 +1147,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.43",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -1163,7 +1181,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.43",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]

View file

@ -1,25 +1,11 @@
[package] [package]
authors = ["Neshura"] authors = ["Neshura"]
name = "cloudflare-dns-updater" name = "cloudflare-dns-updater"
version = "1.1.9" version = "1.1.1-rc.1"
edition = "2021" edition = "2021"
description = "Application for automatically updating Cloudflare DNS records" description = "Application for automatically updating Cloudflare DNS records"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
[package.metadata.deb]
extended-description = "Application for automatically updating Cloudflare DNS records"
maintainer-scripts = "debian/"
revision = "1"
depends = ["libc6", "libssl3", "systemd"]
assets = [
[
"target/release/cloudflare-dns-updater",
"/usr/local/bin/cloudflare-dns-updater",
"755",
]
]
systemd-units = { enable = false }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
@ -28,9 +14,10 @@ reqwest = { version = "^0.11.14", features = ["blocking", "json"] }
serde = "^1.0.152" serde = "^1.0.152"
serde_derive = "^1.0.152" serde_derive = "^1.0.152"
serde_json = "^1.0.93" serde_json = "^1.0.93"
strum_macros = "^0.25.3" strum_macros = "^0.24.3"
log = "^0.4.20" log = "^0.4.20"
systemd-journal-logger = "^2.1.1" systemd-journal-logger = "^2.1.1"
confy = "^0.5.1" confy = "^0.5.1"
dotenv = "^0.15.0"
ipnet = "^2.9.0" ipnet = "^2.9.0"
url = "2.5.0" url = "2.5.0"

View file

@ -7,18 +7,23 @@
The application necessarily requires a valid Cloudflare API Token. The application necessarily requires a valid Cloudflare API Token.
Further the application must be located in the same network as the configured zones. Further the application must be located in the same network as the configured zones.
The actual configuration happens in three or more files located in `~/.config/cloudflare-dns-updater/`: | Environment Variable | Required | Usage |
`config.toml` contains general configuration parameters for the application |:--------------------:|:--------:|:----------------------------------:|
| CF_API_TOKEN | x | Cloudflare API Token |
| STATUS_POST_URL | | Post Endpoint for a Uptime Monitor |
*Note: Variables can be stored in a .env file*
The actual configuration happens in two or more files:
`interfaces.toml` contains all IPv6 interfaces available/used by the zone config files. `interfaces.toml` contains all IPv6 interfaces available/used by the zone config files.
`.toml` files in `zones.d` contain settings for individual zones. `.toml` files in `zone.d` contain settings for individual zones.
Example: Example:
*config.toml* *.env*
```toml ```text
cf_api_token = "0123456789abcdef0123456789abcdef01234" # Cloudflare API Token CF_API_TOKEN=0123456789abcdef0123456789abcdef01234
check_interval_seconds = 30 # Defaults to 60 if missing CHECK_INTERVAL_SECONDS=30 // Defaults to 60 if missing
uptime_url = "https://example.org/uptime/id12" # Post Endpoint for a Uptime Monitor UPTIME_URL=https://example.org/uptime/id12 // Entirely optional
``` ```
*interfaces.toml* *interfaces.toml*
@ -29,7 +34,7 @@ host_address = "::edcb:a098:7654:3210"
example-interface = "::0123:4567:890a:bcde" # static part of the IP, the rest will be dynamically generated using the host example-interface = "::0123:4567:890a:bcde" # static part of the IP, the rest will be dynamically generated using the host
``` ```
*zones.d/example.org.toml* *zone.d/example.org.toml*
```toml ```toml
email = "owner@example.org" # Email of User owning the Zone email = "owner@example.org" # Email of User owning the Zone
zone = "example.org" # Zone Name zone = "example.org" # Zone Name
@ -42,10 +47,4 @@ interface = "example-interface" # Only required on type values 6 and 10
``` ```
## Debian Repository ## Debian Repository
TODO!
Currently supported:
- Debian 12 'Bookworm'
Includes systemd system and user unit files
For more details see [the package registry](https://forgejo.neshweb.net/Neshura/-/packages/debian/cloudflare-dns-updater)

74
cloudflare.json Normal file
View file

@ -0,0 +1,74 @@
{
"AAAA": [
"books",
"calibre",
"docs.gitlab",
"element",
"files",
"gitlab",
"*.gitpages",
"gitpages",
"hentai",
"ipv6",
"jellyfin",
"komga",
"manga",
"mastodon",
"matrix",
"minecraft",
"monitoring",
"mstreaming",
"music",
"navidrome",
"neshura-server.net",
"nextcloud",
"nginx",
"picard",
"porn",
"portainer",
"readyornot",
"registry.gitlab",
"temp1",
"temp2",
"tube",
"video",
"www",
"zomboid"
],
"A": [
"books",
"calibre",
"docs.gitlab",
"element",
"files",
"gitlab",
"*.gitpages",
"gitpages",
"hentai",
"ipv4",
"jellyfin",
"komga",
"manga",
"mastodon",
"matrix",
"minecraft",
"monitoring",
"mstreaming",
"music",
"navidrome",
"neshura-server.net",
"nextcloud",
"nginx",
"picard",
"porn",
"portainer",
"readyornot",
"registry.gitlab",
"temp1",
"temp2",
"tube",
"video",
"www",
"zomboid"
]
}

38
config.json Normal file
View file

@ -0,0 +1,38 @@
{
"ipv6_interface": ":da5e:d3ff:feeb:4346",
"zones": [
{
"email": "neshura@proton.me",
"name": "neshura.net",
"id": "0183f167a051f1e432c0d931478638b5",
"dns_entries": [
{
"name": "*.neshura.net",
"type4": false,
"type6": true,
"interface": ":da5e:d3ff:feeb:4346"
},
{
"name": "neshura.net",
"type4": false,
"type6": true,
"interface": ":da5e:d3ff:feeb:4346"
}
]
},
{
"email": "neshura@proton.me",
"name": "neshura-server.net",
"id": "146d4cd6a1777376b423aaedc6824818",
"dns_entries": [
]
},
{
"email": "neshura@proton.me",
"name": "neshweb.net",
"id": "75b0d52229357478b734ae0f6d075c15",
"dns_entries": [
]
}
]
}

View file

@ -1,14 +0,0 @@
[Unit]
Description="Application for automatically updating Cloudflare DNS records"
After=syslog.target
After=network-online.target
[Service]
Type=simple
User=%i
ExecStart=/usr/local/bin/cloudflare-dns-updater
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target

View file

@ -1,4 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::env;
use std::env::VarError;
use std::error::Error; use std::error::Error;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
use log::{error, warn}; use log::{error, warn};
@ -9,7 +11,7 @@ use serde_derive::{Deserialize, Serialize};
use strum_macros::{Display, IntoStaticStr}; use strum_macros::{Display, IntoStaticStr};
use systemd_journal_logger::connected_to_journal; use systemd_journal_logger::connected_to_journal;
use url::ParseError; use url::ParseError;
use crate::config::{AppConfig, ZoneConfig, ZoneEntry}; use crate::config::{ZoneConfig, ZoneEntry};
const API_BASE: &str = "https://api.cloudflare.com/client/v4"; const API_BASE: &str = "https://api.cloudflare.com/client/v4";
@ -32,14 +34,14 @@ pub(crate) struct CloudflareZone {
} }
impl CloudflareZone { impl CloudflareZone {
pub(crate) fn new(zone: &ZoneConfig, config: &AppConfig) -> Self { pub(crate) fn new(zone: &ZoneConfig) -> Result<Self, VarError> {
let key = config.cloudflare_api_token.clone(); let key = env::var("CF_API_TOKEN")?;
Self { Ok(Self {
name: zone.name.clone(), name: zone.name.clone(),
email: zone.email.clone(), email: zone.email.clone(),
key, key,
id: zone.id.clone(), id: zone.id.clone(),
} })
} }
fn generate_auth_headers(&self) -> HeaderMap { fn generate_auth_headers(&self) -> HeaderMap {
@ -64,7 +66,7 @@ impl CloudflareZone {
let entries = match response.json::<CloudflareApiResults>() { let entries = match response.json::<CloudflareApiResults>() {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
let err_msg = format!("Unable to parse API response: {e}"); let err_msg = format!("Unable to parse API response. Error: {e}");
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -75,7 +77,7 @@ impl CloudflareZone {
Ok(entries.result) Ok(entries.result)
} else { } else {
let err_msg = format!("Unable to fetch Cloudflare Zone Entries for {}: {}",self.name ,response.status()); let err_msg = format!("Unable to fetch Cloudflare Zone Entries for {}. Error: {}",self.name ,response.status());
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -84,7 +86,7 @@ impl CloudflareZone {
} }
} }
Err(e) => { Err(e) => {
let err_msg = format!("Unable to access Cloudflare API: {e}"); let err_msg = format!("Unable to access Cloudflare API. Error: {e}");
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -107,7 +109,7 @@ impl CloudflareZone {
self.validate_response(response) self.validate_response(response)
}, },
Err(e) => { Err(e) => {
let err_msg = format!("Unable to access Cloudflare API: {e}"); let err_msg = format!("Unable to access Cloudflare API. Error: {e}");
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -133,7 +135,7 @@ impl CloudflareZone {
self.validate_response(response) self.validate_response(response)
}, },
Err(e) => { Err(e) => {
let err_msg = format!("Unable to access Cloudflare API: {e}"); let err_msg = format!("Unable to access Cloudflare API. Error: {e}");
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -174,7 +176,7 @@ impl CloudflareZone {
self.validate_response(response) self.validate_response(response)
}, },
Err(e) => { Err(e) => {
let err_msg = format!("Unable to access Cloudflare API: {e}"); let err_msg = format!("Unable to access Cloudflare API. Error: {e}");
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -200,7 +202,7 @@ impl CloudflareZone {
self.validate_response(response) self.validate_response(response)
}, },
Err(e) => { Err(e) => {
let err_msg = format!("Unable to access Cloudflare API: {e}"); let err_msg = format!("Unable to access Cloudflare API. Error: {e}");
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -270,7 +272,7 @@ impl CloudflareZone {
match Url::parse(input) { match Url::parse(input) {
Ok(url) => Ok(url), Ok(url) => Ok(url),
Err(e) => { Err(e) => {
let err_msg = format!("Unable to parse URL: {}", e); let err_msg = format!("Unable to parse URL. Error: {}", e);
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -293,7 +295,7 @@ impl CloudflareZone {
let data = match response.json::<CloudflareApiResult>() { let data = match response.json::<CloudflareApiResult>() {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
let err_msg = format!("Unable to parse API response: {e}"); let err_msg = format!("Unable to parse API response. Error: {e}");
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -305,7 +307,7 @@ impl CloudflareZone {
match data.success { match data.success {
true => Ok(()), true => Ok(()),
false => { false => {
let err_msg = format!("Unexpected error while updating DNS record: {:?}", data); let err_msg = format!("Unexpected error while updating DNS record. Info: {:?}", data);
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -314,7 +316,7 @@ impl CloudflareZone {
} }
} }
} else { } else {
let err_msg = format!("Unable to post/put Cloudflare DNS entry: {}", response.status()); let err_msg = format!("Unable to post/put Cloudflare DNS entry. Error: {}", response.status());
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),

View file

@ -37,7 +37,7 @@ impl InterfaceConfig {
let interface_address = match self.interfaces.get(interface_name) { let interface_address = match self.interfaces.get(interface_name) {
Some(address) => *address, Some(address) => *address,
None => { None => {
let err_msg = format!("Malformed or missing IP in interfaces.toml for interface {}", interface_name); let err_msg = "Malformed IP in interfaces.toml";
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -140,46 +140,3 @@ impl Default for ZoneConfig {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub(crate) struct AppConfig {
pub(crate) cloudflare_api_token: String,
pub(crate) check_interval_seconds: Option<u16>,
pub(crate) uptime_url: Option<String>,
}
impl AppConfig {
pub(crate) fn load() -> Result<Self, Box<dyn Error>> {
let cfg: Self = match confy::load(env!("CARGO_PKG_NAME"),"config") {
Ok(data) => data,
Err(e) => {
match connected_to_journal() {
true => error!("[ERROR] {e}"),
false => eprintln!("[ERROR] {e}")
}
return Err(Box::new(e));
}
};
if cfg.cloudflare_api_token.is_empty() {
let err_msg = "Cloudflare api token not specified. The app cannot work without this";
match connected_to_journal() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}")
}
panic!("{err_msg}");
}
Ok(cfg)
}
}
impl Default for AppConfig {
fn default() -> Self {
Self {
cloudflare_api_token: "".to_owned(),
check_interval_seconds: None,
uptime_url: None
}
}
}

View file

@ -1,15 +1,16 @@
/*use cloudflare_old::{Instance, CloudflareDnsType};*/ /*use cloudflare_old::{Instance, CloudflareDnsType};*/
use reqwest::blocking::get; use reqwest::blocking::get;
use std::{thread::{sleep}}; use std::{env, thread::{sleep}};
use std::error::Error; use std::error::Error;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
use std::str::FromStr; use std::str::FromStr;
use chrono::{Utc, Duration}; use chrono::{Utc, Duration};
use dotenv::dotenv;
use log::{info, warn, error, LevelFilter}; use log::{info, warn, error, LevelFilter};
use reqwest::StatusCode; use reqwest::StatusCode;
use systemd_journal_logger::{connected_to_journal, JournalLog}; use systemd_journal_logger::{connected_to_journal, JournalLog};
use crate::cloudflare::{CloudflareZone, DnsRecordType}; use crate::cloudflare::{CloudflareZone, DnsRecordType};
use crate::config::{AppConfig, InterfaceConfig, ZoneConfig, ZoneEntry}; use crate::config::{InterfaceConfig, ZoneConfig, ZoneEntry};
mod config; mod config;
mod cloudflare; mod cloudflare;
@ -24,8 +25,8 @@ struct Addresses {
impl Addresses { impl Addresses {
fn new() -> Result<Self, Box<dyn Error>> { fn new() -> Result<Self, Box<dyn Error>> {
let mut ret = Self { let mut ret = Self {
ipv4_uri: "http://ip4only.me/api/".to_owned(), ipv4_uri: "https://am.i.mullvad.net/ip".to_owned(),
ipv6_uri: "http://ip6only.me/api/".to_owned(), ipv6_uri: "https://ipv6.am.i.mullvad.net/ip".to_owned(),
ipv4: Ipv4Addr::new(0, 0, 0, 0), ipv4: Ipv4Addr::new(0, 0, 0, 0),
ipv6: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0) ipv6: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)
}; };
@ -61,28 +62,19 @@ impl Addresses {
match self.get_v4() { match self.get_v4() {
Ok(ip) => { Ok(ip) => {
if ip != self.ipv4 { if ip != self.ipv4 {
if ip == Ipv4Addr::new(0,0,0,0) { let info_msg = format!("IPv4 changed from '{}' to '{}'", self.ipv4, ip);
let warn_msg = "'0.0.0.0' detected as new IPv4, skipping changes".to_owned(); match connected_to_journal() {
match connected_to_journal() { true => info!("[INFO] {info_msg}"),
true => warn!("[WARN] {warn_msg}"), false => println!("[INFO] {info_msg}"),
false => println!("[WARN] {warn_msg}"),
}
}
else {
let info_msg = format!("IPv4 changed from '{}' to '{}'", self.ipv4, ip);
match connected_to_journal() {
true => info!("[INFO] {info_msg}"),
false => println!("[INFO] {info_msg}"),
}
self.ipv4 = ip;
} }
self.ipv4 = ip;
} }
} }
Err(e) => { Err(e) => {
let error_msg = format!("Unable to fetch IPv4 from '{}': {}", self.ipv4_uri, e); let warn_msg = format!("Unable to fetch IPv4 from '{}'. Error: {}", self.ipv4_uri, e);
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {error_msg}"), true => warn!("[WARN] {warn_msg}"),
false => eprintln!("[ERROR] {error_msg}"), false => println!("[WARN] {warn_msg}"),
} }
} }
} }
@ -90,28 +82,19 @@ impl Addresses {
match self.get_v6() { match self.get_v6() {
Ok(ip) => { Ok(ip) => {
if ip != self.ipv6 { if ip != self.ipv6 {
if ip == Ipv6Addr::new(0,0,0,0,0,0,0,0) { let info_msg = format!("IPv6 changed from '{}' to '{}'", self.ipv6, ip);
let warn_msg = "'::' detected as new IPv6, skipping changes".to_owned(); match connected_to_journal() {
match connected_to_journal() { true => info!("[INFO] {info_msg}"),
true => warn!("[WARN] {warn_msg}"), false => println!("[INFO] {info_msg}"),
false => println!("[WARN] {warn_msg}"),
}
}
else {
let info_msg = format!("IPv6 changed from '{}' to '{}'", self.ipv6, ip);
match connected_to_journal() {
true => info!("[INFO] {info_msg}"),
false => println!("[INFO] {info_msg}"),
}
self.ipv6 = ip;
} }
self.ipv6 = ip;
} }
} }
Err(e) => { Err(e) => {
let error_msg = format!("Unable to fetch IPv6 from '{}': {}", self.ipv6_uri, e); let warn_msg = format!("Unable to fetch IPv6 from '{}'. Error: {}", self.ipv6_uri, e);
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {error_msg}"), true => warn!("[WARN] {warn_msg}"),
false => eprintln!("[ERROR] {error_msg}"), false => println!("[WARN] {warn_msg}"),
} }
} }
} }
@ -122,7 +105,7 @@ impl Addresses {
Ok(res) => { Ok(res) => {
match res.status() { match res.status() {
StatusCode::OK => { StatusCode::OK => {
let ip_string = res.text().expect("Returned data should always contain text").trim_end().split(',').collect::<Vec<&str>>()[1].to_owned(); let ip_string = res.text().expect("Returned data should always contain text").trim_end().to_owned();
Ok(Ipv4Addr::from_str(ip_string.as_str()).expect("Returned IP should always be parseable")) Ok(Ipv4Addr::from_str(ip_string.as_str()).expect("Returned IP should always be parseable"))
}, },
_ => { _ => {
@ -145,7 +128,7 @@ impl Addresses {
Ok(res) => { Ok(res) => {
match res.status() { match res.status() {
StatusCode::OK => { StatusCode::OK => {
let ip_string: String = res.text().expect("Returned data should always contain text").trim_end().split(',').collect::<Vec<&str>>()[1].to_owned(); let ip_string = res.text().expect("Returned data should always contain text").trim_end().to_owned();
Ok(Ipv6Addr::from_str(ip_string.as_str()).expect("Returned IP should always be parseable")) Ok(Ipv6Addr::from_str(ip_string.as_str()).expect("Returned IP should always be parseable"))
}, },
_ => { _ => {
@ -250,10 +233,10 @@ fn compare_zones(old_zone: &ZoneConfig, new_zone: &ZoneConfig) -> Vec<String> {
} }
fn main() { fn main() {
dotenv().ok();
JournalLog::new().expect("Systemd-Logger crate error").install().expect("Systemd-Logger crate error"); JournalLog::new().expect("Systemd-Logger crate error").install().expect("Systemd-Logger crate error");
log::set_max_level(LevelFilter::Info); log::set_max_level(LevelFilter::Info);
let mut config = AppConfig::load().unwrap();
let mut ifaces = InterfaceConfig::load().unwrap(); let mut ifaces = InterfaceConfig::load().unwrap();
let mut zone_cfgs = ZoneConfig::load().unwrap(); let mut zone_cfgs = ZoneConfig::load().unwrap();
@ -265,22 +248,32 @@ fn main() {
Err(e) => panic!("{}", e) Err(e) => panic!("{}", e)
}; };
let reload_interval = config.check_interval_seconds.unwrap_or_else(|| { let reload_interval = match env::var("CHECK_INTERVAL_SECONDS") {
let warn_msg = "Reload interval option not set, defaulting to 60"; Ok(interval_string) => i64::from_str(&interval_string).unwrap_or_else(|e| {
match connected_to_journal() { let warn_msg = format!("Expected integer number, got '{interval_string}'. Defaulting to 60");
true => warn!("[WARN] {warn_msg}"), match connected_to_journal() {
false => println!("[WARN] {warn_msg}"), true => warn!("[WARN] {warn_msg}"),
} false => println!("[WARN] {warn_msg}"),
60 };
}) as i64; 60
}),
Err(_) => {
let warn_msg = "Reload interval env not set, defaulting to 60";
match connected_to_journal() {
true => warn!("[WARN] {warn_msg}"),
false => println!("[WARN] {warn_msg}"),
}
60
},
};
loop { loop {
now = Utc::now(); now = Utc::now();
if now >= start + Duration::seconds(reload_interval) { if now >= start + Duration::seconds(reload_interval) {
start = now; start = now;
if let Some(uptime_url) = &config.uptime_url { if let Ok(uptime_url) = env::var("UPTIME_URL") {
let _ = get(uptime_url); get(uptime_url);
} }
match InterfaceConfig::load() { match InterfaceConfig::load() {
@ -360,9 +353,10 @@ fn main() {
ifaces = new_cfg ifaces = new_cfg
} }
}, },
Err(e) => { Err(e) => {
let err_msg = format!("Unable to load ínterfaces.toml with error: {e}"); let err_msg = format!("Unable to load ínterfaces.toml with error: {}", e);
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -502,72 +496,7 @@ fn main() {
} }
} }
Err(e) => { Err(e) => {
let err_msg = format!("Unable to load from zones.d with error: {e}"); let err_msg = format!("Unable to load from zones.d with error: {}", e);
match connected_to_journal() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
}
}
}
match AppConfig::load() {
Ok(new_cfg) => {
if config != new_cfg {
if config.cloudflare_api_token != new_cfg.cloudflare_api_token {
let info_msg = "API token in config.toml changed";
match connected_to_journal() {
true => info!("[INFO] {info_msg}"),
false => println!("[INFO] {info_msg}"),
}
}
if config.check_interval_seconds != new_cfg.check_interval_seconds {
let info_msg = match config.check_interval_seconds {
Some(old_interval) => {
match new_cfg.check_interval_seconds {
Some(new_interval) => format!("Check interval in config.toml changed from {old_interval}s to {new_interval}s"),
None => format!("Check interval in config.toml changed from {old_interval}s to 60s"),
}
},
None => {
match new_cfg.check_interval_seconds {
Some(new_interval) => format!("Check interval in config.toml changed from 60s to {new_interval}s"),
None => "This is a unicorn error, congratulations.".to_owned(),
}
}
};
match connected_to_journal() {
true => info!("[INFO] {info_msg}"),
false => println!("[INFO] {info_msg}"),
}
}
if config.uptime_url != new_cfg.uptime_url {
let info_msg = match &config.uptime_url {
Some(old_url) => {
match &new_cfg.uptime_url {
Some(new_url) => format!("Uptime URL in config.toml changed from '{old_url}' to '{new_url}'"),
None => "Uptime URL in config.toml was removed".to_owned(),
}
},
None => {
match &new_cfg.uptime_url {
Some(new_url) => format!("Uptime URL '{new_url}' was added to config.toml"),
None => "This is a unicorn error, congratulations.".to_owned(),
}
}
};
match connected_to_journal() {
true => info!("[INFO] {info_msg}"),
false => println!("[INFO] {info_msg}"),
}
}
config = new_cfg
}
}
Err(e) => {
let err_msg = format!("Unable to load config.toml with error: {e}");
match connected_to_journal() { match connected_to_journal() {
true => error!("[ERROR] {err_msg}"), true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"), false => eprintln!("[ERROR] {err_msg}"),
@ -577,7 +506,17 @@ fn main() {
ips.update(); ips.update();
for zone in &zone_cfgs { for zone in &zone_cfgs {
let cf_zone = CloudflareZone::new(zone, &config); let cf_zone = match CloudflareZone::new(zone) {
Ok(data) => data,
Err(e) => {
let err_msg = format!("Cloudflare Token likely not set. Error: {}", e);
match connected_to_journal() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
}
continue
}
};
let cf_entries = match cf_zone.get_entries() { let cf_entries = match cf_zone.get_entries() {
Ok(entries) => entries, Ok(entries) => entries,
@ -656,7 +595,7 @@ fn main() {
if cf_zone.update(entry, r#type, &cf_entry.id, ipv6, ipv4).is_ok() { if cf_zone.update(entry, r#type, &cf_entry.id, ipv6, ipv4).is_ok() {
let info_msg = format!("Updated {} DNS Record for entry '{}' in zone '{}'", r#type, entry.name, zone.name); let info_msg = format!("Updated {} DNS Record for entry '{}' in zone '{}'", r#type, entry.name, zone.name);
match connected_to_journal() { match connected_to_journal() {
true => info!("[INFO] {info_msg}"), true => warn!("[INFO] {info_msg}"),
false => println!("[INFO] {info_msg}"), false => println!("[INFO] {info_msg}"),
} }
} }