Skip to content

DRAFT: NetworkManager connection profile (keyfile) generation#1267

Draft
Rolv-Apneseth wants to merge 2 commits intocoreos:mainfrom
Rolv-Apneseth:network_manager
Draft

DRAFT: NetworkManager connection profile (keyfile) generation#1267
Rolv-Apneseth wants to merge 2 commits intocoreos:mainfrom
Rolv-Apneseth:network_manager

Conversation

@Rolv-Apneseth
Copy link
Copy Markdown
Member

I will be creating an issue for discussion, but as mentioned in #1266, I think this would be nice to have in afterburn. It is particularly important for the Hetzner implementation though as we need to fallback to IPv4 DHCP initially in order to request the network configuration metadata from Hetzner's locally available HTTP endpoint, meaning that as far as I understand it passing network kargs to initrd can't work.

With these changes and the Hetner PR, I've managed to configure an FCOS Hetzner system using the following butane snippet:

systemd:
  units:
    - name: afterburn-networkmanager.service
      enabled: true
      contents: |
        [Unit]
        Description=Afterburn Network Configuration First Boot (Hetzner)
        Documentation=https://coreos.github.io/afterburn/
        After=network-online.target
        Wants=network-online.target
        ConditionKernelCommandLine=ignition.platform.id=hetzner
        ConditionPathExists=!/var/lib/afterburn-network-configured

        [Service]
        Type=oneshot
        ExecStart=/usr/bin/afterburn multi \
          --cmdline \
          --networkmanager-profiles /etc/NetworkManager/system-connections
        ExecStartPost=/usr/bin/touch /var/lib/afterburn-network-configured
        # Reboot to apply new network config
        ExecStartPost=/usr/bin/systemctl reboot
        RemainAfterExit=yes

        [Install]
        WantedBy=multi-user.target

If we do go ahead with adding this as a feature, I would appreciate any feedback on the code.

Also, I've initially pushed 2 commits, with the difference being how VLANs/Bonds are handled. The first commit tries to directly translate from the VirtualNetDev::sd_netdev_sections to the NetworkManager equivalents, though this had some limitations due to the fundamentally different 1 file vs several per connection, and required a lot of conversions of both keys and values. The second commit instead shifts the burden to the provider implementation, requiring a new VirtualNetDev::nm_sections, which comes out a lot simpler overall in my opinion but I'm open to suggestions.

@Rolv-Apneseth Rolv-Apneseth marked this pull request as draft March 23, 2026 11:00
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant new feature: the ability to generate NetworkManager connection profiles. The changes are well-structured, adding a new command-line option, implementing the configuration generation logic in src/network.rs, and integrating it into the provider infrastructure. The Packet provider is also updated to support this new feature.

My review focuses on a few key areas:

  • A potential panic due to integer underflow which should be addressed.
  • Opportunities to improve code maintainability by reducing duplication.
  • A suggestion to improve code readability in one of the loops.

Overall, this is a great addition and the code is mostly in good shape for a draft. The feedback provided should help finalize it.

Comment thread src/network.rs
Comment on lines +269 to +274
writeln!(
config,
"autoconnect-priority={}",
// Lower number means higher priority for systemd, but it's the opposite for NM
100 - (self.priority as u16)
)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The calculation for autoconnect-priority can cause a panic. self.priority is a u8 and can have a value up to 255. If it's greater than 100, subtracting it from 100 will cause an integer underflow, which panics in debug builds and wraps in release builds, leading to unexpected priority values.

You should use saturating_sub to prevent this, as you've correctly done for VirtualNetDev.

Suggested change
writeln!(
config,
"autoconnect-priority={}",
// Lower number means higher priority for systemd, but it's the opposite for NM
100 - (self.priority as u16)
)?;
writeln!(
config,
"autoconnect-priority={}",
// Lower number means higher priority for systemd, but it's the opposite for NM
100i32.saturating_sub(i32::from(self.priority))
)?;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Maybe we should instead restrict the priority to a type that only allows 0-99? Seems like a misconfiguration of the priority value otherwise

