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