Skip to content

feat(dhcp): include the host's IPv6 DNS resolvers in the agent's DHCP config#2686

Open
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-2640
Open

feat(dhcp): include the host's IPv6 DNS resolvers in the agent's DHCP config#2686
chet wants to merge 1 commit into
NVIDIA:mainfrom
chet:gh-issue-2640

Conversation

@chet

@chet chet commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

The dual-stack DhcpConfig field carbide_nameservers_v6 landed in #2672, but nothing fills it — from_forge_dhcp_config and the agent's config builders are still IPv4-only. This has the agent populate it from the DPU's ServiceAddresses (which already holds both families):

  • A split_nameservers_by_family helper splits the nameservers once, used at both DHCP-config build sites (the gRPC control path and the on-disk config path) so they can't diverge.
  • from_forge_dhcp_config / build_server_config thread the IPv6 set through into the config.
  • The IPv4 DHCPv4 option-6 path is unchanged; the IPv6 set stays inert until a delivery channel (DHCPv6 / RA) consumes it.

Tests cover from_forge_dhcp_config populating the field and the family split.

Implements #2640. Part of the #2628 epic (dual-stack DNS resolver advertisement).

Reworked onto current main. The original version stacked on #2685 (Task 1), now closed as superseded by #2672 — which already added the proto/model fields this PR builds on. So this PR is now just the agent-population step.

Draft pending review.

@copy-pr-bot

copy-pr-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f5f18a1c-b0c4-486a-8cb7-ea11b4399e1c

📥 Commits

Reviewing files that changed from the base of the PR and between d762d21 and d7ccc64.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • crates/agent/Cargo.toml
  • crates/agent/src/dhcp.rs
  • crates/agent/src/ethernet_virtualization.rs
  • crates/rpc-utils/src/dhcp.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/agent/src/dhcp.rs
  • crates/rpc-utils/src/dhcp.rs
  • crates/agent/src/ethernet_virtualization.rs

Summary by CodeRabbit

  • New Features
    • Added support for advertising IPv6 DNS servers in generated DHCP configuration, alongside existing IPv4 nameserver support.
  • Bug Fixes
    • Improved handling of mixed IPv4/IPv6 resolver lists so IPv6 nameservers are correctly separated and propagated to DHCP output.
  • Tests
    • Added/updated unit tests to verify per-family IPv4/IPv6 partitioning and JSON round-trip behavior, including backward-compatible deserialization when IPv6 fields are absent.

Walkthrough

IPv6 DNS nameserver support is added to the DHCP configuration stack. A new carbide_nameservers_v6: Vec<Ipv6Addr> field is introduced in DhcpConfig, and from_forge_dhcp_config is extended to accept and populate it. A shared split_nameservers_by_family helper partitions IpAddr slices by address family; both address families are threaded through the agent's file-write and gRPC DHCP paths, with comprehensive unit test coverage validating the partition logic and round-trip serialization.

Changes

IPv6 Nameserver Propagation through DHCP Stack

Layer / File(s) Summary
rpc-utils DhcpConfig IPv6 field and from_forge_dhcp_config extension
crates/rpc-utils/src/dhcp.rs
Adds carbide_nameservers_v6: Vec<Ipv6Addr> to DhcpConfig with serde default for backward compatibility. from_forge_dhcp_config signature is extended to accept carbide_nameservers_v6 and populate the new field.
rpc-utils test helper and scenario coverage for IPv6
crates/rpc-utils/src/dhcp.rs
Extends DhcpConfigSummary with nameservers_v6 field. summarize_dhcp_config is updated to accept and forward IPv6 nameservers through from_forge_dhcp_config. The configured-addresses scenario is updated to assert IPv6 nameserver round-trip correctness and JSON deserialization backward compatibility.
Agent build_server_config IPv6 parameter and wiring
crates/agent/src/dhcp.rs
Imports Ipv6Addr. Public build_server_config function signature is extended with nameservers_v6: Vec<Ipv6Addr> parameter, which is forwarded into DhcpConfig::from_forge_dhcp_config.
Agent split_nameservers_by_family helper and DHCP path wiring
crates/agent/src/ethernet_virtualization.rs
Imports Ipv6Addr. Introduces split_nameservers_by_family helper to partition IpAddr slices into per-family vectors. Replaces IPv4-only nameserver filtering in update_dhcp_via_grpc with family-aware partitioning and passes nameservers_v6 to from_forge_dhcp_config. Similarly updates write_dhcp_v4_server_config to derive both nameservers_v4 and nameservers_v6 via the helper and passes both to build_server_config. Includes unit test validating correct partition of mixed IPv4/IPv6 nameserver inputs.
Dev-dependencies update
crates/agent/Cargo.toml
Adds carbide-test-support local dev dependency.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

Possibly related PRs

  • NVIDIA/infra-controller#2672: Both PRs wire the same carbide_nameservers_v6 data through the DHCP configuration stack—this PR generates and passes nameservers_v6 into DhcpConfig, while that PR extends the gRPC/proto conversion layer to populate carbide_nameservers_v6 from the model.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary objective: adding IPv6 DNS resolver support to the agent's DHCP configuration.
Description check ✅ Passed The description comprehensively explains the changes, implementation approach, rationale, and how it addresses the stated issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@chet

chet commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai PTAL, thank you!

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Fetch PR details
gh pr view 2686 --repo NVIDIA/infra-controller --json title,body,files,commits,baseRefName,headRefName

echo "---FILES CHANGED---"
gh pr diff 2686 --repo NVIDIA/infra-controller

Length of output: 20399



@chet — thank you for the thorough PR description and test coverage. The implementation is fundamentally sound: the proto evolution is safe, the serde backward-compatibility design (default + skip_serializing_if) is correct, and the error-propagation path through collect::<Result<Vec<_>, _>>()? is idiomatic. A few observations below.


1. Duplicated nameserver-splitting logic (maintainability)

The filter_map block that extracts Ipv6Addr from service_addrs.nameservers appears verbatim in two locations within ethernet_virtualization.rsupdate_dhcp_via_grpc and write_dhcp_v4_server_config. The IPv4 equivalent is similarly duplicated. A small helper would make a future change (e.g., adding a third family or a log line) a single-site edit:

/// Splits a dual-stack nameserver list into its IPv4 and IPv6 components.
fn split_nameservers(addrs: &[IpAddr]) -> (Vec<Ipv4Addr>, Vec<Ipv6Addr>) {
    addrs.iter().fold((vec![], vec![]), |(mut v4, mut v6), addr| {
        match addr {
            IpAddr::V4(a) => v4.push(*a),
            IpAddr::V6(a) => v6.push(*a),
        }
        (v4, v6)
    })
}

Then at each call site:

let (nameservers_v4, nameservers_v6) = split_nameservers(&service_addrs.nameservers);

This is not a blocker, but it is the cleaner production-grade form.


2. Variable shadowing in filter_map closures (minor style)

.filter_map(|x| match x {
    IpAddr::V6(x) => Some(*x),  // inner `x` shadows outer `x`
    _ => None,
})

Prefer distinct names to eliminate the implicit shadowing:

.filter_map(|addr| match addr {
    IpAddr::V6(v6) => Some(*v6),
    _ => None,
})

