Serving a zone

edit

The server has no zone files: a zone is the records the handler answers with. This handler serves example.com from a list of records, as an authoritative server does:

var soa = new DnsResourceRecord
{
    Name = "example.com",
    Type = DnsQueryType.SOA,
    Class = DnsQueryClass.IN,
    TimeToLive = 3600,
    Data = new DnsSoaRecordData
    {
        PrimaryNameServer = "ns1.example.com",
        ResponsibleMailbox = "hostmaster.example.com",
        Serial = 2026102901,
        Refresh = 7200,
        Retry = 3600,
        Expire = 1209600,
        Minimum = 300, // how long resolvers may cache a negative answer
    },
};

DnsResourceRecord[] records =
[
    soa,
    new() { Name = "example.com", Type = DnsQueryType.MX, Class = DnsQueryClass.IN, TimeToLive = 3600, Data = new DnsMxRecordData { Preference = 10, Exchange = "mail.example.com" } },
    new() { Name = "www.example.com", Type = DnsQueryType.A, Class = DnsQueryClass.IN, TimeToLive = 3600, Data = new DnsARecordData { Address = IPAddress.Parse("192.0.2.10") } },
    new() { Name = "www.example.com", Type = DnsQueryType.AAAA, Class = DnsQueryClass.IN, TimeToLive = 3600, Data = new DnsAaaaRecordData { Address = IPAddress.Parse("2001:db8::10") } },
    new() { Name = "mail.example.com", Type = DnsQueryType.A, Class = DnsQueryClass.IN, TimeToLive = 3600, Data = new DnsARecordData { Address = IPAddress.Parse("192.0.2.25") } },
];

DnsRequestDelegate handler = (context, cancellationToken) =>
{
    var response = context.CreateResponse();
    if (context.Query.Questions is not [var question])
    {
        response.ResponseCode = DnsResponseCode.FormError;
        return ValueTask.FromResult(response);
    }

    var name = question.Name.TrimEnd('.');
    if (!string.Equals(name, "example.com", StringComparison.OrdinalIgnoreCase) && !name.EndsWith(".example.com", StringComparison.OrdinalIgnoreCase))
    {
        // Not a name of this zone, and this server does not resolve other names
        response.ResponseCode = DnsResponseCode.Refused;
        return ValueTask.FromResult(response);
    }

    response.IsAuthoritative = true;
    var recordsOfName = records.Where(record => string.Equals(record.Name, name, StringComparison.OrdinalIgnoreCase)).ToList();
    foreach (var record in recordsOfName.Where(record => record.Type == question.Type))
    {
        response.Answers.Add(record);
    }

    if (response.Answers.Count is 0)
    {
        // NXDOMAIN when the name does not exist, NODATA (no error, no answer) when it only has other types
        response.ResponseCode = recordsOfName.Count is 0 ? DnsResponseCode.NameError : DnsResponseCode.NoError;
        response.Authorities.Add(soa);
    }

    return ValueTask.FromResult(response);
};

Register it with app.MapDnsHandler(handler), as in Getting started.

What an authoritative answer contains

  • The AA flag (IsAuthoritative = true) on the answers of the zone: resolvers then know that the answer comes from the zone itself.
  • The records of the question, with their TTL: resolvers cache them for that long.
  • NXDOMAIN (NameError) when the name does not exist, and NODATA (NoError without answers) when the name exists with other record types. Resolvers treat them differently: NXDOMAIN also means "no record of any type, and no subdomain".
  • The SOA record in Authorities with every negative answer. Resolvers cache a negative answer for the lower of the TTL of the SOA record and its Minimum field (RFC 2308). Without it, they may not cache it, or cache it for a default duration.
  • Refused for names outside the zone, when the server does not resolve other names.

Names are compared without case, as DNS requires (WWW.example.com is www.example.com), and a trailing dot can appear in a name: trim it before comparing.

Go further

  • CNAME: when a name has a CNAME record, answer every question type with the CNAME, followed by the records of the target when it is in the zone.
  • Wildcards: to answer *.example.com, look up the name, then the wildcard of its parent when it does not exist, and answer with the queried name.
  • Delegations: for a subdomain served by other servers, answer with its NS records in Authorities, without the AA flag, and their addresses in AdditionalRecords.
  • Other names: to resolve names outside the zone, forward the query to another server with a DnsClient, and copy its response. Meziantou.DnsProxy does this, with a cache. See DnsProxy.
  • DNS over HTTPS caching: the Cache-Control header of DNS over HTTPS responses comes from the TTLs and the SOA minimum, so the same records also drive HTTP caches. See DNS over HTTPS.