Adding a new Resource Record (RR) type to BIND 9
BIND 9 was designed to make it relatively easy to add user defined resource record (RR) types, though you do need some understanding of C.
The descriptions of all the record types known to BIND 9 are in a directory structure under lib/dns/rdata in the source tree. This directory is structured at the first level by the DNS CLASS the record type belongs to. The name of the directory is the <class>_<code>.h (e.g. IN is in_1). The known classes are in_1, ch_3, hs_4, any_255 and generic -- the first four hold RR types that are specific to a particular class, and "generic" holds RR types that are the same across all classes. Within each of those directories there are pairs of files which describe the actual types. These files are named <type>_<code>.c and <type>_<code>.h (e.g. the description of the MX record, which has the RR type code 15, is in mx_15.c and mx_15.h).
Within each of these files there are method functions for various operations that apply to types, such as how to print out a type, how to read a type from a text file, how to read a record from a DNS message in wire format, etc. These methods have names constructed from the type, class (if the record is class specific) and the operation to be performed. These methods are called from the dns_rdata_<method> functions which are declared in <dns/rdata.h>.
Once the two files containing the methods and types definitions for the structures have been written you need to run "make clean" then "make" to incorporate the new record type. This will cause the lib/dns/rdata directory structure to be scanned and header files to be rebuilt which will include the new files. All the tools that are part of BIND will know about the new type.
You can also define auxiliary functions to help walk the structure returned by dns_rdata_tostruct like dns_rdata_txt_first and dns_rdata_txt_next, which are used to walk the text strings in a TXT record. The code goes into the .c file and the function prototype into the .h file, the contents of which are included in <dns/rdatastruct.h>.
lib/dns/rdata/generic/proforma.c and lib/dns/rdata/generic/proforma.h can be copied and used as starting points when defining a adding a new type. Please also look as the existing records for examples of how to implement a method.
Type Value Selection:
Type values range from 0 to 65536. These have been further divided into reserved values, values that have global definition, and values that have local definition as defined in https://tools.ietf.org/html/rfc6895. Please use an appropriate value. You can use a private value (65280 - 65534) while waiting for a type assignment to be made, then rename the file and update the type values when the assignment has been made.
static isc_result_t fromtext[_<class>]_<type>(int rdclass, dns_rdatatype_t type, isc_lex_t *lexer, dns_name_t *origin, unsigned int options, isc_buffer_t *target, dns_rdatacallbacks_t *callbacks);
"fromtext" reads a series of tokens from lexer and constructs a DNS record in wire format which it stores in target. It performs sanity checks on the entered content rejecting any invalid records.
static isc_result_t totext[_<class>]_<type>(dns_rdata_t *rdata, dns_rdata_textctx_t *tctx, isc_buffer_t *target);
"totext" takes a record in wire format and converts it to presentation form which is stored in a buffer for later printing.
static isc_result_t fromwire[_<class>]_<type>(int rdclass, dns_rdatatype_t type, isc_buffer_t *source, dns_decompress_t *dctx, unsigned int options, isc_buffer_t *target_t);
"fromwire" copies in a record received in a DNS message. It performs sanity checks to ensure that the record conforms to the specification for the RR type. It expands any compressed domain names and copies out the expanded record to a buffer. NOTE: It is critical to the security of the name server that only valid records are accepted by this function, as other parts of the name server do not verify the contents of records.
static isc_result_t towire[_<class>]_<type>(dns_rdata_t *rdata, dns_compress_t *cctx, isc_buffer_t *target);
"towire" takes a record in wire format and adds it to a DNS message, optionally compressing domain names if that is allowed by the type's definition. For all new types, compression is not allowed, so this is effectively a wrapper around memmove.
static int compare[_<class>]_<type>(const dns_rdata_t *rdata1, const dns_rdata_t *rdata2);
"compare" takes two records and compares them according to the DNSSEC ordering rules. For new record types this is effectively a wrapper around memcmp.
static isc_result_t fromstruct[_<class>]_<type>(int rdclass, dns_rdatatype_t type, void *source, isc_buffer_t *target);
"fromstruct" takes a C structure (as described in tostruct) and turns it into a record in wire format.
static isc_result_t tostruct[_<class>]_<type>(dns_rdata_t *rdata, void *target, isc_mem_t *mctx);
"tostruct" takes a record in wire format and breaks it down into a type-specific C structure defined in the header file. The name of this structure is dns_rdata_<type>[_<class>]_t with a first element of "dns_rdatacommon_t common; ". If no memory context is passed in then the caller will preserve the contents of the record in wire form until the structure is freed or no longer in use. If a memory context is passed in then memory should be allocated for anything not directly part of the structure.
static void freestruct[_<class>]_<type>(void *source);
"freestruct" frees any memory allocated by "tostruct".
static isc_result_t additional[_<class>]_<type>(dns_rdata_t *rdata, dns_additionaldatafunc_t add, void *arg);
"additional" is a method to request that related records get added to the additional section of a message when this record in added to a message. An empty method is usual here.
static isc_result_t digest[_<class>]_<type>(dns_rdata_t *rdata, dns_digestfunc_t digest, void *arg);
"digest" passes the record content to the digest function, performing any needed DNSSEC canonicalisation. For all new record types, this involves adding the entire record to a region and passing that to digest, as all new record types are treated as opaque blobs of data by DNSSEC.
static isc_boolean_t checkowner[_<class>]_<type>(dns_name_t *name, dns_rdataclass_t rdclass, dns_rdatatype_t type, isc_boolean_t wildcard);
"checkowner" takes the owner name of the record and checks it meets appropriate rules that are defined external to the DNS. In most cases this can just be a function that returns ISC_TRUE.
static isc_boolean_t checknames[_<class>]_<type>(dns_rdata_t *rdata, dns_name_t *owner, dns_name_t *bad);
"checknames" checks the contents of the rdata with the given owner name to ensure that it meets externally defined syntax rules. bad is set to the failing name if ISC_FALSE is returned.
static int casecompare[_<class>]_<type>(const dns_rdata_t *rdata1, const dns_rdata_t *rdata2);
"casecompare" compares two rdatas case sensitively which have case insensitive DNSSEC comparisons. "casecompare" is used when deciding whether to replace a record based on case alone when compared to 0 with "compare". Adding "example NS NS.EXAMPLE" and "example NS ns.example" results in the single record "example NS ns.example" being added as it is the last record to be added.