ooni-probe-cli/internal/model/netx.go
Simone Basso 86ffd6a0c4
feat: reintroduce the tproxy functionality (#975)
We originally removed the TProxy in https://github.com/ooni/probe/issues/2224. Nevertheless, in https://github.com/ooni/probe-cli/pull/969, we determined that something like the previous TProxy, with small changes, was required to support https://github.com/ooni/probe/issues/2340. So, this pull request reintroduces a slightly-modified TProxy functionality that better adapts to the `--remote=REMOTE` use case.
2022-10-12 17:38:33 +02:00

501 lines
17 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)
}
// UnderlyingNetwork implements the underlying network APIs on
// top of which we implement network extensions.
type UnderlyingNetwork interface {
// DialContext is equivalent to net.Dialer.DialContext except that
// there is also an explicit timeout for dialing.
DialContext(ctx context.Context, timeout time.Duration, network, address string) (net.Conn, error)
// ListenUDP is equivalent to net.ListenUDP.
ListenUDP(network string, addr *net.UDPAddr) (UDPLikeConn, error)
// GetaddrinfoLookupANY is like net.Resolver.LookupHost except that it
// also returns to the caller the CNAME when it is available.
GetaddrinfoLookupANY(ctx context.Context, domain string) ([]string, string, error)
// GetaddrinfoResolverNetwork returns the resolver network.
GetaddrinfoResolverNetwork() string
}