Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 158 additions & 6 deletions vm/devices/net/net_consomme/consomme/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -143,6 +144,10 @@ pub struct ConsommeParams {
/// Current IPv6 network mask (if any).
#[inspect(display)]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Remove

pub prefix_len_ipv6: u8,
/// If true, advertise an autonomous IPv6 prefix so guests create a
/// routable IPv6 address with SLAAC.
#[inspect(display)]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)
{
Comment thread
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.
Expand All @@ -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 {
Expand All @@ -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));
}
}

Expand All @@ -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() {
Expand Down Expand Up @@ -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(&params), "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(&params), "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));
}
}
59 changes: 42 additions & 17 deletions vm/devices/net/net_consomme/consomme/src/ndp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ impl<T: Client> Access<'_, T> {
ipv6_src_addr: Ipv6Address,
) -> Result<(), DropReason> {
let icmpv6_packet = Icmpv6Packet::new_unchecked(payload);
let msg_type = icmpv6_packet.msg_type();
tracing::trace!(
icmpv6_type = %msg_type,
ipv6_src_addr = %ipv6_src_addr,
eth_src_addr = %frame.src_addr,
eth_dst_addr = %frame.dst_addr,
"received NDP message"
);
Comment on lines 84 to +92
let ndp = NdiscRepr::parse(&icmpv6_packet)?;

match ndp {
Expand Down Expand Up @@ -172,15 +180,24 @@ impl<T: Client> Access<'_, T> {
dst_addr: Ipv6Address,
eth_dst_addr: EthernetAddress,
) -> Result<(), DropReason> {
// Compute the network prefix from our configured IPv6 parameters
// This is the prefix that clients will use for SLAAC
let prefix = self
.compute_network_prefix(NETWORK_PREFIX_BASE, self.inner.state.params.prefix_len_ipv6);

// RFC 4861 Section 4.6.2: Router Advertisement with Prefix Information
// We set the ADDRCONF flag to enable SLAAC. We intentionally omit ON_LINK
// so the guest treats global addresses as off-link and routes all traffic
// through the gateway rather than attempting on-link NDP resolution.
let prefix_info = self.inner.state.params.advertise_routable_ipv6.then(|| {
// RFC 4861 Section 4.6.2: Router Advertisement with Prefix Information.
// We set the ADDRCONF flag to enable SLAAC. We intentionally omit ON_LINK
// so the guest treats global addresses as off-link and routes all traffic
// through the gateway rather than attempting on-link NDP resolution.
let prefix = self.compute_network_prefix(
NETWORK_PREFIX_BASE,
self.inner.state.params.prefix_len_ipv6,
);
NdiscPrefixInformation {
prefix_len: self.inner.state.params.prefix_len_ipv6,
prefix,
valid_lifetime: smoltcp::time::Duration::from_secs(2592000), // https://www.rfc-editor.org/rfc/rfc4861#section-6.2.1
preferred_lifetime: smoltcp::time::Duration::from_secs(604800), // https://www.rfc-editor.org/rfc/rfc4861#section-6.2.1
flags: NdiscPrefixInfoFlags::ADDRCONF,
}
});

let ndp_repr = NdiscRepr::RouterAdvert {
hop_limit: 255,
flags: NdiscRouterFlags::empty(),
Expand All @@ -191,13 +208,7 @@ impl<T: Client> Access<'_, T> {
self.inner.state.params.gateway_mac_ipv6,
)),
mtu: None,
prefix_info: Some(NdiscPrefixInformation {
prefix_len: self.inner.state.params.prefix_len_ipv6,
prefix,
valid_lifetime: smoltcp::time::Duration::from_secs(2592000), // https://www.rfc-editor.org/rfc/rfc4861#section-6.2.1
preferred_lifetime: smoltcp::time::Duration::from_secs(604800), // https://www.rfc-editor.org/rfc/rfc4861#section-6.2.1
flags: NdiscPrefixInfoFlags::ADDRCONF,
}),
prefix_info,
};

let dns_servers = self.inner.state.params.filtered_ipv6_nameservers();
Expand Down Expand Up @@ -280,11 +291,25 @@ impl<T: Client> Access<'_, T> {
// We learn the client's address here because consomme is the only other
// entity on this virtual link, so DAD will always succeed.
if ipv6_src_addr.is_unspecified() {
tracing::debug!(%target_addr, "learned client IPv6 address from DAD Neighbor Solicitation");
if target_addr.is_unicast_link_local() {
tracing::trace!(
client_ipv6 = %target_addr,
"learned client link-local IPv6 address from DAD Neighbor Solicitation"
);
self.inner.state.params.client_ip_ipv6 = Some(target_addr);
} else {
tracing::trace!(
client_ipv6_routable = %target_addr,
"learned client routable IPv6 address from DAD Neighbor Solicitation"
);
self.inner.state.params.client_ip_ipv6_routable = Some(target_addr);
self.inner
.state
.params
.infer_client_link_local_from_routable(
target_addr,
"DAD Neighbor Solicitation",
);
}
return Ok(());
}
Expand Down
1 change: 1 addition & 0 deletions vm/devices/net/net_consomme/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ impl net_backend::Queue for ConsommeQueue {
consomme::DropReason::SendBufferFull => self.stats.tx_dropped.increment(),
consomme::DropReason::UnsupportedEthertype(_)
| consomme::DropReason::UnsupportedIpProtocol(_)
| consomme::DropReason::UnsupportedIcmpv6(_)
| consomme::DropReason::UnsupportedDhcp(_)
| consomme::DropReason::UnsupportedArp
| consomme::DropReason::UnsupportedDhcpv6(_)
Expand Down
Loading