diff options
Diffstat (limited to 'cmd/resolver')
| -rw-r--r-- | cmd/resolver/main.go | 238 | ||||
| -rw-r--r-- | cmd/resolver/options.go | 162 | ||||
| -rw-r--r-- | cmd/resolver/resolver.go | 243 |
3 files changed, 315 insertions, 328 deletions
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 <ms@kilabit.info> // 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) - rc, err = libnet.NewResolvConf(defResolvConf) - if err != nil { - log.Fatal("! ", err) - } + args []string + optHelp bool + ) - if len(rc.NameServers) == 0 { - ns = "127.0.0.1:53" - } else { - ns = rc.NameServers[0] - } + log.SetFlags(0) - cl, err = dns.NewUDPClient(ns) - if err != nil { - log.Fatal("! ", err) - } + 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, "") - return -} + flag.Parse() -func populateQueries(cr *libnet.ResolvConf, qname string) (queries []string) { - ndots := 0 + args = flag.Args() - for _, c := range qname { - if c == '.' { - ndots++ - continue - } + if optHelp { + 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) - } + if len(args) == 0 { + help() + os.Exit(1) } - return -} + rsol.cmd = strings.ToLower(args[0]) -func messagePrint(nameserver string, msg *dns.Message) string { - var b strings.Builder + switch rsol.cmd { + case cmdQuery: + args = args[1:] + if len(args) == 0 { + log.Fatalf("resolver: %s: missing argument", rsol.cmd) + } - fmt.Fprintf(&b, "< From: %s", nameserver) - fmt.Fprintf(&b, "\n> Header: %+v", msg.Header) - fmt.Fprintf(&b, "\n> Question: %s", msg.Question.String()) + rsol.doCmdQuery(args) - 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] <command> <args> -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 <domain / ip-address> [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 + $ resolver -ns udp://35.240.172.103 query kilabit.info MX - // 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()) +Query the IPv4 records of domain name "kilabit.info" using DNS over TLS on +name server 35.240.172.103, - res = lookup(opts, cl, timeout, qname) - if res != nil { - goto out - } - } - } + $ resolver -ns https://35.240.172.103 -insecure query kilabit.info -out: - if res != nil { - println(messagePrint(cl.RemoteAddr(), res)) - } +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 <ms@kilabit.info> -// 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 <ms@kilabit.info> +// 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()) +} |
