-
Notifications
You must be signed in to change notification settings - Fork 206
consomme: ipv6 address discoverability updates #3701
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
Merged
OneBlue
merged 2 commits into
microsoft:main
from
Brian-Perkins:consomme_ipv6_address_discovery
Jun 19, 2026
+201
−23
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ use smoltcp::wire::EthernetFrame; | |
| use smoltcp::wire::EthernetProtocol; | ||
| use smoltcp::wire::EthernetRepr; | ||
| use smoltcp::wire::IPV4_HEADER_LEN; | ||
| use smoltcp::wire::Icmpv6Message; | ||
| use smoltcp::wire::Icmpv6Packet; | ||
| use smoltcp::wire::IpAddress; | ||
| use smoltcp::wire::IpProtocol; | ||
|
|
@@ -143,6 +144,10 @@ pub struct ConsommeParams { | |
| /// Current IPv6 network mask (if any). | ||
| #[inspect(display)] | ||
| pub prefix_len_ipv6: u8, | ||
| /// If true, advertise an autonomous IPv6 prefix so guests create a | ||
| /// routable IPv6 address with SLAAC. | ||
| #[inspect(display)] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove |
||
| pub advertise_routable_ipv6: bool, | ||
| /// Current IPv6 gateway MAC address (if any). | ||
| #[inspect(display)] | ||
| pub gateway_mac_ipv6: EthernetAddress, | ||
|
|
@@ -197,6 +202,7 @@ impl ConsommeParams { | |
| net_mask: Ipv4Address::new(255, 255, 255, 0), | ||
| nameservers, | ||
| prefix_len_ipv6: 64, | ||
| advertise_routable_ipv6: true, | ||
| gateway_mac_ipv6, | ||
| gateway_link_local_ipv6: Self::compute_link_local_address(gateway_mac_ipv6), | ||
| client_ip_ipv6: None, | ||
|
|
@@ -256,6 +262,38 @@ impl ConsommeParams { | |
| Ipv6Address::from_octets(addr) | ||
| } | ||
|
|
||
| /// Infer the guest's link-local IPv6 address from a learned routable SLAAC address. | ||
| /// | ||
| /// If the routable address uses an EUI-64 address derived from the guest MAC, we can infer a | ||
| /// matching link-local address. | ||
| /// | ||
| /// If the routable address uses a different interface identifier (for example, | ||
| /// privacy or stable-secret addressing), this leaves the link-local address | ||
| /// unknown and waits to learn it from traffic or NDP instead. | ||
| pub(crate) fn infer_client_link_local_from_routable( | ||
| &mut self, | ||
| routable: Ipv6Address, | ||
| source: &'static str, | ||
| ) { | ||
| // Link local address is already known. | ||
| if self.client_ip_ipv6.is_some() { | ||
| return; | ||
| } | ||
|
|
||
| let link_local = Self::compute_link_local_address(self.client_mac); | ||
| if routable.octets()[8..] != link_local.octets()[8..] { | ||
| return; | ||
| } | ||
|
|
||
| tracing::debug!( | ||
| client_ipv6 = %link_local, | ||
| client_ipv6_routable = %routable, | ||
| source, | ||
| "inferred client link-local IPv6 address from routable SLAAC address" | ||
| ); | ||
| self.client_ip_ipv6 = Some(link_local); | ||
| } | ||
|
|
||
| /// Returns the list of IPv6 nameservers suitable for advertisement to | ||
| /// guests via NDP RDNSS or DHCPv6. | ||
| /// | ||
|
|
@@ -539,6 +577,9 @@ pub enum DropReason { | |
| /// The ethertype is unknown. | ||
| #[error("unsupported ip protocol {0}")] | ||
| UnsupportedIpProtocol(IpProtocol), | ||
| /// The ICMPv6 message type is unsupported. | ||
| #[error("unsupported icmpv6 message type {0}")] | ||
| UnsupportedIcmpv6(Icmpv6Message), | ||
| /// The ARP type is unsupported. | ||
| #[error("unsupported dhcp message type {0:?}")] | ||
| UnsupportedDhcp(DhcpMessageType), | ||
|
|
@@ -898,6 +939,18 @@ impl<T: Client> Access<'_, T> { | |
| dst_addr: ipv6.dst_addr(), | ||
| }; | ||
|
|
||
| // Learn the client's link-local IPv6 address from outgoing traffic. | ||
| // This covers clients that do not perform DAD before using the address. | ||
| if src_addr.is_unicast_link_local() | ||
| && self.inner.state.params.client_ip_ipv6 != Some(src_addr) | ||
| { | ||
|
Brian-Perkins marked this conversation as resolved.
|
||
| tracing::debug!( | ||
| client_ipv6 = %src_addr, | ||
| "learned client link-local IPv6 address from outgoing traffic" | ||
| ); | ||
| self.inner.state.params.client_ip_ipv6 = Some(src_addr); | ||
| } | ||
|
|
||
| // Learn the client's routable IPv6 address from outgoing traffic. | ||
| // This is more reliable than relying solely on DAD Neighbor | ||
| // Solicitations, which some clients skip on private virtual links. | ||
|
|
@@ -911,6 +964,10 @@ impl<T: Client> Access<'_, T> { | |
| "learned client routable IPv6 address from outgoing traffic" | ||
| ); | ||
| self.inner.state.params.client_ip_ipv6_routable = Some(src_addr); | ||
| self.inner | ||
| .state | ||
| .params | ||
| .infer_client_link_local_from_routable(src_addr, "outgoing traffic"); | ||
| } | ||
|
|
||
| match next_header { | ||
|
|
@@ -923,14 +980,16 @@ impl<T: Client> Access<'_, T> { | |
| let icmpv6_packet = Icmpv6Packet::new_unchecked(inner); | ||
| let msg_type = icmpv6_packet.msg_type(); | ||
|
|
||
| if msg_type == smoltcp::wire::Icmpv6Message::NeighborSolicit | ||
| || msg_type == smoltcp::wire::Icmpv6Message::NeighborAdvert | ||
| || msg_type == smoltcp::wire::Icmpv6Message::RouterSolicit | ||
| || msg_type == smoltcp::wire::Icmpv6Message::RouterAdvert | ||
| { | ||
| if msg_type.is_ndisc() { | ||
| self.handle_ndp(frame, inner, ipv6.src_addr())?; | ||
| } else { | ||
| return Err(DropReason::UnsupportedIpProtocol(next_header)); | ||
| tracing::trace!( | ||
| icmpv6_type = %msg_type, | ||
| src_addr = %src_addr, | ||
| dst_addr = %addresses.dst_addr, | ||
| "unsupported ICMPv6 message" | ||
| ); | ||
| return Err(DropReason::UnsupportedIcmpv6(msg_type)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -955,6 +1014,7 @@ impl<T: Client> Access<'_, T> { | |
| mod tests { | ||
| use super::*; | ||
| use smoltcp::wire::Ipv6Address; | ||
| use smoltcp::wire::Ipv6Repr; | ||
|
|
||
| #[test] | ||
| fn test_is_same_ipv6_subnet_basic() { | ||
|
|
@@ -992,4 +1052,96 @@ mod tests { | |
| assert!(is_same_ipv6_subnet(a, a, 200)); | ||
| assert!(!is_same_ipv6_subnet(a, b, 255)); | ||
| } | ||
|
|
||
| fn eui64_routable_address(params: &ConsommeParams) -> Ipv6Address { | ||
| let mut octets = ConsommeParams::compute_link_local_address(params.client_mac).octets(); | ||
| octets[..8].copy_from_slice(&[0xfd, 0x00, 0x0d, 0xb8, 0, 0, 0, 0]); | ||
| Ipv6Address::from_octets(octets) | ||
| } | ||
|
|
||
| #[test] | ||
| fn infer_client_link_local_from_routable_with_matching_eui64_iid() { | ||
| let mut params = ConsommeParams::new().unwrap(); | ||
| params.client_ip_ipv6 = None; | ||
| let expected_link_local = ConsommeParams::compute_link_local_address(params.client_mac); | ||
|
|
||
| params.infer_client_link_local_from_routable(eui64_routable_address(¶ms), "test"); | ||
|
|
||
| assert_eq!(params.client_ip_ipv6, Some(expected_link_local)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn infer_client_link_local_from_routable_ignores_privacy_iid() { | ||
| let mut params = ConsommeParams::new().unwrap(); | ||
| params.client_ip_ipv6 = None; | ||
| let privacy_address = Ipv6Address::new(0xfd00, 0x0db8, 0, 0, 1, 2, 3, 4); | ||
|
|
||
| params.infer_client_link_local_from_routable(privacy_address, "test"); | ||
|
|
||
| assert_eq!(params.client_ip_ipv6, None); | ||
| } | ||
|
|
||
| #[test] | ||
| fn infer_client_link_local_from_routable_does_not_overwrite_existing_address() { | ||
| let mut params = ConsommeParams::new().unwrap(); | ||
| let existing_address = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 0x1234); | ||
| params.client_ip_ipv6 = Some(existing_address); | ||
|
|
||
| params.infer_client_link_local_from_routable(eui64_routable_address(¶ms), "test"); | ||
|
|
||
| assert_eq!(params.client_ip_ipv6, Some(existing_address)); | ||
| } | ||
|
|
||
| struct TestClient; | ||
|
|
||
| impl Client for TestClient { | ||
| fn driver(&self) -> &dyn Driver { | ||
| unreachable!("IPv6 address learning tests do not use the client driver") | ||
| } | ||
|
|
||
| fn recv(&mut self, _data: &[u8], _checksum: &ChecksumState) {} | ||
|
|
||
| fn rx_mtu(&mut self) -> usize { | ||
| MIN_MTU | ||
| } | ||
| } | ||
|
|
||
| fn learn_from_ipv6_traffic(params: &mut ConsommeParams, src_addr: Ipv6Address) { | ||
| params.skip_ipv6_checks = true; | ||
| let gateway_ip = params.gateway_link_local_ipv6; | ||
| let mut consomme = Consomme::new(std::mem::replace(params, ConsommeParams::new().unwrap())); | ||
| let mut client = TestClient; | ||
| let frame = EthernetRepr { | ||
| src_addr: consomme.state.params.client_mac, | ||
| dst_addr: consomme.state.params.gateway_mac_ipv6, | ||
| ethertype: EthernetProtocol::Ipv6, | ||
| }; | ||
| let mut payload = [0; smoltcp::wire::IPV6_HEADER_LEN]; | ||
| Ipv6Repr { | ||
| src_addr, | ||
| dst_addr: gateway_ip, | ||
| next_header: IpProtocol::Tcp, | ||
| payload_len: 0, | ||
| hop_limit: 64, | ||
| } | ||
| .emit(&mut Ipv6Packet::new_unchecked(&mut payload)); | ||
|
|
||
| let _ = consomme | ||
| .access(&mut client) | ||
| .handle_ipv6(&frame, &payload, &ChecksumState::TCP6); | ||
| *params = consomme.state.params; | ||
| } | ||
|
|
||
| #[test] | ||
| fn handle_ipv6_updates_link_local_from_traffic() { | ||
| let mut params = ConsommeParams::new().unwrap(); | ||
| params.client_ip_ipv6 = None; | ||
| let first_address = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 1); | ||
| let second_address = Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 2); | ||
|
|
||
| learn_from_ipv6_traffic(&mut params, first_address); | ||
| learn_from_ipv6_traffic(&mut params, second_address); | ||
|
|
||
| assert_eq!(params.client_ip_ipv6, Some(second_address)); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Remove