b78b9aca51
This diff changes the data format to prefer "udp" to "quic" everywhere we were previously using "quic". Previously, the code inconsistently used "quic" for operations where we knew we were using "quic" and "udp" otherwise (e.g., for generic operations like ReadFrom). While it would be more correct to say that a specific HTTP request used "quic" rather than "udp", using "udp" consistently allows one to see how distinct events such as ReadFrom and an handshake all refer to the same address, port, and protocol triple. Therefore, this change makes it easier to programmatically unpack a single measurement and create endpoint stats. Before implementing this change, I discussed the problem with @hellais who mentioned that ooni/data is not currently using the "quic" string anywhere. I know that ooni/pipeline also doesn't rely on this string. The only users of this feature have been research-oriented experiments such as urlgetter, for which such a change would actually be acceptable. See https://github.com/ooni/probe/issues/2238 and https://github.com/ooni/spec/pull/262.
483 lines
16 KiB
Go
483 lines
16 KiB
Go
package model
|
|
|
|
//
|
|
// Network extensions
|
|
//
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net"
|
|
"net/http"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/lucas-clemente/quic-go"
|
|
)
|
|
|
|
// DNSResponse is a parsed DNS response ready for further processing.
|
|
type DNSResponse interface {
|
|
// Query is the query associated with this response.
|
|
Query() DNSQuery
|
|
|
|
// Bytes returns the bytes from which we parsed the query.
|
|
Bytes() []byte
|
|
|
|
// Rcode returns the response's Rcode.
|
|
Rcode() int
|
|
|
|
// DecodeHTTPS returns information gathered from all the HTTPS
|
|
// records found inside of this response.
|
|
DecodeHTTPS() (*HTTPSSvc, error)
|
|
|
|
// DecodeLookupHost returns the addresses in the response matching
|
|
// the original query type (one of A and AAAA).
|
|
DecodeLookupHost() ([]string, error)
|
|
|
|
// DecodeNS returns all the NS entries in this response.
|
|
DecodeNS() ([]*net.NS, error)
|
|
|
|
// DecodeCNAME returns the first CNAME entry in this response.
|
|
DecodeCNAME() (string, error)
|
|
}
|
|
|
|
// The DNSDecoder decodes DNS responses.
|
|
type DNSDecoder interface {
|
|
// DecodeResponse decodes a DNS response message.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - data is the raw reply
|
|
//
|
|
// This function fails if we cannot parse data as a DNS
|
|
// message or the message is not a response.
|
|
//
|
|
// Regarding the returned response, remember that the Rcode
|
|
// MAY still be nonzero (this method does not treat a nonzero
|
|
// Rcode as an error when parsing the response).
|
|
DecodeResponse(data []byte, query DNSQuery) (DNSResponse, error)
|
|
}
|
|
|
|
// DNSQuery is an encoded DNS query ready to be sent using a DNSTransport.
|
|
type DNSQuery interface {
|
|
// Domain is the domain we're querying for.
|
|
Domain() string
|
|
|
|
// Type is the query type.
|
|
Type() uint16
|
|
|
|
// Bytes serializes the query to bytes. This function may fail if we're not
|
|
// able to correctly encode the domain into a query message.
|
|
//
|
|
// The value returned by this function WILL be memoized after the first call,
|
|
// so you SHOULD create a new DNSQuery if you need to retry a query.
|
|
Bytes() ([]byte, error)
|
|
|
|
// ID returns the query ID.
|
|
ID() uint16
|
|
}
|
|
|
|
// The DNSEncoder encodes DNS queries to bytes
|
|
type DNSEncoder interface {
|
|
// Encode transforms its arguments into a serialized DNS query.
|
|
//
|
|
// Every time you call Encode, you get a new DNSQuery value
|
|
// using a query ID selected at random.
|
|
//
|
|
// Serialization to bytes is lazy to acommodate DNS transports that
|
|
// do not need to serialize and send bytes, e.g., getaddrinfo.
|
|
//
|
|
// You serialize to bytes using DNSQuery.Bytes. This operation MAY fail
|
|
// if the domain name cannot be packed into a DNS message (e.g., it is
|
|
// too long to fit into the message).
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - domain is the domain for the query (e.g., x.org);
|
|
//
|
|
// - qtype is the query type (e.g., dns.TypeA);
|
|
//
|
|
// - padding is whether to add padding to the query.
|
|
//
|
|
// This function will transform the domain into an FQDN is it's not
|
|
// already expressed in the FQDN format.
|
|
Encode(domain string, qtype uint16, padding bool) DNSQuery
|
|
}
|
|
|
|
// DNSTransportWrapper is a type that takes in input a DNSTransport
|
|
// and returns in output a wrapped DNSTransport.
|
|
type DNSTransportWrapper interface {
|
|
WrapDNSTransport(txp DNSTransport) DNSTransport
|
|
}
|
|
|
|
// DNSTransport represents an abstract DNS transport.
|
|
type DNSTransport interface {
|
|
// RoundTrip sends a DNS query and receives the reply.
|
|
RoundTrip(ctx context.Context, query DNSQuery) (DNSResponse, error)
|
|
|
|
// RequiresPadding returns whether this transport needs padding.
|
|
RequiresPadding() bool
|
|
|
|
// Network is the network of the round tripper (e.g. "dot").
|
|
Network() string
|
|
|
|
// Address is the address of the round tripper (e.g. "1.1.1.1:853").
|
|
Address() string
|
|
|
|
// CloseIdleConnections closes idle connections, if any.
|
|
CloseIdleConnections()
|
|
}
|
|
|
|
// DialerWrapper is a type that takes in input a Dialer
|
|
// and returns in output a wrapped Dialer.
|
|
type DialerWrapper interface {
|
|
WrapDialer(d Dialer) Dialer
|
|
}
|
|
|
|
// SimpleDialer establishes network connections.
|
|
type SimpleDialer interface {
|
|
// DialContext behaves like net.Dialer.DialContext.
|
|
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
|
}
|
|
|
|
// Dialer is a SimpleDialer with the possibility of closing open connections.
|
|
type Dialer interface {
|
|
// A Dialer is also a SimpleDialer.
|
|
SimpleDialer
|
|
|
|
// CloseIdleConnections closes idle connections, if any.
|
|
CloseIdleConnections()
|
|
}
|
|
|
|
// HTTPClient is an http.Client-like interface.
|
|
type HTTPClient interface {
|
|
Do(req *http.Request) (*http.Response, error)
|
|
CloseIdleConnections()
|
|
}
|
|
|
|
// HTTPTransport is an http.Transport-like structure.
|
|
type HTTPTransport interface {
|
|
// Network returns the network used by the transport, which
|
|
// should be one of "tcp" and "udp".
|
|
Network() string
|
|
|
|
// RoundTrip performs the HTTP round trip.
|
|
RoundTrip(req *http.Request) (*http.Response, error)
|
|
|
|
// CloseIdleConnections closes idle connections.
|
|
CloseIdleConnections()
|
|
}
|
|
|
|
// HTTPSSvc is the reply to an HTTPS DNS query.
|
|
type HTTPSSvc struct {
|
|
// ALPN contains the ALPNs inside the HTTPS reply.
|
|
ALPN []string
|
|
|
|
// IPv4 contains the IPv4 hints (which may be empty).
|
|
IPv4 []string
|
|
|
|
// IPv6 contains the IPv6 hints (which may be empty).
|
|
IPv6 []string
|
|
}
|
|
|
|
// QUICListener listens for QUIC connections.
|
|
type QUICListener interface {
|
|
// Listen creates a new listening UDPLikeConn.
|
|
Listen(addr *net.UDPAddr) (UDPLikeConn, error)
|
|
}
|
|
|
|
// QUICDialerWrapper is a type that takes in input a QUICDialer
|
|
// and returns in output a wrapped QUICDialer.
|
|
type QUICDialerWrapper interface {
|
|
WrapQUICDialer(qd QUICDialer) QUICDialer
|
|
}
|
|
|
|
// QUICDialer dials QUIC sessions.
|
|
type QUICDialer interface {
|
|
// DialContext establishes a new QUIC session using the given
|
|
// network and address. The tlsConfig and the quicConfig arguments
|
|
// MUST NOT be nil. Returns either the session or an error.
|
|
//
|
|
// Recommended tlsConfig setup:
|
|
//
|
|
// - set ServerName to be the SNI;
|
|
//
|
|
// - set RootCAs to NewDefaultCertPool();
|
|
//
|
|
// - set NextProtos to []string{"h3"}.
|
|
//
|
|
// Typically, you want to pass `&quic.Config{}` as quicConfig.
|
|
DialContext(ctx context.Context, address string,
|
|
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error)
|
|
|
|
// CloseIdleConnections closes idle connections, if any.
|
|
CloseIdleConnections()
|
|
}
|
|
|
|
// Resolver performs domain name resolutions.
|
|
type Resolver interface {
|
|
// LookupHost behaves like net.Resolver.LookupHost.
|
|
LookupHost(ctx context.Context, hostname string) (addrs []string, err error)
|
|
|
|
// Network returns the resolver type. It should be one of:
|
|
//
|
|
// - go: means we're using whatever resolver the Go stdlib uses
|
|
// depending on the current build configuration;
|
|
//
|
|
// - system: means we've been compiled with `CGO_ENABLED=1`
|
|
// so we can bypass the go resolver and call getaddrinfo directly;
|
|
//
|
|
// - udp: is a custom DNS-over-UDP resolver;
|
|
//
|
|
// - tcp: is a custom DNS-over-TCP resolver;
|
|
//
|
|
// - dot: is a custom DNS-over-TLS resolver;
|
|
//
|
|
// - doh: is a custom DNS-over-HTTPS resolver;
|
|
//
|
|
// - doh3: is a custom DNS-over-HTTP3 resolver.
|
|
//
|
|
// See https://github.com/ooni/probe/issues/2029#issuecomment-1140805266
|
|
// for an explanation of why it would not be proper to call "netgo" the
|
|
// resolver we get by default from the standard library.
|
|
Network() string
|
|
|
|
// Address returns the resolver address (e.g., 8.8.8.8:53).
|
|
Address() string
|
|
|
|
// CloseIdleConnections closes idle connections, if any.
|
|
CloseIdleConnections()
|
|
|
|
// LookupHTTPS issues an HTTPS query for a domain.
|
|
LookupHTTPS(
|
|
ctx context.Context, domain string) (*HTTPSSvc, error)
|
|
|
|
// LookupNS issues a NS query for a domain.
|
|
LookupNS(ctx context.Context, domain string) ([]*net.NS, error)
|
|
}
|
|
|
|
// TLSDialer is a Dialer dialing TLS connections.
|
|
type TLSDialer interface {
|
|
// CloseIdleConnections closes idle connections, if any.
|
|
CloseIdleConnections()
|
|
|
|
// DialTLSContext dials a TLS connection. This method will always return
|
|
// to you a oohttp.TLSConn, so you can always safely cast to it.
|
|
DialTLSContext(ctx context.Context, network, address string) (net.Conn, error)
|
|
}
|
|
|
|
// TLSHandshaker is the generic TLS handshaker.
|
|
type TLSHandshaker interface {
|
|
// Handshake creates a new TLS connection from the given connection and
|
|
// the given config. This function DOES NOT take ownership of the connection
|
|
// and it's your responsibility to close it on failure.
|
|
//
|
|
// Recommended tlsConfig setup:
|
|
//
|
|
// - set ServerName to be the SNI;
|
|
//
|
|
// - set RootCAs to NewDefaultCertPool();
|
|
//
|
|
// - set NextProtos to []string{"h2", "http/1.1"} for HTTPS
|
|
// and []string{"dot"} for DNS-over-TLS.
|
|
//
|
|
// QUIRK: The returned connection will always implement the TLSConn interface
|
|
// exposed by ooni/oohttp. A future version of this interface may instead
|
|
// return directly a TLSConn to avoid unconditional castings.
|
|
Handshake(ctx context.Context, conn net.Conn, tlsConfig *tls.Config) (
|
|
net.Conn, tls.ConnectionState, error)
|
|
}
|
|
|
|
// Trace allows to collect measurement traces. A trace is injected into
|
|
// netx operations using context.WithValue. Netx code retrieves the trace
|
|
// using context.Value. See docs/design/dd-003-step-by-step.md for the
|
|
// design document explaining why we implemented context-based tracing.
|
|
type Trace interface {
|
|
// TimeNow returns the current time. Normally, this should be the same
|
|
// value returned by time.Now but you may want to manipulate the time
|
|
// returned when testing to have deterministic tests. To this end, you
|
|
// can use functionality exported by the ./internal/testingx pkg.
|
|
TimeNow() time.Time
|
|
|
|
// MaybeWrapNetConn possibly wraps a net.Conn with the caller trace. If there's no
|
|
// desire to wrap the net.Conn, this function just returns the original net.Conn.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - conn is the non-nil underlying net.Conn to be wrapped
|
|
MaybeWrapNetConn(conn net.Conn) net.Conn
|
|
|
|
// MaybeWrapUDPLikeConn is like MaybeWrapNetConn but for UDPLikeConn.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - conn is the non-nil underlying UDPLikeConn to be wrapped
|
|
MaybeWrapUDPLikeConn(conn UDPLikeConn) UDPLikeConn
|
|
|
|
// OnDNSRoundTripForLookupHost is used with a DNSTransport and called
|
|
// when the RoundTrip terminates.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - started is when we called transport.RoundTrip
|
|
//
|
|
// - reso is the parent resolver for the trace;
|
|
//
|
|
// - query is the non-nil DNS query we use for the RoundTrip
|
|
//
|
|
// - response is a valid DNS response, obtained after the RoundTrip;
|
|
//
|
|
// - addrs is the list of addresses obtained after the RoundTrip, which
|
|
// is empty if the RoundTrip failed
|
|
//
|
|
// - err is the result of DNSLookup; either an error or nil
|
|
//
|
|
// - finished is the time right after the RoundTrip
|
|
OnDNSRoundTripForLookupHost(started time.Time, reso Resolver, query DNSQuery,
|
|
response DNSResponse, addrs []string, err error, finished time.Time)
|
|
|
|
// OnDelayedDNSResponse is used with a DNSOverUDPTransport and called
|
|
// when we get delayed, unexpected DNS responses.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - started is when we started reading the delayed response;
|
|
//
|
|
// - txp is the DNS transport used with the resolver;
|
|
//
|
|
// - query is the non-nil DNS query we use for the RoundTrip;
|
|
//
|
|
// - response is the non-nil valid DNS response, obtained after some delay;
|
|
//
|
|
// - addrs is the list of addresses obtained after decoding the delayed response,
|
|
// which is empty if the response did not contain any addresses, which we
|
|
// extract by calling the DecodeLookupHost method.
|
|
//
|
|
// - err is the result of DecodeLookupHost: either an error or nil;
|
|
//
|
|
// - finished is when we have read the delayed response.
|
|
OnDelayedDNSResponse(started time.Time, txp DNSTransport, query DNSQuery,
|
|
resp DNSResponse, addrs []string, err error, finsihed time.Time) error
|
|
|
|
// OnConnectDone is called when connect terminates.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - started is when we called connect;
|
|
//
|
|
// - network is the network we're using (one of "tcp" and "udp");
|
|
//
|
|
// - domain is the domain for which we're calling connect. If the user called
|
|
// connect for an IP address and a port, then domain will be an IP address;
|
|
//
|
|
// - remoteAddr is the TCP endpoint with which we are connecting: it will
|
|
// consist of an IP address and a port (e.g., 8.8.8.8:443, [::1]:5421);
|
|
//
|
|
// - err is the result of connect: either an error or nil;
|
|
//
|
|
// - finished is when connect returned.
|
|
//
|
|
// The error passed to this function will always be wrapped such that the
|
|
// string returned by Error is an OONI error.
|
|
OnConnectDone(
|
|
started time.Time, network, domain, remoteAddr string, err error, finished time.Time)
|
|
|
|
// OnTLSHandshakeStart is called when the TLS handshake starts.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - now is the moment before we start the handshake;
|
|
//
|
|
// - remoteAddr is the TCP endpoint with which we are connecting: it will
|
|
// consist of an IP address and a port (e.g., 8.8.8.8:443, [::1]:5421);
|
|
//
|
|
// - config is the non-nil TLS config we're using.
|
|
OnTLSHandshakeStart(now time.Time, remoteAddr string, config *tls.Config)
|
|
|
|
// OnTLSHandshakeDone is called when the TLS handshake terminates.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - started is when we started the handshake;
|
|
//
|
|
// - remoteAddr is the TCP endpoint with which we are connecting: it will
|
|
// consist of an IP address and a port (e.g., 8.8.8.8:443, [::1]:5421);
|
|
//
|
|
// - config is the non-nil TLS config we're using;
|
|
//
|
|
// - state is the state of the TLS connection after the handshake, where all
|
|
// fields are zero-initialized if the handshake failed;
|
|
//
|
|
// - err is the result of the handshake: either an error or nil;
|
|
//
|
|
// - finished is right after the handshake.
|
|
//
|
|
// The error passed to this function will always be wrapped such that the
|
|
// string returned by Error is an OONI error.
|
|
OnTLSHandshakeDone(started time.Time, remoteAddr string, config *tls.Config,
|
|
state tls.ConnectionState, err error, finished time.Time)
|
|
|
|
// OnQUICHandshakeStart is called before the QUIC handshake.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - now is the moment before we start the handshake;
|
|
//
|
|
// - remoteAddr is the QUIC endpoint with which we are connecting: it will
|
|
// consist of an IP address and a port (e.g., 8.8.8.8:443, [::1]:5421);
|
|
//
|
|
// - config is the possibly-nil QUIC config we're using.
|
|
OnQUICHandshakeStart(now time.Time, remoteAddr string, quicConfig *quic.Config)
|
|
|
|
// OnQUICHandshakeDone is called after the QUIC handshake.
|
|
//
|
|
// Arguments:
|
|
//
|
|
// - started is when we started the handshake;
|
|
//
|
|
// - remoteAddr is the QUIC endpoint with which we are connecting: it will
|
|
// consist of an IP address and a port (e.g., 8.8.8.8:443, [::1]:5421);
|
|
//
|
|
// - qconn is the QUIC connection we receive after the handshake: either
|
|
// a valid quic.EarlyConnection or nil;
|
|
//
|
|
// - config is the non-nil TLS config we are using;
|
|
//
|
|
// - err is the result of the handshake: either an error or nil;
|
|
//
|
|
// - finished is right after the handshake.
|
|
//
|
|
// The error passed to this function will always be wrapped such that the
|
|
// string returned by Error is an OONI error.
|
|
OnQUICHandshakeDone(started time.Time, remoteAddr string, qconn quic.EarlyConnection,
|
|
config *tls.Config, err error, finished time.Time)
|
|
}
|
|
|
|
// UDPLikeConn is a net.PacketConn with some extra functions
|
|
// required to convince the QUIC library (lucas-clemente/quic-go)
|
|
// to inflate the receive buffer of the connection.
|
|
//
|
|
// The QUIC library will treat this connection as a "dumb"
|
|
// net.PacketConn, calling its ReadFrom and WriteTo methods
|
|
// as opposed to more efficient methods that are available
|
|
// under Linux and (maybe?) FreeBSD.
|
|
//
|
|
// It seems fine to avoid performance optimizations, because
|
|
// they would complicate the implementation on our side and
|
|
// our use cases (blocking and heavy throttling) do not seem
|
|
// to require such optimizations.
|
|
//
|
|
// See https://github.com/ooni/probe/issues/1754 for a more
|
|
// comprehensive discussion of UDPLikeConn.
|
|
type UDPLikeConn interface {
|
|
// An UDPLikeConn is a net.PacketConn conn.
|
|
net.PacketConn
|
|
|
|
// SetReadBuffer allows setting the read buffer.
|
|
SetReadBuffer(bytes int) error
|
|
|
|
// SyscallConn returns a conn suitable for calling syscalls,
|
|
// which is also instrumental to setting the read buffer.
|
|
SyscallConn() (syscall.RawConn, error)
|
|
}
|