diff --git a/docs/content/docs/dns/_index.md b/docs/content/docs/dns/_index.md index bc3b3e80d..8eee9f9f8 100644 --- a/docs/content/docs/dns/_index.md +++ b/docs/content/docs/dns/_index.md @@ -29,6 +29,8 @@ Each zone has its individual configuration for how to handle queries, see [Handl A record belongs to one zone and stores one response. To support multiple responses (i.e. multiple IP addresses for an A record), record UIDs are used. A UID is optional, and records with UID can be combined with a record without UID (all their results will be returned). Records created by the DHCP role will automatically have the UID assigned based on the DHCP device's identifier (the MAC address in most cases). +To create a record at the root of the zone, set the name of the record to `@`. + A single record holds the following data: - `data`: The actual response, an IP for A/AAAA records, text for TXT records, etc. diff --git a/pkg/roles/dns/handler_etcd.go b/pkg/roles/dns/handler_etcd.go index e9ccb9d18..874121ad3 100644 --- a/pkg/roles/dns/handler_etcd.go +++ b/pkg/roles/dns/handler_etcd.go @@ -76,7 +76,16 @@ func (eh *EtcdHandler) findWildcard(r *utils.DNSRequest, relRecordName string, q func (eh *EtcdHandler) handleSingleQuestion(question dns.Question, r *utils.DNSRequest) []dns.RR { answers := []dns.RR{} - relRecordName := strings.TrimSuffix(strings.ToLower(question.Name), strings.ToLower(utils.EnsureLeadingPeriod(eh.z.Name))) + // Remove zone from query name + relRecordName := strings.TrimSuffix(strings.ToLower(question.Name), strings.ToLower(eh.z.Name)) + if relRecordName == "" { + // If the query name was the zone, the query should look for a record at the root + relRecordName = types.DNSRoot + } else { + // Otherwise the relative record name still has a dot at the end which is not what we store + // in the database + relRecordName = strings.TrimSuffix(relRecordName, ".") + } directRecordKey := eh.z.inst.KV().Key( eh.z.etcdKey, strings.ToLower(relRecordName), diff --git a/pkg/roles/dns/handler_etcd_test.go b/pkg/roles/dns/handler_etcd_test.go index 4fe28e431..dc4317476 100644 --- a/pkg/roles/dns/handler_etcd_test.go +++ b/pkg/roles/dns/handler_etcd_test.go @@ -12,6 +12,8 @@ import ( "github.com/stretchr/testify/assert" ) +const TestZone = "example.com." + func TestRoleDNS_Etcd(t *testing.T) { defer tests.Setup(t)() rootInst := instance.New() @@ -66,6 +68,61 @@ func TestRoleDNS_Etcd(t *testing.T) { assert.Equal(t, net.ParseIP("10.1.2.3").String(), ans.(*d.A).A.String()) } +// Test DNS Entry at root of zone +func TestRoleDNS_Etcd_Root(t *testing.T) { + defer tests.Setup(t)() + rootInst := instance.New() + ctx := tests.Context() + inst := rootInst.ForRole("dns", ctx) + tests.PanicIfError(inst.KV().Put( + ctx, + inst.KV().Key( + types.KeyRole, + types.KeyZones, + TestZone, + ).String(), + tests.MustJSON(dns.Zone{ + HandlerConfigs: []map[string]string{ + { + "type": "etcd", + }, + }, + }), + )) + tests.PanicIfError(inst.KV().Put( + ctx, + inst.KV().Key( + types.KeyRole, + types.KeyZones, + TestZone, + "@", + types.DNSRecordTypeA, + "0", + ).String(), + tests.MustJSON(dns.Record{ + Data: "10.1.2.3", + }), + )) + + role := dns.New(inst) + assert.NotNil(t, role) + assert.Nil(t, role.Start(ctx, RoleConfig())) + defer role.Stop() + + fw := NewNullDNSWriter() + role.Handler(fw, &d.Msg{ + Question: []d.Question{ + { + Name: TestZone, + Qtype: d.TypeA, + Qclass: d.ClassINET, + }, + }, + }) + ans := fw.Msg().Answer[0] + assert.Equal(t, net.ParseIP("10.1.2.3").String(), ans.(*d.A).A.String()) +} + func TestRoleDNS_Etcd_Wildcard(t *testing.T) { defer tests.Setup(t)() rootInst := instance.New() diff --git a/pkg/roles/dns/types/role.go b/pkg/roles/dns/types/role.go index d39955526..b4269e0bf 100644 --- a/pkg/roles/dns/types/role.go +++ b/pkg/roles/dns/types/role.go @@ -16,6 +16,7 @@ const ( const ( DNSWildcard = "*" + DNSRoot = "@" ) const (