Compare commits

..

27 Commits

Author SHA1 Message Date
64da1e7a2e Clarify license 2024-08-05 14:29:22 +02:00
60bdb83b6b move tests 2024-07-25 21:20:05 +02:00
1cd57845a1 update dependencies 2024-07-25 21:17:57 +02:00
787f74e3ec break out module 2024-07-25 21:16:42 +02:00
9b68b32354 prefer box 2024-07-25 21:07:31 +02:00
e5c9cb6024 prefer match 2024-07-25 21:03:43 +02:00
0cd2d364aa break out network_change_listener 2024-07-25 21:01:40 +02:00
f34c4609a5 exit listener 2024-07-25 20:46:30 +02:00
5466090256 remove main 2024-07-25 20:35:08 +02:00
b6f447e39f add check every so other 2024-07-25 20:32:52 +02:00
8e5d472018 tests and time support 2024-07-25 18:16:12 +02:00
b22616e209 license 2024-07-14 17:13:24 +02:00
4fd9a9832a readme 2024-07-14 17:04:08 +02:00
2e2d723b2a generics 2024-07-14 16:58:22 +02:00
4f3e689218 gitignore 2024-07-14 16:45:01 +02:00
1e2a739e5b logging 2024-07-14 16:35:07 +02:00
b4aa724e26 logging 2024-07-14 16:32:07 +02:00
74a29878be debug 2024-07-14 16:27:19 +02:00
c74361bb56 cloudflareresponse 2024-07-14 16:24:25 +02:00
bb8a83cb62 simplify 2024-07-14 16:10:13 +02:00
e5a40d5ae7 break out message handler 2024-07-14 16:02:28 +02:00
cd191df5c5 break out massive loop 2024-07-14 15:54:15 +02:00
2d31f61af9 logging and other stuff 2024-07-14 15:40:59 +02:00
7b419ab8b7 feels like done 2024-07-14 14:12:36 +02:00
0eaddf064e better error return in config 2024-07-14 13:18:10 +02:00
8de3a762ab add result return 2024-07-14 12:55:12 +02:00
0973bca227 public ip 2024-07-14 12:50:06 +02:00
18 changed files with 1160 additions and 329 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
/target
logs
dynip-cloudflare.toml

496
Cargo.lock generated
View File

@ -18,61 +18,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "1.1.3"
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "anstream"
version = "0.6.14"
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "anstyle-parse"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
"libc",
]
[[package]]
@ -81,6 +38,12 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arc-swap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -146,9 +109,9 @@ checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
[[package]]
name = "cc"
version = "1.1.2"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47de7e88bbbd467951ae7f5a6f34f70d1b4d9cfce53d5fd70f74ebe118b3db56"
checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
[[package]]
name = "cfg-if"
@ -157,10 +120,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "colorchoice"
version = "1.0.1"
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
name = "colored"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
]
[[package]]
name = "core-foundation"
@ -178,6 +159,23 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "destructure_traitobject"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7"
[[package]]
name = "dirs"
version = "5.0.1"
@ -204,15 +202,18 @@ name = "dynip-cloudflare"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"colored",
"dirs",
"env_logger",
"futures",
"log",
"log4rs",
"netlink-packet-core",
"netlink-packet-route",
"netlink-sys",
"reqwest",
"rtnetlink",
"scopeguard",
"serde",
"serde_json",
"tokio",
@ -228,29 +229,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "env_filter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -359,7 +337,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.72",
]
[[package]]
@ -559,6 +537,29 @@ dependencies = [
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "idna"
version = "0.5.0"
@ -585,12 +586,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "itoa"
version = "1.0.11"
@ -606,6 +601,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.155"
@ -643,6 +644,43 @@ name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
dependencies = [
"serde",
]
[[package]]
name = "log-mdc"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7"
[[package]]
name = "log4rs"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6"
dependencies = [
"anyhow",
"arc-swap",
"chrono",
"derivative",
"fnv",
"humantime",
"libc",
"log",
"log-mdc",
"once_cell",
"parking_lot",
"rand",
"serde",
"serde-value",
"serde_json",
"serde_yaml",
"thiserror",
"thread-id",
"typemap-ors",
"winapi",
]
[[package]]
name = "memchr"
@ -667,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]]
@ -770,20 +809,19 @@ dependencies = [
]
[[package]]
name = "num_cpus"
version = "1.16.0"
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"hermit-abi",
"libc",
"autocfg",
]
[[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",
]
@ -796,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",
@ -817,7 +855,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.72",
]
[[package]]
@ -828,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",
@ -844,6 +882,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
@ -896,7 +943,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.72",
]
[[package]]
@ -917,6 +964,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.86"
@ -936,10 +989,40 @@ dependencies = [
]
[[package]]
name = "redox_syscall"
version = "0.5.2"
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
dependencies = [
"bitflags 2.6.0",
]
@ -955,35 +1038,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "regex"
version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "reqwest"
version = "0.12.5"
@ -1081,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",
@ -1110,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",
@ -1142,9 +1196,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
@ -1155,9 +1209,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.11.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf"
dependencies = [
"core-foundation-sys",
"libc",
@ -1172,6 +1226,16 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.204"
@ -1180,7 +1244,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.72",
]
[[package]]
@ -1196,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",
]
@ -1215,6 +1279,19 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@ -1263,9 +1340,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.71"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
dependencies = [
"proc-macro2",
"quote",
@ -1313,22 +1401,32 @@ 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",
"syn 2.0.72",
]
[[package]]
name = "thread-id"
version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea"
dependencies = [
"libc",
"winapi",
]
[[package]]
@ -1348,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",
"syn 2.0.72",
]
[[package]]
@ -1412,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",
@ -1424,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",
@ -1496,6 +1593,15 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typemap-ors"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867"
dependencies = [
"unsafe-any-ors",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"
@ -1517,6 +1623,21 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unsafe-any-ors"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad"
dependencies = [
"destructure_traitobject",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"
@ -1534,12 +1655,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
@ -1582,7 +1697,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
"syn 2.0.72",
"wasm-bindgen-shared",
]
@ -1616,7 +1731,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.72",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -1637,6 +1752,37 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -1778,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",
]

