From 6f1b45cf8a31f99dac43c1950ab239a42ffc0095 Mon Sep 17 00:00:00 2001 From: mkelcik Date: Wed, 3 May 2023 23:06:57 +0200 Subject: [PATCH] Add 1.1.1.1 resolver --- Makefile | 2 + README.md | 2 +- main.go | 4 +- public_resolvers/base_resolver.go | 19 ++++-- public_resolvers/base_resolver_test.go | 9 ++- public_resolvers/cloudflare_trace.go | 49 ++++++++++++++ public_resolvers/cloudflare_trace_test.go | 78 +++++++++++++++++++++++ public_resolvers/ifconfigme.go | 5 +- public_resolvers/v4identme.go | 5 +- 9 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 Makefile create mode 100644 public_resolvers/cloudflare_trace.go create mode 100644 public_resolvers/cloudflare_trace_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..afecbce --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +test: + go test --cover -covermode count -v ./... \ No newline at end of file diff --git a/README.md b/README.md index 8b9266d..82faad0 100644 --- a/README.md +++ b/README.md @@ -17,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. (default: `ifconfig.me`) Available: `ifconfig.me`, `v4.ident.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 diff --git a/main.go b/main.go index fc5c241..5a31e61 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,8 @@ type PublicIpResolver interface { 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: @@ -57,7 +59,7 @@ func main() { if err != nil { log.Fatal(err) } - log.Printf("Current public ip `%s` (%s)", currentPublicIP, resolverTag) + log.Printf("Current public ip `%s` (resolver: %s)", currentPublicIP, resolverTag) dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID)) if err != nil { diff --git a/public_resolvers/base_resolver.go b/public_resolvers/base_resolver.go index f286ea1..a6081a4 100644 --- a/public_resolvers/base_resolver.go +++ b/public_resolvers/base_resolver.go @@ -2,19 +2,30 @@ 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 + client Doer + url string + ipParser ipParserFunc } func (i baseResolver) ResolvePublicIp(ctx context.Context) (net.IP, error) { @@ -35,10 +46,10 @@ func (i baseResolver) ResolvePublicIp(ctx context.Context) (net.IP, error) { return net.IP{}, fmt.Errorf("unexpected response code %d", resp.StatusCode) } - ipText, err := io.ReadAll(resp.Body) + ipText, err := i.ipParser(resp.Body) if err != nil { return net.IP{}, fmt.Errorf("error reading body: %w", err) } - return net.ParseIP(string(ipText)), nil + return net.ParseIP(ipText), nil } diff --git a/public_resolvers/base_resolver_test.go b/public_resolvers/base_resolver_test.go index bb13d66..50e28be 100644 --- a/public_resolvers/base_resolver_test.go +++ b/public_resolvers/base_resolver_test.go @@ -21,7 +21,7 @@ func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { // NewTestClient returns *http.Client with Transport replaced to avoid making real calls func NewTestClient(fn RoundTripFunc) *http.Client { return &http.Client{ - Transport: RoundTripFunc(fn), + Transport: fn, } } @@ -54,6 +54,7 @@ func Test_baseResolver_ResolvePublicIp(t *testing.T) { type fields struct { client Doer url string + fn ipParserFunc } type args struct { ctx context.Context @@ -70,6 +71,7 @@ func Test_baseResolver_ResolvePublicIp(t *testing.T) { fields: fields{ client: client, url: testUrl, + fn: defaultIpParser, }, args: args{ ctx: context.Background(), @@ -81,8 +83,9 @@ func Test_baseResolver_ResolvePublicIp(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { i := baseResolver{ - client: tt.fields.client, - url: tt.fields.url, + client: tt.fields.client, + url: tt.fields.url, + ipParser: tt.fields.fn, } got, err := i.ResolvePublicIp(tt.args.ctx) if (err != nil) != tt.wantErr { diff --git a/public_resolvers/cloudflare_trace.go b/public_resolvers/cloudflare_trace.go new file mode 100644 index 0000000..5d5e940 --- /dev/null +++ b/public_resolvers/cloudflare_trace.go @@ -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, + }, + } +} diff --git a/public_resolvers/cloudflare_trace_test.go b/public_resolvers/cloudflare_trace_test.go new file mode 100644 index 0000000..7e94758 --- /dev/null +++ b/public_resolvers/cloudflare_trace_test.go @@ -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) + } + }) + } +} diff --git a/public_resolvers/ifconfigme.go b/public_resolvers/ifconfigme.go index 88c3763..82d4988 100644 --- a/public_resolvers/ifconfigme.go +++ b/public_resolvers/ifconfigme.go @@ -23,8 +23,9 @@ func NewDefaultIfConfigMe() *IfConfigMe { func NewIfConfigMe(client Doer) *IfConfigMe { return &IfConfigMe{ baseResolver: baseResolver{ - client: client, - url: ifConfigMeUrl, + client: client, + url: ifConfigMeUrl, + ipParser: defaultIpParser, }, } } diff --git a/public_resolvers/v4identme.go b/public_resolvers/v4identme.go index 4252f0f..54501ea 100644 --- a/public_resolvers/v4identme.go +++ b/public_resolvers/v4identme.go @@ -23,8 +23,9 @@ func NewV4IdentMeDefault() *V4IdentMe { func NewV4IdentMe(client Doer) *V4IdentMe { return &V4IdentMe{ baseResolver: baseResolver{ - client: client, - url: v4IdentMeUrl, + client: client, + url: v4IdentMeUrl, + ipParser: defaultIpParser, }, } }