Documentation
¶
Overview ¶
Package dnscore provides a DNS resolver, a DNS transport, a query builder, and a DNS response parser.
This package is designed to facilitate DNS measurements and queries by providing both high-level and low-level APIs. It aims to be flexible, extensible, and easy to integrate with existing Go code.
The high-level *Resolver API provides a DNS resolver that is compatible with the *net.Resolver struct from the net package. The low-level *Transport API allows users to send and receive DNS messages using different protocols and dialers. The package also includes utilities for creating and validating DNS messages.
Features ¶
- High-level *Resolver API compatible with *net.Resolver for easy integration.
- Low-level *Transport API allowing granular control over DNS requests and responses.
- Support for multiple DNS protocols, including UDP, TCP, DoT, DoH, and DoQ.
- Utilities for creating and validating DNS messages.
- Optional logging for structured diagnostic events through log/slog.
- Handling of duplicate responses for DNS over UDP to measure censorship.
The package is structured to allow users to compose their own workflows by providing building blocks for DNS queries and responses. It uses the widely-used github.com/miekg/dns library for DNS message parsing and serialization.
Design Documents ¶
The dd-000-dnscore.md document describes the design of this package.
The df-000-dns.md document describes the data format generated by this package when using log/slog to emit structured diagnostic events.
Index ¶
- Constants
- Variables
- func DecodeLookupA(rrs []dns.RR) (addrs []string, cname string, err error)
- func DecodeLookupAAAA(rrs []dns.RR) (addrs []string, cname string, err error)
- func NewQuery(name string, qtype uint16, options ...QueryOption) (*dns.Msg, error)deprecated
- func NewQueryWithServerAddr(serverAddr *ServerAddr, name string, qtype uint16, options ...QueryOption) (*dns.Msg, error)
- func RCodeToError(resp *dns.Msg) error
- func ValidAnswers(q0 dns.Question, resp *dns.Msg) ([]dns.RR, error)
- func ValidateResponse(query, resp *dns.Msg) error
- type AddServerOption
- type MessageOrError
- type Protocol
- type QueryOption
- type Resolver
- type ResolverConfig
- type ResolverTransport
- type ServerAddr
- type Transport
Examples ¶
Constants ¶
const ( // EDNS0FlagDO enables DNSSEC by setting the DNSSSEC OK (DO) bit. EDNS0FlagDO = 1 << iota // EDNS0FlagBlockLengthPadding enables block-length padding as defined // by https://datatracker.ietf.org/doc/html/rfc8467#section-4.1. // // This helps protect against size-based traffic analysis by padding // DNS queries to a standard block size (128 bytes). // // This flag implies [QueryFlagEDNS0]. EDNS0FlagBlockLengthPadding )
const ( // ProtocolUDP is DNS over UDP. ProtocolUDP = Protocol("udp") // ProtocolTCP is DNS over TCP. ProtocolTCP = Protocol("tcp") // ProtocolDoT is DNS over TLS. ProtocolDoT = Protocol("dot") // ProtocolDoH is DNS over HTTPS. ProtocolDoH = Protocol("doh") // ProtocolDoQ is DNS over QUIC. ProtocolDoQ = Protocol("doq") )
All the implemented DNS protocols.
const ( // ProtocolTLS is an alias for ProtocolDoT. ProtocolTLS = ProtocolDoT // ProtocolHTTPS is an alias for ProtocolDoH. ProtocolHTTPS = ProtocolDoH // ProtocolQUIC is an alias for ProtocolDoQ. ProtocolQUIC = ProtocolDoQ )
Name aliases for DNS protocols.
const DefaultAttempts = 2
DefaultAttempts is the default number of attempts to make for each query.
const DefaultQueryTimeout = 5 * time.Second
DefaultQueryTimeout is the default timeout for each query.
const EDNS0SuggestedMaxResponseSizeOtherwise = 4096
END0SSuggestedMaxResponseSizeOtherwise is the suggested max-response size when not using the DNS over UDP transport.
const EDNS0SuggestedMaxResponseSizeUDP = 1232
EDNS0SuggestedMaxResponseSizeUDP is the suggested max-response size to use for the DNS over UDP transport. This value is same as the one used by the net package in the standard library.
Variables ¶
var ( // ErrCannotUnmarshalMessage indicates that we cannot unmarshal a DNS message. ErrCannotUnmarshalMessage = errors.New("cannot unmarshal DNS message") // ErrInvalidResponse means that the response is not a response message // or does not contain a single question matching the query. ErrInvalidResponse = errors.New("invalid DNS response") // ErrNoName indicates that the server response code is NXDOMAIN. ErrNoName = errors.New("no such host") // ErrServerMisbehaving indicates that the server response code is // neither 0, nor NXDOMAIN, nor SERVFAIL. ErrServerMisbehaving = errors.New("server misbehaving") // ErrServerTemporarilyMisbehaving indicates that the server answer is SERVFAIL. // // The error message is same as [ErrServerMisbehaving] for compatibility with the // Go standard library, which assigns the same error string to both errors. ErrServerTemporarilyMisbehaving = errors.New("server misbehaving") // ErrNoData indicates that there is no pertinent answer in the response. ErrNoData = errors.New("no answer from DNS server") )
These error messages use the same suffixes used by the Go standard library.
var DefaultTransport = &Transport{}
DefaultTransport is the default transport used by the package.
var ( // ErrInvalidQuery means that the query does not contain a single question. ErrInvalidQuery = errors.New("invalid query") )
Additional errors emitted by ValidateResponse.
var ErrNoSuchTransportProtocol = errors.New("no such transport protocol")
ErrNoSuchTransportProtocol is returned when the given protocol is not supported.
var ErrQueryTooLargeForTransport = errors.New("query too large for transport")
ErrQueryTooLargeForTransport indicates that a query is too large for the transport.
var ErrTransportCannotReceiveDuplicates = errors.New("transport cannot receive duplicates")
ErrTransportCannotReceiveDuplicates is returned when the transport cannot receive duplicates.
Functions ¶
func DecodeLookupA ¶
DecodeLookupA decodes RRs from a lookup A response.
func DecodeLookupAAAA ¶
DecodeLookupAAAA decodes RRs from a lookup AAAA response.
func NewQuery
deprecated
NewQuery is equivalent to calling NewQueryWithServerAddr with a zero-initialized *ServerAddr. We retain this function for backward compatibility with the previous API. Existing code that is using this function SHOULD use NewQueryWithServerAddr with DoH (and MUST with DoQ) such that we correctly set the query ID to zero. Other protocols are not impacted by this issue and may continue using NewQuery.
Deprecated: use NewQueryWithServerAddr instead.
func NewQueryWithServerAddr ¶ added in v0.11.0
func NewQueryWithServerAddr(serverAddr *ServerAddr, name string, qtype uint16, options ...QueryOption) (*dns.Msg, error)
NewQueryWithServerAddr constructs a *dns.Message containing a query for the given domain, query type and *ServerAddr. We use the *ServerAddr to enforce protocol-specific query settings, such as, that DoH SHOULD use a zero query ID.
This function takes care of IDNA encoding the domain name and fails if the domain name is invalid.
Additionally, NewQuery ensures the given name is fully qualified.
Use constants such as dns.TypeAAAA to specify the query type.
The QueryOption functions can be used to set additional options.
func RCodeToError ¶
RCodeToError maps an RCODE inside a valid DNS response to an error string using a suffix compatible with the error strings returned by *net.Resolver.
For example, if a domain does not exist, the error will use the "no such host" suffix.
If the RCODE is zero, this function returns nil.
Before invoking this function, make sure the response is valid for the request by calling ValidateResponse.
func ValidAnswers ¶
ValidAnswers extracts valid RRs from the response considering the DNS question that was asked. Before invoking this function, make sure the response is valid using ValidateResponse.
The list of valid RRs is returned in the same order as they appear in the response message. If the response does not contain any valid RRs, this function returns an empty list.
func ValidateResponse ¶
ValidateResponse validates a given DNS response message for a given query message.
Types ¶
type AddServerOption ¶
type AddServerOption func(*resolverConfigServer)
AddServerOption is an option for adding a server to the resolver configuration.
func ServerOptionQueryOptions ¶
func ServerOptionQueryOptions(queryOptions ...QueryOption) AddServerOption
ServerOptionQueryOptions sets the query options to use for constructing queries to this specific server.If this option is not used, we use the default query options suitable for the protocol used by the server. Specifically, we enable DNSSEC validation and block-length padding for DoT, DoH, and DoQ.
func ServerOptionQueryTimeout ¶
func ServerOptionQueryTimeout(timeout time.Duration) AddServerOption
ServerOptionQueryTimeout sets the timeout for each query.
If this option is not used, we use the DefaultQueryTimeout default.
type MessageOrError ¶
MessageOrError contains either a DNS message or an error.
type QueryOption ¶
QueryOption is a function that modifies a DNS query.
func QueryOptionEDNS0 ¶
func QueryOptionEDNS0(maxResponseSize uint16, flags int) QueryOption
QueryOptionEDNS0 configures the EDNS(0) options.
You can configure:
1. The maximum acceptable response size.
2. DNSSEC using EDNS0FlagDO.
3. Block-length padding using EDNS0FlagBlockLengthPadding.
func QueryOptionID ¶ added in v0.11.0
func QueryOptionID(id uint16) QueryOption
QueryOptionID allows setting an arbitrary query ID.
Otherwise, the default is using dns.Id for all protocols except DNS-over-HTTPS and DNS-over-QUIC, where we use zero, thus following RFC 9250 Sect 4.2.1.
type Resolver ¶
type Resolver struct { // Config is the optional resolver configuration. // // If nil, we use an empty [*ResolverConfig]. Config *ResolverConfig // Transport is the optional DNS transport to use for resolving queries. // // If nil, we use [DefaultTransport]. Transport ResolverTransport }
Resolver is a DNS resolver. This struct is API compatible with the *net.Resolver struct from the net package.
The zero value is ready to use.
Example ¶
package main import ( "context" "fmt" "log" "slices" "strings" "github.com/rbmk-project/dnscore" ) func main() { // create resolver reso := &dnscore.Resolver{} // issue the queries and merge the responses addrs, err := reso.LookupHost(context.Background(), "dns.google") if err != nil { log.Fatal(err) } // print the results slices.Sort(addrs) fmt.Printf("%s\n", strings.Join(addrs, "\n")) }
Output: 2001:4860:4860::8844 2001:4860:4860::8888 8.8.4.4 8.8.8.8
func (*Resolver) LookupAAAA ¶
LookupAAAA resolves the IPv6 addresses of a given domain.
type ResolverConfig ¶
type ResolverConfig struct {
// contains filtered or unexported fields
}
ResolverConfig contains configuration for the resolver.
Construct using NewConfig.
This struct is safe for concurrent use by multiple goroutines.
If the configuration is empty, it uses the "8.8.8.8:53/udp" and "8.8.4.4:53/udp" servers as the default servers.
func (*ResolverConfig) AddServer ¶
func (c *ResolverConfig) AddServer(address *ServerAddr, options ...AddServerOption)
AddServer adds a new server to the resolver configuration.
func (*ResolverConfig) Attempts ¶
func (c *ResolverConfig) Attempts() int
Attempts returns the number of attempts to make for each query.
func (*ResolverConfig) SetAttempts ¶
func (c *ResolverConfig) SetAttempts(attempts int)
SetAttempts sets the number of attempts to make for each query.
type ResolverTransport ¶
type ResolverTransport interface {
Query(ctx context.Context, addr *ServerAddr, query *dns.Msg) (*dns.Msg, error)
}
ResolverTransport is the interface defining the *Transport methods used by the *Resolver struct.
The *Transport type implements this interface.
type ServerAddr ¶
type ServerAddr struct { // Protocol is the transport protocol to use. // // Use one of: // // - [ProtocolUDP] // - [ProtocolTCP] // - [ProtocolDoT] // - [ProtocolDoH] // - [ProtocolDoQ] Protocol Protocol // Address is the network address of the server. // // For [ProtocolUDP], [ProtocolTCP], and [ProtocolDoT] this is // a string in the form returned by [net.JoinHostPort]. // // For [ProtocolDoH] this is a URL. Address string }
ServerAddr is a DNS server address.
While currently minimal, ServerAddr is designed as a pointer type to allow for future extensions of server-specific properties (e.g., custom headers for DoH) without requiring breaking API changes.
Construct using NewServerAddr.
func NewServerAddr ¶
func NewServerAddr(protocol Protocol, address string) *ServerAddr
NewServerAddr constructs a new *ServerAddr with the given protocol and address.
type Transport ¶
type Transport struct { // DialContext is the optional dialer for creating new // TCP and UDP connections. If this field is nil, the default // dialer from the [net] package will be used. DialContext func(ctx context.Context, network, address string) (net.Conn, error) // DialTLSContext is like DialContext but for creating new // TLS connections. If this field is nil, we will configure // a suitable [*tls.Config] and use [*tls.Dialer]. DialTLSContext func(ctx context.Context, network, address string) (net.Conn, error) // HTTPClient is the optional HTTP client to use for DNS-over-HTTPS. // If this field is nil, we use the default HTTP client from [net/http]. // // When HTTPClientDo is nil and this field is not nil, we use this client to // perform queries and http/httptrace to obtain connection information. HTTPClient *http.Client // HTTPClientDo optionally allows full control over how HTTP requests // are performed and how to obtain connection information. When this // field is non-nil, it takes precedence over HTTPClient. // // This field is mainly useful for measurement scenarios where you need // precise control over connection handling and addressing information. HTTPClientDo func(req *http.Request) (*http.Response, netip.AddrPort, netip.AddrPort, error) // Logger is the optional structured logger for emitting // structured diagnostic events. If this field is nil, we // will not be emitting structured logs. Logger *slog.Logger // NewHTTPRequestWithContext is an optional function that creates a new // HTTP request with the given context. If this field is nil, the // [http.NewRequestWithContext] function will be used. NewHTTPRequestWithContext func(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) // ReadAllContext is the optional function to read the whole HTTP response // body in DNS-over-HTTPS. If this field is nil, we use the [io.ReadAll] function // instead. Compared to [io.ReadAll], this function has a context argument // and an [io.Closer] argument, which SHOULD be used to close the connection // when the context is cancelled. In general, this is not useful, but in censored // places censorship may desync the TCP connection, making context-based // interruption useful to avoid being blocked ~forever. ReadAllContext func(ctx context.Context, r io.Reader, c io.Closer) ([]byte, error) // RootCAs contains the [*x509.CertPool] used by DNS-over-TLS // when the DialTLSContext function pointer is nil. Leaving this // field nil implies using the system's root CAs. RootCAs *x509.CertPool // TimeNow is an optional function that returns the current time. // If this field is nil, the [time.Now] function will be used. TimeNow func() time.Time }
Transport allows sending and receiving DNS messages.
The zero value is ready to use.
A *Transport is safe for concurrent use by multiple goroutines as long as you don't modify its fields after construction and the underlying fields you may set (e.g., DialContext) are also safe.
Example (DnsOverHTTPS) ¶
package main import ( "context" "fmt" "log" "slices" "strings" "time" "github.com/miekg/dns" "github.com/rbmk-project/common/runtimex" "github.com/rbmk-project/dnscore" ) func main() { // create transport, server addr, and query txp := &dnscore.Transport{} serverAddr := &dnscore.ServerAddr{ Protocol: dnscore.ProtocolDoH, Address: "https://8.8.8.8/dns-query", } options := []dnscore.QueryOption{ dnscore.QueryOptionEDNS0( dnscore.EDNS0SuggestedMaxResponseSizeOtherwise, dnscore.EDNS0FlagDO|dnscore.EDNS0FlagBlockLengthPadding, ), } query, err := dnscore.NewQueryWithServerAddr(serverAddr, "dns.google", dns.TypeA, options...) if err != nil { log.Fatal(err) } // issue the query and get the response ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := txp.Query(ctx, serverAddr, query) if err != nil { log.Fatal(err) } // validate the response if err := dnscore.ValidateResponse(query, resp); err != nil { log.Fatal(err) } runtimex.Assert(len(query.Question) > 0, "expected at least one question") rrs, err := dnscore.ValidAnswers(query.Question[0], resp) if err != nil { log.Fatal(err) } // print the results var addrs []string for _, rr := range rrs { switch rr := rr.(type) { case *dns.A: addrs = append(addrs, rr.A.String()) } } slices.Sort(addrs) fmt.Printf("%s\n", strings.Join(addrs, "\n")) }
Output: 8.8.4.4 8.8.8.8
Example (DnsOverQUIC) ¶
package main import ( "context" "fmt" "log" "slices" "strings" "time" "github.com/miekg/dns" "github.com/rbmk-project/common/runtimex" "github.com/rbmk-project/dnscore" ) func main() { // create transport, server addr, and query txp := &dnscore.Transport{} serverAddr := &dnscore.ServerAddr{ Protocol: dnscore.ProtocolDoQ, Address: "dns0.eu:853", } options := []dnscore.QueryOption{ dnscore.QueryOptionEDNS0( dnscore.EDNS0SuggestedMaxResponseSizeOtherwise, dnscore.EDNS0FlagDO|dnscore.EDNS0FlagBlockLengthPadding, ), } query, err := dnscore.NewQueryWithServerAddr(serverAddr, "dns.google", dns.TypeA, options...) if err != nil { log.Fatal(err) } // issue the query and get the response ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := txp.Query(ctx, serverAddr, query) if err != nil { log.Fatal(err) } // validate the response if err := dnscore.ValidateResponse(query, resp); err != nil { log.Fatal(err) } runtimex.Assert(len(query.Question) > 0, "expected at least one question") rrs, err := dnscore.ValidAnswers(query.Question[0], resp) if err != nil { log.Fatal(err) } // print the results var addrs []string for _, rr := range rrs { switch rr := rr.(type) { case *dns.A: addrs = append(addrs, rr.A.String()) } } slices.Sort(addrs) fmt.Printf("%s\n", strings.Join(addrs, "\n")) }
Output: 8.8.4.4 8.8.8.8
Example (DnsOverTCP) ¶
package main import ( "context" "fmt" "log" "slices" "strings" "time" "github.com/miekg/dns" "github.com/rbmk-project/common/runtimex" "github.com/rbmk-project/dnscore" ) func main() { // create transport, server addr, and query txp := &dnscore.Transport{} serverAddr := &dnscore.ServerAddr{ Protocol: dnscore.ProtocolTCP, Address: "8.8.8.8:53", } options := []dnscore.QueryOption{ dnscore.QueryOptionEDNS0( dnscore.EDNS0SuggestedMaxResponseSizeOtherwise, 0, ), } query, err := dnscore.NewQueryWithServerAddr(serverAddr, "dns.google", dns.TypeA, options...) if err != nil { log.Fatal(err) } // issue the query and get the response ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := txp.Query(ctx, serverAddr, query) if err != nil { log.Fatal(err) } // validate the response if err := dnscore.ValidateResponse(query, resp); err != nil { log.Fatal(err) } runtimex.Assert(len(query.Question) > 0, "expected at least one question") rrs, err := dnscore.ValidAnswers(query.Question[0], resp) if err != nil { log.Fatal(err) } // print the results var addrs []string for _, rr := range rrs { switch rr := rr.(type) { case *dns.A: addrs = append(addrs, rr.A.String()) } } slices.Sort(addrs) fmt.Printf("%s\n", strings.Join(addrs, "\n")) }
Output: 8.8.4.4 8.8.8.8
Example (DnsOverTLS) ¶
package main import ( "context" "fmt" "log" "slices" "strings" "time" "github.com/miekg/dns" "github.com/rbmk-project/common/runtimex" "github.com/rbmk-project/dnscore" ) func main() { // create transport, server addr, and query txp := &dnscore.Transport{} serverAddr := &dnscore.ServerAddr{ Protocol: dnscore.ProtocolDoT, Address: "8.8.8.8:853", } options := []dnscore.QueryOption{ dnscore.QueryOptionEDNS0( dnscore.EDNS0SuggestedMaxResponseSizeOtherwise, dnscore.EDNS0FlagDO|dnscore.EDNS0FlagBlockLengthPadding, ), } query, err := dnscore.NewQueryWithServerAddr(serverAddr, "dns.google", dns.TypeA, options...) if err != nil { log.Fatal(err) } // issue the query and get the response ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := txp.Query(ctx, serverAddr, query) if err != nil { log.Fatal(err) } // validate the response if err := dnscore.ValidateResponse(query, resp); err != nil { log.Fatal(err) } runtimex.Assert(len(query.Question) > 0, "expected at least one question") rrs, err := dnscore.ValidAnswers(query.Question[0], resp) if err != nil { log.Fatal(err) } // print the results var addrs []string for _, rr := range rrs { switch rr := rr.(type) { case *dns.A: addrs = append(addrs, rr.A.String()) } } slices.Sort(addrs) fmt.Printf("%s\n", strings.Join(addrs, "\n")) }
Output: 8.8.4.4 8.8.8.8
Example (DnsOverUDP) ¶
package main import ( "context" "fmt" "log" "slices" "strings" "time" "github.com/miekg/dns" "github.com/rbmk-project/common/runtimex" "github.com/rbmk-project/dnscore" ) func main() { // create transport, server addr, and query txp := &dnscore.Transport{} serverAddr := &dnscore.ServerAddr{ Protocol: dnscore.ProtocolUDP, Address: "8.8.8.8:53", } options := []dnscore.QueryOption{ dnscore.QueryOptionEDNS0( dnscore.EDNS0SuggestedMaxResponseSizeUDP, 0, ), } query, err := dnscore.NewQueryWithServerAddr(serverAddr, "dns.google", dns.TypeA, options...) if err != nil { log.Fatal(err) } // issue the query and get the response ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := txp.Query(ctx, serverAddr, query) if err != nil { log.Fatal(err) } // validate the response if err := dnscore.ValidateResponse(query, resp); err != nil { log.Fatal(err) } runtimex.Assert(len(query.Question) > 0, "expected at least one question") rrs, err := dnscore.ValidAnswers(query.Question[0], resp) if err != nil { log.Fatal(err) } // print the results var addrs []string for _, rr := range rrs { switch rr := rr.(type) { case *dns.A: addrs = append(addrs, rr.A.String()) } } slices.Sort(addrs) fmt.Printf("%s\n", strings.Join(addrs, "\n")) }
Output: 8.8.4.4 8.8.8.8
func (*Transport) Query ¶
Query sends a DNS query to the given server address and returns the response.
The context is used to control the query lifetime. If the context is cancelled or times out, the query will be aborted and an error will be immediately returned to the caller.
The returned DNS message is the first message received from the server and it is not guaranteed to be valid for the query. You will still need to validate the response using the ValidateResponse function.
func (*Transport) QueryWithDuplicates ¶
func (t *Transport) QueryWithDuplicates(ctx context.Context, addr *ServerAddr, query *dns.Msg) <-chan *MessageOrError
QueryWithDuplicates sends a DNS query to the given server address and returns the received responses. Use this method when you expect duplicate responses possibly caused by censorship. For example, the GFW (Great Firewall of China) typically causes duplicate responses with different addresses when a given domain is censored.
This method only works with ProtocolUDP.
As for *Transport.Query, the context is used to control the query lifetime. If the context is cancelled or times out, the query will be aborted and the returned channel will be then closed.
The returned DNS messages are the responses received from the server and they are not guaranteed to be valid for the query. You will still need to validate the responses using the ValidateResponse function.
Source Files
¶
Directories
¶
Path | Synopsis |
---|---|
Package dnscoretest contains fake servers to test dnscore.
|
Package dnscoretest contains fake servers to test dnscore. |
internal
|
|
cmd/lookup
Command lookup shows how to use the resolver to perform a DNS lookup.
|
Command lookup shows how to use the resolver to perform a DNS lookup. |
cmd/mkcert
Command mkcert generates a self-signed certificate for testing purposes.
|
Command mkcert generates a self-signed certificate for testing purposes. |
cmd/transport
Command transport shows how to use the transport to perform a DNS lookup.
|
Command transport shows how to use the transport to perform a DNS lookup. |