2021-02-02 12:05:47 +01:00
|
|
|
package netx
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/engine/legacy/netx/handlers"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/engine/legacy/netx/modelx"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/engine/netx/resolver"
|
refactor: start pivoting netx (#396)
What do I mean by pivoting? Netx is currently organized by row:
```
| dialer | quicdialer | resolver | ...
saving | | | | ...
errorwrapping | | | | ...
logging | | | | ...
mocking/sys | | | | ...
```
Every row needs to implement saving, errorwrapping, logging, mocking (or
adapting to the system or to some underlying library).
This causes cross package dependencies and, in turn, complexity. For
example, we need the `trace` package for supporting saving.
And `dialer`, `quickdialer`, et al. need to depend on such a package.
The same goes for errorwrapping.
This arrangement further complicates testing. For example, I am
currently working on https://github.com/ooni/probe/issues/1505 and
I realize it need to repeat integration tests in multiple places.
Let's say instead we pivot the above matrix as follows:
```
| saving | errorwrapping | logging | ...
dialer | | | | ...
quicdialer | | | | ...
logging | | | | ...
mocking/sys | | | | ...
...
```
In this way, now every row contains everything related to a specific
action to perform. We can now share code without relying on extra
support packages. What's more, we can write tests and, judding from
the way in which things are made, it seems we only need integration
testing in `errorwrapping` because it's where data quality matters
whereas, in all other cases, unit testing is fine.
I am going, therefore, to proceed with these changes and "pivot"
`netx`. Hopefully, it won't be too painful.
2021-06-23 15:53:12 +02:00
|
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
2021-02-02 12:05:47 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
dohClientHandle *http.Client
|
|
|
|
dohClientOnce sync.Once
|
|
|
|
)
|
|
|
|
|
|
|
|
func newHTTPClientForDoH(beginning time.Time, handler modelx.Handler) *http.Client {
|
|
|
|
if handler == handlers.NoHandler {
|
|
|
|
// A bit of extra complexity for a good reason: if the user is not
|
|
|
|
// interested into setting a default handler, then it is fine to
|
|
|
|
// always return the same *http.Client for DoH. This means that we
|
|
|
|
// don't need to care about closing the connections used by this
|
|
|
|
// *http.Client, therefore we don't leak resources because we fail
|
|
|
|
// to close the idle connections.
|
|
|
|
dohClientOnce.Do(func() {
|
|
|
|
transport := newHTTPTransport(
|
|
|
|
time.Now(),
|
|
|
|
handlers.NoHandler,
|
|
|
|
newDialer(time.Now(), handler),
|
|
|
|
false, // DisableKeepAlives
|
|
|
|
http.ProxyFromEnvironment,
|
|
|
|
)
|
|
|
|
dohClientHandle = &http.Client{Transport: transport}
|
|
|
|
})
|
|
|
|
return dohClientHandle
|
|
|
|
}
|
|
|
|
// Otherwise, if the user wants to have a default handler, we
|
|
|
|
// return a transport that does not leak connections.
|
|
|
|
transport := newHTTPTransport(
|
|
|
|
beginning,
|
|
|
|
handler,
|
|
|
|
newDialer(beginning, handler),
|
|
|
|
true, // DisableKeepAlives
|
|
|
|
http.ProxyFromEnvironment,
|
|
|
|
)
|
|
|
|
return &http.Client{Transport: transport}
|
|
|
|
}
|
|
|
|
|
|
|
|
func withPort(address, port string) string {
|
|
|
|
// Handle the case where port was not specified. We have written in
|
|
|
|
// a bunch of places that we can just pass a domain in this case and
|
|
|
|
// so we need to gracefully ensure this is still possible.
|
|
|
|
_, _, err := net.SplitHostPort(address)
|
|
|
|
if err != nil && strings.Contains(err.Error(), "missing port in address") {
|
|
|
|
address = net.JoinHostPort(address, port)
|
|
|
|
}
|
|
|
|
return address
|
|
|
|
}
|
|
|
|
|
|
|
|
type resolverWrapper struct {
|
|
|
|
beginning time.Time
|
|
|
|
handler modelx.Handler
|
|
|
|
resolver modelx.DNSResolver
|
|
|
|
}
|
|
|
|
|
|
|
|
func newResolverWrapper(
|
|
|
|
beginning time.Time, handler modelx.Handler,
|
|
|
|
resolver modelx.DNSResolver,
|
|
|
|
) *resolverWrapper {
|
|
|
|
return &resolverWrapper{
|
|
|
|
beginning: beginning,
|
|
|
|
handler: handler,
|
|
|
|
resolver: resolver,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// LookupHost returns the IP addresses of a host
|
|
|
|
func (r *resolverWrapper) LookupHost(ctx context.Context, hostname string) ([]string, error) {
|
|
|
|
ctx = maybeWithMeasurementRoot(ctx, r.beginning, r.handler)
|
|
|
|
return r.resolver.LookupHost(ctx, hostname)
|
|
|
|
}
|
|
|
|
|
|
|
|
func newResolver(
|
|
|
|
beginning time.Time, handler modelx.Handler, network, address string,
|
|
|
|
) (modelx.DNSResolver, error) {
|
|
|
|
// Implementation note: system need to be dealt with
|
|
|
|
// separately because it doesn't have any transport.
|
|
|
|
if network == "system" || network == "" {
|
|
|
|
return newResolverWrapper(
|
|
|
|
beginning, handler, newResolverSystem()), nil
|
|
|
|
}
|
|
|
|
if network == "doh" {
|
|
|
|
return newResolverWrapper(beginning, handler, newResolverHTTPS(
|
|
|
|
newHTTPClientForDoH(beginning, handler), address,
|
|
|
|
)), nil
|
|
|
|
}
|
|
|
|
if network == "dot" {
|
|
|
|
// We need a child dialer here to avoid an endless loop where the
|
|
|
|
// dialer will ask us to resolve, we'll tell the dialer to dial, it
|
|
|
|
// will ask us to resolve, ...
|
|
|
|
return newResolverWrapper(beginning, handler, newResolverTLS(
|
|
|
|
newDialer(beginning, handler).DialTLSContext, withPort(address, "853"),
|
|
|
|
)), nil
|
|
|
|
}
|
|
|
|
if network == "tcp" {
|
|
|
|
// Same rationale as above: avoid possible endless loop
|
|
|
|
return newResolverWrapper(beginning, handler, newResolverTCP(
|
|
|
|
newDialer(beginning, handler).DialContext, withPort(address, "53"),
|
|
|
|
)), nil
|
|
|
|
}
|
|
|
|
if network == "udp" {
|
|
|
|
// Same rationale as above: avoid possible endless loop
|
|
|
|
return newResolverWrapper(beginning, handler, newResolverUDP(
|
|
|
|
newDialer(beginning, handler), withPort(address, "53"),
|
|
|
|
)), nil
|
|
|
|
}
|
|
|
|
return nil, errors.New("resolver.New: unsupported network value")
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewResolver creates a standalone Resolver
|
|
|
|
func NewResolver(network, address string) (modelx.DNSResolver, error) {
|
|
|
|
return newResolver(time.Now(), handlers.NoHandler, network, address)
|
|
|
|
}
|
|
|
|
|
|
|
|
type chainWrapperResolver struct {
|
|
|
|
modelx.DNSResolver
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r chainWrapperResolver) Network() string {
|
|
|
|
return "chain"
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r chainWrapperResolver) Address() string {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// ChainResolvers chains a primary and a secondary resolver such that
|
|
|
|
// we can fallback to the secondary if primary is broken.
|
|
|
|
func ChainResolvers(primary, secondary modelx.DNSResolver) modelx.DNSResolver {
|
|
|
|
return resolver.ChainResolver{
|
|
|
|
Primary: chainWrapperResolver{DNSResolver: primary},
|
|
|
|
Secondary: chainWrapperResolver{DNSResolver: secondary},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func resolverWrapResolver(r resolver.Resolver) resolver.EmitterResolver {
|
|
|
|
return resolver.EmitterResolver{Resolver: resolver.ErrorWrapperResolver{Resolver: r}}
|
|
|
|
}
|
|
|
|
|
|
|
|
func resolverWrapTransport(txp resolver.RoundTripper) resolver.EmitterResolver {
|
|
|
|
return resolverWrapResolver(resolver.NewSerialResolver(
|
|
|
|
resolver.EmitterTransport{RoundTripper: txp}))
|
|
|
|
}
|
|
|
|
|
|
|
|
func newResolverSystem() resolver.EmitterResolver {
|
refactor: start pivoting netx (#396)
What do I mean by pivoting? Netx is currently organized by row:
```
| dialer | quicdialer | resolver | ...
saving | | | | ...
errorwrapping | | | | ...
logging | | | | ...
mocking/sys | | | | ...
```
Every row needs to implement saving, errorwrapping, logging, mocking (or
adapting to the system or to some underlying library).
This causes cross package dependencies and, in turn, complexity. For
example, we need the `trace` package for supporting saving.
And `dialer`, `quickdialer`, et al. need to depend on such a package.
The same goes for errorwrapping.
This arrangement further complicates testing. For example, I am
currently working on https://github.com/ooni/probe/issues/1505 and
I realize it need to repeat integration tests in multiple places.
Let's say instead we pivot the above matrix as follows:
```
| saving | errorwrapping | logging | ...
dialer | | | | ...
quicdialer | | | | ...
logging | | | | ...
mocking/sys | | | | ...
...
```
In this way, now every row contains everything related to a specific
action to perform. We can now share code without relying on extra
support packages. What's more, we can write tests and, judding from
the way in which things are made, it seems we only need integration
testing in `errorwrapping` because it's where data quality matters
whereas, in all other cases, unit testing is fine.
I am going, therefore, to proceed with these changes and "pivot"
`netx`. Hopefully, it won't be too painful.
2021-06-23 15:53:12 +02:00
|
|
|
return resolverWrapResolver(netxlite.ResolverSystem{})
|
2021-02-02 12:05:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func newResolverUDP(dialer resolver.Dialer, address string) resolver.EmitterResolver {
|
|
|
|
return resolverWrapTransport(resolver.NewDNSOverUDP(dialer, address))
|
|
|
|
}
|
|
|
|
|
|
|
|
func newResolverTCP(dial resolver.DialContextFunc, address string) resolver.EmitterResolver {
|
|
|
|
return resolverWrapTransport(resolver.NewDNSOverTCP(dial, address))
|
|
|
|
}
|
|
|
|
|
|
|
|
func newResolverTLS(dial resolver.DialContextFunc, address string) resolver.EmitterResolver {
|
|
|
|
return resolverWrapTransport(resolver.NewDNSOverTLS(dial, address))
|
|
|
|
}
|
|
|
|
|
|
|
|
func newResolverHTTPS(client *http.Client, address string) resolver.EmitterResolver {
|
|
|
|
return resolverWrapTransport(resolver.NewDNSOverHTTPS(client, address))
|
|
|
|
}
|