feat(netxlite): support extracting the CNAME (#875)

* 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.
This commit is contained in:
Simone Basso 2022-08-23 13:04:00 +02:00 committed by GitHub
commit cc24f28b9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 390 additions and 39 deletions

View file

@ -22,10 +22,10 @@ import (
// endpoint for several seconds when you query for blocked domains. We could also
// have used an unconnected UDP socket here, but:
//
// 1. connected sockets are great because they get some ICMP errors to be
// 1. connected UDP sockets are great because they get some ICMP errors to be
// translated into socket errors (among them, host_unreachable);
//
// 2. connected sockets ignore responses from illegitimate IP addresses but
// 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
@ -34,8 +34,8 @@ import (
// Being able to observe some ICMP errors is good because it could possibly
// make this code suitable to implement parasitic traceroute.
//
// This transport is capable of collecting additional responses after the first
// response. To see these responses, use the AsyncRoundTrip method.
// 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
@ -45,6 +45,11 @@ type DNSOverUDPTransport struct {
// 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
@ -63,20 +68,23 @@ type DNSOverUDPTransport struct {
// 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,
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 code had a five seconds timeout, which is
// consistent with the Bionic implementation.
// 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()
@ -87,7 +95,7 @@ func (t *DNSOverUDPTransport) RoundTrip(
if err != nil {
return nil, err
}
conn.SetDeadline(time.Now().Add(opTimeout))
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 {
@ -153,6 +161,13 @@ func (t *DNSOverUDPTransport) ownConnAndSendRecvLoop(ctx context.Context, conn n
// 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