mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-03-21 22:48:57 -03:00
Compare commits
16 Commits
93d351d997
...
v2.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8d5b5cb7e | ||
|
|
bb5cc43651 | ||
|
|
7ff8379cfb | ||
|
|
943e38d70c | ||
|
|
ac982a208e | ||
|
|
4b1875b0cd | ||
|
|
54ca4a5eae | ||
|
|
94ce10fccc | ||
|
|
7e96816740 | ||
|
|
8a4b57c163 | ||
|
|
3c7072f4b6 | ||
|
|
3d796d470c | ||
|
|
36bdbea568 | ||
|
|
6085ba0cc2 | ||
|
|
560a3b7b28 | ||
|
|
1b3928865b |
10
.github/workflows/image.yml
vendored
10
.github/workflows/image.yml
vendored
@@ -15,14 +15,14 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: timothyjmiller/cloudflare-ddns
|
images: timothyjmiller/cloudflare-ddns
|
||||||
tags: |
|
tags: |
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
type=raw,enable=${{ github.ref == 'refs/heads/master' }},value=${{ steps.version.outputs.version }}
|
type=raw,enable=${{ github.ref == 'refs/heads/master' }},value=${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
|||||||
227
Cargo.lock
generated
227
Cargo.lock
generated
@@ -20,6 +20,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert-json-diff"
|
name = "assert-json-diff"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@@ -68,9 +74,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.56"
|
version = "1.2.57"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -103,7 +109,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cloudflare-ddns"
|
name = "cloudflare-ddns"
|
||||||
version = "2.0.1"
|
version = "2.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"idna",
|
"idna",
|
||||||
@@ -187,6 +193,12 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -306,11 +318,24 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi 5.3.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi 6.0.0",
|
||||||
|
"wasip2",
|
||||||
|
"wasip3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@@ -330,12 +355,27 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -555,6 +595,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-arena"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -593,7 +639,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.16.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -634,6 +682,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.183"
|
||||||
@@ -702,9 +756,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
@@ -742,6 +796,16 @@ dependencies = [
|
|||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -821,6 +885,12 @@ version = "5.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
@@ -997,6 +1067,12 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -1140,7 +1216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.4.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -1178,9 +1254,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.10.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tinyvec_macros",
|
"tinyvec_macros",
|
||||||
]
|
]
|
||||||
@@ -1317,6 +1393,12 @@ version = "1.0.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -1365,6 +1447,15 @@ dependencies = [
|
|||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip3"
|
||||||
|
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.114"
|
version = "0.2.114"
|
||||||
@@ -1424,6 +1515,40 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-encoder"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||||
|
dependencies = [
|
||||||
|
"leb128fmt",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-metadata"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"indexmap",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmparser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"indexmap",
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.91"
|
version = "0.3.91"
|
||||||
@@ -1705,6 +1830,88 @@ name = "wit-bindgen"
|
|||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rust-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-core"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"indexmap",
|
||||||
|
"prettyplease",
|
||||||
|
"syn",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-component",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust-macro"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-bindgen-rust",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-component"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wasmparser",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-parser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"id-arena",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"unicode-xid",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cloudflare-ddns"
|
name = "cloudflare-ddns"
|
||||||
version = "2.0.1"
|
version = "2.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Access your home network remotely via a custom domain name without a static IP"
|
description = "Access your home network remotely via a custom domain name without a static IP"
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -28,6 +28,7 @@ Configure everything with environment variables. Supports notifications, heartbe
|
|||||||
- 🎨 **Pretty output with emoji** — Configurable emoji and verbosity levels
|
- 🎨 **Pretty output with emoji** — Configurable emoji and verbosity levels
|
||||||
- 🔒 **Zero-log IP detection** — Uses Cloudflare's [cdn-cgi/trace](https://www.cloudflare.com/cdn-cgi/trace) by default
|
- 🔒 **Zero-log IP detection** — Uses Cloudflare's [cdn-cgi/trace](https://www.cloudflare.com/cdn-cgi/trace) by default
|
||||||
- 🏠 **CGNAT-aware local detection** — Filters out shared address space (100.64.0.0/10) and private ranges
|
- 🏠 **CGNAT-aware local detection** — Filters out shared address space (100.64.0.0/10) and private ranges
|
||||||
|
- 🚫 **Cloudflare IP rejection** — Optionally reject Cloudflare anycast IPs to prevent incorrect DNS updates
|
||||||
- 🤏 **Tiny static binary** — ~1.9 MB Docker image built from scratch, zero runtime dependencies
|
- 🤏 **Tiny static binary** — ~1.9 MB Docker image built from scratch, zero runtime dependencies
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
@@ -87,6 +88,16 @@ Available providers:
|
|||||||
| `literal:<ips>` | 📌 Static IP addresses (comma-separated) |
|
| `literal:<ips>` | 📌 Static IP addresses (comma-separated) |
|
||||||
| `none` | 🚫 Disable this IP type |
|
| `none` | 🚫 Disable this IP type |
|
||||||
|
|
||||||
|
## 🚫 Cloudflare IP Rejection
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `REJECT_CLOUDFLARE_IPS` | `false` | Reject detected IPs that fall within Cloudflare's IP ranges |
|
||||||
|
|
||||||
|
Some IP detection providers occasionally return a Cloudflare anycast IP instead of your real public IP. When this happens, your DNS record gets updated to point at Cloudflare infrastructure rather than your actual address.
|
||||||
|
|
||||||
|
Setting `REJECT_CLOUDFLARE_IPS=true` prevents this. Each update cycle fetches [Cloudflare's published IP ranges](https://www.cloudflare.com/ips/) and skips any detected IP that falls within them. A warning is logged for every rejected IP.
|
||||||
|
|
||||||
## ⏱️ Scheduling
|
## ⏱️ Scheduling
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
@@ -210,6 +221,7 @@ Heartbeats are sent after each update cycle. On failure, a fail signal is sent.
|
|||||||
| `MANAGED_WAF_LIST_ITEMS_COMMENT_REGEX` | — | 🎯 Managed WAF items regex |
|
| `MANAGED_WAF_LIST_ITEMS_COMMENT_REGEX` | — | 🎯 Managed WAF items regex |
|
||||||
| `DETECTION_TIMEOUT` | `5s` | ⏳ IP detection timeout |
|
| `DETECTION_TIMEOUT` | `5s` | ⏳ IP detection timeout |
|
||||||
| `UPDATE_TIMEOUT` | `30s` | ⏳ API request timeout |
|
| `UPDATE_TIMEOUT` | `30s` | ⏳ API request timeout |
|
||||||
|
| `REJECT_CLOUDFLARE_IPS` | `false` | 🚫 Reject Cloudflare anycast IPs |
|
||||||
| `EMOJI` | `true` | 🎨 Enable emoji output |
|
| `EMOJI` | `true` | 🎨 Enable emoji output |
|
||||||
| `QUIET` | `false` | 🤫 Suppress info output |
|
| `QUIET` | `false` | 🤫 Suppress info output |
|
||||||
| `HEALTHCHECKS` | — | 💓 Healthchecks.io URL |
|
| `HEALTHCHECKS` | — | 💓 Healthchecks.io URL |
|
||||||
@@ -356,6 +368,42 @@ Some ISP provided modems only allow port forwarding over IPv4 or IPv6. Disable t
|
|||||||
| `aaaa` | bool | `true` | Enable IPv6 (AAAA record) updates |
|
| `aaaa` | bool | `true` | Enable IPv6 (AAAA record) updates |
|
||||||
| `purgeUnknownRecords` | bool | `false` | Delete stale/duplicate DNS records |
|
| `purgeUnknownRecords` | bool | `false` | Delete stale/duplicate DNS records |
|
||||||
| `ttl` | int | `300` | DNS record TTL in seconds (30-86400, values < 30 become auto) |
|
| `ttl` | int | `300` | DNS record TTL in seconds (30-86400, values < 30 become auto) |
|
||||||
|
| `ip4_provider` | string | `"cloudflare.trace"` | IPv4 detection provider (same values as `IP4_PROVIDER` env var) |
|
||||||
|
| `ip6_provider` | string | `"cloudflare.trace"` | IPv6 detection provider (same values as `IP6_PROVIDER` env var) |
|
||||||
|
|
||||||
|
### 🚫 Cloudflare IP Rejection (Legacy Mode)
|
||||||
|
|
||||||
|
The `REJECT_CLOUDFLARE_IPS` environment variable is supported in legacy config mode. Set it alongside your `config.json`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
REJECT_CLOUDFLARE_IPS=true cloudflare-ddns
|
||||||
|
```
|
||||||
|
|
||||||
|
Or in Docker Compose:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
environment:
|
||||||
|
- REJECT_CLOUDFLARE_IPS=true
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔍 IP Detection (Legacy Mode)
|
||||||
|
|
||||||
|
Legacy mode now uses the same shared provider abstraction as environment variable mode. By default it uses the `cloudflare.trace` provider, which builds an IP-family-bound HTTP client (`0.0.0.0` for IPv4, `[::]` for IPv6) to guarantee the correct address family on dual-stack hosts.
|
||||||
|
|
||||||
|
You can override the detection method per address family with `ip4_provider` and `ip6_provider` in your `config.json`. Supported values are the same as the `IP4_PROVIDER` / `IP6_PROVIDER` environment variables: `cloudflare.trace`, `cloudflare.doh`, `ipify`, `local`, `local.iface:<name>`, `url:<https://...>`, `none`.
|
||||||
|
|
||||||
|
Set a provider to `"none"` to disable detection for that address family (overrides `a`/`aaaa`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"a": true,
|
||||||
|
"aaaa": true,
|
||||||
|
"ip4_provider": "cloudflare.trace",
|
||||||
|
"ip6_provider": "none"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Each zone entry contains:
|
Each zone entry contains:
|
||||||
|
|
||||||
|
|||||||
@@ -24,5 +24,7 @@
|
|||||||
"a": true,
|
"a": true,
|
||||||
"aaaa": true,
|
"aaaa": true,
|
||||||
"purgeUnknownRecords": false,
|
"purgeUnknownRecords": false,
|
||||||
"ttl": 300
|
"ttl": 300,
|
||||||
|
"ip4_provider": "cloudflare.trace",
|
||||||
|
"ip6_provider": "cloudflare.trace"
|
||||||
}
|
}
|
||||||
|
|||||||
237
src/cf_ip_filter.rs
Normal file
237
src/cf_ip_filter.rs
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
use crate::pp::{self, PP};
|
||||||
|
use reqwest::Client;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const CF_IPV4_URL: &str = "https://www.cloudflare.com/ips-v4";
|
||||||
|
const CF_IPV6_URL: &str = "https://www.cloudflare.com/ips-v6";
|
||||||
|
|
||||||
|
/// A CIDR range parsed from "address/prefix" notation.
|
||||||
|
struct CidrRange {
|
||||||
|
addr: IpAddr,
|
||||||
|
prefix_len: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CidrRange {
|
||||||
|
fn parse(s: &str) -> Option<Self> {
|
||||||
|
let (addr_str, prefix_str) = s.split_once('/')?;
|
||||||
|
let addr: IpAddr = addr_str.parse().ok()?;
|
||||||
|
let prefix_len: u8 = prefix_str.parse().ok()?;
|
||||||
|
match addr {
|
||||||
|
IpAddr::V4(_) if prefix_len > 32 => None,
|
||||||
|
IpAddr::V6(_) if prefix_len > 128 => None,
|
||||||
|
_ => Some(Self { addr, prefix_len }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains(&self, ip: &IpAddr) -> bool {
|
||||||
|
match (self.addr, ip) {
|
||||||
|
(IpAddr::V4(net), IpAddr::V4(ip)) => {
|
||||||
|
let net_bits = u32::from(net);
|
||||||
|
let ip_bits = u32::from(*ip);
|
||||||
|
if self.prefix_len == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mask = !0u32 << (32 - self.prefix_len);
|
||||||
|
(net_bits & mask) == (ip_bits & mask)
|
||||||
|
}
|
||||||
|
(IpAddr::V6(net), IpAddr::V6(ip)) => {
|
||||||
|
let net_bits = u128::from(net);
|
||||||
|
let ip_bits = u128::from(*ip);
|
||||||
|
if self.prefix_len == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mask = !0u128 << (128 - self.prefix_len);
|
||||||
|
(net_bits & mask) == (ip_bits & mask)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds parsed Cloudflare CIDR ranges for IP filtering.
|
||||||
|
pub struct CloudflareIpFilter {
|
||||||
|
ranges: Vec<CidrRange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloudflareIpFilter {
|
||||||
|
/// Fetch Cloudflare IP ranges from their published URLs and parse them.
|
||||||
|
pub async fn fetch(client: &Client, timeout: Duration, ppfmt: &PP) -> Option<Self> {
|
||||||
|
let mut ranges = Vec::new();
|
||||||
|
|
||||||
|
for url in [CF_IPV4_URL, CF_IPV6_URL] {
|
||||||
|
match client.get(url).timeout(timeout).send().await {
|
||||||
|
Ok(resp) if resp.status().is_success() => match resp.text().await {
|
||||||
|
Ok(body) => {
|
||||||
|
for line in body.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match CidrRange::parse(line) {
|
||||||
|
Some(range) => ranges.push(range),
|
||||||
|
None => {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!(
|
||||||
|
"Failed to parse Cloudflare IP range '{line}'"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!("Failed to read Cloudflare IP ranges from {url}: {e}"),
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(resp) => {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!(
|
||||||
|
"Failed to fetch Cloudflare IP ranges from {url}: HTTP {}",
|
||||||
|
resp.status()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!("Failed to fetch Cloudflare IP ranges from {url}: {e}"),
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ranges.is_empty() {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
"No Cloudflare IP ranges loaded; skipping filter",
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
ppfmt.infof(
|
||||||
|
pp::EMOJI_DETECT,
|
||||||
|
&format!("Loaded {} Cloudflare IP ranges for filtering", ranges.len()),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(Self { ranges })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse ranges from raw text lines (for testing).
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn from_lines(lines: &str) -> Option<Self> {
|
||||||
|
let ranges: Vec<CidrRange> = lines
|
||||||
|
.lines()
|
||||||
|
.filter_map(|l| {
|
||||||
|
let l = l.trim();
|
||||||
|
if l.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
CidrRange::parse(l)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if ranges.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Self { ranges })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an IP address falls within any Cloudflare range.
|
||||||
|
pub fn contains(&self, ip: &IpAddr) -> bool {
|
||||||
|
self.ranges.iter().any(|net| net.contains(ip))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
|
|
||||||
|
const SAMPLE_RANGES: &str = "\
|
||||||
|
173.245.48.0/20
|
||||||
|
103.21.244.0/22
|
||||||
|
103.22.200.0/22
|
||||||
|
104.16.0.0/13
|
||||||
|
2400:cb00::/32
|
||||||
|
2606:4700::/32
|
||||||
|
";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_ranges() {
|
||||||
|
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||||
|
assert_eq!(filter.ranges.len(), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_contains_cloudflare_ipv4() {
|
||||||
|
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||||
|
// 104.16.0.1 is within 104.16.0.0/13
|
||||||
|
let ip: IpAddr = IpAddr::V4(Ipv4Addr::new(104, 16, 0, 1));
|
||||||
|
assert!(filter.contains(&ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rejects_non_cloudflare_ipv4() {
|
||||||
|
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||||
|
// 203.0.113.42 is a documentation IP, not Cloudflare
|
||||||
|
let ip: IpAddr = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 42));
|
||||||
|
assert!(!filter.contains(&ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_contains_cloudflare_ipv6() {
|
||||||
|
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||||
|
// 2606:4700::1 is within 2606:4700::/32
|
||||||
|
let ip: IpAddr = IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0, 0, 0, 0, 0, 1));
|
||||||
|
assert!(filter.contains(&ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rejects_non_cloudflare_ipv6() {
|
||||||
|
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||||
|
// 2001:db8::1 is a documentation address, not Cloudflare
|
||||||
|
let ip: IpAddr = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
|
||||||
|
assert!(!filter.contains(&ip));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_input() {
|
||||||
|
assert!(CloudflareIpFilter::from_lines("").is_none());
|
||||||
|
assert!(CloudflareIpFilter::from_lines(" \n \n").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edge_of_range() {
|
||||||
|
let filter = CloudflareIpFilter::from_lines("104.16.0.0/13").unwrap();
|
||||||
|
// First IP in range
|
||||||
|
assert!(filter.contains(&IpAddr::V4(Ipv4Addr::new(104, 16, 0, 0))));
|
||||||
|
// Last IP in range (104.23.255.255)
|
||||||
|
assert!(filter.contains(&IpAddr::V4(Ipv4Addr::new(104, 23, 255, 255))));
|
||||||
|
// Just outside range (104.24.0.0)
|
||||||
|
assert!(!filter.contains(&IpAddr::V4(Ipv4Addr::new(104, 24, 0, 0))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_prefix_rejected() {
|
||||||
|
assert!(CidrRange::parse("10.0.0.0/33").is_none());
|
||||||
|
assert!(CidrRange::parse("::1/129").is_none());
|
||||||
|
assert!(CidrRange::parse("not-an-ip/24").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v4_does_not_match_v6() {
|
||||||
|
let filter = CloudflareIpFilter::from_lines("104.16.0.0/13").unwrap();
|
||||||
|
let ip: IpAddr = IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0, 0, 0, 0, 0, 1));
|
||||||
|
assert!(!filter.contains(&ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
161
src/config.rs
161
src/config.rs
@@ -27,6 +27,10 @@ pub struct LegacyConfig {
|
|||||||
pub purge_unknown_records: bool,
|
pub purge_unknown_records: bool,
|
||||||
#[serde(default = "default_ttl")]
|
#[serde(default = "default_ttl")]
|
||||||
pub ttl: i64,
|
pub ttl: i64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ip4_provider: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub ip6_provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
@@ -89,6 +93,7 @@ pub struct AppConfig {
|
|||||||
pub managed_waf_comment_regex: Option<regex::Regex>,
|
pub managed_waf_comment_regex: Option<regex::Regex>,
|
||||||
pub detection_timeout: Duration,
|
pub detection_timeout: Duration,
|
||||||
pub update_timeout: Duration,
|
pub update_timeout: Duration,
|
||||||
|
pub reject_cloudflare_ips: bool,
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
pub emoji: bool,
|
pub emoji: bool,
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
@@ -386,7 +391,7 @@ pub fn parse_legacy_config(content: &str) -> Result<LegacyConfig, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a legacy config into a unified AppConfig
|
/// Convert a legacy config into a unified AppConfig
|
||||||
fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> AppConfig {
|
fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Result<AppConfig, String> {
|
||||||
// Extract auth from first entry
|
// Extract auth from first entry
|
||||||
let auth = if let Some(entry) = legacy.cloudflare.first() {
|
let auth = if let Some(entry) = legacy.cloudflare.first() {
|
||||||
if !entry.authentication.api_token.is_empty()
|
if !entry.authentication.api_token.is_empty()
|
||||||
@@ -405,13 +410,27 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Ap
|
|||||||
Auth::Token(String::new())
|
Auth::Token(String::new())
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build providers
|
// Build providers — ip4_provider/ip6_provider override the default cloudflare.trace
|
||||||
let mut providers = HashMap::new();
|
let mut providers = HashMap::new();
|
||||||
if legacy.a {
|
if legacy.a {
|
||||||
providers.insert(IpType::V4, ProviderType::CloudflareTrace { url: None });
|
let provider = match &legacy.ip4_provider {
|
||||||
|
Some(s) => ProviderType::parse(s)
|
||||||
|
.map_err(|e| format!("Invalid ip4_provider in config.json: {e}"))?,
|
||||||
|
None => ProviderType::CloudflareTrace { url: None },
|
||||||
|
};
|
||||||
|
if !matches!(provider, ProviderType::None) {
|
||||||
|
providers.insert(IpType::V4, provider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if legacy.aaaa {
|
if legacy.aaaa {
|
||||||
providers.insert(IpType::V6, ProviderType::CloudflareTrace { url: None });
|
let provider = match &legacy.ip6_provider {
|
||||||
|
Some(s) => ProviderType::parse(s)
|
||||||
|
.map_err(|e| format!("Invalid ip6_provider in config.json: {e}"))?,
|
||||||
|
None => ProviderType::CloudflareTrace { url: None },
|
||||||
|
};
|
||||||
|
if !matches!(provider, ProviderType::None) {
|
||||||
|
providers.insert(IpType::V6, provider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ttl = TTL::new(legacy.ttl);
|
let ttl = TTL::new(legacy.ttl);
|
||||||
@@ -422,7 +441,7 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Ap
|
|||||||
CronSchedule::Once
|
CronSchedule::Once
|
||||||
};
|
};
|
||||||
|
|
||||||
AppConfig {
|
Ok(AppConfig {
|
||||||
auth,
|
auth,
|
||||||
providers,
|
providers,
|
||||||
domains: HashMap::new(),
|
domains: HashMap::new(),
|
||||||
@@ -439,13 +458,14 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Ap
|
|||||||
managed_waf_comment_regex: None,
|
managed_waf_comment_regex: None,
|
||||||
detection_timeout: Duration::from_secs(5),
|
detection_timeout: Duration::from_secs(5),
|
||||||
update_timeout: Duration::from_secs(30),
|
update_timeout: Duration::from_secs(30),
|
||||||
|
reject_cloudflare_ips: getenv_bool("REJECT_CLOUDFLARE_IPS", false),
|
||||||
dry_run,
|
dry_run,
|
||||||
emoji: false,
|
emoji: false,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
legacy_mode: true,
|
legacy_mode: true,
|
||||||
legacy_config: Some(legacy),
|
legacy_config: Some(legacy),
|
||||||
repeat,
|
repeat,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -509,6 +529,7 @@ pub fn load_env_config(ppfmt: &PP) -> Result<AppConfig, String> {
|
|||||||
|
|
||||||
let emoji = getenv_bool("EMOJI", true);
|
let emoji = getenv_bool("EMOJI", true);
|
||||||
let quiet = getenv_bool("QUIET", false);
|
let quiet = getenv_bool("QUIET", false);
|
||||||
|
let reject_cloudflare_ips = getenv_bool("REJECT_CLOUDFLARE_IPS", false);
|
||||||
|
|
||||||
// Validate: must have at least one update target
|
// Validate: must have at least one update target
|
||||||
if domains.is_empty() && waf_lists.is_empty() {
|
if domains.is_empty() && waf_lists.is_empty() {
|
||||||
@@ -559,6 +580,7 @@ pub fn load_env_config(ppfmt: &PP) -> Result<AppConfig, String> {
|
|||||||
managed_waf_comment_regex,
|
managed_waf_comment_regex,
|
||||||
detection_timeout,
|
detection_timeout,
|
||||||
update_timeout,
|
update_timeout,
|
||||||
|
reject_cloudflare_ips,
|
||||||
dry_run: false, // Set later from CLI args
|
dry_run: false, // Set later from CLI args
|
||||||
emoji,
|
emoji,
|
||||||
quiet,
|
quiet,
|
||||||
@@ -579,7 +601,7 @@ pub fn load_config(dry_run: bool, repeat: bool, ppfmt: &PP) -> Result<AppConfig,
|
|||||||
} else {
|
} else {
|
||||||
ppfmt.infof(pp::EMOJI_CONFIG, "Using config.json configuration");
|
ppfmt.infof(pp::EMOJI_CONFIG, "Using config.json configuration");
|
||||||
let legacy = load_legacy_config()?;
|
let legacy = load_legacy_config()?;
|
||||||
Ok(legacy_to_app_config(legacy, dry_run, repeat))
|
legacy_to_app_config(legacy, dry_run, repeat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,6 +681,10 @@ pub fn print_config_summary(config: &AppConfig, ppfmt: &PP) {
|
|||||||
inner.infof("", "Delete on stop: enabled");
|
inner.infof("", "Delete on stop: enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.reject_cloudflare_ips {
|
||||||
|
inner.infof("", "Reject Cloudflare IPs: enabled");
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(ref comment) = config.record_comment {
|
if let Some(ref comment) = config.record_comment {
|
||||||
inner.infof("", &format!("Record comment: {comment}"));
|
inner.infof("", &format!("Record comment: {comment}"));
|
||||||
}
|
}
|
||||||
@@ -987,8 +1013,10 @@ mod tests {
|
|||||||
aaaa: false,
|
aaaa: false,
|
||||||
purge_unknown_records: false,
|
purge_unknown_records: false,
|
||||||
ttl: 300,
|
ttl: 300,
|
||||||
|
ip4_provider: None,
|
||||||
|
ip6_provider: None,
|
||||||
};
|
};
|
||||||
let config = legacy_to_app_config(legacy, false, false);
|
let config = legacy_to_app_config(legacy, false, false).unwrap();
|
||||||
assert!(config.legacy_mode);
|
assert!(config.legacy_mode);
|
||||||
assert!(matches!(config.auth, Auth::Token(ref t) if t == "my-token"));
|
assert!(matches!(config.auth, Auth::Token(ref t) if t == "my-token"));
|
||||||
assert!(config.providers.contains_key(&IpType::V4));
|
assert!(config.providers.contains_key(&IpType::V4));
|
||||||
@@ -1013,8 +1041,10 @@ mod tests {
|
|||||||
aaaa: true,
|
aaaa: true,
|
||||||
purge_unknown_records: false,
|
purge_unknown_records: false,
|
||||||
ttl: 120,
|
ttl: 120,
|
||||||
|
ip4_provider: None,
|
||||||
|
ip6_provider: None,
|
||||||
};
|
};
|
||||||
let config = legacy_to_app_config(legacy, true, true);
|
let config = legacy_to_app_config(legacy, true, true).unwrap();
|
||||||
assert!(matches!(config.update_cron, CronSchedule::Every(d) if d == Duration::from_secs(120)));
|
assert!(matches!(config.update_cron, CronSchedule::Every(d) if d == Duration::from_secs(120)));
|
||||||
assert!(config.repeat);
|
assert!(config.repeat);
|
||||||
assert!(config.dry_run);
|
assert!(config.dry_run);
|
||||||
@@ -1039,12 +1069,118 @@ mod tests {
|
|||||||
aaaa: true,
|
aaaa: true,
|
||||||
purge_unknown_records: false,
|
purge_unknown_records: false,
|
||||||
ttl: 300,
|
ttl: 300,
|
||||||
|
ip4_provider: None,
|
||||||
|
ip6_provider: None,
|
||||||
};
|
};
|
||||||
let config = legacy_to_app_config(legacy, false, false);
|
let config = legacy_to_app_config(legacy, false, false).unwrap();
|
||||||
assert!(matches!(config.auth, Auth::Key { ref api_key, ref email }
|
assert!(matches!(config.auth, Auth::Key { ref api_key, ref email }
|
||||||
if api_key == "key123" && email == "test@example.com"));
|
if api_key == "key123" && email == "test@example.com"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_to_app_config_custom_providers() {
|
||||||
|
let legacy = LegacyConfig {
|
||||||
|
cloudflare: vec![LegacyCloudflareEntry {
|
||||||
|
authentication: LegacyAuthentication {
|
||||||
|
api_token: "tok".to_string(),
|
||||||
|
api_key: None,
|
||||||
|
},
|
||||||
|
zone_id: "z".to_string(),
|
||||||
|
subdomains: vec![],
|
||||||
|
proxied: false,
|
||||||
|
}],
|
||||||
|
a: true,
|
||||||
|
aaaa: true,
|
||||||
|
purge_unknown_records: false,
|
||||||
|
ttl: 300,
|
||||||
|
ip4_provider: Some("ipify".to_string()),
|
||||||
|
ip6_provider: Some("cloudflare.doh".to_string()),
|
||||||
|
};
|
||||||
|
let config = legacy_to_app_config(legacy, false, false).unwrap();
|
||||||
|
assert!(matches!(config.providers[&IpType::V4], ProviderType::Ipify));
|
||||||
|
assert!(matches!(config.providers[&IpType::V6], ProviderType::CloudflareDOH));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_to_app_config_provider_none_overrides_a_flag() {
|
||||||
|
let legacy = LegacyConfig {
|
||||||
|
cloudflare: vec![LegacyCloudflareEntry {
|
||||||
|
authentication: LegacyAuthentication {
|
||||||
|
api_token: "tok".to_string(),
|
||||||
|
api_key: None,
|
||||||
|
},
|
||||||
|
zone_id: "z".to_string(),
|
||||||
|
subdomains: vec![],
|
||||||
|
proxied: false,
|
||||||
|
}],
|
||||||
|
a: true,
|
||||||
|
aaaa: true,
|
||||||
|
purge_unknown_records: false,
|
||||||
|
ttl: 300,
|
||||||
|
ip4_provider: Some("none".to_string()),
|
||||||
|
ip6_provider: None,
|
||||||
|
};
|
||||||
|
let config = legacy_to_app_config(legacy, false, false).unwrap();
|
||||||
|
// ip4_provider=none should exclude V4 even though a=true
|
||||||
|
assert!(!config.providers.contains_key(&IpType::V4));
|
||||||
|
assert!(config.providers.contains_key(&IpType::V6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_to_app_config_invalid_provider_returns_error() {
|
||||||
|
let legacy = LegacyConfig {
|
||||||
|
cloudflare: vec![LegacyCloudflareEntry {
|
||||||
|
authentication: LegacyAuthentication {
|
||||||
|
api_token: "tok".to_string(),
|
||||||
|
api_key: None,
|
||||||
|
},
|
||||||
|
zone_id: "z".to_string(),
|
||||||
|
subdomains: vec![],
|
||||||
|
proxied: false,
|
||||||
|
}],
|
||||||
|
a: true,
|
||||||
|
aaaa: false,
|
||||||
|
purge_unknown_records: false,
|
||||||
|
ttl: 300,
|
||||||
|
ip4_provider: Some("totally_invalid".to_string()),
|
||||||
|
ip6_provider: None,
|
||||||
|
};
|
||||||
|
let result = legacy_to_app_config(legacy, false, false);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.err().unwrap();
|
||||||
|
assert!(err.contains("ip4_provider"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_config_deserializes_providers() {
|
||||||
|
let json = r#"{
|
||||||
|
"cloudflare": [{
|
||||||
|
"authentication": { "api_token": "tok" },
|
||||||
|
"zone_id": "z",
|
||||||
|
"subdomains": ["@"]
|
||||||
|
}],
|
||||||
|
"ip4_provider": "ipify",
|
||||||
|
"ip6_provider": "none"
|
||||||
|
}"#;
|
||||||
|
let config = parse_legacy_config(json).unwrap();
|
||||||
|
assert_eq!(config.ip4_provider, Some("ipify".to_string()));
|
||||||
|
assert_eq!(config.ip6_provider, Some("none".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_legacy_config_deserializes_without_providers() {
|
||||||
|
let json = r#"{
|
||||||
|
"cloudflare": [{
|
||||||
|
"authentication": { "api_token": "tok" },
|
||||||
|
"zone_id": "z",
|
||||||
|
"subdomains": ["@"]
|
||||||
|
}]
|
||||||
|
}"#;
|
||||||
|
let config = parse_legacy_config(json).unwrap();
|
||||||
|
assert!(config.ip4_provider.is_none());
|
||||||
|
assert!(config.ip6_provider.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
// --- is_env_config_mode ---
|
// --- is_env_config_mode ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1190,6 +1326,7 @@ mod tests {
|
|||||||
managed_waf_comment_regex: None,
|
managed_waf_comment_regex: None,
|
||||||
detection_timeout: Duration::from_secs(5),
|
detection_timeout: Duration::from_secs(5),
|
||||||
update_timeout: Duration::from_secs(30),
|
update_timeout: Duration::from_secs(30),
|
||||||
|
reject_cloudflare_ips: false,
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
emoji: false,
|
emoji: false,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
@@ -1223,6 +1360,7 @@ mod tests {
|
|||||||
managed_waf_comment_regex: None,
|
managed_waf_comment_regex: None,
|
||||||
detection_timeout: Duration::from_secs(5),
|
detection_timeout: Duration::from_secs(5),
|
||||||
update_timeout: Duration::from_secs(30),
|
update_timeout: Duration::from_secs(30),
|
||||||
|
reject_cloudflare_ips: false,
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
emoji: false,
|
emoji: false,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
@@ -1881,6 +2019,7 @@ mod tests {
|
|||||||
managed_waf_comment_regex: None,
|
managed_waf_comment_regex: None,
|
||||||
detection_timeout: Duration::from_secs(5),
|
detection_timeout: Duration::from_secs(5),
|
||||||
update_timeout: Duration::from_secs(30),
|
update_timeout: Duration::from_secs(30),
|
||||||
|
reject_cloudflare_ips: false,
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
emoji: false,
|
emoji: false,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
@@ -1916,6 +2055,7 @@ mod tests {
|
|||||||
managed_waf_comment_regex: None,
|
managed_waf_comment_regex: None,
|
||||||
detection_timeout: Duration::from_secs(5),
|
detection_timeout: Duration::from_secs(5),
|
||||||
update_timeout: Duration::from_secs(30),
|
update_timeout: Duration::from_secs(30),
|
||||||
|
reject_cloudflare_ips: false,
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
emoji: false,
|
emoji: false,
|
||||||
quiet: true,
|
quiet: true,
|
||||||
@@ -1948,6 +2088,7 @@ mod tests {
|
|||||||
managed_waf_comment_regex: None,
|
managed_waf_comment_regex: None,
|
||||||
detection_timeout: Duration::from_secs(5),
|
detection_timeout: Duration::from_secs(5),
|
||||||
update_timeout: Duration::from_secs(30),
|
update_timeout: Duration::from_secs(30),
|
||||||
|
reject_cloudflare_ips: false,
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
emoji: false,
|
emoji: false,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod cf_ip_filter;
|
||||||
mod cloudflare;
|
mod cloudflare;
|
||||||
mod config;
|
mod config;
|
||||||
mod domain;
|
mod domain;
|
||||||
@@ -300,6 +301,8 @@ mod tests {
|
|||||||
aaaa: false,
|
aaaa: false,
|
||||||
purge_unknown_records: false,
|
purge_unknown_records: false,
|
||||||
ttl: 300,
|
ttl: 300,
|
||||||
|
ip4_provider: None,
|
||||||
|
ip6_provider: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,6 +816,8 @@ mod tests {
|
|||||||
aaaa: false,
|
aaaa: false,
|
||||||
purge_unknown_records: true,
|
purge_unknown_records: true,
|
||||||
ttl: 300,
|
ttl: 300,
|
||||||
|
ip4_provider: None,
|
||||||
|
ip6_provider: None,
|
||||||
};
|
};
|
||||||
ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, true)
|
ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, true)
|
||||||
.await;
|
.await;
|
||||||
@@ -912,6 +917,8 @@ mod tests {
|
|||||||
aaaa: false,
|
aaaa: false,
|
||||||
purge_unknown_records: false,
|
purge_unknown_records: false,
|
||||||
ttl: 300,
|
ttl: 300,
|
||||||
|
ip4_provider: None,
|
||||||
|
ip6_provider: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
ddns.commit_record("203.0.113.99", "A", &config.cloudflare, 300, false)
|
ddns.commit_record("203.0.113.99", "A", &config.cloudflare, 300, false)
|
||||||
|
|||||||
@@ -145,12 +145,15 @@ impl ProviderType {
|
|||||||
|
|
||||||
// --- Cloudflare Trace ---
|
// --- Cloudflare Trace ---
|
||||||
|
|
||||||
/// Primary trace URL uses a hostname so DNS resolves normally, avoiding the
|
/// Primary trace URLs use literal IPs to guarantee the correct address family.
|
||||||
/// problem where WARP/Zero Trust intercepts requests to literal 1.1.1.1.
|
/// api.cloudflare.com is dual-stack, so on dual-stack hosts (e.g. Docker
|
||||||
const CF_TRACE_PRIMARY: &str = "https://api.cloudflare.com/cdn-cgi/trace";
|
/// --net=host with IPv6) the connection may go via IPv6 even when detecting
|
||||||
/// Fallback URLs use literal IPs for when api.cloudflare.com is unreachable.
|
/// IPv4, causing the trace endpoint to return the wrong address family.
|
||||||
const CF_TRACE_V4_FALLBACK: &str = "https://1.0.0.1/cdn-cgi/trace";
|
const CF_TRACE_V4_PRIMARY: &str = "https://1.0.0.1/cdn-cgi/trace";
|
||||||
const CF_TRACE_V6_FALLBACK: &str = "https://[2606:4700:4700::1001]/cdn-cgi/trace";
|
const CF_TRACE_V6_PRIMARY: &str = "https://[2606:4700:4700::1001]/cdn-cgi/trace";
|
||||||
|
/// Fallback uses a hostname, which works when literal IPs are intercepted
|
||||||
|
/// (e.g. Cloudflare WARP/Zero Trust).
|
||||||
|
const CF_TRACE_FALLBACK: &str = "https://api.cloudflare.com/cdn-cgi/trace";
|
||||||
|
|
||||||
pub fn parse_trace_ip(body: &str) -> Option<String> {
|
pub fn parse_trace_ip(body: &str) -> Option<String> {
|
||||||
for line in body.lines() {
|
for line in body.lines() {
|
||||||
@@ -161,13 +164,17 @@ pub fn parse_trace_ip(body: &str) -> Option<String> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_trace_ip(client: &Client, url: &str, timeout: Duration) -> Option<IpAddr> {
|
async fn fetch_trace_ip(
|
||||||
let resp = client
|
client: &Client,
|
||||||
.get(url)
|
url: &str,
|
||||||
.timeout(timeout)
|
timeout: Duration,
|
||||||
.send()
|
host_override: Option<&str>,
|
||||||
.await
|
) -> Option<IpAddr> {
|
||||||
.ok()?;
|
let mut req = client.get(url).timeout(timeout);
|
||||||
|
if let Some(host) = host_override {
|
||||||
|
req = req.header("Host", host);
|
||||||
|
}
|
||||||
|
let resp = req.send().await.ok()?;
|
||||||
let body = resp.text().await.ok()?;
|
let body = resp.text().await.ok()?;
|
||||||
let ip_str = parse_trace_ip(&body)?;
|
let ip_str = parse_trace_ip(&body)?;
|
||||||
ip_str.parse::<IpAddr>().ok()
|
ip_str.parse::<IpAddr>().ok()
|
||||||
@@ -176,7 +183,7 @@ async fn fetch_trace_ip(client: &Client, url: &str, timeout: Duration) -> Option
|
|||||||
/// Build an HTTP client that only connects via the given IP family.
|
/// Build an HTTP client that only connects via the given IP family.
|
||||||
/// Binding to 0.0.0.0 forces IPv4-only; binding to [::] forces IPv6-only.
|
/// Binding to 0.0.0.0 forces IPv4-only; binding to [::] forces IPv6-only.
|
||||||
/// This ensures the trace endpoint sees the correct address family.
|
/// This ensures the trace endpoint sees the correct address family.
|
||||||
fn build_split_client(ip_type: IpType, timeout: Duration) -> Client {
|
pub fn build_split_client(ip_type: IpType, timeout: Duration) -> Client {
|
||||||
let local_addr: IpAddr = match ip_type {
|
let local_addr: IpAddr = match ip_type {
|
||||||
IpType::V4 => Ipv4Addr::UNSPECIFIED.into(),
|
IpType::V4 => Ipv4Addr::UNSPECIFIED.into(),
|
||||||
IpType::V6 => Ipv6Addr::UNSPECIFIED.into(),
|
IpType::V6 => Ipv6Addr::UNSPECIFIED.into(),
|
||||||
@@ -199,7 +206,7 @@ async fn detect_cloudflare_trace(
|
|||||||
let client = build_split_client(ip_type, timeout);
|
let client = build_split_client(ip_type, timeout);
|
||||||
|
|
||||||
if let Some(url) = custom_url {
|
if let Some(url) = custom_url {
|
||||||
if let Some(ip) = fetch_trace_ip(&client, url, timeout).await {
|
if let Some(ip) = fetch_trace_ip(&client, url, timeout, None).await {
|
||||||
if validate_detected_ip(&ip, ip_type, ppfmt) {
|
if validate_detected_ip(&ip, ip_type, ppfmt) {
|
||||||
return vec![ip];
|
return vec![ip];
|
||||||
}
|
}
|
||||||
@@ -211,13 +218,13 @@ async fn detect_cloudflare_trace(
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let fallback = match ip_type {
|
let primary = match ip_type {
|
||||||
IpType::V4 => CF_TRACE_V4_FALLBACK,
|
IpType::V4 => CF_TRACE_V4_PRIMARY,
|
||||||
IpType::V6 => CF_TRACE_V6_FALLBACK,
|
IpType::V6 => CF_TRACE_V6_PRIMARY,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try primary (api.cloudflare.com — resolves via DNS, avoids literal-IP interception)
|
// Try primary (literal IP — guarantees correct address family)
|
||||||
if let Some(ip) = fetch_trace_ip(&client, CF_TRACE_PRIMARY, timeout).await {
|
if let Some(ip) = fetch_trace_ip(&client, primary, timeout, Some("one.one.one.one")).await {
|
||||||
if validate_detected_ip(&ip, ip_type, ppfmt) {
|
if validate_detected_ip(&ip, ip_type, ppfmt) {
|
||||||
return vec![ip];
|
return vec![ip];
|
||||||
}
|
}
|
||||||
@@ -227,8 +234,8 @@ async fn detect_cloudflare_trace(
|
|||||||
&format!("{} not detected via primary, trying fallback", ip_type.describe()),
|
&format!("{} not detected via primary, trying fallback", ip_type.describe()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Try fallback (literal IP — useful when DNS is broken)
|
// Try fallback (hostname-based — works when literal IPs are intercepted by WARP/Zero Trust)
|
||||||
if let Some(ip) = fetch_trace_ip(&client, fallback, timeout).await {
|
if let Some(ip) = fetch_trace_ip(&client, CF_TRACE_FALLBACK, timeout, None).await {
|
||||||
if validate_detected_ip(&ip, ip_type, ppfmt) {
|
if validate_detected_ip(&ip, ip_type, ppfmt) {
|
||||||
return vec![ip];
|
return vec![ip];
|
||||||
}
|
}
|
||||||
@@ -918,14 +925,13 @@ mod tests {
|
|||||||
// ---- trace URL constants ----
|
// ---- trace URL constants ----
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_trace_primary_uses_hostname_not_ip() {
|
fn test_trace_urls() {
|
||||||
// Primary must use a hostname (api.cloudflare.com) so DNS resolves normally
|
// Primary URLs use literal IPs to guarantee correct address family.
|
||||||
// and WARP/Zero Trust doesn't intercept the request.
|
assert!(CF_TRACE_V4_PRIMARY.contains("1.0.0.1"));
|
||||||
assert_eq!(CF_TRACE_PRIMARY, "https://api.cloudflare.com/cdn-cgi/trace");
|
assert!(CF_TRACE_V6_PRIMARY.contains("2606:4700:4700::1001"));
|
||||||
assert!(CF_TRACE_PRIMARY.contains("api.cloudflare.com"));
|
// Fallback uses a hostname for when literal IPs are intercepted (WARP/Zero Trust).
|
||||||
// Fallbacks use literal IPs for when DNS is broken.
|
assert_eq!(CF_TRACE_FALLBACK, "https://api.cloudflare.com/cdn-cgi/trace");
|
||||||
assert!(CF_TRACE_V4_FALLBACK.contains("1.0.0.1"));
|
assert!(CF_TRACE_FALLBACK.contains("api.cloudflare.com"));
|
||||||
assert!(CF_TRACE_V6_FALLBACK.contains("2606:4700:4700::1001"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- build_split_client ----
|
// ---- build_split_client ----
|
||||||
|
|||||||
463
src/updater.rs
463
src/updater.rs
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::cf_ip_filter::CloudflareIpFilter;
|
||||||
use crate::cloudflare::{CloudflareHandle, SetResult};
|
use crate::cloudflare::{CloudflareHandle, SetResult};
|
||||||
use crate::config::{AppConfig, LegacyCloudflareEntry, LegacySubdomainEntry};
|
use crate::config::{AppConfig, LegacyCloudflareEntry, LegacySubdomainEntry};
|
||||||
use crate::domain::make_fqdn;
|
use crate::domain::make_fqdn;
|
||||||
@@ -24,6 +25,7 @@ pub async fn update_once(
|
|||||||
|
|
||||||
let mut all_ok = true;
|
let mut all_ok = true;
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
|
let mut notify = false; // NEW: track meaningful events
|
||||||
|
|
||||||
if config.legacy_mode {
|
if config.legacy_mode {
|
||||||
all_ok = update_legacy(config, ppfmt).await;
|
all_ok = update_legacy(config, ppfmt).await;
|
||||||
@@ -64,6 +66,49 @@ pub async fn update_once(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out Cloudflare IPs if enabled
|
||||||
|
if config.reject_cloudflare_ips {
|
||||||
|
if let Some(cf_filter) =
|
||||||
|
CloudflareIpFilter::fetch(&detection_client, config.detection_timeout, ppfmt).await
|
||||||
|
{
|
||||||
|
for (ip_type, ips) in detected_ips.iter_mut() {
|
||||||
|
let before_count = ips.len();
|
||||||
|
ips.retain(|ip| {
|
||||||
|
if cf_filter.contains(ip) {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!(
|
||||||
|
"Rejected {ip}: matches Cloudflare IP range ({})",
|
||||||
|
ip_type.describe()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if ips.is_empty() && before_count > 0 {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!(
|
||||||
|
"All detected {} addresses were Cloudflare IPs; skipping updates for this type",
|
||||||
|
ip_type.describe()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
messages.push(Message::new_fail(&format!(
|
||||||
|
"All {} addresses rejected (Cloudflare IPs)",
|
||||||
|
ip_type.describe()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
"Could not fetch Cloudflare IP ranges; skipping filter",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update DNS records (env var mode - domain-based)
|
// Update DNS records (env var mode - domain-based)
|
||||||
for (ip_type, domains) in &config.domains {
|
for (ip_type, domains) in &config.domains {
|
||||||
let ips = detected_ips.get(ip_type).cloned().unwrap_or_default();
|
let ips = detected_ips.get(ip_type).cloned().unwrap_or_default();
|
||||||
@@ -108,6 +153,7 @@ pub async fn update_once(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
SetResult::Updated => {
|
SetResult::Updated => {
|
||||||
|
notify = true; // NEW
|
||||||
let ip_strs: Vec<String> = ips.iter().map(|ip| ip.to_string()).collect();
|
let ip_strs: Vec<String> = ips.iter().map(|ip| ip.to_string()).collect();
|
||||||
messages.push(Message::new_ok(&format!(
|
messages.push(Message::new_ok(&format!(
|
||||||
"Updated {domain_str} -> {}",
|
"Updated {domain_str} -> {}",
|
||||||
@@ -115,6 +161,7 @@ pub async fn update_once(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
SetResult::Failed => {
|
SetResult::Failed => {
|
||||||
|
notify = true; // NEW
|
||||||
all_ok = false;
|
all_ok = false;
|
||||||
messages.push(Message::new_fail(&format!(
|
messages.push(Message::new_fail(&format!(
|
||||||
"Failed to update {domain_str}"
|
"Failed to update {domain_str}"
|
||||||
@@ -147,12 +194,14 @@ pub async fn update_once(
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
SetResult::Updated => {
|
SetResult::Updated => {
|
||||||
|
notify = true; // NEW
|
||||||
messages.push(Message::new_ok(&format!(
|
messages.push(Message::new_ok(&format!(
|
||||||
"Updated WAF list {}",
|
"Updated WAF list {}",
|
||||||
waf_list.describe()
|
waf_list.describe()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
SetResult::Failed => {
|
SetResult::Failed => {
|
||||||
|
notify = true; // NEW
|
||||||
all_ok = false;
|
all_ok = false;
|
||||||
messages.push(Message::new_fail(&format!(
|
messages.push(Message::new_fail(&format!(
|
||||||
"Failed to update WAF list {}",
|
"Failed to update WAF list {}",
|
||||||
@@ -164,19 +213,28 @@ pub async fn update_once(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send heartbeat
|
// Send heartbeat ONLY if something meaningful happened
|
||||||
|
if notify {
|
||||||
let heartbeat_msg = Message::merge(messages.clone());
|
let heartbeat_msg = Message::merge(messages.clone());
|
||||||
heartbeat.ping(&heartbeat_msg).await;
|
heartbeat.ping(&heartbeat_msg).await;
|
||||||
|
}
|
||||||
|
|
||||||
// Send notifications
|
// Send notifications ONLY when IP changed or failed
|
||||||
|
if notify {
|
||||||
let notifier_msg = Message::merge(messages);
|
let notifier_msg = Message::merge(messages);
|
||||||
notifier.send(¬ifier_msg).await;
|
notifier.send(¬ifier_msg).await;
|
||||||
|
}
|
||||||
|
|
||||||
all_ok
|
all_ok
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run legacy mode update (using the original cloudflare-ddns logic with zone_id-based config).
|
/// Run legacy mode update (using the original cloudflare-ddns logic with zone_id-based config).
|
||||||
async fn update_legacy(config: &AppConfig, _ppfmt: &PP) -> bool {
|
///
|
||||||
|
/// IP detection uses the shared provider abstraction (`config.providers`), which builds
|
||||||
|
/// IP-family-bound clients (0.0.0.0 for IPv4, [::] for IPv6). This prevents the old
|
||||||
|
/// wrong-family warning on dual-stack hosts and honours `ip4_provider`/`ip6_provider`
|
||||||
|
/// overrides from config.json.
|
||||||
|
async fn update_legacy(config: &AppConfig, ppfmt: &PP) -> bool {
|
||||||
let legacy = match &config.legacy_config {
|
let legacy = match &config.legacy_config {
|
||||||
Some(l) => l,
|
Some(l) => l,
|
||||||
None => return false,
|
None => return false,
|
||||||
@@ -190,29 +248,82 @@ async fn update_legacy(config: &AppConfig, _ppfmt: &PP) -> bool {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client,
|
client,
|
||||||
cf_api_base: "https://api.cloudflare.com/client/v4".to_string(),
|
cf_api_base: "https://api.cloudflare.com/client/v4".to_string(),
|
||||||
ipv4_urls: vec![
|
|
||||||
"https://api.cloudflare.com/cdn-cgi/trace".to_string(),
|
|
||||||
"https://1.0.0.1/cdn-cgi/trace".to_string(),
|
|
||||||
],
|
|
||||||
ipv6_urls: vec![
|
|
||||||
"https://api.cloudflare.com/cdn-cgi/trace".to_string(),
|
|
||||||
"https://[2606:4700:4700::1001]/cdn-cgi/trace".to_string(),
|
|
||||||
],
|
|
||||||
dry_run: config.dry_run,
|
dry_run: config.dry_run,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut warnings = LegacyWarningState::default();
|
// Detect IPs using the shared provider abstraction
|
||||||
|
let detection_client = Client::builder()
|
||||||
|
.timeout(config.detection_timeout)
|
||||||
|
.build()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let ips = ddns
|
let mut ips = HashMap::new();
|
||||||
.get_ips(
|
|
||||||
legacy.a,
|
for (ip_type, provider) in &config.providers {
|
||||||
legacy.aaaa,
|
ppfmt.infof(
|
||||||
legacy.purge_unknown_records,
|
pp::EMOJI_DETECT,
|
||||||
&legacy.cloudflare,
|
&format!("Detecting {} via {}", ip_type.describe(), provider.name()),
|
||||||
&mut warnings,
|
);
|
||||||
)
|
let detected = provider
|
||||||
|
.detect_ips(&detection_client, *ip_type, config.detection_timeout, ppfmt)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
if detected.is_empty() {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!("No {} address detected", ip_type.describe()),
|
||||||
|
);
|
||||||
|
if legacy.purge_unknown_records {
|
||||||
|
ddns.delete_entries(ip_type.record_type(), &legacy.cloudflare)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let key = match ip_type {
|
||||||
|
IpType::V4 => "ipv4",
|
||||||
|
IpType::V6 => "ipv6",
|
||||||
|
};
|
||||||
|
ppfmt.infof(
|
||||||
|
pp::EMOJI_DETECT,
|
||||||
|
&format!("Detected {}: {}", ip_type.describe(), detected[0]),
|
||||||
|
);
|
||||||
|
ips.insert(
|
||||||
|
key.to_string(),
|
||||||
|
LegacyIpInfo {
|
||||||
|
record_type: ip_type.record_type().to_string(),
|
||||||
|
ip: detected[0].to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out Cloudflare IPs if enabled
|
||||||
|
if config.reject_cloudflare_ips {
|
||||||
|
if let Some(cf_filter) =
|
||||||
|
CloudflareIpFilter::fetch(&detection_client, config.detection_timeout, ppfmt).await
|
||||||
|
{
|
||||||
|
ips.retain(|key, ip_info| {
|
||||||
|
if let Ok(addr) = ip_info.ip.parse::<std::net::IpAddr>() {
|
||||||
|
if cf_filter.contains(&addr) {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!(
|
||||||
|
"Rejected {}: matches Cloudflare IP range ({})",
|
||||||
|
ip_info.ip, key
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
"Could not fetch Cloudflare IP ranges; skipping filter",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ddns.update_ips(
|
ddns.update_ips(
|
||||||
&ips,
|
&ips,
|
||||||
&legacy.cloudflare,
|
&legacy.cloudflare,
|
||||||
@@ -270,141 +381,13 @@ pub struct LegacyIpInfo {
|
|||||||
pub ip: String,
|
pub ip: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LegacyWarningState {
|
|
||||||
shown_ipv4: bool,
|
|
||||||
shown_ipv4_secondary: bool,
|
|
||||||
shown_ipv6: bool,
|
|
||||||
shown_ipv6_secondary: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LegacyWarningState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
shown_ipv4: false,
|
|
||||||
shown_ipv4_secondary: false,
|
|
||||||
shown_ipv6: false,
|
|
||||||
shown_ipv6_secondary: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LegacyDdnsClient {
|
struct LegacyDdnsClient {
|
||||||
client: Client,
|
client: Client,
|
||||||
cf_api_base: String,
|
cf_api_base: String,
|
||||||
ipv4_urls: Vec<String>,
|
|
||||||
ipv6_urls: Vec<String>,
|
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LegacyDdnsClient {
|
impl LegacyDdnsClient {
|
||||||
async fn get_ips(
|
|
||||||
&self,
|
|
||||||
ipv4_enabled: bool,
|
|
||||||
ipv6_enabled: bool,
|
|
||||||
purge_unknown_records: bool,
|
|
||||||
config: &[LegacyCloudflareEntry],
|
|
||||||
warnings: &mut LegacyWarningState,
|
|
||||||
) -> HashMap<String, LegacyIpInfo> {
|
|
||||||
let mut ips = HashMap::new();
|
|
||||||
|
|
||||||
if ipv4_enabled {
|
|
||||||
let a = self
|
|
||||||
.try_trace_urls(
|
|
||||||
&self.ipv4_urls,
|
|
||||||
&mut warnings.shown_ipv4,
|
|
||||||
&mut warnings.shown_ipv4_secondary,
|
|
||||||
"IPv4",
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
if a.is_none() && purge_unknown_records {
|
|
||||||
self.delete_entries("A", config).await;
|
|
||||||
}
|
|
||||||
if let Some(ip) = a {
|
|
||||||
ips.insert(
|
|
||||||
"ipv4".to_string(),
|
|
||||||
LegacyIpInfo {
|
|
||||||
record_type: "A".to_string(),
|
|
||||||
ip,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ipv6_enabled {
|
|
||||||
let aaaa = self
|
|
||||||
.try_trace_urls(
|
|
||||||
&self.ipv6_urls,
|
|
||||||
&mut warnings.shown_ipv6,
|
|
||||||
&mut warnings.shown_ipv6_secondary,
|
|
||||||
"IPv6",
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
if aaaa.is_none() && purge_unknown_records {
|
|
||||||
self.delete_entries("AAAA", config).await;
|
|
||||||
}
|
|
||||||
if let Some(ip) = aaaa {
|
|
||||||
ips.insert(
|
|
||||||
"ipv6".to_string(),
|
|
||||||
LegacyIpInfo {
|
|
||||||
record_type: "AAAA".to_string(),
|
|
||||||
ip,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ips
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_trace_urls(
|
|
||||||
&self,
|
|
||||||
urls: &[String],
|
|
||||||
shown_primary: &mut bool,
|
|
||||||
shown_secondary: &mut bool,
|
|
||||||
label: &str,
|
|
||||||
expect_v4: bool,
|
|
||||||
) -> Option<String> {
|
|
||||||
for (i, url) in urls.iter().enumerate() {
|
|
||||||
match self.client.get(url).send().await {
|
|
||||||
Ok(resp) => {
|
|
||||||
if let Some(ip) =
|
|
||||||
crate::provider::parse_trace_ip(&resp.text().await.unwrap_or_default())
|
|
||||||
{
|
|
||||||
// Validate the IP matches the expected address family
|
|
||||||
if let Ok(addr) = ip.parse::<std::net::IpAddr>() {
|
|
||||||
if expect_v4 && !addr.is_ipv4() {
|
|
||||||
eprintln!("{label} trace returned IPv6 address, skipping");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !expect_v4 && !addr.is_ipv6() {
|
|
||||||
eprintln!("{label} trace returned IPv4 address, skipping");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Some(ip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
if i == 0 && !*shown_primary {
|
|
||||||
*shown_primary = true;
|
|
||||||
let next = if urls.len() > 1 {
|
|
||||||
", trying fallback"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
eprintln!("{label} not detected via primary{next}");
|
|
||||||
} else if i > 0 && !*shown_secondary {
|
|
||||||
*shown_secondary = true;
|
|
||||||
eprintln!("{label} not detected via fallback. Verify your ISP or DNS provider isn't blocking Cloudflare's IPs.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn cf_api<T: serde::de::DeserializeOwned>(
|
async fn cf_api<T: serde::de::DeserializeOwned>(
|
||||||
&self,
|
&self,
|
||||||
endpoint: &str,
|
endpoint: &str,
|
||||||
@@ -684,6 +667,7 @@ mod tests {
|
|||||||
managed_waf_comment_regex: None,
|
managed_waf_comment_regex: None,
|
||||||
detection_timeout: Duration::from_secs(5),
|
detection_timeout: Duration::from_secs(5),
|
||||||
update_timeout: Duration::from_secs(5),
|
update_timeout: Duration::from_secs(5),
|
||||||
|
reject_cloudflare_ips: false,
|
||||||
dry_run,
|
dry_run,
|
||||||
emoji: false,
|
emoji: false,
|
||||||
quiet: true,
|
quiet: true,
|
||||||
@@ -1647,86 +1631,6 @@ mod tests {
|
|||||||
// LegacyDdnsClient tests (internal/private struct)
|
// LegacyDdnsClient tests (internal/private struct)
|
||||||
// -------------------------------------------------------
|
// -------------------------------------------------------
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_legacy_try_trace_urls_primary_success() {
|
|
||||||
let server = MockServer::start().await;
|
|
||||||
Mock::given(method("GET"))
|
|
||||||
.and(path("/trace"))
|
|
||||||
.respond_with(
|
|
||||||
ResponseTemplate::new(200)
|
|
||||||
.set_body_string("fl=1\nh=mock\nip=198.51.100.1\nts=0\n"),
|
|
||||||
)
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let ddns = LegacyDdnsClient {
|
|
||||||
client: Client::new(),
|
|
||||||
cf_api_base: server.uri(),
|
|
||||||
ipv4_urls: vec![format!("{}/trace", server.uri())],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
|
||||||
};
|
|
||||||
let mut shown_primary = false;
|
|
||||||
let mut shown_secondary = false;
|
|
||||||
let result = ddns
|
|
||||||
.try_trace_urls(&ddns.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
|
|
||||||
.await;
|
|
||||||
assert_eq!(result, Some("198.51.100.1".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_legacy_try_trace_urls_primary_fails_fallback_succeeds() {
|
|
||||||
let server = MockServer::start().await;
|
|
||||||
Mock::given(method("GET"))
|
|
||||||
.and(path("/fallback"))
|
|
||||||
.respond_with(
|
|
||||||
ResponseTemplate::new(200)
|
|
||||||
.set_body_string("fl=1\nh=mock\nip=198.51.100.2\nts=0\n"),
|
|
||||||
)
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let ddns = LegacyDdnsClient {
|
|
||||||
client: Client::new(),
|
|
||||||
cf_api_base: server.uri(),
|
|
||||||
ipv4_urls: vec![
|
|
||||||
"http://127.0.0.1:1/nonexistent".to_string(), // will fail
|
|
||||||
format!("{}/fallback", server.uri()),
|
|
||||||
],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
|
||||||
};
|
|
||||||
let mut shown_primary = false;
|
|
||||||
let mut shown_secondary = false;
|
|
||||||
let result = ddns
|
|
||||||
.try_trace_urls(&ddns.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
|
|
||||||
.await;
|
|
||||||
assert_eq!(result, Some("198.51.100.2".to_string()));
|
|
||||||
assert!(shown_primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_legacy_try_trace_urls_all_fail() {
|
|
||||||
let ddns = LegacyDdnsClient {
|
|
||||||
client: Client::builder().timeout(Duration::from_millis(100)).build().unwrap(),
|
|
||||||
cf_api_base: String::new(),
|
|
||||||
ipv4_urls: vec![
|
|
||||||
"http://127.0.0.1:1/fail1".to_string(),
|
|
||||||
"http://127.0.0.1:1/fail2".to_string(),
|
|
||||||
],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
|
||||||
};
|
|
||||||
let mut shown_primary = false;
|
|
||||||
let mut shown_secondary = false;
|
|
||||||
let result = ddns
|
|
||||||
.try_trace_urls(&ddns.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
|
|
||||||
.await;
|
|
||||||
assert!(result.is_none());
|
|
||||||
assert!(shown_primary);
|
|
||||||
assert!(shown_secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_legacy_cf_api_get_success() {
|
async fn test_legacy_cf_api_get_success() {
|
||||||
let server = MockServer::start().await;
|
let server = MockServer::start().await;
|
||||||
@@ -1741,8 +1645,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let entry = crate::config::LegacyCloudflareEntry {
|
let entry = crate::config::LegacyCloudflareEntry {
|
||||||
@@ -1775,8 +1677,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let entry = crate::config::LegacyCloudflareEntry {
|
let entry = crate::config::LegacyCloudflareEntry {
|
||||||
@@ -1806,8 +1706,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let entry = crate::config::LegacyCloudflareEntry {
|
let entry = crate::config::LegacyCloudflareEntry {
|
||||||
@@ -1830,8 +1728,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: "http://localhost".to_string(),
|
cf_api_base: "http://localhost".to_string(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let entry = crate::config::LegacyCloudflareEntry {
|
let entry = crate::config::LegacyCloudflareEntry {
|
||||||
@@ -1862,8 +1758,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let entry = crate::config::LegacyCloudflareEntry {
|
let entry = crate::config::LegacyCloudflareEntry {
|
||||||
@@ -1884,75 +1778,6 @@ mod tests {
|
|||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_legacy_get_ips_ipv4_enabled() {
|
|
||||||
let server = MockServer::start().await;
|
|
||||||
Mock::given(method("GET"))
|
|
||||||
.and(path("/trace"))
|
|
||||||
.respond_with(
|
|
||||||
ResponseTemplate::new(200)
|
|
||||||
.set_body_string("ip=198.51.100.42\n"),
|
|
||||||
)
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let ddns = LegacyDdnsClient {
|
|
||||||
client: Client::new(),
|
|
||||||
cf_api_base: server.uri(),
|
|
||||||
ipv4_urls: vec![format!("{}/trace", server.uri())],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
|
||||||
};
|
|
||||||
let mut warnings = LegacyWarningState::default();
|
|
||||||
let config: Vec<crate::config::LegacyCloudflareEntry> = vec![];
|
|
||||||
let ips = ddns.get_ips(true, false, false, &config, &mut warnings).await;
|
|
||||||
assert!(ips.contains_key("ipv4"));
|
|
||||||
assert_eq!(ips["ipv4"].ip, "198.51.100.42");
|
|
||||||
assert_eq!(ips["ipv4"].record_type, "A");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_legacy_get_ips_ipv6_enabled() {
|
|
||||||
let server = MockServer::start().await;
|
|
||||||
Mock::given(method("GET"))
|
|
||||||
.and(path("/trace6"))
|
|
||||||
.respond_with(
|
|
||||||
ResponseTemplate::new(200)
|
|
||||||
.set_body_string("ip=2001:db8::1\n"),
|
|
||||||
)
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let ddns = LegacyDdnsClient {
|
|
||||||
client: Client::new(),
|
|
||||||
cf_api_base: server.uri(),
|
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![format!("{}/trace6", server.uri())],
|
|
||||||
dry_run: false,
|
|
||||||
};
|
|
||||||
let mut warnings = LegacyWarningState::default();
|
|
||||||
let config: Vec<crate::config::LegacyCloudflareEntry> = vec![];
|
|
||||||
let ips = ddns.get_ips(false, true, false, &config, &mut warnings).await;
|
|
||||||
assert!(ips.contains_key("ipv6"));
|
|
||||||
assert_eq!(ips["ipv6"].ip, "2001:db8::1");
|
|
||||||
assert_eq!(ips["ipv6"].record_type, "AAAA");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_legacy_get_ips_both_disabled() {
|
|
||||||
let ddns = LegacyDdnsClient {
|
|
||||||
client: Client::new(),
|
|
||||||
cf_api_base: String::new(),
|
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
|
||||||
};
|
|
||||||
let mut warnings = LegacyWarningState::default();
|
|
||||||
let config: Vec<crate::config::LegacyCloudflareEntry> = vec![];
|
|
||||||
let ips = ddns.get_ips(false, false, false, &config, &mut warnings).await;
|
|
||||||
assert!(ips.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_legacy_commit_record_creates_new() {
|
async fn test_legacy_commit_record_creates_new() {
|
||||||
let server = MockServer::start().await;
|
let server = MockServer::start().await;
|
||||||
@@ -1989,8 +1814,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let ip = LegacyIpInfo {
|
let ip = LegacyIpInfo {
|
||||||
@@ -2047,8 +1870,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let ip = LegacyIpInfo {
|
let ip = LegacyIpInfo {
|
||||||
@@ -2091,8 +1912,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: true,
|
dry_run: true,
|
||||||
};
|
};
|
||||||
let ip = LegacyIpInfo {
|
let ip = LegacyIpInfo {
|
||||||
@@ -2144,8 +1963,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let ip = LegacyIpInfo {
|
let ip = LegacyIpInfo {
|
||||||
@@ -2201,8 +2018,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let ip = LegacyIpInfo {
|
let ip = LegacyIpInfo {
|
||||||
@@ -2252,8 +2067,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let mut ips = HashMap::new();
|
let mut ips = HashMap::new();
|
||||||
@@ -2298,8 +2111,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
};
|
};
|
||||||
let config = vec![crate::config::LegacyCloudflareEntry {
|
let config = vec![crate::config::LegacyCloudflareEntry {
|
||||||
@@ -2332,8 +2143,6 @@ mod tests {
|
|||||||
let ddns = LegacyDdnsClient {
|
let ddns = LegacyDdnsClient {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
cf_api_base: server.uri(),
|
cf_api_base: server.uri(),
|
||||||
ipv4_urls: vec![],
|
|
||||||
ipv6_urls: vec![],
|
|
||||||
dry_run: true,
|
dry_run: true,
|
||||||
};
|
};
|
||||||
let config = vec![crate::config::LegacyCloudflareEntry {
|
let config = vec![crate::config::LegacyCloudflareEntry {
|
||||||
@@ -2349,14 +2158,6 @@ mod tests {
|
|||||||
ddns.delete_entries("A", &config).await;
|
ddns.delete_entries("A", &config).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_legacy_warning_state_default() {
|
|
||||||
let w = LegacyWarningState::default();
|
|
||||||
assert!(!w.shown_ipv4);
|
|
||||||
assert!(!w.shown_ipv4_secondary);
|
|
||||||
assert!(!w.shown_ipv6);
|
|
||||||
assert!(!w.shown_ipv6_secondary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy types for backwards compatibility
|
// Legacy types for backwards compatibility
|
||||||
|
|||||||
Reference in New Issue
Block a user