mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-03-22 06:58:57 -03:00
Compare commits
146 Commits
latest
...
f0d9510fff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0d9510fff | ||
|
|
4ea9ba5745 | ||
|
|
9a295bbf91 | ||
|
|
fecf30cd2a | ||
|
|
f7d1ff8687 | ||
|
|
fa398b83fc | ||
|
|
9eb395031e | ||
|
|
a8a7ed1e5f | ||
|
|
060257fe12 | ||
|
|
4be08d8811 | ||
|
|
0ca623329a | ||
|
|
d3fe3940f9 | ||
|
|
fa79547f9b | ||
|
|
6e92fc0d09 | ||
|
|
82b97f9cda | ||
|
|
190b90f769 | ||
|
|
fff882be11 | ||
|
|
713f0de5b0 | ||
|
|
414ef99f96 | ||
|
|
ed65aff55f | ||
|
|
cb7b1804cf | ||
|
|
c135a7d343 | ||
|
|
af347f89b9 | ||
|
|
9824815e12 | ||
|
|
0dbd2f7c2b | ||
|
|
e913d94eb8 | ||
|
|
83fa74831e | ||
|
|
f22ec89f3e | ||
|
|
bd3f4a94cb | ||
|
|
7212161f7b | ||
|
|
2ad7e57d65 | ||
|
|
5c909e25cd | ||
|
|
2c2e929d17 | ||
|
|
d92976993d | ||
|
|
a1fa3b9714 | ||
|
|
7e6d74f1f6 | ||
|
|
9855ca6249 | ||
|
|
e0f0280656 | ||
|
|
e86695f77d | ||
|
|
b0a396b8f1 | ||
|
|
c648b81b25 | ||
|
|
ceeb011366 | ||
|
|
f0357c71c1 | ||
|
|
6933cbe27f | ||
|
|
566ad3a7cf | ||
|
|
3287447e0a | ||
|
|
62c360cff2 | ||
|
|
ae7be14004 | ||
|
|
cb539ad64d | ||
|
|
a4d29036c5 | ||
|
|
ef4e3a5787 | ||
|
|
2b9ebdeab2 | ||
|
|
86976e5133 | ||
|
|
2401e7a995 | ||
|
|
8acd8e5f59 | ||
|
|
0e0e9f9989 | ||
|
|
464d2792b1 | ||
|
|
a9d25c743a | ||
|
|
254e978971 | ||
|
|
bf6135739d | ||
|
|
bc06202b35 | ||
|
|
eebbcfbbdf | ||
|
|
2a4d9530dd | ||
|
|
6587d86c65 | ||
|
|
6e68d2623f | ||
|
|
ffa4963ddd | ||
|
|
def75e282d | ||
|
|
870da367a9 | ||
|
|
571a22ac22 | ||
|
|
5136c925d2 | ||
|
|
96527aaab2 | ||
|
|
f7d2e7dc00 | ||
|
|
4e4d3cebf1 | ||
|
|
01993807a9 | ||
|
|
0d9a9a0579 | ||
|
|
0a85b04287 | ||
|
|
1a6ffc9681 | ||
|
|
458559d52c | ||
|
|
1f6daa5968 | ||
|
|
c34401c43f | ||
|
|
9a8d7d57e1 | ||
|
|
04d87d3aa6 | ||
|
|
bdf8c75cad | ||
|
|
6fe23a2aee | ||
|
|
47ae1238e2 | ||
|
|
55b705072a | ||
|
|
d3cc054b03 | ||
|
|
6b25c64846 | ||
|
|
378c600084 | ||
|
|
975fba4d42 | ||
|
|
3cd26feb03 | ||
|
|
1ca225b85c | ||
|
|
80bd7801fe | ||
|
|
000c833f43 | ||
|
|
29771030b1 | ||
|
|
3753542dce | ||
|
|
c34ba8e94c | ||
|
|
6be8add640 | ||
|
|
0f3708a482 | ||
|
|
8c55892f32 | ||
|
|
86c935dea7 | ||
|
|
27ccdd0203 | ||
|
|
a816fb6c3f | ||
|
|
e129789a85 | ||
|
|
4ffbb98f29 | ||
|
|
6140917119 | ||
|
|
d763be7931 | ||
|
|
839ffe2551 | ||
|
|
16352e4543 | ||
|
|
65d8c44ec3 | ||
|
|
de4e2ac5b6 | ||
|
|
efefa0ae7a | ||
|
|
748170926c | ||
|
|
0ca979f91d | ||
|
|
3b92c57a75 | ||
|
|
db5edef4f0 | ||
|
|
1235464e18 | ||
|
|
58c69e2c5f | ||
|
|
3e1fcb13f3 | ||
|
|
2b67615330 | ||
|
|
344b056a6d | ||
|
|
18ad6c6bc4 | ||
|
|
bc837c61a0 | ||
|
|
f63b0f13fc | ||
|
|
cbfd628f22 | ||
|
|
3f2346db6f | ||
|
|
a8be42292b | ||
|
|
f77a72f4e3 | ||
|
|
a633478239 | ||
|
|
96f781f8b3 | ||
|
|
242575d7aa | ||
|
|
2ad3d6b564 | ||
|
|
d6d3cb54d2 | ||
|
|
142fbaa8ba | ||
|
|
fa56332d18 | ||
|
|
78042582bb | ||
|
|
96d92accaa | ||
|
|
18654798e0 | ||
|
|
1e14700d4e | ||
|
|
3b9a961f61 | ||
|
|
bd15e6f117 | ||
|
|
5ac69b8274 | ||
|
|
e2deea1d6e | ||
|
|
ddc84cec96 | ||
|
|
33334a529f | ||
|
|
86499b038a |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,5 +1,3 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: [timothymiller]
|
github: [timothymiller]
|
||||||
patreon: timknowsbest
|
|
||||||
custom: ['https://timknowsbest.com/donate']
|
|
||||||
|
|||||||
16
.github/dependabot.yml
vendored
Normal file
16
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: 'pip'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
|
|
||||||
|
- package-ecosystem: 'docker'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
|
|
||||||
|
- package-ecosystem: 'github-actions'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
54
.github/workflows/image.yml
vendored
Normal file
54
.github/workflows/image.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: Build cloudflare-ddns Docker image (multi-arch)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
# https://github.com/docker/setup-qemu-action
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
# https://github.com/docker/setup-buildx-action
|
||||||
|
- name: Setting up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract branch name
|
||||||
|
shell: bash
|
||||||
|
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
|
||||||
|
id: extract_branch
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
with:
|
||||||
|
images: timothyjmiller/cloudflare-ddns
|
||||||
|
sep-tags: ','
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
|
tags: |
|
||||||
|
type=raw,enable=${{ steps.extract_branch.outputs.branch == 'master' }},value=latest
|
||||||
|
type=schedule
|
||||||
|
type=ref,event=pr
|
||||||
|
|
||||||
|
- name: Build and publish
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
platforms: linux/ppc64le,linux/s390x,linux/386,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/amd64
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.source=${{ github.event.repository.html_url }}
|
||||||
|
org.opencontainers.image.created=${{ steps.meta.outputs.created }}
|
||||||
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -57,4 +57,7 @@ env/
|
|||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# Git History
|
||||||
|
**/.history/*
|
||||||
|
|||||||
40
.vscode/settings.json
vendored
40
.vscode/settings.json
vendored
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/.git": true,
|
"**/.git": true,
|
||||||
"**/.svn": true,
|
"**/.svn": true,
|
||||||
"**/.hg": true,
|
"**/.hg": true,
|
||||||
"**/CVS": true,
|
"**/CVS": true,
|
||||||
"**/.DS_Store": true,
|
"**/.DS_Store": true,
|
||||||
".github": true,
|
"**/Thumbs.db": true,
|
||||||
".vscode": true,
|
".github": true,
|
||||||
"LICENSE": true,
|
".gitignore": true,
|
||||||
"requirements.txt": true,
|
".vscode": true,
|
||||||
"build-docker-image.sh": false,
|
"Dockerfile": true,
|
||||||
".gitignore": true,
|
"LICENSE": true,
|
||||||
"Dockerfile": false,
|
"requirements.txt": true,
|
||||||
"start-sync.sh": false,
|
"venv": true
|
||||||
"venv": true
|
},
|
||||||
},
|
"explorerExclude.backup": {},
|
||||||
"explorerExclude.backup": null,
|
"python.linting.pylintEnabled": true,
|
||||||
"python.linting.pylintEnabled": true,
|
"python.linting.enabled": true,
|
||||||
"python.linting.enabled": true
|
"python.formatting.provider": "autopep8"
|
||||||
}
|
}
|
||||||
|
|||||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
11
Dockerfile
11
Dockerfile
@@ -6,12 +6,13 @@ FROM python:alpine AS base
|
|||||||
FROM base AS dependencies
|
FROM base AS dependencies
|
||||||
# install dependencies
|
# install dependencies
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install --user -r requirements.txt
|
||||||
|
|
||||||
#
|
#
|
||||||
# ---- Release ----
|
# ---- Release ----
|
||||||
FROM dependencies AS release
|
FROM base AS release
|
||||||
# copy project file(s)
|
# copy installed dependencies and project source file(s)
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
COPY --from=dependencies /root/.local /root/.local
|
||||||
COPY cloudflare-ddns.py .
|
COPY cloudflare-ddns.py .
|
||||||
CMD ["python", "/cloudflare-ddns.py", "--repeat"]
|
CMD ["python", "-u", "/cloudflare-ddns.py", "--repeat"]
|
||||||
|
|||||||
373
README.md
373
README.md
@@ -1,12 +1,37 @@
|
|||||||
# :rocket: Cloudflare DDNS
|
<p align="center"><a href="https://timknowsbest.com/free-dynamic-dns" target="_blank" rel="noopener noreferrer"><img width="1024" src="feature-graphic.jpg" alt="Cloudflare DDNS"/></a></p>
|
||||||
|
|
||||||
Dynamic DNS service based on Cloudflare! Access your home network remotely via a custom domain name without a static IP!
|
# 🚀 Cloudflare DDNS
|
||||||
|
|
||||||
## :us: Origin
|
Access your home network remotely via a custom domain name without a static IP!
|
||||||
|
|
||||||
This script was written for the Raspberry Pi platform to enable low cost, simple self hosting to promote a more decentralized internet. On execution, the script fetches public IPv4 and IPv6 addresses and creates/updates DNS records for the subdomains in Cloudflare. Stale, duplicate DNS records are removed for housekeeping.
|
## ⚡ Efficiency
|
||||||
|
|
||||||
## :vertical_traffic_light: Getting Started
|
- ❤️ Easy config. List your domains and you're done.
|
||||||
|
- 🔁 The Python runtime will re-use existing HTTP connections.
|
||||||
|
- 🗃️ Cloudflare API responses are cached to reduce API usage.
|
||||||
|
- 🤏 The Docker image is small and efficient.
|
||||||
|
- 0️⃣ Zero dependencies.
|
||||||
|
- 💪 Supports all platforms.
|
||||||
|
- 🏠 Enables low cost self hosting to promote a more decentralized internet.
|
||||||
|
- 🔒 Zero-log IP provider ([cdn-cgi/trace](https://www.cloudflare.com/cdn-cgi/trace))
|
||||||
|
- 👐 GPL-3.0 License. Open source for open audits.
|
||||||
|
|
||||||
|
## 💯 Complete Support of Domain Names, Subdomains, IPv4 & IPv6, and Load Balancing
|
||||||
|
|
||||||
|
- 🌐 Supports multiple domains (zones) on the same IP.
|
||||||
|
- 📠 Supports multiple subdomains on the same IP.
|
||||||
|
- 📡 IPv4 and IPv6 support.
|
||||||
|
- 🌍 Supports all Cloudflare regions.
|
||||||
|
- ⚖️ Supports [Cloudflare Load Balancing](https://developers.cloudflare.com/load-balancing/understand-basics/pools/).
|
||||||
|
- 🇺🇸 Made in the U.S.A.
|
||||||
|
|
||||||
|
## 📊 Stats
|
||||||
|
|
||||||
|
| Size | Downloads | Discord |
|
||||||
|
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| [](https://hub.docker.com/r/timothyjmiller/cloudflare-ddns 'cloudflare-ddns docker image size') | [](https://hub.docker.com/r/timothyjmiller/cloudflare-ddns 'Total DockerHub pulls') | [](https://discord.gg/UgGmwMvNxm 'Official Discord Server') |
|
||||||
|
|
||||||
|
## 🚦 Getting Started
|
||||||
|
|
||||||
First copy the example configuration file into the real one.
|
First copy the example configuration file into the real one.
|
||||||
|
|
||||||
@@ -16,17 +41,19 @@ cp config-example.json config.json
|
|||||||
|
|
||||||
Edit `config.json` and replace the values with your own.
|
Edit `config.json` and replace the values with your own.
|
||||||
|
|
||||||
### Authentication methods
|
### 🔑 Authentication methods
|
||||||
|
|
||||||
You can choose to use either the newer API tokens, or the traditional API keys
|
You can choose to use either the newer API tokens, or the traditional API keys
|
||||||
|
|
||||||
To generate a new API tokens, go to https://dash.cloudflare.com/profile/api-tokens and create a token capable of **Edit DNS**. Then replace the value in
|
To generate a new API tokens, go to your [Cloudflare Profile](https://dash.cloudflare.com/profile/api-tokens) and create a token capable of **Edit DNS**. Then replace the value in
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"authentication":
|
"authentication":
|
||||||
"api_token": "Your cloudflare API token, including the capability of **Edit DNS**"
|
"api_token": "Your cloudflare API token, including the capability of **Edit DNS**"
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can use the traditional API keys by setting appropriate values for:
|
Alternatively, you can use the traditional API keys by setting appropriate values for:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"authentication":
|
"authentication":
|
||||||
"api_key":
|
"api_key":
|
||||||
@@ -34,40 +61,249 @@ Alternatively, you can use the traditional API keys by setting appropriate value
|
|||||||
"account_email": "The email address you use to sign in to cloudflare",
|
"account_email": "The email address you use to sign in to cloudflare",
|
||||||
```
|
```
|
||||||
|
|
||||||
### Other values explained
|
### 📍 Enable or disable IPv4 or IPv6
|
||||||
|
|
||||||
|
Some ISP provided modems only allow port forwarding over IPv4 or IPv6. In this case, you would want to disable any interface not accessible via port forward.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"a": true,
|
||||||
|
"aaaa": true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎛️ Other values explained
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"zone_id": "The ID of the zone that will get the records. From your dashboard click into the zone. Under the overview tab, scroll down and the zone ID is listed in the right rail",
|
"zone_id": "The ID of the zone that will get the records. From your dashboard click into the zone. Under the overview tab, scroll down and the zone ID is listed in the right rail",
|
||||||
"subdomains": "Array of subdomains you want to update the A & where applicable, AAAA records. IMPORTANT! Only write subdomain name. Do not include the base domain name. (e.g. foo or an empty string to update the base domain)",
|
"subdomains": "Array of subdomains you want to update the A & where applicable, AAAA records. IMPORTANT! Only write subdomain name. Do not include the base domain name. (e.g. foo or an empty string to update the base domain)",
|
||||||
"proxied": false (defaults to false. Make it true if you want CDN/SSL benefits from cloudflare. This usually disables SSH)
|
"proxied": "Defaults to false. Make it true if you want CDN/SSL benefits from cloudflare. This usually disables SSH)",
|
||||||
|
"ttl": "Defaults to 300 seconds. Longer TTLs speed up DNS lookups by increasing the chance of cached results, but a longer TTL also means that updates to your records take longer to go into effect. You can choose a TTL between 30 seconds and 1 day. For more information, see [Cloudflare's TTL documentation](https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/)",
|
||||||
```
|
```
|
||||||
|
|
||||||
## :fax: Hosting multiple domains on the same IP?
|
## 📠 Hosting multiple subdomains on the same IP?
|
||||||
You can save yourself some trouble when hosting multiple domains pointing to the same IP address (in the case of Traefik) by defining one A & AAAA record 'ddns.example.com' pointing to the IP of the server that will be updated by this DDNS script. For each subdomain, create a CNAME record pointing to 'ddns.example.com'. Now you don't have to manually modify the script config every time you add a new subdomain to your site!
|
|
||||||
|
|
||||||
## :whale: Deploy with Docker Compose
|
This script can be used to update multiple subdomains on the same IP address.
|
||||||
|
|
||||||
Precompiled images are available via the official docker container [on DockerHub](https://hub.docker.com/r/timothyjmiller/cloudflare-ddns).
|
For example, if you have a domain `example.com` and you want to host additional subdomains at `foo.example.com` and `bar.example.com` on the same IP address, you can use this script to update the DNS records for all subdomains.
|
||||||
|
|
||||||
|
### ⚠️ Note
|
||||||
|
|
||||||
|
Please remove the comments after `//` in the below example. They are only there to explain the config.
|
||||||
|
|
||||||
|
Do not include the base domain name in your `subdomains` config. Do not use the [FQDN](https://en.wikipedia.org/wiki/Fully_qualified_domain_name).
|
||||||
|
|
||||||
|
### 👉 Example 🚀
|
||||||
|
|
||||||
|
```bash
|
||||||
|
{
|
||||||
|
"cloudflare": [
|
||||||
|
{
|
||||||
|
"authentication": {
|
||||||
|
"api_token": "api_token_here", // Either api_token or api_key
|
||||||
|
"api_key": {
|
||||||
|
"api_key": "api_key_here",
|
||||||
|
"account_email": "your_email_here"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zone_id": "your_zone_id_here",
|
||||||
|
"subdomains": [
|
||||||
|
{
|
||||||
|
"name": "", // Root domain (example.com)
|
||||||
|
"proxied": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "foo", // (foo.example.com)
|
||||||
|
"proxied": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bar", // (bar.example.com)
|
||||||
|
"proxied": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a": true,
|
||||||
|
"aaaa": true,
|
||||||
|
"purgeUnknownRecords": false,
|
||||||
|
"ttl": 300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Hosting multiple domains (zones) on the same IP?
|
||||||
|
|
||||||
|
You can handle ddns for multiple domains (cloudflare zones) using the same docker container by duplicating your configs inside the `cloudflare: []` key within `config.json` like below:
|
||||||
|
|
||||||
|
### ⚠️ Note:
|
||||||
|
|
||||||
|
If you are using API Tokens, make sure the token used supports editing your zone ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
{
|
||||||
|
"cloudflare": [
|
||||||
|
{
|
||||||
|
"authentication": {
|
||||||
|
"api_token": "api_token_here",
|
||||||
|
"api_key": {
|
||||||
|
"api_key": "api_key_here",
|
||||||
|
"account_email": "your_email_here"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zone_id": "your_first_zone_id_here",
|
||||||
|
"subdomains": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"proxied": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_or_replace_with_your_subdomain",
|
||||||
|
"proxied": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authentication": {
|
||||||
|
"api_token": "api_token_here",
|
||||||
|
"api_key": {
|
||||||
|
"api_key": "api_key_here",
|
||||||
|
"account_email": "your_email_here"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zone_id": "your_second_zone_id_here",
|
||||||
|
"subdomains": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"proxied": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_or_replace_with_your_subdomain",
|
||||||
|
"proxied": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a": true,
|
||||||
|
"aaaa": true,
|
||||||
|
"purgeUnknownRecords": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚖️ Load Balancing
|
||||||
|
|
||||||
|
If you have multiple IP addresses and want to load balance between them, you can use the `loadBalancing` option. This will create a CNAME record for each subdomain that points to the subdomain with the lowest IP address.
|
||||||
|
|
||||||
|
### 📜 Example config to support load balancing
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cloudflare": [
|
||||||
|
{
|
||||||
|
"authentication": {
|
||||||
|
"api_token": "api_token_here",
|
||||||
|
"api_key": {
|
||||||
|
"api_key": "api_key_here",
|
||||||
|
"account_email": "your_email_here"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zone_id": "your_zone_id_here",
|
||||||
|
"subdomains": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"proxied": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_or_replace_with_your_subdomain",
|
||||||
|
"proxied": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],{
|
||||||
|
"cloudflare": [
|
||||||
|
{
|
||||||
|
"authentication": {
|
||||||
|
"api_token": "api_token_here",
|
||||||
|
"api_key": {
|
||||||
|
"api_key": "api_key_here",
|
||||||
|
"account_email": "your_email_here"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zone_id": "your_zone_id_here",
|
||||||
|
"subdomains": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"proxied": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_or_replace_with_your_subdomain",
|
||||||
|
"proxied": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"load_balancer": [
|
||||||
|
{
|
||||||
|
"authentication": {
|
||||||
|
"api_token": "api_token_here",
|
||||||
|
"api_key": {
|
||||||
|
"api_key": "api_key_here",
|
||||||
|
"account_email": "your_email_here"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pool_id": "your_pool_id_here",
|
||||||
|
"origin": "your_origin_name_here"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a": true,
|
||||||
|
"aaaa": true,
|
||||||
|
"purgeUnknownRecords": false,
|
||||||
|
"ttl": 300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker environment variable support
|
||||||
|
|
||||||
|
Define environmental variables starts with `CF_DDNS_` and use it in config.json
|
||||||
|
|
||||||
|
For ex:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cloudflare": [
|
||||||
|
{
|
||||||
|
"authentication": {
|
||||||
|
"api_token": "${CF_DDNS_API_TOKEN}",
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🧹 Optional features
|
||||||
|
|
||||||
|
`purgeUnknownRecords` removes stale DNS records from Cloudflare. This is useful if you have a dynamic DNS record that you no longer want to use. If you have a dynamic DNS record that you no longer want to use, you can set `purgeUnknownRecords` to `true` and the script will remove the stale DNS record from Cloudflare.
|
||||||
|
|
||||||
|
## 🐳 Deploy with Docker Compose
|
||||||
|
|
||||||
|
Pre-compiled images are available via [the official docker container on DockerHub](https://hub.docker.com/r/timothyjmiller/cloudflare-ddns).
|
||||||
|
|
||||||
Modify the host file path of config.json inside the volumes section of docker-compose.yml.
|
Modify the host file path of config.json inside the volumes section of docker-compose.yml.
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
version: "3.7"
|
version: '3.9'
|
||||||
services:
|
services:
|
||||||
cloudflare-ddns:
|
cloudflare-ddns:
|
||||||
image: timothymiller/cloudflare-ddns:latest
|
image: timothyjmiller/cloudflare-ddns:latest
|
||||||
container_name: cloudflare-ddns
|
container_name: cloudflare-ddns
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
network_mode: 'host'
|
||||||
environment:
|
environment:
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
- PGID=1000
|
- PGID=1000
|
||||||
volumes:
|
volumes:
|
||||||
- /EDIT/YOUR/PATH/HERE/config.json:/config.json
|
- /YOUR/PATH/HERE/config.json:/config.json
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
### :running: Running
|
### ⚠️ IPv6
|
||||||
|
|
||||||
|
Docker requires network_mode be set to host in order to access the IPv6 public address.
|
||||||
|
|
||||||
|
### 🏃♂️ Running
|
||||||
|
|
||||||
From the project root directory
|
From the project root directory
|
||||||
|
|
||||||
@@ -75,31 +311,42 @@ From the project root directory
|
|||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Building from source
|
## 🐋 Kubernetes
|
||||||
|
|
||||||
Create a config.json file with your production credentials.
|
Create config File
|
||||||
|
|
||||||
Give build-docker-image.sh permission to execute.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo chmod +x ./build-docker-image.sh
|
cp ../../config-example.json config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
At project root, run the build-docker-image.sh script.
|
Edit config.jsonon (vim, nvim, nano... )
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build-docker-image.sh
|
${EDITOR} config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Run the locally compiled version
|
Create config file as Secret.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d timothyjmiller/cloudflare_ddns:latest
|
kubectl create secret generic config-cloudflare-ddns --from-file=config.json --dry-run=client -oyaml -n ddns > config-cloudflare-ddns-Secret.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
## :penguin: (legacy) Linux + cron instructions (all distros)
|
apply this secret
|
||||||
|
|
||||||
### :running: Running
|
```bash
|
||||||
|
kubectl apply -f config-cloudflare-ddns-Secret.yaml
|
||||||
|
rm config.json # recomended Just keep de secret on Kubernetes Cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
apply this Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f cloudflare-ddns-Deployment.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐧 Deploy with Linux + Cron
|
||||||
|
|
||||||
|
### 🏃 Running (all distros)
|
||||||
|
|
||||||
This script requires Python 3.5+, which comes preinstalled on the latest version of Raspbian. Download/clone this repo and give permission to the project's bash script by running `chmod +x ./start-sync.sh`. Now you can execute `./start-sync.sh`, which will set up a virtualenv, pull in any dependencies, and fire the script.
|
This script requires Python 3.5+, which comes preinstalled on the latest version of Raspbian. Download/clone this repo and give permission to the project's bash script by running `chmod +x ./start-sync.sh`. Now you can execute `./start-sync.sh`, which will set up a virtualenv, pull in any dependencies, and fire the script.
|
||||||
|
|
||||||
@@ -117,16 +364,74 @@ crontab -e
|
|||||||
*/15 * * * * /home/your_username_here/cloudflare-ddns/start-sync.sh
|
*/15 * * * * /home/your_username_here/cloudflare-ddns/start-sync.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
Create a config.json file with your production credentials.
|
||||||
|
|
||||||
|
### 💖 Please Note
|
||||||
|
|
||||||
|
The optional `docker-build-all.sh` script requires Docker experimental support to be enabled.
|
||||||
|
|
||||||
|
Docker Hub has experimental support for multi-architecture builds. Their official blog post specifies easy instructions for building with [Mac and Windows versions of Docker Desktop](https://docs.docker.com/docker-for-mac/multi-arch/).
|
||||||
|
|
||||||
|
1. Choose build platform
|
||||||
|
|
||||||
|
- Multi-architecture (experimental) `docker-build-all.sh`
|
||||||
|
|
||||||
|
- Linux/amd64 by default `docker-build.sh`
|
||||||
|
|
||||||
|
2. Give your bash script permission to execute.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chmod +x ./docker-build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chmod +x ./docker-build-all.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. At project root, run the `docker-build.sh` script.
|
||||||
|
|
||||||
|
Recommended for local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./docker-build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended for production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./docker-build-all.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the locally compiled version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d timothyjmiller/cloudflare_ddns:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Platforms
|
||||||
|
|
||||||
|
- [Docker](https://docs.docker.com/get-docker/)
|
||||||
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
|
- [Kubernetes](https://kubernetes.io/docs/tasks/tools/)
|
||||||
|
- [Python 3](https://www.python.org/downloads/)
|
||||||
|
- [Systemd](https://www.freedesktop.org/wiki/Software/systemd/)
|
||||||
|
|
||||||
|
## 📜 Helpful links
|
||||||
|
|
||||||
|
- [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens)
|
||||||
|
- [Cloudflare zone ID](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-IP-address-)
|
||||||
|
- [Cloudflare zone DNS record ID](https://support.cloudflare.com/hc/en-us/articles/360019093151-Managing-DNS-records-in-Cloudflare)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This Template is licensed under the GNU General Public License, version 3 (GPLv3) and is distributed free of charge.
|
This Template is licensed under the GNU General Public License, version 3 (GPLv3).
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
Timothy Miller
|
Timothy Miller
|
||||||
|
|
||||||
GitHub: https://github.com/timothymiller 💡
|
[View my GitHub profile 💡](https://github.com/timothymiller)
|
||||||
|
|
||||||
Website: https://timknowsbest.com 💻
|
[View my personal website 💻](https://timknowsbest.com)
|
||||||
|
|
||||||
Donation: https://timknowsbest.com/donate 💸
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
docker build -t timothyjmiller/cloudflare-ddns:latest .
|
|
||||||
@@ -1,127 +1,319 @@
|
|||||||
import requests, json, sys, os
|
#!/usr/bin/env python3
|
||||||
import time, traceback
|
# cloudflare-ddns.py
|
||||||
|
# Summary: Access your home network remotely via a custom domain name without a static IP!
|
||||||
|
# Description: Access your home network remotely via a custom domain
|
||||||
|
# Access your home network remotely via a custom domain
|
||||||
|
# A small, 🕵️ privacy centric, and ⚡
|
||||||
|
# lightning fast multi-architecture Docker image for self hosting projects.
|
||||||
|
|
||||||
PATH = os.getcwd() + "/"
|
__version__ = "1.0.2"
|
||||||
version = float(str(sys.version_info[0]) + "." + str(sys.version_info[1]))
|
|
||||||
|
|
||||||
if(version < 3.5):
|
from string import Template
|
||||||
raise Exception("This script requires Python 3.5+")
|
|
||||||
|
|
||||||
with open(PATH + "config.json") as config_file:
|
import json
|
||||||
config = json.loads(config_file.read())
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
CONFIG_PATH = os.environ.get('CONFIG_PATH', os.getcwd())
|
||||||
|
# Read in all environment variables that have the correct prefix
|
||||||
|
ENV_VARS = {key: value for (key, value) in os.environ.items() if key.startswith('CF_DDNS_')}
|
||||||
|
|
||||||
|
class GracefulExit:
|
||||||
|
def __init__(self):
|
||||||
|
self.kill_now = threading.Event()
|
||||||
|
signal.signal(signal.SIGINT, self.exit_gracefully)
|
||||||
|
signal.signal(signal.SIGTERM, self.exit_gracefully)
|
||||||
|
|
||||||
|
def exit_gracefully(self, signum, frame):
|
||||||
|
print("🛑 Stopping main thread...")
|
||||||
|
self.kill_now.set()
|
||||||
|
|
||||||
|
|
||||||
|
def deleteEntries(type):
|
||||||
|
# Helper function for deleting A or AAAA records
|
||||||
|
# in the case of no IPv4 or IPv6 connection, yet
|
||||||
|
# existing A or AAAA records are found.
|
||||||
|
for option in config["cloudflare"]:
|
||||||
|
answer = cf_api(
|
||||||
|
"zones/" + option['zone_id'] +
|
||||||
|
"/dns_records?per_page=100&type=" + type,
|
||||||
|
"GET", option)
|
||||||
|
if answer is None or answer["result"] is None:
|
||||||
|
time.sleep(5)
|
||||||
|
return
|
||||||
|
for record in answer["result"]:
|
||||||
|
identifier = str(record["id"])
|
||||||
|
cf_api(
|
||||||
|
"zones/" + option['zone_id'] + "/dns_records/" + identifier,
|
||||||
|
"DELETE", option)
|
||||||
|
print("🗑️ Deleted stale record " + identifier)
|
||||||
|
|
||||||
|
|
||||||
def getIPs():
|
def getIPs():
|
||||||
a = requests.get("https://api.ipify.org?format=json").json().get("ip")
|
a = None
|
||||||
aaaa = requests.get("https://api6.ipify.org?format=json").json().get("ip")
|
aaaa = None
|
||||||
ips = []
|
global ipv4_enabled
|
||||||
|
global ipv6_enabled
|
||||||
if(a.find(".") > -1):
|
global purgeUnknownRecords
|
||||||
ips.append({
|
if ipv4_enabled:
|
||||||
|
try:
|
||||||
|
a = requests.get(
|
||||||
|
"https://1.1.1.1/cdn-cgi/trace").text.split("\n")
|
||||||
|
a.pop()
|
||||||
|
a = dict(s.split("=") for s in a)["ip"]
|
||||||
|
except Exception:
|
||||||
|
global shown_ipv4_warning
|
||||||
|
if not shown_ipv4_warning:
|
||||||
|
shown_ipv4_warning = True
|
||||||
|
print("🧩 IPv4 not detected via 1.1.1.1, trying 1.0.0.1")
|
||||||
|
# Try secondary IP check
|
||||||
|
try:
|
||||||
|
a = requests.get(
|
||||||
|
"https://1.0.0.1/cdn-cgi/trace").text.split("\n")
|
||||||
|
a.pop()
|
||||||
|
a = dict(s.split("=") for s in a)["ip"]
|
||||||
|
except Exception:
|
||||||
|
global shown_ipv4_warning_secondary
|
||||||
|
if not shown_ipv4_warning_secondary:
|
||||||
|
shown_ipv4_warning_secondary = True
|
||||||
|
print("🧩 IPv4 not detected via 1.0.0.1. Verify your ISP or DNS provider isn't blocking Cloudflare's IPs.")
|
||||||
|
if purgeUnknownRecords:
|
||||||
|
deleteEntries("A")
|
||||||
|
if ipv6_enabled:
|
||||||
|
try:
|
||||||
|
aaaa = requests.get(
|
||||||
|
"https://[2606:4700:4700::1111]/cdn-cgi/trace").text.split("\n")
|
||||||
|
aaaa.pop()
|
||||||
|
aaaa = dict(s.split("=") for s in aaaa)["ip"]
|
||||||
|
except Exception:
|
||||||
|
global shown_ipv6_warning
|
||||||
|
if not shown_ipv6_warning:
|
||||||
|
shown_ipv6_warning = True
|
||||||
|
print("🧩 IPv6 not detected via 1.1.1.1, trying 1.0.0.1")
|
||||||
|
try:
|
||||||
|
aaaa = requests.get(
|
||||||
|
"https://[2606:4700:4700::1001]/cdn-cgi/trace").text.split("\n")
|
||||||
|
aaaa.pop()
|
||||||
|
aaaa = dict(s.split("=") for s in aaaa)["ip"]
|
||||||
|
except Exception:
|
||||||
|
global shown_ipv6_warning_secondary
|
||||||
|
if not shown_ipv6_warning_secondary:
|
||||||
|
shown_ipv6_warning_secondary = True
|
||||||
|
print("🧩 IPv6 not detected via 1.0.0.1. Verify your ISP or DNS provider isn't blocking Cloudflare's IPs.")
|
||||||
|
if purgeUnknownRecords:
|
||||||
|
deleteEntries("AAAA")
|
||||||
|
ips = {}
|
||||||
|
if (a is not None):
|
||||||
|
ips["ipv4"] = {
|
||||||
"type": "A",
|
"type": "A",
|
||||||
"ip": a
|
"ip": a
|
||||||
})
|
}
|
||||||
|
if (aaaa is not None):
|
||||||
if(aaaa.find(":") > -1):
|
ips["ipv6"] = {
|
||||||
ips.append({
|
|
||||||
"type": "AAAA",
|
"type": "AAAA",
|
||||||
"ip": aaaa
|
"ip": aaaa
|
||||||
})
|
}
|
||||||
|
|
||||||
return ips
|
return ips
|
||||||
|
|
||||||
|
|
||||||
def commitRecord(ip):
|
def commitRecord(ip):
|
||||||
stale_record_ids = []
|
global ttl
|
||||||
for c in config["cloudflare"]:
|
for option in config["cloudflare"]:
|
||||||
subdomains = c["subdomains"]
|
subdomains = option["subdomains"]
|
||||||
response = cf_api("zones/" + c['zone_id'], "GET", c)
|
response = cf_api("zones/" + option['zone_id'], "GET", option)
|
||||||
|
if response is None or response["result"]["name"] is None:
|
||||||
|
time.sleep(5)
|
||||||
|
return
|
||||||
base_domain_name = response["result"]["name"]
|
base_domain_name = response["result"]["name"]
|
||||||
for subdomain in subdomains:
|
for subdomain in subdomains:
|
||||||
exists = False
|
try:
|
||||||
|
name = subdomain["name"].lower().strip()
|
||||||
|
proxied = subdomain["proxied"]
|
||||||
|
except:
|
||||||
|
name = subdomain
|
||||||
|
proxied = option["proxied"]
|
||||||
|
fqdn = base_domain_name
|
||||||
|
# Check if name provided is a reference to the root domain
|
||||||
|
if name != '' and name != '@':
|
||||||
|
fqdn = name + "." + base_domain_name
|
||||||
record = {
|
record = {
|
||||||
"type": ip["type"],
|
"type": ip["type"],
|
||||||
"name": subdomain,
|
"name": fqdn,
|
||||||
"content": ip["ip"],
|
"content": ip["ip"],
|
||||||
"proxied": c["proxied"]
|
"proxied": proxied,
|
||||||
|
"ttl": ttl
|
||||||
}
|
}
|
||||||
list = cf_api(
|
dns_records = cf_api(
|
||||||
"zones/" + c['zone_id'] + "/dns_records?per_page=100&type=" + ip["type"], "GET", c)
|
"zones/" + option['zone_id'] +
|
||||||
|
"/dns_records?per_page=100&type=" + ip["type"],
|
||||||
full_subdomain = base_domain_name
|
"GET", option)
|
||||||
if subdomain:
|
identifier = None
|
||||||
full_subdomain = subdomain + "." + full_subdomain
|
modified = False
|
||||||
|
duplicate_ids = []
|
||||||
dns_id = ""
|
if dns_records is not None:
|
||||||
for r in list["result"]:
|
for r in dns_records["result"]:
|
||||||
if (r["name"] == full_subdomain):
|
if (r["name"] == fqdn):
|
||||||
exists = True
|
if identifier:
|
||||||
if (r["content"] != ip["ip"]):
|
if r["content"] == ip["ip"]:
|
||||||
if (dns_id == ""):
|
duplicate_ids.append(identifier)
|
||||||
dns_id = r["id"]
|
identifier = r["id"]
|
||||||
|
else:
|
||||||
|
duplicate_ids.append(r["id"])
|
||||||
else:
|
else:
|
||||||
stale_record_ids.append(r["id"])
|
identifier = r["id"]
|
||||||
if(exists == False):
|
if r['content'] != record['content'] or r['proxied'] != record['proxied']:
|
||||||
print("Adding new record " + str(record))
|
modified = True
|
||||||
|
if identifier:
|
||||||
|
if modified:
|
||||||
|
print("📡 Updating record " + str(record))
|
||||||
|
response = cf_api(
|
||||||
|
"zones/" + option['zone_id'] +
|
||||||
|
"/dns_records/" + identifier,
|
||||||
|
"PUT", option, {}, record)
|
||||||
|
else:
|
||||||
|
print("➕ Adding new record " + str(record))
|
||||||
response = cf_api(
|
response = cf_api(
|
||||||
"zones/" + c['zone_id'] + "/dns_records", "POST", c, {}, record)
|
"zones/" + option['zone_id'] + "/dns_records", "POST", option, {}, record)
|
||||||
elif(dns_id != ""):
|
if purgeUnknownRecords:
|
||||||
# Only update if the record content is different
|
for identifier in duplicate_ids:
|
||||||
print("Updating record " + str(record))
|
identifier = str(identifier)
|
||||||
response = cf_api(
|
print("🗑️ Deleting stale record " + identifier)
|
||||||
"zones/" + c['zone_id'] + "/dns_records/" + dns_id, "PUT", c, {}, record)
|
response = cf_api(
|
||||||
|
"zones/" + option['zone_id'] +
|
||||||
# Delete duplicate, stale records
|
"/dns_records/" + identifier,
|
||||||
for identifier in stale_record_ids:
|
"DELETE", option)
|
||||||
print("Deleting stale record " + str(identifier))
|
|
||||||
response = cf_api(
|
|
||||||
"zones/" + c['zone_id'] + "/dns_records/" + identifier, "DELETE", c)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def updateLoadBalancer(ip):
|
||||||
|
|
||||||
|
for option in config["load_balancer"]:
|
||||||
|
pools = cf_api('user/load_balancers/pools', 'GET', option)
|
||||||
|
|
||||||
|
if pools:
|
||||||
|
idxr = dict((p['id'], i) for i, p in enumerate(pools['result']))
|
||||||
|
idx = idxr.get(option['pool_id'])
|
||||||
|
|
||||||
|
origins = pools['result'][idx]['origins']
|
||||||
|
|
||||||
|
idxr = dict((o['name'], i) for i, o in enumerate(origins))
|
||||||
|
idx = idxr.get(option['origin'])
|
||||||
|
|
||||||
|
origins[idx]['address'] = ip['ip']
|
||||||
|
data = {'origins': origins}
|
||||||
|
|
||||||
|
response = cf_api(f'user/load_balancers/pools/{option["pool_id"]}', 'PATCH', option, {}, data)
|
||||||
|
|
||||||
|
|
||||||
def cf_api(endpoint, method, config, headers={}, data=False):
|
def cf_api(endpoint, method, config, headers={}, data=False):
|
||||||
api_token = config['authentication']['api_token']
|
api_token = config['authentication']['api_token']
|
||||||
if api_token != '' and api_token != 'api_token_here':
|
if api_token != '' and api_token != 'api_token_here':
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + api_token,
|
"Authorization": "Bearer " + api_token, **headers
|
||||||
**headers
|
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
headers = {
|
headers = {
|
||||||
"X-Auth-Email": config['authentication']['api_key']['account_email'],
|
"X-Auth-Email": config['authentication']['api_key']['account_email'],
|
||||||
"X-Auth-Key": config['authentication']['api_key']['api_key'],
|
"X-Auth-Key": config['authentication']['api_key']['api_key'],
|
||||||
}
|
}
|
||||||
|
|
||||||
if(data == False):
|
|
||||||
response = requests.request(
|
|
||||||
method, "https://api.cloudflare.com/client/v4/" + endpoint, headers=headers)
|
|
||||||
else:
|
|
||||||
response = requests.request(
|
|
||||||
method, "https://api.cloudflare.com/client/v4/" + endpoint, headers=headers, json=data)
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def every(delay, task):
|
|
||||||
next_time = time.time() + delay
|
|
||||||
while True:
|
|
||||||
time.sleep(max(0, next_time - time.time()))
|
|
||||||
try:
|
try:
|
||||||
task()
|
if (data == False):
|
||||||
except Exception:
|
response = requests.request(
|
||||||
traceback.print_exc()
|
method, "https://api.cloudflare.com/client/v4/" + endpoint, headers=headers)
|
||||||
# in production code you might want to have this instead of course:
|
else:
|
||||||
# logger.exception("Problem while executing repetitive task.")
|
response = requests.request(
|
||||||
# skip tasks if we are behind schedule:
|
method, "https://api.cloudflare.com/client/v4/" + endpoint,
|
||||||
next_time += (time.time() - next_time) // delay * delay + delay
|
headers=headers, json=data)
|
||||||
|
|
||||||
def updateIPs():
|
if response.ok:
|
||||||
for ip in getIPs():
|
return response.json()
|
||||||
print("Checking " + ip["type"] + " records")
|
else:
|
||||||
|
print("😡 Error sending '" + method +
|
||||||
|
"' request to '" + response.url + "':")
|
||||||
|
print(response.text)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print("😡 An exception occurred while sending '" +
|
||||||
|
method + "' request to '" + endpoint + "': " + str(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def updateIPs(ips):
|
||||||
|
for ip in ips.values():
|
||||||
commitRecord(ip)
|
commitRecord(ip)
|
||||||
|
#updateLoadBalancer(ip)
|
||||||
|
|
||||||
if(len(sys.argv) > 1):
|
|
||||||
if(sys.argv[1] == "--repeat"):
|
if __name__ == '__main__':
|
||||||
import threading
|
shown_ipv4_warning = False
|
||||||
threading.Thread(target=lambda: every(60*15, updateIPs)).start()
|
shown_ipv4_warning_secondary = False
|
||||||
updateIPs()
|
shown_ipv6_warning = False
|
||||||
|
shown_ipv6_warning_secondary = False
|
||||||
|
ipv4_enabled = True
|
||||||
|
ipv6_enabled = True
|
||||||
|
purgeUnknownRecords = False
|
||||||
|
|
||||||
|
if sys.version_info < (3, 5):
|
||||||
|
raise Exception("🐍 This script requires Python 3.5+")
|
||||||
|
|
||||||
|
config = None
|
||||||
|
try:
|
||||||
|
with open(os.path.join(CONFIG_PATH, "config.json")) as config_file:
|
||||||
|
if len(ENV_VARS) != 0:
|
||||||
|
config = json.loads(Template(config_file.read()).safe_substitute(ENV_VARS))
|
||||||
|
else:
|
||||||
|
config = json.loads(config_file.read())
|
||||||
|
except:
|
||||||
|
print("😡 Error reading config.json")
|
||||||
|
# wait 10 seconds to prevent excessive logging on docker auto restart
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
if config is not None:
|
||||||
|
try:
|
||||||
|
ipv4_enabled = config["a"]
|
||||||
|
ipv6_enabled = config["aaaa"]
|
||||||
|
except:
|
||||||
|
ipv4_enabled = True
|
||||||
|
ipv6_enabled = True
|
||||||
|
print("⚙️ Individually disable IPv4 or IPv6 with new config.json options. Read more about it here: https://github.com/timothymiller/cloudflare-ddns/blob/master/README.md")
|
||||||
|
try:
|
||||||
|
purgeUnknownRecords = config["purgeUnknownRecords"]
|
||||||
|
except:
|
||||||
|
purgeUnknownRecords = False
|
||||||
|
print("⚙️ No config detected for 'purgeUnknownRecords' - defaulting to False")
|
||||||
|
try:
|
||||||
|
ttl = int(config["ttl"])
|
||||||
|
except:
|
||||||
|
ttl = 300 # default Cloudflare TTL
|
||||||
|
print(
|
||||||
|
"⚙️ No config detected for 'ttl' - defaulting to 300 seconds (5 minutes)")
|
||||||
|
if ttl < 30:
|
||||||
|
ttl = 1 #
|
||||||
|
print("⚙️ TTL is too low - defaulting to 1 (auto)")
|
||||||
|
if (len(sys.argv) > 1):
|
||||||
|
if (sys.argv[1] == "--repeat"):
|
||||||
|
if ipv4_enabled and ipv6_enabled:
|
||||||
|
print(
|
||||||
|
"🕰️ Updating IPv4 (A) & IPv6 (AAAA) records every " + str(ttl) + " seconds")
|
||||||
|
elif ipv4_enabled and not ipv6_enabled:
|
||||||
|
print("🕰️ Updating IPv4 (A) records every " +
|
||||||
|
str(ttl) + " seconds")
|
||||||
|
elif ipv6_enabled and not ipv4_enabled:
|
||||||
|
print("🕰️ Updating IPv6 (AAAA) records every " +
|
||||||
|
str(ttl) + " seconds")
|
||||||
|
next_time = time.time()
|
||||||
|
killer = GracefulExit()
|
||||||
|
prev_ips = None
|
||||||
|
while True:
|
||||||
|
updateIPs(getIPs())
|
||||||
|
if killer.kill_now.wait(ttl):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("❓ Unrecognized parameter '" +
|
||||||
|
sys.argv[1] + "'. Stopping now.")
|
||||||
|
else:
|
||||||
|
updateIPs(getIPs())
|
||||||
|
|||||||
@@ -2,18 +2,27 @@
|
|||||||
"cloudflare": [
|
"cloudflare": [
|
||||||
{
|
{
|
||||||
"authentication": {
|
"authentication": {
|
||||||
"api_token": "api_token_here",
|
"api_token": "api_token_here",
|
||||||
"api_key": {
|
"api_key": {
|
||||||
"api_key": "api_key_here",
|
"api_key": "api_key_here",
|
||||||
"account_email": "your_email_here"
|
"account_email": "your_email_here"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zone_id": "your_zone_id_here",
|
"zone_id": "your_zone_id_here",
|
||||||
"subdomains": [
|
"subdomains": [
|
||||||
"",
|
{
|
||||||
"subdomain"
|
"name": "",
|
||||||
],
|
"proxied": false
|
||||||
"proxied": false
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_or_replace_with_your_subdomain",
|
||||||
|
"proxied": false
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"a": true,
|
||||||
|
"aaaa": true,
|
||||||
|
"purgeUnknownRecords": false,
|
||||||
|
"ttl": 300
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
version: "3.7"
|
version: '3.9'
|
||||||
services:
|
services:
|
||||||
cloudflare-ddns:
|
cloudflare-ddns:
|
||||||
image: timothymiller/cloudflare-ddns:latest
|
image: timothyjmiller/cloudflare-ddns:latest
|
||||||
container_name: cloudflare-ddns
|
container_name: cloudflare-ddns
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
network_mode: 'host'
|
||||||
environment:
|
environment:
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
- PGID=1000
|
- PGID=1000
|
||||||
volumes:
|
volumes:
|
||||||
- /EDIT/YOUR/PATH/HERE/config.json:/config.json
|
- /YOUR/PATH/HERE/config.json:/config.json
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
BIN
feature-graphic.jpg
Normal file
BIN
feature-graphic.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 440 KiB |
33
k8s/cloudflare-ddns.yml
Normal file
33
k8s/cloudflare-ddns.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: cloudflare-ddns
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: cloudflare-ddns
|
||||||
|
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: cloudflare-ddns
|
||||||
|
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: cloudflare-ddns
|
||||||
|
image: timothyjmiller/cloudflare-ddns:latest
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: '32Mi'
|
||||||
|
cpu: '50m'
|
||||||
|
env:
|
||||||
|
- name: CONFIG_PATH
|
||||||
|
value: '/etc/cloudflare-ddns/'
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: '/etc/cloudflare-ddns'
|
||||||
|
name: config-cloudflare-ddns
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: config-cloudflare-ddns
|
||||||
|
secret:
|
||||||
|
secretName: config-cloudflare-ddns
|
||||||
@@ -1 +1 @@
|
|||||||
requests==2.24.0
|
requests==2.31.0
|
||||||
4
scripts/docker-build-all.sh
Executable file
4
scripts/docker-build-all.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
BASH_DIR=$(dirname $(realpath "${BASH_SOURCE}"))
|
||||||
|
docker buildx build --platform linux/ppc64le,linux/s390x,linux/386,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/amd64 --tag timothyjmiller/cloudflare-ddns:latest ${BASH_DIR}/../
|
||||||
|
# TODO: Support linux/riscv64
|
||||||
3
scripts/docker-build.sh
Executable file
3
scripts/docker-build.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
BASH_DIR=$(dirname $(realpath "${BASH_SOURCE}"))
|
||||||
|
docker build --platform linux/amd64 --tag timothyjmiller/cloudflare-ddns:latest ${BASH_DIR}/../
|
||||||
3
scripts/docker-publish.sh
Executable file
3
scripts/docker-publish.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
BASH_DIR=$(dirname $(realpath "${BASH_SOURCE}"))
|
||||||
|
docker buildx build --platform linux/ppc64le,linux/s390x,linux/386,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/amd64 --tag timothyjmiller/cloudflare-ddns:latest --push ${BASH_DIR}/../
|
||||||
2
scripts/docker-run.sh
Executable file
2
scripts/docker-run.sh
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker run timothyjmiller/cloudflare-ddns:latest
|
||||||
@@ -4,7 +4,7 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source ./venv/bin/activate
|
source ./venv/bin/activate
|
||||||
|
|
||||||
pip3 install requests
|
|
||||||
|
|
||||||
cd $DIR
|
cd $DIR
|
||||||
|
set -o pipefail; pip install -r requirements.txt | { grep -v "already satisfied" || :; }
|
||||||
|
|
||||||
python3 cloudflare-ddns.py
|
python3 cloudflare-ddns.py
|
||||||
|
|||||||
13
systemd/cloudflare-ddns.service
Normal file
13
systemd/cloudflare-ddns.service
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Update DDNS on Cloudflare
|
||||||
|
ConditionPathExists=/etc/cloudflare-ddns/config.json
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
Environment=CONFIG_PATH=/etc/cloudflare-ddns
|
||||||
|
ExecStart=cloudflare-ddns
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
9
systemd/cloudflare-ddns.timer
Normal file
9
systemd/cloudflare-ddns.timer
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Update DDNS on Cloudflare every 15 minutes
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=2min
|
||||||
|
OnUnitActiveSec=15m
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Reference in New Issue
Block a user