package main import ( "encoding/base64" "encoding/json" "errors" "fmt" "io" "net" "net/http" "os" "runtime" "strconv" "strings" "time" "github.com/c-robinson/iplib" "github.com/miekg/dns" "github.com/urfave/cli/v2" "golang.org/x/net/idna" ) // The basic structure of a DNS request type request struct { server string // The server to make the DNS request from request uint16 // The type of request name string // The domain name to make a DNS request for } func main() { // Custom version string cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("%s version %s, built with %s\n", c.App.Name, c.App.Version, runtime.Version()) } cli.VersionFlag = &cli.BoolFlag{ Name: "v", Usage: "show version", } cli.HelpFlag = &cli.BoolFlag{ Name: "h", Usage: "show this help", } // Hack to get rid of the annoying default on the CLI oldFlagStringer := cli.FlagStringer cli.FlagStringer = func(f cli.Flag) string { return strings.TrimSuffix(oldFlagStringer(f), " (default: false)") } cli.AppHelpTemplate = `{{.Name}} - {{.Usage}} Usage: {{.HelpName}} name [@server] [type] can be a name or an IP address defaults to A arguments can be in any order {{if .VisibleFlags}} Options: {{range .VisibleFlags}}{{.}} {{end}}{{end}}` app := &cli.App{ Name: "awl", Usage: "drill, writ small", Version: "v0.2.0", Flags: []cli.Flag{ &cli.IntFlag{ Name: "port", Aliases: []string{"p"}, Usage: "port to make DNS query", DefaultText: "53 over plain TCP/UDP, 853 over TLS or QUIC, and 443 over HTTPS", }, &cli.BoolFlag{ Name: "4", Usage: "force IPv4", }, &cli.BoolFlag{ Name: "6", Usage: "force IPv6", }, &cli.BoolFlag{ Name: "dnssec", Aliases: []string{"D"}, Usage: "enable DNSSEC", }, &cli.BoolFlag{ Name: "json", Aliases: []string{"j"}, Usage: "return the result(s) as JSON", }, &cli.BoolFlag{ Name: "short", Aliases: []string{"s"}, Usage: "print just the results, equivalent to dig +short", }, &cli.BoolFlag{ Name: "tcp", Aliases: []string{"t"}, Usage: "use TCP (default: use UDP)", }, &cli.BoolFlag{ Name: "tls", Aliases: []string{"T"}, Usage: "use DNS-over-TLS", }, &cli.BoolFlag{ Name: "https", Aliases: []string{"H"}, Usage: "use DNS-over-HTTPS (NOT FULLY COMPLETE)", }, &cli.BoolFlag{ Name: "quic", Aliases: []string{"Q"}, Usage: "use DNS-over-QUIC (NOT YET IMPLEMENTED)", }, &cli.BoolFlag{ Name: "no-truncate", Usage: "Ignore truncation if a UDP request truncates (default: retry with TCP)", }, &cli.BoolFlag{ Name: "reverse", Aliases: []string{"x"}, Usage: "do a reverse lookup", }, }, Action: func(c *cli.Context) error { var err error req := parseArgs(c.Args().Slice()) // Set DNS-over-TLS, if enabled port := c.Int("port") // If port is not set, set it if port == 0 { if c.Bool("tls") || c.Bool("quic") { port = 853 } else { port = 53 } } if !c.Bool("https") { req.server = net.JoinHostPort(req.server, strconv.Itoa(port)) } else { req.server = "https://" + req.server } // Process the IP/Phone number so a PTR/NAPTR can be done if c.Bool("reverse") { if dns.TypeToString[req.request] == "A" { req.request = dns.StringToType["PTR"] } req.name, err = reverseDNS(req.name, dns.TypeToString[req.request]) if err != nil { return err } } // if the domain is not canonical, make it canonical if !strings.HasSuffix(req.name, ".") { req.name = fmt.Sprintf("%s.", req.name) } msg := new(dns.Msg) msg.SetQuestion(req.name, req.request) // Set DNSSEC if requested if c.Bool("dnssec") { msg.SetEdns0(1232, true) } var ( in *dns.Msg rtt time.Duration ) // Make the DNS request if c.Bool("https") { in, err = resolveHTTPS(msg, req.server) } else if c.Bool("quic") { return fmt.Errorf("quic: not yet implemented") } else { d := new(dns.Client) // Set TCP/UDP, depending on flags if c.Bool("tcp") { d.Net = "tcp" if c.Bool("4") { d.Net = "tcp4" } if c.Bool("6") { d.Net = "tcp6" } } else { d.Net = "udp" if c.Bool("4") { d.Net = "udp4" } if c.Bool("6") { d.Net = "udp6" } } // This is apparently all it takes to enable DoT // TODO: Is it really? if c.Bool("tls") { d.Net = "tcp-tls" } in, rtt, err = d.Exchange(msg, req.server) // If UDP truncates, use TCP instead if !c.Bool("no-truncate") { if in.MsgHdr.Truncated { fmt.Printf(";; Truncated, retrying with TCP\n\n") d.Net = "tcp" if c.Bool("4") { d.Net = "tcp4" } if c.Bool("6") { d.Net = "tcp6" } in, rtt, err = d.Exchange(msg, req.server) } } } if err != nil { return err } if c.Bool("json") { json, _ := json.Marshal(in) fmt.Println(string(json)) } else { if !c.Bool("short") { // Print everything fmt.Println(in) fmt.Println(";; Query time:", rtt) fmt.Println(";; SERVER:", req.server) fmt.Println(";; WHEN:", time.Now().Format(time.RFC1123)) fmt.Println(";; MSG SIZE rcvd:", in.Len()) } else { // Print just the responses, nothing else for _, res := range in.Answer { temp := strings.Split(res.String(), "\t") fmt.Println(temp[len(temp)-1]) } } } return nil }, } err := app.Run(os.Args) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func parseArgs(args []string) request { var ( server string req uint16 name string ) for _, arg := range args { // If it starts with @, it's a DNS server if strings.HasPrefix(arg, "@") { server = strings.Split(arg, "@")[1] continue } // If there's a dot, it's a name if strings.Contains(arg, ".") { name, _ = idna.ToUnicode(arg) continue } // If it's a request, it's a request (duh) if r, ok := dns.StringToType[strings.ToUpper(arg)]; ok { req = r continue } //else, assume it's a name name, _ = idna.ToUnicode(arg) } // If nothing was set, set a default if name == "" { name = "." if req == 0 { req = dns.StringToType["NS"] } } else { if req == 0 { req = dns.StringToType["A"] } } if server == "" { resolv, err := dns.ClientConfigFromFile("/etc/resolv.conf") if err != nil { // Query Google by default, needed for Windows since the DNS library doesn't support Windows // TODO: Actually find where windows stuffs its dns resolvers server = "8.8.4.4" } else { server = resolv.Servers[0] } } return request{server: server, request: req, name: name} } func reverseDNS(dom string, q string) (string, error) { if q == "PTR" { if strings.Contains(dom, ".") { // It's an IPv4 address ip := net.ParseIP(dom) if ip != nil { return iplib.IP4ToARPA(ip), nil } else { return "", errors.New("error: Could not parse IPv4 address") } } else if strings.Contains(dom, ":") { // It's an IPv6 address ip := net.ParseIP(dom) if ip != nil { return iplib.IP6ToARPA(ip), nil } else { return "", errors.New("error: Could not parse IPv6 address") } } } return "", errors.New("error: -x flag given but no IP found") } func resolveHTTPS(msg *dns.Msg, server string) (*dns.Msg, error) { httpR := &http.Client{} buf, err := msg.Pack() if err != nil { return nil, err } query := server + "?dns=" + base64.RawURLEncoding.EncodeToString(buf) req, err := http.NewRequest("GET", query, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/dns-message") res, err := httpR.Do(req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("bad HTTP Request: %d", res.StatusCode) } fullRes, err := io.ReadAll(res.Body) if err != nil { return nil, err } response := dns.Msg{} err = response.Unpack(fullRes) if err != nil { return nil, err } return &response, nil }