Compare commits

..

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

10 changed files with 82 additions and 190 deletions

View file

@ -1,4 +1,4 @@
name: 'Build and release binary file and packages'
name: 'Build and Release Binary File'
author: 'Neshura'
on:
@ -63,7 +63,7 @@ jobs:
name: Bundle .deb package
run: |
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
DEBIAN_REV=-$(cat Cargo.toml | grep -E "(^|\|)revision =" | cut -f2- -d= | tr -d \" | tr -d " ")
echo "DEBIAN_REV=$DEBIAN_REV" >> dist/build.env
@ -76,7 +76,7 @@ jobs:
path: dist
if-no-files-found: error
upload-generic-package:
upload-release:
needs: build
if: success()
runs-on: docker
@ -89,55 +89,23 @@ jobs:
run: |
echo 'curl -v --user ${{ secrets.FORGEJO_USERNAME }}:${{ secrets.FORGEJO_TOKEN }} \
--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 }} \
--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
upload-debian-package:
needs: build
if: success()
runs-on: docker
steps:
https://forgejo.neshweb.net/api/packages/${{ secrets.FORGEJO_USERNAME }}/generic/${{ github.event.repository.name }}/${{ github.ref_name }}/chellaris-rust-api-linux-amd64
-
name: Downloading All Build Artifacts
uses: actions/download-artifact@v3
-
name: Upload Debian Package to staging
name: Upload Debian Package
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/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'
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
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
uses: actions/forgejo-release@v2
uses: actions/forgejo-release@v1
with:
direction: upload
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/
.vscode/
/.env
/interfaces.toml
/zones.d

29
Cargo.lock generated
View file

@ -114,7 +114,7 @@ dependencies = [
[[package]]
name = "cloudflare-dns-updater"
version = "1.1.8"
version = "1.1.1-rc.3"
dependencies = [
"chrono",
"confy",
@ -620,7 +620,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.43",
]
[[package]]
@ -821,7 +821,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.43",
]
[[package]]
@ -868,15 +868,26 @@ dependencies = [
[[package]]
name = "strum_macros"
version = "0.25.3"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2",
"quote",
"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]]
@ -951,7 +962,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.43",
]
[[package]]
@ -1129,7 +1140,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
"syn 2.0.43",
"wasm-bindgen-shared",
]
@ -1163,7 +1174,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.43",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]

View file

@ -1,7 +1,7 @@
[package]
authors = ["Neshura"]
name = "cloudflare-dns-updater"
version = "1.1.8"
version = "1.1.1-rc.3"
edition = "2021"
description = "Application for automatically updating Cloudflare DNS records"
license = "GPL-3.0-or-later"
@ -10,14 +10,6 @@ license = "GPL-3.0-or-later"
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
@ -28,7 +20,7 @@ reqwest = { version = "^0.11.14", features = ["blocking", "json"] }
serde = "^1.0.152"
serde_derive = "^1.0.152"
serde_json = "^1.0.93"
strum_macros = "^0.25.3"
strum_macros = "^0.24.3"
log = "^0.4.20"
systemd-journal-logger = "^2.1.1"
confy = "^0.5.1"

View file

