feat: Add support for multiple resolvers

This commit is contained in:
Karan Sharma 2020-12-15 23:09:10 +05:30
parent 80d43011f8
commit b46b64c1ae
13 changed files with 332 additions and 226 deletions

View file

@ -2,8 +2,11 @@ package resolvers
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/miekg/dns"
@ -15,10 +18,26 @@ type DOHResolver struct {
servers []string
}
type DOHResolverOpts struct {
Timeout time.Duration
}
// NewDOHResolver accepts a list of nameservers and configures a DOH based resolver.
func NewDOHResolver(servers []string) (Resolver, error) {
func NewDOHResolver(servers []string, opts DOHResolverOpts) (Resolver, error) {
if len(servers) == 0 {
return nil, errors.New(`no DOH server specified`)
}
for _, s := range servers {
u, err := url.ParseRequestURI(s)
if err != nil {
return nil, fmt.Errorf("%s is not a valid HTTPS nameserver", s)
}
if u.Scheme != "https" {
return nil, fmt.Errorf("missing https in %s", s)
}
}
httpClient := &http.Client{
Timeout: 10 * time.Second,
Timeout: opts.Timeout,
}
return &DOHResolver{
client: httpClient,
@ -46,7 +65,7 @@ func (d *DOHResolver) Lookup(questions []dns.Question) ([]Response, error) {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, err
return nil, fmt.Errorf("error from nameserver %s", resp.Status)
}
rtt := time.Since(now)
// extract the binary response in DNS Message.

View file

@ -7,14 +7,14 @@ import (
)
// Resolver implements the configuration for a DNS
// Client. Different types of client like (UDP/TCP/DOH/DOT)
// can be initialised.
// Client. Different types of providers can load
// a DNS Resolver satisfying this interface.
type Resolver interface {
Lookup([]dns.Question) ([]Response, error)
}
// Response represents a custom output format
// which wraps certain metadata about the DNS query
// for DNS queries. It wraps metadata about the DNS query
// and the DNS Answer as well.
type Response struct {
Message dns.Msg

View file

@ -1,73 +0,0 @@
package resolvers
import (
"fmt"
"net"
"github.com/miekg/dns"
)
// SystemResolver represents the config options based on the
// resolvconf file.
type SystemResolver struct {
client *dns.Client
config *dns.ClientConfig
servers []string
}
// NewSystemResolver loads the configuration from resolv config file
// and initialises a DNS resolver.
func NewSystemResolver(resolvFilePath string) (Resolver, error) {
if resolvFilePath == "" {
resolvFilePath = DefaultResolvConfPath
}
cfg, err := dns.ClientConfigFromFile(resolvFilePath)
if err != nil {
return nil, err
}
servers := make([]string, 0, len(cfg.Servers))
for _, s := range cfg.Servers {
ip := net.ParseIP(s)
// handle IPv6
if ip != nil && ip.To4() != nil {
servers = append(servers, fmt.Sprintf("%s:%s", s, cfg.Port))
} else {
servers = append(servers, fmt.Sprintf("[%s]:%s", s, cfg.Port))
}
}
client := &dns.Client{}
return &SystemResolver{
client: client,
servers: servers,
config: cfg,
}, nil
}
// Lookup prepare a list of DNS messages to be sent to the server.
// It's possible to send multiple question in one message
// but some nameservers are not able to
func (s *SystemResolver) Lookup(questions []dns.Question) ([]Response, error) {
var (
messages = prepareMessages(questions)
responses []Response
)
for _, msg := range messages {
for _, srv := range s.servers {
in, rtt, err := s.client.Exchange(&msg, srv)
if err != nil {
return nil, err
}
msg.Answer = in.Answer
rsp := Response{
Message: msg,
RTT: rtt,
Nameserver: srv,
}
responses = append(responses, rsp)
}
}
return responses, nil
}

93
pkg/resolvers/tcp.go Normal file
View file

@ -0,0 +1,93 @@
package resolvers
import (
"net"
"time"
"github.com/miekg/dns"
)
const (
// DefaultTLSPort specifies the default port for a DNS server connecting over TCP over TLS
DefaultTLSPort = "853"
)
// TCPResolver represents the config options for setting up a Resolver.
type TCPResolver struct {
client *dns.Client
servers []string
}
// TCPResolverOpts represents the config options for setting up a TCPResolver.
type TCPResolverOpts struct {
IPv4Only bool
IPv6Only bool
Timeout time.Duration
}
// NewTCPResolver accepts a list of nameservers and configures a DNS resolver.
func NewTCPResolver(servers []string, opts TCPResolverOpts) (Resolver, error) {
client := &dns.Client{
Timeout: opts.Timeout,
}
var nameservers []string
// load list of nameservers to the config
if len(servers) == 0 {
ns, err := getDefaultServers()
if err != nil {
return nil, err
}
nameservers = ns
} else {
// load the list of servers that user specified.
for _, srv := range servers {
if i := net.ParseIP(srv); i != nil {
// if no port specified in nameserver, append defaults.
nameservers = append(nameservers, net.JoinHostPort(srv, DefaultTLSPort))
} else {
// use the port user specified.
nameservers = append(nameservers, srv)
}
}
}
client.Net = "tcp"
if opts.IPv4Only {
client.Net = "tcp4"
}
if opts.IPv6Only {
client.Net = "tcp6"
}
return &TCPResolver{
client: client,
servers: nameservers,
}, nil
}
// Lookup prepare a list of DNS messages to be sent to the server.
// It's possible to send multiple question in one message
// but some nameservers are not able to
func (r *TCPResolver) Lookup(questions []dns.Question) ([]Response, error) {
var (
messages = prepareMessages(questions)
responses []Response
)
for _, msg := range messages {
for _, srv := range r.servers {
in, rtt, err := r.client.Exchange(&msg, srv)
if err != nil {
return nil, err
}
msg.Answer = in.Answer
rsp := Response{
Message: msg,
RTT: rtt,
Nameserver: srv,
}
responses = append(responses, rsp)
}
}
return responses, nil
}

View file

@ -2,6 +2,7 @@ package resolvers
import (
"net"
"time"
"github.com/miekg/dns"
)
@ -9,58 +10,58 @@ import (
const (
// DefaultUDPPort specifies the default port for a DNS server connecting over UDP
DefaultUDPPort = "53"
// DefaultTLSPort specifies the default port for a DNS server connecting over TCP over TLS
DefaultTLSPort = "853"
//DefaultResolvConfPath specifies path to default resolv config file on UNIX.
DefaultResolvConfPath = "/etc/resolv.conf"
)
// ClassicResolver represents the config options for setting up a Resolver.
type ClassicResolver struct {
// UDPResolver represents the config options for setting up a Resolver.
type UDPResolver struct {
client *dns.Client
servers []string
}
// ClassicResolverOpts holds options for setting up a Classic resolver.
type ClassicResolverOpts struct {
UseIPv4 bool
UseIPv6 bool
UseTCP bool
UseTLS bool
// UDPResolverOpts holds options for setting up a Classic resolver.
type UDPResolverOpts struct {
IPv4Only bool
IPv6Only bool
Timeout time.Duration
}
// NewClassicResolver accepts a list of nameservers and configures a DNS resolver.
func NewClassicResolver(servers []string, opts ClassicResolverOpts) (Resolver, error) {
client := &dns.Client{}
// NewUDPResolver accepts a list of nameservers and configures a DNS resolver.
func NewUDPResolver(servers []string, opts UDPResolverOpts) (Resolver, error) {
client := &dns.Client{
Timeout: opts.Timeout,
}
var nameservers []string
for _, srv := range servers {
if i := net.ParseIP(srv); i != nil {
// if no port specified in nameserver, append defaults.
if opts.UseTLS == true {
nameservers = append(nameservers, net.JoinHostPort(srv, DefaultTLSPort))
} else {
// load list of nameservers to the config
if len(servers) == 0 {
ns, err := getDefaultServers()
if err != nil {
return nil, err
}
nameservers = ns
} else {
// load the list of servers that user specified.
for _, srv := range servers {
if i := net.ParseIP(srv); i != nil {
// if no port specified in nameserver, append defaults.
nameservers = append(nameservers, net.JoinHostPort(srv, DefaultUDPPort))
} else {
// use the port user specified.
nameservers = append(nameservers, srv)
}
} else {
// use the port user specified.
nameservers = append(nameservers, srv)
}
}
client.Net = "udp"
if opts.UseIPv4 {
if opts.IPv4Only {
client.Net = "udp4"
}
if opts.UseIPv6 {
if opts.IPv6Only {
client.Net = "udp6"
}
if opts.UseTCP {
client.Net = "tcp"
}
if opts.UseTLS {
client.Net = "tcp-tls"
}
return &ClassicResolver{
return &UDPResolver{
client: client,
servers: nameservers,
}, nil
@ -69,7 +70,7 @@ func NewClassicResolver(servers []string, opts ClassicResolverOpts) (Resolver, e
// Lookup prepare a list of DNS messages to be sent to the server.
// It's possible to send multiple question in one message
// but some nameservers are not able to
func (c *ClassicResolver) Lookup(questions []dns.Question) ([]Response, error) {
func (c *UDPResolver) Lookup(questions []dns.Question) ([]Response, error) {
var (
messages = prepareMessages(questions)
responses []Response

View file

@ -1,6 +1,13 @@
package resolvers
import "github.com/miekg/dns"
import (
"errors"
"fmt"
"net"
"runtime"
"github.com/miekg/dns"
)
// prepareMessages takes a slice fo `dns.Question`
// and initialises `dns.Messages` for each question
@ -16,3 +23,26 @@ func prepareMessages(questions []dns.Question) []dns.Msg {
}
return messages
}
func getDefaultServers() ([]string, error) {
if runtime.GOOS == "windows" {
// TODO: Add a method for reading system default nameserver in windows.
return nil, errors.New(`unable to read default nameservers in this machine`)
}
// if no nameserver is provided, take it from `resolv.conf`
cfg, err := dns.ClientConfigFromFile(DefaultResolvConfPath)
if err != nil {
return nil, err
}
servers := make([]string, 0, len(cfg.Servers))
for _, s := range cfg.Servers {
ip := net.ParseIP(s)
// handle IPv6
if ip != nil && ip.To4() != nil {
servers = append(servers, fmt.Sprintf("%s:%s", s, cfg.Port))
} else {
servers = append(servers, fmt.Sprintf("[%s]:%s", s, cfg.Port))
}
}
return servers, nil
}