Internet Protocols · DNS

Zone Transfers and Secondary Replication: AXFR, IXFR, and the SOA Timers

How a secondary server detects that a zone changed, what a full transfer costs against an incremental one, and how serial number arithmetic makes wrap-around safe.

A zone with more than one authoritative server needs those servers to hold the same data. The protocol’s answer is a replication protocol built from the same message format as everything else: one server is designated primary and holds the writable copy, others are secondaries that periodically pull the zone over TCP. The timers assume a primary that may be unreachable for long periods, and the consequences of that assumption are still visible in how zones behave after an edit.

The SOA as a replication contract

The SOA record does double duty. In a negative answer it supplies the negative TTL, as covered in Negative Caching and the NXDOMAIN Distinction. In replication it is the state machine: four of its numeric fields tell a secondary when to check, how long to wait after a failure, when to give up, and which version of the zone it is looking at; the fifth, MINIMUM, belongs to negative caching rather than replication.

FieldRole in replication
SerialVersion number of the zone contents
RefreshInterval between change checks
RetryInterval between retries after a failed check
ExpireTime after which unreplicated data is discarded
MinimumNegative cache TTL; not a replication timer

The serial is a 32-bit counter that the primary increments on every change. The secondary’s whole job reduces to comparing its serial with the primary’s. If the primary’s serial is greater, the secondary pulls a new copy; if it is equal, nothing has changed; if it is less, the primary has gone backwards, which is an error rather than a signal to roll back.

The refresh interval is the polling period. A secondary wakes every refresh seconds, queries the primary’s SOA, compares serials, and transfers if needed. With a refresh of an hour, a change takes up to an hour to appear on the secondary; with a refresh of a minute, the primary absorbs a query per minute per secondary forever. The number of secondaries divided by the refresh interval is the standing query rate the zone imposes, which is why NOTIFY exists.

Retry governs the case where the check fails: the secondary waits retry seconds and tries again, rather than waiting a full refresh. Expire is the deadline for a secondary that has been unable to reach the primary at all. Once expire seconds have passed since the last successful transfer, the secondary must stop answering for the zone, because its copy is no longer guaranteed to reflect the primary and serving it could return records that were deliberately deleted. The relationship that must hold is expire greater than refresh plus retry by a comfortable margin; a zone where expire is shorter than refresh will have secondaries discarding data during normal operation.

The minimum field is not a timer. RFC 1035 defined it as a floor on the TTL of records exported from the zone, practice used it as the default TTL for records that did not state one, and RFC 2308 redefined it as the negative TTL. Treating it as a replication parameter is a persistent confusion because it sits in the same record as four fields that are timers.

Full transfers and what they cost

An AXFR is a TCP query for the zone’s apex with type AXFR. The response is a stream of messages rather than a single message, each carrying records, and the stream ends with a second copy of the SOA. The first SOA opens the transfer and the closing SOA ends it; a client that sees the closing SOA knows the transfer is complete, and a transfer that ends without it is truncated and must be discarded.

The format has no per-record framing beyond the message boundaries, so the receiving side must count records and verify that the closing SOA matches the opening one. Because the transfer is a sequence of messages, the two-octet TCP length prefix applies to each message independently, and there is no limit on the total size other than the zone’s own size.

The cost of AXFR is proportional to the zone size, not to the change size. A zone with a hundred thousand records transfers all of them to communicate a one-record edit. The transfer time and the receiver’s memory both scale with the record count rather than with the size of the edit, which is why incremental transfer exists.

/* AXFR receiving loop: the transfer is complete only when the SOA repeats. */
int receive_axfr(int fd, struct zone *z)
{
    uint8_t msg[65535];
    uint32_t opening_serial = 0;
    int seen_open = 0;
    for (;;) {
        ssize_t n = read_tcp_message(fd, msg, sizeof msg);
        if (n <= 0)
            return -1;                    /* truncated: no closing SOA */
        struct msg *m = parse(msg, (size_t)n);
        for (struct rr *r = m->answer; r; r = r->next) {
            if (!seen_open && (r->type != TYPE_SOA || r->owner != z->apex))
                return -1;                    /* AXFR must begin with the apex SOA */
            /* The parser interns owner names, so pointer equality compares names. */
            if (r->type == TYPE_SOA && r->owner == z->apex) {
                if (!seen_open) {
                    seen_open = 1;
                    opening_serial = r->soa.serial;
                } else {
                    if (r->soa.serial != opening_serial)
                        return -1;        /* the two SOAs disagree */
                    return 0;             /* closing SOA: commit the zone */
                }
            }
            zone_add(z, r);               /* buffer; do not serve mid-transfer */
        }
    }
}

The commit-at-the-end rule is not a style preference. A secondary that installs records as they arrive serves a partially transferred zone for the duration of the transfer, and a query that lands in that window can see a zone with no MX records and a valid serial. The usual implementation is a shadow copy that becomes live only when the closing SOA validates.

Incremental transfer and the journal it requires

IXFR asks the primary for the difference between the secondary’s serial and the primary’s current one. The secondary sends its SOA in the authority section of the request, and the primary responds either with the full zone, when it has no journal covering that serial or the difference is larger than the zone, or with a sequence of deltas.