Comment thread src/network.rs
Comment on lines +295 to +389
fn write_nm_config_common(&self, config: &mut String) -> Result<()> {
// [ipv4] section
writeln!(config, "\n[ipv4]")?;

let ipv4_addresses: Vec<_> = self.ip_addresses.iter().filter(|a| a.is_ipv4()).collect();
if matches!(self.dhcp, Some(DhcpSetting::V4 | DhcpSetting::Both)) {
writeln!(config, "method=auto")?;
} else if ipv4_addresses.is_empty() {
writeln!(config, "method=disabled")?;
} else {
writeln!(config, "method=manual")?;
for (i, addr) in ipv4_addresses.iter().enumerate() {
writeln!(config, "address{}={}", i + 1, addr)?;
}

// IPv4 gateway
let (default_route, ipv4_routes): (Vec<NetworkRoute>, Vec<NetworkRoute>) = self
.routes
.iter()
.filter(|r| r.destination.is_ipv4())
.partition(|r| r.destination.prefix() == 0);
if let Some(default_route) = default_route.first() {
writeln!(config, "gateway={}", default_route.gateway)?;
}

// IPv4 routes (non-default)
for (i, route) in ipv4_routes.iter().enumerate() {
writeln!(
config,
"route{}={},{},0",
i + 1,
route.destination,
route.gateway
)?;
}
}

// IPv4 DNS servers
let ipv4_dns: Vec<_> = self.nameservers.iter().filter(|n| n.is_ipv4()).collect();
if !ipv4_dns.is_empty() {
let dns_list: Vec<_> = ipv4_dns
.iter()
.map(std::string::ToString::to_string)
.collect();
writeln!(config, "dns={}", dns_list.join(";"))?;
}

// [ipv6] section
writeln!(config, "\n[ipv6]")?;

let ipv6_addresses: Vec<_> = self.ip_addresses.iter().filter(|a| a.is_ipv6()).collect();
if matches!(self.dhcp, Some(DhcpSetting::V6 | DhcpSetting::Both)) {
writeln!(config, "method=auto")?;
} else if ipv6_addresses.is_empty() {
writeln!(config, "method=disabled")?;
} else {
writeln!(config, "method=manual")?;
for (i, addr) in ipv6_addresses.iter().enumerate() {
writeln!(config, "address{}={}", i + 1, addr)?;
}

// IPv6 gateway
let (default_route, ipv6_routes): (Vec<NetworkRoute>, Vec<NetworkRoute>) = self
.routes
.iter()
.filter(|r| r.destination.is_ipv6())
.partition(|r| r.destination.prefix() == 0);
if let Some(default_route) = default_route.first() {
writeln!(config, "gateway={}", default_route.gateway)?;
}

// IPv6 routes (non-default)
for (i, route) in ipv6_routes.iter().enumerate() {
writeln!(
config,
"route{}={},{},0",
i + 1,
route.destination,
route.gateway
)?;
}
}

// IPv6 DNS servers
let ipv6_dns: Vec<_> = self.nameservers.iter().filter(|n| n.is_ipv6()).collect();
if !ipv6_dns.is_empty() {
let dns_list: Vec<_> = ipv6_dns
.iter()
.map(std::string::ToString::to_string)
.collect();
writeln!(config, "dns={}", dns_list.join(";"))?;
}

Ok(())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The function write_nm_config_common has significant code duplication between the IPv4 and IPv6 configuration blocks. The logic for determining the method, writing addresses, handling routes, and setting DNS servers is nearly identical for both IP versions.

To improve maintainability and readability, I recommend refactoring this logic. You could extract the common functionality into a helper function that is parameterized by the IP version. This would make the code more concise and easier to manage in the future.

Comment thread src/providers/mod.rs
@prestist prestist self-requested a review March 23, 2026 15:45
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