You've already forked ddns-updater
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f31f2d74b7 | |||
| e1bb5adf36 | |||
| a52034216b | |||
| 17014eeae1 | |||
| a977adf929 | |||
| 5e4a7f8135 | |||
| f09bbfa6b7 | |||
| cbe676846a | |||
| cffeeaa2f8 | |||
| adf83e7782 | |||
| ddd6326a6e | |||
|
|
13e2fa6f7b | ||
|
|
401884304e | ||
|
|
df74a0e159 | ||
|
|
316a40b662 | ||
|
|
17afc65f92 | ||
|
|
51958d719e | ||
|
|
10a5a12b86 | ||
|
|
c7646cbf63 | ||
|
|
6a028ead30 | ||
|
|
fa2e4426f4 | ||
|
|
f911b9ff16 | ||
|
|
ffd5253f59 | ||
|
|
2d52cbe920 | ||
|
|
6f1b45cf8a | ||
|
|
f859e86a08 | ||
|
|
bbcc6eaa44 | ||
|
|
3222a6c54c | ||
|
|
796e7b53fa | ||
|
|
8412e68929 |
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
|
||||||
11
Dockerfile
11
Dockerfile
@@ -1,15 +1,22 @@
|
|||||||
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"]
|
CMD ["cloudflare-ddns-updater"]
|
||||||
63
README.md
63
README.md
@@ -1,21 +1,42 @@
|
|||||||
|

