You've already forked ddns-updater
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
401884304e | ||
|
|
df74a0e159 | ||
|
|
316a40b662 | ||
|
|
17afc65f92 | ||
|
|
51958d719e | ||
|
|
10a5a12b86 | ||
|
|
c7646cbf63 | ||
|
|
6a028ead30 | ||
|
|
fa2e4426f4 | ||
|
|
f911b9ff16 | ||
|
|
ffd5253f59 | ||
|
|
2d52cbe920 | ||
|
|
6f1b45cf8a | ||
|
|
f859e86a08 | ||
|
|
bbcc6eaa44 | ||
|
|
3222a6c54c |
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 ./...
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM golang:1.20 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
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates=20210119
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates=20230311
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -1,9 +1,11 @@
|
|||||||
|

|
||||||
|
|
||||||
## 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
|
||||||
@@ -15,7 +17,25 @@ Before run, you need configure this environment variables.
|
|||||||
- `CLOUDFLARE_ZONE` - (required) zone name with domain you want to check. See: [https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones](https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones)
|
- `CLOUDFLARE_ZONE` - (required) zone name with domain you want to check. See: [https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones](https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones)
|
||||||
- `ON_CHANGE_COMMENT` - (optional) in the event that the ip address of the dns record changes, this comment will be added to the record
|
- `ON_CHANGE_COMMENT` - (optional) in the event that the ip address of the dns record changes, this comment will be added to the record
|
||||||
- `CHECK_INTERVAL_SECONDS` - (optional) how often will the ip address of the records be checked (default: `300`)
|
- `CHECK_INTERVAL_SECONDS` - (optional) how often will the ip address of the records be checked (default: `300`)
|
||||||
- `PUBLIC_IP_RESOLVER` - (optional) public ip address resolver. (default: `ifconfig.me`) Available: `ifconfig.me`, `v4.ident.me`
|
- `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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Other notification methods will be implemented later (check future plans section).
|
||||||
|
|
||||||
### Building from source
|
### Building from source
|
||||||
|
|
||||||
@@ -47,6 +67,7 @@ services:
|
|||||||
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
|
||||||
|
- NOTIFIERS=webhook@http://localhost/cloudflare-updated-notification
|
||||||
- CLOUDFLARE_ZONE=testdomain.com
|
- CLOUDFLARE_ZONE=testdomain.com
|
||||||
- ON_CHANGE_COMMENT="automatically updated"
|
- ON_CHANGE_COMMENT="automatically updated"
|
||||||
- CHECK_INTERVAL_SECONDS=300
|
- CHECK_INTERVAL_SECONDS=300
|
||||||
@@ -57,6 +78,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
|
||||||
13
go.mod
13
go.mod
@@ -1,14 +1,15 @@
|
|||||||
module github.com/mkelcik/cloudflare-ddns-update
|
module github.com/mkelcik/cloudflare-ddns-update
|
||||||
|
|
||||||
go 1.20
|
go 1.21
|
||||||
|
|
||||||
require github.com/cloudflare/cloudflare-go v0.66.0
|
require github.com/cloudflare/cloudflare-go v0.83.0
|
||||||
|
|
||||||
require (
|
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
|
golang.org/x/net v0.19.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
13
go.sum
13
go.sum
@@ -1,8 +1,12 @@
|
|||||||
github.com/cloudflare/cloudflare-go v0.66.0 h1:B74IvVGQ4UFYJnqQSK/9GbR+Y1HwNxqqdN2Bmg0dckg=
|
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.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/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/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.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
@@ -13,6 +17,8 @@ github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrj
|
|||||||
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-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.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/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -21,10 +27,17 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
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/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
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.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/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
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.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/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=
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const (
|
|||||||
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
||||||
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
||||||
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
||||||
|
envKeyNotifiers = "NOTIFIERS"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -25,6 +26,7 @@ type Config struct {
|
|||||||
ApiToken string
|
ApiToken string
|
||||||
CloudflareZone string
|
CloudflareZone string
|
||||||
OnChangeComment string
|
OnChangeComment string
|
||||||
|
Notifiers []string
|
||||||
CheckInterval time.Duration
|
CheckInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,11 +54,12 @@ func NewConfig() Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
DnsRecordsToCheck: parseDNSToCheck(os.Getenv(envKeyDnsToCheck)),
|
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||||
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
||||||
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||||
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||||
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
||||||
|
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
|
||||||
CheckInterval: time.Duration(checkInterval) * time.Second,
|
CheckInterval: time.Duration(checkInterval) * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ 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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
23
main.go
23
main.go
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/cloudflare/cloudflare-go"
|
"github.com/cloudflare/cloudflare-go"
|
||||||
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
||||||
|
"github.com/mkelcik/cloudflare-ddns-update/notifications"
|
||||||
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ type PublicIpResolver interface {
|
|||||||
func getResolver(resolverName string) (PublicIpResolver, string) {
|
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:
|
case public_resolvers.V4IdentMeTag:
|
||||||
return public_resolvers.NewV4IdentMeDefault(), public_resolvers.V4IdentMeTag
|
return public_resolvers.NewV4IdentMeDefault(), public_resolvers.V4IdentMeTag
|
||||||
case public_resolvers.IfConfigMeTag:
|
case public_resolvers.IfConfigMeTag:
|
||||||
@@ -49,6 +52,8 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifiers := notifications.GetNotifiers(config.Notifiers)
|
||||||
|
|
||||||
// public ip resolver
|
// public ip resolver
|
||||||
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
||||||
|
|
||||||
@@ -57,7 +62,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
log.Printf("Current public ip `%s` (%s)", currentPublicIP, resolverTag)
|
log.Printf("Current public ip `%s` (resolver: %s)", currentPublicIP, resolverTag)
|
||||||
|
|
||||||
dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID))
|
dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,14 +83,24 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.OnChangeComment != "" {
|
if config.OnChangeComment != "" {
|
||||||
update.Comment = config.OnChangeComment
|
update.Comment = &config.OnChangeComment
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), update); err != nil {
|
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
|
||||||
log.Printf("Updated to `%s`", currentPublicIP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := notifiers.NotifyWithLog(ctx, notifications.Notification{
|
||||||
|
OldIp: net.ParseIP(dnsRecord.Content),
|
||||||
|
NewIp: currentPublicIP,
|
||||||
|
CheckedAt: time.Now(),
|
||||||
|
ResolverTag: resolverTag,
|
||||||
|
Domain: dnsRecord.Name,
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("errors in notifications: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("Updated to `%s`", currentPublicIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
76
notifications/types.go
Normal file
76
notifications/types.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
configDelimiter = "@"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
66
notifications/webhook.go
Normal file
66
notifications/webhook.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
webhookTag = "webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Doer interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,19 +2,30 @@ package public_resolvers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var NoIPInResponseError = errors.New("no ip found in response")
|
||||||
|
|
||||||
type Doer interface {
|
type Doer interface {
|
||||||
Do(*http.Request) (*http.Response, error)
|
Do(*http.Request) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ipParserFunc func(reader io.Reader) (string, error)
|
||||||
|
|
||||||
|
func defaultIpParser(reader io.Reader) (string, error) {
|
||||||
|
out, err := io.ReadAll(reader)
|
||||||
|
return string(out), err
|
||||||
|
}
|
||||||
|
|
||||||
type baseResolver struct {
|
type baseResolver struct {
|
||||||
client Doer
|
client Doer
|
||||||
url string
|
url string
|
||||||
|
ipParser ipParserFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i baseResolver) ResolvePublicIp(ctx context.Context) (net.IP, error) {
|
func (i baseResolver) ResolvePublicIp(ctx context.Context) (net.IP, error) {
|
||||||
@@ -35,10 +46,10 @@ func (i baseResolver) 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
|
// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
|
||||||
func NewTestClient(fn RoundTripFunc) *http.Client {
|
func NewTestClient(fn RoundTripFunc) *http.Client {
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Transport: RoundTripFunc(fn),
|
Transport: fn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ func Test_baseResolver_ResolvePublicIp(t *testing.T) {
|
|||||||
type fields struct {
|
type fields struct {
|
||||||
client Doer
|
client Doer
|
||||||
url string
|
url string
|
||||||
|
fn ipParserFunc
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -70,6 +71,7 @@ func Test_baseResolver_ResolvePublicIp(t *testing.T) {
|
|||||||
fields: fields{
|
fields: fields{
|
||||||
client: client,
|
client: client,
|
||||||
url: testUrl,
|
url: testUrl,
|
||||||
|
fn: defaultIpParser,
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
@@ -83,6 +85,7 @@ func Test_baseResolver_ResolvePublicIp(t *testing.T) {
|
|||||||
i := baseResolver{
|
i := baseResolver{
|
||||||
client: tt.fields.client,
|
client: tt.fields.client,
|
||||||
url: tt.fields.url,
|
url: tt.fields.url,
|
||||||
|
ipParser: tt.fields.fn,
|
||||||
}
|
}
|
||||||
got, err := i.ResolvePublicIp(tt.args.ctx)
|
got, err := i.ResolvePublicIp(tt.args.ctx)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
|
|||||||
49
public_resolvers/cloudflare_trace.go
Normal file
49
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
public_resolvers/cloudflare_trace_test.go
Normal file
78
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ func NewIfConfigMe(client Doer) *IfConfigMe {
|
|||||||
baseResolver: baseResolver{
|
baseResolver: baseResolver{
|
||||||
client: client,
|
client: client,
|
||||||
url: ifConfigMeUrl,
|
url: ifConfigMeUrl,
|
||||||
|
ipParser: defaultIpParser,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func NewV4IdentMe(client Doer) *V4IdentMe {
|
|||||||
baseResolver: baseResolver{
|
baseResolver: baseResolver{
|
||||||
client: client,
|
client: client,
|
||||||
url: v4IdentMeUrl,
|
url: v4IdentMeUrl,
|
||||||
|
ipParser: defaultIpParser,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user