Skip to content

Commit dbe74e4

Browse files
authored
va: log experiment results without base64 encoding (#8707)
Go's JSON marshaling encodes `[]byte` in base64. Previously, experiment results were logging `*vapb.ValidationResult`, which contained `repeated corepb.ValidationRecord`. In turn, `corepb.ValidationRecord` contains three `[]byte` fields: ```proto repeated bytes addressesResolved = 3; // netip.Addr.MarshalText() bytes addressUsed = 4; // netip.Addr.MarshalText() repeated bytes addressesTried = 7; // netip.Addr.MarshalText() ``` By contrast, `core.ValidationRecord` (a non-protobuf struct), has fields of type `netip.Addr`: ```go AddressesResolved []netip.Addr `json:"addressesResolved,omitempty"` AddressUsed netip.Addr `json:"addressUsed"` AddressesTried []netip.Addr `json:"addressesTried,omitempty"` ``` Those get serialized like `127.0.01`, which is what we want. This PR reworks the `runExperiment` function so that instead of receiving `remoteResult` (which could contain `*vapb.IsCAAValidResponse` or `*vapb.ValidationResult`), it receives a problem and an (optional) `core.ValidationRecord`. This allows us to more straightforwardly log things without running into base64-encoding problems. To test this I modified `TestExperimentalVAConcurrence` to use HTTP-01 validation, since the affected log fields don't show up under DNS-01 validation. I also tweaked `ipFakeDNS` to allow customizing the IP address it returns. Fixes #8701
1 parent 289c88b commit dbe74e4

File tree

4 files changed

+96
-54
lines changed

4 files changed

+96
-54
lines changed

va/caa.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
corepb "github.com/letsencrypt/boulder/core/proto"
78
"net/url"
89
"regexp"
910
"strings"
@@ -105,25 +106,24 @@ func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAVal
105106
prob.Detail = fmt.Sprintf("While processing CAA for %s: %s", ident.Value, prob.Detail)
106107
}
107108

