unfinished mess
This commit is contained in:
parent
6c3b17ba0d
commit
2e2e3b1ec8
26 changed files with 364 additions and 171 deletions
53
internal/client/client.go
Normal file
53
internal/client/client.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/mr-karan/doggo/internal/models"
|
||||
"github.com/mr-karan/doggo/internal/resolvers"
|
||||
"github.com/mr-karan/logf"
|
||||
)
|
||||
|
||||
// Client represents the structure for all app wide configuration.
|
||||
type Client struct {
|
||||
Log *logf.Logger
|
||||
Version string
|
||||
QueryFlags models.QueryFlags
|
||||
Questions []dns.Question
|
||||
Resolvers []resolvers.Resolver
|
||||
Nameservers []models.Nameserver
|
||||
}
|
||||
|
||||
// New initializes an instance of App which holds app wide configuration.
|
||||
func New(logger *logf.Logger, buildVersion string) Client {
|
||||
return Client{
|
||||
Log: logger,
|
||||
Version: buildVersion,
|
||||
QueryFlags: models.QueryFlags{
|
||||
QNames: []string{},
|
||||
QTypes: []string{},
|
||||
QClasses: []string{},
|
||||
Nameservers: []string{},
|
||||
},
|
||||
Nameservers: []models.Nameserver{},
|
||||
}
|
||||
}
|
||||
|
||||
// LookupWithRetry attempts a DNS Lookup with retries for a given Question.
|
||||
func (hub *Client) LookupWithRetry(attempts int, resolver resolvers.Resolver, ques dns.Question) (resolvers.Response, error) {
|
||||
resp, err := resolver.Lookup(ques)
|
||||
if err != nil {
|
||||
// Retry lookup.
|
||||
attempts--
|
||||
if attempts > 0 {
|
||||
// Add some random delay.
|
||||
time.Sleep(time.Millisecond*300 + (time.Duration(rand.Int63n(int64(time.Millisecond*100))))/2)
|
||||
hub.Log.Debug("retrying lookup")
|
||||
return hub.LookupWithRetry(attempts, resolver, ques)
|
||||
}
|
||||
return resolvers.Response{}, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
162
internal/client/nameservers.go
Normal file
162
internal/client/nameservers.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/ameshkov/dnsstamps"
|
||||
"github.com/mr-karan/doggo/internal/config"
|
||||
"github.com/mr-karan/doggo/internal/models"
|
||||
)
|
||||
|
||||
// LoadNameservers reads all the user given
|
||||
// nameservers and loads to Client.
|
||||
func (hub *Client) LoadNameservers() error {
|
||||
for _, srv := range hub.QueryFlags.Nameservers {
|
||||
ns, err := initNameserver(srv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing nameserver: %s", srv)
|
||||
}
|
||||
// check if properly initialised.
|
||||
if ns.Address != "" && ns.Type != "" {
|
||||
hub.Nameservers = append(hub.Nameservers, ns)
|
||||
}
|
||||
}
|
||||
|
||||
// Set `ndots` to the user specified value.
|
||||
hub.ResolverOpts.Ndots = hub.QueryFlags.Ndots
|
||||
// fallback to system nameserver
|
||||
// in case no nameserver is specified by user.
|
||||
if len(hub.Nameservers) == 0 {
|
||||
ns, ndots, search, err := getDefaultServers(hub.QueryFlags.Strategy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching system default nameserver")
|
||||
}
|
||||
// `-1` indicates the flag is not set.
|
||||
// use from config if user hasn't specified any value.
|
||||
if hub.ResolverOpts.Ndots == -1 {
|
||||
hub.ResolverOpts.Ndots = ndots
|
||||
}
|
||||
if len(search) > 0 && hub.QueryFlags.UseSearchList {
|
||||
hub.ResolverOpts.SearchList = search
|
||||
}
|
||||
hub.Nameservers = append(hub.Nameservers, ns...)
|
||||
}
|
||||
// if the user hasn't given any override of `ndots` AND has
|
||||
// given a custom nameserver. Set `ndots` to 1 as the fallback value
|
||||
if hub.ResolverOpts.Ndots == -1 {
|
||||
hub.ResolverOpts.Ndots = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initNameserver(n string) (models.Nameserver, error) {
|
||||
// Instantiate a UDP resolver with default port as a fallback.
|
||||
ns := models.Nameserver{
|
||||
Type: models.UDPResolver,
|
||||
Address: net.JoinHostPort(n, models.DefaultUDPPort),
|
||||
}
|
||||
u, err := url.Parse(n)
|
||||
if err != nil {
|
||||
return ns, err
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "sdns":
|
||||
stamp, err := dnsstamps.NewServerStampFromString(n)
|
||||
if err != nil {
|
||||
return ns, err
|
||||
}
|
||||
switch stamp.Proto {
|
||||
case dnsstamps.StampProtoTypeDoH:
|
||||
ns.Type = models.DOHResolver
|
||||
address := url.URL{Scheme: "https", Host: stamp.ProviderName, Path: stamp.Path}
|
||||
ns.Address = address.String()
|
||||
case dnsstamps.StampProtoTypeDNSCrypt:
|
||||
ns.Type = models.DNSCryptResolver
|
||||
ns.Address = n
|
||||
default:
|
||||
return ns, fmt.Errorf("unsupported protocol: %v", stamp.Proto.String())
|
||||
}
|
||||
|
||||
case "https":
|
||||
ns.Type = models.DOHResolver
|
||||
ns.Address = u.String()
|
||||
|
||||
case "tls":
|
||||
ns.Type = models.DOTResolver
|
||||
if u.Port() == "" {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), models.DefaultTLSPort)
|
||||
} else {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), u.Port())
|
||||
}
|
||||
|
||||
case "tcp":
|
||||
ns.Type = models.TCPResolver
|
||||
if u.Port() == "" {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), models.DefaultTCPPort)
|
||||
} else {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), u.Port())
|
||||
}
|
||||
|
||||
case "udp":
|
||||
ns.Type = models.UDPResolver
|
||||
if u.Port() == "" {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), models.DefaultUDPPort)
|
||||
} else {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), u.Port())
|
||||
}
|
||||
case "quic":
|
||||
ns.Type = models.DOQResolver
|
||||
if u.Port() == "" {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), models.DefaultDOQPort)
|
||||
} else {
|
||||
ns.Address = net.JoinHostPort(u.Hostname(), u.Port())
|
||||
}
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func getDefaultServers(strategy string) ([]models.Nameserver, int, []string, error) {
|
||||
// Load nameservers from `/etc/resolv.conf`.
|
||||
dnsServers, ndots, search, err := config.GetDefaultServers()
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
servers := make([]models.Nameserver, 0, len(dnsServers))
|
||||
|
||||
switch strategy {
|
||||
case "random":
|
||||
// Choose a random server from the list.
|
||||
rand.Seed(time.Now().Unix())
|
||||
srv := dnsServers[rand.Intn(len(dnsServers))]
|
||||
ns := models.Nameserver{
|
||||
Type: models.UDPResolver,
|
||||
Address: net.JoinHostPort(srv, models.DefaultUDPPort),
|
||||
}
|
||||
servers = append(servers, ns)
|
||||
|
||||
case "first":
|
||||
// Choose the first from the list, always.
|
||||
srv := dnsServers[0]
|
||||
ns := models.Nameserver{
|
||||
Type: models.UDPResolver,
|
||||
Address: net.JoinHostPort(srv, models.DefaultUDPPort),
|
||||
}
|
||||
servers = append(servers, ns)
|
||||
|
||||
default:
|
||||
// Default behaviour is to load all nameservers.
|
||||
for _, s := range dnsServers {
|
||||
ns := models.Nameserver{
|
||||
Type: models.UDPResolver,
|
||||
Address: net.JoinHostPort(s, models.DefaultUDPPort),
|
||||
}
|
||||
servers = append(servers, ns)
|
||||
}
|
||||
}
|
||||
|
||||
return servers, ndots, search, nil
|
||||
}
|
149
internal/client/output.go
Normal file
149
internal/client/output.go
Normal file
|
@ -0,0 +1,149 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/mr-karan/doggo/internal/resolvers"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
func (hub *Client) outputJSON(rsp []resolvers.Response) {
|
||||
// Pretty print with 4 spaces.
|
||||
res, err := json.MarshalIndent(rsp, "", " ")
|
||||
if err != nil {
|
||||
hub.Log.WithError(err).Fatal("unable to output data in JSON")
|
||||
}
|
||||
fmt.Printf("%s", res)
|
||||
}
|
||||
|
||||
func (hub *Client) outputShort(rsp []resolvers.Response) {
|
||||
for _, r := range rsp {
|
||||
for _, a := range r.Answers {
|
||||
fmt.Printf("%s\n", a.Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (hub *Client) outputTerminal(rsp []resolvers.Response) {
|
||||
var (
|
||||
green = color.New(color.FgGreen, color.Bold).SprintFunc()
|
||||
blue = color.New(color.FgBlue, color.Bold).SprintFunc()
|
||||
yellow = color.New(color.FgYellow, color.Bold).SprintFunc()
|
||||
cyan = color.New(color.FgCyan, color.Bold).SprintFunc()
|
||||
red = color.New(color.FgRed, color.Bold).SprintFunc()
|
||||
magenta = color.New(color.FgMagenta, color.Bold).SprintFunc()
|
||||
)
|
||||
|
||||
// Disables colorized output if user specified.
|
||||
if !hub.QueryFlags.Color {
|
||||
color.NoColor = true
|
||||
}
|
||||
|
||||
// Conditional Time column.
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
header := []string{"Name", "Type", "Class", "TTL", "Address", "Nameserver"}
|
||||
if hub.QueryFlags.DisplayTimeTaken {
|
||||
header = append(header, "Time Taken")
|
||||
}
|
||||
|
||||
// Show output in case if it's not
|
||||
// a NOERROR.
|
||||
outputStatus := false
|
||||
for _, r := range rsp {
|
||||
for _, a := range r.Authorities {
|
||||
if dns.StringToRcode[a.Status] != dns.RcodeSuccess {
|
||||
outputStatus = true
|
||||
}
|
||||
}
|
||||
for _, a := range r.Answers {
|
||||
if dns.StringToRcode[a.Status] != dns.RcodeSuccess {
|
||||
outputStatus = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if outputStatus {
|
||||
header = append(header, "Status")
|
||||
}
|
||||
|
||||
// Formatting options for the table.
|
||||
table.SetHeader(header)
|
||||
table.SetAutoWrapText(true)
|
||||
table.SetAutoFormatHeaders(true)
|
||||
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
table.SetCenterSeparator("")
|
||||
table.SetColumnSeparator("")
|
||||
table.SetRowSeparator("")
|
||||
table.SetHeaderLine(false)
|
||||
table.SetBorder(false)
|
||||
table.SetTablePadding("\t") // pad with tabs
|
||||
table.SetNoWhiteSpace(true)
|
||||
|
||||
for _, r := range rsp {
|
||||
for _, ans := range r.Answers {
|
||||
var typOut string
|
||||
switch typ := ans.Type; typ {
|
||||
case "A":
|
||||
typOut = blue(ans.Type)
|
||||
case "AAAA":
|
||||
typOut = blue(ans.Type)
|
||||
case "MX":
|
||||
typOut = magenta(ans.Type)
|
||||
case "NS":
|
||||
typOut = cyan(ans.Type)
|
||||
case "CNAME":
|
||||
typOut = yellow(ans.Type)
|
||||
case "TXT":
|
||||
typOut = yellow(ans.Type)
|
||||
case "SOA":
|
||||
typOut = red(ans.Type)
|
||||
default:
|
||||
typOut = blue(ans.Type)
|
||||
}
|
||||
output := []string{green(ans.Name), typOut, ans.Class, ans.TTL, ans.Address, ans.Nameserver}
|
||||
// Print how long it took
|
||||
if hub.QueryFlags.DisplayTimeTaken {
|
||||
output = append(output, ans.RTT)
|
||||
}
|
||||
if outputStatus {
|
||||
output = append(output, red(ans.Status))
|
||||
}
|
||||
table.Append(output)
|
||||
}
|
||||
for _, auth := range r.Authorities {
|
||||
var typOut string
|
||||
switch typ := auth.Type; typ {
|
||||
case "SOA":
|
||||
typOut = red(auth.Type)
|
||||
default:
|
||||
typOut = blue(auth.Type)
|
||||
}
|
||||
output := []string{green(auth.Name), typOut, auth.Class, auth.TTL, auth.MName, auth.Nameserver}
|
||||
// Print how long it took
|
||||
if hub.QueryFlags.DisplayTimeTaken {
|
||||
output = append(output, auth.RTT)
|
||||
}
|
||||
if outputStatus {
|
||||
output = append(output, red(auth.Status))
|
||||
}
|
||||
table.Append(output)
|
||||
}
|
||||
}
|
||||
table.Render()
|
||||
}
|
||||
|
||||
// Output takes a list of `dns.Answers` and based
|
||||
// on the output format specified displays the information.
|
||||
func (hub *Client) Output(responses []resolvers.Response) {
|
||||
if hub.QueryFlags.ShowJSON {
|
||||
hub.outputJSON(responses)
|
||||
} else if hub.QueryFlags.ShortOutput {
|
||||
hub.outputShort(responses)
|
||||
} else {
|
||||
hub.outputTerminal(responses)
|
||||
}
|
||||
}
|
54
internal/client/questions.go
Normal file
54
internal/client/questions.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// LoadFallbacks sets fallbacks for options
|
||||
// that are not specified by the user but necessary
|
||||
// for the resolver.
|
||||
func (hub *Client) LoadFallbacks() {
|
||||
if len(hub.QueryFlags.QTypes) == 0 {
|
||||
hub.QueryFlags.QTypes = append(hub.QueryFlags.QTypes, "A")
|
||||
}
|
||||
if len(hub.QueryFlags.QClasses) == 0 {
|
||||
hub.QueryFlags.QClasses = append(hub.QueryFlags.QClasses, "IN")
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareQuestions takes a list of query names, query types and query classes
|
||||
// and prepare a question for each combination of the above.
|
||||
func (hub *Client) PrepareQuestions() {
|
||||
for _, n := range hub.QueryFlags.QNames {
|
||||
for _, t := range hub.QueryFlags.QTypes {
|
||||
for _, c := range hub.QueryFlags.QClasses {
|
||||
hub.Questions = append(hub.Questions, dns.Question{
|
||||
Name: n,
|
||||
Qtype: dns.StringToType[strings.ToUpper(t)],
|
||||
Qclass: dns.StringToClass[strings.ToUpper(c)],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReverseLookup is used to perform a reverse DNS Lookup
|
||||
// using an IPv4 or IPv6 address.
|
||||
// Query Type is set to PTR, Query Class is set to IN.
|
||||
// Query Names must be formatted in in-addr.arpa. or ip6.arpa format.
|
||||
func (hub *Client) ReverseLookup() {
|
||||
hub.QueryFlags.QTypes = []string{"PTR"}
|
||||
hub.QueryFlags.QClasses = []string{"IN"}
|
||||
formattedNames := make([]string, 0, len(hub.QueryFlags.QNames))
|
||||
|
||||
for _, n := range hub.QueryFlags.QNames {
|
||||
addr, err := dns.ReverseAddr(n)
|
||||
if err != nil {
|
||||
hub.Log.WithError(err).Fatal("error formatting address")
|
||||
}
|
||||
formattedNames = append(formattedNames, addr)
|
||||
}
|
||||
hub.QueryFlags.QNames = formattedNames
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue