From bcc463160e7a139bb99f92565945e217024f5990 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Fri, 13 Oct 2023 19:26:38 -0400 Subject: [PATCH] Create DNS resolve tool --- x/examples/resolve/README.md | 90 +++++++++++++++++++++ x/examples/resolve/main.go | 150 +++++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 x/examples/resolve/README.md create mode 100644 x/examples/resolve/main.go diff --git a/x/examples/resolve/README.md b/x/examples/resolve/README.md new file mode 100644 index 00000000..39577f40 --- /dev/null +++ b/x/examples/resolve/README.md @@ -0,0 +1,90 @@ +# Domain Resolution Tool + +The `resolve` tool lets you resolve domain names with custom DNS resolvers and using configurable transports. + +Usage: + +```txt +Usage: resolve [flags...] + -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) diff --git a/x/examples/resolve/main.go b/x/examples/resolve/main.go new file mode 100644 index 00000000..000fefc0 --- /dev/null +++ b/x/examples/resolve/main.go @@ -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...] \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 NS: %v", err) + } + for _, line := range lines { + fmt.Println(line) + } + default: + log.Fatalf("Invalid query type %v", *typeFlag) + } +}