Skip to content

Commit

Permalink
Use DNS data format in DDR experiment
Browse files Browse the repository at this point in the history
  • Loading branch information
frcroth committed Jan 31, 2025
1 parent 8ddec18 commit 8739b76
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 33 deletions.
88 changes: 63 additions & 25 deletions internal/experiment/ddr/ddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/apex/log"
"github.com/miekg/dns"
"github.com/ooni/probe-cli/v3/internal/geoipx"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)
Expand Down Expand Up @@ -39,22 +40,13 @@ func (m *Measurer) ExperimentVersion() string {
return testVersion
}

type DDRResponse struct {
Priority int `json:"priority"`
Target string `json:"target"`
Keys map[string]string `json:"keys"`
}

type TestKeys struct {
// DDRResponse is the DDR response.
DDRResponse []DDRResponse `json:"ddr_responses"`
// DNS Queries and results (as specified in https://github.com/ooni/spec/blob/master/data-formats/df-002-dnst.md#dns-data-format)
Queries model.ArchivalDNSLookupResult `json:"queries"`

// SupportsDDR is true if DDR is supported.
SupportsDDR bool `json:"supports_ddr"`

// Resolver is the resolver used (the system resolver of the host).
Resolver string `json:"resolver"`

// Failure is the failure that occurred, or nil.
Failure *string `json:"failure"`
}
Expand All @@ -73,28 +65,30 @@ func (m *Measurer) Run(
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

resolver := ""
if m.config.CustomResolver == nil {
systemResolver := getSystemResolverAddress()
if systemResolver == "" {
return errors.New("could not get system resolver")
}
log.Infof("Using system resolver: %s", systemResolver)
tk.Resolver = systemResolver
resolver = systemResolver
} else {
tk.Resolver = *m.config.CustomResolver
resolver = *m.config.CustomResolver
}

// DDR queries are queries of the SVCB type for the _dns.resolver.arpa. domain.

netx := &netxlite.Netx{}
dialer := netx.NewDialerWithoutResolver(log.Log)
transport := netxlite.NewUnwrappedDNSOverUDPTransport(
dialer, tk.Resolver)
dialer, resolver)
encoder := &netxlite.DNSEncoderMiekg{}
query := encoder.Encode(
"_dns.resolver.arpa.", // As specified in RFC 9462
dns.TypeSVCB,
true)
t0 := time.Since(measurement.MeasurementStartTimeSaved).Seconds()
resp, err := transport.RoundTrip(ctx, query)
if err != nil {
failure := err.Error()
Expand All @@ -115,20 +109,19 @@ func (m *Measurer) Run(
if err != nil {
decodingError := err.Error()
tk.Failure = &decodingError
} else {
tk.DDRResponse = ddrResponse
}
t := time.Since(measurement.MeasurementStartTimeSaved).Seconds()
tk.Queries = createResult(t, t0, tk.Failure, resp, resolver, ddrResponse)

tk.SupportsDDR = len(tk.DDRResponse) > 0
tk.SupportsDDR = len(ddrResponse) > 0

log.Infof("Gathered DDR Responses: %+v", tk.DDRResponse)
return nil
}

// decodeResponse decodes the response from the DNS query.
// DDR is only concerned with SVCB records, so we only decode those.
func decodeResponse(responseFields []dns.RR) ([]DDRResponse, error) {
responses := make([]DDRResponse, 0)
func decodeResponse(responseFields []dns.RR) ([]model.SVCBData, error) {
responses := make([]model.SVCBData, 0)
for _, rr := range responseFields {
switch rr := rr.(type) {
case *dns.SVCB:
Expand All @@ -144,18 +137,18 @@ func decodeResponse(responseFields []dns.RR) ([]DDRResponse, error) {
return responses, nil
}

func parseSvcb(rr *dns.SVCB) (DDRResponse, error) {
func parseSvcb(rr *dns.SVCB) (model.SVCBData, error) {
keys := make(map[string]string)
for _, kv := range rr.Value {
value := kv.String()
key := kv.Key().String()
keys[key] = value
}

return DDRResponse{
Priority: int(rr.Priority),
Target: rr.Target,
Keys: keys,
return model.SVCBData{
Priority: rr.Priority,
TargetName: rr.Target,
Params: keys,
}, nil
}

Expand All @@ -174,6 +167,51 @@ func getSystemResolverAddress() string {
return ""
}

func createResult(t float64, t0 float64, failure *string, resp model.DNSResponse, resolver string, svcbRecords []model.SVCBData) model.ArchivalDNSLookupResult {
resolverHost, _, err := net.SplitHostPort(resolver)
if err != nil {
log.Warnf("Could not split resolver address %s: %s", resolver, err)
resolverHost = resolver
}
asn, org, err := geoipx.LookupASN(resolverHost)
if err != nil {
log.Warnf("Could not lookup ASN for resolver %s: %s", resolverHost, err)
asn = 0
org = ""
}

answers := make([]model.ArchivalDNSAnswer, 0)
for _, record := range svcbRecords {
// Create an ArchivalDNSAnswer for each SVCB record
// for this experiment, only the SVCB key is relevant.
answers = append(answers, model.ArchivalDNSAnswer{
ASN: int64(asn),
ASOrgName: org,
AnswerType: "SVCB",
Hostname: "",
IPv4: "",
IPv6: "",
SVCB: &record,
})
}

return model.ArchivalDNSLookupResult{
Answers: answers,
Engine: "udp",
Failure: failure,
GetaddrinfoError: 0,
Hostname: "_dns.resolver.arpa.",
QueryType: "SVCB",
RawResponse: resp.Bytes(),
Rcode: int64(resp.Rcode()),
ResolverAddress: resolverHost,
T0: t0,
T: t,
Tags: nil,
TransactionID: 0,
}
}

func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
return &Measurer{
config: config,
Expand Down
8 changes: 7 additions & 1 deletion internal/experiment/ddr/ddr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ func TestMeasurerRun(t *testing.T) {
t.Fatal("unexpected Failure")
}

if tk.Resolver != oneOneOneOneResolver {
firstAnswer := tk.Queries.Answers[0]

if firstAnswer.AnswerType != "SVCB" {
t.Fatal("unexpected AnswerType")
}

if tk.Queries.ResolverAddress != oneOneOneOneResolver {
t.Fatal("Resolver should be written to TestKeys")
}

Expand Down
22 changes: 15 additions & 7 deletions internal/model/archival.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,21 @@ type ArchivalDNSLookupResult struct {

// ArchivalDNSAnswer is a DNS answer.
type ArchivalDNSAnswer struct {
ASN int64 `json:"asn,omitempty"`
ASOrgName string `json:"as_org_name,omitempty"`
AnswerType string `json:"answer_type"`
Hostname string `json:"hostname,omitempty"`
IPv4 string `json:"ipv4,omitempty"`
IPv6 string `json:"ipv6,omitempty"`
TTL *uint32 `json:"ttl"`
ASN int64 `json:"asn,omitempty"`
ASOrgName string `json:"as_org_name,omitempty"`
AnswerType string `json:"answer_type"`
Hostname string `json:"hostname,omitempty"`
IPv4 string `json:"ipv4,omitempty"`
IPv6 string `json:"ipv6,omitempty"`
TTL *uint32 `json:"ttl"`
SVCB *SVCBData `json:"svcb,omitempty"` // SVCB-specific data
}

// SVCBData represents details of an SVCB record.
type SVCBData struct {
Priority uint16 `json:"priority"`
TargetName string `json:"target_name"`
Params map[string]string `json:"params,omitempty"` // SvcParams key-value pairs
}

//
Expand Down

0 comments on commit 8739b76

Please sign in to comment.