awl/awl.go
grumbulon 9b979667d6 Make everything more readable (#2)
1. I made JSON print indented instead of inline
2. I created a util package and moved `ReverseDNS` and `ResolveHTTPS` there
3. I created a new struct called Answers where all variables for the output live now, and adjusted the response struct to have the Answers struct in it. This lets us reuse the struct more instead of having variables floating around, lets us make the response more custom, offers more understandable code reuse, and makes things (I think) more readable.

Aside: you will notice the struct has little json defs. This is because struct values encode as JSON objects and can preserve the output name this way

Most of this just makes expanding on awl easier in the future.

Co-authored-by: grumbulon <grumbulon@dismail.de>
Reviewed-on: sam/awl#2
Co-authored-by: grumbulon <grumbulon@grumbulon.xyz>
Co-committed-by: grumbulon <grumbulon@grumbulon.xyz>
2022-06-22 11:55:38 +00:00

297 lines
6.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net"
"os"
"runtime"
"strconv"
"strings"
"time"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
"github.com/urfave/cli/v2"
"golang.org/x/net/idna"
)
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
var resp util.Response
resp.Answers = 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") {
resp.Answers.Server = net.JoinHostPort(resp.Answers.Server, strconv.Itoa(port))
} else {
resp.Answers.Server = "https://" + resp.Answers.Server
}
// Process the IP/Phone number so a PTR/NAPTR can be done
if c.Bool("reverse") {
if dns.TypeToString[resp.Answers.Request] == "A" {
resp.Answers.Request = dns.StringToType["PTR"]
}
resp.Answers.Name, err = util.ReverseDNS(resp.Answers.Name, dns.TypeToString[resp.Answers.Request])
}
// if the domain is not canonical, make it canonical
if !strings.HasSuffix(resp.Answers.Name, ".") {
resp.Answers.Name = fmt.Sprintf("%s.", resp.Answers.Name)
}
msg := new(dns.Msg)
msg.SetQuestion(resp.Answers.Name, resp.Answers.Request)
// Set DNSSEC if requested
if c.Bool("dnssec") {
msg.SetEdns0(1232, true)
}
var (
in *dns.Msg
)
// Make the DNS request
if c.Bool("https") {
in, err = util.ResolveHTTPS(msg, resp.Answers.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, resp.Answers.RTT, err = d.Exchange(msg, resp.Answers.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, resp.Answers.RTT, err = d.Exchange(msg, resp.Answers.Server)
}
}
}
if err != nil {
return err
}
if c.Bool("json") {
json, _ := json.MarshalIndent(in, "", " ")
fmt.Println(string(json))
} else {
if !c.Bool("short") {
// Print everything
fmt.Println(in)
fmt.Println(";; Query time:", resp.Answers.RTT)
fmt.Println(";; SERVER:", resp.Answers.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) util.Answers {
var (
resp util.Response
)
for _, arg := range args {
// If it starts with @, it's a DNS server
if strings.HasPrefix(arg, "@") {
resp.Answers.Server = strings.Split(arg, "@")[1]
continue
}
// If there's a dot, it's a name
if strings.Contains(arg, ".") {
resp.Answers.Name, _ = idna.ToUnicode(arg)
continue
}
// If it's a request, it's a request (duh)
if r, ok := dns.StringToType[strings.ToUpper(arg)]; ok {
resp.Answers.Request = r
continue
}
//else, assume it's a name
resp.Answers.Name, _ = idna.ToUnicode(arg)
}
// If nothing was set, set a default
if resp.Answers.Name == "" {
resp.Answers.Name = "."
if resp.Answers.Request == 0 {
resp.Answers.Request = dns.StringToType["NS"]
}
} else {
if resp.Answers.Request == 0 {
resp.Answers.Request = dns.StringToType["A"]
}
}
if resp.Answers.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
resp.Answers.Server = "8.8.4.4"
} else {
resp.Answers.Server = resolv.Servers[0]
}
}
return util.Answers{Server: resp.Answers.Server, Request: resp.Answers.Request, Name: resp.Answers.Name}
}