You've already forked ddns-updater
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d52cbe920 | ||
|
|
6f1b45cf8a | ||
|
|
f859e86a08 | ||
|
|
bbcc6eaa44 | ||
|
|
3222a6c54c | ||
|
|
796e7b53fa | ||
|
|
8412e68929 |
@@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
## What is Cloudflare Dynamic DNS?
|
||||
DNS records are static, and it does not play well with dynamic IP addresses. Now, to solve that problem, you’ll need to set up dynamic DNS. Cloudflare provides an API that allows you to manage DNS records programmatically.
|
||||
|
||||
@@ -15,7 +17,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`, `1.1.1.1`
|
||||
|
||||
### Building from source
|
||||
|
||||
@@ -43,6 +45,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
|
||||
|
||||
12
main.go
12
main.go
@@ -17,13 +17,17 @@ 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.CloudflareTraceTag:
|
||||
return public_resolvers.NewDefaultCloudflareTrace(), public_resolvers.CloudflareTraceTag
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +52,14 @@ func main() {
|
||||
}
|
||||
|
||||
// public ip resolver
|
||||
publicIpResolver := getResolver(config.PublicIpResolverTag)
|
||||
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` (resolver: %s)", currentPublicIP, resolverTag)
|
||||
|
||||
dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID))
|
||||
if err != nil {
|
||||
|
||||
55
public_resolvers/base_resolver.go
Normal file
55
public_resolvers/base_resolver.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var NoIPInResponseError = errors.New("no ip found in response")
|
||||
|
||||
type Doer interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type ipParserFunc func(reader io.Reader) (string, error)
|
||||
|
||||
func defaultIpParser(reader io.Reader) (string, error) {
|
||||
out, err := io.ReadAll(reader)
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
type baseResolver struct {
|
||||
client Doer
|
||||
url string
|
||||
ipParser ipParserFunc
|
||||
}
|
||||
|
||||
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 := i.ipParser(resp.Body)
|
||||
if err != nil {
|
||||
return net.IP{}, fmt.Errorf("error reading body: %w", err)
|
||||
}
|
||||
|
||||
return net.ParseIP(ipText), nil
|
||||
}
|
||||
100
public_resolvers/base_resolver_test.go
Normal file
100
public_resolvers/base_resolver_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
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: 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
|
||||
fn ipParserFunc
|
||||
}
|
||||
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,
|
||||
fn: defaultIpParser,
|
||||
},
|
||||
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,
|
||||
ipParser: tt.fields.fn,
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
49
public_resolvers/cloudflare_trace.go
Normal file
49
public_resolvers/cloudflare_trace.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CloudflareTraceTag = "1.1.1.1"
|
||||
CloudflareTraceUrl = "https://1.1.1.1/cdn-cgi/trace"
|
||||
|
||||
ipPrefix = "ip="
|
||||
)
|
||||
|
||||
type CloudflareTrace struct {
|
||||
baseResolver
|
||||
}
|
||||
|
||||
func NewDefaultCloudflareTrace() *CloudflareTrace {
|
||||
return NewCloudflareTrace(&http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
func cloudflareTraceResponseParser(reader io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, row := range strings.Split(string(data), "\n") {
|
||||
if strings.Index(row, ipPrefix) == 0 {
|
||||
return strings.TrimSpace(strings.ReplaceAll(row, ipPrefix, "")), nil
|
||||
}
|
||||
}
|
||||
return "", NoIPInResponseError
|
||||
}
|
||||
|
||||
func NewCloudflareTrace(client Doer) *CloudflareTrace {
|
||||
return &CloudflareTrace{
|
||||
baseResolver: baseResolver{
|
||||
client: client,
|
||||
url: CloudflareTraceUrl,
|
||||
ipParser: cloudflareTraceResponseParser,
|
||||
},
|
||||
}
|
||||
}
|
||||
78
public_resolvers/cloudflare_trace_test.go
Normal file
78
public_resolvers/cloudflare_trace_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_cloudflareTraceResponseParser(t *testing.T) {
|
||||
|
||||
type args struct {
|
||||
reader io.Reader
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
reader: bytes.NewBuffer([]byte(`fl=31f118
|
||||
h=1.1.1.1
|
||||
ip=94.113.142.206
|
||||
ts=1683145336.383
|
||||
visit_scheme=https
|
||||
uag=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
|
||||
colo=PRG
|
||||
sliver=none
|
||||
http=http/2
|
||||
loc=CZ
|
||||
tls=TLSv1.3
|
||||
sni=off
|
||||
warp=off
|
||||
gateway=off
|
||||
rbi=off
|
||||
kex=X25519`)),
|
||||
},
|
||||
want: "94.113.142.206",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no ip in response",
|
||||
args: args{
|
||||
reader: bytes.NewBuffer([]byte(`fl=31f118
|
||||
h=1.1.1.1
|
||||
ts=1683145336.383
|
||||
visit_scheme=https
|
||||
uag=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
|
||||
colo=PRG
|
||||
sliver=none
|
||||
http=http/2
|
||||
loc=CZ
|
||||
tls=TLSv1.3
|
||||
sni=off
|
||||
warp=off
|
||||
gateway=off
|
||||
rbi=off
|
||||
kex=X25519`)),
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := cloudflareTraceResponseParser(tt.args.reader)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("cloudflareTraceResponseParser() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("cloudflareTraceResponseParser() 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,12 @@ 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,
|
||||
ipParser: defaultIpParser,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
31
public_resolvers/v4identme.go
Normal file
31
public_resolvers/v4identme.go
Normal file
@@ -0,0 +1,31 @@
|
||||
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,
|
||||
ipParser: defaultIpParser,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user