@ -7,10 +7,10 @@
The application necessarily requires a valid Cloudflare API Token.
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/`:
The actual configuration happens in three or more files:
`config.toml` contains general configuration parameters for the application
`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:
@ -29,7 +29,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
```
*zones.d/example.org.toml*
*zone.d/example.org.toml*
```toml
email = "owner@example.org" # Email of User owning the Zone
zone = "example.org" # Zone Name
@ -42,10 +42,4 @@ interface = "example-interface" # Only required on type values 6 and 10
```
## Debian Repository
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)
TODO!

View file

@ -5,7 +5,6 @@ After=network-online.target
[Service]
Type=simple
User=%i
ExecStart=/usr/local/bin/cloudflare-dns-updater
Restart=always
RestartSec=3

View file

@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::env::VarError;
use std::error::Error;
use std::net::{Ipv4Addr, Ipv6Addr};
use log::{error, warn};
@ -32,14 +33,14 @@ pub(crate) struct CloudflareZone {
}
impl CloudflareZone {
pub(crate) fn new(zone: &ZoneConfig, config: &AppConfig) -> Self {
pub(crate) fn new(zone: &ZoneConfig, config: &AppConfig) -> Result<Self, VarError> {
let key = config.cloudflare_api_token.clone();
Self {
Ok(Self {
name: zone.name.clone(),
email: zone.email.clone(),
key,
id: zone.id.clone(),
}
})
}
fn generate_auth_headers(&self) -> HeaderMap {
@ -64,7 +65,7 @@ impl CloudflareZone {
let entries = match response.json::<CloudflareApiResults>() {
Ok(data) => data,
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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -75,7 +76,7 @@ impl CloudflareZone {
Ok(entries.result)
} 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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -84,7 +85,7 @@ impl CloudflareZone {
}
}
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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -107,7 +108,7 @@ impl CloudflareZone {
self.validate_response(response)
},
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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -133,7 +134,7 @@ impl CloudflareZone {
self.validate_response(response)
},
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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -174,7 +175,7 @@ impl CloudflareZone {
self.validate_response(response)
},
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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -200,7 +201,7 @@ impl CloudflareZone {
self.validate_response(response)
},
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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -270,7 +271,7 @@ impl CloudflareZone {
match Url::parse(input) {
Ok(url) => Ok(url),
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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -293,7 +294,7 @@ impl CloudflareZone {
let data = match response.json::<CloudflareApiResult>() {
Ok(data) => data,
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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -305,7 +306,7 @@ impl CloudflareZone {
match data.success {
true => Ok(()),
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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),
@ -314,7 +315,7 @@ impl CloudflareZone {
}
}
} 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() {
true => error!("[ERROR] {err_msg}"),
false => eprintln!("[ERROR] {err_msg}"),

View file

@ -142,6 +142,7 @@ impl Default for ZoneConfig {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub(crate) struct AppConfig {
#[serde(alias="cf_api_token")]
pub(crate) cloudflare_api_token: String,
pub(crate) check_interval_seconds: Option<u16>,
pub(crate) uptime_url: Option<String>,

View file

@ -24,8 +24,8 @@ struct Addresses {
impl Addresses {
fn new() -> Result<Self, Box<dyn Error>> {
let mut ret = Self {
ipv4_uri: "http://ip4only.me/api/".to_owned(),
ipv6_uri: "http://ip6only.me/api/".to_owned(),
ipv4_uri: "https://am.i.mullvad.net/ip".to_owned(),
ipv6_uri: "https://ipv6.am.i.mullvad.net/ip".to_owned(),
ipv4: Ipv4Addr::new(0, 0, 0, 0),
ipv6: Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)
};
@ -61,28 +61,19 @@ impl Addresses {
match self.get_v4() {
Ok(ip) => {
if ip != self.ipv4 {
if ip == Ipv4Addr::new(0,0,0,0) {
let warn_msg = "'0.0.0.0' detected as new IPv4, skipping changes".to_owned();
match connected_to_journal() {
true => warn!("[WARN] {warn_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;
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;
}
}
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() {
true => error!("[ERROR] {error_msg}"),
false => eprintln!("[ERROR] {error_msg}"),
true => warn!("[WARN] {warn_msg}"),
false => println!("[WARN] {warn_msg}"),
}
}
}
@ -90,28 +81,19 @@ impl Addresses {
match self.get_v6() {
Ok(ip) => {
if ip != self.ipv6 {
if ip == Ipv6Addr::new(0,0,0,0,0,0,0,0) {
let warn_msg = "'::' detected as new IPv6, skipping changes".to_owned();
match connected_to_journal() {
true => warn!("[WARN] {warn_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;
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;
}
}
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() {
true => error!("[ERROR] {error_msg}"),
false => eprintln!("[ERROR] {error_msg}"),
true => warn!("[WARN] {warn_msg}"),
false => println!("[WARN] {warn_msg}"),
}
}
}
@ -122,7 +104,7 @@ impl Addresses {
Ok(res) => {
match res.status() {
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"))
},
_ => {
@ -145,7 +127,7 @@ impl Addresses {
Ok(res) => {
match res.status() {
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"))
},
_ => {
@ -577,7 +559,17 @@ fn main() {
ips.update();
for zone in &zone_cfgs {
let cf_zone = CloudflareZone::new(zone, &config);
let cf_zone = match CloudflareZone::new(zone, &config) {
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() {
Ok(entries) => entries,
@ -656,7 +648,7 @@ fn main() {
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);
match connected_to_journal() {
true => info!("[INFO] {info_msg}"),
true => warn!("[INFO] {info_msg}"),
false => println!("[INFO] {info_msg}"),
}
}