|
||||||
|
|
||||||
## What is Cloudflare Dynamic DNS?
|
## What is Cloudflare Dynamic DNS?
|
||||||
DNS records are static, and it does not play well with dynamic IP addresses. Now, to solve that problem, you’ll need to set up dynamic DNS. Cloudflare provides an API that allows you to manage DNS records programmatically.
|
DNS records are static, and it does not play well with dynamic IP addresses. Now, to solve that problem, you’ll need to set up dynamic DNS. Cloudflare provides an API that allows you to manage DNS records programmatically.
|
||||||
|
|
||||||
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. For now only resolving via `https://ifconfig.me` is implemented. (default: `ifconfig.me`)
|
- `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`
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
@@ -37,18 +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
|
||||||
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`
|
||||||
@@ -56,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
|
||||||
@@ -2,6 +2,7 @@ version: "3"
|
|||||||
services:
|
services:
|
||||||
cf-dns-updater:
|
cf-dns-updater:
|
||||||
image: mkelcik/cloudflare-ddns-update:latest
|
image: mkelcik/cloudflare-ddns-update:latest
|
||||||
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com
|
- CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com
|
||||||
- CLOUDFLARE_API_KEY=your_cloudflare_api_key
|
- CLOUDFLARE_API_KEY=your_cloudflare_api_key
|
||||||
|
|||||||
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,31 +10,55 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
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 {
|
||||||
@@ -52,11 +76,26 @@ func NewConfig() Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
DnsRecordsToCheck: parseDNSToCheck(os.Getenv(envKeyDnsToCheck)),
|
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||||
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
DNSProviderTag: getEnvDefault(envKeyDNSProviderTag, "cloudflare"),
|
||||||
|
PublicDNSServer: getEnvDefault(envKeyPublicDNSServer, "1.1.1.1"),
|
||||||
|
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)),
|
||||||
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,11 +1,13 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
func parseDNSToCheck(data string) []string {
|
func parseCommaDelimited(data string) []string {
|
||||||
out := make([]string, 0, strings.Count(data, ",")+1)
|
out := make([]string, 0, strings.Count(data, ",")+1)
|
||||||
for _, dns := range strings.Split(data, ",") {
|
for _, item := range strings.Split(data, ",") {
|
||||||
if w := strings.TrimSpace(dns); w != "" {
|
if w := strings.TrimSpace(item); w != "" {
|
||||||
out = append(out, w)
|
out = append(out, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ func Test_parseDNSToCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := parseDNSToCheck(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
if got := parseCommaDelimited(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("parseDNSToCheck() = %v, want %v", got, tt.want)
|
t.Errorf("parseCommaDelimited() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
81
internal/notifications/types.go
Normal file
81
internal/notifications/types.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
configDelimiter = "@"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Doer interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Notifiers []Notifier
|
||||||
|
|
||||||
|
func (n Notifiers) NotifyWithLog(ctx context.Context, notification Notification) error {
|
||||||
|
var outErr error
|
||||||
|
for _, notifier := range n {
|
||||||
|
if err := notifier.Notify(ctx, notification); err != nil {
|
||||||
|
outErr = errors.Join(outErr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("Notification sent via %s\n", notifier.Tag())
|
||||||
|
}
|
||||||
|
return outErr
|
||||||
|
}
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
OldIp net.IP `json:"old_ip,omitempty"`
|
||||||
|
NewIp net.IP `json:"new_ip"`
|
||||||
|
CheckedAt time.Time `json:"checked_at"`
|
||||||
|
ResolverTag string `json:"resolver_tag"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
WebhookToken string `json:"token,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n Notification) ToSlice() []string {
|
||||||
|
return []string{n.OldIp.String(), n.NewIp.String(), n.CheckedAt.Format(time.RFC3339), n.ResolverTag, n.Domain}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Available = map[string]func(string) (Notifier, error){
|
||||||
|
webhookTag: func(config string) (Notifier, error) {
|
||||||
|
parts := strings.Split(config, configDelimiter)
|
||||||
|
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return nil, fmt.Errorf("wrong webhook config, missing url part")
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewWebhookNotification(WebhookConfig{Url: parts[1]}, &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Notifier interface {
|
||||||
|
Tag() string
|
||||||
|
Notify(ctx context.Context, notification Notification) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNotifiers(tags []string) Notifiers {
|
||||||
|
out := Notifiers{}
|
||||||
|
for _, t := range tags {
|
||||||
|
if initFn, ok := Available[strings.Split(t, configDelimiter)[0]]; ok {
|
||||||
|
notifier, err := initFn(t)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, notifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
64
internal/notifications/webhook.go
Normal file
64
internal/notifications/webhook.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
webhookTag = "webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebhookConfig struct {
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookNotification struct {
|
||||||
|
config WebhookConfig
|
||||||
|
client Doer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebhookNotification) Tag() string {
|
||||||
|
return webhookTag
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookNotification(config WebhookConfig, client Doer) *WebhookNotification {
|
||||||
|
return &WebhookNotification{config: config, client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebhookNotification) getRequestBody(notification Notification) (io.Reader, error) {
|
||||||
|
out := bytes.NewBuffer(nil)
|
||||||
|
if err := json.NewEncoder(out).Encode(notification); err != nil {
|
||||||
|
return nil, fmt.Errorf("error encoding json notification body: %w", err)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebhookNotification) Notify(ctx context.Context, notification Notification) error {
|
||||||
|
body, err := w.getRequestBody(notification)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("WebhookNotification::NotifyWithLog error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, w.config.Url, body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("WebhookNotification::NotifyWithLog error creating request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := w.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("WebhookNotification::NotifyWithLog error while sending notification: %w", err)
|
||||||
|
}
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("WebhookNotification::NotifyWithLog unexpected non 2xx code %d returned", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,41 +2,34 @@ package public_resolvers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var NoIPInResponseError = errors.New("no ip found in response")
|
||||||
IfConfigMeTag = "ifconfig.me"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Doer interface {
|
type Doer interface {
|
||||||
Do(*http.Request) (*http.Response, error)
|
Do(*http.Request) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
type ipParserFunc func(reader io.Reader) (string, error)
|
||||||
ifConfigMeUrl = "https://ifconfig.me"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IfConfigMe struct {
|
func defaultIpParser(reader io.Reader) (string, error) {
|
||||||
|
out, err := io.ReadAll(reader)
|
||||||
|
return string(out), err
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseResolver struct {
|
||||||
client Doer
|
client Doer
|
||||||
|
url string
|
||||||
|
ipParser ipParserFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultIfConfigMe() *IfConfigMe {
|
func (i baseResolver) ResolvePublicIp(ctx context.Context) (net.IP, error) {
|
||||||
return NewIfConfigMe(&http.Client{
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, i.url, nil)
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIfConfigMe(c Doer) *IfConfigMe {
|
|
||||||
return &IfConfigMe{client: c}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i IfConfigMe) ResolvePublicIp(ctx context.Context) (net.IP, error) {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ifConfigMeUrl, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return net.IP{}, fmt.Errorf("error creating ifconfig request: %w", err)
|
return net.IP{}, fmt.Errorf("error creating ifconfig request: %w", err)
|
||||||
}
|
}
|
||||||
@@ -53,10 +46,10 @@ func (i IfConfigMe) ResolvePublicIp(ctx context.Context) (net.IP, error) {
|
|||||||
return net.IP{}, fmt.Errorf("unexpected response code %d", resp.StatusCode)
|
return net.IP{}, fmt.Errorf("unexpected response code %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
ipText, err := io.ReadAll(resp.Body)
|
ipText, err := i.ipParser(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return net.IP{}, fmt.Errorf("error reading body: %w", err)
|
return net.IP{}, fmt.Errorf("error reading body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return net.ParseIP(string(ipText)), nil
|
return net.ParseIP(ipText), nil
|
||||||
}
|
}
|
||||||
100
internal/public_resolvers/base_resolver_test.go
Normal file
100
internal/public_resolvers/base_resolver_test.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package public_resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RoundTripFunc .
|
||||||
|
type RoundTripFunc func(req *http.Request) *http.Response
|
||||||
|
|
||||||
|
// RoundTrip .
|
||||||
|
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
return f(req), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
|
||||||
|
func NewTestClient(fn RoundTripFunc) *http.Client {
|
||||||
|
return &http.Client{
|
||||||
|
Transport: fn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_baseResolver_ResolvePublicIp(t *testing.T) {
|
||||||
|
|
||||||
|
testUrl := "http://my-test-url.url"
|
||||||
|
testIp := `192.168.0.100`
|
||||||
|
|
||||||
|
client := NewTestClient(func(req *http.Request) *http.Response {
|
||||||
|
|
||||||
|
if req.URL.String() != testUrl {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: 500,
|
||||||
|
// Send response to be tested
|
||||||
|
Body: io.NopCloser(bytes.NewBufferString(`invalid url`)),
|
||||||
|
// Must be set to non-nil value or it panics
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
// Send response to be tested
|
||||||
|
Body: io.NopCloser(bytes.NewBufferString(testIp)),
|
||||||
|
// Must be set to non-nil value or it panics
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
client Doer
|
||||||
|
url string
|
||||||
|
fn ipParserFunc
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want net.IP
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "check parse ip4",
|
||||||
|
fields: fields{
|
||||||
|
client: client,
|
||||||
|
url: testUrl,
|
||||||
|
fn: defaultIpParser,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
want: net.ParseIP(testIp),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
i := baseResolver{
|
||||||
|
client: tt.fields.client,
|
||||||
|
url: tt.fields.url,
|
||||||
|
ipParser: tt.fields.fn,
|
||||||
|
}
|
||||||
|
got, err := i.ResolvePublicIp(tt.args.ctx)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ResolvePublicIp() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("ResolvePublicIp() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
49
internal/public_resolvers/cloudflare_trace.go
Normal file
49
internal/public_resolvers/cloudflare_trace.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package public_resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CloudflareTraceTag = "1.1.1.1"
|
||||||
|
CloudflareTraceUrl = "https://1.1.1.1/cdn-cgi/trace"
|
||||||
|
|
||||||
|
ipPrefix = "ip="
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloudflareTrace struct {
|
||||||
|
baseResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultCloudflareTrace() *CloudflareTrace {
|
||||||
|
return NewCloudflareTrace(&http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloudflareTraceResponseParser(reader io.Reader) (string, error) {
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range strings.Split(string(data), "\n") {
|
||||||
|
if strings.Index(row, ipPrefix) == 0 {
|
||||||
|
return strings.TrimSpace(strings.ReplaceAll(row, ipPrefix, "")), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", NoIPInResponseError
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCloudflareTrace(client Doer) *CloudflareTrace {
|
||||||
|
return &CloudflareTrace{
|
||||||
|
baseResolver: baseResolver{
|
||||||
|
client: client,
|
||||||
|
url: CloudflareTraceUrl,
|
||||||
|
ipParser: cloudflareTraceResponseParser,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
78
internal/public_resolvers/cloudflare_trace_test.go
Normal file
78
internal/public_resolvers/cloudflare_trace_test.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package public_resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_cloudflareTraceResponseParser(t *testing.T) {
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
reader io.Reader
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
args: args{
|
||||||
|
reader: bytes.NewBuffer([]byte(`fl=31f118
|
||||||
|
h=1.1.1.1
|
||||||
|
ip=94.113.142.206
|
||||||
|
ts=1683145336.383
|
||||||
|
visit_scheme=https
|
||||||
|
uag=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
|
||||||
|
colo=PRG
|
||||||
|
sliver=none
|
||||||
|
http=http/2
|
||||||
|
loc=CZ
|
||||||
|
tls=TLSv1.3
|
||||||
|
sni=off
|
||||||
|
warp=off
|
||||||
|
gateway=off
|
||||||
|
rbi=off
|
||||||
|
kex=X25519`)),
|
||||||
|
},
|
||||||
|
want: "94.113.142.206",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no ip in response",
|
||||||
|
args: args{
|
||||||
|
reader: bytes.NewBuffer([]byte(`fl=31f118
|
||||||
|
h=1.1.1.1
|
||||||
|
ts=1683145336.383
|
||||||
|
visit_scheme=https
|
||||||
|
uag=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
|
||||||
|
colo=PRG
|
||||||
|
sliver=none
|
||||||
|
http=http/2
|
||||||
|
loc=CZ
|
||||||
|
tls=TLSv1.3
|
||||||
|
sni=off
|
||||||
|
warp=off
|
||||||
|
gateway=off
|
||||||
|
rbi=off
|
||||||
|
kex=X25519`)),
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := cloudflareTraceResponseParser(tt.args.reader)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("cloudflareTraceResponseParser() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("cloudflareTraceResponseParser() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
31
internal/public_resolvers/ifconfigme.go
Normal file
31
internal/public_resolvers/ifconfigme.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package public_resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IfConfigMeTag = "ifconfig.me"
|
||||||
|
ifConfigMeUrl = "https://ifconfig.me"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IfConfigMe struct {
|
||||||
|
baseResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultIfConfigMe() *IfConfigMe {
|
||||||
|
return NewIfConfigMe(&http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIfConfigMe(client Doer) *IfConfigMe {
|
||||||
|
return &IfConfigMe{
|
||||||
|
baseResolver: baseResolver{
|
||||||
|
client: client,
|
||||||
|
url: ifConfigMeUrl,
|
||||||
|
ipParser: defaultIpParser,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
31
internal/public_resolvers/v4identme.go
Normal file
31
internal/public_resolvers/v4identme.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package public_resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
V4IdentMeTag = "v4.ident.me"
|
||||||
|
v4IdentMeUrl = "https://v4.ident.me/"
|
||||||
|
)
|
||||||
|
|
||||||
|
type V4IdentMe struct {
|
||||||
|
baseResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewV4IdentMeDefault() *V4IdentMe {
|
||||||
|
return NewV4IdentMe(&http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewV4IdentMe(client Doer) *V4IdentMe {
|
||||||
|
return &V4IdentMe{
|
||||||
|
baseResolver: baseResolver{
|
||||||
|
client: client,
|
||||||
|
url: v4IdentMeUrl,
|
||||||
|
ipParser: defaultIpParser,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
106
main.go
106
main.go
@@ -8,22 +8,40 @@ 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/public_resolvers"
|
"hub.cybercinch.nz/cybercinch/ddns-update/internal/notifications"
|
||||||
|
"hub.cybercinch.nz/cybercinch/ddns-update/internal/public_resolvers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PublicIpResolver interface {
|
type PublicIpResolver interface {
|
||||||
ResolvePublicIp(ctx context.Context) (net.IP, error)
|
ResolvePublicIp(ctx context.Context) (net.IP, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getResolver(resolverName string) PublicIpResolver {
|
func getResolver(resolverName string) (PublicIpResolver, string) {
|
||||||
switch resolverName {
|
switch resolverName {
|
||||||
// HERE add another resolver if needed
|
// HERE add another resolver if needed
|
||||||
|
case public_resolvers.CloudflareTraceTag:
|
||||||
|
return public_resolvers.NewDefaultCloudflareTrace(), public_resolvers.CloudflareTraceTag
|
||||||
|
case 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:
|
||||||
return public_resolvers.NewDefaultIfConfigMe()
|
return public_resolvers.NewDefaultIfConfigMe(), public_resolvers.IfConfigMeTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,57 +54,55 @@ func main() {
|
|||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
api, err := cloudflare.NewWithAPIToken(config.ApiToken)
|
notifiers := notifications.GetNotifiers(config.Notifiers)
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch user details on the account
|
|
||||||
zoneID, err := api.ZoneIDByName(config.CloudflareZone)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// public ip resolver
|
// public ip resolver
|
||||||
publicIpResolver := 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`", currentPublicIP)
|
|
||||||
|
|
||||||
dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID))
|
log.Printf("Current public ip `%s` (resolver: %s)", currentPublicIP, resolverTag)
|
||||||
|
|
||||||
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)
|
||||||
} else {
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := notifiers.NotifyWithLog(ctx, notifications.Notification{
|
||||||
|
OldIp: net.ParseIP(string(current_dns_record)),
|
||||||
|
NewIp: currentPublicIP,
|
||||||
|
CheckedAt: time.Now(),
|
||||||
|
ResolverTag: resolverTag,
|
||||||
|
Domain: dnsRecord,
|
||||||
|
WebhookToken: config.WebhookToken,
|
||||||
|
}); err != nil {
|
||||||
|
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()
|
||||||
@@ -104,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