va: Make the primary VA aware of the Perspective and RIR of each remote (#7839)

- Make the primary VA aware of the expected Perspective and RIR of each
remote VA.
- All Perspectives should be unique, have the primary VA check for
duplicate Perspectives at startup.
- Update test setup functions to ensure that each remote VA client and
corresponding inmem impl have a matching perspective and RIR.

Part of #7819
This commit is contained in:
Samantha Frank 2024-11-25 13:02:03 -05:00 committed by GitHub
parent 7791262815
commit c3948314ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 420 additions and 318 deletions

View File

@ -15,10 +15,44 @@ import (
vapb "github.com/letsencrypt/boulder/va/proto" vapb "github.com/letsencrypt/boulder/va/proto"
) )
// RemoteVAGRPCClientConfig contains the information necessary to setup a gRPC
// client connection. The following GRPC client configuration field combinations
// are allowed:
//
// ServerIPAddresses, [Timeout]
// ServerAddress, DNSAuthority, [Timeout], [HostOverride]
// SRVLookup, DNSAuthority, [Timeout], [HostOverride], [SRVResolver]
// SRVLookups, DNSAuthority, [Timeout], [HostOverride], [SRVResolver]
type RemoteVAGRPCClientConfig struct {
cmd.GRPCClientConfig
// Perspective uniquely identifies the Network Perspective used to
// perform the validation, as specified in BRs Section 5.4.1,
// Requirement 2.7 ("Multi-Perspective Issuance Corroboration attempts
// from each Network Perspective"). It should uniquely identify a group
// of RVAs deployed in the same datacenter.
//
// TODO(#7615): Make mandatory.
Perspective string `validate:"omitempty"`
// RIR indicates the Regional Internet Registry where this RVA is
// located. This field is used to identify the RIR region from which a
// given validation was performed, as specified in the "Phased
// Implementation Timeline" in BRs Section 3.2.2.9. It must be one of
// the following values:
// - ARIN
// - RIPE
// - APNIC
// - LACNIC
// - AfriNIC
//
// TODO(#7615): Make mandatory.
RIR string `validate:"omitempty,oneof=ARIN RIPE APNIC LACNIC AfriNIC"`
}
type Config struct { type Config struct {
VA struct { VA struct {
vaConfig.Common vaConfig.Common
RemoteVAs []cmd.GRPCClientConfig `validate:"omitempty,dive"` RemoteVAs []RemoteVAGRPCClientConfig `validate:"omitempty,dive"`
// Deprecated and ignored // Deprecated and ignored
MaxRemoteValidationFailures int `validate:"omitempty,min=0,required_with=RemoteVAs"` MaxRemoteValidationFailures int `validate:"omitempty,min=0,required_with=RemoteVAs"`
Features features.Config Features features.Config
@ -92,7 +126,7 @@ func main() {
if len(c.VA.RemoteVAs) > 0 { if len(c.VA.RemoteVAs) > 0 {
for _, rva := range c.VA.RemoteVAs { for _, rva := range c.VA.RemoteVAs {
rva := rva rva := rva
vaConn, err := bgrpc.ClientSetup(&rva, tlsConfig, scope, clk) vaConn, err := bgrpc.ClientSetup(&rva.GRPCClientConfig, tlsConfig, scope, clk)
cmd.FailOnError(err, "Unable to create remote VA client") cmd.FailOnError(err, "Unable to create remote VA client")
remotes = append( remotes = append(
remotes, remotes,
@ -102,6 +136,8 @@ func main() {
CAAClient: vapb.NewCAAClient(vaConn), CAAClient: vapb.NewCAAClient(vaConn),
}, },
Address: rva.ServerAddress, Address: rva.ServerAddress,
Perspective: rva.Perspective,
RIR: rva.RIR,
}, },
) )
} }

View File

@ -37,7 +37,7 @@
"http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4000/acme/reg/",
"http://boulder.service.consul:4001/acme/acct/" "http://boulder.service.consul:4001/acme/acct/"
], ],
"perspective": "development", "perspective": "dadaist",
"rir": "ARIN" "rir": "ARIN"
}, },
"syslog": { "syslog": {

View File

@ -37,7 +37,7 @@
"http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4000/acme/reg/",
"http://boulder.service.consul:4001/acme/acct/" "http://boulder.service.consul:4001/acme/acct/"
], ],
"perspective": "development", "perspective": "surrealist",
"rir": "RIPE" "rir": "RIPE"
}, },
"syslog": { "syslog": {

View File

@ -37,7 +37,7 @@
"http://boulder.service.consul:4000/acme/reg/", "http://boulder.service.consul:4000/acme/reg/",
"http://boulder.service.consul:4001/acme/acct/" "http://boulder.service.consul:4001/acme/acct/"
], ],
"perspective": "development", "perspective": "cubist",
"rir": "ARIN" "rir": "ARIN"
}, },
"syslog": { "syslog": {

View File

@ -46,17 +46,23 @@
{ {
"serverAddress": "rva1.service.consul:9397", "serverAddress": "rva1.service.consul:9397",
"timeout": "15s", "timeout": "15s",
"hostOverride": "rva1.boulder" "hostOverride": "rva1.boulder",
"perspective": "dadaist",
"rir": "ARIN"
}, },
{ {
"serverAddress": "rva1.service.consul:9498", "serverAddress": "rva1.service.consul:9498",
"timeout": "15s", "timeout": "15s",
"hostOverride": "rva1.boulder" "hostOverride": "rva1.boulder",
"perspective": "surrealist",
"rir": "RIPE"
}, },
{ {
"serverAddress": "rva1.service.consul:9499", "serverAddress": "rva1.service.consul:9499",
"timeout": "15s", "timeout": "15s",
"hostOverride": "rva1.boulder" "hostOverride": "rva1.boulder",
"perspective": "cubist",
"rir": "ARIN"
} }
], ],
"accountURIPrefixes": [ "accountURIPrefixes": [

View File

@ -590,9 +590,8 @@ func (b caaBrokenDNS) LookupCAA(_ context.Context, domain string) ([]*dns.CAA, s
} }
func TestDisabledMultiCAARechecking(t *testing.T) { func TestDisabledMultiCAARechecking(t *testing.T) {
brokenRVA := setupRemote(nil, "broken", caaBrokenDNS{}, "", "") remoteVAs := []remoteConf{{ua: "broken", rir: arin, dns: caaBrokenDNS{}}}
remoteVAs := []RemoteVA{{brokenRVA, "broken"}} va, _ := setupWithRemotes(nil, "local", remoteVAs, nil)
va, _ := setup(nil, "local", remoteVAs, nil)
features.Set(features.Config{ features.Set(features.Config{
EnforceMultiCAA: false, EnforceMultiCAA: false,
@ -664,15 +663,11 @@ func TestMultiCAARechecking(t *testing.T) {
brokenUA = "broken" brokenUA = "broken"
hijackedUA = "hijacked" hijackedUA = "hijacked"
) )
remoteVA := setupRemote(nil, remoteUA, nil, "", "")
brokenVA := setupRemote(nil, brokenUA, caaBrokenDNS{}, "", "")
// Returns incorrect results
hijackedVA := setupRemote(nil, hijackedUA, caaHijackedDNS{}, "", "")
testCases := []struct { testCases := []struct {
name string name string
domains string domains string
remoteVAs []RemoteVA remoteVAs []remoteConf
expectedProbSubstring string expectedProbSubstring string
expectedProbType probs.ProblemType expectedProbType probs.ProblemType
expectedDiffLogSubstring string expectedDiffLogSubstring string
@ -683,10 +678,10 @@ func TestMultiCAARechecking(t *testing.T) {
name: "all VAs functional, no CAA records", name: "all VAs functional, no CAA records",
domains: "present-dns-only.com", domains: "present-dns-only.com",
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{remoteVA, remoteUA}, {ua: remoteUA, rir: arin},
{remoteVA, remoteUA}, {ua: remoteUA, rir: ripe},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
expectedLabels: prometheus.Labels{ expectedLabels: prometheus.Labels{
"operation": opCAA, "operation": opCAA,
@ -702,10 +697,10 @@ func TestMultiCAARechecking(t *testing.T) {
localDNSClient: caaBrokenDNS{}, localDNSClient: caaBrokenDNS{},
expectedProbSubstring: "While processing CAA for present-dns-only.com: dnsClient is broken", expectedProbSubstring: "While processing CAA for present-dns-only.com: dnsClient is broken",
expectedProbType: probs.DNSProblem, expectedProbType: probs.DNSProblem,
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{remoteVA, remoteUA}, {ua: remoteUA, rir: arin},
{remoteVA, remoteUA}, {ua: remoteUA, rir: ripe},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
expectedLabels: prometheus.Labels{ expectedLabels: prometheus.Labels{
"operation": opCAA, "operation": opCAA,
@ -720,10 +715,10 @@ func TestMultiCAARechecking(t *testing.T) {
domains: "present-dns-only.com", domains: "present-dns-only.com",
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`,
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{brokenVA, brokenUA}, {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: ripe},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
expectedLabels: prometheus.Labels{ expectedLabels: prometheus.Labels{
"operation": opCAA, "operation": opCAA,
@ -740,10 +735,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.DNSProblem, expectedProbType: probs.DNSProblem,
expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{brokenVA, brokenUA}, {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
{brokenVA, brokenUA}, {ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
expectedLabels: prometheus.Labels{ expectedLabels: prometheus.Labels{
"operation": opCAA, "operation": opCAA,
@ -760,10 +755,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.DNSProblem, expectedProbType: probs.DNSProblem,
expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{brokenVA, brokenUA}, {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
{brokenVA, brokenUA}, {ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
{brokenVA, brokenUA}, {ua: brokenUA, rir: apnic, dns: caaBrokenDNS{}},
}, },
expectedLabels: prometheus.Labels{ expectedLabels: prometheus.Labels{
"operation": opCAA, "operation": opCAA,
@ -777,10 +772,10 @@ func TestMultiCAARechecking(t *testing.T) {
name: "all VAs functional, CAA issue type present", name: "all VAs functional, CAA issue type present",
domains: "present.com", domains: "present.com",
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{remoteVA, remoteUA}, {ua: remoteUA, rir: arin},
{remoteVA, remoteUA}, {ua: remoteUA, rir: ripe},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
expectedLabels: prometheus.Labels{ expectedLabels: prometheus.Labels{
"operation": opCAA, "operation": opCAA,
@ -795,10 +790,10 @@ func TestMultiCAARechecking(t *testing.T) {
domains: "present.com", domains: "present.com",
expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{brokenVA, brokenUA}, {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: ripe},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
expectedLabels: prometheus.Labels{ expectedLabels: prometheus.Labels{
"operation": opCAA, "operation": opCAA,
@ -815,10 +810,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.DNSProblem, expectedProbType: probs.DNSProblem,
expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{brokenVA, brokenUA}, {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
{brokenVA, brokenUA}, {ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
expectedLabels: prometheus.Labels{ expectedLabels: prometheus.Labels{
"operation": opCAA, "operation": opCAA,
@ -835,10 +830,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.DNSProblem, expectedProbType: probs.DNSProblem,
expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"broken","Problem":{"type":"dns","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{brokenVA, brokenUA}, {ua: brokenUA, rir: arin, dns: caaBrokenDNS{}},
{brokenVA, brokenUA}, {ua: brokenUA, rir: ripe, dns: caaBrokenDNS{}},
{brokenVA, brokenUA}, {ua: brokenUA, rir: apnic, dns: caaBrokenDNS{}},
}, },
expectedLabels: prometheus.Labels{ expectedLabels: prometheus.Labels{
"operation": opCAA, "operation": opCAA,
@ -857,10 +852,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbSubstring: "CAA record for unsatisfiable.com prevents issuance", expectedProbSubstring: "CAA record for unsatisfiable.com prevents issuance",
expectedProbType: probs.CAAProblem, expectedProbType: probs.CAAProblem,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{remoteVA, remoteUA}, {ua: remoteUA, rir: arin},
{remoteVA, remoteUA}, {ua: remoteUA, rir: ripe},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
}, },
{ {
@ -868,10 +863,10 @@ func TestMultiCAARechecking(t *testing.T) {
domains: "present.com", domains: "present.com",
expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: ripe},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
}, },
{ {
@ -881,10 +876,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.CAAProblem, expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
}, },
{ {
@ -894,10 +889,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.CAAProblem, expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}},
}, },
}, },
{ {
@ -905,10 +900,10 @@ func TestMultiCAARechecking(t *testing.T) {
domains: "satisfiable-wildcard.com", domains: "satisfiable-wildcard.com",
expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: ripe},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
}, },
{ {
@ -918,10 +913,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.CAAProblem, expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
}, },
{ {
@ -931,10 +926,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.CAAProblem, expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}},
}, },
}, },
{ {
@ -942,10 +937,10 @@ func TestMultiCAARechecking(t *testing.T) {
domains: "satisfiable-wildcard.com", domains: "satisfiable-wildcard.com",
expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":2,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: ripe},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
}, },
{ {
@ -955,10 +950,10 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.CAAProblem, expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":1,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
{remoteVA, remoteUA}, {ua: remoteUA, rir: apnic},
}, },
}, },
{ {
@ -968,17 +963,17 @@ func TestMultiCAARechecking(t *testing.T) {
expectedProbType: probs.CAAProblem, expectedProbType: probs.CAAProblem,
expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`, expectedDiffLogSubstring: `RemoteSuccesses":0,"RemoteFailures":[{"VAHostname":"hijacked","Problem":{"type":"caa","detail":"While processing CAA for`,
localDNSClient: caaMockDNS{}, localDNSClient: caaMockDNS{},
remoteVAs: []RemoteVA{ remoteVAs: []remoteConf{
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: arin, dns: caaHijackedDNS{}},
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: ripe, dns: caaHijackedDNS{}},
{hijackedVA, hijackedUA}, {ua: hijackedUA, rir: apnic, dns: caaHijackedDNS{}},
}, },
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
va, mockLog := setup(nil, localUA, tc.remoteVAs, tc.localDNSClient) va, mockLog := setupWithRemotes(nil, localUA, tc.remoteVAs, tc.localDNSClient)
defer mockLog.Clear() defer mockLog.Clear()
// MultiCAAFullResults: false is inherently flaky because of the // MultiCAAFullResults: false is inherently flaky because of the
@ -1011,8 +1006,8 @@ func TestMultiCAARechecking(t *testing.T) {
} }
var invalidRVACount int var invalidRVACount int
for _, x := range va.remoteVAs { for _, x := range tc.remoteVAs {
if x.Address == "broken" || x.Address == "hijacked" { if x.ua == brokenUA || x.ua == hijackedUA {
invalidRVACount++ invalidRVACount++
} }
} }

View File

@ -88,6 +88,8 @@ type RemoteClients struct {
type RemoteVA struct { type RemoteVA struct {
RemoteClients RemoteClients
Address string Address string
Perspective string
RIR string
} }
type vaMetrics struct { type vaMetrics struct {
@ -235,6 +237,15 @@ func NewValidationAuthorityImpl(
return nil, errors.New("no account URI prefixes configured") return nil, errors.New("no account URI prefixes configured")
} }
for i, va1 := range remoteVAs {
for j, va2 := range remoteVAs {
// TODO(#7819): Remove the != "" check once perspective is required.
if i != j && va1.Perspective == va2.Perspective && va1.Perspective != "" {
return nil, fmt.Errorf("duplicate remote VA perspective %q", va1.Perspective)
}
}
}
pc := newDefaultPortConfig() pc := newDefaultPortConfig()
va := &ValidationAuthorityImpl{ va := &ValidationAuthorityImpl{
@ -457,6 +468,8 @@ func (va *ValidationAuthorityImpl) performRemoteValidation(
type response struct { type response struct {
addr string addr string
perspective string
rir string
result *vapb.ValidationResult result *vapb.ValidationResult
err error err error
} }
@ -468,7 +481,7 @@ func (va *ValidationAuthorityImpl) performRemoteValidation(
for _, i := range rand.Perm(remoteVACount) { for _, i := range rand.Perm(remoteVACount) {
go func(rva RemoteVA) { go func(rva RemoteVA) {
res, err := rva.PerformValidation(subCtx, req) res, err := rva.PerformValidation(subCtx, req)
responses <- &response{rva.Address, res, err} responses <- &response{rva.Address, rva.Perspective, rva.RIR, res, err}
}(va.remoteVAs[i]) }(va.remoteVAs[i])
} }
@ -482,7 +495,7 @@ func (va *ValidationAuthorityImpl) performRemoteValidation(
if resp.err != nil { if resp.err != nil {
// Failed to communicate with the remote VA. // Failed to communicate with the remote VA.
failed = append(failed, resp.addr) failed = append(failed, resp.perspective)
if core.IsCanceled(resp.err) { if core.IsCanceled(resp.err) {
currProb = probs.ServerInternal("Secondary domain validation RPC canceled") currProb = probs.ServerInternal("Secondary domain validation RPC canceled")
@ -492,7 +505,7 @@ func (va *ValidationAuthorityImpl) performRemoteValidation(
} }
} else if resp.result.Problems != nil { } else if resp.result.Problems != nil {
// The remote VA returned a problem. // The remote VA returned a problem.
failed = append(failed, resp.result.Perspective) failed = append(failed, resp.perspective)
var err error var err error
currProb, err = bgrpc.PBToProblemDetails(resp.result.Problems) currProb, err = bgrpc.PBToProblemDetails(resp.result.Problems)
@ -502,7 +515,7 @@ func (va *ValidationAuthorityImpl) performRemoteValidation(
} }
} else { } else {
// The remote VA returned a successful result. // The remote VA returned a successful result.
passed = append(passed, resp.result.Perspective) passed = append(passed, resp.perspective)
} }
if firstProb == nil && currProb != nil { if firstProb == nil && currProb != nil {

View File

@ -162,6 +162,67 @@ func setupRemote(srv *httptest.Server, userAgent string, mockDNSClientOverride b
return RemoteClients{VAClient: &inMemVA{*rva}, CAAClient: &inMemVA{*rva}} return RemoteClients{VAClient: &inMemVA{*rva}, CAAClient: &inMemVA{*rva}}
} }
// RIRs
const (
arin = "ARIN"
ripe = "RIPE"
apnic = "APNIC"
lacnic = "LACNIC"
afrinic = "AFRINIC"
)
// remoteConf is used in conjunction with setupRemotes/withRemotes to configure
// a remote VA.
type remoteConf struct {
// ua is optional, will default to "user agent 1.0". When set to "broken" or
// "hijacked", the Address field of the resulting RemoteVA will be set to
// match. This is a bit hacky, but it's the easiest way to satisfy some of
// our existing TestMultiCAARechecking tests.
ua string
// rir is required.
rir string
// dns is optional.
dns bdns.Client
// impl is optional.
impl RemoteClients
}
func setupRemotes(confs []remoteConf, srv *httptest.Server) []RemoteVA {
remoteVAs := make([]RemoteVA, 0, len(confs))
for i, c := range confs {
if c.rir == "" {
panic("rir is required")
}
// perspective MUST be unique for each remote VA, otherwise the VA will
// fail to start.
perspective := fmt.Sprintf("dc-%d-%s", i, c.rir)
clients := setupRemote(srv, c.ua, c.dns, perspective, c.rir)
if c.impl != (RemoteClients{}) {
clients = c.impl
}
var address string
if c.ua == "broken" {
address = "broken"
}
if c.ua == "hijacked" {
address = "hijacked"
}
remoteVAs = append(remoteVAs, RemoteVA{
Address: address,
RemoteClients: clients,
Perspective: perspective,
RIR: c.rir,
})
}
return remoteVAs
}
func setupWithRemotes(srv *httptest.Server, userAgent string, remotes []remoteConf, mockDNSClientOverride bdns.Client) (*ValidationAuthorityImpl, *blog.Mock) {
remoteVAs := setupRemotes(remotes, srv)
return setup(srv, userAgent, remoteVAs, mockDNSClientOverride)
}
type multiSrv struct { type multiSrv struct {
*httptest.Server *httptest.Server
@ -169,13 +230,10 @@ type multiSrv struct {
allowedUAs map[string]bool allowedUAs map[string]bool
} }
func (s *multiSrv) setAllowedUAs(allowedUAs map[string]bool) { const (
s.mu.Lock() slowUA = "slow remote"
defer s.mu.Unlock() slowRemoteSleepMillis = 1000
s.allowedUAs = allowedUAs )
}
const slowRemoteSleepMillis = 1000
func httpMultiSrv(t *testing.T, token string, allowedUAs map[string]bool) *multiSrv { func httpMultiSrv(t *testing.T, token string, allowedUAs map[string]bool) *multiSrv {
t.Helper() t.Helper()
@ -185,7 +243,7 @@ func httpMultiSrv(t *testing.T, token string, allowedUAs map[string]bool) *multi
ms := &multiSrv{server, sync.Mutex{}, allowedUAs} ms := &multiSrv{server, sync.Mutex{}, allowedUAs}
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.UserAgent() == "slow remote" { if r.UserAgent() == slowUA {
time.Sleep(slowRemoteSleepMillis) time.Sleep(slowRemoteSleepMillis)
} }
ms.mu.Lock() ms.mu.Lock()
@ -248,6 +306,32 @@ func (inmem inMemVA) IsCAAValid(ctx context.Context, req *vapb.IsCAAValidRequest
return inmem.rva.IsCAAValid(ctx, req) return inmem.rva.IsCAAValid(ctx, req)
} }
func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) {
var remoteVAs []RemoteVA
for i := 0; i < 3; i++ {
remoteVAs = append(remoteVAs, RemoteVA{
RemoteClients: setupRemote(nil, "", nil, "dadaist", arin),
Perspective: "dadaist",
RIR: arin,
})
}
_, err := NewValidationAuthorityImpl(
&bdns.MockClient{Log: blog.NewMock()},
remoteVAs,
"user agent 1.0",
"letsencrypt.org",
metrics.NoopRegisterer,
clock.NewFake(),
blog.NewMock(),
accountURIPrefixes,
"example perspective",
"",
)
test.AssertError(t, err, "NewValidationAuthorityImpl allowed duplicate remote perspectives")
test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"")
}
func TestValidateMalformedChallenge(t *testing.T) { func TestValidateMalformedChallenge(t *testing.T) {
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
@ -366,37 +450,11 @@ func TestDCVAndCAASequencing(t *testing.T) {
} }
func TestMultiVA(t *testing.T) { func TestMultiVA(t *testing.T) {
t.Parallel()
// Create a new challenge to use for the httpSrv // Create a new challenge to use for the httpSrv
req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) req := createValidationRequest("localhost", core.ChallengeTypeHTTP01)
const (
remoteUA1 = "remote 1"
remoteUA2 = "remote 2"
remoteUA3 = "remote 3"
remoteUA4 = "remote 4"
localUA = "local 1"
)
allowedUAs := map[string]bool{
localUA: true,
remoteUA1: true,
remoteUA2: true,
remoteUA3: true,
remoteUA4: true,
}
// Create an IPv4 test server
ms := httpMultiSrv(t, expectedToken, allowedUAs)
defer ms.Close()
remoteVA1 := setupRemote(ms.Server, remoteUA1, nil, "", "")
remoteVA2 := setupRemote(ms.Server, remoteUA2, nil, "", "")
remoteVA3 := setupRemote(ms.Server, remoteUA3, nil, "", "")
remoteVA4 := setupRemote(ms.Server, remoteUA4, nil, "", "")
remoteVAs := []RemoteVA{
{remoteVA1, remoteUA1},
{remoteVA2, remoteUA2},
{remoteVA3, remoteUA3},
}
brokenVA := RemoteClients{ brokenVA := RemoteClients{
VAClient: brokenRemoteVA{}, VAClient: brokenRemoteVA{},
CAAClient: brokenRemoteVA{}, CAAClient: brokenRemoteVA{},
@ -406,176 +464,187 @@ func TestMultiVA(t *testing.T) {
CAAClient: cancelledVA{}, CAAClient: cancelledVA{},
} }
unauthorized := probs.Unauthorized(fmt.Sprintf(
`The key authorization file from the server did not match this challenge. Expected %q (got "???")`,
expectedKeyAuthorization))
expectedInternalErrLine := fmt.Sprintf(
`ERR: \[AUDIT\] Remote VA "broken".PerformValidation failed: %s`,
errBrokenRemoteVA.Error())
testCases := []struct { testCases := []struct {
Name string Name string
RemoteVAs []RemoteVA Remotes []remoteConf
AllowedUAs map[string]bool PrimaryUA string
ExpectedProb *probs.ProblemDetails ExpectedProbType string
ExpectedLog string ExpectedLogContains string
}{ }{
{ {
// With local and all remote VAs working there should be no problem. // With local and all remote VAs working there should be no problem.
Name: "Local and remote VAs OK", Name: "Local and remote VAs OK",
RemoteVAs: remoteVAs, Remotes: []remoteConf{
AllowedUAs: allowedUAs, {ua: pass, rir: arin},
{ua: pass, rir: ripe},
{ua: pass, rir: apnic},
},
PrimaryUA: pass,
}, },
{ {
// If the local VA fails everything should fail // If the local VA fails everything should fail
Name: "Local VA bad, remote VAs OK", Name: "Local VA bad, remote VAs OK",
RemoteVAs: remoteVAs, Remotes: []remoteConf{
AllowedUAs: map[string]bool{remoteUA1: true, remoteUA2: true}, {ua: pass, rir: arin},
ExpectedProb: unauthorized, {ua: pass, rir: ripe},
{ua: pass, rir: apnic},
},
PrimaryUA: fail,
ExpectedProbType: string(probs.UnauthorizedProblem),
}, },
{ {
// If one out of three remote VAs fails with an internal err it should succeed // If one out of three remote VAs fails with an internal err it should succeed
Name: "Local VA ok, 1/3 remote VA internal err", Name: "Local VA ok, 1/3 remote VA internal err",
RemoteVAs: []RemoteVA{ Remotes: []remoteConf{
{remoteVA1, remoteUA1}, {ua: pass, rir: arin},
{remoteVA2, remoteUA2}, {ua: pass, rir: ripe},
{brokenVA, "broken"}, {ua: pass, rir: apnic, impl: brokenVA},
}, },
AllowedUAs: allowedUAs, PrimaryUA: pass,
}, },
{ {
// If two out of three remote VAs fail with an internal err it should fail // If two out of three remote VAs fail with an internal err it should fail
Name: "Local VA ok, 2/3 remote VAs internal err", Name: "Local VA ok, 2/3 remote VAs internal err",
RemoteVAs: []RemoteVA{ Remotes: []remoteConf{
{remoteVA1, remoteUA1}, {ua: pass, rir: arin},
{brokenVA, "broken"}, {ua: pass, rir: ripe, impl: brokenVA},
{brokenVA, "broken"}, {ua: pass, rir: apnic, impl: brokenVA},
}, },
AllowedUAs: allowedUAs, PrimaryUA: pass,
ExpectedProb: probs.ServerInternal("During secondary domain validation: Secondary domain validation RPC failed"), ExpectedProbType: string(probs.ServerInternalProblem),
// The real failure cause should be logged // The real failure cause should be logged
ExpectedLog: expectedInternalErrLine, ExpectedLogContains: errBrokenRemoteVA.Error(),
}, },
{ {
// If one out of five remote VAs fail with an internal err it should succeed // If one out of five remote VAs fail with an internal err it should succeed
Name: "Local VA ok, 1/5 remote VAs internal err", Name: "Local VA ok, 1/5 remote VAs internal err",
RemoteVAs: []RemoteVA{ Remotes: []remoteConf{
{remoteVA1, remoteUA1}, {ua: pass, rir: arin},
{remoteVA2, remoteUA2}, {ua: pass, rir: ripe},
{remoteVA3, remoteUA3}, {ua: pass, rir: apnic},
{remoteVA4, remoteUA4}, {ua: pass, rir: lacnic},
{brokenVA, "broken"}, {ua: pass, rir: afrinic, impl: brokenVA},
}, },
AllowedUAs: allowedUAs, PrimaryUA: pass,
}, },
{ {
// If two out of five remote VAs fail with an internal err it should fail // If two out of five remote VAs fail with an internal err it should fail
Name: "Local VA ok, 2/5 remote VAs internal err", Name: "Local VA ok, 2/5 remote VAs internal err",
RemoteVAs: []RemoteVA{ Remotes: []remoteConf{
{remoteVA1, remoteUA1}, {ua: pass, rir: arin},
{remoteVA2, remoteUA2}, {ua: pass, rir: ripe},
{remoteVA3, remoteUA3}, {ua: pass, rir: apnic},
{brokenVA, "broken"}, {ua: pass, rir: arin, impl: brokenVA},
{brokenVA, "broken"}, {ua: pass, rir: ripe, impl: brokenVA},
}, },
AllowedUAs: allowedUAs, PrimaryUA: pass,
ExpectedProb: probs.ServerInternal("During secondary domain validation: Secondary domain validation RPC failed"), ExpectedProbType: string(probs.ServerInternalProblem),
// The real failure cause should be logged // The real failure cause should be logged
ExpectedLog: expectedInternalErrLine, ExpectedLogContains: errBrokenRemoteVA.Error(),
}, },
{ {
// If two out of six remote VAs fail with an internal err it should succeed // If two out of six remote VAs fail with an internal err it should succeed
Name: "Local VA ok, 2/6 remote VAs internal err", Name: "Local VA ok, 2/6 remote VAs internal err",
RemoteVAs: []RemoteVA{ Remotes: []remoteConf{
{remoteVA1, remoteUA1}, {ua: pass, rir: arin},
{remoteVA2, remoteUA2}, {ua: pass, rir: ripe},
{remoteVA3, remoteUA3}, {ua: pass, rir: apnic},
{remoteVA4, remoteUA4}, {ua: pass, rir: lacnic},
{brokenVA, "broken"}, {ua: pass, rir: afrinic, impl: brokenVA},
{brokenVA, "broken"}, {ua: pass, rir: arin, impl: brokenVA},
}, },
AllowedUAs: allowedUAs, PrimaryUA: pass,
}, },
{ {
// If three out of six remote VAs fail with an internal err it should fail // If three out of six remote VAs fail with an internal err it should fail
Name: "Local VA ok, 4/6 remote VAs internal err", Name: "Local VA ok, 4/6 remote VAs internal err",
RemoteVAs: []RemoteVA{ Remotes: []remoteConf{
{remoteVA1, remoteUA1}, {ua: pass, rir: arin},
{remoteVA2, remoteUA2}, {ua: pass, rir: ripe},
{remoteVA3, remoteUA3}, {ua: pass, rir: apnic},
{brokenVA, "broken"}, {ua: pass, rir: lacnic, impl: brokenVA},
{brokenVA, "broken"}, {ua: pass, rir: afrinic, impl: brokenVA},
{brokenVA, "broken"}, {ua: pass, rir: arin, impl: brokenVA},
}, },
AllowedUAs: allowedUAs, PrimaryUA: pass,
ExpectedProb: probs.ServerInternal("During secondary domain validation: Secondary domain validation RPC failed"), ExpectedProbType: string(probs.ServerInternalProblem),
// The real failure cause should be logged // The real failure cause should be logged
ExpectedLog: expectedInternalErrLine, ExpectedLogContains: errBrokenRemoteVA.Error(),
}, },
{ {
// With only one working remote VA there should be a validation failure // With only one working remote VA there should be a validation failure
Name: "Local VA and one remote VA OK", Name: "Local VA and one remote VA OK",
RemoteVAs: remoteVAs, Remotes: []remoteConf{
AllowedUAs: map[string]bool{localUA: true, remoteUA2: true}, {ua: pass, rir: arin},
ExpectedProb: probs.Unauthorized(fmt.Sprintf( {ua: fail, rir: ripe},
`During secondary domain validation: The key authorization file from the server did not match this challenge. Expected %q (got "???")`, {ua: fail, rir: apnic},
expectedKeyAuthorization)), },
PrimaryUA: pass,
ExpectedProbType: string(probs.UnauthorizedProblem),
ExpectedLogContains: "During secondary domain validation: The key authorization file from the server",
}, },
{ {
// If one remote VA cancels, it should succeed // If one remote VA cancels, it should succeed
Name: "Local VA and one remote VA OK, one cancelled VA", Name: "Local VA and one remote VA OK, one cancelled VA",
RemoteVAs: []RemoteVA{ Remotes: []remoteConf{
{remoteVA1, remoteUA1}, {ua: pass, rir: arin},
{cancelledVA, remoteUA2}, {ua: pass, rir: ripe, impl: cancelledVA},
{remoteVA3, remoteUA3}, {ua: pass, rir: apnic},
}, },
AllowedUAs: allowedUAs, PrimaryUA: pass,
}, },
{ {
// If all remote VAs cancel, it should fail // If all remote VAs cancel, it should fail
Name: "Local VA OK, three cancelled remote VAs", Name: "Local VA OK, three cancelled remote VAs",
RemoteVAs: []RemoteVA{ Remotes: []remoteConf{
{cancelledVA, remoteUA1}, {ua: pass, rir: arin, impl: cancelledVA},
{cancelledVA, remoteUA2}, {ua: pass, rir: ripe, impl: cancelledVA},
{cancelledVA, remoteUA3}, {ua: pass, rir: apnic, impl: cancelledVA},
}, },
AllowedUAs: allowedUAs, PrimaryUA: pass,
ExpectedProb: probs.ServerInternal("During secondary domain validation: Secondary domain validation RPC canceled"), ExpectedProbType: string(probs.ServerInternalProblem),
ExpectedLogContains: "During secondary domain validation: Secondary domain validation RPC canceled",
}, },
{ {
// With the local and remote VAs seeing diff problems, we expect a problem. // With the local and remote VAs seeing diff problems, we expect a problem.
Name: "Local and remote VA differential, full results, enforce multi VA", Name: "Local and remote VA differential, full results, enforce multi VA",
RemoteVAs: remoteVAs, Remotes: []remoteConf{
AllowedUAs: map[string]bool{localUA: true}, {ua: fail, rir: arin},
ExpectedProb: probs.Unauthorized(fmt.Sprintf( {ua: fail, rir: ripe},
`During secondary domain validation: The key authorization file from the server did not match this challenge. Expected %q (got "???")`, {ua: fail, rir: apnic},
expectedKeyAuthorization)), },
PrimaryUA: pass,
ExpectedProbType: string(probs.UnauthorizedProblem),
ExpectedLogContains: "During secondary domain validation: The key authorization file from the server",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) { t.Run(tc.Name, func(t *testing.T) {
// Configure the test server with the testcase allowed UAs. t.Parallel()
ms.setAllowedUAs(tc.AllowedUAs)
// Configure one test server per test case so that all tests can run in parallel.
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
defer ms.Close()
// Configure a primary VA with testcase remote VAs. // Configure a primary VA with testcase remote VAs.
localVA, mockLog := setup(ms.Server, localUA, tc.RemoteVAs, nil) localVA, mockLog := setupWithRemotes(ms.Server, tc.PrimaryUA, tc.Remotes, nil)
// Perform all validations // Perform all validations
res, _ := localVA.PerformValidation(ctx, req) res, _ := localVA.PerformValidation(ctx, req)
if res.Problems == nil && tc.ExpectedProb != nil { if res.Problems == nil && tc.ExpectedProbType != "" {
t.Errorf("expected prob %v, got nil", tc.ExpectedProb) t.Errorf("expected prob %v, got nil", tc.ExpectedProbType)
} else if res.Problems != nil && tc.ExpectedProb == nil { } else if res.Problems != nil && tc.ExpectedProbType == "" {
t.Errorf("expected no prob, got %v", res.Problems) t.Errorf("expected no prob, got %v", res.Problems)
} else if res.Problems != nil && tc.ExpectedProb != nil { } else if res.Problems != nil && tc.ExpectedProbType != "" {
// That result should match expected. // That result should match expected.
test.AssertEquals(t, res.Problems.ProblemType, string(tc.ExpectedProb.Type)) test.AssertEquals(t, res.Problems.ProblemType, tc.ExpectedProbType)
test.AssertEquals(t, res.Problems.Detail, tc.ExpectedProb.Detail)
} }
if tc.ExpectedLog != "" { if tc.ExpectedLogContains != "" {
lines := mockLog.GetAllMatching(tc.ExpectedLog) lines := mockLog.GetAllMatching(tc.ExpectedLogContains)
if len(lines) == 0 { if len(lines) == 0 {
t.Fatalf("Got log %v; expected %q", mockLog.GetAll(), tc.ExpectedLog) t.Fatalf("Got log %v; expected %q", mockLog.GetAll(), tc.ExpectedLogContains)
} }
} }
}) })
@ -583,39 +652,48 @@ func TestMultiVA(t *testing.T) {
} }
func TestMultiVAEarlyReturn(t *testing.T) { func TestMultiVAEarlyReturn(t *testing.T) {
const passUA = "pass" t.Parallel()
const failUA = "fail"
// httpMultiSrv handles this specially by being slow
const slowRemoteUA = "slow remote"
allowedUAs := map[string]bool{
passUA: true,
}
ms := httpMultiSrv(t, expectedToken, allowedUAs)
defer ms.Close()
makeRemotes := func(userAgent ...string) []RemoteVA {
var rvas []RemoteVA
for i, ua := range userAgent {
clients := setupRemote(ms.Server, ua, nil, "", "")
rva := RemoteVA{clients, fmt.Sprintf("remote VA %d hostname", i)}
rvas = append(rvas, rva)
}
return rvas
}
testCases := []struct { testCases := []struct {
remoteUserAgents []string remoteConfs []remoteConf
}{ }{
{remoteUserAgents: []string{slowRemoteUA, passUA, failUA}}, {
{remoteUserAgents: []string{slowRemoteUA, slowRemoteUA, passUA, passUA, failUA}}, remoteConfs: []remoteConf{
{remoteUserAgents: []string{slowRemoteUA, slowRemoteUA, passUA, passUA, failUA, failUA}}, {ua: slowUA, rir: arin},
{ua: pass, rir: ripe},
{ua: fail, rir: apnic},
},
},
{
remoteConfs: []remoteConf{
{ua: slowUA, rir: arin},
{ua: slowUA, rir: ripe},
{ua: pass, rir: apnic},
{ua: pass, rir: arin},
{ua: fail, rir: ripe},
},
},
{
remoteConfs: []remoteConf{
{ua: slowUA, rir: arin},
{ua: slowUA, rir: ripe},
{ua: pass, rir: apnic},
{ua: pass, rir: arin},
{ua: fail, rir: ripe},
{ua: fail, rir: apnic},
},
},
} }
for i, tc := range testCases { for i, tc := range testCases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
rvas := makeRemotes(tc.remoteUserAgents...) t.Parallel()
localVA, _ := setup(ms.Server, pass, rvas, nil)
// Configure one test server per test case so that all tests can run in parallel.
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
defer ms.Close()
localVA, _ := setupWithRemotes(ms.Server, pass, tc.remoteConfs, nil)
// Perform all validations // Perform all validations
start := time.Now() start := time.Now()
@ -643,35 +721,19 @@ func TestMultiVAEarlyReturn(t *testing.T) {
} }
func TestMultiVAPolicy(t *testing.T) { func TestMultiVAPolicy(t *testing.T) {
const ( t.Parallel()
remoteUA1 = "remote 1"
remoteUA2 = "remote 2"
remoteUA3 = "remote 3"
localUA = "local 1"
)
// Forbid all remote UAs to ensure that multi-va fails
allowedUAs := map[string]bool{
localUA: true,
remoteUA1: false,
remoteUA2: false,
remoteUA3: false,
}
ms := httpMultiSrv(t, expectedToken, allowedUAs) ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
defer ms.Close() defer ms.Close()
remoteVA1 := setupRemote(ms.Server, remoteUA1, nil, "", "") remoteConfs := []remoteConf{
remoteVA2 := setupRemote(ms.Server, remoteUA2, nil, "", "") {ua: fail, rir: arin},
remoteVA3 := setupRemote(ms.Server, remoteUA3, nil, "", "") {ua: fail, rir: ripe},
{ua: fail, rir: apnic},
remoteVAs := []RemoteVA{
{remoteVA1, remoteUA1},
{remoteVA2, remoteUA2},
{remoteVA3, remoteUA3},
} }
// Create a local test VA with the two remote VAs // Create a local test VA with the remote VAs
localVA, _ := setup(ms.Server, localUA, remoteVAs, nil) localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
// Perform validation for a domain not in the disabledDomains list // Perform validation for a domain not in the disabledDomains list
req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01) req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01)
@ -681,28 +743,19 @@ func TestMultiVAPolicy(t *testing.T) {
t.Error("expected prob from PerformValidation, got nil") t.Error("expected prob from PerformValidation, got nil")
} }
} }
func TestMultiVALogging(t *testing.T) { func TestMultiVALogging(t *testing.T) {
const ( t.Parallel()
rva1UA = "remote 1"
rva2UA = "remote 2"
rva3UA = "remote 3"
localUA = "local 1"
)
ms := httpMultiSrv(t, expectedToken, map[string]bool{localUA: true, rva1UA: true, rva2UA: true}) ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
defer ms.Close() defer ms.Close()
rva1 := setupRemote(ms.Server, rva1UA, nil, "dev-arin", "ARIN") remoteConfs := []remoteConf{
rva2 := setupRemote(ms.Server, rva2UA, nil, "dev-ripe", "RIPE") {ua: pass, rir: arin},
rva3 := setupRemote(ms.Server, rva3UA, nil, "dev-ripe", "RIPE") {ua: pass, rir: ripe},
{ua: pass, rir: apnic},
remoteVAs := []RemoteVA{
{rva1, rva1UA},
{rva2, rva2UA},
{rva3, rva3UA},
} }
va, _ := setup(ms.Server, localUA, remoteVAs, nil)
va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01) req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01)
res, err := va.PerformValidation(ctx, req) res, err := va.PerformValidation(ctx, req)
test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed with: %#v", res.Problems)) test.Assert(t, res.Problems == nil, fmt.Sprintf("validation failed with: %#v", res.Problems))
@ -762,19 +815,12 @@ func TestDetailedError(t *testing.T) {
} }
func TestLogRemoteDifferentials(t *testing.T) { func TestLogRemoteDifferentials(t *testing.T) {
// Create some remote VAs remoteConfs := []remoteConf{
remoteVA1 := setupRemote(nil, "remote 1", nil, "", "") {ua: pass, rir: arin},
remoteVA2 := setupRemote(nil, "remote 2", nil, "", "") {ua: pass, rir: ripe},
remoteVA3 := setupRemote(nil, "remote 3", nil, "", "") {ua: pass, rir: apnic},
// The VA will allow a max of 1 remote failure based on MPIC.
remoteVAs := []RemoteVA{
{remoteVA1, "remote 1"},
{remoteVA2, "remote 2"},
{remoteVA3, "remote 3"},
} }
localVA, mockLog := setup(nil, "local 1", remoteVAs, nil)
egProbA := probs.DNS("root DNS servers closed at 4:30pm") egProbA := probs.DNS("root DNS servers closed at 4:30pm")
egProbB := probs.OrderNotReady("please take a number") egProbB := probs.OrderNotReady("please take a number")
@ -813,7 +859,13 @@ func TestLogRemoteDifferentials(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
mockLog.Clear() t.Parallel()
// Configure one test server per test case so that all tests can run in parallel.
ms := httpMultiSrv(t, expectedToken, map[string]bool{pass: true, fail: false})
defer ms.Close()
localVA, mockLog := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
localVA.logRemoteResults( localVA.logRemoteResults(
"example.com", 1999, "blorpus-01", tc.remoteProbs) "example.com", 1999, "blorpus-01", tc.remoteProbs)