You've already forked ddns-updater
init
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/env
|
||||||
|
.idea
|
||||||
|
/vendor
|
||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM golang:1.20 as build
|
||||||
|
|
||||||
|
# Copy project sources
|
||||||
|
COPY . /opt/project/
|
||||||
|
WORKDIR /opt/project
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /cloudflare-ddns-updater
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
COPY --from=build /cloudflare-ddns-updater /cloudflare-ddns-updater
|
||||||
|
ENTRYPOINT ["/cloudflare-ddns-updater"]
|
||||||
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module cloudflare-ddns
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require github.com/cloudflare/cloudflare-go v0.66.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
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
|
||||||
|
)
|
||||||
62
internal/config.go
Normal file
62
internal/config.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultCheckInterval = 5 * 60
|
||||||
|
|
||||||
|
envKeyDnsToCheck = "CLOUDFLARE_DNS_TO_CHECK"
|
||||||
|
envKeyPublicIpResolverTag = "PUBLIC_IP_RESOLVER"
|
||||||
|
envKeyCloudflareApiKey = "CLOUDFLARE_API_KEY"
|
||||||
|
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
||||||
|
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
||||||
|
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DnsRecordsToCheck []string
|
||||||
|
PublicIpResolverTag string
|
||||||
|
ApiToken string
|
||||||
|
CloudflareZone string
|
||||||
|
OnChangeComment string
|
||||||
|
CheckInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Validate() error {
|
||||||
|
if c.ApiToken == "" {
|
||||||
|
return fmt.Errorf("empty api token env key %s", envKeyCloudflareApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.CloudflareZone == "" {
|
||||||
|
return fmt.Errorf("empty zone in env key %s", envKeyCloudflareZone)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.DnsRecordsToCheck) == 0 {
|
||||||
|
return fmt.Errorf("no dns to check defined in env key %s", envKeyDnsToCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig() Config {
|
||||||
|
checkInterval, err := strconv.ParseInt(os.Getenv(envKeyCheckIntervalSeconds), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("wrong `%s` value. Check interval set default(%ds)", envKeyCheckIntervalSeconds, defaultCheckInterval)
|
||||||
|
checkInterval = defaultCheckInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
return Config{
|
||||||
|
DnsRecordsToCheck: parseDNSToCheck(os.Getenv(envKeyDnsToCheck)),
|
||||||
|
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
||||||
|
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||||
|
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||||
|
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
||||||
|
CheckInterval: time.Duration(checkInterval) * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
20
internal/helpers.go
Normal file
20
internal/helpers.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func parseDNSToCheck(data string) []string {
|
||||||
|
out := make([]string, 0, strings.Count(data, ",")+1)
|
||||||
|
for _, dns := range strings.Split(data, ",") {
|
||||||
|
out = append(out, strings.TrimSpace(dns))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func Contains[T comparable](haystack []T, needle T) bool {
|
||||||
|
for _, v := range haystack {
|
||||||
|
if v == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
107
main.go
Normal file
107
main.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cloudflare-ddns/internal"
|
||||||
|
"cloudflare-ddns/public_resolvers"
|
||||||
|
"github.com/cloudflare/cloudflare-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PublicIpResolver interface {
|
||||||
|
ResolvePublicIp(ctx context.Context) (net.IP, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResolver(resolverName string) PublicIpResolver {
|
||||||
|
switch resolverName {
|
||||||
|
// HERE add another resolver if needed
|
||||||
|
case public_resolvers.IfConfigMeTag:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return public_resolvers.NewIfConfigMe(&http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
config := internal.NewConfig()
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPublicIP, err := getResolver(config.PublicIpResolverTag).ResolvePublicIp(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dnsRecord := range dns {
|
||||||
|
if internal.Contains(config.DnsRecordsToCheck, dnsRecord.Name) {
|
||||||
|
log.Printf("Checking record `%s` with current value `%s` ...", dnsRecord.Name, dnsRecord.Content)
|
||||||
|
if currentPublicIP.String() == dnsRecord.Content {
|
||||||
|
log.Println("OK")
|
||||||
|
continue // no update needed
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
log.Printf("Updated to `%s`", currentPublicIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
53
public_resolvers/ifconfigme.go
Normal file
53
public_resolvers/ifconfigme.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package public_resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IfConfigMeTag = "ifconfig.me"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Doer interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ifConfigMeUrl = "https://ifconfig.me"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IfConfigMe struct {
|
||||||
|
client Doer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIfConfigMe(c Doer) *IfConfigMe {
|
||||||
|
return &IfConfigMe{client: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i IfConfigMe) ResolvePublicIp(ctx context.Context) (net.IP, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ifConfigMeUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return net.IP{}, fmt.Errorf("error creating ifconfig request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := i.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return net.IP{}, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return net.IP{}, fmt.Errorf("unexpected response code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipText, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return net.IP{}, fmt.Errorf("error reading body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return net.ParseIP(string(ipText)), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user