You've already forked ddns-updater
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
401884304e | ||
|
|
df74a0e159 | ||
|
|
316a40b662 | ||
|
|
17afc65f92 | ||
|
|
51958d719e | ||
|
|
10a5a12b86 | ||
|
|
c7646cbf63 | ||
|
|
6a028ead30 | ||
|
|
fa2e4426f4 | ||
|
|
f911b9ff16 | ||
|
|
ffd5253f59 |
6
.github/workflows/quality-checks.yml
vendored
6
.github/workflows/quality-checks.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20'
|
||||
go-version: '1.21'
|
||||
cache: false
|
||||
- uses: actions/checkout@v3
|
||||
- name: golangci-lint
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Prepare go environment
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20'
|
||||
go-version: '1.21.5'
|
||||
cache: false
|
||||
- name: Install dep scanner
|
||||
run: |
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- name: Prepare go environment
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20'
|
||||
go-version: '1.21'
|
||||
cache: false
|
||||
- name: Run tests
|
||||
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 . /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
|
||||
|
||||
|
||||
27
README.md
27
README.md
@@ -5,7 +5,7 @@ DNS records are static, and it does not play well with dynamic IP addresses. Now
|
||||
|
||||
To set up a Cloudflare dynamic DNS, you’ll need to run a process on a client inside your network that does two main actions: get your network’s current public IP address and automatically update the corresponding DNS record.
|
||||
|
||||
This simple updater do the job.
|
||||
This simple updater do the job, and send notifications, if change happen.
|
||||
|
||||
## How to run
|
||||
### Environment variables
|
||||
@@ -18,6 +18,24 @@ Before run, you need configure this environment variables.
|
||||
- `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`)
|
||||
- `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
|
||||
|
||||
@@ -49,6 +67,7 @@ services:
|
||||
environment:
|
||||
- CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com
|
||||
- CLOUDFLARE_API_KEY=your_cloudflare_api_key
|
||||
- NOTIFIERS=webhook@http://localhost/cloudflare-updated-notification
|
||||
- CLOUDFLARE_ZONE=testdomain.com
|
||||
- ON_CHANGE_COMMENT="automatically updated"
|
||||
- CHECK_INTERVAL_SECONDS=300
|
||||
@@ -59,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
|
||||
```
|
||||
|
||||
### Future plans
|
||||
|
||||
- prometheus metrics
|
||||
- mqtt and rabbitmq notifiers
|
||||
- IPv6 support
|
||||
|
||||
### Contributing
|
||||
|
||||
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
|
||||
|
||||
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 (
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.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/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
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-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/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||
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/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=
|
||||
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/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/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/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/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
||||
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
||||
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
||||
envKeyNotifiers = "NOTIFIERS"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -25,6 +26,7 @@ type Config struct {
|
||||
ApiToken string
|
||||
CloudflareZone string
|
||||
OnChangeComment string
|
||||
Notifiers []string
|
||||
CheckInterval time.Duration
|
||||
}
|
||||
|
||||
@@ -52,11 +54,12 @@ func NewConfig() Config {
|
||||
}
|
||||
|
||||
return Config{
|
||||
DnsRecordsToCheck: parseDNSToCheck(os.Getenv(envKeyDnsToCheck)),
|
||||
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
||||
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
||||
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
|
||||
CheckInterval: time.Duration(checkInterval) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package internal
|
||||
|
||||
import "strings"
|
||||
|
||||
func parseDNSToCheck(data string) []string {
|
||||
func parseCommaDelimited(data string) []string {
|
||||
out := make([]string, 0, strings.Count(data, ",")+1)
|
||||
for _, dns := range strings.Split(data, ",") {
|
||||
if w := strings.TrimSpace(dns); w != "" {
|
||||
for _, item := range strings.Split(data, ",") {
|
||||
if w := strings.TrimSpace(item); w != "" {
|
||||
out = append(out, w)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ func Test_parseDNSToCheck(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := parseDNSToCheck(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseDNSToCheck() = %v, want %v", got, tt.want)
|
||||
if got := parseCommaDelimited(tt.args.data); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseCommaDelimited() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
19
main.go
19
main.go
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
||||
"github.com/mkelcik/cloudflare-ddns-update/notifications"
|
||||
"github.com/mkelcik/cloudflare-ddns-update/public_resolvers"
|
||||
)
|
||||
|
||||
@@ -51,6 +52,8 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
notifiers := notifications.GetNotifiers(config.Notifiers)
|
||||
|
||||
// public ip resolver
|
||||
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
||||
|
||||
@@ -80,14 +83,24 @@ func main() {
|
||||
}
|
||||
|
||||
if config.OnChangeComment != "" {
|
||||
update.Comment = 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)
|
||||
} else {
|
||||
log.Printf("Updated to `%s`", currentPublicIP)
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user