You've already forked ddns-updater
Compare commits
5 Commits
main
...
ignore-ip-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6f9ce7aea | ||
|
|
26a3cf2a18 | ||
|
|
bad00230a9 | ||
|
|
29c3312ab1 | ||
|
|
ecd2776a50 |
@@ -1,49 +0,0 @@
|
|||||||
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
|
|
||||||
10
Dockerfile
10
Dockerfile
@@ -1,21 +1,15 @@
|
|||||||
FROM golang:1.22 as build
|
FROM golang:1.21 as build
|
||||||
|
|
||||||
# Copy project sources
|
# Copy project sources
|
||||||
COPY . /opt/project/
|
COPY . /opt/project/
|
||||||
WORKDIR /opt/project
|
WORKDIR /opt/project
|
||||||
|
|
||||||
# Install Pre-Requisites
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates=20230311
|
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"]
|
||||||
33
README.md
33
README.md
@@ -12,7 +12,7 @@ This simple updater do the job, and send notifications, if change happen.
|
|||||||
|
|
||||||
Before run, you need configure this environment variables.
|
Before run, you need configure this environment variables.
|
||||||
|
|
||||||
- `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_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`
|
||||||
- `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
|
||||||
@@ -31,8 +31,7 @@ Request body example:
|
|||||||
"new_ip": "xxx.xxx.xxx.xxx",
|
"new_ip": "xxx.xxx.xxx.xxx",
|
||||||
"checked_at": "2023-05-04T17:39:42.942850354+02:00",
|
"checked_at": "2023-05-04T17:39:42.942850354+02:00",
|
||||||
"resolver_tag": "ifconfig.me",
|
"resolver_tag": "ifconfig.me",
|
||||||
"domain": "my.domain.com",
|
"domain": "my.domain.com"
|
||||||
"token": "a-webhook-token"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -58,40 +57,20 @@ 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 for Cloudflare`
|
### Via `docker-compose`
|
||||||
```yaml
|
```yaml
|
||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
cf-dns-updater:
|
cf-dns-updater:
|
||||||
image: hub.cybercinch.nz/cybercinch/ddns-update:latest
|
image: mkelcik/cloudflare-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
|
||||||
- DNS_NAMES=my.testdomain.com,your.testdomain.com
|
|
||||||
- NOTIFIERS=webhook@http://localhost/cloudflare-updated-notification
|
- NOTIFIERS=webhook@http://localhost/cloudflare-updated-notification
|
||||||
|
- CLOUDFLARE_ZONE=testdomain.com
|
||||||
- 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`
|
||||||
|
|||||||
13
go.mod
13
go.mod
@@ -1,21 +1,14 @@
|
|||||||
module hub.cybercinch.nz/cybercinch/ddns-update
|
module github.com/mkelcik/cloudflare-ddns-update
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
go 1.22
|
require github.com/cloudflare/cloudflare-go v0.83.0
|
||||||
|
|
||||||
toolchain go1.22.1
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/cloudflare/cloudflare-go v0.83.0
|
|
||||||
github.com/levelzerotechnology/directadmin-go v0.0.0-20240302013738-63b7793ebfd3
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
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.5 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||||
github.com/spf13/cast v1.6.0 // indirect
|
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
|||||||
35
go.sum
35
go.sum
@@ -1,52 +1,43 @@
|
|||||||
|
github.com/cloudflare/cloudflare-go v0.66.0 h1:B74IvVGQ4UFYJnqQSK/9GbR+Y1HwNxqqdN2Bmg0dckg=
|
||||||
|
github.com/cloudflare/cloudflare-go v0.66.0/go.mod h1:tA44hjU9FfycofKT+lWWMHb/dEq1pRbiVPGuJo1WzLQ=
|
||||||
github.com/cloudflare/cloudflare-go v0.83.0 h1:aq85Hbr5W6KfXZV7v3lx6fhBkiu0FYqY+3+xzG14mdY=
|
github.com/cloudflare/cloudflare-go v0.83.0 h1:aq85Hbr5W6KfXZV7v3lx6fhBkiu0FYqY+3+xzG14mdY=
|
||||||
github.com/cloudflare/cloudflare-go v0.83.0/go.mod h1:5pkAzpoWJYI5NekLZoRryQAcghYDhdbUxdcal1f7lu4=
|
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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
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.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
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-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
|
||||||
|
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 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||||
|
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/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
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/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,55 +10,35 @@ 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"
|
||||||
|
envKeyIgnoredIpChange = "IGNORED_IP_CHANGE"
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
CloudflareOnChangeComment string
|
OnChangeComment string
|
||||||
Notifiers []string
|
Notifiers []string
|
||||||
CheckInterval time.Duration
|
CheckInterval time.Duration
|
||||||
|
IgnoredIpChange []string
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "directadmin":
|
if c.CloudflareZone == "" {
|
||||||
if c.DirectadminUrl == "" {
|
return fmt.Errorf("empty zone in env key %s", envKeyCloudflareZone)
|
||||||
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 {
|
||||||
@@ -71,30 +51,18 @@ 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 or missing `%s` value. Check interval set to default(%ds)", envKeyCheckIntervalSeconds, defaultCheckInterval)
|
log.Printf("wrong `%s` value. Check interval set default(%ds)", envKeyCheckIntervalSeconds, defaultCheckInterval)
|
||||||
checkInterval = defaultCheckInterval
|
checkInterval = defaultCheckInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||||
DNSProviderTag: getEnvDefault(envKeyDNSProviderTag, "cloudflare"),
|
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
||||||
PublicIpResolverTag: getEnvDefault(envKeyPublicIpResolverTag, "icanhazip"),
|
|
||||||
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||||
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||||
CloudflareOnChangeComment: os.Getenv(envKeyOnChangeComment),
|
OnChangeComment: 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"),
|
IgnoredIpChange: parseCommaDelimited(os.Getenv(envKeyIgnoredIpChange)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
CloudflareOnChangeComment: tt.fields.OnChangeComment,
|
OnChangeComment: 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 {
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
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[:]
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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])
|
|
||||||
}
|
|
||||||
26
internal/ignore_list.go
Normal file
26
internal/ignore_list.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkAddress(address, pattern string) bool {
|
||||||
|
pattern = "^" + pattern + "$"
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
return re.MatchString(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IgnoredIpChange(ip net.IP, ignored []string) bool {
|
||||||
|
if len(ignored) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range ignored {
|
||||||
|
if checkAddress(ip.String(), i) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
75
internal/ignore_list_test.go
Normal file
75
internal/ignore_list_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_checkAddress(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
address string
|
||||||
|
pattern string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
args: args{
|
||||||
|
address: "192.168.0.1",
|
||||||
|
pattern: "",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
}, {
|
||||||
|
name: "true match 192.*",
|
||||||
|
args: args{
|
||||||
|
address: "192.168.0.1",
|
||||||
|
pattern: "192.*",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
}, {
|
||||||
|
name: "false match 193.*",
|
||||||
|
args: args{
|
||||||
|
address: "192.168.0.1",
|
||||||
|
pattern: "193.*",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "true match 192.168.0.1",
|
||||||
|
args: args{
|
||||||
|
address: "192.168.0.1",
|
||||||
|
pattern: "192.168.0.1",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "false not match 192.168.0.2",
|
||||||
|
args: args{
|
||||||
|
address: "192.168.0.1",
|
||||||
|
pattern: "192.168.0.2",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
}, {
|
||||||
|
name: "true match 192.168.0.*",
|
||||||
|
args: args{
|
||||||
|
address: "192.168.0.10",
|
||||||
|
pattern: "192.168.0.*",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
}, {
|
||||||
|
name: "false match 192.168.0.*",
|
||||||
|
args: args{
|
||||||
|
address: "192.168.1.10",
|
||||||
|
pattern: "192.168.0.*",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := checkAddress(tt.args.address, tt.args.pattern); got != tt.want {
|
||||||
|
t.Errorf("checkAddress() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
91
main.go
91
main.go
@@ -8,10 +8,10 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hub.cybercinch.nz/cybercinch/ddns-update/internal"
|
"github.com/cloudflare/cloudflare-go"
|
||||||
"hub.cybercinch.nz/cybercinch/ddns-update/internal/dns_providers"
|
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
||||||
"hub.cybercinch.nz/cybercinch/ddns-update/internal/notifications"
|
"github.com/mkelcik/cloudflare-ddns-update/notifications"
|
||||||
"hub.cybercinch.nz/cybercinch/ddns-update/internal/public_resolvers"
|
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PublicIpResolver interface {
|
type PublicIpResolver interface {
|
||||||
@@ -25,8 +25,6 @@ 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:
|
||||||
@@ -34,17 +32,6 @@ 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()
|
||||||
@@ -54,55 +41,75 @@ 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)
|
||||||
|
|
||||||
|
// check if ip is not in ignore list
|
||||||
|
if internal.IgnoredIpChange(currentPublicIP, config.IgnoredIpChange) {
|
||||||
|
log.Printf("Ignored ip change `%s`, skipping.", currentPublicIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dnsRecord := range config.DnsRecordsToCheck {
|
for _, dnsRecord := range dns {
|
||||||
current_dns_record, err := dnsProvider.FetchRecord(dnsRecord)
|
if internal.Contains(config.DnsRecordsToCheck, dnsRecord.Name) {
|
||||||
if err != nil {
|
log.Printf("Checking record `%s` with current value `%s` ...", dnsRecord.Name, dnsRecord.Content)
|
||||||
log.Fatalf("Failed to fetch DNS record: %s", err)
|
if currentPublicIP.String() == dnsRecord.Content {
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dnsProvider.UpdateRecord(dnsRecord, currentPublicIP.String()); err != nil {
|
update := cloudflare.UpdateDNSRecordParams{
|
||||||
|
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(string(current_dns_record)),
|
OldIp: net.ParseIP(dnsRecord.Content),
|
||||||
NewIp: currentPublicIP,
|
NewIp: currentPublicIP,
|
||||||
CheckedAt: time.Now(),
|
CheckedAt: time.Now(),
|
||||||
ResolverTag: resolverTag,
|
ResolverTag: resolverTag,
|
||||||
Domain: dnsRecord,
|
Domain: dnsRecord.Name,
|
||||||
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()
|
||||||
@@ -120,3 +127,23 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ 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 {
|
||||||
@@ -39,7 +35,6 @@ 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,6 +13,10 @@ const (
|
|||||||
webhookTag = "webhook"
|
webhookTag = "webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Doer interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
type WebhookConfig struct {
|
type WebhookConfig struct {
|
||||||
Url string
|
Url string
|
||||||
}
|
}
|
||||||
@@ -49,8 +53,6 @@ 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)
|
||||||
Reference in New Issue
Block a user