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
This commit is contained in:
Jamil
2025-08-13 17:40:26 -04:00
committed by GitHub
parent 92137ee76b
commit 3e3f555c1e
3 changed files with 20 additions and 94 deletions

View File

@@ -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 => {

View File

@@ -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<IpAddr>) -> 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"
);
}
}

View File

@@ -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);