dnscore

package module
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 9, 2025 License: GPL-3.0 Imports: 23 Imported by: 3

README

dnscore - DNS Measurement Library

GoDoc Build Status codecov

dnscore is a Go library designed for performing DNS measurements. Its high-level API, *dnscore.Resolver, is compatible with *net.Resolver. Its low-level API, *dnscore.Transport, provides granular control over performing DNS queries using specific protocols (including UDP, TCP, TLS, HTTPS, and QUIC).

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 miekg/dns library for DNS message parsing and serialization.

Minimum Required Go Version

We maintain compatibility with the oldest supported version of Go as documented by the Go Release Policy and update our minimum required version of Go as needed.

Installation

go get github.com/rbmk-project/dnscore

Usage

High-Level API

The *dnscore.Resolver API is compatible with *net.Resolver.

See example_resolver_test.go for a complete example.

See also internal/cmd/lookup/main.go for a simple command line tool that demonstrates how to use the *dnscore.Resolver API along with log/slog to emit structured logs.

Low-Level Transport

The *dnscore.Transport API provides granular control over DNS queries and responses.

See

for complete examples using DNS over HTTPS, QUIC, TCP, TLS, and UDP respectively.

See also internal/cmd/transport/main.go for a simple command line tool that demonstrates how to use the *dnscore.Transport API along with log/slog to emit structured logs.

Design

See DESIGN.md for an overview of the design.

Contributing

Contributions are welcome! Please submit a pull requests using GitHub. Use rbmk-project/issues to create issues and discuss features related to this package.

License

SPDX-License-Identifier: GPL-3.0-or-later

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

Examples

Constants

View Source
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
)
View Source
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.

View Source
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.

View Source
const DefaultAttempts = 2

DefaultAttempts is the default number of attempts to make for each query.

View Source
const DefaultQueryTimeout = 5 * time.Second

DefaultQueryTimeout is the default timeout for each query.

View Source
const EDNS0SuggestedMaxResponseSizeOtherwise = 4096

END0SSuggestedMaxResponseSizeOtherwise is the suggested max-response size when not using the DNS over UDP transport.

View Source
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

View Source
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.

View Source
var DefaultTransport = &Transport{}

DefaultTransport is the default transport used by the package.

View Source
var (
	// ErrInvalidQuery means that the query does not contain a single question.
	ErrInvalidQuery = errors.New("invalid query")
)

Additional errors emitted by ValidateResponse.

View Source
var ErrNoSuchTransportProtocol = errors.New("no such transport protocol")

ErrNoSuchTransportProtocol is returned when the given protocol is not supported.

View Source
var ErrQueryTooLargeForTransport = errors.New("query too large for transport")

ErrQueryTooLargeForTransport indicates that a query is too large for the transport.

View Source
var ErrTransportCannotReceiveDuplicates = errors.New("transport cannot receive duplicates")

ErrTransportCannotReceiveDuplicates is returned when the transport cannot receive duplicates.

Functions

func DecodeLookupA

func DecodeLookupA(rrs []dns.RR) (addrs []string, cname string, err error)

DecodeLookupA decodes RRs from a lookup A response.

func DecodeLookupAAAA

func DecodeLookupAAAA(rrs []dns.RR) (addrs []string, cname string, err error)

DecodeLookupAAAA decodes RRs from a lookup AAAA response.

func NewQuery deprecated

func NewQuery(name string, qtype uint16, options ...QueryOption) (*dns.Msg, error)

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

func RCodeToError(resp *dns.Msg) error

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

func ValidAnswers(q0 dns.Question, resp *dns.Msg) ([]dns.RR, error)

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

func ValidateResponse(query, resp *dns.Msg) error

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

type MessageOrError struct {
	Err error
	Msg *dns.Msg
}

MessageOrError contains either a DNS message or an error.

type Protocol

type Protocol string

Protocol is a transport protocol.

type QueryOption

type QueryOption func(*dns.Msg) error

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) LookupA

func (r *Resolver) LookupA(ctx context.Context, host string) ([]string, error)

LookupA resolves the IPv4 addresses of a given domain.

func (*Resolver) LookupAAAA

func (r *Resolver) LookupAAAA(ctx context.Context, host string) ([]string, error)

LookupAAAA resolves the IPv6 addresses of a given domain.

func (*Resolver) LookupHost

func (r *Resolver) LookupHost(ctx context.Context, host string) ([]string, error)

LookupHost looks up the given host named using the DNS resolver.

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 NewConfig

func NewConfig() *ResolverConfig

NewConfig creates a new resolver configuration.

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

func (t *Transport) Query(ctx context.Context,
	addr *ServerAddr, query *dns.Msg) (*dns.Msg, error)

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.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL