mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
So we don't have to worry about network stack changes causes an upstream to be broken. Just send requests to all nameservers concurrently, and get the first success response.
117 lines
2.8 KiB
Go
117 lines
2.8 KiB
Go
package ctrld
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
const (
|
|
ResolverTypeDOH = "doh"
|
|
ResolverTypeDOH3 = "doh3"
|
|
ResolverTypeDOT = "dot"
|
|
ResolverTypeDOQ = "doq"
|
|
ResolverTypeOS = "os"
|
|
ResolverTypeLegacy = "legacy"
|
|
)
|
|
|
|
var bootstrapDNS = "76.76.2.0"
|
|
var or = &osResolver{nameservers: nameservers()}
|
|
|
|
// Resolver is the interface that wraps the basic DNS operations.
|
|
//
|
|
// Resolve resolves the DNS query, return the result and the corresponding error.
|
|
type Resolver interface {
|
|
Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error)
|
|
}
|
|
|
|
var errUnknownResolver = errors.New("unknown resolver")
|
|
|
|
// NewResolver creates a Resolver based on the given upstream config.
|
|
func NewResolver(uc *UpstreamConfig) (Resolver, error) {
|
|
typ, endpoint := uc.Type, uc.Endpoint
|
|
switch typ {
|
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
|
return newDohResolver(uc), nil
|
|
case ResolverTypeDOT:
|
|
return &dotResolver{uc: uc}, nil
|
|
case ResolverTypeDOQ:
|
|
return &doqResolver{uc: uc}, nil
|
|
case ResolverTypeOS:
|
|
return or, nil
|
|
case ResolverTypeLegacy:
|
|
return &legacyResolver{endpoint: endpoint}, nil
|
|
}
|
|
return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ)
|
|
}
|
|
|
|
type osResolver struct {
|
|
nameservers []string
|
|
}
|
|
|
|
type osResolverResult struct {
|
|
answer *dns.Msg
|
|
err error
|
|
}
|
|
|
|
// Resolve performs DNS resolvers using OS default nameservers. Nameserver is chosen from
|
|
// available nameservers with a roundrobin algorithm.
|
|
func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
|
numServers := len(o.nameservers)
|
|
if numServers == 0 {
|
|
return nil, errors.New("no nameservers available")
|
|
}
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
dnsClient := &dns.Client{Net: "udp"}
|
|
ch := make(chan *osResolverResult, numServers)
|
|
for _, server := range o.nameservers {
|
|
go func(server string) {
|
|
answer, _, err := dnsClient.ExchangeContext(ctx, msg, server)
|
|
ch <- &osResolverResult{answer: answer, err: err}
|
|
}(server)
|
|
}
|
|
|
|
errs := make([]error, 0, numServers)
|
|
for res := range ch {
|
|
if res.err == nil {
|
|
cancel()
|
|
return res.answer, res.err
|
|
}
|
|
errs = append(errs, res.err)
|
|
}
|
|
|
|
return nil, joinErrors(errs...)
|
|
}
|
|
|
|
func newDialer(dnsAddress string) *net.Dialer {
|
|
return &net.Dialer{
|
|
Resolver: &net.Resolver{
|
|
PreferGo: true,
|
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
d := net.Dialer{}
|
|
return d.DialContext(ctx, network, dnsAddress)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
type legacyResolver struct {
|
|
endpoint string
|
|
}
|
|
|
|
func (r *legacyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
|
// See comment in (*dotResolver).resolve method.
|
|
dialer := newDialer(net.JoinHostPort(bootstrapDNS, "53"))
|
|
dnsClient := &dns.Client{
|
|
Net: "udp",
|
|
Dialer: dialer,
|
|
}
|
|
answer, _, err := dnsClient.ExchangeContext(ctx, msg, r.endpoint)
|
|
return answer, err
|
|
}
|