You've already forked ddns-updater
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
796e7b53fa | ||
|
|
8412e68929 | ||
|
|
262fbff97c | ||
|
|
0d244649d2 |
@@ -15,7 +15,7 @@ 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)
|
||||
- `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. For now only resolving via `https://ifconfig.me` is implemented. (default: `ifconfig.me`)
|
||||
- `PUBLIC_IP_RESOLVER` - (optional) public ip address resolver. (default: `ifconfig.me`) Available: `ifconfig.me`, `v4.ident.me`
|
||||
|
||||
### Building from source
|
||||
|
||||
@@ -43,6 +43,7 @@ version: "3"
|
||||
services:
|
||||
cf-dns-updater:
|
||||
image: mkelcik/cloudflare-ddns-update:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com
|
||||
- CLOUDFLARE_API_KEY=your_cloudflare_api_key
|
||||
|
||||
@@ -2,6 +2,7 @@ version: "3"
|
||||
services:
|
||||
cf-dns-updater:
|
||||
image: mkelcik/cloudflare-ddns-update:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com
|
||||
- CLOUDFLARE_API_KEY=your_cloudflare_api_key
|
||||
|
||||
91
main.go
91
main.go
@@ -17,13 +17,15 @@ type PublicIpResolver interface {
|
||||
ResolvePublicIp(ctx context.Context) (net.IP, error)
|
||||
}
|
||||
|
||||
func getResolver(resolverName string) PublicIpResolver {
|
||||
func getResolver(resolverName string) (PublicIpResolver, string) {
|
||||
switch resolverName {
|
||||
// HERE add another resolver if needed
|
||||
case public_resolvers.V4IdentMeTag:
|
||||
return public_resolvers.NewV4IdentMeDefault(), public_resolvers.V4IdentMeTag
|
||||
case public_resolvers.IfConfigMeTag:
|
||||
fallthrough
|
||||
default:
|
||||
return public_resolvers.NewDefaultIfConfigMe()
|
||||
return public_resolvers.NewDefaultIfConfigMe(), public_resolvers.IfConfigMeTag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,51 +49,58 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("waiting for update tick ...")
|
||||
// public ip resolver
|
||||
publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
|
||||
|
||||
checkFunc := func() {
|
||||
currentPublicIP, err := publicIpResolver.ResolvePublicIp(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Current public ip `%s` (%s)", currentPublicIP, resolverTag)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("checking ...")
|
||||
checkFunc()
|
||||
|
||||
log.Println("waiting for check tick ...")
|
||||
ticker := time.NewTicker(config.CheckInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
log.Println("tick received checking ...")
|
||||
func() {
|
||||
currentPublicIP, err := getResolver(config.PublicIpResolverTag).ResolvePublicIp(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Current public ip `%s`", currentPublicIP)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
checkFunc()
|
||||
case <-ctx.Done():
|
||||
break
|
||||
}
|
||||
|
||||
44
public_resolvers/base_resolver.go
Normal file
44
public_resolvers/base_resolver.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Doer interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type baseResolver struct {
|
||||
client Doer
|
||||
url string
|
||||
}
|
||||
|
||||
func (i baseResolver) ResolvePublicIp(ctx context.Context) (net.IP, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, i.url, 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 func() {
|
||||
_ = 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
|
||||
}
|
||||
97
public_resolvers/base_resolver_test.go
Normal file
97
public_resolvers/base_resolver_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// RoundTripFunc .
|
||||
type RoundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
// RoundTrip .
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
|
||||
func NewTestClient(fn RoundTripFunc) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: RoundTripFunc(fn),
|
||||
}
|
||||
}
|
||||
|
||||
func Test_baseResolver_ResolvePublicIp(t *testing.T) {
|
||||
|
||||
testUrl := "http://my-test-url.url"
|
||||
testIp := `192.168.0.100`
|
||||
|
||||
client := NewTestClient(func(req *http.Request) *http.Response {
|
||||
|
||||
if req.URL.String() != testUrl {
|
||||
return &http.Response{
|
||||
StatusCode: 500,
|
||||
// Send response to be tested
|
||||
Body: io.NopCloser(bytes.NewBufferString(`invalid url`)),
|
||||
// Must be set to non-nil value or it panics
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
// Send response to be tested
|
||||
Body: io.NopCloser(bytes.NewBufferString(testIp)),
|
||||
// Must be set to non-nil value or it panics
|
||||
Header: make(http.Header),
|
||||
}
|
||||
})
|
||||
|
||||
type fields struct {
|
||||
client Doer
|
||||
url string
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want net.IP
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "check parse ip4",
|
||||
fields: fields{
|
||||
client: client,
|
||||
url: testUrl,
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
},
|
||||
want: net.ParseIP(testIp),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
i := baseResolver{
|
||||
client: tt.fields.client,
|
||||
url: tt.fields.url,
|
||||
}
|
||||
got, err := i.ResolvePublicIp(tt.args.ctx)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ResolvePublicIp() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ResolvePublicIp() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,17 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
IfConfigMeTag = "ifconfig.me"
|
||||
)
|
||||
|
||||
type Doer interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
var (
|
||||
ifConfigMeUrl = "https://ifconfig.me"
|
||||
)
|
||||
|
||||
type IfConfigMe struct {
|
||||
client Doer
|
||||
baseResolver
|
||||
}
|
||||
|
||||
func NewDefaultIfConfigMe() *IfConfigMe {
|
||||
@@ -31,32 +20,11 @@ func NewDefaultIfConfigMe() *IfConfigMe {
|
||||
})
|
||||
}
|
||||
|
||||
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 func() {
|
||||
_ = 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
|
||||
func NewIfConfigMe(client Doer) *IfConfigMe {
|
||||
return &IfConfigMe{
|
||||
baseResolver: baseResolver{
|
||||
client: client,
|
||||
url: ifConfigMeUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
30
public_resolvers/v4identme.go
Normal file
30
public_resolvers/v4identme.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
V4IdentMeTag = "v4.ident.me"
|
||||
v4IdentMeUrl = "https://v4.ident.me/"
|
||||
)
|
||||
|
||||
type V4IdentMe struct {
|
||||
baseResolver
|
||||
}
|
||||
|
||||
func NewV4IdentMeDefault() *V4IdentMe {
|
||||
return NewV4IdentMe(&http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
func NewV4IdentMe(client Doer) *V4IdentMe {
|
||||
return &V4IdentMe{
|
||||
baseResolver: baseResolver{
|
||||
client: client,
|
||||
url: v4IdentMeUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user