3. Everything else looks good

  • Proto field 9: safe addition; repeated fields default to empty in protobuf3 — no wire-format breakage.
  • #[serde(default, skip_serializing_if = "Vec::is_empty")]: correctly handles both forward and backward compatibility on disk.
  • TryFrom error propagation in grpc_server.rs: collect::<Result<Vec<_>, _>>()? is the right pattern; the rejects_malformed_ipv6_nameserver test provides meaningful regression coverage.
  • Default impl includes carbide_nameservers_v6: vec![] — correct.
  • Comment update in `write_dhcp_v4_server_config**: the new wording is clearer than the removed TODO.

The two items above are code-quality suggestions; nothing here is a correctness concern. Approving is reasonable once you've weighed the helper-extraction trade-off against the PR's stated scope.

@chet

chet commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/agent/src/ethernet_virtualization.rs (1)

1069-1085: ⚡ Quick win

Extract nameserver family splitting into one helper to prevent path drift.

The same split logic is duplicated in Line 1069+ and Line 1487+. Centralizing it avoids divergence between gRPC and file-write paths.

♻️ Proposed refactor
+fn split_nameservers_by_family(nameservers: &[IpAddr]) -> (Vec<Ipv4Addr>, Vec<Ipv6Addr>) {
+    nameservers.iter().fold((Vec::new(), Vec::new()), |mut acc, addr| {
+        match addr {
+            IpAddr::V4(v4) => acc.0.push(*v4),
+            IpAddr::V6(v6) => acc.1.push(*v6),
+        }
+        acc
+    })
+}
-    let nameservers_v4 = service_addrs
-        .nameservers
-        .iter()
-        .filter_map(|x| match x {
-            IpAddr::V4(x) => Some(*x),
-            _ => None,
-        })
-        .collect::<Vec<Ipv4Addr>>();
-
-    let nameservers_v6 = service_addrs
-        .nameservers
-        .iter()
-        .filter_map(|x| match x {
-            IpAddr::V6(x) => Some(*x),
-            _ => None,
-        })
-        .collect::<Vec<Ipv6Addr>>();
+    let (nameservers_v4, nameservers_v6) =
+        split_nameservers_by_family(&service_addrs.nameservers);

As per coding guidelines, prefer simple/explicit implementations with justified abstractions; this shared helper keeps dual-stack behavior consistent.

Also applies to: 1487-1503

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agent/src/ethernet_virtualization.rs` around lines 1069 - 1085,
Extract the nameserver IPv4 and IPv6 filtering logic that appears around line
1069 (where nameservers_v4 and nameservers_v6 are created using filter_map
operations) into a single helper function. This same filtering logic is
duplicated around line 1487 in the file-write path. Create a helper function
that takes the service_addrs.nameservers collection and returns both IPv4 and
IPv6 nameserver lists, then use this helper in both locations to prevent the
gRPC and file-write paths from diverging.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/agent/src/ethernet_virtualization.rs`:
- Around line 1069-1085: Extract the nameserver IPv4 and IPv6 filtering logic
that appears around line 1069 (where nameservers_v4 and nameservers_v6 are
created using filter_map operations) into a single helper function. This same
filtering logic is duplicated around line 1487 in the file-write path. Create a
helper function that takes the service_addrs.nameservers collection and returns
both IPv4 and IPv6 nameserver lists, then use this helper in both locations to
prevent the gRPC and file-write paths from diverging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ce8b94d5-2b5a-46bb-8415-78093e586a79

📥 Commits

Reviewing files that changed from the base of the PR and between 6d36a43 and 9dea821.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • crates/agent/src/dhcp.rs
  • crates/agent/src/dhcp_server_grpc_client.rs
  • crates/agent/src/ethernet_virtualization.rs
  • crates/dhcp-server/Cargo.toml
  • crates/dhcp-server/proto/dhcp_server_control.proto
  • crates/dhcp-server/src/grpc_server.rs
  • crates/rpc-utils/src/dhcp.rs

@chet

chet commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai Thanks for the eyes. Updated -- extracted a split_nameservers_by_family helper used at both the gRPC and file-write DHCP-config sites, so the v4/v6 split cannot drift. PTAL. Resolve if good, thanks!

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh pr diff 2686 --repo NVIDIA/infra-controller | grep -A 60 "split_nameservers"

Length of output: 4564



@chet — the extraction is clean. split_nameservers_by_family uses distinct names (v4_addr, v6_addr) throughout, eliminating the shadowing concern simultaneously, and both call sites now delegate to it with a single destructuring line. The test fixtures cover both the populated and empty-vec paths. Nothing outstanding — resolving.

🐇 ✨

[resolve]

@chet

chet commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chet

chet commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
crates/agent/src/ethernet_virtualization.rs (2)

143-153: ⚡ Quick win

Prefer iterator-based construction in split_nameservers_by_family.

This helper is correct, but building the result vectors via a for loop diverges from repo guidance to prefer iterator-based Vec construction.

♻️ Proposed refactor
 fn split_nameservers_by_family(nameservers: &[IpAddr]) -> (Vec<Ipv4Addr>, Vec<Ipv6Addr>) {
-    let mut v4 = Vec::new();
-    let mut v6 = Vec::new();
-    for addr in nameservers {
-        match addr {
-            IpAddr::V4(v4_addr) => v4.push(*v4_addr),
-            IpAddr::V6(v6_addr) => v6.push(*v6_addr),
-        }
-    }
-    (v4, v6)
+    nameservers
+        .iter()
+        .copied()
+        .fold((Vec::new(), Vec::new()), |(mut v4, mut v6), addr| {
+            match addr {
+                IpAddr::V4(v4_addr) => v4.push(v4_addr),
+                IpAddr::V6(v6_addr) => v6.push(v6_addr),
+            }
+            (v4, v6)
+        })
 }

As per coding guidelines, "When building a Vec or a HashMap in Rust, prefer using iterators to building them from a for-loop."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agent/src/ethernet_virtualization.rs` around lines 143 - 153, The
split_nameservers_by_family function currently uses a for loop with manual
vector mutation to build the result vectors, which diverges from the repo's
coding guidelines to prefer iterator-based Vec construction. Refactor this
function to use iterator methods (such as filter_map or filter combined with
collect) to separate the nameservers by IP version (IPv4 and IPv6) rather than
using explicit for loops and push operations. This will make the code more
idiomatic and align with project conventions.

