DNS Edit

Author : james@2longbeans.net
Start Date : 25th May 2006

Overview

The objective of DNS Edit is to provide a complete suite of people-oriented DNS management tools for the coherent delegation of DNS records in a robust environment with various autonomous domains (or departments). In addition, it enforces role based access controls due to the fact that in real life, different people are placed in charge of different IP ranges spanning diverse networks. Dnsedit is designed to generate DNS databases for the following scenerios.

  1. To provide authoritative records for forward and reverse lookups.
  2. To delegate forward or reverse lookups to 3rd party DNS servers.
  3. To handle both classful and classless reverse delegation.

Dnsedit is currently designed to generate databases suitable for use with BIND version 9.x. Much effort has been made to make dnsedit as generic as possible. This is because future versions of dnseditd are intended to generate databases suitable for use with multiple DNS server software (eg TinyDNS).

Quick Links :

  • Architecture
  • Runtime Configuration
  • Permission Checking
  • Data Storage
  • Command Protocol
  • Reverse Delegation Scenerios
  • Classless Reverse Delegation
  • Forward and Reverse Record Manipulation
  • Safety Features
  • Project Status and Known Bugs

  • Source code
  • RFC 2317
  • Note: The reader should be familiar with the operation of DNS services for both forward and reverse (classless) lookups.


    Architecture

    The following diagram illustrates the various components in DNS Edit. In this example, there is a DNS server and a web server which are used for administration. Central to the implementation is the daemon dnseditd which handles user authentication, authorization, accounting, logging and also performing the actual DNS record changes. Various dnseditd processes maintain a TCP connection with each other, so as to synchronise events. This TCP socket is also used for communication with the commandline tool dnsedit.

  • GDBM Database : This is where DNS Edit stores all the stuff.
  • dnseditd : This is the main daemon process.
  • dnsedit : User's commandline tool which interfaces user requests to dnseditd.
  • dnsedit.php : A PHP script which comes between httpd and the dnsedit commandline tool.
  • DNS records : This is the database which the DNS server uses.
  • DNS server : This is the daemon which listens on UDP 53 and faces the world.
  • Runtime Configuration

    Usage : ENV=VALUE... dnseditd

    Dnseditd does not read any config files. All configurables are obtained from environment variables. A list of configurable environment variables may be obtained from "dnseditd -?". ACLs which determine user permissions are read from "./acl/". In this directory, files bearing usernames should be created. When a user connects to dnseditd, the ACL file is read upon authentication. The following is an example of such a file.

    Since this file is always read during user login, there is no need to restart dnseditd if changes are made to ACL files. Any connected users should re-login for changes to take effect. By default, user passwords are obtained using the system's getspnam(). Thus it is usually not necessary to specify user passwords in their ACL files. However, if custom passwords are required for certain accounts, simply specify the DES or MD5 hash in the user ACL file's password statement. This password will take preference over the system account password.

    Permission Checking

    Only authenticated users may perform modifications to DNS records. Once authenticated, logic checks are performed depending on the modification requested. The purpose of these checks is to not only ensure validity of data, but also to ensure that users make modifications only within their boundaries of responsibility (ie, department A admins cannot change entries belonging to department B). In general, logic checks are categorized into :

    Universal Checks Record Specific Checks (forward) Record Specific Check (reverse) Host and IP Checks SOA Checks

    Dnsedit does not recognise any "superuser" account, as user ACLs are always reloaded upon login. However, the site administrator may turn a normal user into a superuser by specifying "allow 0.0.0.0/0", declaring all required domains and permitting all record types (eg A, MX, NS, etc). Note that if a user needs to configure classless reverse delegation, the site administrator must allow access to the appropriate "in-addr.arpa" domain.

    A special condition arises in the case of classless reverse delegation. Since an ISP is free to assign any arbituary naming convention to the in-addr.arpa mappings, it is important to specifically allow a user access to the reverse domains used in classless lookups. For example, if an ISP CNAMEs "4.3.2.1.in-addr.arpa" to "4.subnet0.3.2.1.in-addr", then the user administering reverse lookup in those IP ranges must have the following line in the ACL file :

    When dealing with reverse lookups, dnseditd notation does not reverse IP octets and append "in-addr.arpa". Forward and reverse records are all named similarly, instead the records are placed in forward or reverse databases. Consequently, when used in a forward context, the host component is on the left, and the domain component is on the right. In a reverse context, the host component is on the right, and the domain component is on the left.

    Data Storage

    Dnsedit utilizes its own GDBM backend storage for all DNS records. This is located in the "./db/" directory. In this directory, the filename represents the domainname and holds records of the hosts for that domain. Since records in GDBM are all stored as key/content entries, our "key" will simply be a unique integer, and "content" will be a single DNS record. Since every domain must have an SOA, a "key" of 0 must be present, and its "content" will be the SOA parameters. Any DNS entries from this point will then have incremental "key" values. All "content" data structures will conform to the following format :

    Record Type (int) Object Length (int) Target Length (int) Owner (uid_t) Time (time_t) Object data (variable) Target data (variable)

    From the above diagram, "Object" is the entity we're interested in, and "Target" represents the information related to this entity. For example, in a CNAME record : "foo.example.com CNAME bar.example.com", the record type is CNAME, the Object is "foo.example.com" and the target is "bar.example.com". In this case, Object Length and Target Length are basically the length of the text strings. This scenerio becomes more complex for Objects/Targets which are multi-parameter. Take for example an SOA record. An SOA has several timer parameters, and may optionally have several MX and NS entries. The domainname is the Object, which is simply a text string. However, the Target must hold several (variable length) parameters. To accomplish this, we put fixed length data (and meta data) in front, followed by variable length data behind. Thus, the entire record looks like this :

    SOA Fixed Format Structure
    Variable Lengthed Segment
    Refresh Retry Expire Minimum MX offset NS offset Admin email N
    U
    L
    L
    MX list ... N
    U
    L
    L
    NS list ... N
    U
    L
    L

    1. Admin email is in the format of "[user].[domain]". For example, "hostmaster.example.com".

    2. MX and NS lists are comma seperated (no spaces) fields. For example, "smtp1.example.com,smtp2.example.com".

    3. For MX entries, dnseditd will determine the preference numbers. Hosts which come earlier will have higher priority. If the administrator wishes to have 2 hosts with equal preference, simply specify one MX host in the SOA, and map that host to 2 IPs by creating 2 A records for that host.

    Apart from the SOA record, all other DNS records are relatively simpler to store (since they have simpler parameters). The following table illustrates how different DNS records types are stored by the "content" data structure.

    Type Object Data Target Data
    SOA Text string bearing the domainname. Multiple parameters.
    A Text string bearing hostname. Text string bearing hostname or IP address.
    MX Text string bearing hostname. Text string bearing hostname(s) or IP address(es).
    NS Text string bearing subdomain's name. Text string bearing hostname(s) or IP address(es).
    PTR Text string bearing IP address or classless reverse name. Text string bearing hostname.
    TXT Text string bearing hostname or IP address. Any text string.
    CNAME Text string bearing hostname. Text string bearing hostname.

    Forward records are organized into GDBM files, where each GDBM file stores records for each domainname. For example, a forward record for "www.xyz.com" will be stored in "xyz.com.gdbm". On the other hand, reverse records are saved into reverse GDBMs. A reverse database is one which stores answers to "in-addr.arpa" queries. Dnseditd always prefixes these database names with "_." (since underscore is an illegal character in hostnames) and it typically consists of network numbers. Take a simple reverse record "203.116.36.100 PTR www.xyz.com". This record is stored in the reverse GDBM "_.203.116.36.gdbm". Similarly, for "203.116.36.96-127.100 PTR www.xyz.com", which is a classless reverse lookup zone, dnseditd stores a record in "_.203.116.36.96-127.gdbm". Note that the "_.[foo]" naming convention extends beyond file naming schemes. Dnseditd uses this name format to denote "in-addr.arpa" lookups. That is to say, instead of reversing the IP octets and appending "in-addr.arpa", users should simply use "_.[foo]". For example, "_.203.116.36" in dnsedit terminology is equivalent to "36.116.203.in-addr.arpa" in standard DNS terminology.

    An unfortunate side effect of such reverse database organization is that a single reverse database may in fact hold records belonging to different domain names. Thus all users with reverse records must share common SOA parameters for these reverse databases.

    Command Protocol

    Dnseditd listens on a TCP socket and awaits user connections. Typically users will NOT connect directly to the TCP socket but would instead use the dnsedit commandline tool. The TCP session operates quite similarly to SMTP. It is operates in plain text, line by line. The user is expected to identify himself before being given access to commands. All responses are delivered line by line, with the following prefixes :

      OK: Command executed with no problems.
      INFO: Informative text, typically on-line help.
      DATA: A data stream, typically in response to a user query.
      ERROR: An internal fault or invalid user supplied data.

    A typical login will be as follows :

      % telnet localhost 3230
      Trying 127.0.0.1...
      Connected to localhost.
      Escape character is '^]'.
      OK: Ready.
      user foo
      OK: CommandUser() Username foo.
      pass secret
      OK: CommandPass() Authenticated.
      

    Forward records are placed in domain databases which must be created by SOA declaration (use the "add_soa" command). Parameters within the SOA may be replaced later (use the "replace_soa" command) but the domain name itself cannot be changed. All records created with "add_rec" command are added to the forward lookup database. This includes PTR records. Similarly, the "del_rec" command only removes records from the forward lookup database. To add reverse lookup records (ie N.in-addr.arpa), use the "add_rev" command. Similarly, to remove reverse records, use "del_rev". All changes made by the user only affect the backend GDBM database. In order for changes to be pushed to the DNS server, use the "commit" command. This instructs dnseditd to generate fresh DNS data files for the DNS server software. If the specified domain is a forward zone, then a forward databases are generate, similarly, if a reverse zone is specified, a reverse database is generated. The following commands illustrate the above processes for creating a domain "example.com" :

      % telnet localhost 3230
      Trying 127.0.0.1...
      Connected to localhost.
      Escape character is '^]'.
      OK: Ready.
      user foo
      OK: CommandUser() Username foo.
      pass secret
      OK: CommandPass() Authenticated.
      add_soa example.com root.example.com 300,90,600,60 mx1.example.com,mx2.example.com ns1.example.com,ns2.example.com
      OK: AddSOA() Done.
      add_rec mx1.example.com a 202.6.243.111
      OK: AddRec() Done.
      add_rec mx2.example.com a 202.6.243.112
      OK: AddRec() Done.
      add_rec ns1.example.com a 202.6.243.113
      OK: AddRec() Done.
      add_rec ns2.example.com a 202.6.243.114
      OK: AddRec() Done.
      add_rec redtape.example.com a 202.6.243.115
      OK: AddRec() Done.
      add_rec www.example.com cname redtape.example.com
      OK: AddRec() Done.
      add_rec web.example.com cname redtape.example.com
      OK: AddRec() Done.
      add_rev 202.6.243.115 ptr redtape.example.com
      OK: AddRec() Done.
      commit example.com
      OK: CommitDomain() Queued.
      commit _.202.6.243
      OK: CommitDomain() Queued.
      exit
      Connection to localhost closed by foreign host.
      

    Note that both forward and reverse records have been created in the above example (dnseditd automatically created "example.com.gdbm" as well as "_.202.6.243.gdbm"). So far, the assumption has been that dnseditd manages all forward and reverse lookups. If the user wishes to have forward lookups for the domain delegated to an external DNS server(s), the name servers in the SOA are simply modified to point to the external DNS server(s). If the user wishes to have reverse lookups delegated to an external DNS server, the IP range to be delegated must be declared using the "add_rdelegation" command.

    Reverse Delegation Scenerios

    The representation and storage of forward lookup data has been fairly straight forward so far. Essentially, hosts live in domain databases. But this is different for reverse lookups. In dnseditd, the mechanism of reverse delegation essentially maps the final IP address octet to a text string. A text string is usually a hostname, or may be a CNAME (in the case of classless delegation).

    Typical reverse delegations will be a 4 stage process. For example, to look up 20.243.6.202.in-addr.arpa (ie 202.6.243.20), we first find out which DNS server handles the class A for "202". Then the second query will be for the DNS server handling the class B for "202.6" and the third query will be for the DNS server holding host records for the class C "202.6.243". In this case, the final database will hold the 4th IP octet to host records.

    However, classless reverse delegation changes the 4th step. The difference is that the 4th stage query will be a CNAME to another record. For example, sending a 4th stage query for the host 203.116.36.100 results in :

    If a particular user is in charge of an IP space larger or smaller than a class C, dnseditd must provide an interface for configuring delegation in a flexible and robust manner. Let's say a user is given the IP range 172.20.0.0/18. The user wants to configure reverse delegation in the following manner :

    Scenerio IP Hostname Network Desired DNS Server
    1. Exactly 1 class C 172.20.1.11 ns1.dept.private 172.20.1.0/24 Managed by dnseditd
    172.20.1.12 ns2.dept.private 172.20.1.0/24 Managed by dnseditd
    2. Smaller than a class C 172.20.2.31 db.internal.private 172.20.2.0/25 Managed by dnseditd
    172.20.2.32 backup.internal.private 172.20.2.0/25 Managed by dnseditd
    3. Smaller than a class C 172.20.2.131 www.open.private 172.20.2.128/25 Delegated to 172.20.1.11
    172.20.2.132 smtp.open.private 172.20.2.128/25 Delegated to 172.20.1.11
    4. Several class Cs 172.20.4.51 pc1.lab.private 172.20.4.0/22 Delegated to 172.20.1.12
    172.20.4.52 pc2.lab.private 172.20.4.0/22 Delegated to 172.20.1.12

    Scenerio 1 : This IP range is managed by dnseditd. For each Class C, dnseditd uses a GDBM file called "_.172.20.1.gdbm" to store data containing IP octet to hostname records.

    Scenerio 2 : This class C is split into 2 subnets, the first IP range is managed by dnseditd. Similarly, dnseditd uses a GDBM file called "_.172.20.2.gdbm". At this point, we still don't know what will happen to the rest of the subnet.

    Scenerio 3 : The user wants the second half of the class C to be delegated to a seperate DNS server. Since this is a classless delegation, dnseditd populates the 2nd half of the IP range with CNAME entries (still using GDBM database "_.172.20.2.gdbm").

    Scenerio 4 : The user wants several class Cs delegated to a seperate DNS server. For each class C, dnseditd creates a GDBM database delegating the entire class C to the desired DNS server.

    Classless Reverse Delegation

    While there is more than 1 naming convention for doing classless reverse delegation (see RFC 2317), we will focus on the default method in dnseditd. We return to scenerio 3 in the above section where a /25 is being delegated to a user's DNS server. Configuration needs to be performed on both the parent DNS (ie DNS server delegating the request) and the child DNS (ie the DNS server receiving the delegation). Since the child DNS is responsible for the "open.private" domain, the parent DNS server starts by creating CNAME records for all 128 IPs in the /25 as follows :

    ; this is "named.conf" on parent DNS.
    zone "2.20.172.in-addr.arpa"
      { type master ; file "_.172.20.2" ; } ;

    ; this is "_.172.20.2"
    @ IN SOA open.private. root.open.private. (
      123456789 ; Serial
      600 ; Refresh
      120 ; Retry
      120 ; Expire
      60 ) ; Minimum

    31 PTR db.internal.private.
    32 PTR backup.internal.private.

    128 CNAME 128.open.private.
    129 CNAME 129.open.private.
    130 CNAME 130.open.private.
    131 CNAME 131.open.private.
    132 CNAME 132.open.private.
     :
     :
     :
    254 CNAME 254.open.private.
    255 CNAME 255.open.private.

    Thus, the child DNS server satisfies the original reverse look ups by providing forward lookups for the CNAME redirections introduced by the parent DNS server. The child DNS server would therefore have the following configuration for forward lookups (note it holds records for both forward and "reverse") :

    ; this is "named.conf" on child DNS.
    zone "open.private"
      { type master ; file "open.private.forward" ; } ;

    ; this is "open.private.forward"
    @ IN SOA open.private. root.open.private. (
      123456789 ; Serial
      600 ; Refresh
      120 ; Retry
      120 ; Expire
      60 ) ; Minimum
    IN NS ns1.dept.private.

    www A 172.20.2.131
    131 PTR www.open.private.

    smtp A 172.20.2.132
    132 PTR smtp.open.private.

    So far, we CNAME reverse lookups to your domain name for convenience (ie, child DNS just needs 1 file for both forward and reverse). The fundamental flaw in the above implementation is this : what if a domain name spans 2 or more IP ranges from multiple ISPs ? For example your domain name is "xyz.com" and ISP A assigns you 128.113.24.0/25 and ISP B assigns you 18.200.50.0/25. You have "apple.xyz.com" mapped to 128.113.24.91 and "orange.xyz.com" mapped to 18.200.5.91. What should the PTR record be for "91.xyz.com" ? To prevent this type of naming conflict, we must CNAME "91.24.113.128.in-addr.arpa" and "91.5.200.18.in-addr.arpa" to different names. In event that such a naming conflict occurs, dnseditd must allow the user to override the default CNAME naming convention. For example, "91.24.113.128.in-addr.arpa" gets CNAME'ed to "91.128-113-24.xyz.com" (user creates a new domain "128-113-24.xyz.com") and "91.5.200.18.in-addr.arpa" gets CNAME'ed to "91.18-200-5.xyz.com" (user creates a new domain "18-200-5.xyz.com"). All reverse delegation is configured using the "add_rdelegation" command :

      add_rdelegation <starting IP> <ending IP> <DNS server(s)> [CNAME domainname]

    For example,

      add_rdelegation 128.113.24.0 128.113.24.127 ns1.example.com

    If the "CNAME domainname" parameter is not specified, dnseditd uses its default CNAME naming convention. In the above case for example, dnseditd creates the record "111 CNAME 111.example.com". The domain name "example.com" is derrived from the dns server supplied in the "add_rdelegation" command. If the user supplies more than 1 dns server, the domain name of the first one is used. If a domain spans 2 or more ISPs, the optional "CNAME domainname" parameter may be used to prevent ambiguity. For example :

      add_rdelegation 128.113.24.0 128.113.24.127 ns1.example.com isp1.example.com
      add_rdelegation 18.200.50.0 18.200.50.127 ns1.example.com isp2.example.com

    This causes dnseditd to generate CNAME records :

      (For the 128.113.24.0 network ...)
      0 CNAME 0.isp1.example.com
      1 CNAME 1.isp1.example.com
      2 CNAME 2.isp1.example.com

      (For the 18.200.50.0 network ...)
      0 CNAME 0.isp2.example.com
      1 CNAME 1.isp2.example.com
      2 CNAME 2.isp2.example.com

    Note that if the starting IP and ending IP are not aligned to octet boundaries, dnseditd will treat it as a classless reverse delegation. Thus, if the user issues the command :

      add_rdelegation 202.6.243.0 202.6.245.10 ns1.example.com

    Dnseditd will perform reverse delegations as :

    1. Normal class C delegation of 202.6.243.0/24.
    2. Normal class C delegation of 202.6.244.0/24.
    3. Classless reverse delegation of 202.6.245.0 to 202.6.243.10.

    To remove reverse lookup delegation, use the command :

      del_rdelegation <starting IP> <ending IP> <DNS server(s)> [CNAME domainname]

    Note that the starting IP, ending IP addresses, DNS server(s) and optional domain name prefix, must match those used in the creation of the reverse delegation records. This is so that dnseditd knows exactly which records to remove. Both the commands used to create and delete reverse delegations result in multiple records being generated. Take for example, an IP range starting from 192.168.0.10 and ending at 192.168.255.200. A total of 245 + 200 = 445 CNAME records will be created for the 192.168.0.0/24 and 192.168.255.0/24 networks. Another 253 octet aligned delegations will be created for the 192.168.0.0/16 network, thus a total of 698 records will be generated by this single command alone ! The reverse happens when the delete command is used.

    Forward and Reverse Record Manipulation

    When manipulating DNS records, it is very important to bear in mind whether the changes are being made to forward databases or reverse databases. Technically, a reverse record is defined as any object within the in-addr.arpa domain. Thus a reverse database is a file which stores only in-addr.arpa records. Note that it is perfectly valid for PTR records to exist in both forward and reverse databases.

    A forward database for a domain is created using the "add_soa" command. However, reverse lookups are not tied to domains (a particular range of IPs may be mapped to multiple domain names), but rather they are tied to IP network ranges. Thus there is no command for creating reverse databases. Instead, dnseditd automatically creates them when reverse records are created by users.

    Manipulation of records in forward databases are performed using "add_rec" and "del_rec" commands. These commands always manipulate forward lookup databases. The following commands add records to forward databases.

    Command Description
    add_rec web.example.com a 202.6.243.20 Adds an A record for the host "web" to the "example.com" domain.
    add_rec www.xyz.com cname web.example.com Adds a CNAME record "www" in the "xyz.com" forward database which points to "web.example.com".
    add_rec 20.example.com ptr web.example.com Adds a PTR record for the host "20". Probably a case of classless reverse delegation.
    add_rec web.example.com mx mail.example.com Adds an MX record, stating that mail going to "user@web.example.com" should be delivered to the host "mail.example.com".
    add_rec finance.example.com ns dns1.example.com Adds an NS record, stating that "dns1.example.com" is authoritative for the domain "finance.example.com".

    When manipulating reverse databases, use the "add_rev" and "del_rev" commands. These commands always manipulate reverse lookup databases.

    Command Description
    add_rev 202.6.243.20 ptr web.example.com Adds a PTR entry for "20" into the "202.6.243" reverse database.
    add_rev 202.6.243.50 cname 50.32.243.6.202.in-addr.arpa Adds a CNAME entry for "50" into the "202.6.243" reverse database.
    add_rev 202.6.243.32 ns dns1.example.com Adds a record in the reverse database "202.6.243", stating that "dns1.example.com" is authoritative for the "32" subdomain.
    add_rev 202.6.243.32.50 ptr mail.example.com Adds a record to the reverse database "202.6.243.32", mapping "50" to the host "mail.example.com".

    In summary, the GDBM database manipulated is determined by 2 factors. Firstly, the command determines if the database is a forward or reverse. Secondly, the database name is derived from the object. The following example illustrates a forward record and a classless reverse record :

    Command Description
    add_rec myserver.domain.com a 202.6.243.122
            ^      ^ ^        ^
            `------' `--------'
             record   database 
    This command adds a host entry for "myserver" into the forward database "domain.com.gdbm".
    add_rev 202.6.243.96-127.122 ptr myserver.domain.com
            ^              ^ ^ ^
            `--------------' `-'
                database    record 
    This command adds a host entry for "122" into the reverse database "202.6.243.96-127.gdbm".

    Safety Features

    While dnseditd should be run using a non-root account, it is designed to be safe even if accidentally (or purposefully) started as root. If dnseditd detects it is running as root (uid 0), it will call chroot() to jail itself in the current working directory. Next, it obtains UID from the environment and uses this number to call setuid() and seteuid(). Finally, it repeats the check again to see if it is still running as UID 0, if so, it exits immediately. One exception to this is if dnseditd needs to read the contents of /etc/shadow. That being the case, dnseditd must run as root. To permit this behavior, set the environment "RUN_AS_ROOT=yes".

    All user commands entered (whether successful or not) are logged to disk before the commands are evaluated. The only exception are passwords which are delibrately blanked out. Changes applied to any database are logged in a seperate file. All log entries bear a time stamp, as well as the user responsible for the event. All log files are automatically rotated once they become too large.

    A built in watchdog timer causes the program to self terminate with a core file should it become unresponsive. For example, it got stuck in an infinite loop, or got stuck in blocking IO. In addition, internal memory buffers are monitored by a built in memory management system, which also cause the program to self terminate if an unreasonable number or amount of memory is allocated (eg memory leak). All these parameters may be tuned by setting environment variables.

    Project Status and Known Bugs

    Milestones

    25th Aug 2006. After exactly (coincidentally?) 3 months of development, I'm freezing the 6,000+ lines of code (and scripts) for version 0.1. I'm pleased to say that dnseditd comes with its own diagnostic script which performs 65 various tests on the running application. The code has been built (for 32-bit and 64-bit binaries) and tested on Linux (i386), Linux (alpha), Solaris 9 (sparc), Solaris 10 (sparc) and Solaris 10 (x86). The following tools were used :

    Known Bugs

    If a classless reverse delegation is set up for a particular network, it is possible that it may get superceeded by a user configuring classful reverse delegation for that subnet. This is because dnseditd does not performing any checking for overlapping of records.

    The code does not build on FreeBSD. This is mainly due to the fact that getspnam() does not exist on FreeBSD, and since I'm a lazy pig, the code has not been modified to use alternative means to acquire the user's password for authentication.