├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── go.mod
├── go.sum
├── justfile
└── main.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | bin/
21 |
22 | # Go workspace file
23 | go.work
24 |
25 | extip
26 |
27 | .DS_Store
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## [v0.2.0] - 2023-03-03
3 | ### Features
4 | - now has the option to use a custom [extip server](https://github.com/hrbrmstr/extip-svr)
5 |
6 |
7 |
8 | ## [v0.1.0] - 2023-02-28
9 | ### Features
10 | - initial release
11 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 boB Rudis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # extip - retrieve your external IP address via DNS
2 |
3 | Small Golang package/cli that uses special DNS resolvers to return your external IP address.
4 |
5 | By default it uses Google, OpenDNS, and Akamai. If there is a conflict between the resolver answers a message will be delivered on stderr with the conflicting values.
6 |
7 | Alternatively, you can specify an [extip server](https://github.com/hrbrmstr/extip-svr) to use. See below for how to do that.
8 |
9 | ## References
10 |
11 | - [Bizarre and Unusual Uses of DNS](https://fosdem.org/2023/schedule/event/dns_bizarre_and_unusual_uses_of_dns/)
12 | - [Akamai blog](https://www.akamai.com/blog/developers/introducing-new-whoami-tool-dns-resolver-information)
13 |
14 | ## Build
15 |
16 | ```
17 | just build # requires https://github.com/casey/just
18 | ```
19 |
20 | ## Install
21 |
22 | ```
23 | go install -ldflags "-s -w" github.com/hrbrmstr/extip@latest
24 | ```
25 |
26 | ## Options
27 |
28 | ```
29 | Lookup external IP address via DNS.
30 |
31 | Defaults to using Akamai, OpenDNS, and Google services.
32 | You can specify an extip server via the following command line options.
33 | NOTE: Both server and domain should be specified to override default behavior.
34 | More info about running an extip server can be found at .
35 |
36 | extip 0.2.0
37 | Usage: extip [--server EXTIP_SERVER] [--domain DOMAIN] [--record-type RECORD_TYPE] [--port PORT]
38 |
39 | Options:
40 | --server EXTIP_SERVER, -s EXTIP_SERVER
41 | extip-svr IP/FQDN. e.g., ip.rudis.net [env: EXTIP_SERVER]
42 | --domain DOMAIN, -d DOMAIN
43 | Domain to use for IP Lookup. e.g., myip.is [env: EXTIP_DOMAIN]
44 | --record-type RECORD_TYPE, -r RECORD_TYPE
45 | DNS record type to lookup. One of TXT or A. [default: TXT, env: EXTIP_RECORD_TYPE]
46 | --port PORT, -p PORT Port extip resolver is listening on. [default: 53, env: EXTIP_PORT]
47 | --help, -h display this help and exit
48 | ```
49 |
50 | ## Usage
51 |
52 | Default:
53 |
54 | ```
55 | extip
56 | ```
57 |
58 | Use an extip server:
59 |
60 | ```
61 | extip -s ip.rudis.net -d ip.is
62 | ```
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hrbrmstr/extip
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/alexflint/go-arg v1.4.3 // indirect
7 | github.com/alexflint/go-scalar v1.1.0 // indirect
8 | )
9 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo=
2 | github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA=
3 | github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
4 | github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
9 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
10 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
13 |
--------------------------------------------------------------------------------
/justfile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env just --justfile
2 |
3 | set shell := ["zsh", "-cu"]
4 |
5 | # Lists the justfile commands
6 | @default:
7 | @just --list
8 |
9 | # Build the package
10 | @build:
11 | go build -ldflags "-s -w" -o bin/extip
12 |
13 | @run: build
14 | ./bin/extip
15 |
16 | # Be a good citizen
17 | @fmt:
18 | go fmt
19 |
20 | # Build for other platforms
21 | @release-build:
22 | GOOS=windows GOARCH=386 go build -ldflags "-s -w" -o bin/extip.exe main.go && cd bin && zip extip.zip extip.exe && rm extip.exe
23 | GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/extip-linux-amd64 main.go && cd bin && gzip extip-linux-amd64
24 | GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o bin/extip-linux-arm64 main.go && cd bin && gzip extip-linux-arm64
25 | GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o bin/extip-darwin-amd64 main.go && cd bin && gzip extip-darwin-amd64
26 | GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o bin/extip-darwin-arm64 main.go && cd bin && gzip extip-darwin-arm64
27 |
28 | # Check results against dig. Requires dig.
29 | @test: build
30 | [ "$(dig myip.opendns.com @resolver1.opendns.com +short)" = "$(./bin/extip)" ] && echo "Passed OpenDNS test"
31 | [ "$(dig o-o.myaddr.1.google.com @ns1.google.com TXT +short | tr -d '"')" = "$(./bin/extip)" ] && echo "Passed Google test"
32 | [ "$(dig +short TXT whoami.ds.akahelp.net @$(dig +short +answer NS akamai.com | head -1) | grep ns | sed -e 's/[^0-9\.\:]//g')" = "$(./bin/extip)" ] && echo "Passed Akamai test"
33 | [ "$(dig +short TXT myip.is @ip.rudis.net | tr -d '"')" = "$(./bin/extip)" ] && echo "Passed custom extip server test"
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "net"
8 | "os"
9 | "regexp"
10 | "strconv"
11 | "strings"
12 | "time"
13 |
14 | "github.com/alexflint/go-arg"
15 | )
16 |
17 | const (
18 | version = "0.2.0"
19 | resolverTimeout = 10000
20 | defaultPort = 53
21 |
22 | googleResolver = "ns1.google.com"
23 | openDNSResolver = "resolver1.opendns.com"
24 |
25 | googleHost = "o-o.myaddr.1.google.com"
26 | openDNSHost = "myip.opendns.com"
27 | akamaiHost = "whoami.ds.akahelp.net"
28 | akamaiDomain = "akamai.com"
29 | )
30 |
31 | // Test if all strings in a list are equal
32 | func AllEqual(a []string) bool {
33 | for i := 1; i < len(a); i++ {
34 | if a[i] != a[0] {
35 | return false
36 | }
37 | }
38 | return true
39 | }
40 |
41 | // Remove last char in a string if it is `suffix`
42 | func TrimSuffix(s, suffix string) string {
43 | hasSuf := strings.HasSuffix(s, suffix)
44 | if hasSuf {
45 | s = s[:len(s)-len(suffix)]
46 | }
47 | return s
48 | }
49 |
50 | // Setup a specific resovler to use for DNS lookups
51 | func UseResolver(resolver string, port int) *net.Resolver {
52 |
53 | return &net.Resolver{
54 | PreferGo: true,
55 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
56 | d := net.Dialer{Timeout: time.Millisecond * time.Duration(resolverTimeout)}
57 | return d.DialContext(ctx, network, resolver+":"+strconv.Itoa(port))
58 | },
59 | }
60 |
61 | }
62 |
63 | // Get one of Akamai's authoritative nameservers
64 | func AkamaiResolver() (string, error) {
65 |
66 | ns, err := net.LookupNS(akamaiDomain)
67 |
68 | nsHost := ""
69 | if err == nil {
70 | nsHost = TrimSuffix((*ns[0]).Host, ".")
71 | }
72 |
73 | return nsHost, err
74 |
75 | }
76 |
77 | // Get our local, external IP via Akamai's hack
78 | //
79 | // Akamai returns "ns" "ip.ad.dr.ess" so we have to get rid of cruft
80 | func AkamaiExtIP() ([]string, error) {
81 |
82 | regEx := regexp.MustCompile(`[^0-9\.\:]`)
83 | akamaiResolver, err := AkamaiResolver()
84 |
85 | if err != nil {
86 | return []string{""}, err
87 | }
88 |
89 | r := UseResolver(akamaiResolver, defaultPort)
90 | txts, err := r.LookupTXT(context.Background(), akamaiHost)
91 |
92 | if err != nil {
93 | return []string{""}, err
94 | }
95 |
96 | txts[0] = regEx.ReplaceAllString(txts[0], "")
97 |
98 | return txts, err
99 |
100 | }
101 |
102 | // Get our local, external IP via Google's hack
103 | func GoogleExtIP() ([]string, error) {
104 |
105 | r := UseResolver(googleResolver, defaultPort)
106 | txts, err := r.LookupTXT(context.Background(), googleHost)
107 |
108 | if err != nil {
109 | txts = []string{""}
110 | }
111 |
112 | return txts, err
113 |
114 | }
115 |
116 | // Get our local, external IP via OpenDNS's hack
117 | func OpenDNSExtIP() ([]string, error) {
118 |
119 | r := UseResolver(openDNSResolver, defaultPort)
120 | ips, err := r.LookupHost(context.Background(), openDNSHost)
121 |
122 | if err != nil {
123 | ips = []string{""}
124 | }
125 |
126 | return ips, err
127 |
128 | }
129 |
130 | // Get our local, external IP via custom extip resolver
131 | func CustomExtIP(resolver string, host string, rectype string, port int) ([]string, error) {
132 |
133 | r := UseResolver(resolver, port)
134 |
135 | var res [](string)
136 | var err error
137 |
138 | if rectype == "A" {
139 | res, err = r.LookupHost(context.Background(), host)
140 | } else {
141 | res, err = r.LookupTXT(context.Background(), host)
142 | }
143 |
144 | if err != nil {
145 | res = []string{""}
146 | }
147 |
148 | return res, err
149 |
150 | }
151 |
152 | type args struct {
153 | Server string `arg:"-s,--server,env:EXTIP_SERVER" help:"extip-svr IP/FQDN. e.g., ip.rudis.net" placeholder:"EXTIP_SERVER"`
154 | Domain string `arg:"-d,--domain,env:EXTIP_DOMAIN" help:"Domain to use for IP Lookup. e.g., myip.is" placeholder:"DOMAIN"`
155 | RecordType string `arg:"-r,--record-type,env:EXTIP_RECORD_TYPE" help:"DNS record type to lookup. One of TXT or A." placeholder:"RECORD_TYPE" default:"TXT"`
156 | Port int `arg:"-p,--port,env:EXTIP_PORT" help:"Port extip resolver is listening on." placeholder:"PORT" default:"53"`
157 | }
158 |
159 | func (args) Description() string {
160 | return "Lookup external IP address via DNS.\n\nDefaults to using Akamai, OpenDNS, and Google services.\nYou can specify an extip server via the following command line options.\nNOTE: Both server and domain should be specified to override default behavior.\nMore info about running an extip server can be found at .\n"
161 | }
162 |
163 | func (args) Version() string {
164 | return "extip " + version
165 | }
166 |
167 | // TODO: Make this a proper CLI with cmdline options since we have 3 services
168 | func main() {
169 |
170 | l := log.New(os.Stderr, "", 1)
171 |
172 | var args args
173 |
174 | arg.MustParse(&args)
175 |
176 | if args.Server != "" && args.Domain != "" {
177 | custom, cerr := CustomExtIP(args.Server, args.Domain, args.RecordType, args.Port)
178 | if cerr != nil {
179 | l.Println("No DNS resolutions worked.")
180 | os.Exit(3)
181 | } else {
182 | fmt.Println(custom[0])
183 | os.Exit(0)
184 | }
185 |
186 | }
187 |
188 | opendns, oerr := OpenDNSExtIP()
189 | google, gerr := GoogleExtIP()
190 | akamai, aerr := AkamaiExtIP()
191 |
192 | if (oerr != nil) && (gerr != nil) && (aerr != nil) {
193 | l.Println("No DNS resolutions worked.")
194 | os.Exit(2)
195 | }
196 |
197 | if oerr != nil {
198 | l.Println("OpenDNS resolver query failed.")
199 | opendns[0] = "FAILED"
200 | }
201 |
202 | if gerr != nil {
203 | l.Println("Google resolver query failed")
204 | google[0] = "FAILED"
205 | }
206 |
207 | if aerr != nil {
208 | l.Println("Akamai resovler query failed")
209 | akamai[0] = "FAILED"
210 | }
211 |
212 | // If at least one worked, compare the three; if not all equal then error out
213 | // otherwise return one of them.
214 |
215 | if AllEqual([]string{akamai[0], google[0], opendns[0]}) {
216 |
217 | fmt.Println(opendns[0])
218 |
219 | } else {
220 |
221 | l.Println("Resolvers have different ideas regarding your external IP address.")
222 | l.Printf("Akamai thinks it is: %s\n", akamai[0])
223 | l.Printf("Google thinks it is: %s\n", google[0])
224 | l.Printf("OpenDNS thinks it is: %s\n", opendns[0])
225 |
226 | os.Exit(1)
227 |
228 | }
229 |
230 | }
231 |
--------------------------------------------------------------------------------