Source: Coding guidelines


3313-3328: ⚡ Quick win

Use a table-driven test for nameserver family partitioning.

This operation maps inputs to observable outputs, so table-driven style is preferred here; it will also make it easy to cover empty, v4-only, and v6-only cases.

🧪 Proposed refactor
 #[test]
-fn split_nameservers_by_family_partitions_both_families() {
-    let (v4, v6) = split_nameservers_by_family(&[
-        IpAddr::from([10, 0, 0, 1]),
-        "2001:db8::1".parse().unwrap(),
-        IpAddr::from([10, 0, 0, 2]),
-    ]);
-    assert_eq!(
-        v4,
-        vec![Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 2)]
-    );
-    assert_eq!(
-        v6,
-        vec!["2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap()]
-    );
+fn split_nameservers_by_family_partitions_by_family() {
+    use carbide_test_support::value_scenarios;
+
+    value_scenarios!(
+        run = |input: Vec<IpAddr>| split_nameservers_by_family(&input);
+        "split nameservers" {
+            vec![
+                IpAddr::from([10, 0, 0, 1]),
+                "2001:db8::1".parse().unwrap(),
+                IpAddr::from([10, 0, 0, 2]),
+            ] => (
+                vec![Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 2)],
+                vec!["2001:db8::1".parse().unwrap()],
+            ),
+            vec![IpAddr::from([10, 0, 0, 1])] => (vec![Ipv4Addr::new(10, 0, 0, 1)], vec![]),
+            vec!["2001:db8::1".parse().unwrap()] => (vec![], vec!["2001:db8::1".parse().unwrap()]),
+            vec![] => (vec![], vec![]),
+        }
+    );
 }

