Skip to content

fix(nsupdate): proper UPDATE response, serial consistency, TTL overflow guard#75

Merged
kweonminsung merged 3 commits into72-feature-add-nsupdate-supportfrom
copilot/sub-pr-73
Mar 24, 2026
Merged

fix(nsupdate): proper UPDATE response, serial consistency, TTL overflow guard#75
kweonminsung merged 3 commits into72-feature-add-nsupdate-supportfrom
copilot/sub-pr-73

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 24, 2026

Three correctness issues in the RFC 2136 nsupdate path identified by code review.

Changes

  • build_response zone echo (src/dns/nsupdate/mod.rs): Added zone_section_end() to locate the end of the first question/zone section in the DNS wire buffer. build_response now copies that section into the response and sets QDCOUNT=1, matching what clients like nsupdate expect — a header-only reply was treated as malformed.

  • Serial consistency (src/dns/nsupdate/update.rs): generate_serial() was called independently inside log_zone_change for every RR change (an N+1 zone reload), risking different serials across changes in the same request. It is now called once in apply_update before the update loop; the resulting new_serial is threaded through apply_single_updateadd_record/delete_recordslog_zone_change and used directly in bump_zone_serial, guaranteeing all ZoneChange rows and the zone serial share the same value.

  • TTL overflow guard (src/dns/nsupdate/update.rs): update.ttl as i32 silently wrapped values above i32::MAX into negatives in the DB. Added an explicit range check in add_record that returns UpdateError::Refused for out-of-range TTLs:

let ttl = if update.ttl > i32::MAX as u32 {
    return Err(UpdateError::Refused(format!(
        "TTL value {} exceeds maximum allowed value ({})",
        update.ttl, i32::MAX
    )));
} else {
    update.ttl as i32
};

⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.

- build_response now copies the zone section into the response and sets
  QDCOUNT=1 so clients don't treat the reply as malformed
- Compute new_serial once per apply_update request and thread it through
  the call chain; removes per-change zone reload in log_zone_change
- Validate u32 TTL <= i32::MAX in add_record before casting to avoid
  negative TTLs being stored in the database

Co-authored-by: kweonminsung <79230043+kweonminsung@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kweonminsung/bindizr/sessions/38cc6a44-4082-40f0-b5df-4e45ff363f4c
Copilot AI changed the title [WIP] Add nsupdate support fix(nsupdate): proper UPDATE response, serial consistency, TTL overflow guard Mar 24, 2026
Copilot AI requested a review from kweonminsung March 24, 2026 20:39
@kweonminsung kweonminsung marked this pull request as ready for review March 24, 2026 20:53
Copilot AI review requested due to automatic review settings March 24, 2026 20:53
@kweonminsung kweonminsung merged commit a682491 into 72-feature-add-nsupdate-support Mar 24, 2026
1 check passed
@kweonminsung kweonminsung deleted the copilot/sub-pr-73 branch March 24, 2026 20:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes correctness issues in the RFC 2136 nsupdate implementation by improving DNS UPDATE response formatting, ensuring a single consistent zone serial is used across all changes in a request, and preventing TTL overflow when persisting records.

Changes:

  • Update DNS UPDATE responses to echo the zone/question section (so clients like nsupdate accept the response as well-formed).
  • Generate a single new_serial per UPDATE request and thread it through change logging and zone serial bumping to keep serials consistent.
  • Add an explicit TTL range check to prevent u32 -> i32 wraparound when storing record TTLs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/dns/nsupdate/mod.rs Builds RFC-expected UPDATE responses by copying the zone/question section into the reply.
src/dns/nsupdate/update.rs Ensures per-request serial consistency and guards against TTL overflow when writing to the DB.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +187 to 188
// Preserve upper flag nibble, set RCODE in lower nibble.
response[3] = (query_data[3] & 0xF0) | (rcode & 0x0F);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_response currently copies the upper flag nibble from the query (query_data[3] & 0xF0). That nibble contains RA/AD/CD and reserved Z bits, and mirroring it can produce incorrect or non-compliant responses (e.g., echoing AD=1 even though the server didn't authenticate anything). Consider explicitly setting these bits for UPDATE responses (typically RA=0 and Z=0; only copy bits you intentionally support) and only OR in the RCODE.

Suggested change
// Preserve upper flag nibble, set RCODE in lower nibble.
response[3] = (query_data[3] & 0xF0) | (rcode & 0x0F);
// For UPDATE responses, do not mirror RA/AD/CD/Z bits from the query.
// Explicitly set the upper nibble to 0 and only encode the RCODE.
response[3] = rcode & 0x0F;

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +176
let qdcount = u16::from_be_bytes([query_data[4], query_data[5]]);
let zone_end = if qdcount > 0 {
zone_section_end(query_data)
} else {
None
};
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zone section is echoed whenever qdcount > 0, and the response then forces QDCOUNT=1. If a malformed UPDATE arrives with QDCOUNT != 1 (which the parser rejects as InvalidHeader), this response will claim a single question and copy only the first one, making the reply internally inconsistent. Consider only echoing the zone/question section when qdcount == 1 and zone_section_end() succeeds; otherwise leave QDCOUNT=0 (or return None and drop the packet) to avoid sending a structurally confusing response.

Copilot uses AI. Check for mistakes.
offset += 1;
break;
}

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zone_section_end treats any length byte that isn't 0, a normal label (00xxxxxx), or a compression pointer (11xxxxxx) as a “regular label”. Values with 01xxxxxx / 10xxxxxx are reserved label types in DNS wire format; accepting them can cause the function to parse/echo invalid names and potentially allocate/copy unexpectedly large slices. Consider rejecting those reserved forms (return None) when (len & 0xC0) != 0 && (len & 0xC0) != 0xC0.

Suggested change
// Reserved label types (01xxxxxx / 10xxxxxx) are invalid.
if (len & 0xC0) != 0 {
return None;
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants