From 6ead9209bbc431b33ccbdebfffe98ffd23e4f9a7 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Thu, 14 Apr 2022 01:33:40 +0700 Subject: cmd/resolver: refactor the resolver as client of DNS and rescached Previously, the resolver command only for querying DNS server. In this changes and in the future, the resolver command will be client for DNS and rescached server. --- cmd/resolver/main.go | 244 +++++++++++++++-------------------------------- cmd/resolver/options.go | 162 ------------------------------- cmd/resolver/resolver.go | 243 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 318 insertions(+), 331 deletions(-) delete mode 100644 cmd/resolver/options.go create mode 100644 cmd/resolver/resolver.go (limited to 'cmd/resolver') diff --git a/cmd/resolver/main.go b/cmd/resolver/main.go index 2fdfe3b..49ba6a6 100644 --- a/cmd/resolver/main.go +++ b/cmd/resolver/main.go @@ -1,217 +1,123 @@ // SPDX-FileCopyrightText: 2018 M. Shulhan // SPDX-License-Identifier: GPL-3.0-or-later +// Command resolver is client for DNS server to resolve query and client for +// rescached HTTP server. package main import ( + "flag" "fmt" "log" - "math/rand" + "os" "strings" - "time" - - "github.com/shuLhan/share/lib/dns" - libnet "github.com/shuLhan/share/lib/net" ) +// List of valid commands. const ( - defResolvConf = "/etc/resolv.conf" + cmdQuery = "query" ) -// -// initSystemResolver read the system resolv.conf to create fallback DNS -// resolver. -// -func initSystemResolver() (rc *libnet.ResolvConf, cl dns.Client) { +func main() { var ( - err error - ns string + rsol = new(resolver) + + args []string + optHelp bool ) - rc, err = libnet.NewResolvConf(defResolvConf) - if err != nil { - log.Fatal("! ", err) - } + log.SetFlags(0) - if len(rc.NameServers) == 0 { - ns = "127.0.0.1:53" - } else { - ns = rc.NameServers[0] - } + flag.StringVar(&rsol.nameserver, "ns", "", "Parent name server address using scheme based.") + flag.BoolVar(&rsol.insecure, "insecure", false, "Ignore invalid server certificate") + flag.BoolVar(&optHelp, "h", false, "") - cl, err = dns.NewUDPClient(ns) - if err != nil { - log.Fatal("! ", err) - } + flag.Parse() - return -} + args = flag.Args() -func populateQueries(cr *libnet.ResolvConf, qname string) (queries []string) { - ndots := 0 + if optHelp { + help() + os.Exit(1) + } - for _, c := range qname { - if c == '.' { - ndots++ - continue - } + if len(args) == 0 { + help() + os.Exit(1) } - if ndots >= cr.NDots { - queries = append(queries, qname) - } else { - if len(cr.Domain) > 0 { - queries = append(queries, qname+"."+cr.Domain) - } - for _, s := range cr.Search { - queries = append(queries, qname+"."+s) + rsol.cmd = strings.ToLower(args[0]) + + switch rsol.cmd { + case cmdQuery: + args = args[1:] + if len(args) == 0 { + log.Fatalf("resolver: %s: missing argument", rsol.cmd) } - } - return -} + rsol.doCmdQuery(args) -func messagePrint(nameserver string, msg *dns.Message) string { - var b strings.Builder - - fmt.Fprintf(&b, "< From: %s", nameserver) - fmt.Fprintf(&b, "\n> Header: %+v", msg.Header) - fmt.Fprintf(&b, "\n> Question: %s", msg.Question.String()) - - b.WriteString("\n> Status:") - switch msg.Header.RCode { - case dns.RCodeOK: - b.WriteString(" OK") - case dns.RCodeErrFormat: - b.WriteString(" Invalid request format") - case dns.RCodeErrServer: - b.WriteString(" Server internal failure") - case dns.RCodeErrName: - b.WriteString(" Domain name did not exist") - case dns.RCodeNotImplemented: - b.WriteString(" Unknown query") - case dns.RCodeRefused: - b.WriteString(" Server refused the request") + default: + log.Printf("resolver: unknown command: %s", rsol.cmd) + os.Exit(2) } +} - if msg.Header.RCode != dns.RCodeOK { - return b.String() - } +func help() { + fmt.Println(` += resolver: command line interface for DNS and rescached server - for x, rr := range msg.Answer { - fmt.Fprintf(&b, "\n> Answer #%d:", x+1) - fmt.Fprintf(&b, "\n>> Resource record: %s", rr.String()) - fmt.Fprintf(&b, "\n>> RDATA: %s", rr.Value) - } - for x, rr := range msg.Authority { - fmt.Fprintf(&b, "\n> Authority #%d:", x+1) - fmt.Fprintf(&b, "\n>> Resource record: %s", rr.String()) - fmt.Fprintf(&b, "\n>> RDATA: %s", rr.Value) - } - for x, rr := range msg.Additional { - fmt.Fprintf(&b, "\n> Additional #%d:", x+1) - fmt.Fprintf(&b, "\n>> Resource record: %s", rr.String()) - fmt.Fprintf(&b, "\n>> RDATA: %s", rr.Value) - } +== Usage - return b.String() -} + resolver [-ns nameserver] [-insecure] -func lookup(opts *options, cl dns.Client, timeout time.Duration, qname string, -) *dns.Message { - var ( - err error - ) +== Options - rand.Seed(time.Now().Unix()) +Accepted command is query. - cl.SetTimeout(timeout) +-ns nameserver - req := dns.NewMessage() - req.Header.ID = uint16(rand.Intn(65535)) - req.Question.Name = qname - req.Question.Type = opts.qtype - req.Question.Class = opts.qclass - _, err = req.Pack() - if err != nil { - log.Fatal("! Pack:", err) - } + Parent name server address using scheme based. + For example, + udp://35.240.172.103:53 for querying with UDP, + tcp://35.240.172.103:53 for querying with TCP, + https://35.240.172:103:853 for querying with DNS over TLS (DoT), and + https://kilabit.info/dns-query for querying with DNS over HTTPS (DoH). - res, err := cl.Query(req) - if err != nil { - log.Println("! Lookup: ", err) - return nil - } +-insecure - if res.Header.RCode == 0 { - return res - } + Ignore invalid server certificate when querying DoT, DoH, or rescached + server. - switch res.Header.RCode { - case dns.RCodeErrFormat: - log.Println("! ResponseCode: Format error") - case dns.RCodeErrServer: - log.Println("! ResponseCode: Server failure") - case dns.RCodeErrName: - log.Println("! ResponseCode: Domain not exist") - case dns.RCodeNotImplemented: - log.Println("! ResponseCode: Not implemented") - case dns.RCodeRefused: - log.Println("! ResponseCode: Refused") - } - return nil -} +== Commands -func main() { - var ( - cl dns.Client - rc *libnet.ResolvConf - res *dns.Message - err error - ) +query [type] [class] - log.SetFlags(0) + Query the domain or IP address with optional type and/or class. - opts, err := newOptions() - if err != nil { - log.Fatal("! ", err) - } + Unless the option "-ns" is given, the query command will use the + nameserver defined in the system resolv.conf file. - fmt.Printf("= options: %+v\n", opts) + Valid type are either A, NS, CNAME, SOA, MB, MG, MR, NULL, + WKS, PTR, HINFO, MINFO, MX, TXT, AAAA, or SRV. + Default value is A." - rc, systemResolver := initSystemResolver() + Valid class are either IN, CS, HS. + Default value is IN. - fmt.Printf("= resolv.conf: %+v\n", rc) +== Examples - if len(opts.nameserver) == 0 { - cl = systemResolver - } else { - cl, err = dns.NewClient(opts.nameserver, opts.insecure) - if err != nil { - log.Fatal(err) - } - } +Query the MX records using UDP on name server 35.240.172.103, - queries := populateQueries(rc, opts.qname) - timeout := time.Duration(rc.Timeout) * time.Second - - // The algorithm used is to try a name server, and if the query - // times out, try the next, until out of name servers, then repeat - // trying all the name servers until a maximum number of retries are - // made.) - for _, qname := range queries { - for x := 0; x < rc.Attempts; x++ { - fmt.Printf("> Lookup %s at %s\n", qname, cl.RemoteAddr()) - - res = lookup(opts, cl, timeout, qname) - if res != nil { - goto out - } - } - } + $ resolver -ns udp://35.240.172.103 query kilabit.info MX -out: - if res != nil { - println(messagePrint(cl.RemoteAddr(), res)) - } +Query the IPv4 records of domain name "kilabit.info" using DNS over TLS on +name server 35.240.172.103, + + $ resolver -ns https://35.240.172.103 -insecure query kilabit.info + +Query the IPv4 records of domain name "kilabit.info" using DNS over HTTPS on +name server kilabit.info, + + $ resolver -ns https://kilabit.info/dns-query query kilabit.info`) } diff --git a/cmd/resolver/options.go b/cmd/resolver/options.go deleted file mode 100644 index 95d9aeb..0000000 --- a/cmd/resolver/options.go +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-FileCopyrightText: 2018 M. Shulhan -// SPDX-License-Identifier: GPL-3.0-or-later - -package main - -import ( - "errors" - "flag" - "fmt" - "os" - "strings" - - "github.com/shuLhan/share/lib/dns" -) - -// List of error messages. -var ( - errQueryName = errors.New("invalid or empty query name") - errRecordType = errors.New("unknown query type") - errRecordClass = errors.New("unknown query class") -) - -// List of command line usages. -const ( - usageInsecure = `Skip verifying server certificate` - usageNameServer = "Parent name server address using scheme based.\n" + - "\tFor example,\n" + - "\tudp://35.240.172.103:53 for querying with UDP,\n" + - "\ttcp://35.240.172.103:53 for querying with TCP,\n" + - "\thttps://35.240.172:103:853 for querying with DNS over TLS, and\n" + - "\thttps://kilabit.info/dns-query for querying with DNS over HTTPS." - - usageType = "Query type. Valid values are either A, NS, CNAME, SOA,\n" + - "\tMB, MG, MR, NULL, WKS, PTR, HINFO, MINFO, MX, TXT, AAAA, or SRV.\n" + - "\tDefault value is A." - - usageClass = "Query class. Valid values are either IN, CS, HS.\n" + - "\tDefault value is IN." -) - -type options struct { - sqtype string - sqclass string - - nameserver string - qname string - qtype dns.RecordType - qclass dns.RecordClass - - insecure bool -} - -func help() { - fmt.Println(` -= resolver: command line interface for DNS query - -== Usage - - resolver [-ns nameserver] [-insecure] [-t string] [-c string] [domain|address] - -== Options - --ns nameserver - - ` + usageNameServer + ` - --insecure - - ` + usageInsecure + ` - --t string - - ` + usageType + ` - --c string - - ` + usageClass + ` - -== Examples - -Query the MX records using UDP on name server 35.240.172.103, - - $ resolver -ns udp://35.240.172.103 -t MX kilabit.info - -Query the IPv4 records of domain name "kilabit.info" using DNS over TLS on -name server 35.240.172.103, - - $ resolver -ns https://35.240.172.103 -insecure kilabit.info - -Query the IPv4 records of domain name "kilabit.info" using DNS over HTTPS on -name server kilabit.info, - - $ resolver -ns https://kilabit.info/dns-query kilabit.info`) -} - -func newOptions() (*options, error) { - var optHelp bool - - opts := new(options) - - flag.StringVar(&opts.nameserver, "ns", "", usageNameServer) - flag.BoolVar(&opts.insecure, "insecure", false, usageInsecure) - flag.BoolVar(&optHelp, "h", false, "") - flag.StringVar(&opts.sqtype, "t", "A", usageType) - flag.StringVar(&opts.sqclass, "c", "IN", usageClass) - - flag.Parse() - - args := flag.Args() - - if optHelp { - help() - os.Exit(1) - } - - if len(args) == 0 { - help() - os.Exit(1) - } - - opts.qname = args[0] - - err := opts.parseQType() - if err != nil { - help() - os.Exit(1) - } - - err = opts.parseQClass() - if err != nil { - help() - os.Exit(1) - } - - return opts, nil -} - -func (opts *options) parseQType() error { - var ok bool - - opts.sqtype = strings.ToUpper(opts.sqtype) - - opts.qtype, ok = dns.RecordTypes[opts.sqtype] - if !ok { - return errRecordType - } - - return nil -} - -func (opts *options) parseQClass() error { - var ok bool - - opts.sqclass = strings.ToUpper(opts.sqclass) - - opts.qclass, ok = dns.RecordClasses[opts.sqclass] - if !ok { - return errRecordClass - } - - return nil -} diff --git a/cmd/resolver/resolver.go b/cmd/resolver/resolver.go new file mode 100644 index 0000000..19cebaf --- /dev/null +++ b/cmd/resolver/resolver.go @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: 2018 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package main + +import ( + "fmt" + "log" + "math/rand" + "strings" + "time" + + "github.com/shuLhan/share/lib/dns" + libnet "github.com/shuLhan/share/lib/net" +) + +const ( + defAttempts = 1 + defQueryType = "A" + defQueryClass = "IN" + defResolvConf = "/etc/resolv.conf" + defTimeout = 5 * time.Second +) + +type resolver struct { + conf *libnet.ResolvConf + dnsc dns.Client + + cmd string + qname string + sqtype string + sqclass string + + nameserver string + qtype dns.RecordType + qclass dns.RecordClass + + insecure bool +} + +func (rsol *resolver) doCmdQuery(args []string) { + var ( + maxAttempts = defAttempts + timeout = defTimeout + + res *dns.Message + qname string + queries []string + nAttempts int + err error + ok bool + ) + + rsol.qname = args[0] + + switch len(args) { + case 1: + rsol.sqtype = defQueryType + rsol.sqclass = defQueryClass + + case 2: + rsol.sqtype = args[1] + rsol.sqclass = defQueryClass + + case 3: + rsol.sqtype = args[1] + rsol.sqclass = args[2] + } + + rsol.sqtype = strings.ToUpper(rsol.sqtype) + rsol.qtype, ok = dns.RecordTypes[rsol.sqtype] + if !ok { + log.Fatalf("resolver: invalid query type: %q", rsol.sqtype) + } + + rsol.sqclass = strings.ToUpper(rsol.sqclass) + rsol.qclass, ok = dns.RecordClasses[rsol.sqclass] + if !ok { + log.Fatalf("resolver: invalid query class: %q", rsol.sqclass) + } + + fmt.Printf("= options: %+v\n", rsol) + + if len(rsol.nameserver) == 0 { + // Use the nameserver and configuration from resolv.conf. + err = rsol.initSystemResolver() + if err != nil { + log.Fatalf("resolver: %s", err) + } + + fmt.Printf("= resolv.conf: %+v\n", rsol.conf) + + queries = populateQueries(rsol.conf, rsol.qname) + timeout = time.Duration(rsol.conf.Timeout) * time.Second + maxAttempts = rsol.conf.Attempts + } else { + rsol.dnsc, err = dns.NewClient(rsol.nameserver, rsol.insecure) + if err != nil { + log.Fatalf("resolver: %s", err) + } + + queries = append(queries, rsol.qname) + } + + for _, qname = range queries { + for nAttempts = 0; nAttempts < maxAttempts; nAttempts++ { + fmt.Printf("< Query %s at %s\n", qname, rsol.dnsc.RemoteAddr()) + + res, err = rsol.query(timeout, qname) + if err != nil { + log.Printf("resolver: %s", err) + continue + } + + printQueryResponse(rsol.dnsc.RemoteAddr(), res) + return + } + } +} + +// +// initSystemResolver read the system resolv.conf to create fallback DNS +// resolver. +// +func (rsol *resolver) initSystemResolver() (err error) { + var ( + logp = "initSystemResolver" + + ns string + ) + + rsol.conf, err = libnet.NewResolvConf(defResolvConf) + if err != nil { + return fmt.Errorf("%s: %w", logp, err) + } + + if len(rsol.conf.NameServers) == 0 { + ns = "127.0.0.1:53" + } else { + ns = rsol.conf.NameServers[0] + } + + rsol.dnsc, err = dns.NewUDPClient(ns) + if err != nil { + return fmt.Errorf("%s: %w", logp, err) + } + return nil +} + +func (rsol *resolver) query(timeout time.Duration, qname string) (res *dns.Message, err error) { + var ( + logp = "query" + req = dns.NewMessage() + ) + + rand.Seed(time.Now().Unix()) + + rsol.dnsc.SetTimeout(timeout) + + req.Header.ID = uint16(rand.Intn(65535)) + req.Question.Name = qname + req.Question.Type = rsol.qtype + req.Question.Class = rsol.qclass + _, err = req.Pack() + if err != nil { + return nil, fmt.Errorf("%s: %s: %w", logp, qname, err) + } + + res, err = rsol.dnsc.Query(req) + if err != nil { + return nil, fmt.Errorf("%s: %s: %w", logp, qname, err) + } + + return res, nil +} + +func populateQueries(cr *libnet.ResolvConf, qname string) (queries []string) { + ndots := 0 + + for _, c := range qname { + if c == '.' { + ndots++ + continue + } + } + + if ndots >= cr.NDots { + queries = append(queries, qname) + } else { + if len(cr.Domain) > 0 { + queries = append(queries, qname+"."+cr.Domain) + } + for _, s := range cr.Search { + queries = append(queries, qname+"."+s) + } + } + + return +} + +func printQueryResponse(nameserver string, msg *dns.Message) { + var b strings.Builder + + fmt.Fprintf(&b, "> From: %s", nameserver) + fmt.Fprintf(&b, "\n> Header: %+v", msg.Header) + fmt.Fprintf(&b, "\n> Question: %s", msg.Question.String()) + + b.WriteString("\n> Status: ") + switch msg.Header.RCode { + case dns.RCodeOK: + b.WriteString("OK") + case dns.RCodeErrFormat: + b.WriteString("Invalid request format") + case dns.RCodeErrServer: + b.WriteString("Server internal failure") + case dns.RCodeErrName: + fmt.Fprintf(&b, "Domain name with type %s and class %s did not exist", + dns.RecordTypeNames[msg.Question.Type], + dns.RecordClassName[msg.Question.Class]) + case dns.RCodeNotImplemented: + b.WriteString(" Unknown query") + case dns.RCodeRefused: + b.WriteString(" Server refused the request") + } + + for x, rr := range msg.Answer { + fmt.Fprintf(&b, "\n> Answer #%d:", x+1) + fmt.Fprintf(&b, "\n>> Resource record: %s", rr.String()) + fmt.Fprintf(&b, "\n>> RDATA: %+v", rr.Value) + } + for x, rr := range msg.Authority { + fmt.Fprintf(&b, "\n> Authority #%d:", x+1) + fmt.Fprintf(&b, "\n>> Resource record: %s", rr.String()) + fmt.Fprintf(&b, "\n>> RDATA: %+v", rr.Value) + } + for x, rr := range msg.Additional { + fmt.Fprintf(&b, "\n> Additional #%d:", x+1) + fmt.Fprintf(&b, "\n>> Resource record: %s", rr.String()) + fmt.Fprintf(&b, "\n>> RDATA: %+v", rr.Value) + } + + fmt.Println(b.String()) +} -- cgit v1.3