- 
                Notifications
    
You must be signed in to change notification settings  - Fork 121
 
feat(dhcpv6): Add DHCPv6 support for macvlan and bridge drivers #1323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
          
Reviewer's GuideThis PR adds full stateful DHCPv6 support alongside DHCPv4 by refactoring the proxy layer into a unified DhcpService enum, introducing a new DhcpV6Service using the mozim library, extending gRPC Lease messages for IPv6, adjusting network drivers to configure IPv6 sysctls, updating client code to perform sequential IPv4/IPv6 leases, and enhancing test scripts and dependencies to cover IPv6 scenarios. Class diagram for unified DHCP service and lease typesclassDiagram
    class DhcpService {
        +get_net_config()
        +get_previous_lease()
        +set_previous_lease(lease)
        +release_lease()
    }
    class DhcpV4Service {
        +client
        +network_config
        +previous_lease
        +release_lease()
    }
    class DhcpV6Service {
        +client
        +network_config
        +previous_lease
        +get_lease()
        +release_lease()
    }
    class MozimLease {
    }
    class MozimV4Lease {
    }
    class MozimV6Lease {
    }
    DhcpService <|-- DhcpV4Service
    DhcpService <|-- DhcpV6Service
    MozimLease <|-- MozimV4Lease
    MozimLease <|-- MozimV6Lease
    DhcpV4Service --> MozimV4Lease : previous_lease
    DhcpV6Service --> MozimV6Lease : previous_lease
    DhcpService --> MozimLease : get_previous_lease/set_previous_lease
    File-Level Changes
 Assessment against linked issues
 Possibly linked issues
 Tips and commandsInteracting with Sourcery
 Customizing Your ExperienceAccess your dashboard to: 
 Getting Help
  | 
    
eba4a4b    to
    68032d7      
    Compare
  
    | 
           Ephemeral COPR build failed. @containers/packit-build please check.  | 
    
    
      
        5 similar comments
      
    
  
    | 
           Ephemeral COPR build failed. @containers/packit-build please check.  | 
    
| 
           Ephemeral COPR build failed. @containers/packit-build please check.  | 
    
| 
           Ephemeral COPR build failed. @containers/packit-build please check.  | 
    
| 
           Ephemeral COPR build failed. @containers/packit-build please check.  | 
    
| 
           Ephemeral COPR build failed. @containers/packit-build please check.  | 
    
68032d7    to
    3aa0fe1      
    Compare
  
    29ba56a    to
    7b52545      
    Compare
  
    There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes - here's some feedback:
- lease_has_changed’s catch-all arm treats any mismatched pair as ‘changed’, which may hide logic errors—replace the wildcard with unreachable!() or explicit error handling.
 
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- lease_has_changed’s catch-all arm treats any mismatched pair as ‘changed’, which may hide logic errors—replace the wildcard with unreachable!() or explicit error handling.
## Individual Comments
### Comment 1
<location> `src/dhcp_proxy/dhcp_service.rs:296` </location>
<code_context>
+        (MozimLease::V6(old_v6), MozimLease::V6(new_v6)) => {
+            old_v6.addr != new_v6.addr || old_v6.prefix_len != new_v6.prefix_len
+        }
+        _ => true, // could have used unreachable!()
+    }
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using 'true' for mismatched lease types in lease_has_changed may mask logic errors.
Using 'true' here may conceal incorrect mixing of lease types. Instead, consider unreachable!() or logging to highlight these cases during development.
```suggestion
        _ => unreachable!("Mismatched MozimLease types in lease_has_changed"),
```
</issue_to_address>
### Comment 2
<location> `src/dhcp_proxy/dhcp_service.rs:369` </location>
<code_context>
             }
+            // NO gateway logic for IPv6. This is intentional.
         }
+        _ => return Err(NetavarkError::msg("Lease type mismatch during IP update")),
     }
