For the complete documentation index, see llms.txt. This page is also available as Markdown.

Cookbook

This document provides "cookbook" recipes for doing common tasks.

Create a models.DomainConfig

  • What: Create a models.DomainConfig

  • Why: Providers are handed a models.DomainConfig that is already created. However often when we write tests we need to create a list of models.RecordConfig's which are stored in a models.DomainConfig.

Recommended:

Deprecated:

Create a models.RecordConfig

  • What: Create a models.RecordConfig

  • Why: One of the most important functions of a provider is to translate the native DNS records (as received from the API) to standard models.RecordConfig structs. Previously there were many ways to do this. Starting in DNSControl v5, we've standaredized on two factories:

Recommended:

  • These are a method of models.DomainConfig (typically the variable name is dc). Records only exist in the context of a domain. For example, if an MX record's target it "@", this is meaningless unless we know what the domain is. This also makes the call shorter: it eliminates parameters that contain info already stored in dc such as the zone name (required for normalizing labels).

  • NewRecordConfig() takes a list of arguments. It doesn't matter if the arguments are strings, ints, netip.Addrs... the function will convert them to the correct type and return and error if they can't be converted.

  • NewRecordConfigParse() takes the arguments as one long string, which is parsed. If your provider returns (for example) the MX record data as 10 mx.example.com. and the SRV record data as 4 100 123 three.example.com., you can just send the whole string to this function. This replaces models.PopulateFromString()

  • LABEL: Must be the output of one of these functions:

    • dc.LabelFromShort(): Use this if your provider always gives you the shortname (foo of foo.example.com)

    • dc.LabelFromFQDNNoDot(): Use this if your provider always gives you the FQDN (foo.example.com)

    • dc.LabelFromFQDNWithDot(): Use this if your provider always give syou the FQDN+"." (foo.example.com.)

  • Which to use?

    • Unsurer? Try LabelFromFQDNWithDot() and watch for errors. They often suggest what function to use.

    • Errors like `DEBUG: LabelFromFQDNWithDot(quux.a.dnscontrol-azure.com) called WRONG.'

    • In this case, the hostname (quux.a.dnscontrol-azure.com) indicates LabelFromFQDNNoDot is more appropriate.

    • If you see a shortname, use dc.LabelFromShort()

    • If the integration tests for IGNORE() fail, you've probably picked the wrong function.

  • Why 3 functions? Can't NewRecordConfig figure it out?

    • There are ambiguous cases that make it impossible to guess with 100 percent accurately.

    • It is faster and more accurate to simply have multiple functions, one for each situation.

    • The truth is that your provider's API is going to only deliver the label one way. They're not going to change.

  • TTL must be the desired TTL or 0 if it is unknown. Unknown TTLs are converted into the default TTL.

  • TYPE_STR_OR_NUM can be either the record type's codepoint or a string:

    • codepoint: e.g. dnsv2.TypeA, dnsv2.TypeMX, dnsv2.TypeCNAME, privatetypes.CLOUDFLAREAPISINGLEREDIRECT

    • string ("A", "MX", "CNAME", "CLOUDFLAREAPI_SINGLE_REDIRECT")

    • Please use the constant when possible. Eventually we'll eliminate the strings. However, if all you have is the string, please let the factory do the conversion for you.

  • ARGS is a list of fields in the order they appear in the struct. The type doesn't matter as they will be converted automatically. No need to convert strings to ints and so on. Even IP addresses are handled properly. Examples:

    • dc.NewRecordConfig("mxhost", 0, dnsv2.TypeMX, "10", "mx.example.com.")

    • dc.NewRecordConfig("mxback", 0, dnsv2.TypeMX, 20, "mx2.example.com.")

    • dc.NewRecordConfig("www", 0, dnsv2.TypeA, "1.2.3.4")

    • addr, _ := netip.ParseAddr("192.168.1.1"); dc.NewRecordConfig("www", 0, dnsv2.TypeA, addr)

    • dc.NewRecordConfig("public", 0, dnsv2.TypeLOC, 42, 21, 54, "N", 71, 6, 18, "W", -24.05, 30, 0, 0)

    • dc.NewRecordConfig("public", 0, dnsv2.TypeLOC, "42", "21", "54", "N", "71", "6", "18", "W", "-24.05", 30, "0", 0)

  • RFC1038_STRING is a string that is parsed like the fields in a ZoneFile

    • dc.NewRecordConfigParse("mxhost", 0, dnsv2.TypeMX, "10 mx.example.com.")

    • dc.NewRecordConfigParse("mxback", 0, dnsv2.TypeMX, "20 mx2.example.com.")

    • dc.NewRecordConfigParse("www", 0, dnsv2.TypeA, "1.2.3.4")

    • dc.NewRecordConfigParse("public", 0, dnsv2.TypeLOC, "42 21 54 N 71 6 18 W -24.05 30 0 0)

Typically either dc.NewRecordConfig() or dc.NewRecordConfigParse() will satisfy all of your needs. However occasionally there are situations where a particular record type needs special handling. We recommend using a switch statement to handle the special case.

In this example, *Parse works just fine for all cases except MX records. Therefore, we process MX records first as a special case:

Deprecated:

Please do not create your own RecordConfig's:

Getters/Setters for RDATA in models.RecordConfig

The fields of a DNS record are called the RDATA (resource data).

Accessing the RDATA using the .GetRDATA() getter, and the .SetRDATA() setter.

.GetRDATA()) returns the generic interface (dnsv2.RDATA). The .String() function generates a zonefile-like string representing every field in the struct:

Generally you then need to cast it to the correct type.

Typical useage:

To alter fields call GetRDATA(), change the fields, then call SetRDATA() to save it back. GetRDATA() returns a copy of the RDATA, therefore mutating it without saving it back is fruitless.

Create models.RecordConfig literals for testdata

This is for creating test data only. They panic on error.

If you want to create an models.RecordConfig without adding it to a dc, there are Must versions of dc.NewRecordConfig() and dc.NewRecordConfigParse():

How to create a "builder"

A builder is a function that can be used in dnsconfig.js which outputs one or more DNS records. For example, the SPF_BUILDER() function generates TXT records.

Assume the builder is named "robert" and appears in dnsconfig.js as ROBERT(label, data)

  • Create the builder in models/b_robert.go . Register the builder:

(see models/b_loc.go for an example)

  • Create the BuilderNAME() function

Add a function called BuilderROBERT() with this signature:

  • dc: The domain the builder was called in.

  • ttl: the desired TTL or 0 if it is unknown. Unknown TTLs are converted into the default TTL.

  • args: the arguments passed to the function in dnsconfig.js. Each should be passed through a mustbe.* function before use.

  • metadata: Any {foo: "foo"} (Javascript objects) passed to the function in dnsconfig.js.

  • subdomain: If the builder was used in a D_EXTEND(), the subdomain will be non-nil.

    • If D("example.com") is followed by D_EXTEND("foo.example.com"), subdomain will be foo.

    • To calculate the label: name, _ := dc.LabelFromDnsconfigjs(args[0].(string), subdomain)

    • To calcuate a target name: name, _ := mustbe.TagetHostWithSubdoman(dc.Name, subdomain, name)

The builder function can do basically anything and generate as many records as it wants. SPF_BUILDER() returns many records. LOC() returns just one record.

All builders must have unit tests.

How to manipulate domain/zone names

Rather than use strings.Trim() or other functions, use these functions. They handle "@", apex domains, and other edge cases properly. See the unit tests for examples.

Turn a short name (foo) into a FQDN (foo.example.com.) (with trailing dot).

Turn a short name (foo) into a FQDN (foo.example.com) (no trailing dot).

Turn a name (FQDN end with a ".") into a shortname:

What you should know about TXT records

The DNS protocol stores a TXT record as a series of segements:

  • Each segment can be a maximum of 255-octets (octets == bytes).

  • It's possible to have no segments.

  • It's possible to zero-length segments.

  • In a DNS Zone File, TXT records are presented as quoted strings (each segment quoted individually), with \DDD (3-digit) escapes for non-printing chars and \x (1-char escapes) for literals (typically \\ to represent a backslach and \" to represent a double-quote inside a quoted string).

DNSControl stores TXT records as segments. However...

  • We "re-segment" strings so that all but the last one is 255-octets.

  • Zero-length segments are removed.

  • Records with no segments are "improved" to have a single 0-length segment. This eliminates an edge case to deal with.

To manage this, we have getters and setters that assure the above rules happen transparently.

TXT functions

Reading TXT data:

Creating TXT records from scratch:

Updating existing records:

  • SetTargetTXT(string): Takes one (possibly long) string.

  • SetTargetTXTs([]string): Takes a []string. Will re-segment if needed.

FYI: The above functions will re-segment the text such that each segment is 255 octets long, except the last one which contains the remainder. If a record is (for example) 3 segments of length 200, 200, and 200 each, they will be merged and resegmented to lengths 255, 255, and 90.

Please do not use rc.GetTargetField() on TXT records.

How to change the rtype of a RecordConfig

Suppose you have a models.RecordConfig{} but the type needs to change. For example, the provider doesn't support ALIAS but they call it a CNAME.

For any other transformation:

  1. Set .Type and .TypeNum

  2. Create the RDATA using a MakeFOO() function and use the SetRDATA() to store it. Be sure to use SetDATA() last since it rebuilds any pre-computed strings.

Example:

Doing it this way preserves all other fields such as .Metadata.

How to label imports

To avoid confusion between old and new DNS modules, we always import them with explicit v1 and v2 names.

Here is the canonical list:

Last updated