374 lines
8.1 KiB
Go
374 lines
8.1 KiB
Go
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]
|
|
<name> can be a name or an IP address
|
|
<type> 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
|
|
|
|
}
|