doggo/internal/app/nameservers.go
Maximilian Bosch 4c5a00ebc0
Support literal IPv6 nameservers
This effectively means that `doggo @2001:DB8::53 example.org` works just
like we know it from `dig(1)`. This already works for IPv4 because
`url.Parse` accepts a literal IPv4 address, but not a literal IPv6
address (due to parsing ambiguities with strings that look like an IPv4
address I guess).

This is implemented by checking if the nameserver argument is a valid IP
if the URL validation failed. If that's the case, `ns` is returned
directly with the `[2001:DB8::53]:53` as address (from
`net.JoinHostPort` with the default DNS port) and UDP as protocol (also
the default if not specified).
2022-12-13 12:35:44 +01:00

166 lines
4.4 KiB
Go

package app
import (
"fmt"
"math/rand"
"net"
"net/url"
"time"
"github.com/ameshkov/dnsstamps"
"github.com/mr-karan/doggo/pkg/config"
"github.com/mr-karan/doggo/pkg/models"
)
// LoadNameservers reads all the user given
// nameservers and loads to App.
func (app *App) LoadNameservers() error {
for _, srv := range app.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 != "" {
app.Nameservers = append(app.Nameservers, ns)
}
}
// Set `ndots` to the user specified value.
app.ResolverOpts.Ndots = app.QueryFlags.Ndots
// fallback to system nameserver
// in case no nameserver is specified by user.
if len(app.Nameservers) == 0 {
ns, ndots, search, err := getDefaultServers(app.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 app.ResolverOpts.Ndots == -1 {
app.ResolverOpts.Ndots = ndots
}
if len(search) > 0 && app.QueryFlags.UseSearchList {
app.ResolverOpts.SearchList = search
}
app.Nameservers = append(app.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 app.ResolverOpts.Ndots == -1 {
app.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 {
ip := net.ParseIP(n)
if ip == nil {
return ns, err
}
return ns, nil
}
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
}