wiki:NameClassDesign

DNS Name Class API Design

1. Introduction

Domain names are one of the most fundamental concepts of the DNS for the obvious reason. We'll therefore need to provide convenient API to manipulate domain names as a major part of BIND10. This document describes design decisions and API specifications at an early stage of the development. This document is expected to be updated as the development proceeds...hopefully.

This API is designed to be used not only internally in BIND10, but also as a general purpose DNS library. So one important design goal is that the interface be intuitive for external developers.

2. High Level Structure

There isn't much to be described for names per se; we'll probably need this when we describe the entire DNS message API, which will involve names, resource records, and other DNS protocol related concepts.

3. API Description

C++ Namespace

Everything described here belongs to the namespace ISC::DNS.

The Name Class

This class encapsulates DNS names. It provides interfaces to construct a name from string or wire-format data, transform a name into a string or wire-format data, compare two names, get access to attributes.

class Name {
public:
    Name();
    explicit Name(const std::string& namestr);
    explicit Name(Buffer& b, NameDecompressor& d);
    std::string to_text(bool omit_final_dot = false) const;
    void to_wire(Buffer& b, NameCompressor& c) const;
    size_t get_length() const;
    unsigned int get_labels() const;
    NameComparisonResult compare(const Name& other) const;
    Name split(unsigned int first, unsigned int n) const;
    Name concatenate(const Name& suffix) const;
    bool is_wildcard() const;
    bool operator==(const Name& other) const;
    bool equals(const Name& other) const; // alias of ==
    bool operator!=(const Name& other) const;
    bool nequals(const Name& other) const; // alias of !=
    bool operator<=(const Name& other) const;
    bool leq(const Name& other) const; // alias of <=
    bool operator>=(const Name& other) const;
    bool geq(const Name& other) const; // alias of >=
    bool operator<(const Name& other) const;
    bool lthan(const Name& other) const; // alias of <
    bool operator>(const Name& other) const;
    bool gthan(const Name& other) const; // alias of >

    static const unsigned int MAXWIRE = 255;
private:
    std::string _ndata;
    std::vector<char> _offsets;
    unsigned int _length;
    unsigned int _labels;
};
  • Constructors
    • Name::Name(const std::string& namestr);
      Construct a new Name object from a plain string namestr.
    • Name(Buffer& buffer, NameDecompressor& d);
      Construct a new Name object from wire-format data. When the name to be constructed is compressed in the wire data, NameDecompressor d contains the context for decompression. This interface may not be a good one and may be revisited.
  • Destructor
    We use the default destructor for this class.
  • Copy Constructor
    We use the default copy constructor for this class.
  • Name::to_text(bool omit_final_dot);
    Returns a std::string object representing the Name as a string. Unless omit_final_dot is true, the returned string ends with a dot '.'; the default is false.
  • Name::to_wire(Buffer& b, NameCompressor& c);
    Dump the Name in the DNS wire format in Buffer b. When the Name should be compressed, NameCompressor c contains the compression context. This interface may not be a good one and may be revisited.
  • Name::get_length();
    Returns the length of the Name (in its wire format).
  • Name::get_labels();
    Returns the number of labels containing in the Name. Note that an empty label (corresponding to a trailing '.') is counted as a single label, so the return value of get_labels() must be >0.
  • Name::compare(const Name& other);
    Compare the Name and other and returns the result in the form of a NameComparisonResult object. Note that this is a case-insensitive comparison.
  • Name::split(unsigned int first, unsigned int n);
    Returns a new Name object based on the Name containing n labels including and following the first label. Note: we may want to have different versions (signatures) of this method. For example, we want to split the Name based on a given suffix name.
  • Name::concatenate(const Name& suffix);
    Returns a new Name object concatenating 'suffix' to this Name.
  • Name::is_wildcard();
    Returns true if the least significant label of this Name is '*'; otherwise returns false.
  • Name::operator==(const Name& other);
  • Name::operator!=(const Name& other);
  • Name::operator<=(const Name& other);
  • Name::operator>=(const Name& other);
  • Name::operator<(const Name& other);
  • Name::operator>(const Name& other);
    These are implemented based on the compare() method as follows: operator Op returns true if this->compare(other).get_order() Op Order; otherwise returns false. For example, operator == returns true if this->compare(other).get_order() == 0.

The NameComparisonResult Class

This is a supplemental class used only as a result of Name::compare(). It encapsulate a tuple of the comparison: ordering, number of common labels, and relationship. The semantics of these tuple elements for the case of name1->compare(name2) are as follows:

ordering
relative ordering under the DNSSEC order relation
labels
the number of common significant labels of the two names being compared
relationship
a value of NameComparisonResult::NameRelation and can be one of the following.
  • none: there's no hierarchical relationship between the two names
  • contains: name1 properly contains name2; i.e. name2 is a proper subdomain of name1.
  • subdomain: name1 is a proper subdomain of name2.
  • equal: name1 and name2 are equal.
  • commonancestor: name1 and name2 share a common ancestor.
class NameComparisonResult {
public:
    typedef enum NameRelation {
        none = 0,
        contains = 1,
        subdomain = 2,
        equal = 3,
        commonancestor = 4
    };
    explicit NameComparisonResult(int order, int nlabels, NameRelation reln);
    int get_order() const;
    int get_nlabels() const;
    NameRelation get_relation() const;
private:
    int _order;
    int _nlabels;
    NameRelation _reln;
};
  • Constructors
    • NameComparisonResult::NameComparisonResult(int order, int nlabels, NameRelation reln);
      Simply initializes the object in the straightforward way.
  • Destructor
    We use the default destructor for this class.
  • Copy Constructor
    We use the default copy constructor for this class.
  • NameComparisonResult::get_order();
    Returns the ordering of the comparison result.
  • NameComparisonResult::get_nlabels();
    Returns the number of common labels of the comparison result.
  • NameComparisonResult::get_relation();
    Returns the NameRelation (see above) of the comparison result.

4. Python Binding

TBD (we are still not sure how the python binding to BIND10 will look like in general).

5. Design Choices

There are several existing APIs that can manipulate domain names (see below). This API is largely derived from the name module of BIND9 libdns and the dns.name module of dnspython as the design of these modules generally seem reasonable to us. There are still non trivial design decisions specific to our API. This section discusses such issues.

  • for conversion and transformation, this API generally returns a new object. For example, to_text() returns a new std::string object representing the name as a string. This will be convenient for the API user (unlike BIND9's libdns) in that the caller doesn't have to prepare the new object beforehand. Also, since all dynamically allocated data is encapsulated, we don't have to worry about resource leak (too much).
  • one possible concern with this approach is that it may tend to require data copy more frequently than other approaches (such as pointer/reference-based design) and may cause non-negligible performance penalty. This may be a non-issue in practice, however, if the underlying containers are carefully designed/implemented (e.g., multiple instances of std::string can share the actual data, making copies cheaper). In any case, we'll eventually need to do benchmark.
  • the Name::compare() method is similar to libdns's dns_name_fullcompare(), but returns an abstract "result" object (an instance of the NameComparisonResult class) encapsulating the comparison result, ordering as an integer, and the number of common labels. dns_name_fullcompare() takes pointers to integers as arguments for the latter two, but I believe returning a single result is more intuitive for the API user.
  • in general, error cases trigger C++ exceptions, rather than returning an error code.
  • many existing APIs introduce an "absolute or relative" attribute of names as defined in RFC1035. On the other hand, names are always "absolute" in the initial design of our API. In fact, separating absolute and relative would confuse API users unnecessarily. For example, it's not so intuitive to consider the comparison result of an absolute name with a relative name. We've looked into how the concept of absolute names is used in BIND9, and found that in many cases names are generally absolute. The only reasonable case of separating absolute and relative is in a master file parser, where a relative name must be a complete name with an "origin" name, which must be absolute. So, in this initial design, we chose a simpler approach: the API generally handles names as absolute; when we introduce a parser of master files, we'll introduce the notion of relative names as a special case.

6. Test Plans

We can reuse BIND9's test cases (located in bind9/bin/tests/names), which cover:

  1. initialization (construction?)
  2. invalidation (maybe N/A)
  3. set/has buffer (probably N/A)
  4. isabsolute (whether to add this feature is still open)
  5. hash (not sure if this applicable)
  6. full-comparison (DONE)
  7. simple comparison
  8. "rdata compare": comparison as part of radata in DNSSEC canonical form
  9. subdomain testing (issubdomain)
  10. count labels
  11. getlabel: getting n-th label (applicable?)
  12. getlabelsequence (= split)
  13. to/from wire (DONE for fromWire)
  14. to/from text (DONE)
  15. concatenate (not implemented in BIND9)

7. Security Considerations

Constructors need to parse (though indirectly) wire-format or textual names, which can come from the network and can be malformed. We are not intending to directly handle such raw data within this API (we'll use other standard objects such as std::string or std::vector) so that our implementation won't be a direct source of severe security bug such as a buffer overflow, but the parser implementation should still be carefully reviewed.

8. Performance Considerations

The primary goal of this API is to provide intuitive programming interface; although we gave some thoughts to performance (e.g., avoiding data copy by using std::string internally, depending on the string implementation) performance of this API is a secondary concern.

From our experiences with BIND9, major performance bottlenecks in name handling is manipulation of wire-format data, such as parsing names in an incoming DNS message or name compression for outgoing messages. When performance really matters (e.g., for a busy DNS server implementation), we believe we should optimize such specific cases by bypassing the general purpose API, and we believe we can do this by carefully designing higher level APIs such as message rendering/parsing APIs.

9. Open Issues

  • we probably need some more methods. BIND9 libdns defines get_label, downcase, ismailbox, hash, etc. We may need some of these in BIND10, too.
  • do we need a case sensitive comparison?
  • performance considerations (see above)
  • wire-related APIs should be revisited.
  • the design decision of absolute names (see above) may have to be revisited as we develop other details.
  • IDN consideration:
    dnspython has a method named to_unicode, which returns a unicode representation of a given name object. The initial proposal doesn't contain such a thing, but if we want BIND10 to support IDN more explicitly we may want to have something like that.

10. Related APIs

We've done some research into existing libraries/tools: BIND9's libdns, NLnet Lab's ldns, perl Net::DNS, dnspython, and dnsruby. The design of libdns and dnspython are quite similar (not so surprisingly, as the author of dnspython was involved in the original BIND9 development). Net::DNS doesn't support a notion of "names" explicitly. ldns handles DNS names as part of a generic data structure (ldns_struct_rdf) and doesn't support so many operations on names (like counting labels, concatenating two names, etc) as far as we can see.

dnspython represents names as a list of labels, while the initial implementation of our API realizes the name data as a std::string in wire format. This design is derived from BIND9, but if we try to achieve performance in a different way, we may be able to reconsider the actual representation. In any case, the representation should be private so that we can change it in future versions.

11. References

RFC1035
Section 3 defines wire-format DNS name. Section 5.1 defines a textual representation of DNS names used in master files.
RFC4034
Section 6.1 defines the Canonical DNS Name Order used in the Name::compare() method.
Last modified 8 years ago Last modified on Dec 6, 2009, 10:25:11 PM