Browse Source

Implement CAA record support in GeoDNS

Co-authored-by: abh <[email protected]>
copilot-swe-agent[bot] 4 months ago
parent
commit
8889dfad52
3 changed files with 150 additions and 0 deletions
  1. 17 0
      README.md
  2. 36 0
      zones/reader.go
  3. 97 0
      zones/reader_test.go

+ 17 - 0
README.md

@@ -256,6 +256,23 @@ Internally resolved cname, of sorts. Only works internally in a zone.
 
 
 The target will have the current zone name appended if it's not a FQDN (since v2.2.0).
 The target will have the current zone name appended if it's not a FQDN (since v2.2.0).
 
 
+### CAA
+
+CAA (Certificate Authority Authorization) records allow domain owners to specify which Certificate Authorities are authorized to issue certificates for their domain.
+
+Each CAA record has a flag, tag, and value:
+
+    { "tag": "issue", "value": "ca.example.net" }
+    { "flag": 0, "tag": "issuewild", "value": "ca.example.net" }
+    { "flag": 128, "tag": "iodef", "value": "mailto:[email protected]", "weight": 100 }
+
+The `flag` field is optional and defaults to 0. Common tags include:
+- `issue`: Authorizes the specified CA to issue certificates for this domain
+- `issuewild`: Authorizes the specified CA to issue wildcard certificates for this domain  
+- `iodef`: Specifies a URL or email address for reporting certificate issue violations
+
+The `weight` field is optional and works the same as other record types.
+
 ### MX
 ### MX
 
 
 MX records support a `weight` similar to A records to indicate how often the particular
 MX records support a `weight` similar to A records to indicate how often the particular

+ 36 - 0
zones/reader.go

@@ -155,6 +155,7 @@ func setupZoneData(data map[string]interface{}, zone *Zone) {
 		"a":     dns.TypeA,
 		"a":     dns.TypeA,
 		"aaaa":  dns.TypeAAAA,
 		"aaaa":  dns.TypeAAAA,
 		"alias": dns.TypeMF,
 		"alias": dns.TypeMF,
+		"caa":   dns.TypeCAA,
 		"cname": dns.TypeCNAME,
 		"cname": dns.TypeCNAME,
 		"mx":    dns.TypeMX,
 		"mx":    dns.TypeMX,
 		"ns":    dns.TypeNS,
 		"ns":    dns.TypeNS,
@@ -472,6 +473,41 @@ func setupZoneData(data map[string]interface{}, zone *Zone) {
 						continue
 						continue
 					}
 					}
 
 
+				case dns.TypeCAA:
+					rec := records[rType][i].(map[string]interface{})
+					
+					var flag uint8 = 0
+					var tag, value string
+					
+					if rec["flag"] != nil {
+						flag = uint8(typeutil.ToInt(rec["flag"]))
+					}
+					
+					if rec["tag"] != nil {
+						tag = rec["tag"].(string)
+					} else {
+						log.Printf("CAA record missing required 'tag' field for '%s' in '%s'\n", label.Label, zone.Origin)
+						continue
+					}
+					
+					if rec["value"] != nil {
+						value = rec["value"].(string)
+					} else {
+						log.Printf("CAA record missing required 'value' field for '%s' in '%s'\n", label.Label, zone.Origin)
+						continue
+					}
+					
+					if rec["weight"] != nil {
+						record.Weight = typeutil.ToInt(rec["weight"])
+					}
+					
+					record.RR = &dns.CAA{
+						Hdr:   h,
+						Flag:  flag,
+						Tag:   tag,
+						Value: value,
+					}
+
 				default:
 				default:
 					log.Println("type:", rType)
 					log.Println("type:", rType)
 					panic("Don't know how to handle this type")
 					panic("Don't know how to handle this type")

+ 97 - 0
zones/reader_test.go

@@ -8,6 +8,7 @@ import (
 
 
 	"github.com/abh/geodns/v3/targeting"
 	"github.com/abh/geodns/v3/targeting"
 	"github.com/abh/geodns/v3/targeting/geoip2"
 	"github.com/abh/geodns/v3/targeting/geoip2"
+	"github.com/miekg/dns"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
 )
 )
 
 
