Merge pull request #6 from mkelcik/new_ident_me_resolver

Add v4.ident.me resolver
This commit is contained in:
mkelcik
2023-05-01 10:04:04 +02:00
committed by GitHub
7 changed files with 188 additions and 45 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) - `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. 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 ### Building from source
@@ -43,6 +43,7 @@ version: "3"
services: services:
cf-dns-updater: cf-dns-updater:
image: mkelcik/cloudflare-ddns-update:latest image: mkelcik/cloudflare-ddns-update:latest
restart: unless-stopped
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

View File

@@ -2,6 +2,7 @@ version: "3"
services: services:
cf-dns-updater: cf-dns-updater:
image: mkelcik/cloudflare-ddns-update:latest image: mkelcik/cloudflare-ddns-update:latest
restart: unless-stopped
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

10
main.go
View File

@@ -17,13 +17,15 @@ type PublicIpResolver interface {
ResolvePublicIp(ctx context.Context) (net.IP, error) ResolvePublicIp(ctx context.Context) (net.IP, error)
} }
func getResolver(resolverName string) PublicIpResolver { 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.V4IdentMeTag:
return public_resolvers.NewV4IdentMeDefault(), public_resolvers.V4IdentMeTag
case public_resolvers.IfConfigMeTag: case public_resolvers.IfConfigMeTag:
fallthrough fallthrough
default: default:
return public_resolvers.NewDefaultIfConfigMe() return public_resolvers.NewDefaultIfConfigMe(), public_resolvers.IfConfigMeTag
} }
} }
@@ -48,14 +50,14 @@ func main() {
} }
// public ip resolver // public ip resolver
publicIpResolver := getResolver(config.PublicIpResolverTag) publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag)
checkFunc := func() { checkFunc := func() {
currentPublicIP, err := publicIpResolver.ResolvePublicIp(ctx) currentPublicIP, err := publicIpResolver.ResolvePublicIp(ctx)
if err != nil { if err != nil {
log.Fatal(err) 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)) dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID))
if err != nil { if err != nil {

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 package public_resolvers
import ( import (
"context"
"fmt"
"io"
"net"
"net/http" "net/http"
"time" "time"
) )
const ( const (
IfConfigMeTag = "ifconfig.me" IfConfigMeTag = "ifconfig.me"
)
type Doer interface {
Do(*http.Request) (*http.Response, error)
}
var (
ifConfigMeUrl = "https://ifconfig.me" ifConfigMeUrl = "https://ifconfig.me"
) )
type IfConfigMe struct { type IfConfigMe struct {
client Doer baseResolver
} }
func NewDefaultIfConfigMe() *IfConfigMe { func NewDefaultIfConfigMe() *IfConfigMe {
@@ -31,32 +20,11 @@ func NewDefaultIfConfigMe() *IfConfigMe {
}) })
} }
func NewIfConfigMe(c Doer) *IfConfigMe { func NewIfConfigMe(client Doer) *IfConfigMe {
return &IfConfigMe{client: c} return &IfConfigMe{
baseResolver: baseResolver{
client: client,
url: ifConfigMeUrl,
},
} }
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
} }

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