-
</code_context>
<issue_to_address>
**suggestion:** Returning a generic error for lease type mismatch may obscure the root cause.
Include the actual lease types in the error message to make debugging easier.
```suggestion
        _ => {
            return Err(NetavarkError::msg(format!(
                "Lease type mismatch during IP update: new lease type is {:?}, old lease type is {:?}",
                new_lease, old_lease
            )));
        }
```
</issue_to_address>
### Comment 3
<location> `src/network/dhcp.rs:67-70` </location>
<code_context>
+
+    // If IPv4 fails but IPv6 is requested, should we continue or fail all?
+    // For now, we'll let it continue.
+    let v4_lease = match get_lease_from_proxy(nvp_config_v4) {
+        Ok(l) => l,
+        Err(e) => {
+            return Err(NetavarkError::msg(format!("unable to obtain lease: {e}")));
+        }
+    };
</code_context>
<issue_to_address>
**suggestion:** Failing immediately on IPv4 lease error may prevent IPv6-only containers from working.
If ipam.ipv6_enabled is true and no IPv4 addresses are needed, consider allowing the process to continue when IPv4 lease acquisition fails.
Suggested implementation:
```rust
    let v4_lease = match get_lease_from_proxy(nvp_config_v4) {
        Ok(l) => l,
        Err(e) => {
            // If IPv6 is enabled and no IPv4 addresses are needed, allow to continue
            if ipam.ipv6_enabled && nvp_config_v4.is_none() {
                // No IPv4 lease, but that's acceptable for IPv6-only containers
                None
            } else {
                return Err(NetavarkError::msg(format!("unable to obtain lease: {e}")));
            }
        }
    };
```
You may need to ensure that `nvp_config_v4` is an `Option` and that downstream code can handle `v4_lease` being `None`. If `v4_lease` is expected to always be present, update its type to `Option<LeaseType>` (or whatever type is returned by `get_lease_from_proxy`). Also, make sure that the logic for handling leases later in the code can handle the case where `v4_lease` is `None`.
</issue_to_address>
### Comment 4
<location> `test-dhcp/002-setup.bats:33-42` </location>
<code_context>
+: <<'DISABLED_TEST'
</code_context>
<issue_to_address>
**issue (testing):** IPv6 setup test is currently disabled.
Please enable the test to verify DHCPv6 functionality. If it cannot be enabled now, add a note explaining the reason and a plan for future activation.
</issue_to_address>
### Comment 5
<location> `test-dhcp/helpers.bash:344-353` </location>
<code_context>
+function run_dhcp() {
</code_context>
<issue_to_address>
**suggestion (testing):** No negative/edge case tests for DHCPv6 lease failures.
Please add tests for scenarios such as no available addresses, misconfigured dnsmasq, and timeouts to ensure proper error handling and messaging.
</issue_to_address>
### Comment 6
<location> `src/dhcp_proxy/dhcp_service.rs:300` </location>
<code_context>
+    }
+}
+
 fn update_lease_ip(
     netns: &str,
     interface: &str,
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the large match blocks in `update_lease_ip` and `lease_has_changed` into smaller helper functions for each IP version to improve readability and maintainability.
Here’s one way to tame the nesting in `update_lease_ip` (and the free‐fn `lease_has_changed`) by pulling each IP/version case into its own small helper.  You’ll end up with a single `match` that just calls into two focused routines.
```rust
fn update_lease_ip(
    netns: &str,
    interface: &str,
    old_lease: &MozimLease,
    new_lease: &MozimLease,
) -> NetavarkResult<()> {
    let (_, ns) = core_utils::open_netlink_sockets(netns)
        .wrap("failed to open netlink socket in netns")?;
    let mut sock = ns.netlink;
    match (old_lease, new_lease) {
        (MozimLease::V4(o), MozimLease::V4(n)) =>
            update_lease_ip_v4(&mut sock, interface, o, n),
        (MozimLease::V6(o), MozimLease::V6(n)) =>
            update_lease_ip_v6(&mut sock, interface, o, n),
        _ => Err(NetavarkError::msg("Lease type mismatch during IP update")),
    }
}
fn update_lease_ip_v4(
    sock: &mut NetlinkSocket, interface: &str,
    old: &MozimV4Lease, new: &MozimV4Lease
) -> NetavarkResult<()> {
    let old_net = ipnet::Ipv4Net::with_netmask(old.yiaddr, old.subnet_mask)?;
    let new_net = ipnet::Ipv4Net::with_netmask(new.yiaddr, new.subnet_mask)?;
    if new_net != old_net {
        let link = sock.get_link(LinkID::Name(interface.to_string()))?
            .wrap("get interface in netns")?;
        sock.add_addr(link.header.index, &ipnet::IpNet::V4(new_net))
            .wrap("add new addr")?;
        sock.del_addr(link.header.index, &ipnet::IpNet::V4(old_net))
            .wrap("remove old addr")?;
    }
    // ... default‐gateway logic here ...
    Ok(())
}
fn update_lease_ip_v6(
    sock: &mut NetlinkSocket, interface: &str,
    old: &MozimV6Lease, new: &MozimV6Lease
) -> NetavarkResult<()> {
    let old_net = ipnet::Ipv6Net::new(old.addr, old.prefix_len)?;
    let new_net = ipnet::Ipv6Net::new(new.addr, new.prefix_len)?;
    if new_net != old_net {
        let link = sock.get_link(LinkID::Name(interface.to_string()))?
            .wrap("get interface in netns")?;
        sock.add_addr(link.header.index, &ipnet::IpNet::V6(new_net))?;
        sock.del_addr(link.header.index, &ipnet::IpNet::V6(old_net))?;
    }
    Ok(())
}
```
And likewise push `lease_has_changed` onto the enum itself:
```rust
impl MozimLease {
    fn has_changed(&self, other: &MozimLease) -> bool {
        match (self, other) {
          (MozimLease::V4(a), MozimLease::V4(b)) =>
            a.yiaddr   != b.yiaddr ||
            a.subnet_mask != b.subnet_mask ||
            a.gateways != b.gateways,
          (MozimLease::V6(a), MozimLease::V6(b)) =>
            a.addr     != b.addr ||
            a.prefix_len != b.prefix_len,
          _ => true,
        }
    }
}
```
This flattening pulls each version’s logic out of one huge `match` block and into small, self‐contained functions, making both the control flow and each protocol’s behavior easier to read & maintain.
</issue_to_address>
### Comment 7
<location> `src/network/dhcp.rs:54` </location>
<code_context>
+    // --- Perform DHCPv4 Lease ---
+    // For now, we assume IPv4 is always desired unless explicitly disabled in future logic.
+    // You could make this conditional based on `ipam.container_addresses` if needed.
+    let nvp_config_v4 = NetworkConfig {
+        version: 0, // Explicitly set for IPv4
         host_iface: host_network_interface.to_string(),
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting a helper function for NetworkConfig creation and using Option::or to simplify merging logic.
```rust
// 1. Extract a helper to remove the v4/v6 NetworkConfig duplication:
fn build_network_config(
    host_iface: &str,
    container_iface: &str,
    ns_path: &str,
    mac: &str,
    hostname: &str,
    container_id: &str,
    version: u8,
) -> NetworkConfig {
    NetworkConfig {
        version,
        host_iface: host_iface.to_string(),
        container_iface: container_iface.to_string(),
        ns_path: ns_path.to_string(),
        container_mac_addr: mac.to_string(),
        host_name: hostname.to_string(),
        container_id: container_id.to_string(),
        domain_name: "".into(),
    }
}
// Then in get_dhcp_lease:
let nvp_v4 = build_network_config(
    host_network_interface,
    container_network_interface,
    ns_path,
    container_macvlan_mac,
    container_hostname,
    container_id,
    0,
);
let v4_lease = get_lease_from_proxy(nvp_v4)?;
// ...
if ipam.ipv6_enabled {
    let nvp_v6 = build_network_config(
        host_network_interface,
        container_network_interface,
        ns_path,
        container_macvlan_mac,
        container_hostname,
        container_id,
        1,
    );
    let (v6_subnet, v6_dns, v6_domain) = parse_lease(get_lease_from_proxy(nvp_v6)?)?;
    subnets.push(v6_subnet);
    // 2. Simplify the merge logic with `Option::or`:
    all_dns_servers   = all_dns_servers.or(v6_dns);
    all_domain_names  = all_domain_names.or(v6_domain);
}
```
</issue_to_address>
### Comment 8
<location> `src/dhcp_proxy/lib.rs:117` </location>
<code_context>
+    // DhcpV6Lease
+    // here the fields of DhcpV4Lease are repurposed tarry their equivalents in the DhcpV6Lease
+    impl From<DhcpV6Lease> for Lease {
+        fn from(l: DhcpV6Lease) -> Lease {
+            let mut domain_name = String::new();
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the option-parsing loop into separate helper functions for clarity and maintainability.
```markdown
You can collapse that 70-line loop into three small, self-contained parsers. For example:
```rust
impl From<DhcpV6Lease> for Lease {
    fn from(l: DhcpV6Lease) -> Lease {
        let dns_servers   = parse_dns_servers(&l.dhcp_opts);
        let domain_name   = parse_domain_search(&l.dhcp_opts);
        let ntp_servers   = parse_ntp_servers(&l.dhcp_opts);
        Lease {
            t1:            l.t1,
            t2:            l.t2,
            lease_time:    l.valid_life,
            yiaddr:        l.addr.to_string(),
            siaddr:        l.srv_ip.to_string(),
            srv_id:        hex::encode(l.srv_duid),
            is_v6:         true,
            domain_name,
            dns_servers,
            ntp_servers,
            subnet_mask:   l.prefix_len.to_string(),
            mac_address:   String::new(),
            mtu:           0,
            broadcast_addr:String::new(),
            gateways:      Vec::new(),
            host_name:     String::new(),
        }
    }
}
fn parse_dns_servers(opts: &[DhcpOption]) -> Vec<String> {
    opts.iter()
        .find_map(|opt| {
            if let DhcpOption::DomainNameServers(servers) = opt {
                Some(servers.iter().map(ToString::to_string).collect())
            } else {
                None
            }
        })
        .unwrap_or_default()
}
fn parse_domain_search(opts: &[DhcpOption]) -> String {
    opts.iter()
        .find_map(|opt| {
            if let DhcpOption::DomainSearchList(domains) = opt {
                Some(domains.iter()
                            .map(ToString::to_string)
                            .collect::<Vec<_>>()
                            .join(" "))
            } else {
                None
            }
        })
        .unwrap_or_default()
}
fn parse_ntp_servers(opts: &[DhcpOption]) -> Vec<String> {
    opts.iter()
        .filter_map(|opt| match opt {
            DhcpOption::NtpServer(subopts) => Some(subopts),
            _ => None,
        })
        .flat_map(|subopts| subopts.iter().filter_map(|sub| match sub {
            NtpSuboption::ServerAddress(addr)
            | NtpSuboption::MulticastAddress(addr) => Some(addr.to_string()),
            NtpSuboption::FQDN(name) => Some(name.to_string()),
        }))
        .collect()
}
```
This keeps all the functionality but breaks the logic into focused, testable units.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| (MozimLease::V6(old_v6), MozimLease::V6(new_v6)) => { | ||
| old_v6.addr != new_v6.addr || old_v6.prefix_len != new_v6.prefix_len | ||
| } | ||
| _ => true, // could have used unreachable!() | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): Using 'true' for mismatched lease types in lease_has_changed may mask logic errors.
Using 'true' here may conceal incorrect mixing of lease types. Instead, consider unreachable!() or logging to highlight these cases during development.
| _ => true, // could have used unreachable!() | |
| _ => unreachable!("Mismatched MozimLease types in lease_has_changed"), | 
| } | ||
| // NO gateway logic for IPv6. This is intentional. | ||
| } | ||
| _ => return Err(NetavarkError::msg("Lease type mismatch during IP update")), | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Returning a generic error for lease type mismatch may obscure the root cause.
Include the actual lease types in the error message to make debugging easier.
| _ => return Err(NetavarkError::msg("Lease type mismatch during IP update")), | |
| _ => { | |
| return Err(NetavarkError::msg(format!( | |
| "Lease type mismatch during IP update: new lease type is {:?}, old lease type is {:?}", | |
| new_lease, old_lease | |
| ))); | |
| } | 
| let v4_lease = match get_lease_from_proxy(nvp_config_v4) { | ||
| Ok(l) => l, | ||
| Err(e) => { | ||
| return Err(NetavarkError::msg(format!("unable to obtain lease: {e}"))); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: Failing immediately on IPv4 lease error may prevent IPv6-only containers from working.
If ipam.ipv6_enabled is true and no IPv4 addresses are needed, consider allowing the process to continue when IPv4 lease acquisition fails.
Suggested implementation:
    let v4_lease = match get_lease_from_proxy(nvp_config_v4) {
        Ok(l) => l,
        Err(e) => {
            // If IPv6 is enabled and no IPv4 addresses are needed, allow to continue
            if ipam.ipv6_enabled && nvp_config_v4.is_none() {
                // No IPv4 lease, but that's acceptable for IPv6-only containers
                None
            } else {
                return Err(NetavarkError::msg(format!("unable to obtain lease: {e}")));
            }
        }
    };You may need to ensure that nvp_config_v4 is an Option and that downstream code can handle v4_lease being None. If v4_lease is expected to always be present, update its type to Option<LeaseType> (or whatever type is returned by get_lease_from_proxy). Also, make sure that the logic for handling leases later in the code can handle the case where v4_lease is None.
        
          
                test-dhcp/002-setup.bats
              
                Outdated
          
        
      | : <<'DISABLED_TEST' | ||
| @test "basic ipv6 setup" { | ||
| read -r -d '\0' input_config <<EOF | ||
| { | ||
| "host_iface": "veth1", | ||
| "container_iface": "veth0", | ||
| "container_mac_addr": "$CONTAINER_MAC", | ||
| "domain_name": "example.com", | ||
| "host_name": "foobar-v6", | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (testing): IPv6 setup test is currently disabled.
Please enable the test to verify DHCPv6 functionality. If it cannot be enabled now, add a note explaining the reason and a plan for future activation.
| function run_dhcp() { | ||
| gw=$(gateway_from_subnet "$SUBNET_CIDR") | ||
| stripped_subnet=$(strip_last_octet_from_subnet) | ||
| 
               | 
          ||
| read -r -d '\0' dnsmasq_config <<EOF | ||
| local version=${1:-4} | ||
| local gw | ||
| gw=$(gateway_from_subnet "$SUBNET_CIDR") | ||
| local dnsmasq_config="" | ||
| 
               | 
          ||
| if [ "$version" == "6" ]; then | ||
| # --- FIX IS HERE --- | ||
| # Get the IPv6 network prefix from the full CIDR. | ||
| # For example, turn "fd1d:5139:5cb5:1a99::/64" into "fd1d:5139:5cb5:1a99::" | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): No negative/edge case tests for DHCPv6 lease failures.
Please add tests for scenarios such as no available addresses, misconfigured dnsmasq, and timeouts to ensure proper error handling and messaging.
| } | ||
| } | ||
| 
               | 
          ||
| fn update_lease_ip( | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (complexity): Consider refactoring the large match blocks in update_lease_ip and lease_has_changed into smaller helper functions for each IP version to improve readability and maintainability.
Here’s one way to tame the nesting in update_lease_ip (and the free‐fn lease_has_changed) by pulling each IP/version case into its own small helper.  You’ll end up with a single match that just calls into two focused routines.
fn update_lease_ip(
    netns: &str,
    interface: &str,
    old_lease: &MozimLease,
    new_lease: &MozimLease,
) -> NetavarkResult<()> {
    let (_, ns) = core_utils::open_netlink_sockets(netns)
        .wrap("failed to open netlink socket in netns")?;
    let mut sock = ns.netlink;
    match (old_lease, new_lease) {
        (MozimLease::V4(o), MozimLease::V4(n)) =>
            update_lease_ip_v4(&mut sock, interface, o, n),
        (MozimLease::V6(o), MozimLease::V6(n)) =>
            update_lease_ip_v6(&mut sock, interface, o, n),
        _ => Err(NetavarkError::msg("Lease type mismatch during IP update")),
    }
}
fn update_lease_ip_v4(
    sock: &mut NetlinkSocket, interface: &str,
    old: &MozimV4Lease, new: &MozimV4Lease
) -> NetavarkResult<()> {
    let old_net = ipnet::Ipv4Net::with_netmask(old.yiaddr, old.subnet_mask)?;
    let new_net = ipnet::Ipv4Net::with_netmask(new.yiaddr, new.subnet_mask)?;
    if new_net != old_net {
        let link = sock.get_link(LinkID::Name(interface.to_string()))?
            .wrap("get interface in netns")?;
        sock.add_addr(link.header.index, &ipnet::IpNet::V4(new_net))
            .wrap("add new addr")?;
        sock.del_addr(link.header.index, &ipnet::IpNet::V4(old_net))
            .wrap("remove old addr")?;
    }
    // ... default‐gateway logic here ...
    Ok(())
}
fn update_lease_ip_v6(
    sock: &mut NetlinkSocket, interface: &str,
    old: &MozimV6Lease, new: &MozimV6Lease
) -> NetavarkResult<()> {
    let old_net = ipnet::Ipv6Net::new(old.addr, old.prefix_len)?;
    let new_net = ipnet::Ipv6Net::new(new.addr, new.prefix_len)?;
    if new_net != old_net {
        let link = sock.get_link(LinkID::Name(interface.to_string()))?
            .wrap("get interface in netns")?;
        sock.add_addr(link.header.index, &ipnet::IpNet::V6(new_net))?;
        sock.del_addr(link.header.index, &ipnet::IpNet::V6(old_net))?;
    }
    Ok(())
}And likewise push lease_has_changed onto the enum itself:
impl MozimLease {
    fn has_changed(&self, other: &MozimLease) -> bool {
        match (self, other) {
          (MozimLease::V4(a), MozimLease::V4(b)) =>
            a.yiaddr   != b.yiaddr ||
            a.subnet_mask != b.subnet_mask ||
            a.gateways != b.gateways,
          (MozimLease::V6(a), MozimLease::V6(b)) =>
            a.addr     != b.addr ||
            a.prefix_len != b.prefix_len,
          _ => true,
        }
    }
}This flattening pulls each version’s logic out of one huge match block and into small, self‐contained functions, making both the control flow and each protocol’s behavior easier to read & maintain.
| // --- Perform DHCPv4 Lease --- | ||
| // For now, we assume IPv4 is always desired unless explicitly disabled in future logic. | ||
| // You could make this conditional based on `ipam.container_addresses` if needed. | ||
| let nvp_config_v4 = NetworkConfig { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (complexity): Consider extracting a helper function for NetworkConfig creation and using Option::or to simplify merging logic.
// 1. Extract a helper to remove the v4/v6 NetworkConfig duplication:
fn build_network_config(
    host_iface: &str,
    container_iface: &str,
    ns_path: &str,
    mac: &str,
    hostname: &str,
    container_id: &str,
    version: u8,
) -> NetworkConfig {
    NetworkConfig {
        version,
        host_iface: host_iface.to_string(),
        container_iface: container_iface.to_string(),
        ns_path: ns_path.to_string(),
        container_mac_addr: mac.to_string(),
        host_name: hostname.to_string(),
        container_id: container_id.to_string(),
        domain_name: "".into(),
    }
}
// Then in get_dhcp_lease:
let nvp_v4 = build_network_config(
    host_network_interface,
    container_network_interface,
    ns_path,
    container_macvlan_mac,
    container_hostname,
    container_id,
    0,
);
let v4_lease = get_lease_from_proxy(nvp_v4)?;
// ...
if ipam.ipv6_enabled {
    let nvp_v6 = build_network_config(
        host_network_interface,
        container_network_interface,
        ns_path,
        container_macvlan_mac,
        container_hostname,
        container_id,
        1,
    );
    let (v6_subnet, v6_dns, v6_domain) = parse_lease(get_lease_from_proxy(nvp_v6)?)?;
    subnets.push(v6_subnet);
    // 2. Simplify the merge logic with `Option::or`:
    all_dns_servers   = all_dns_servers.or(v6_dns);
    all_domain_names  = all_domain_names.or(v6_domain);
}| 
               | 
          ||
| // DhcpV6Lease | ||
| // here the fields of DhcpV4Lease are repurposed tarry their equivalents in the DhcpV6Lease | ||
| impl From<DhcpV6Lease> for Lease { | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (complexity): Consider refactoring the option-parsing loop into separate helper functions for clarity and maintainability.
You can collapse that 70-line loop into three small, self-contained parsers. For example:
```rust
impl From<DhcpV6Lease> for Lease {
    fn from(l: DhcpV6Lease) -> Lease {
        let dns_servers   = parse_dns_servers(&l.dhcp_opts);
        let domain_name   = parse_domain_search(&l.dhcp_opts);
        let ntp_servers   = parse_ntp_servers(&l.dhcp_opts);
        Lease {
            t1:            l.t1,
            t2:            l.t2,
            lease_time:    l.valid_life,
            yiaddr:        l.addr.to_string(),
            siaddr:        l.srv_ip.to_string(),
            srv_id:        hex::encode(l.srv_duid),
            is_v6:         true,
            domain_name,
            dns_servers,
            ntp_servers,
            subnet_mask:   l.prefix_len.to_string(),
            mac_address:   String::new(),
            mtu:           0,
            broadcast_addr:String::new(),
            gateways:      Vec::new(),
            host_name:     String::new(),
        }
    }
}
fn parse_dns_servers(opts: &[DhcpOption]) -> Vec<String> {
    opts.iter()
        .find_map(|opt| {
            if let DhcpOption::DomainNameServers(servers) = opt {
                Some(servers.iter().map(ToString::to_string).collect())
            } else {
                None
            }
        })
        .unwrap_or_default()
}
fn parse_domain_search(opts: &[DhcpOption]) -> String {
    opts.iter()
        .find_map(|opt| {
            if let DhcpOption::DomainSearchList(domains) = opt {
                Some(domains.iter()
                            .map(ToString::to_string)
                            .collect::<Vec<_>>()
                            .join(" "))
            } else {
                None
            }
        })
        .unwrap_or_default()
}
fn parse_ntp_servers(opts: &[DhcpOption]) -> Vec<String> {
    opts.iter()
        .filter_map(|opt| match opt {
            DhcpOption::NtpServer(subopts) => Some(subopts),
            _ => None,
        })
        .flat_map(|subopts| subopts.iter().filter_map(|sub| match sub {
            NtpSuboption::ServerAddress(addr)
            | NtpSuboption::MulticastAddress(addr) => Some(addr.to_string()),
            NtpSuboption::FQDN(name) => Some(name.to_string()),
        }))
        .collect()
}This keeps all the functionality but breaks the logic into focused, testable units.
| 
           [APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: Rishikpulhani, sourcery-ai[bot] The full list of commands accepted by this bot can be found here. 
Needs approval from an approver in each of these files:
 
      Approvers can indicate their approval by writing   | 
    
fdfbbca    to
    2f136af      
    Compare
  
    92215c5    to
    16ed14e      
    Compare
  
    16ed14e    to
    50eb1da      
    Compare
  
    This commit introduces full support for stateful IPv6 address assignment via DHCPv6, resolving the issue where containers would not receive a global IPv6 address on macvlan networks. The implementation follows the standard IPv6 design of separating gateway discovery (via Router Advertisements) from stateful address assignment (via DHCPv6). Key changes include: - **Kernel Configuration:** Netavark now configures the container's kernel to accept Router Advertisements (`accept_ra=2`) for automatic gateway discovery, while disabling SLAAC (`autoconf=0`) to ensure a managed, deterministic network environment. - **DHCPv6 Service:** A new `DhcpV6Service` is added to the DHCP proxy. It uses the `mozim` library to acquire IPv6 leases and correctly generates a stable DUID-LL from the container's static MAC address to ensure a persistent network identity. - **gRPC Layer:** The gRPC `Lease` object and its `From` implementations have been updated to act as a universal carrier for both IPv4 and IPv6 lease information. - **Generic Proxy Logic:** Core functions like `process_client_stream` and `update_lease_ip` have been refactored to handle both DHCPv4 and DHCPv6 services generically, with conditional logic to correctly handle the differences between the protocols (e.g., gateway handling). Signed-off-by: Rishikpulhani <[email protected]>
50eb1da    to
    83b79ad      
    Compare
  
    
Fixes #1072
This commit introduces full support for stateful IPv6 address assignment via DHCPv6, resolving the issue where containers would not receive a global IPv6 address on macvlan networks.
The implementation follows the standard IPv6 design of separating gateway discovery (via Router Advertisements) from stateful address assignment (via DHCPv6).
Design
The implementation follows the standard IPv6 model:
sysctl(accept_ra=1,autoconf=0) to automatically learn its default gateway from RAs while preventing SLAAC. Netavark does not parse RA messages itself.DhcpV6Servicein the DHCP proxy uses themozimlibrary to acquire a lease, providing the container with a stable IP address and other options like DNS.Key changes include:
Kernel Configuration: Netavark now configures the container's kernel to accept Router Advertisements (
accept_ra=1) for automatic gateway discovery, while disabling SLAAC (autoconf=0) to ensure a managed, deterministic network environment.DHCPv6 Service: A new
DhcpV6Serviceis added to the DHCP proxy. It uses themozimlibrary to acquire IPv6 leases and correctly generates a stable DUID-LL from the container's static MAC address to ensure a persistent network identity.gRPC Layer: The gRPC
Leaseobject and itsFromimplementations have been updated to act as a universal carrier for both IPv4 and IPv6 lease information.Generic Proxy Logic: Core functions like
process_client_streamandupdate_lease_iphave been refactored to handle both DHCPv4 and DHCPv6 services generically, with conditional logic to correctly handle the differences between the protocols (e.g., gateway handling).Summary by Sourcery
Add comprehensive DHCPv6 support alongside existing DHCPv4 functionality, allowing macvlan and bridge networks to obtain stateful IPv6 addresses via DHCPv6 while preserving deterministic gateway discovery via Router Advertisements.
New Features:
Enhancements:
Build:
Tests: