unfinished mess

This commit is contained in:
Karan Sharma 2022-07-06 21:52:59 +05:30
parent 6c3b17ba0d
commit 2e2e3b1ec8
26 changed files with 364 additions and 171 deletions

166
cmd/doggo-v1/cli.go Normal file
View file

@ -0,0 +1,166 @@
package main
import (
"fmt"
"os"
"time"
"github.com/knadh/koanf"
"github.com/knadh/koanf/providers/posflag"
"github.com/mr-karan/doggo/internal/app"
"github.com/mr-karan/doggo/internal/resolvers"
"github.com/mr-karan/doggo/internal/utils"
"github.com/mr-karan/logf"
flag "github.com/spf13/pflag"
)
var (
// Version of the build. This is injected at build-time.
buildString = "unknown"
logger = utils.InitLogger()
k = koanf.New(".")
)
func main() {
// Initialize app.
app := app.New(logger, buildString)
// Configure Flags.
f := flag.NewFlagSet("config", flag.ContinueOnError)
// Custom Help Text.
f.Usage = renderCustomHelp
// Query Options.
f.StringSliceP("query", "q", []string{}, "Domain name to query")
f.StringSliceP("type", "t", []string{}, "Type of DNS record to be queried (A, AAAA, MX etc)")
f.StringSliceP("class", "c", []string{}, "Network class of the DNS record to be queried (IN, CH, HS etc)")
f.StringSliceP("nameservers", "n", []string{}, "Address of the nameserver to send packets to")
f.BoolP("reverse", "x", false, "Performs a DNS Lookup for an IPv4 or IPv6 address. Sets the query type and class to PTR and IN respectively.")
// Resolver Options
f.Int("timeout", 2, "Sets the timeout for a query to T seconds. The default timeout is 2 seconds.")
f.Int("retry", 3, "Number of times to retry DNS lookup")
f.Bool("search", true, "Use the search list provided in resolv.conf. It sets the `ndots` parameter as well unless overridden by `ndots` flag.")
f.Int("ndots", -1, "Specify the ndots parameter. Default value is taken from resolv.conf and fallbacks to 1 if ndots statement is missing in resolv.conf")
f.BoolP("ipv4", "4", false, "Use IPv4 only")
f.BoolP("ipv6", "6", false, "Use IPv6 only")
f.String("strategy", "all", "Strategy to query nameservers in resolv.conf file (`all`, `random`, `first`)")
f.String("tls-hostname", "", "Provide a hostname for doing verification of the certificate if the provided DoT nameserver is an IP")
f.Bool("skip-hostname-verification", false, "Skip TLS Hostname Verification")
f.StringSliceP("tweaks", "Z", []string{}, "Specify protocol tweaks. Set flags like aa,ad,cd")
// Output Options
f.BoolP("json", "J", false, "Set the output format as JSON")
f.Bool("short", false, "Short output format")
f.Bool("time", false, "Display how long it took for the response to arrive")
f.Bool("color", true, "Show colored output")
f.Bool("debug", false, "Enable debug mode")
f.Bool("version", false, "Show version of doggo")
// Parse and Load Flags.
err := f.Parse(os.Args[1:])
if err != nil {
app.Logger.WithError(err).Fatal("error parsing flags")
}
if err = k.Load(posflag.Provider(f, ".", k), nil); err != nil {
f.Usage()
app.Logger.WithError(err).Fatal("error loading flags")
}
// If version flag is set, output version and quit.
if k.Bool("version") {
fmt.Println(buildString)
os.Exit(0)
}
// Set log level.
if k.Bool("debug") {
app.Logger.SetLevel(logf.DebugLevel)
}
// Unmarshall flags to the app.
err = k.Unmarshal("", &app.QueryFlags)
if err != nil {
app.Logger.WithError(err).Fatal("error loading args")
}
// Load all `non-flag` arguments
// which will be parsed separately.
nsvrs, qt, qc, qn := loadUnparsedArgs(f.Args())
app.QueryFlags.Nameservers = append(app.QueryFlags.Nameservers, nsvrs...)
app.QueryFlags.QTypes = append(app.QueryFlags.QTypes, qt...)
app.QueryFlags.QClasses = append(app.QueryFlags.QClasses, qc...)
app.QueryFlags.QNames = append(app.QueryFlags.QNames, qn...)
// Check if reverse flag is passed. If it is, then set
// query type as PTR and query class as IN.
// Modify query name like 94.2.0.192.in-addr.arpa if it's an IPv4 address.
// Use IP6.ARPA nibble format otherwise.
if app.QueryFlags.ReverseLookup {
app.ReverseLookup()
}
// Load fallbacks.
app.LoadFallbacks()
// Load Questions.
app.PrepareQuestions()
// Load Nameservers.
err = app.LoadNameservers()
if err != nil {
app.Logger.WithError(err).Fatal("error loading nameservers")
}
ropts := resolvers.Options{
Nameservers: app.Nameservers,
UseIPv4: app.QueryFlags.UseIPv4,
UseIPv6: app.QueryFlags.UseIPv6,
SearchList: app.ResolverOpts.SearchList,
Ndots: app.ResolverOpts.Ndots,
Timeout: app.QueryFlags.Timeout * time.Second,
Logger: logger,
Strategy: app.QueryFlags.Strategy,
InsecureSkipVerify: app.QueryFlags.InsecureSkipVerify,
TLSHostname: app.QueryFlags.TLSHostname,
}
if contains(app.QueryFlags.Tweaks, "aa") {
ropts.Authoritative = true
}
if contains(app.QueryFlags.Tweaks, "ad") {
ropts.AuthenticatedData = true
}
if contains(app.QueryFlags.Tweaks, "cd") {
ropts.CheckingDisabled = true
}
// Load Resolvers.
rslvrs, err := resolvers.LoadResolvers(ropts)
if err != nil {
app.Logger.WithError(err).Fatal("error loading resolver")
}
app.Resolvers = rslvrs
app.Logger.Debug("Starting doggo 🐶")
if len(app.QueryFlags.QNames) == 0 {
f.Usage()
os.Exit(0)
}
// Resolve Queries.
var responses []resolvers.Response
for _, q := range app.Questions {
for _, rslv := range app.Resolvers {
resp, err := app.LookupWithRetry(app.QueryFlags.RetryCount, rslv, q)
if err != nil {
app.Logger.WithError(err).Error("error looking up DNS records")
}
responses = append(responses, resp)
}
}
app.Output(responses)
// Quitting.
os.Exit(0)
}