As per coding guidelines, "Prefer table-driven tests for any function that maps inputs to outputs" and "Use carbide_test_support::value_scenarios! macro for total operations."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/agent/src/ethernet_virtualization.rs` around lines 3313 - 3328, The
test `split_nameservers_by_family_partitions_both_families` should be refactored
from a single-case test to a table-driven test format using the
`carbide_test_support::value_scenarios!` macro. Define multiple test scenarios
to cover all edge cases: empty nameserver list, IPv4-only addresses, IPv6-only
addresses, and mixed IPv4/IPv6 addresses. Since `split_nameservers_by_family` is
a total operation that maps inputs to outputs, the table-driven approach will
provide better coverage and maintainability.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/agent/src/ethernet_virtualization.rs`:
- Around line 143-153: The split_nameservers_by_family function currently uses a
for loop with manual vector mutation to build the result vectors, which diverges
from the repo's coding guidelines to prefer iterator-based Vec construction.
Refactor this function to use iterator methods (such as filter_map or filter
combined with collect) to separate the nameservers by IP version (IPv4 and IPv6)
rather than using explicit for loops and push operations. This will make the
code more idiomatic and align with project conventions.
- Around line 3313-3328: The test
`split_nameservers_by_family_partitions_both_families` should be refactored from
a single-case test to a table-driven test format using the
`carbide_test_support::value_scenarios!` macro. Define multiple test scenarios
to cover all edge cases: empty nameserver list, IPv4-only addresses, IPv6-only
addresses, and mixed IPv4/IPv6 addresses. Since `split_nameservers_by_family` is
a total operation that maps inputs to outputs, the table-driven approach will
provide better coverage and maintainability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 342fddd1-a72a-4d98-9693-6fa4a84eb037

📥 Commits

Reviewing files that changed from the base of the PR and between 47e42bc and d762d21.

📒 Files selected for processing (3)
  • crates/agent/src/dhcp.rs
  • crates/agent/src/ethernet_virtualization.rs
  • crates/rpc-utils/src/dhcp.rs

The dual-stack `DhcpConfig` added in NVIDIA#2672 has a `carbide_nameservers_v6` field, but nothing fills it -- `from_forge_dhcp_config` and the agent's config builders are IPv4-only, so a host's IPv6 DNS resolvers never reach the config. The agent already learns both families from the DPU's `ServiceAddresses`; this splits them by family and fills `carbide_nameservers_v6` at both DHCP-config build sites (the gRPC control path and the on-disk config path), threading the IPv6 set through `from_forge_dhcp_config` and `build_server_config`.

A `split_nameservers_by_family` helper does the split once, so the two build sites cannot diverge. The IPv4 DHCPv4 option-6 path is unchanged; the IPv6 set stays inert until a delivery channel consumes it (DHCPv6 or RA, tracked by the broader IPv6 effort).

Tests cover `from_forge_dhcp_config` populating the v6 field and the family split itself.

This supports NVIDIA#2640.

Signed-off-by: Chet Nichols III <chetn@nvidia.com>
@chet

chet commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai Thanks for the eyes. Updated -- split_nameservers_by_family now folds into the two vecs instead of a for-loop, and its test is a value_scenarios! table (both / v4-only / v6-only / empty). PTAL. Resolve if good, thanks!

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh pr diff 2686 --repo NVIDIA/infra-controller | grep -A 80 "split_nameservers_by_family"

Length of output: 6837



@chet — the revision is production-grade. .iter().copied().fold(...) is the correct idiomatic form here: the copied() call eliminates the dereference noise inside the match arms, and the fold accumulates cleanly into a tuple without any exposed intermediate mut bindings at the call sites. The value_scenarios! table covers every discriminant of the partition (mixed → both non-empty, v4-only → v6 empty, v6-only → v4 empty, empty → both empty), which is exactly the right boundary coverage for a splitting function. No concerns outstanding — resolving.

🐇 ✨

[resolve]

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.

1 participant