@@ -161,3 +162,99 @@ func CopyFile(src, dst string) (int64, error) {
 	defer df.Close()
 	defer df.Close()
 	return io.Copy(df, sf)
 	return io.Copy(df, sf)
 }
 }
+
+func TestCAARecords(t *testing.T) {
+	// Create test data inline
+	jsonData := map[string]interface{}{
+		"": map[string]interface{}{
+			"ns": map[string]interface{}{
+				"ns1.example.com.": nil,
+				"ns2.example.com.": nil,
+			},
+			"caa": []interface{}{
+				map[string]interface{}{
+					"flag":  float64(0),
+					"tag":   "issue",
+					"value": "ca.example.net",
+				},
+				map[string]interface{}{
+					"tag":   "issuewild",
+					"value": "ca.example.net",
+				},
+				map[string]interface{}{
+					"flag":   float64(128),
+					"tag":    "iodef",
+					"value":  "mailto:[email protected]",
+					"weight": float64(100),
+				},
+			},
+		},
+	}
+
+	// Create a test zone
+	zone := &Zone{
+		Origin:  "test.com",
+		Labels:  make(map[string]*Label),
+		Options: ZoneOptions{Ttl: 600},
+	}
+
+	// Set up zone data
+	setupZoneData(jsonData, zone)
+
+	// Verify the apex label was created
+	apexLabel, exists := zone.Labels[""]
+	if !exists {
+		t.Fatal("Apex label not found")
+	}
+
+	// Verify CAA records exist
+	caaRecords, exists := apexLabel.Records[dns.TypeCAA]
+	if !exists {
+		t.Fatal("CAA records not found")
+	}
+
+	// Should have 3 CAA records
+	assert.Equal(t, 3, len(caaRecords), "Expected 3 CAA records")
+
+	// Debug: print the actual records
+	for i, record := range caaRecords {
+		caa := record.RR.(*dns.CAA)
+		t.Logf("CAA record %d: flag=%d, tag=%s, value=%s, weight=%d", i, caa.Flag, caa.Tag, caa.Value, record.Weight)
+	}
+
+	// Test records - order may vary based on JSON parsing
+	var issue, issuewild, iodef *dns.CAA
+	var iodefWeight int
+	
+	for _, record := range caaRecords {
+		caa := record.RR.(*dns.CAA)
+		switch caa.Tag {
+		case "issue":
+			issue = caa
+		case "issuewild":
+			issuewild = caa
+		case "iodef":
+			iodef = caa
+			iodefWeight = record.Weight
+		}
+	}
+
+	// Test issue CAA record
+	assert.NotNil(t, issue, "Issue CAA record should exist")
+	assert.Equal(t, uint8(0), issue.Flag, "Issue CAA record flag should be 0")
+	assert.Equal(t, "issue", issue.Tag, "Issue CAA record tag should be 'issue'")
+	assert.Equal(t, "ca.example.net", issue.Value, "Issue CAA record value should be 'ca.example.net'")
+
+	// Test issuewild CAA record
+	assert.NotNil(t, issuewild, "Issuewild CAA record should exist")
+	assert.Equal(t, uint8(0), issuewild.Flag, "Issuewild CAA record flag should default to 0")
+	assert.Equal(t, "issuewild", issuewild.Tag, "Issuewild CAA record tag should be 'issuewild'")
+	assert.Equal(t, "ca.example.net", issuewild.Value, "Issuewild CAA record value should be 'ca.example.net'")
+
+	// Test iodef CAA record
+	assert.NotNil(t, iodef, "Iodef CAA record should exist")
+	assert.Equal(t, uint8(128), iodef.Flag, "Iodef CAA record flag should be 128")
+	assert.Equal(t, "iodef", iodef.Tag, "Iodef CAA record tag should be 'iodef'")
+	assert.Equal(t, "mailto:[email protected]", iodef.Value, "Iodef CAA record value should be 'mailto:[email protected]'")
+	assert.Equal(t, 100, iodefWeight, "Iodef CAA record weight should be 100")
+}