View File

@ -5,16 +5,23 @@ edition = "2021"
[dependencies]
anyhow = "1.0.86"
chrono = "0.4.38"
dirs = "5.0.1"
env_logger = "0.11.3"
futures = "0.3.30"
log = "0.4.22"
netlink-packet-core = "0.7.0"
netlink-packet-route = "0.19.0"
netlink-sys = { version = "0.8.6", features = ["tokio"] }
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"
serde = { version = "1.0.204", features = ["rc", "derive"] }
serde_json = "1.0.120"
tokio = { version = "1.38.0", features = ["full"] }
toml = "0.8.14"
log4rs = { version = "1.3.0", features = [
"console_appender",
"file_appender",
"rolling_file_appender",
] }
scopeguard = "1.2.0"
colored = "2.1.0"

24
LICENSE Normal file
View 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
View 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.

View File

@ -1,51 +1,129 @@
// SPDX: BSD-2-Clause
use crate::utils::duration_to_string;
use anyhow;
use dirs;
use log::warn;
use serde::{Deserialize, Serialize};
use std::{env, path::PathBuf};
use serde::{self, Deserialize, Serialize};
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 cloudflare_zone_id: Box<str>,
pub cloudflare_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() -> Option<PathBuf> {
pub async fn get_config_path() -> Result<PathBuf, Vec<PathBuf>> {
let mut tried_paths = Vec::with_capacity(2);
match env::current_dir() {
Ok(current_dir) => {
let cwd_config = current_dir.join(format!("{}.toml", PROGRAM_NAME));
if let Ok(meta) = fs::metadata(&cwd_config).await {
if meta.is_file() {
return Some(cwd_config);
}
let is_valid_path = fs::metadata(&cwd_config)
.await
.map_or_else(|_| false, |meta| meta.is_file());
if is_valid_path {
return Ok(cwd_config);
} else {
tried_paths.push(cwd_config);
}
}
Err(e) => {
warn!("Failed to get current directory {:?}", &e);
warn!("Failed to get current working directory: {:?}", e);
}
}
match dirs::config_dir() {
Some(config_dir) => {
let config_file = config_dir.join(PROGRAM_NAME).join("config.toml");
if let Ok(meta) = fs::metadata(&config_file).await {
if meta.is_file() {
return Some(config_file);
let is_valid_path = fs::metadata(&config_file)
.await
.map_or_else(|_| false, |meta| meta.is_file());
if is_valid_path {
return Ok(config_file);
} else {
tried_paths.push(config_file);
}
}
None => {
warn!("No configuration directory found.");
}
None => {}
}
None
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
View 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)
}
}

View File

@ -1,55 +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},
sync::Arc,
};
pub struct CloudflareClient {
use super::cloudflare_responses::{CloudflareResponse, DnsRecord};
use crate::get_current_public_ipv4;
pub struct CloudflareClient<A, Z>
where
A: fmt::Display,
Z: fmt::Display,
{
client: Client,
domains: Vec<Arc<str>>,
domains: Vec<Box<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()?)
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<String, serde_json::Value>,
}
impl CloudflareClient {
pub async fn new(api_key: Box<str>, zone_id: Box<str>, domains: Vec<Arc<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)
@ -57,33 +36,40 @@ impl CloudflareClient {
let current_ip = get_current_public_ipv4(&client).await?;
Ok(Self {
let this = Self {
client,
domains,
current_ip,
api_key,
zone_id,
})
};
this.update_dns_records(current_ip).await?;
Ok(this)
}
pub async fn check(&mut self) -> Result<()> {
let new_ip = get_current_public_ipv4(&self.client).await?;
if new_ip == self.current_ip {
info!("IP '{}' is already set", new_ip);
return Ok(());
}
self.update_dns_records(new_ip).await;
info!(
"IP has changed from '{}' -> '{}'",
&self.current_ip, &new_ip
);
self.update_dns_records(new_ip).await?;
self.current_ip = new_ip;
Ok(())
}
async fn update_dns_records(&self, new_ip: Ipv4Addr) {
async fn update_dns_records(&self, new_ip: Ipv4Addr) -> Result<()> {
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 '{}': {:?}",
"Could not fetch DNS records for domain '{}': {:?}",
&domain, &e
);
continue;
@ -93,6 +79,10 @@ impl CloudflareClient {
let new_ip_s = new_ip.to_string().into_boxed_str();
for mut record in records.into_iter() {
if record.content == new_ip_s {
info!(
"On {}, ip already updated to '{}'",
&record.name, &record.content
);
continue;
}
info!(
@ -104,17 +94,21 @@ impl CloudflareClient {
"On {}, failed to update {}: '{}' -> '{}': {:?}",
&domain, record.name, record.content, &new_ip_s, &e
);
}
return Err(e);
}
}
}
async fn get_dns_records(&self, domain: &str) -> Result<Vec<DnsRecord>> {
Ok(())
}
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
);
let mut response = self
let response = self
.client
.get(&url)
.header("Authorization", format!("Bearer {}", self.api_key))
@ -122,12 +116,23 @@ impl CloudflareClient {
.send()
.await?
.error_for_status()?
.json::<HashMap<String, Vec<DnsRecord>>>()
.text()
.await?;
response
.remove("result")
.context("Key result not in return")
let cloudflare_response: CloudflareResponse =
serde_json::from_str(&response).context("Failed to deserialize Cloudflare response")?;
if !cloudflare_response.success {
error!(
"Cloudflare API returned errors: {:?}",
cloudflare_response.errors
);
return Err(anyhow::anyhow!("Cloudflare API returned errors"));
}
cloudflare_response
.result
.context("Key 'result' not found in response")
}
async fn update_dns_record(&self, record: &mut DnsRecord, ip_content: Box<str>) -> Result<()> {

View 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
View File

@ -0,0 +1,3 @@
mod cloudflare;
pub mod cloudflare_responses;
pub use cloudflare::CloudflareClient;

View File

@ -1,6 +1,22 @@
mod cloudflare;
mod config;
pub use config::{Config, get_config_path, read_config};
// SPDX: BSD-2-Clause
mod config;
mod exit_listener;
mod internet;
mod logging;
mod message_handler;
mod network_change_listener;
mod public_ip;
pub mod utils;
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 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;
const LOG_DIR: &str = "logs";

89
src/logging.rs Normal file
View File

@ -0,0 +1,89 @@
// SPDX: BSD-2-Clause
use chrono::Local;
use colored::*;
use log::LevelFilter;
use log4rs::append::console::{ConsoleAppender, Target};
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::config::{Appender, Config, Root};
use log4rs::encode::pattern::PatternEncoder;
use log4rs::encode::{self, Encode};
use log4rs::filter::threshold::ThresholdFilter;
use crate::{LOG_DIR, PROGRAM_NAME};
#[derive(Debug)]
struct ColoredPatternEncoder;
impl ColoredPatternEncoder {
pub fn new() -> Self {
ColoredPatternEncoder {}
}
}
impl Encode for ColoredPatternEncoder {
fn encode(&self, w: &mut dyn encode::Write, record: &log::Record) -> anyhow::Result<()> {
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let level = match record.level() {
log::Level::Error => record.level().to_string().red().bold(),
log::Level::Warn => record.level().to_string().yellow().bold(),
log::Level::Info => record.level().to_string().green().bold(),
log::Level::Debug => record.level().to_string().blue().bold(),
log::Level::Trace => record.level().to_string().purple().bold(),
};
let message = record.args().to_string().cyan();
Ok(writeln!(w, "{} [{}] {}", now.bold(), level, message)?)
}
}
pub fn init_logger() {
let today = Local::now().format("%Y-%m-%d").to_string();
if std::fs::metadata(LOG_DIR).is_err() {
std::fs::create_dir(LOG_DIR).expect("Failed to create log dir");
}
let log_file_path = format!("{}/{}-{}.log", LOG_DIR, PROGRAM_NAME, today);
let log_file_pattern = format!("{}/{}-{}-{{}}.log", LOG_DIR, PROGRAM_NAME, today);
let level = log::LevelFilter::Info;
let stderr = ConsoleAppender::builder()
.encoder(Box::new(ColoredPatternEncoder::new()))
.target(Target::Stderr)
.build();
const TRIGGER_FILE_SIZE: u64 = 5 * 1024; // 5KB as max log file size to roll
let trigger = SizeTrigger::new(TRIGGER_FILE_SIZE);
let roller = FixedWindowRoller::builder()
.base(0)
.build(&log_file_pattern, 3)
.unwrap();
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let logfile = log4rs::append::rolling_file::RollingFileAppender::builder()
.encoder(Box::new(PatternEncoder::new(
"{d(%Y-%m-%d %H:%M:%S)} [{l}] {m}\n",
)))
.build(log_file_path, Box::new(policy))
.unwrap();
let config = Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.appender(
Appender::builder()
.filter(Box::new(ThresholdFilter::new(level)))
.build("stderr", Box::new(stderr)),
)
.build(
Root::builder()
.appender("logfile")
.appender("stderr")
.build(LevelFilter::Trace),
)
.unwrap();
log4rs::init_config(config).expect("Failed to initialize logger");
}

View File

@ -1,91 +1,80 @@
use env_logger;
use futures::stream::StreamExt;
use log::{debug, error, info, log_enabled, Level};
use netlink_packet_core::NetlinkPayload;
use netlink_packet_route::RouteNetlinkMessage as RtnlMessage;
use netlink_sys::{AsyncSocket, SocketAddr};
use rtnetlink::new_connection;
// SPDX: BSD-2-Clause
use dynip_cloudflare::{get_config_path, read_config, Config};
use futures::future::{self, Either};
use log::{error, info};
use scopeguard::defer;
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() {
env_logger::init();
let config_path = match get_config_path().await {
Some(cp) => cp,
None => {
error!("Failed to find any config file");
return;
dynip_cloudflare::init_logger();
defer! {
log::logger().flush();
}
let exit_listener = ExitListener::new();
let config = match utils::get_config().await {
Some(aux) => aux,
None => return,
};
let config = match read_config(&config_path).await {
Ok(config) => config,
let mut cloudflare =
match CloudflareClient::new(config.api_key, config.zone_id, config.domains).await {
Ok(cloudflare) => cloudflare,
Err(e) => {
error!("Failed to read and parse config file {:?}", &e);
error!("Failed to initialize cloudflare client: {:?}", &e);
return;
}
};
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 let Some((message, _)) = messages.next().await {
match message.payload {
NetlinkPayload::InnerMessage(RtnlMessage::NewAddress(msg)) => {
if log_enabled!(Level::Debug) {
debug!("New IPv4 address message: {:?}", msg);
} else {
info!("New IPv4 address");
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! {
_ = 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;
}
}
NetlinkPayload::InnerMessage(RtnlMessage::DelAddress(msg)) => {
if log_enabled!(Level::Debug) {
debug!("Deleted IPv4 address message: {:?}", msg);
} else {
info!("Deleted IPv4 address");
message = network_change_listener.next_message() => {
if let Some((message, _)) = message {
if let Some(interval) = interval.as_mut() {
interval.reset();
}
}
NetlinkPayload::InnerMessage(RtnlMessage::NewLink(link)) => {
if log_enabled!(Level::Debug) {
debug!("New link message (interface connected): {:?}", link);
} else {
info!("New link (interface connected)");
}
}
NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => {
if log_enabled!(Level::Debug) {
debug!("Deleted link message (interface disconnected): {:?}", link);
} else {
info!("Deleted link (interface disconnected)");
}
}
_ => {
if log_enabled!(Level::Debug) {
debug!("Unhandled message payload: {:?}", message.payload);
message_handler.handle_message(message).await;
}
}
}

90
src/message_handler.rs Normal file
View File

@ -0,0 +1,90 @@
// 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, Api, Zone>
where
Api: fmt::Display,
Zone: fmt::Display,
{
cloudflare: &'a mut CloudflareClient<Api, Zone>,
errs_counter: usize,
errs_max: usize,
}
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,
errs_max,
}
}
pub async fn handle_message(&mut self, message: NetlinkMessage<RtnlMessage>) -> Option<()> {
match message.payload {
NetlinkPayload::InnerMessage(RtnlMessage::NewAddress(msg)) => {
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(Some("New link (interface connected)"), Some(&link))
.await
}
NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => {
self.log_info("Deleted link (interface disconnected)", &link)
.await
}
_ => {
debug!("Unhandled message payload: {:?}", message.payload);
Some(())
}
}
}
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,
{
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!(
"Failed to check cloudflare ({}/{}): {:?}",
self.errs_counter, self.errs_max, &e
);
if self.errs_counter >= self.errs_max {
return None;
}
}
Some(())
}
async fn log_info<M: fmt::Debug>(&self, log_msg: &str, msg: &M) -> Option<()> {
info!("{}", log_msg);
debug!("{:?} message: {:?}", log_msg, msg);
Some(())
}
}

View 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();
}
}

52
src/public_ip.rs Normal file
View File

@ -0,0 +1,52 @@
// SPDX: BSD-2-Clause
use anyhow::{anyhow, Context, Result};
use log::error;
use reqwest::Client;
use std::{collections::HashMap, net::Ipv4Addr};
async fn ipify_org(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()?)
}
async fn ifconfig_me(client: &Client) -> Result<Ipv4Addr> {
Ok(client
.get("https://ifconfig.me")
.header("user-agent", "curl/8.8.0")
.send()
.await?
.error_for_status()?
.text()
.await?
.parse()?)
}
pub async fn get_current_public_ipv4(client: &Client) -> Result<Ipv4Addr> {
let e_ipify = match ipify_org(client).await {
Ok(ipv4) => return Ok(ipv4),
Err(e) => {
error!("Failed to get ip from ipify.org: {:?}", &e);
e
}
};
ifconfig_me(client).await.map_err(|e_ifconfig| {
error!("Failed to get ip from ifconfig.me: {:?}", &e_ifconfig);
anyhow!(
"Failed to get ip from ipify.org with error '{:?}', and ifconfig.me with error {:?}",
&e_ipify,
&e_ifconfig
)
})
}

61
src/utils.rs Normal file
View File

@ -0,0 +1,61 @@
// SPDX: BSD-2-Clause
use std::time::Duration;
use crate::{get_config_path, read_config, Config};
use log::error;
pub async fn get_config() -> Option<Config> {
let config_path = match get_config_path().await {
Ok(path) => path,
Err(tried_paths) => {
let extra: String = if !tried_paths.is_empty() {
let joined = tried_paths
.iter()
.filter_map(|path| path.to_str())
.collect::<Vec<_>>()
.join(", ");
format!(", tried the paths: {}", &joined)
} else {
String::with_capacity(0)
};
error!("Failed to find any config file{}", &extra);
return None;
}
};
let read_result = read_config(&config_path).await;
if let Err(e) = &read_result {
error!("Error occurred while getting config path: {:?}", e);
}
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
}

View 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());
}