From 5533ade854034c632aca65c99bb4a4abf226fa27 Mon Sep 17 00:00:00 2001 From: dwrz Date: Sat, 24 Oct 2026 02:43:25 +0000 Subject: [PATCH] Add digip pkg --- pkg/digip/digip.go | 124 ++++++++++++++++++++++++++++++++++++++++ pkg/digip/digip_test.go | 54 +++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 pkg/digip/digip.go create mode 100644 pkg/digip/digip_test.go diff --git a/pkg/digip/digip.go b/pkg/digip/digip.go new file mode 100644 index 0000000..35b4599 --- /dev/null +++ b/pkg/digip/digip.go @@ -0,0 +1,124 @@ +// Package digip runs "dig" to find the public IP address of the local host. +// +// Each function sends a DNS query directly to a third-party name server that +// returns the source IP address of the query. Because the query does not go +// through the local DNS resolver, the answer is the public IP address of the +// host (or its NAT gateway), not the IP address of the local resolver. +// +// The "dig" executable must be in PATH. +package digip + +import ( + "context" + "fmt" + "net" + "os/exec" + "strings" +) + +// Version is a dig flag that selects IPv4 or IPv6. +type Version string + +const ( + // V4 is the dig flag for IPv4. + V4 = "-4" + // V6 is the dig flag for IPv6. + V6 = "-6" +) + +// Record is a DNS record type to ask for. +type Record string + +const ( + // A is the DNS record type for an IPv4 address. + A = "A" + // AAAA is the DNS record type for an IPv6 address. + AAAA = "AAAA" +) + +const ( + // googleDomain is a hostname that resolves to the client's IP address. + // A TXT query for this name returns the IP address as a quoted string. + googleDomain = "o-o.myaddr.l.google.com" + // googleServer is the name server that answers queries for + // googleDomain. + // The "@" prefix tells dig to query this server directly. + googleServer = "@ns1.google.com" + + // opendnsDomain is a hostname that resolves to the client's IP address. + // An A query returns the IPv4 address, and an AAAA query returns the + // IPv6 address. + opendnsDomain = "myip.opendns.com" + // opendnsServer is the name server that answers queries for + // opendnsDomain. + // The "@" prefix tells dig to query this server directly. + opendnsServer = "@resolver1.opendns.com" +) + +// Google queries the Google name server for the host's public IP address. +// Because Google returns the address in a TXT record, v selects IPv4 (V4) or +// IPv6 (V6) transport. +// +// dig TXT +short -4 o-o.myaddr.l.google.com @ns1.google.com +// dig TXT +short -6 o-o.myaddr.l.google.com @ns1.google.com +func Google(ctx context.Context, v Version) (net.IP, error) { + cmd := exec.CommandContext( + ctx, + "dig", + "TXT", + "+short", + string(v), + googleDomain, + googleServer, + ) + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("exec dig: %v", err) + } + if string(output) == "" { + return nil, fmt.Errorf("exec dig: no output") + } + if len(output) < 2 { + return nil, fmt.Errorf( + "exec dig: unexpected output: %q", string(output), + ) + } + + // Remove the quotes from the Google TXT answer. + trimmed := strings.TrimSpace(string(output)) + ip := net.ParseIP(trimmed[1 : len(trimmed)-1]) + + return ip, nil +} + +// OpenDNS queries the OpenDNS name server for the host's public IP address. +// Use r to request the IPv4 address (A) or the IPv6 address (AAAA). +// +// dig +short A myip.opendns.com @resolver1.opendns.com +// dig +short AAAA myip.opendns.com @resolver1.opendns.com +func OpenDNS(ctx context.Context, r Record) (net.IP, error) { + cmd := exec.CommandContext( + ctx, + "dig", + "+short", + string(r), + opendnsDomain, + opendnsServer, + ) + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("exec dig: %v", err) + } + if string(output) == "" { + return nil, fmt.Errorf("exec dig: no output") + } + if len(output) < 2 { + return nil, fmt.Errorf( + "exec dig: unexpected output: %q", string(output), + ) + } + + ip := net.ParseIP(strings.TrimSpace(string(output))) + + return ip, nil +} diff --git a/pkg/digip/digip_test.go b/pkg/digip/digip_test.go new file mode 100644 index 0000000..b335056 --- /dev/null +++ b/pkg/digip/digip_test.go @@ -0,0 +1,54 @@ +package digip + +import ( + "context" + "testing" +) + +func TestGoogleV4(t *testing.T) { + ctx := context.Background() + + ip, err := Google(ctx, V4) + if err != nil { + t.Errorf("google v4 failed: %v", err) + return + } + + t.Logf("got ip: %s", ip) +} + +func TestGoogleV6(t *testing.T) { + ctx := context.Background() + + ip, err := Google(ctx, V6) + if err != nil { + t.Errorf("google v6 failed: %v", err) + return + } + + t.Logf("got ip: %s", ip) +} + +func TestOpenDNSV4(t *testing.T) { + ctx := context.Background() + + ip, err := OpenDNS(ctx, A) + if err != nil { + t.Errorf("opendns A failed: %v", err) + return + } + + t.Logf("got ip: %s", ip) +} + +func TestOpenDNSV6(t *testing.T) { + ctx := context.Background() + + ip, err := OpenDNS(ctx, AAAA) + if err != nil { + t.Errorf("opendns AAAA failed: %v", err) + return + } + + t.Logf("got ip: %s", ip) +}