You've already forked ddns-updater
Compare commits
21 Commits
notificati
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e4691d9be5 | |||
| f31f2d74b7 | |||
| e1bb5adf36 | |||
| a52034216b | |||
| 17014eeae1 | |||
| a977adf929 | |||
| 5e4a7f8135 | |||
| f09bbfa6b7 | |||
| cbe676846a | |||
| cffeeaa2f8 | |||
| adf83e7782 | |||
| ddd6326a6e | |||
|
|
13e2fa6f7b | ||
|
|
401884304e | ||
|
|
df74a0e159 | ||
|
|
316a40b662 | ||
|
|
17afc65f92 | ||
|
|
51958d719e | ||
|
|
10a5a12b86 | ||
|
|
c7646cbf63 | ||
|
|
6a028ead30 |
6
.github/workflows/quality-checks.yml
vendored
6
.github/workflows/quality-checks.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.21'
|
||||||
cache: false
|
cache: false
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Prepare go environment
|
- name: Prepare go environment
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.21.5'
|
||||||
cache: false
|
cache: false
|
||||||
- name: Install dep scanner
|
- name: Install dep scanner
|
||||||
run: |
|
run: |
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
- name: Prepare go environment
|
- name: Prepare go environment
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.21'
|
||||||
cache: false
|
cache: false
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test --cover -coverprofile coverage.out -covermode count -v ./...
|
run: go test --cover -coverprofile coverage.out -covermode count -v ./...
|
||||||
|
|||||||
49
.woodpecker.yml
Normal file
49
.woodpecker.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
variables:
|
||||||
|
- &platforms 'linux/arm/v7,linux/arm64/v8,linux/amd64'
|
||||||
|
- &docker_creds
|
||||||
|
username:
|
||||||
|
from_secret: hub_username_cybercinch
|
||||||
|
password:
|
||||||
|
from_secret: docker_password_cybercinch
|
||||||
|
- &pypi_creds
|
||||||
|
username:
|
||||||
|
from_secret: hub_username_cybercinch
|
||||||
|
password:
|
||||||
|
from_secret: docker_password_cybercinch
|
||||||
|
|
||||||
|
steps:
|
||||||
|
publish-docker-latest:
|
||||||
|
image: docker.io/cybercinch/woodpecker-plugin-depot
|
||||||
|
pull: true
|
||||||
|
settings:
|
||||||
|
<<: *docker_creds
|
||||||
|
token:
|
||||||
|
from_secret: depot_token
|
||||||
|
repohost: hub.cybercinch.nz
|
||||||
|
repo: cybercinch/${CI_REPO_NAME}
|
||||||
|
project: 8b4ht8th6p
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: *platforms
|
||||||
|
tags: ["latest"]
|
||||||
|
when:
|
||||||
|
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- manual
|
||||||
|
update-swarm-service-portainer:
|
||||||
|
image: docker.io/plugins/webhook
|
||||||
|
settings:
|
||||||
|
urls:
|
||||||
|
from_secret: deploy_url
|
||||||
|
method: POST
|
||||||
|
# depends_on: publish-docker-latest
|
||||||
|
# settings:
|
||||||
|
# <<: *docker_creds
|
||||||
|
# repohost: hub.cybercinch.nz
|
||||||
|
# repo: cybercinch/imap_retention_manager
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# platforms: *platforms
|
||||||
|
# tags: ["latest", "${CI_COMMIT_TAG}"]
|
||||||
|
# when:
|
||||||
|
# event: tag
|
||||||
12
Dockerfile
12
Dockerfile
@@ -1,15 +1,21 @@
|
|||||||
FROM golang:1.20 as build
|
FROM golang:1.22 as build
|
||||||
|
|
||||||
# Copy project sources
|
# Copy project sources
|
||||||
COPY . /opt/project/
|
COPY . /opt/project/
|
||||||
WORKDIR /opt/project
|
WORKDIR /opt/project
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates=20210119
|
# Install Pre-Requisites
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates=20230311
|
||||||
|
# Create a user
|
||||||
|
RUN useradd --no-create-home --system --shell /bin/false ddnsuser
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /cloudflare-ddns-updater
|
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /cloudflare-ddns-updater
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY --from=build /cloudflare-ddns-updater /cloudflare-ddns-updater
|
COPY --from=build /cloudflare-ddns-updater /cloudflare-ddns-updater
|
||||||
|
COPY --from=build /bin/false /bin/false
|
||||||
|
COPY --from=build /etc/passwd /etc/passwd
|
||||||
|
|
||||||
|
USER nobody
|
||||||
ENTRYPOINT ["/cloudflare-ddns-updater"]
|
ENTRYPOINT ["/cloudflare-ddns-updater"]
|
||||||
CMD ["cloudflare-ddns-updater"]
|
|
||||||
59
README.md
59
README.md
@@ -5,22 +5,38 @@ DNS records are static, and it does not play well with dynamic IP addresses. Now
|
|||||||
|
|
||||||
To set up a Cloudflare dynamic DNS, you’ll need to run a process on a client inside your network that does two main actions: get your network’s current public IP address and automatically update the corresponding DNS record.
|
To set up a Cloudflare dynamic DNS, you’ll need to run a process on a client inside your network that does two main actions: get your network’s current public IP address and automatically update the corresponding DNS record.
|
||||||
|
|
||||||
This simple updater do the job.
|
This simple updater do the job, and send notifications, if change happen.
|
||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
Before run, you need configure this environment variables.
|
Before run, you need configure this environment variables.
|
||||||
|
|
||||||
- `CLOUDFLARE_DNS_TO_CHECK` - (required) dns records that will be automatically checked and modified based on the current public IP address. Multiple entries are separated by commas. For example: `domain.com,sub1.domain.com,sub2.domain.com`
|
- `DNS_NAMES` - (required) dns records that will be automatically checked and modified based on the current public IP address. Multiple entries are separated by commas. For example: `domain.com,sub1.domain.com,sub2.domain.com`
|
||||||
- `CLOUDFLARE_API_KEY` - (required) your cloudflare api key, with access rights to edit selected domains. See: [https://developers.cloudflare.com/fundamentals/api/get-started/create-token/](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)
|
- `CLOUDFLARE_API_KEY` - (required) your cloudflare api key, with access rights to edit selected domains. See: [https://developers.cloudflare.com/fundamentals/api/get-started/create-token/](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)
|
||||||
- `CLOUDFLARE_ZONE` - (required) zone name with domain you want to check. See: [https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones](https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones)
|
- `CLOUDFLARE_ZONE` - (required) zone name with domain you want to check. See: [https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones](https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones)
|
||||||
- `ON_CHANGE_COMMENT` - (optional) in the event that the ip address of the dns record changes, this comment will be added to the record
|
- `ON_CHANGE_COMMENT` - (optional) in the event that the ip address of the dns record changes, this comment will be added to the record
|
||||||
- `CHECK_INTERVAL_SECONDS` - (optional) how often will the ip address of the records be checked (default: `300`)
|
- `CHECK_INTERVAL_SECONDS` - (optional) how often will the ip address of the records be checked (default: `300`)
|
||||||
- `PUBLIC_IP_RESOLVER` - (optional) public ip address resolver. (default: `ifconfig.me`) Available: `ifconfig.me`, `v4.ident.me`, `1.1.1.1`
|
- `PUBLIC_IP_RESOLVER` - (optional) public ip address resolver. (default: `ifconfig.me`) Available: `ifconfig.me`, `v4.ident.me`, `1.1.1.1`
|
||||||
- `NOTIFIERS` - (optional) setting the notifier in case of an update of the dns record. Multiple entries are separated by commas. (default none). Example: `webhook@http://localhost/cloudflare-notification`
|
- `NOTIFIERS` - (optional) setting the notifier in case of an update of the dns record. Multiple entries are separated by commas. (default none). Example: `webhook@http://localhost/cloudflare-notification`
|
||||||
- Available(
|
|
||||||
- `webhook` - Call defined webhook. Example: `webhook@http://localhost/cloudflare-notification`
|
### Notifications
|
||||||
|
|
||||||
|
Currently, only webhook notification is available. Webhook sends a POST request to the specified endpoint in json format.
|
||||||
|
|
||||||
|
Request body example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"old_ip": "xxx.xxx.xxx.xxx",
|
||||||
|
"new_ip": "xxx.xxx.xxx.xxx",
|
||||||
|
"checked_at": "2023-05-04T17:39:42.942850354+02:00",
|
||||||
|
"resolver_tag": "ifconfig.me",
|
||||||
|
"domain": "my.domain.com",
|
||||||
|
"token": "a-webhook-token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Other notification methods will be implemented later (check future plans section).
|
||||||
|
|
||||||
### Building from source
|
### Building from source
|
||||||
|
|
||||||
@@ -42,19 +58,40 @@ go install github.com/mkelcik/cloudflare-ddns-update
|
|||||||
CLOUDFLARE_DNS_TO_CHECK="domain.com" CLOUDFLARE_API_KEY="my_key" CLOUDFLARE_ZONE="domain.com" cloudflare-ddns-update
|
CLOUDFLARE_DNS_TO_CHECK="domain.com" CLOUDFLARE_API_KEY="my_key" CLOUDFLARE_ZONE="domain.com" cloudflare-ddns-update
|
||||||
```
|
```
|
||||||
|
|
||||||
### Via `docker-compose`
|
### Via `docker-compose for Cloudflare`
|
||||||
```yaml
|
```yaml
|
||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
cf-dns-updater:
|
cf-dns-updater:
|
||||||
image: mkelcik/cloudflare-ddns-update:latest
|
image: hub.cybercinch.nz/cybercinch/ddns-update:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com
|
|
||||||
- CLOUDFLARE_API_KEY=your_cloudflare_api_key
|
- CLOUDFLARE_API_KEY=your_cloudflare_api_key
|
||||||
- CLOUDFLARE_ZONE=testdomain.com
|
- DNS_NAMES=my.testdomain.com,your.testdomain.com
|
||||||
|
- NOTIFIERS=webhook@http://localhost/cloudflare-updated-notification
|
||||||
- ON_CHANGE_COMMENT="automatically updated"
|
- ON_CHANGE_COMMENT="automatically updated"
|
||||||
- CHECK_INTERVAL_SECONDS=300
|
- CHECK_INTERVAL_SECONDS=300
|
||||||
|
# Optional if your webhook receiver requires a token for verification
|
||||||
|
# - WEBHOOK_TOKEN="SomeSup3rs3cureT0k3n"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via `docker-compose for DirectAdmin`
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
cf-dns-updater:
|
||||||
|
image: hub.cybercinch.nz/cybercinch/ddns-update:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DNS_PROVIDER=directadmin
|
||||||
|
- DA_USER=some-directadmin-username
|
||||||
|
- DA_KEY=your-directadmin-password-or-login-key
|
||||||
|
- DA_URL=https://your.daserver.com:2222
|
||||||
|
- DNS_NAMES=my.testdomain.com,your.testdomain.com
|
||||||
|
- NOTIFIERS=webhook@http://localhost/cloudflare-updated-notification
|
||||||
|
- CHECK_INTERVAL_SECONDS=300
|
||||||
|
# Optional if your webhook receiver requires a token for verification
|
||||||
|
# - WEBHOOK_TOKEN="SomeSup3rs3cureT0k3n"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Via `docker run`
|
### Via `docker run`
|
||||||
@@ -62,6 +99,12 @@ services:
|
|||||||
docker run -e CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com -e CLOUDFLARE_API_KEY=your_cloudflare_api_key -e CLOUDFLARE_ZONE=testdomain.com -e ON_CHANGE_COMMENT="automatically updated" -e CHECK_INTERVAL_SECONDS=300 mkelcik/cloudflare-ddns-update:latest
|
docker run -e CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com -e CLOUDFLARE_API_KEY=your_cloudflare_api_key -e CLOUDFLARE_ZONE=testdomain.com -e ON_CHANGE_COMMENT="automatically updated" -e CHECK_INTERVAL_SECONDS=300 mkelcik/cloudflare-ddns-update:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Future plans
|
||||||
|
|
||||||
|
- prometheus metrics
|
||||||
|
- mqtt and rabbitmq notifiers
|
||||||
|
- IPv6 support
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
Feel free to contribute and pls report bugs. Thanks
|
Feel free to contribute and pls report bugs. Thanks
|
||||||
|
|||||||
22
go.mod
22
go.mod
@@ -1,14 +1,22 @@
|
|||||||
module github.com/mkelcik/cloudflare-ddns-update
|
module hub.cybercinch.nz/cybercinch/ddns-update
|
||||||
|
|
||||||
go 1.20
|
|
||||||
|
|
||||||
require github.com/cloudflare/cloudflare-go v0.66.0
|
go 1.22
|
||||||
|
|
||||||
|
toolchain go1.22.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/cloudflare/cloudflare-go v0.83.0
|
||||||
|
github.com/levelzerotechnology/directadmin-go v0.0.0-20240302013738-63b7793ebfd3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||||
golang.org/x/net v0.9.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
48
go.sum
48
go.sum
@@ -1,30 +1,52 @@
|
|||||||
github.com/cloudflare/cloudflare-go v0.66.0 h1:B74IvVGQ4UFYJnqQSK/9GbR+Y1HwNxqqdN2Bmg0dckg=
|
github.com/cloudflare/cloudflare-go v0.83.0 h1:aq85Hbr5W6KfXZV7v3lx6fhBkiu0FYqY+3+xzG14mdY=
|
||||||
github.com/cloudflare/cloudflare-go v0.66.0/go.mod h1:tA44hjU9FfycofKT+lWWMHb/dEq1pRbiVPGuJo1WzLQ=
|
github.com/cloudflare/cloudflare-go v0.83.0/go.mod h1:5pkAzpoWJYI5NekLZoRryQAcghYDhdbUxdcal1f7lu4=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||||
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
|
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
|
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/levelzerotechnology/directadmin-go v0.0.0-20240302013738-63b7793ebfd3 h1:6kGC8EpZlYTvNI2ABYX87bBtgzfPrtZV8CKqLtSvyek=
|
||||||
|
github.com/levelzerotechnology/directadmin-go v0.0.0-20240302013738-63b7793ebfd3/go.mod h1:FJ/EtEMwe3k2ABVYLXmE/K+H8cHVpVaSgihVT2XbSkc=
|
||||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
|
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
defaultCheckInterval = 5 * 60
|
defaultCheckInterval = 5 * 60
|
||||||
|
envKeyDnsToCheck = "DNS_NAMES"
|
||||||
envKeyDnsToCheck = "CLOUDFLARE_DNS_TO_CHECK"
|
|
||||||
envKeyPublicIpResolverTag = "PUBLIC_IP_RESOLVER"
|
envKeyPublicIpResolverTag = "PUBLIC_IP_RESOLVER"
|
||||||
|
envKeyDNSProviderTag = "DNS_PROVIDER"
|
||||||
|
envKeyPublicDNSServer = "PUBLIC_DNS_SERVER"
|
||||||
envKeyCloudflareApiKey = "CLOUDFLARE_API_KEY"
|
envKeyCloudflareApiKey = "CLOUDFLARE_API_KEY"
|
||||||
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
||||||
|
envKeyDirectadminUser = "DA_USER"
|
||||||
|
envKeyDirectadminKey = "DA_KEY"
|
||||||
|
envKeyDirectadminUrl = "DA_URL"
|
||||||
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
||||||
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
||||||
envKeyNotifiers = "NOTIFIERS"
|
envKeyNotifiers = "NOTIFIERS"
|
||||||
@@ -23,20 +27,38 @@ const (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
DnsRecordsToCheck []string
|
DnsRecordsToCheck []string
|
||||||
PublicIpResolverTag string
|
PublicIpResolverTag string
|
||||||
|
PublicDNSServer string
|
||||||
|
DNSProviderTag string
|
||||||
|
DirectadminUsername string
|
||||||
|
DirectadminKey string
|
||||||
|
DirectadminUrl string
|
||||||
ApiToken string
|
ApiToken string
|
||||||
|
WebhookToken string
|
||||||
CloudflareZone string
|
CloudflareZone string
|
||||||
OnChangeComment string
|
CloudflareOnChangeComment string
|
||||||
Notifiers []string
|
Notifiers []string
|
||||||
CheckInterval time.Duration
|
CheckInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) Validate() error {
|
func (c Config) Validate() error {
|
||||||
|
switch c.DNSProviderTag {
|
||||||
|
case "cloudflare":
|
||||||
if c.ApiToken == "" {
|
if c.ApiToken == "" {
|
||||||
return fmt.Errorf("empty api token env key %s", envKeyCloudflareApiKey)
|
return fmt.Errorf("empty api token env key %s", envKeyCloudflareApiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.CloudflareZone == "" {
|
case "directadmin":
|
||||||
return fmt.Errorf("empty zone in env key %s", envKeyCloudflareZone)
|
if c.DirectadminUrl == "" {
|
||||||
|
return fmt.Errorf("empty DirectAdmin URL env key %s", envKeyDirectadminUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DirectadminUsername == "" {
|
||||||
|
return fmt.Errorf("empty DirectAdmin Username in env key %s", envKeyDirectadminUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DirectadminKey == "" {
|
||||||
|
return fmt.Errorf("empty DirectAdmin Login Key in env key %s", envKeyDirectadminKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.DnsRecordsToCheck) == 0 {
|
if len(c.DnsRecordsToCheck) == 0 {
|
||||||
@@ -49,17 +71,30 @@ func (c Config) Validate() error {
|
|||||||
func NewConfig() Config {
|
func NewConfig() Config {
|
||||||
checkInterval, err := strconv.ParseInt(os.Getenv(envKeyCheckIntervalSeconds), 10, 64)
|
checkInterval, err := strconv.ParseInt(os.Getenv(envKeyCheckIntervalSeconds), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("wrong `%s` value. Check interval set default(%ds)", envKeyCheckIntervalSeconds, defaultCheckInterval)
|
log.Printf("wrong or missing `%s` value. Check interval set to default(%ds)", envKeyCheckIntervalSeconds, defaultCheckInterval)
|
||||||
checkInterval = defaultCheckInterval
|
checkInterval = defaultCheckInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||||
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
DNSProviderTag: getEnvDefault(envKeyDNSProviderTag, "cloudflare"),
|
||||||
|
PublicIpResolverTag: getEnvDefault(envKeyPublicIpResolverTag, "icanhazip"),
|
||||||
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||||
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||||
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
CloudflareOnChangeComment: os.Getenv(envKeyOnChangeComment),
|
||||||
|
DirectadminUsername: os.Getenv(envKeyDirectadminUser),
|
||||||
|
DirectadminKey: os.Getenv(envKeyDirectadminKey),
|
||||||
|
DirectadminUrl: os.Getenv(envKeyDirectadminUrl),
|
||||||
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
|
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
|
||||||
CheckInterval: time.Duration(checkInterval) * time.Second,
|
CheckInterval: time.Duration(checkInterval) * time.Second,
|
||||||
|
WebhookToken: os.Getenv("WEBHOOK_TOKEN"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getEnvDefault(key, fallback string) string {
|
||||||
|
value, exists := os.LookupEnv(key)
|
||||||
|
if !exists {
|
||||||
|
value = fallback
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func TestConfig_Validate(t *testing.T) {
|
|||||||
PublicIpResolverTag: tt.fields.PublicIpResolverTag,
|
PublicIpResolverTag: tt.fields.PublicIpResolverTag,
|
||||||
ApiToken: tt.fields.ApiToken,
|
ApiToken: tt.fields.ApiToken,
|
||||||
CloudflareZone: tt.fields.CloudflareZone,
|
CloudflareZone: tt.fields.CloudflareZone,
|
||||||
OnChangeComment: tt.fields.OnChangeComment,
|
CloudflareOnChangeComment: tt.fields.OnChangeComment,
|
||||||
CheckInterval: tt.fields.CheckInterval,
|
CheckInterval: tt.fields.CheckInterval,
|
||||||
}
|
}
|
||||||
if err := c.Validate(); (err != nil) != tt.wantErr {
|
if err := c.Validate(); (err != nil) != tt.wantErr {
|
||||||
|
|||||||
33
internal/dns_providers/base_provider.go
Normal file
33
internal/dns_providers/base_provider.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package dns_providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DNSProvider interface {
|
||||||
|
UpdateRecord(hostname string, ip string) error
|
||||||
|
FetchRecord(hostname string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainParts struct {
|
||||||
|
Name string
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDomainParts(hostname string) *DomainParts {
|
||||||
|
data := arrayToSlice(strings.Split(hostname, "."))
|
||||||
|
|
||||||
|
if len(data) <= 2 {
|
||||||
|
// This might be the actual root domain
|
||||||
|
out := DomainParts{Name: strings.Join(data[0:], "."), Domain: strings.Join(data[0:], ".")}
|
||||||
|
return &out
|
||||||
|
} else {
|
||||||
|
// This is a subdomain
|
||||||
|
out := DomainParts{Name: data[0], Domain: strings.Join(data[1:], ".")}
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func arrayToSlice(array []string) []string {
|
||||||
|
return array[:]
|
||||||
|
}
|
||||||
92
internal/dns_providers/cloudflare.go
Normal file
92
internal/dns_providers/cloudflare.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package dns_providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflare-go"
|
||||||
|
"hub.cybercinch.nz/cybercinch/ddns-update/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CloudflareTag = "cloudflare"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloudflareProvider struct {
|
||||||
|
Context context.Context
|
||||||
|
Client *cloudflare.API
|
||||||
|
Config internal.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CloudflareProvider) UpdateRecord(hostname string, ip string) error {
|
||||||
|
// old_ip is not required for Cloudflare updates
|
||||||
|
domain_parts := GetDomainParts(hostname)
|
||||||
|
zoneId := d.FindZoneIdByName(domain_parts.Domain)
|
||||||
|
dnsRecords, err := d.GetDnsRecord(hostname, zoneId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
update := cloudflare.UpdateDNSRecordParams{
|
||||||
|
ID: dnsRecords[0].ID,
|
||||||
|
Content: ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Config.CloudflareOnChangeComment != "" {
|
||||||
|
update.Comment = &d.Config.CloudflareOnChangeComment
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.Client.UpdateDNSRecord(d.Context, cloudflare.ZoneIdentifier(zoneId), update)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CloudflareProvider) FetchRecord(hostname string) (string, error) {
|
||||||
|
domain_parts := GetDomainParts(hostname)
|
||||||
|
zoneId := d.FindZoneIdByName(domain_parts.Domain)
|
||||||
|
dnsRecord, err := d.GetDnsRecord(hostname, zoneId)
|
||||||
|
return dnsRecord[0].Content, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CloudflareProvider) NewClient(api_token string) {
|
||||||
|
api, err := cloudflare.NewWithAPIToken(api_token)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Client = api
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CloudflareProvider) FindZoneIdByName(hostname string) string {
|
||||||
|
// Fetch user details on the account
|
||||||
|
zoneID, err := d.Client.ZoneIDByName(hostname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return zoneID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *CloudflareProvider) GetDnsRecord(name string, zoneId string) ([]cloudflare.DNSRecord, error) {
|
||||||
|
|
||||||
|
params := cloudflare.ListDNSRecordsParams{
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
page, _, err := d.Client.ListDNSRecords(d.Context, cloudflare.ZoneIdentifier(zoneId), params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return page, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCloudflareProvider(ctx context.Context, config internal.Config) *CloudflareProvider {
|
||||||
|
c := &CloudflareProvider{}
|
||||||
|
c.Context = ctx
|
||||||
|
c.Config = config
|
||||||
|
c.NewClient(c.Config.ApiToken)
|
||||||
|
return c
|
||||||
|
}
|
||||||
91
internal/dns_providers/directadmin.go
Normal file
91
internal/dns_providers/directadmin.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package dns_providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/levelzerotechnology/directadmin-go"
|
||||||
|
"hub.cybercinch.nz/cybercinch/ddns-update/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DirectadminTag = "directadmin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Directadmin struct {
|
||||||
|
Client *directadmin.UserContext
|
||||||
|
Context context.Context
|
||||||
|
Config internal.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListDNSRecordsParams struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Directadmin) FetchRecord(hostname string) (string, error) {
|
||||||
|
dnsRecord, err := d.GetDnsRecord(hostname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("unable to retrieve DNS record")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dnsRecord[0].Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Directadmin) UpdateRecord(hostname string, ip string) error {
|
||||||
|
|
||||||
|
result := GetDomainParts(hostname)
|
||||||
|
current_record, _ := d.GetDnsRecord(hostname)
|
||||||
|
|
||||||
|
// Create an updated record for the new ip
|
||||||
|
new_record := directadmin.DNSRecord{Name: current_record[0].Name, Ttl: current_record[0].Ttl, Type: current_record[0].Type, Value: ip}
|
||||||
|
|
||||||
|
err := d.Client.UpdateDNSRecord(result.Domain, current_record[0], new_record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Directadmin) GetDnsRecord(hostname string) ([]directadmin.DNSRecord, error) {
|
||||||
|
|
||||||
|
domainParts := GetDomainParts(hostname)
|
||||||
|
dnsRecords, err := d.Client.GetDNSRecords(domainParts.Domain)
|
||||||
|
var slice []directadmin.DNSRecord
|
||||||
|
|
||||||
|
for _, dnsRecord := range dnsRecords {
|
||||||
|
if domainParts.Name == dnsRecord.Name {
|
||||||
|
slice = append(slice, dnsRecord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(slice) == 0 {
|
||||||
|
return nil, fmt.Errorf("unable to find DNS record for %s", hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
return slice, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Directadmin) NewClient(server_url string, username string, key string) {
|
||||||
|
api, err := directadmin.New(server_url, 5*time.Second, false, false)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userCtx, err := api.LoginAsUser(username, key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Client = userCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDirectAdminProvider(ctx context.Context, config internal.Config) *Directadmin {
|
||||||
|
c := &Directadmin{}
|
||||||
|
c.Context = ctx
|
||||||
|
c.Config = config
|
||||||
|
c.NewClient(config.DirectadminUrl, config.DirectadminUsername, config.DirectadminKey)
|
||||||
|
return c
|
||||||
|
}
|
||||||
29
internal/dns_resolver/dns_resolver.go
Normal file
29
internal/dns_resolver/dns_resolver.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package dns_resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ResolveHostname(host string, dns_resolver_ip string) net.IP {
|
||||||
|
r := &net.Resolver{
|
||||||
|
PreferGo: true,
|
||||||
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
d := net.Dialer{
|
||||||
|
Timeout: time.Millisecond * time.Duration(10000),
|
||||||
|
}
|
||||||
|
return d.DialContext(ctx, network, dns_resolver_ip+":53")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ips, err := r.LookupHost(context.Background(), host)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Could not get IPs: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return net.ParseIP(ips[len(ips)-1])
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
func parseCommaDelimited(data string) []string {
|
func parseCommaDelimited(data string) []string {
|
||||||
out := make([]string, 0, strings.Count(data, ",")+1)
|
out := make([]string, 0, strings.Count(data, ",")+1)
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const (
|
|||||||
configDelimiter = "@"
|
configDelimiter = "@"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Doer interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Notifiers []Notifier
|
type Notifiers []Notifier
|
||||||
|
|
||||||
func (n Notifiers) NotifyWithLog(ctx context.Context, notification Notification) error {
|
func (n Notifiers) NotifyWithLog(ctx context.Context, notification Notification) error {
|
||||||
@@ -35,6 +39,7 @@ type Notification struct {
|
|||||||
CheckedAt time.Time `json:"checked_at"`
|
CheckedAt time.Time `json:"checked_at"`
|
||||||
ResolverTag string `json:"resolver_tag"`
|
ResolverTag string `json:"resolver_tag"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
|
WebhookToken string `json:"token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n Notification) ToSlice() []string {
|
func (n Notification) ToSlice() []string {
|
||||||
@@ -13,10 +13,6 @@ const (
|
|||||||
webhookTag = "webhook"
|
webhookTag = "webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Doer interface {
|
|
||||||
Do(*http.Request) (*http.Response, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebhookConfig struct {
|
type WebhookConfig struct {
|
||||||
Url string
|
Url string
|
||||||
}
|
}
|
||||||
@@ -53,6 +49,8 @@ func (w WebhookNotification) Notify(ctx context.Context, notification Notificati
|
|||||||
return fmt.Errorf("WebhookNotification::NotifyWithLog error creating request: %w", err)
|
return fmt.Errorf("WebhookNotification::NotifyWithLog error creating request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := w.client.Do(req)
|
resp, err := w.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("WebhookNotification::NotifyWithLog error while sending notification: %w", err)
|
return fmt.Errorf("WebhookNotification::NotifyWithLog error while sending notification: %w", err)
|
||||||
31
internal/public_resolvers/icanhazip.go
Normal file
31
internal/public_resolvers/icanhazip.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package public_resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IcanhazipTag = "icanhazip"
|
||||||
|
IcanhazipUrl = "https://v4.icanhazip.com/"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Icanhazip struct {
|
||||||
|
baseResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIcanhazipDefault() *Icanhazip {
|
||||||
|
return NewIcanhazip(&http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIcanhazip(client Doer) *Icanhazip {
|
||||||
|
return &Icanhazip{
|
||||||
|
baseResolver: baseResolver{
|
||||||
|
client: client,
|
||||||
|
url: v4IdentMeUrl,
|
||||||
|
ipParser: defaultIpParser,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
85
main.go
85
main.go
@@ -8,10 +8,10 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflare-go"
|
"hub.cybercinch.nz/cybercinch/ddns-update/internal"
|
||||||
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
"hub.cybercinch.nz/cybercinch/ddns-update/internal/dns_providers"
|
||||||
"github.com/mkelcik/cloudflare-ddns-update/notifications"
|
"hub.cybercinch.nz/cybercinch/ddns-update/internal/notifications"
|
||||||
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
"hub.cybercinch.nz/cybercinch/ddns-update/internal/public_resolvers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PublicIpResolver interface {
|
type PublicIpResolver interface {
|
||||||
@@ -25,6 +25,8 @@ func getResolver(resolverName string) (PublicIpResolver, string) {
|
|||||||
return public_resolvers.NewDefaultCloudflareTrace(), public_resolvers.CloudflareTraceTag
|
return public_resolvers.NewDefaultCloudflareTrace(), public_resolvers.CloudflareTraceTag
|
||||||
case public_resolvers.V4IdentMeTag:
|
case public_resolvers.V4IdentMeTag:
|
||||||
return public_resolvers.NewV4IdentMeDefault(), public_resolvers.V4IdentMeTag
|
return public_resolvers.NewV4IdentMeDefault(), public_resolvers.V4IdentMeTag
|
||||||
|
case public_resolvers.IcanhazipTag:
|
||||||
|
return public_resolvers.NewIcanhazipDefault(), public_resolvers.IcanhazipTag
|
||||||
case public_resolvers.IfConfigMeTag:
|
case public_resolvers.IfConfigMeTag:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
@@ -32,6 +34,17 @@ func getResolver(resolverName string) (PublicIpResolver, string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getProvider(ctx context.Context, config internal.Config) (dns_providers.DNSProvider, string) {
|
||||||
|
switch config.DNSProviderTag {
|
||||||
|
case dns_providers.DirectadminTag:
|
||||||
|
return dns_providers.NewDirectAdminProvider(ctx, config), dns_providers.DirectadminTag
|
||||||
|
case dns_providers.CloudflareTag:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return dns_providers.NewCloudflareProvider(ctx, config), dns_providers.CloudflareTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
@@ -41,69 +54,55 @@ func main() {
|
|||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
api, err := cloudflare.NewWithAPIToken(config.ApiToken)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch user details on the account
|
|
||||||
zoneID, err := api.ZoneIDByName(config.CloudflareZone)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
notifiers := notifications.GetNotifiers(config.Notifiers)
|
notifiers := notifications.GetNotifiers(config.Notifiers)
|
||||||
|
|
||||||
// public ip resolver
|
// public ip resolver
|
||||||
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
||||||
|
dnsProvider, providerTag := getProvider(ctx, config)
|
||||||
|
log.Printf("Using DNS Provider %s", providerTag)
|
||||||
|
|
||||||
checkFunc := func() {
|
checkFunc := func() {
|
||||||
currentPublicIP, err := publicIpResolver.ResolvePublicIp(ctx)
|
currentPublicIP, err := publicIpResolver.ResolvePublicIp(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Current public ip `%s` (resolver: %s)", currentPublicIP, resolverTag)
|
log.Printf("Current public ip `%s` (resolver: %s)", currentPublicIP, resolverTag)
|
||||||
|
|
||||||
dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dnsRecord := range dns {
|
for _, dnsRecord := range config.DnsRecordsToCheck {
|
||||||
if internal.Contains(config.DnsRecordsToCheck, dnsRecord.Name) {
|
current_dns_record, err := dnsProvider.FetchRecord(dnsRecord)
|
||||||
log.Printf("Checking record `%s` with current value `%s` ...", dnsRecord.Name, dnsRecord.Content)
|
if err != nil {
|
||||||
if currentPublicIP.String() == dnsRecord.Content {
|
log.Fatalf("Failed to fetch DNS record: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Checking record `%s` with current value `%s` ...", dnsRecord, current_dns_record)
|
||||||
|
if currentPublicIP.String() == current_dns_record {
|
||||||
log.Println("OK")
|
log.Println("OK")
|
||||||
continue // no update needed
|
continue // no update needed
|
||||||
}
|
}
|
||||||
|
|
||||||
update := cloudflare.UpdateDNSRecordParams{
|
if err := dnsProvider.UpdateRecord(dnsRecord, currentPublicIP.String()); err != nil {
|
||||||
ID: dnsRecord.ID,
|
|
||||||
Content: currentPublicIP.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.OnChangeComment != "" {
|
|
||||||
update.Comment = config.OnChangeComment
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := api.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), update); err != nil {
|
|
||||||
log.Printf("error updating dns record: %s", err)
|
log.Printf("error updating dns record: %s", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := notifiers.NotifyWithLog(ctx, notifications.Notification{
|
if err := notifiers.NotifyWithLog(ctx, notifications.Notification{
|
||||||
OldIp: net.ParseIP(dnsRecord.Content),
|
OldIp: net.ParseIP(string(current_dns_record)),
|
||||||
NewIp: currentPublicIP,
|
NewIp: currentPublicIP,
|
||||||
CheckedAt: time.Now(),
|
CheckedAt: time.Now(),
|
||||||
ResolverTag: resolverTag,
|
ResolverTag: resolverTag,
|
||||||
Domain: dnsRecord.Name,
|
Domain: dnsRecord,
|
||||||
|
WebhookToken: config.WebhookToken,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("errors in notifications: %s", err)
|
log.Printf("errors in notifications: %s", err)
|
||||||
}
|
}
|
||||||
log.Printf("Updated to `%s`", currentPublicIP)
|
log.Printf("Updated to `%s`", currentPublicIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("checking ...")
|
log.Printf("checking ...")
|
||||||
checkFunc()
|
checkFunc()
|
||||||
@@ -121,23 +120,3 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func allDNSRecords(ctx context.Context, api *cloudflare.API, rc *cloudflare.ResourceContainer) ([]cloudflare.DNSRecord, error) {
|
|
||||||
out := make([]cloudflare.DNSRecord, 0, 100)
|
|
||||||
params := cloudflare.ListDNSRecordsParams{
|
|
||||||
ResultInfo: cloudflare.ResultInfo{Page: 1},
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
page, res, err := api.ListDNSRecords(ctx, rc, params)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out = append(out, page...)
|
|
||||||
|
|
||||||
if res.Page >= res.TotalPages {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
params.Page++
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user