diff --git a/internal/measurex/library.go b/internal/measurex/library.go index 290cf51..da2474e 100644 --- a/internal/measurex/library.go +++ b/internal/measurex/library.go @@ -51,9 +51,9 @@ type NetxliteLibrary interface { // NewResolverSystem creates a new "system" resolver. NewResolverSystem(logger model.Logger) model.Resolver - // NewResolverSerial creates a new "serial" resolver. (Note that + // NewUnwrappedParallelResolver creates a new "parallel" resolver. (Note that // this resolver needs to be wrapped.) - NewResolverSerial(txp model.DNSTransport) model.Resolver + NewUnwrappedParallelResolver(txp model.DNSTransport) model.Resolver // NewSingleUseDialer creates a new "single use" dialer. NewSingleUseDialer(conn net.Conn) model.Dialer @@ -175,7 +175,7 @@ func (lib *Library) NewResolverUDP(saver *archival.Saver, address string) model. return saver.WrapResolver( lib.netxlite.WrapResolver( lib.logger, - lib.netxlite.NewResolverSerial( + lib.netxlite.NewUnwrappedParallelResolver( saver.WrapDNSTransport( lib.netxlite.NewDNSOverUDPTransport( lib.newDialerWithSystemResolver(saver), @@ -242,8 +242,8 @@ func (nl *netxliteLibrary) NewResolverSystem(logger model.Logger) model.Resolver return netxlite.NewResolverStdlib(logger) } -func (nl *netxliteLibrary) NewResolverSerial(txp model.DNSTransport) model.Resolver { - return netxlite.NewSerialResolver(txp) +func (nl *netxliteLibrary) NewUnwrappedParallelResolver(txp model.DNSTransport) model.Resolver { + return netxlite.NewUnwrappedParallelResolver(txp) } func (nl *netxliteLibrary) NewSingleUseDialer(conn net.Conn) model.Dialer { diff --git a/internal/netxlite/parallelresolver.go b/internal/netxlite/parallelresolver.go new file mode 100644 index 0000000..6668770 --- /dev/null +++ b/internal/netxlite/parallelresolver.go @@ -0,0 +1,130 @@ +package netxlite + +import ( + "context" + + "github.com/bassosimone/websteps-illustrated/internal/atomicx" + "github.com/bassosimone/websteps-illustrated/internal/model" + "github.com/miekg/dns" +) + +// ParallelResolver uses a transport and sends performs a LookupHost +// operation in a parallel fashion, hence its name. +// +// You should probably use NewUnwrappedParallel to create a new instance. +type ParallelResolver struct { + // Encoder is the MANDATORY encoder to use. + Encoder model.DNSEncoder + + // Decoder is the MANDATORY decoder to use. + Decoder model.DNSDecoder + + // NumTimeouts is MANDATORY and counts the number of timeouts. + NumTimeouts *atomicx.Int64 + + // Txp is the underlying DNS transport. + Txp model.DNSTransport +} + +// UnwrappedParallelResolver creates a new ParallelResolver instance. +func NewUnwrappedParallelResolver(t model.DNSTransport) *ParallelResolver { + return &ParallelResolver{ + Encoder: &DNSEncoderMiekg{}, + Decoder: &DNSDecoderMiekg{}, + NumTimeouts: &atomicx.Int64{}, + Txp: t, + } +} + +// Transport returns the transport being used. +func (r *ParallelResolver) Transport() model.DNSTransport { + return r.Txp +} + +// Network returns the "network" of the underlying transport. +func (r *ParallelResolver) Network() string { + return r.Txp.Network() +} + +// Address returns the "address" of the underlying transport. +func (r *ParallelResolver) Address() string { + return r.Txp.Address() +} + +// CloseIdleConnections closes idle connections, if any. +func (r *ParallelResolver) CloseIdleConnections() { + r.Txp.CloseIdleConnections() +} + +// LookupHost performs an A lookup followed by an AAAA lookup for hostname. +func (r *ParallelResolver) LookupHost(ctx context.Context, hostname string) ([]string, error) { + resch := make(chan *parallelResolverResult) + go r.lookupHost(ctx, hostname, dns.TypeA, resch) + go r.lookupHost(ctx, hostname, dns.TypeAAAA, resch) + first := <-resch + second := <-resch + if first.err != nil && second.err != nil { + // Note: we choose to return the A error because we assume that + // it's the more meaningful one: the AAAA error may just be telling + // us that there is no AAAA record for the website. + if first.qtype == dns.TypeA { + return nil, first.err + } + return nil, second.err + } + var addrs []string + addrs = append(addrs, first.addrs...) + addrs = append(addrs, second.addrs...) + return addrs, nil +} + +// LookupHTTPS implements Resolver.LookupHTTPS. +func (r *ParallelResolver) LookupHTTPS( + ctx context.Context, hostname string) (*model.HTTPSSvc, error) { + querydata, err := r.Encoder.Encode( + hostname, dns.TypeHTTPS, r.Txp.RequiresPadding()) + if err != nil { + return nil, err + } + replydata, err := r.Txp.RoundTrip(ctx, querydata) + if err != nil { + return nil, err + } + return r.Decoder.DecodeHTTPS(replydata) +} + +// parallelResolverResult is the internal representation of a lookup result. +type parallelResolverResult struct { + addrs []string + err error + qtype uint16 +} + +// lookupHost issues a lookup host query for the specified qtype (e.g., dns.A). +func (r *ParallelResolver) lookupHost(ctx context.Context, hostname string, + qtype uint16, out chan<- *parallelResolverResult) { + querydata, err := r.Encoder.Encode(hostname, qtype, r.Txp.RequiresPadding()) + if err != nil { + out <- ¶llelResolverResult{ + addrs: []string{}, + err: err, + qtype: qtype, + } + return + } + replydata, err := r.Txp.RoundTrip(ctx, querydata) + if err != nil { + out <- ¶llelResolverResult{ + addrs: []string{}, + err: err, + qtype: qtype, + } + return + } + addrs, err := r.Decoder.DecodeLookupHost(qtype, replydata) + out <- ¶llelResolverResult{ + addrs: addrs, + err: err, + qtype: qtype, + } +}