Each delta is bracketed by two SOAs: the SOA of the older version, then the records deleted, then the SOA of the newer version, then the records added. A client applies the deltas in order until it reaches the primary’s current serial, which is the serial carried by the SOA that opens the response. A delta whose older serial does not match what the client holds means the client missed one and must fall back to AXFR.

The journal is the primary’s cost. It must retain enough history to cover the range of serials its secondaries might still hold, which in practice means retaining every change since the oldest secondary’s last transfer. A primary that keeps a fixed-size journal can be forced into full transfers by a secondary that has been offline long enough, and the fallback is invisible from the secondary’s side except in transfer duration. Journal size is therefore a function of how long the least reliable secondary can be down, not of the zone size.

A primary is not obliged to serve IXFR. One that does not understand the request answers with the full zone, and the client distinguishes the two cases by inspecting the second record. An incremental response begins with the current SOA and then, as its second record, the SOA of the client’s older version; a full response begins with the current SOA and then a record that is not an SOA. A response whose only record is the current SOA means the client is already up to date.

Serial arithmetic and the wrap at 2^32

The serial is 32 bits and wraps. Comparing two serials with ordinary integer comparison breaks at the wrap: a primary that increments from 4294967295 to 0 appears to have gone backwards.

RFC 1982 defines the comparison as modular arithmetic over half the space. Two serials s1 and s2 are compared by computing (s2 - s1) mod 2^32; if that value is less than 2^31, s2 is greater; if it is greater than 2^31, s2 is less; if it is exactly 2^31, the comparison is undefined because the two values are maximally far apart. The undefined case is a real limit: two serials exactly 2^31 apart have no defined order. A jump of more than 2^31 in one step is worse, because the new serial compares as older than the previous one and secondaries will not transfer at all. Ordinary monotonic increments stay far below that bound.

/* RFC 1982 serial comparison: returns -1, 0, 1, or 2 for undefined. */
int serial_cmp(uint32_t a, uint32_t b)
{
    if (a == b)
        return 0;
    uint32_t d = b - a;                  /* wraps modulo 2^32 by definition */
    if (d < 0x80000000u)
        return 1;                        /* b is ahead of a */
    if (d > 0x80000000u)
        return -1;
    return 2;                            /* exactly half the space apart */
}

The practical implication is that serials should increase by small amounts. A common convention is a date-based serial such as 2025010501, which is readable and monotonic but jumps by large values at each date boundary; the jumps stay below 2^31, so the arithmetic holds. A convention that sets the serial to a timestamp in seconds is also monotonic, and it stays unambiguous as long as the old and new values are less than 2^31 seconds apart, which is about 68 years.

NOTIFY removes the polling delay

NOTIFY is a small out-of-band message the primary sends to its secondaries when the zone changes. It names the zone’s apex in the question with type SOA, may carry the new SOA in the answer section as a hint, and is sent over UDP with the expectation that the secondary will acknowledge it and then perform its normal serial comparison.

The secondary must not trust the notification. It answers the primary, then queries the SOA and compares serials exactly as it would after a refresh tick. A forged notification therefore costs one extra query and nothing else. The reason for the verification is that a NOTIFY can come from anyone who can reach the secondary on the network, and the answer section is only a hint.

The mechanism does not replace refresh. A notification that is lost, filtered, or sent to a stale address leaves the secondary waiting for its next refresh, so the refresh interval becomes the upper bound on staleness rather than the expected delay. Deployments that set refresh to an hour and rely on NOTIFY for promptness are relying on a mechanism with no delivery guarantee, and the correct reading is that NOTIFY shortens the common case while refresh bounds the worst case.

Transfers are also a data exposure surface

AXFR has no authentication in the protocol. A server that permits transfers from any address publishes its entire zone to anyone who asks, which for many zones is a complete inventory of internal hostnames. The control is server-side: allow transfers only to the addresses of known secondaries, and refuse everything else. Because a transfer runs over TCP, an off-path attacker cannot claim an allowed source address without completing the handshake at that address, which makes the check stronger here than for a single UDP query; a host that genuinely holds an allowed address, or that sits on the path, still sees the zone.

TSIG, defined for transaction authentication, adds a shared-secret signature over the request and response. It is the usual mechanism for authenticating transfers between servers that do not share a trusted network, and it covers the transfer’s messages individually. It does not encrypt anything, so a passive observer still sees the zone contents; the signature only establishes who asked and that the answer was not modified.

The exposure is the reason transfers and the query surface are usually configured separately. A server can be authoritative for a zone and answer queries for it from anywhere while restricting AXFR to a list of addresses, and that split is the normal configuration for any zone whose contents are not intended to be public.

References

  1. RFC 5936: DNS Zone Transfer Protocol (AXFR) — IETF
  2. RFC 1995: Incremental Zone Transfer in DNS — IETF
  3. RFC 1996: A Mechanism for Prompt Notification of Zone Changes (DNS NOTIFY) — IETF
  4. RFC 1982: Serial Number Arithmetic — IETF