108-
// Capture the local validation result for experimental resolver comparison
109-
// before MPIC can influence the outcome.
110-
localResult, err := bgrpc.CAAResultToPB(filterProblemDetails(prob), va.perspective, va.rir)
111-
if err != nil {
112-
return nil, err
113-
}
114-
115109
if va.shouldRunExperiment() {
116110
go va.runExperiment(
117111
ctx,
118112
opCAA,
119-
proto.Clone(localResult).(*vapb.IsCAAValidResponse),
120-
func(ctx context.Context) (remoteResult, error) {
121-
return va.experimentalVA.DoCAA(ctx, req)
113+
prob,
114+
nil,
115+
//nolint:unparam // core.ValidationRecord is always nil because we don't return those for CAA.
116+
func(ctx context.Context) ([]core.ValidationRecord, *corepb.ProblemDetails, error) {
117+
result, err := va.experimentalVA.DoCAA(ctx, req)
118+
if err != nil {
119+
return nil, nil, err
120+
}
121+
return nil, result.Problem, err
122122
})
123123
}
124124

125125
if prob != nil {
126-
return localResult, nil
126+
return bgrpc.CAAResultToPB(filterProblemDetails(prob), va.perspective, va.rir)
127127
}
128128

129129
if va.isPrimaryVA() {

va/http_test.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import (
3030
)
3131

3232
type ipFakeDNS struct {
33+
// If non-nil, this IP address will be returned by the appropriate method (LookupA / LookupAAAA) depending on
34+
// whether it is IPv4 or IPv6. Otherwise those methods return 127.0.0.1 / ::1.
35+
ip net.IP
3336
bdns.Client
3437
}
3538

@@ -46,14 +49,19 @@ func (c *ipFakeDNS) LookupA(_ context.Context, hostname string) (*bdns.Result[*d
4649
if strings.HasSuffix(hostname, ".invalid") {
4750
return wrapA()
4851
}
52+
53+
ip := net.IPv4(127, 0, 0, 1)
54+
if c.ip != nil && c.ip.To4() != nil {
55+
ip = c.ip
56+
}
4957
// dual-homed host with an IPv6 and an IPv4 address
5058
if hostname == "ipv4.and.ipv6.localhost" {
51-
return wrapA(net.IPv4(127, 0, 0, 1))
59+
return wrapA(ip)
5260
}
5361
if hostname == "ipv6.localhost" {
5462
return wrapA()
5563
}
56-
return wrapA(net.IPv4(127, 0, 0, 1))
64+
return wrapA(ip)
5765
}
5866

5967
func (c *ipFakeDNS) LookupAAAA(_ context.Context, hostname string) (*bdns.Result[*dns.AAAA], string, error) {
@@ -65,12 +73,17 @@ func (c *ipFakeDNS) LookupAAAA(_ context.Context, hostname string) (*bdns.Result
6573
return &bdns.Result[*dns.AAAA]{Final: rrs}, "ipFakeDNS", nil
6674
}
6775

76+
ip := net.IPv6loopback
77+
if c.ip != nil && c.ip.To4() == nil {
78+
ip = c.ip
79+
}
80+
6881
// dual-homed host with an IPv6 and an IPv4 address
6982
if hostname == "ipv4.and.ipv6.localhost" {
70-
return wrapAAAA(net.IPv6loopback)
83+
return wrapAAAA(ip)
7184
}
7285
if hostname == "ipv6.localhost" {
73-
return wrapAAAA(net.IPv6loopback)
86+
return wrapAAAA(ip)
7487
}
7588
return wrapAAAA()
7689
}

va/va.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -306,14 +306,23 @@ func (va *ValidationAuthorityImpl) shouldRunExperiment() bool {
306306
// VA's result and records a concurrence metric. On disagreement, it logs a
307307
// structured event with both results. The primary argument must be non-nil.
308308
// Callers should invoke this in a goroutine.
309-
func (va *ValidationAuthorityImpl) runExperiment(ctx context.Context, operation string, primary remoteResult, experimentFunc func(context.Context) (remoteResult, error)) {
309+
//
310+
// The passed experimentFunc should return a list of validation records (or nil in the case of CAA),
311+
// followed by any ProblemDetails relevant (which will be nil on success), and any errors in validation.
312+
func (va *ValidationAuthorityImpl) runExperiment(
313+
ctx context.Context,
314+
operation string,
315+
primaryProblem *probs.ProblemDetails,
316+
primaryValidationRecords []core.ValidationRecord,
317+
experimentFunc func(context.Context) ([]core.ValidationRecord, *corepb.ProblemDetails, error),
318+
) {
310319
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), va.experimentalVATimeout)
311320
defer cancel()
312321

313-
experimentResult, err := experimentFunc(ctx)
322+
experimentValidationRecords, experimentProblem, err := experimentFunc(ctx)
314323

315-
primaryPassed := primary.GetProblem() == nil
316-
experimentPassed := (err == nil) && (experimentResult.GetProblem() == nil)
324+
primaryPassed := primaryProblem == nil
325+
experimentPassed := err == nil && experimentProblem == nil
317326

318327
if primaryPassed == experimentPassed {
319328
va.metrics.experimentConcurrence.WithLabelValues(operation, "true").Inc()
@@ -322,11 +331,11 @@ func (va *ValidationAuthorityImpl) runExperiment(ctx context.Context, operation
322331
va.metrics.experimentConcurrence.WithLabelValues(operation, "false").Inc()
323332

324333
logArgs := map[string]any{
325-
"operation": operation,
326-
"primaryPassed": primaryPassed,
327-
"primaryResult": primary,
328-
"experimentPassed": experimentPassed,
329-
"experimentResult": experimentResult,
334+
"operation": operation,
335+
"primaryProblem": primaryProblem,
336+
"primaryValidationRecords": primaryValidationRecords,
337+
"experimentProblem": experimentProblem,
338+
"experimentValidationRecords": experimentValidationRecords,
330339
}
331340
if err != nil {
332341
logArgs["experimentErr"] = err.Error()
@@ -835,9 +844,22 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV
835844
go va.runExperiment(
836845
ctx,
837846
opDCV,
838-
proto.Clone(localResult).(*vapb.ValidationResult),
839-
func(ctx context.Context) (remoteResult, error) {
840-
return va.experimentalVA.DoDCV(ctx, req)
847+
prob,
848+
records,
849+
func(ctx context.Context) ([]core.ValidationRecord, *corepb.ProblemDetails, error) {
850+
result, err := va.experimentalVA.DoDCV(ctx, req)
851+
if err != nil {
852+
return nil, nil, err
853+
}
854+
var loggableRecords []core.ValidationRecord
855+
for _, record := range result.Records {
856+
converted, err := bgrpc.PBToValidationRecord(record)
857+
if err != nil {
858+
return nil, nil, err
859+
}
860+
loggableRecords = append(loggableRecords, converted)
861+
}
862+
return loggableRecords, result.Problem, nil
841863
})
842864
}
843865

va/va_test.go

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -394,46 +394,41 @@ func TestValidateMalformedChallenge(t *testing.T) {
394394
test.AssertEquals(t, prob.Type, probs.MalformedProblem)
395395
}
396396

397-
func setupWithExperimental(experimentalDNS bdns.Client) (*ValidationAuthorityImpl, *blog.Mock) {
398-
va, mockLog := setup(nil, "", nil, &txtFakeDNS{})
399-
expVA, _ := setup(nil, "", nil, experimentalDNS)
400-
expVA.perspective = "Experimental"
401-
va.experimentalVA = expVA
402-
va.experimentalVASampleRate = 1.0
403-
return va, mockLog
404-
}
405-
406397
func TestExperimentalVAConcurrence(t *testing.T) {
407-
t.Parallel()
398+
testSrvIPv4 := httpSrv(t, expectedToken, false)
399+
t.Cleanup(testSrvIPv4.Close)
408400

409401
tests := []struct {
410402
name string
403+
primaryDNS bdns.Client
411404
experimentalDNS bdns.Client
412-
domain string
413405
primaryProblem bool
414406
expectConcur float64
415407
expectDisagree float64
416408
expectLog string
417409
}{
418410
{
419411
name: "both pass",
420-
experimentalDNS: &txtFakeDNS{},
421-
domain: "good-dns01.com",
412+
primaryDNS: &ipFakeDNS{},
413+
experimentalDNS: &ipFakeDNS{},
422414
expectConcur: 1,
423415
expectDisagree: 0,
424416
},
425417
{
426418
name: "primary passes experimental fails",
427-
experimentalDNS: &bdns.MockClient{},
428-
domain: "good-dns01.com",
419+
primaryDNS: &ipFakeDNS{},
420+
experimentalDNS: &ipFakeDNS{ip: net.IPv4(127, 0, 0, 99)},
429421
expectConcur: 0,
430422
expectDisagree: 1,
431-
expectLog: "Primary VA disagreed with experimental VA",
423+
// The addressesResolved and addressUsed fields are checked here to make sure they are not accidentally
424+
// base64-encoded (which can happen if we log the protobuf `corepb.ValidationRecord` instead of the nicely
425+
// JSON-serializable struct `core.ValidationRecord`)
426+
expectLog: `Primary VA disagreed with experimental VA.*"addressesResolved":\["127.0.0.1"\],"addressUsed":"127.0.0.1"`,
432427
},
433428
{
434429
name: "both fail",
435-
experimentalDNS: &bdns.MockClient{},
436-
domain: "servfail.com",
430+
primaryDNS: &ipFakeDNS{ip: net.IPv4(127, 0, 0, 99)},
431+
experimentalDNS: &ipFakeDNS{ip: net.IPv4(127, 0, 0, 99)},
437432
primaryProblem: true,
438433
expectConcur: 1,
439434
expectDisagree: 0,
@@ -442,15 +437,24 @@ func TestExperimentalVAConcurrence(t *testing.T) {
442437

443438
for _, tc := range tests {
444439
t.Run(tc.name, func(t *testing.T) {
445-
t.Parallel()
446-
va, mockLog := setupWithExperimental(tc.experimentalDNS)
447-
req := createValidationRequest(identifier.NewDNS(tc.domain), core.ChallengeTypeDNS01)
448-
res, err := va.DoDCV(context.Background(), req)
440+
va, mockLog := setup(testSrvIPv4, "", nil, tc.primaryDNS)
441+
expVA, _ := setup(testSrvIPv4, "", nil, tc.experimentalDNS)
442+
expVA.perspective = "Experimental"
443+
va.experimentalVA = expVA
444+
va.experimentalVASampleRate = 1.0
445+
va.experimentalVATimeout = 10 * time.Second
446+
447+
ident := identifier.NewDNS("experiment.example.com")
448+
req := createValidationRequest(ident, core.ChallengeTypeHTTP01)
449+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
450+
defer cancel()
451+
452+
res, err := va.DoDCV(ctx, req)
449453
test.AssertNotError(t, err, "DoDCV failed")
450-
if tc.primaryProblem {
451-
test.Assert(t, res.Problem != nil, "expected a problem")
452-
} else {
453-
test.Assert(t, res.Problem == nil, "expected no problem")
454+
if tc.primaryProblem && res.Problem == nil {
455+
t.Errorf("for primary validation, got: nil Problem, want: any non-nil Problem")
456+
} else if !tc.primaryProblem && res.Problem != nil {
457+
t.Errorf("for primary validation, got: %q, want: nil Problem", res.Problem)
454458
}
455459

456460
time.Sleep(100 * time.Millisecond)
@@ -465,7 +469,10 @@ func TestExperimentalVAConcurrence(t *testing.T) {
465469
}, tc.expectDisagree)
466470

467471
if tc.expectLog != "" {
468-
test.AssertEquals(t, len(mockLog.GetAllMatching(tc.expectLog)), 1)
472+
err := mockLog.ExpectMatch(tc.expectLog)
473+
if err != nil {
474+
t.Error(err)
475+
}
469476
}
470477
})
471478
}

0 commit comments

Comments
 (0)