cc24f28b9d
* feat(netxlite): support extracting the CNAME Closes https://github.com/ooni/probe/issues/2225 * fix(netxlite): attempt to increase coverage and improve tests 1. dnsovergetaddrinfo: specify the behavior of a DNSResponse returned by this file to make it line with normal responses and write unit tests to make sure we adhere to expectations; 2. dnsoverudp: make sure we wait to deferred responses also w/o a custom context and post on a private channel and test that; 3. utls: recognize that we can actually write a test for NetConn and what needs to change when we'll use go1.19 by default will just be a cast that at that point can be removed.
191 lines
6.7 KiB
Go
191 lines
6.7 KiB
Go
package netxlite
|
|
|
|
//
|
|
// DNS-over-UDP transport
|
|
//
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
)
|
|
|
|
// DNSOverUDPTransport is a DNS-over-UDP DNSTransport.
|
|
//
|
|
// To construct this type, either manually fill the fields marked as MANDATORY
|
|
// or just use the NewDNSOverUDPTransport factory directly.
|
|
//
|
|
// RoundTrip creates a new connected UDP socket for each outgoing query. Using a
|
|
// new socket is good because some censored environments will block the client UDP
|
|
// endpoint for several seconds when you query for blocked domains. We could also
|
|
// have used an unconnected UDP socket here, but:
|
|
//
|
|
// 1. connected UDP sockets are great because they get some ICMP errors to be
|
|
// translated into socket errors (among them, host_unreachable);
|
|
//
|
|
// 2. connected UDP sockets ignore responses from illegitimate IP addresses but
|
|
// most if not all DNS resolvers also do that, therefore this does not seem to
|
|
// be a realistic censorship vector. At the same time, connected sockets
|
|
// provide us for free with the feature that we don't need to bother with checking
|
|
// whether the reply comes from the expected server.
|
|
//
|
|
// Being able to observe some ICMP errors is good because it could possibly
|
|
// make this code suitable to implement parasitic traceroute.
|
|
//
|
|
// This transport by default listens for additional responses after the first
|
|
// one and makes them available using the context-configured trace.
|
|
type DNSOverUDPTransport struct {
|
|
// Decoder is the MANDATORY DNSDecoder to use.
|
|
Decoder model.DNSDecoder
|
|
|
|
// Dialer is the MANDATORY dialer used to create the conn.
|
|
Dialer model.Dialer
|
|
|
|
// Endpoint is the MANDATORY server's endpoint (e.g., 1.1.1.1:53)
|
|
Endpoint string
|
|
|
|
// lateResponses is posted in nonblocking mode each time this
|
|
// transport detects there was a late response for a query that had
|
|
// already been answered. Use this channel for testing.
|
|
lateResponses chan any
|
|
}
|
|
|
|
// NewUnwrappedDNSOverUDPTransport creates a DNSOverUDPTransport instance
|
|
// that has not been wrapped yet.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - dialer is any type that implements the Dialer interface;
|
|
//
|
|
// - address is the endpoint address (e.g., 8.8.8.8:53).
|
|
//
|
|
// If the address contains a domain name rather than an IP address
|
|
// (e.g., dns.google:53), we will end up using the first of the
|
|
// IP addresses returned by the underlying DNS lookup performed using
|
|
// the dialer. This usage pattern is NOT RECOMMENDED because we'll
|
|
// have less control over which IP address is being used.
|
|
func NewUnwrappedDNSOverUDPTransport(dialer model.Dialer, address string) *DNSOverUDPTransport {
|
|
return &DNSOverUDPTransport{
|
|
Decoder: &DNSDecoderMiekg{},
|
|
Dialer: dialer,
|
|
Endpoint: address,
|
|
lateResponses: nil, // not interested by default
|
|
}
|
|
}
|
|
|
|
// RoundTrip sends a query and receives a response.
|
|
func (t *DNSOverUDPTransport) RoundTrip(
|
|
ctx context.Context, query model.DNSQuery) (model.DNSResponse, error) {
|
|
// QUIRK: the original DNS-over-UDP code had a five seconds timeout, which is
|
|
// consistent with the Bionic implementation. Let's try to preserve such a
|
|
// behavior by combining dialing and I/O timeout together.
|
|
//
|
|
// See https://labs.ripe.net/Members/baptiste_jonglez_1/persistent-dns-connections-for-reliability-and-performance
|
|
const opTimeout = 5 * time.Second
|
|
deadline := time.Now().Add(opTimeout)
|
|
ctx, cancel := context.WithTimeout(ctx, opTimeout)
|
|
defer cancel()
|
|
rawQuery, err := query.Bytes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn, err := t.Dialer.DialContext(ctx, "udp", t.Endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn.SetDeadline(deadline) // time to dial (usually ~zero) already factored in
|
|
joinedch := make(chan bool)
|
|
myaddr := conn.LocalAddr().String()
|
|
if _, err := conn.Write(rawQuery); err != nil {
|
|
conn.Close() // we still own the conn
|
|
return nil, err
|
|
}
|
|
resp, err := t.recv(query, conn)
|
|
if err != nil {
|
|
conn.Close() // we still own the conn
|
|
return nil, err
|
|
}
|
|
// start a goroutine to listen for any delayed DNS response and
|
|
// TRANSFER the conn's OWNERSHIP to such a goroutine.
|
|
go t.ownConnAndSendRecvLoop(ctx, conn, query, myaddr, joinedch)
|
|
return resp, nil
|
|
}
|
|
|
|
// RequiresPadding returns false for UDP according to RFC8467.
|
|
func (t *DNSOverUDPTransport) RequiresPadding() bool {
|
|
return false
|
|
}
|
|
|
|
// Network returns the transport network, i.e., "udp".
|
|
func (t *DNSOverUDPTransport) Network() string {
|
|
return "udp"
|
|
}
|
|
|
|
// Address returns the upstream server address.
|
|
func (t *DNSOverUDPTransport) Address() string {
|
|
return t.Endpoint
|
|
}
|
|
|
|
// CloseIdleConnections closes idle connections, if any.
|
|
func (t *DNSOverUDPTransport) CloseIdleConnections() {
|
|
// The underlying dialer MAY have idle connections so let's
|
|
// forward the call...
|
|
t.Dialer.CloseIdleConnections()
|
|
}
|
|
|
|
var _ model.DNSTransport = &DNSOverUDPTransport{}
|
|
|
|
// ownConnAndSendRecvLoop listens for delayed DNS responses after we have returned the
|
|
// first response. As the name implies, this function TAKES OWNERSHIP of the [conn].
|
|
func (t *DNSOverUDPTransport) ownConnAndSendRecvLoop(ctx context.Context, conn net.Conn,
|
|
query model.DNSQuery, myaddr string, eofch chan<- bool) {
|
|
defer close(eofch) // synchronize with the caller
|
|
defer conn.Close() // we own the conn
|
|
trace := ContextTraceOrDefault(ctx)
|
|
for {
|
|
started := trace.TimeNow()
|
|
resp, err := t.recv(query, conn)
|
|
finished := trace.TimeNow()
|
|
if err != nil {
|
|
// We are going to consider all errors as fatal for now until we
|
|
// hear of specific errs that it might have sense to ignore.
|
|
//
|
|
// Note that erroring out here includes the expiration of the conn's
|
|
// I/O deadline, which we set above precisely because we want
|
|
// the total runtime of this goroutine to be bounded.
|
|
//
|
|
// Also, we ARE NOT going to report any failure here as a delayed
|
|
// DNS response because we only care about duplicate messages, since
|
|
// this seems how censorship is implemented in, e.g., China.
|
|
return
|
|
}
|
|
// if there's testing code waiting to be unblocked because we
|
|
// received a delayed response, unblock it
|
|
select {
|
|
case t.lateResponses <- true:
|
|
default:
|
|
// there's no one waiting and it does not matter
|
|
}
|
|
addrs, err := resp.DecodeLookupHost()
|
|
if err := trace.OnDelayedDNSResponse(started, t, query, resp, addrs, err, finished); err != nil {
|
|
// This error typically indicates that the buffer on which we're
|
|
// writing is now full, so there's no point in persisting.
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// recv receives a single response for the given query using the given conn.
|
|
func (t *DNSOverUDPTransport) recv(query model.DNSQuery, conn net.Conn) (model.DNSResponse, error) {
|
|
const maxmessagesize = 1 << 17
|
|
rawResponse := make([]byte, maxmessagesize)
|
|
count, err := conn.Read(rawResponse)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rawResponse = rawResponse[:count]
|
|
return t.Decoder.DecodeResponse(rawResponse, query)
|
|
}
|