108
cmd/doggo-v1/help.go Normal file
View file

@ -0,0 +1,108 @@
package main
import (
"os"
"text/template"
"github.com/fatih/color"
)
// appHelpTextTemplate is the text/template to customise the Help output.
// Uses text/template to render templates.
var appHelpTextTemplate = `{{ "NAME" | color "" "heading" }}:
{{ .Name | color "green" "bold" }} 🐶 {{.Description}}
{{ "USAGE" | color "" "heading" }}:
{{ .Name | color "green" "bold" }} [--] {{ "[query options]" | color "yellow" "" }} {{ "[arguments...]" | color "cyan" "" }}
{{ "VERSION" | color "" "heading" }}:
{{.Version | color "red" "" }}
{{ "EXAMPLES" | color "" "heading" }}:
{{ .Name | color "green" "bold" }} {{ "mrkaran.dev" | color "cyan" "" }} {{"\t"}} Query a domain using defaults.
{{ .Name | color "green" "bold" }} {{ "mrkaran.dev CNAME" | color "cyan" "" }} {{"\t"}} Looks up for a CNAME record.
{{ .Name | color "green" "bold" }} {{ "mrkaran.dev MX @9.9.9.9" | color "cyan" "" }} {{"\t"}} Uses a custom DNS resolver.
{{ .Name | color "green" "bold" }} {{"-q mrkaran.dev -t MX -n 1.1.1.1" | color "yellow" ""}} {{"\t"}} Using named arguments.
{{ "Free Form Arguments" | color "" "heading" }}:
Supply hostnames, query types, classes without any flag. For eg:
{{ .Name | color "green" "bold" }} {{"mrkaran.dev A @1.1.1.1" | color "cyan" "" }}
{{ "Transport Options" | color "" "heading" }}:
Based on the URL scheme the correct resolver is chosen.
Fallbacks to UDP resolver if no scheme is present.
{{"@udp://" | color "yellow" ""}} eg: @1.1.1.1 initiates a {{"UDP" | color "cyan" ""}} resolver for 1.1.1.1:53.
{{"@tcp://" | color "yellow" ""}} eg: @tcp://1.1.1.1 initiates a {{"TCP" | color "cyan" ""}} resolver for 1.1.1.1:53.
{{"@https://" | color "yellow" ""}} eg: @https://cloudflare-dns.com/dns-query initiates a {{"DOH" | color "cyan" ""}} resolver for Cloudflare DoH server.
{{"@tls://" | color "yellow" ""}} eg: @tls://1.1.1.1 initiates a {{"DoT" | color "cyan" ""}} resolver for 1.1.1.1:853.
{{"@sdns://" | color "yellow" ""}} initiates a {{"DNSCrypt" | color "cyan" ""}} or {{"DoH" | color "cyan" ""}} resolver using its DNS stamp.
{{"@quic://" | color "yellow" ""}} initiates a {{"DOQ" | color "cyan" ""}} resolver.
{{ "Query Options" | color "" "heading" }}:
{{"-q, --query=HOSTNAME" | color "yellow" ""}} Hostname to query the DNS records for (eg {{"mrkaran.dev" | color "cyan" ""}}).
{{"-t, --type=TYPE" | color "yellow" ""}} Type of the DNS Record ({{"A, MX, NS" | color "cyan" ""}} etc).
{{"-n, --nameserver=ADDR" | color "yellow" ""}} Address of a specific nameserver to send queries to ({{"9.9.9.9, 8.8.8.8" | color "cyan" ""}} etc).
{{"-c, --class=CLASS" | color "yellow" ""}} Network class of the DNS record ({{"IN, CH, HS" | color "cyan" ""}} etc).
{{"-x, --reverse" | color "yellow" ""}} Performs a DNS Lookup for an IPv4 or IPv6 address. Sets the query type and class to PTR and IN respectively.
{{ "Resolver Options" | color "" "heading" }}:
{{"--strategy=STRATEGY" | color "yellow" ""}} Specify strategy to query nameserver listed in etc/resolv.conf. ({{"all, random, first" | color "cyan" ""}}).
{{"--ndots=INT" | color "yellow" ""}} Specify ndots parameter. Takes value from /etc/resolv.conf if using the system namesever or 1 otherwise.
{{"--search" | color "yellow" ""}} Use the search list defined in resolv.conf. Defaults to true. Set --search=false to disable search list.
{{"--timeout" | color "yellow" ""}} Specify timeout (in seconds) for the resolver to return a response.
{{"-4 --ipv4" | color "yellow" ""}} Use IPv4 only.
{{"-6 --ipv6" | color "yellow" ""}} Use IPv6 only.
{{"--ndots=INT" | color "yellow" ""}} Specify ndots parameter. Takes value from /etc/resolv.conf if using the system namesever or 1 otherwise.
{{"--tls-hostname=HOSTNAME" | color "yellow" ""}} Provide a hostname for doing verification of the certificate if the provided DoT nameserver is an IP.
{{"--skip-hostname-verification" | color "yellow" ""}} Skip TLS Hostname Verification in case of DOT Lookups.
{{ "Output Options" | color "" "heading" }}:
{{"-J, --json " | color "yellow" ""}} Format the output as JSON.
{{"--short" | color "yellow" ""}} Short output format. Shows only the response section.
{{"--color " | color "yellow" ""}} Defaults to true. Set --color=false to disable colored output.
{{"--debug " | color "yellow" ""}} Enable debug logging.
{{"--time" | color "yellow" ""}} Shows how long the response took from the server.
`
func renderCustomHelp() {
helpTmplVars := map[string]string{
"Name": "doggo",
"Description": "DNS Client for Humans",
"Version": buildString,
}
tmpl, err := template.New("test").Funcs(template.FuncMap{
"color": func(clr string, format string, str string) string {
formatter := color.New()
switch c := clr; c {
case "yellow":
formatter = formatter.Add(color.FgYellow)
case "red":
formatter = formatter.Add(color.FgRed)
case "cyan":
formatter = formatter.Add(color.FgCyan)
case "green":
formatter = formatter.Add(color.FgGreen)
}
switch f := format; f {
case "bold":
formatter = formatter.Add(color.Bold)
case "underline":
formatter = formatter.Add(color.Underline)
case "heading":
formatter = formatter.Add(color.Bold, color.Underline)
}
return formatter.SprintFunc()(str)
},
}).Parse(appHelpTextTemplate)
if err != nil {
// should ideally never happen.
panic(err)
}
err = tmpl.Execute(os.Stdout, helpTmplVars)
if err != nil {
// should ideally never happen.
panic(err)
}
os.Exit(0)
}

45
cmd/doggo-v1/parse.go Normal file
View file

@ -0,0 +1,45 @@
package main
import (
"strings"
"github.com/miekg/dns"
)
// loadUnparsedArgs tries to parse all the arguments
// which are unparsed by `flag` library. These arguments don't have any specific
// order so we have to deduce based on the pattern of argument.
// For eg, a nameserver must always begin with `@`. In this
// pattern we deduce the arguments and append it to the
// list of internal query flags.
// In case an argument isn't able to fit in any of the existing
// pattern it is considered to be a "hostname".
// Eg of unparsed argument: `dig mrkaran.dev @1.1.1.1 AAAA`
// where `@1.1.1.1` and `AAAA` are "unparsed" args.
// Returns a list of nameserver, queryTypes, queryClasses, queryNames.
func loadUnparsedArgs(args []string) ([]string, []string, []string, []string) {
var ns, qt, qc, qn []string
for _, arg := range args {
if strings.HasPrefix(arg, "@") {
ns = append(ns, strings.Trim(arg, "@"))
} else if _, ok := dns.StringToType[strings.ToUpper(arg)]; ok {
qt = append(qt, arg)
} else if _, ok := dns.StringToClass[strings.ToUpper(arg)]; ok {
qc = append(qc, arg)
} else {
// if nothing matches, consider it's a query name.
qn = append(qn, arg)
}
}
return ns, qt, qc, qn
}
// contains is a helper method to check if a paritcular element exists in the slice.
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}