Skip to content

providers/hetzner: add network configuration and public IPv6 attribute#1266

Open
Rolv-Apneseth wants to merge 2 commits intocoreos:mainfrom
Rolv-Apneseth:hetzner_network
Open

providers/hetzner: add network configuration and public IPv6 attribute#1266
Rolv-Apneseth wants to merge 2 commits intocoreos:mainfrom
Rolv-Apneseth:hetzner_network

Conversation

@Rolv-Apneseth
Copy link
Copy Markdown
Member

Closes #1141

This adds support for configuring networking on Hetzner systems, both via systemd-networkd unit files and netplan configurations, as well as providing a HETZNER_PUBLIC_IPV6 attribute. This should be enough for, e.g. Flatcar, but Fedora CoreOS (NetworkManager) requires some more work.

The main issue is that unlike other providers, it seems (as far as I can tell from everywhere I've looked) that Hetzner only provides the networking metadata from a HTTP endpoint, meaning you need some form of networking up first before further configuration. To my understanding, this means we can't use the kargs approach like with other providers, and need to instead fallback to the automatic network configuration before making a request to the metadata endpoint (which is available locally even on systems with only a public IPv6).

I've confirmed that the netplan configuration can work on FCOS using:

sudo afterburn multi --cmdline --netplan-config /etc/netplan
# Hetzner reports iface name as eth0 but system uses predictable naming
sudo sed -i 's/eth0:/<actual_interface_name>:/' /etc/netplan/50-afterburn.yaml
echo '  renderer: NetworkManager' | sudo tee -a /etc/netplan/50-afterburn.yaml
sudo rpm-ostree install netplan
sudo reboot
...
ping6 google.com

And I'm planning on creating a separate draft PR for generating NetworkManager configurations, which I've also confirmed works with these changes and which I believe would be very nice to have in afterburn regardless. I'll create an issue for discussion though.

On a side note, the netplan configuration was largely copied from another provider in the codebase. I have some code ready for a default implementation, if we think that's a good change.

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 network configuration support for the Hetzner provider, including systemd-networkd and netplan configurations, and adds a HETZNER_PUBLIC_IPV6 attribute. The changes are comprehensive, covering documentation, implementation, and extensive tests. My review focuses on a few areas to improve code readability, maintainability, and robustness.

Comment thread src/network.rs
Comment on lines +133 to +207
fn netplan_config(&self) -> Result<Option<String>> {
let networks = self.networks()?;

let mut ethernets = serde_yaml::Mapping::new();

for iface in networks {
let mut eth_config = serde_yaml::Mapping::new();

// Add DHCP settings
if let Some(dhcp) = iface.dhcp {
match dhcp {
DhcpSetting::V4 => {
eth_config.insert("dhcp4".into(), true.into());
}
DhcpSetting::V6 => {
eth_config.insert("dhcp6".into(), true.into());
}
DhcpSetting::Both => {
eth_config.insert("dhcp4".into(), true.into());
eth_config.insert("dhcp6".into(), true.into());
}
}
}

if !iface.ip_addresses.is_empty() {
let addresses: Vec<String> = iface
.ip_addresses
.iter()
.map(std::string::ToString::to_string)
.collect();
eth_config.insert("addresses".into(), addresses.into());
}

if !iface.routes.is_empty() {
let routes: Vec<serde_yaml::Value> = iface
.routes
.iter()
.map(|route| {
let mut route_map = serde_yaml::Mapping::new();
route_map.insert("to".into(), route.destination.to_string().into());
route_map.insert("via".into(), route.gateway.to_string().into());
serde_yaml::Value::Mapping(route_map)
})
.collect();
eth_config.insert("routes".into(), routes.into());
}

if !iface.nameservers.is_empty() {
let nameservers: Vec<_> = iface
.nameservers
.iter()
.map(std::string::ToString::to_string)
.collect();
eth_config.insert(
"nameservers".into(),
serde_yaml::Value::Mapping(serde_yaml::Mapping::from_iter([(
"addresses".into(),
nameservers.into(),
)])),
);
}

if let Some(name) = iface.name {
ethernets.insert(name.into(), eth_config.into());
}
}

let network = serde_yaml::Mapping::from_iter([
("version".into(), 2.into()),
("ethernets".into(), ethernets.into()),
]);
let netplan = serde_yaml::Mapping::from_iter([("network".into(), network.into())]);

Ok(Some(serde_yaml::to_string(&netplan)?))
}
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

This function manually constructs the YAML structure using serde_yaml::Mapping. While this works, it's quite verbose and can be hard to maintain. Consider defining structs that derive serde::Serialize to represent the Netplan configuration. This would allow you to build the configuration in a more structured, type-safe way and then serialize it, leading to more declarative and maintainable code.

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.

Basically copied from an existing provider and this is how it was done, though I kind of agree

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.

Might be more useful with a default implementation though, rather than in this PR

Comment thread src/providers/hetzner/mod.rs Outdated
Comment thread src/providers/hetzner/mod.rs
Copy link
Copy Markdown
Contributor

@prestist prestist left a comment

Choose a reason for hiding this comment

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

Overall this looks really good @Rolv-Apneseth great work! I love the test coverage!

});
}

"dhcp" | "dhcp4" | "dhcp6" => {
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.

hmm I think this is correct, but what a weird state lol... if subnet is set to "dhcp6" but our ipv6=false we end up with dhcp:4?

So by this statement we are saying that ipv6 takes the lead not dhcp.

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.

Yeah... not ideal. Those ipv4 and ipv6 booleans are a deviation from cloud-init v1 network config and I've only seen them set to true. I don't even know if they actually use dhcp6, I just thought it should be covered in case.

Comment thread src/providers/hetzner/mod.rs Outdated
Comment thread src/providers/hetzner/mod.rs Outdated
Comment thread src/providers/hetzner/mod.rs
This should enable the generation of systemd-networkd unit and netplan
configuration files for Hetzner systems. Note that a network connection
is required to request network metadata (no alternative on Hetzner as
far as I can tell).
@Rolv-Apneseth
Copy link
Copy Markdown
Member Author

Rolv-Apneseth commented Apr 24, 2026

@apricote, in relation to flatcar/Flatcar#1968, if possible could you confirm this would work for Flatcar?

Edit: @tormath1 might be more relevant actually, sorry.

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.

Support IPv6 on Hetzner

2 participants