Skip to content

Commit

Permalink
Create DNS resolve tool (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna authored Oct 16, 2023
1 parent 57e7a68 commit 72410f3
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 0 deletions.
91 changes: 91 additions & 0 deletions x/examples/resolve/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Domain Resolution Tool

The `resolve` tool lets you resolve domain names with custom DNS resolvers and using configurable transports.

Usage:

```txt
Usage: resolve [flags...] <domain>
-resolver string
The address of the recursive DNS resolver to use in host:port format. If the port is missing, it's assumed to be 53
-tcp
Force TCP when querying the DNS resolver
-transport string
The transport for the connection to the recursive DNS resolver
-type string
The type of the query (A, AAAA, CNAME, NS or TXT). (default "A")
-v Enable debug output
```

Lookup the IPv4 for `www.rferl.org` using the system resolver:

```console
$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve www.rferl.org
104.102.138.8
```

Use `-type aaaa` to lookup the IPv6:

```console
$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve -type aaaa www.rferl.org
2600:141b:1c00:1098::1317
2600:141b:1c00:10a1::1317
```

Use `-resolver` to specify which resolver to use. In this case we use Google's Public DNS:

```console
$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve -resolver 8.8.8.8 www.rferl.org
104.102.138.83
```

It's possible to specify a proxy to connect to the resolver using the `-transport` flag. This is very helpful for experimentation. In the example below, we resolve via a remote proxy in Russia. When using a remote server, you must also specify the resolver to use. Note in the example output how the domain is blocked with a CNAME to `fz139.ttk.ru`

```console
$ KEY=ss://[REDACTED OUTLINE KEY]
$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve -type CNAME -transport "$KEY" -resolver 8.8.8.8 www.rferl.org
fz139.ttk.ru.
```

Using Quad9 in the Russian server bypasses the blocking:

```console
$ KEY=ss://[REDACTED OUTLINE KEY]
$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve -type CNAME -transport "$KEY" -resolver 9.9.9.9 www.rferl.org
e4887.dscb.akamaiedge.net.
```

It's possible to specify non-standard ports. For example, OpenDNS supports port 443:

```console
$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve -type CNAME -resolver 208.67.222.222:443 www.rferl.org
e4887.dscb.akamaiedge.net.
```

However, it seems UDP on alternate ports is blocked in our remote test proxy:

```console
$ KEY=ss://[REDACTED OUTLINE KEY]
$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve -type CNAME -transport "$KEY" -resolver 208.67.222.222:443 www.rferl.org
2023/10/13 19:04:18 Failed to lookup CNAME: lookup www.rferl.org on 208.67.222.222:443: could not create PacketConn: could not connect to endpoint: dial udp [REDACTED ADDRESS]: i/o timeout
exit status 1
```

By forcing TCP with the `-tcp` flag, you can make it work again:

```console
$ KEY=ss://[REDACTED OUTLINE KEY]
$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve -type CNAME -transport "$KEY" -resolver 208.67.222.222:443 -tcp www.rferl.org
e4887.dscb.akamaiedge.net.
```

Forcing TCP lets you use stream fragmentation. In this example, we split the first 20 bytes:

```console
$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve -type CNAME -tcp -transport "split:20" -resolver 208.67.222.222:443 www.rferl.org
e4887.dscb.akamaiedge.net.
```

You can see that the domain name in the query got split:

![image](https://github.com/Jigsaw-Code/outline-sdk/assets/113565/195bfa95-6d35-40ef-84e0-b1d6e690bb84)
150 changes: 150 additions & 0 deletions x/examples/resolve/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"path"
"strings"
"time"

"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/x/config"
)

var debugLog log.Logger = *log.New(io.Discard, "", 0)

func init() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [flags...] <domain>\n", path.Base(os.Args[0]))
flag.PrintDefaults()
}
}

func cleanDNSError(err error, resolverAddr string) error {
dnsErr := &net.DNSError{}
if resolverAddr != "" && errors.As(err, &dnsErr) {
dnsErr.Server = resolverAddr
return dnsErr
}
return err
}

func main() {
verboseFlag := flag.Bool("v", false, "Enable debug output")
typeFlag := flag.String("type", "A", "The type of the query (A, AAAA, CNAME, NS or TXT).")
resolverFlag := flag.String("resolver", "", "The address of the recursive DNS resolver to use in host:port format. If the port is missing, it's assumed to be 53")
transportFlag := flag.String("transport", "", "The transport for the connection to the recursive DNS resolver")
tcpFlag := flag.Bool("tcp", false, "Force TCP when querying the DNS resolver")

flag.Parse()
if *verboseFlag {
debugLog = *log.New(os.Stderr, "[DEBUG] ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
}

domain := strings.TrimSpace(flag.Arg(0))
if domain == "" {
log.Fatal("Need to pass the domain to resolve in the command-line")
}

resolverAddr := *resolverFlag
if resolverAddr != "" && !strings.Contains(resolverAddr, ":") {
resolverAddr = net.JoinHostPort(resolverAddr, "53")
}

var err error
var packetDialer transport.PacketDialer
if !*tcpFlag {
packetDialer, err = config.NewPacketDialer(*transportFlag)
if err != nil {
log.Fatalf("Could not create packet dialer: %v", err)
}
}
streamDialer, err := config.NewStreamDialer(*transportFlag)
if err != nil {
log.Fatalf("Could not create stream dialer: %v", err)
}

resolver := net.Resolver{PreferGo: true}
resolver.Dial = func(ctx context.Context, network, sysResolverAddr string) (net.Conn, error) {
dialAddr := sysResolverAddr
if resolverAddr != "" {
dialAddr = resolverAddr
}
if strings.HasPrefix(network, "tcp") || *tcpFlag {
debugLog.Printf("Dial TCP: %v", dialAddr)
return streamDialer.Dial(ctx, dialAddr)
}
debugLog.Printf("Dial UDP: %v", dialAddr)
return packetDialer.Dial(ctx, dialAddr)
}

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
switch strings.ToUpper(*typeFlag) {
case "A":
ips, err := resolver.LookupIP(ctx, "ip4", domain)
err = cleanDNSError(err, resolverAddr)
if err != nil {
log.Fatalf("Failed to lookup IPs: %v", err)
}
for _, ip := range ips {
fmt.Println(ip.String())
}
case "AAAA":
ips, err := resolver.LookupIP(ctx, "ip6", domain)
err = cleanDNSError(err, resolverAddr)
if err != nil {
log.Fatalf("Failed to lookup IPs: %v", err)
}
for _, ip := range ips {
fmt.Println(ip.String())
}
case "CNAME":
cname, err := resolver.LookupCNAME(ctx, domain)
err = cleanDNSError(err, resolverAddr)
if err != nil {
log.Fatalf("Failed to lookup CNAME: %v", err)
}
fmt.Println(cname)
case "NS":
nss, err := resolver.LookupNS(ctx, domain)
err = cleanDNSError(err, resolverAddr)
if err != nil {
log.Fatalf("Failed to lookup NS: %v", err)
}
for _, ns := range nss {
fmt.Println(ns.Host)
}
case "TXT":
lines, err := resolver.LookupTXT(ctx, domain)
err = cleanDNSError(err, resolverAddr)
if err != nil {
log.Fatalf("Failed to lookup TXT: %v", err)
}
for _, line := range lines {
fmt.Println(line)
}
default:
log.Fatalf("Invalid query type %v", *typeFlag)
}
}

0 comments on commit 72410f3

Please sign in to comment.