4 Commits

Author SHA1 Message Date
mkelcik
796e7b53fa Merge pull request #6 from mkelcik/new_ident_me_resolver
Add v4.ident.me resolver
2023-05-01 10:04:04 +02:00
mkelcik
8412e68929 Add v4.ident.me resolver 2023-05-01 10:01:43 +02:00
mkelcik
262fbff97c Merge pull request #5 from mkelcik/minor-refactor
Minor check refactor
2023-04-30 08:47:36 +02:00
mkelcik
0d244649d2 minor refactor 2023-04-30 08:46:25 +02:00
7 changed files with 232 additions and 82 deletions

View File

@@ -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

View File

@@ -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

37
main.go
View File

@@ -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,20 +49,15 @@ func main() {
log.Fatal(err)
}
log.Println("waiting for update 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)
// 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`", currentPublicIP)
log.Printf("Current public ip `%s` (%s)", currentPublicIP, resolverTag)
dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID))
if err != nil {
@@ -91,7 +88,19 @@ func main() {
}
}
}
}()
}
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 ...")
checkFunc()
case <-ctx.Done():
break
}

View 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
}

View 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)
}
})
}
}

View File

@@ -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,
},
}
}

View 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,
},
}
}