diff --git a/documentation/provider/vercel.md b/documentation/provider/vercel.md index 05ff207cb..54f8cec17 100644 --- a/documentation/provider/vercel.md +++ b/documentation/provider/vercel.md @@ -144,3 +144,13 @@ Vercel does not allow the record type to be changed after creation. If you try t ### Minimum TTL Vercel enforces a minimum TTL of 60 seconds (1 minute) for all records. We will always silently override the TTL to 60 seconds if you try to set a lower TTL. + +### HTTPS Record ECH Base64 Validation + +Currently, Vercel does implements IETF's "Bootstrapping TLS Encrypted ClientHello with DNS Service Bindings" draft. However, Vercel also implements a validation process for the `ech` parameter in the `HTTPS` records, and will reject the request with the following error message if Vercel considers the `ech` value is invalid: + +``` +Invalid base64 string: [input] (key: ech) +``` + +The detail of Vercel's validation process is unknown, thus we can not support static validation for `dnscontrol check` or `dnscontrol preview`. You should use `ech=` with caution. diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 6492aa357..3eca97b54 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -292,6 +292,18 @@ func makeTests() []*TestGroup { testgroup("Ech", requires(providers.CanUseHTTPS), + not( + // Last tested in 2025-12-04. Turns out that Vercel implements an unknown validation + // on the `ech` parameter, and our dummy base64 string are being rejected with: + // + // Invalid base64 string: [our base64] (key: ech) + // + // Since Vercel's validation process is unknown and not documented, we can't implement + // a rejectif within auditrecord to reject them statically. + // + // Let's just ignore ECH test for Vercel for now. + "VERCEL", + ), tc("Create a HTTPS record", https("@", 1, "example.com.", "alpn=h2,h3")), tc("Add an ECH key", https("@", 1, "example.com.", "alpn=h2,h3 ech=some+base64+encoded+value///")), tc("Ignore the ECH key while changing other values", https("@", 1, "example.net.", "port=80 ech=IGNORE")), diff --git a/providers/vercel/api.go b/providers/vercel/api.go index 46ab01e8b..392c716f2 100644 --- a/providers/vercel/api.go +++ b/providers/vercel/api.go @@ -89,12 +89,13 @@ func (c *vercelProvider) ListDNSRecords(ctx context.Context, domain string) ([]D type httpsRecord struct { Priority int64 `json:"priority"` Target string `json:"target"` - Params string `json:"params,omitempty"` + Params string `json:"params"` } // createDNSRecordRequest embeds the official SDK request but adds HTTPS support type createDNSRecordRequest struct { vercelClient.CreateDNSRecordRequest + Value *string `json:"value,omitempty"` HTTPS *httpsRecord `json:"https,omitempty"` } diff --git a/providers/vercel/vercelProvider.go b/providers/vercel/vercelProvider.go index fd1c7be41..e39fa5d06 100644 --- a/providers/vercel/vercelProvider.go +++ b/providers/vercel/vercelProvider.go @@ -317,7 +317,7 @@ func toVercelCreateRequest(domain string, rc *models.RecordConfig) (createDNSRec } req.Name = name req.Type = rc.Type - req.Value = rc.GetTargetField() + req.Value = ptrString(rc.GetTargetField()) req.TTL = int64(rc.TTL) req.Comment = "" @@ -331,17 +331,24 @@ func toVercelCreateRequest(domain string, rc *models.RecordConfig) (createDNSRec Port: int64(rc.SrvPort), Target: rc.GetTargetField(), } - req.Value = "" // SRV uses the SRV struct, not Value + // When dealing with SRV records, we must not set the Value fields, + // otherwise the API throws an error: + // bad_request - Invalid request: should NOT have additional property `value` + req.Value = nil case "TXT": - req.Value = rc.GetTargetTXTJoined() + req.Value = ptrString(rc.GetTargetTXTJoined()) case "HTTPS": req.HTTPS = &httpsRecord{ Priority: int64(rc.SvcPriority), Target: rc.GetTargetField(), Params: rc.SvcParams, } + // When dealing with HTTPS records, we must not set the Value fields, + // otherwise the API throws an error: + // bad_request - Invalid request: should NOT have additional property `value`. + req.Value = nil case "CAA": - req.Value = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField()) + req.Value = ptrString(fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())) } return req, nil @@ -373,7 +380,10 @@ func toVercelUpdateRequest(rc *models.RecordConfig) (updateDNSRecordRequest, err Port: ptrInt64(int64(rc.SrvPort)), Target: &value, } - req.Value = nil // SRV uses the SRV struct, not Value + // When dealing with SRV records, we must not set the Value fields, + // otherwise the API throws an error: + // bad_request - Invalid request: should NOT have additional property `value` + req.Value = nil case "TXT": txtValue := rc.GetTargetTXTJoined() req.Value = &txtValue @@ -383,6 +393,10 @@ func toVercelUpdateRequest(rc *models.RecordConfig) (updateDNSRecordRequest, err Target: rc.GetTargetField(), Params: rc.SvcParams, } + // When dealing with HTTPS records, we must not set the Value fields, + // otherwise the API throws an error: + // bad_request - Invalid request: should NOT have additional property `value`. + req.Value = nil case "CAA": value := fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField()) req.Value = &value @@ -395,3 +409,7 @@ func toVercelUpdateRequest(rc *models.RecordConfig) (updateDNSRecordRequest, err func ptrInt64(v int64) *int64 { return &v } + +func ptrString(v string) *string { + return &v +}