From 3e3f555c1ebb0d11ab4057fd200c3f71032d8db5 Mon Sep 17 00:00:00 2001 From: Jamil Date: Wed, 13 Aug 2025 17:40:26 -0400 Subject: [PATCH] fix(relay): swap MACs for relayed traffic (#10193) In nearly all environments, we can safely assume that we will always use the same network gateway for forwarding relayed packets as the one we received them from. By leveraging this assumption, we can simply swap the SRC and DST MAC addresses, removing the need to keep a HaspMap for these, which eliminates the need to worry about thread-safety for this particular functionality. Related: #10138 --- rust/relay/ebpf-turn-router/src/error.rs | 2 - rust/relay/ebpf-turn-router/src/eth.rs | 97 ++++-------------------- rust/relay/ebpf-turn-router/src/main.rs | 15 +--- 3 files changed, 20 insertions(+), 94 deletions(-) diff --git a/rust/relay/ebpf-turn-router/src/error.rs b/rust/relay/ebpf-turn-router/src/error.rs index 123e3243b..4302f817e 100644 --- a/rust/relay/ebpf-turn-router/src/error.rs +++ b/rust/relay/ebpf-turn-router/src/error.rs @@ -6,7 +6,6 @@ pub enum Error { NotUdp, NotTurn, NotIp, - NoMacAddress, Ipv4PacketWithOptions, NotAChannelDataMessage, BadChannelDataLength, @@ -41,7 +40,6 @@ impl aya_log_ebpf::WriteToBuf for Error { Error::NotUdp => "Not a UDP packet".write(buf), Error::NotTurn => "Not TURN traffic".write(buf), Error::NotIp => "Not an IP packet".write(buf), - Error::NoMacAddress => "No MAC address".write(buf), Error::Ipv4PacketWithOptions => "IPv4 packet has options".write(buf), Error::NotAChannelDataMessage => "Not a channel data message".write(buf), Error::BadChannelDataLength => { diff --git a/rust/relay/ebpf-turn-router/src/eth.rs b/rust/relay/ebpf-turn-router/src/eth.rs index 30f5fb4d2..51cb1f519 100644 --- a/rust/relay/ebpf-turn-router/src/eth.rs +++ b/rust/relay/ebpf-turn-router/src/eth.rs @@ -1,14 +1,9 @@ use aya_ebpf::programs::XdpContext; -use aya_ebpf::{macros::map, maps::HashMap}; use aya_log_ebpf::debug; use network_types::eth::{EthHdr, EtherType}; -use core::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - use crate::{error::Error, ref_mut_at::ref_mut_at}; -const MAX_ETHERNET_MAPPINGS: u32 = 0x100000; - pub struct Eth<'a> { inner: &'a mut EthHdr, ctx: &'a XdpContext, @@ -31,89 +26,29 @@ impl<'a> Eth<'a> { self.inner.ether_type } - pub fn src(&self) -> [u8; 6] { - self.inner.src_addr - } - - pub fn dst(&self) -> [u8; 6] { - self.inner.dst_addr - } - - /// Update the Ethernet header with the appropriate destination MAC address based on the new destination IP. + /// Swap source and destination MAC addresses for TURN traffic. + /// + /// NOTE: This only works when all channel data traffic uses the same next hop for relayed + /// traffic as the one it received the packets from. In all cases where the production relays + /// are deployed, this will be a safe assumption to make. We avoid swapping MAC for traffic + /// passed to userspace in case the above assumption does not hold true. #[inline(always)] - pub fn update(self, new_dst_ip: impl Into) -> Result<(), Error> { - let new_dst_mac = match new_dst_ip.into() { - IpAddr::V4(ip) => get_mac_for_ipv4(ip).ok_or(Error::NoMacAddress)?, - IpAddr::V6(ip) => get_mac_for_ipv6(ip).ok_or(Error::NoMacAddress)?, - }; + pub fn swap_macs(self) -> Result<(), Error> { + let old_src = self.inner.src_addr; + let old_dst = self.inner.dst_addr; - let src = self.src(); - let dst = self.dst(); - - let new_src_mac = self.inner.dst_addr; - self.inner.src_addr = new_src_mac; - self.inner.dst_addr = new_dst_mac; + self.inner.src_addr = old_dst; + self.inner.dst_addr = old_src; debug!( self.ctx, - "ETH header update: src {:mac} -> {:mac}; dst {:mac} -> {:mac}", - src, - new_src_mac, - dst, - new_dst_mac, + "ETH header swap: src {:mac} -> {:mac}; dst {:mac} -> {:mac}", + old_src, + old_dst, + old_dst, + old_src, ); Ok(()) } } - -#[map] -static IP4_TO_MAC: HashMap<[u8; 4], [u8; 6]> = HashMap::with_max_entries(MAX_ETHERNET_MAPPINGS, 0); - -#[map] -static IP6_TO_MAC: HashMap<[u8; 16], [u8; 6]> = HashMap::with_max_entries(MAX_ETHERNET_MAPPINGS, 0); - -pub(crate) fn get_mac_for_ipv4(ip: Ipv4Addr) -> Option<[u8; 6]> { - unsafe { IP4_TO_MAC.get(&ip.octets()).copied() } -} - -pub(crate) fn get_mac_for_ipv6(ip: Ipv6Addr) -> Option<[u8; 6]> { - unsafe { IP6_TO_MAC.get(&ip.octets()).copied() } -} - -pub(crate) fn save_mac_for_ipv4(ip: Ipv4Addr, mac: [u8; 6]) { - let _ = IP4_TO_MAC.insert(&ip.octets(), &mac, 0); -} - -pub(crate) fn save_mac_for_ipv6(ip: Ipv6Addr, mac: [u8; 6]) { - let _ = IP6_TO_MAC.insert(&ip.octets(), &mac, 0); -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Memory overhead of an eBPF map. - /// - /// Determined empirically. - const HASH_MAP_OVERHEAD: f32 = 1.5; - - #[test] - fn hashmaps_are_less_than_100_mb() { - let ipv4_datatypes = 4 + 6; - let ipv6_datatypes = 16 + 6; - - let ipv4_map_size = - ipv4_datatypes as f32 * MAX_ETHERNET_MAPPINGS as f32 * HASH_MAP_OVERHEAD; - let ipv6_map_size = - ipv6_datatypes as f32 * MAX_ETHERNET_MAPPINGS as f32 * HASH_MAP_OVERHEAD; - - let total_map_size = (ipv4_map_size + ipv6_map_size) * 2_f32; - let total_map_size_mb = total_map_size / 1024_f32 / 1024_f32; - - assert!( - total_map_size_mb < 100_f32, - "Map size is {total_map_size_mb} MB" - ); - } -} diff --git a/rust/relay/ebpf-turn-router/src/main.rs b/rust/relay/ebpf-turn-router/src/main.rs index 7e6768df2..ca75004d4 100644 --- a/rust/relay/ebpf-turn-router/src/main.rs +++ b/rust/relay/ebpf-turn-router/src/main.rs @@ -88,7 +88,6 @@ pub fn handle_turn(ctx: XdpContext) -> u32 { | Error::NotTurn | Error::NotAChannelDataMessage | Error::Ipv4PacketWithOptions - | Error::NoMacAddress | Error::UnsupportedChannel(_) | Error::NoEntry(_) => { debug!(&ctx, "Passing packet to userspace: {}", e); @@ -128,9 +127,6 @@ fn try_handle_turn_ipv4(ctx: &XdpContext, eth: Eth) -> Result<(), Error> { // Safety: This is the only instance of `Ip4`. let ipv4 = unsafe { Ip4::parse(ctx) }?; - eth::save_mac_for_ipv4(ipv4.src(), eth.src()); - eth::save_mac_for_ipv4(ipv4.dst(), eth.dst()); - if ipv4.protocol() != IpProto::Udp { return Err(Error::NotUdp); } @@ -191,7 +187,7 @@ fn try_handle_ipv4_channel_data_to_udp( let new_dst = port_and_peer.peer_ip(); let new_ipv4_total_len = ipv4.total_len() - CdHdr::LEN as u16; - eth.update(new_dst)?; + eth.swap_macs()?; let pseudo_header = ipv4.update(new_src, new_dst, new_ipv4_total_len); @@ -231,7 +227,7 @@ fn try_handle_ipv4_udp_to_channel_data( let new_dst = client_and_channel.client_ip(); let new_ipv4_total_len = ipv4.total_len() + CdHdr::LEN as u16; - eth.update(new_dst)?; + eth.swap_macs()?; let pseudo_header = ipv4.update(new_src, new_dst, new_ipv4_total_len); @@ -266,9 +262,6 @@ fn try_handle_turn_ipv6(ctx: &XdpContext, eth: Eth) -> Result<(), Error> { // Safety: This is the only instance of `Ip6` in this scope. let ipv6 = unsafe { Ip6::parse(ctx) }?; - eth::save_mac_for_ipv6(ipv6.src(), eth.src()); - eth::save_mac_for_ipv6(ipv6.dst(), eth.dst()); - if ipv6.protocol() != IpProto::Udp { return Err(Error::NotUdp); } @@ -324,7 +317,7 @@ fn try_handle_ipv6_udp_to_channel_data( let new_dst = client_and_channel.client_ip(); let new_ipv6_total_len = ipv6.payload_len() + CdHdr::LEN as u16; - eth.update(new_dst)?; + eth.swap_macs()?; let pseudo_header = ipv6.update(new_src, new_dst, new_ipv6_total_len); @@ -378,7 +371,7 @@ fn try_handle_ipv6_channel_data_to_udp( let new_dst = port_and_peer.peer_ip(); let new_ipv6_payload_len = ipv6.payload_len() - CdHdr::LEN as u16; - eth.update(new_dst)?; + eth.swap_macs()?; let pseudo_header = ipv6.update(new_src, new_dst, new_ipv6_payload_len);