diff --git a/rust/relay/ebpf-turn-router/src/error.rs b/rust/relay/ebpf-turn-router/src/error.rs index a1a633311..ed65b3851 100644 --- a/rust/relay/ebpf-turn-router/src/error.rs +++ b/rust/relay/ebpf-turn-router/src/error.rs @@ -6,6 +6,7 @@ pub enum Error { NotUdp, NotTurn, NotIp, + NoMacAddress, Ipv4PacketWithOptions, NotAChannelDataMessage, BadChannelDataLength, @@ -22,6 +23,7 @@ impl aya_log_ebpf::WriteToBuf for Error { Error::NotUdp => "Not a UDP packet", Error::NotTurn => "Not TURN traffic", Error::NotIp => "Not an IP packet", + Error::NoMacAddress => "No MAC address", Error::Ipv4PacketWithOptions => "IPv4 packet has options", Error::NotAChannelDataMessage => "Not a channel data message", Error::BadChannelDataLength => "Channel data length does not match packet length", diff --git a/rust/relay/ebpf-turn-router/src/eth.rs b/rust/relay/ebpf-turn-router/src/eth.rs index d93d58496..b4fced512 100644 --- a/rust/relay/ebpf-turn-router/src/eth.rs +++ b/rust/relay/ebpf-turn-router/src/eth.rs @@ -1,8 +1,13 @@ use aya_ebpf::programs::XdpContext; +use aya_ebpf::{macros::map, maps::HashMap}; use network_types::eth::{EthHdr, EtherType}; +use core::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + use crate::{error::Error, slice_mut_at::slice_mut_at}; +const MAX_ETHERNET_MAPPINGS: u32 = 0x100000; + pub struct Eth<'a> { inner: &'a mut EthHdr, } @@ -19,7 +24,75 @@ impl<'a> Eth<'a> { self.inner.ether_type } - pub fn swap_src_and_dst(&mut self) { - core::mem::swap(&mut self.inner.src_addr, &mut self.inner.dst_addr); + 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. + 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)?, + }; + + self.inner.src_addr = self.inner.dst_addr; + self.inner.dst_addr = new_dst_mac; + + 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/ip4.rs b/rust/relay/ebpf-turn-router/src/ip4.rs index 4c3193eba..fc667f8f5 100644 --- a/rust/relay/ebpf-turn-router/src/ip4.rs +++ b/rust/relay/ebpf-turn-router/src/ip4.rs @@ -14,14 +14,17 @@ pub struct Ip4<'a> { impl<'a> Ip4<'a> { #[inline(always)] pub fn parse(ctx: &'a XdpContext) -> Result { - let hdr = slice_mut_at::(ctx, EthHdr::LEN)?; + let ip4_hdr = slice_mut_at::(ctx, EthHdr::LEN)?; // IPv4 packets with options are handled in user-space. - if usize::from(hdr.ihl()) * 4 != Ipv4Hdr::LEN { + if usize::from(ip4_hdr.ihl()) * 4 != Ipv4Hdr::LEN { return Err(Error::Ipv4PacketWithOptions); } - Ok(Self { ctx, inner: hdr }) + Ok(Self { + ctx, + inner: ip4_hdr, + }) } pub fn src(&self) -> Ipv4Addr { diff --git a/rust/relay/ebpf-turn-router/src/main.rs b/rust/relay/ebpf-turn-router/src/main.rs index e2c22a7e6..ceabbabc2 100644 --- a/rust/relay/ebpf-turn-router/src/main.rs +++ b/rust/relay/ebpf-turn-router/src/main.rs @@ -67,6 +67,7 @@ pub fn handle_turn(ctx: XdpContext) -> u32 { | Error::XdpAdjustHeadFailed | Error::XdpLoadBytesFailed | Error::PacketTooShort + | Error::NoMacAddress | Error::NoChannelBinding => { debug!(&ctx, "Failed to handle packet: {}", e); @@ -86,21 +87,23 @@ fn try_handle_turn(ctx: &XdpContext) -> Result { let eth = Eth::parse(ctx)?; match eth.ether_type() { - EtherType::Ipv4 => try_handle_turn_ipv4(ctx)?, - EtherType::Ipv6 => try_handle_turn_ipv6(ctx)?, + EtherType::Ipv4 => try_handle_turn_ipv4(ctx, eth)?, + EtherType::Ipv6 => try_handle_turn_ipv6(ctx, eth)?, _ => return Err(Error::NotIp), }; // If we get to here, we modified the packet and need to send it back out again. - Eth::parse(ctx)?.swap_src_and_dst(); Ok(xdp_action::XDP_TX) } #[inline(always)] -fn try_handle_turn_ipv4(ctx: &XdpContext) -> Result<(), Error> { +fn try_handle_turn_ipv4(ctx: &XdpContext, eth: Eth) -> Result<(), Error> { let ipv4 = 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); } @@ -119,14 +122,14 @@ fn try_handle_turn_ipv4(ctx: &XdpContext) -> Result<(), Error> { ); if config::allocation_range().contains(&udp.dst()) { - try_handle_ipv4_udp_to_channel_data(ctx, ipv4, udp)?; + try_handle_ipv4_udp_to_channel_data(ctx, eth, ipv4, udp)?; stats::emit_data_relayed(ctx, udp_payload_len); return Ok(()); } if udp.dst() == 3478 { - try_handle_ipv4_channel_data_to_udp(ctx, ipv4, udp)?; + try_handle_ipv4_channel_data_to_udp(ctx, eth, ipv4, udp)?; stats::emit_data_relayed(ctx, udp_payload_len - CdHdr::LEN as u16); return Ok(()); @@ -136,7 +139,12 @@ fn try_handle_turn_ipv4(ctx: &XdpContext) -> Result<(), Error> { } #[inline(always)] -fn try_handle_ipv4_channel_data_to_udp(ctx: &XdpContext, ipv4: Ip4, udp: Udp) -> Result<(), Error> { +fn try_handle_ipv4_channel_data_to_udp( + ctx: &XdpContext, + eth: Eth, + ipv4: Ip4, + udp: Udp, +) -> Result<(), Error> { let cd = ChannelData::parse(ctx, Ipv4Hdr::LEN)?; // SAFETY: ??? @@ -145,8 +153,12 @@ fn try_handle_ipv4_channel_data_to_udp(ctx: &XdpContext, ipv4: Ip4, udp: Udp) -> .ok_or(Error::NoChannelBinding)?; let new_src = ipv4.dst(); // The IP we received the packet on will be the new source IP. + let new_dst = port_and_peer.peer_ip(); let new_ipv4_total_len = ipv4.total_len() - CdHdr::LEN as u16; - let pseudo_header = ipv4.update(new_src, port_and_peer.peer_ip(), new_ipv4_total_len); + + eth.update(new_dst)?; + + let pseudo_header = ipv4.update(new_src, new_dst, new_ipv4_total_len); let new_udp_len = udp.len() - CdHdr::LEN as u16; udp.update( @@ -164,14 +176,23 @@ fn try_handle_ipv4_channel_data_to_udp(ctx: &XdpContext, ipv4: Ip4, udp: Udp) -> } #[inline(always)] -fn try_handle_ipv4_udp_to_channel_data(ctx: &XdpContext, ipv4: Ip4, udp: Udp) -> Result<(), Error> { +fn try_handle_ipv4_udp_to_channel_data( + ctx: &XdpContext, + eth: Eth, + ipv4: Ip4, + udp: Udp, +) -> Result<(), Error> { let client_and_channel = unsafe { UDP_TO_CHAN_44.get(&PortAndPeerV4::new(ipv4.src(), udp.dst(), udp.src())) } .ok_or(Error::NoChannelBinding)?; let new_src = ipv4.dst(); // The IP we received the packet on will be the new source IP. + let new_dst = client_and_channel.client_ip(); let new_ipv4_total_len = ipv4.total_len() + CdHdr::LEN as u16; - let pseudo_header = ipv4.update(new_src, client_and_channel.client_ip(), new_ipv4_total_len); + + eth.update(new_dst)?; + + let pseudo_header = ipv4.update(new_src, new_dst, new_ipv4_total_len); let udp_len = udp.len(); let new_udp_len = udp_len + CdHdr::LEN as u16; @@ -199,9 +220,12 @@ fn try_handle_ipv4_udp_to_channel_data(ctx: &XdpContext, ipv4: Ip4, udp: Udp) -> } #[inline(always)] -fn try_handle_turn_ipv6(ctx: &XdpContext) -> Result<(), Error> { +fn try_handle_turn_ipv6(ctx: &XdpContext, eth: Eth) -> Result<(), Error> { let ipv6 = 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); } @@ -220,14 +244,14 @@ fn try_handle_turn_ipv6(ctx: &XdpContext) -> Result<(), Error> { ); if config::allocation_range().contains(&udp.dst()) { - try_handle_ipv6_udp_to_channel_data(ctx, ipv6, udp)?; + try_handle_ipv6_udp_to_channel_data(ctx, eth, ipv6, udp)?; stats::emit_data_relayed(ctx, udp_payload_len); return Ok(()); } if udp.dst() == 3478 { - try_handle_ipv6_channel_data_to_udp(ctx, ipv6, udp)?; + try_handle_ipv6_channel_data_to_udp(ctx, eth, ipv6, udp)?; stats::emit_data_relayed(ctx, udp_payload_len - CdHdr::LEN as u16); return Ok(()); @@ -236,14 +260,23 @@ fn try_handle_turn_ipv6(ctx: &XdpContext) -> Result<(), Error> { Err(Error::NotTurn) } -fn try_handle_ipv6_udp_to_channel_data(ctx: &XdpContext, ipv6: Ip6, udp: Udp) -> Result<(), Error> { +fn try_handle_ipv6_udp_to_channel_data( + ctx: &XdpContext, + eth: Eth, + ipv6: Ip6, + udp: Udp, +) -> Result<(), Error> { let client_and_channel = unsafe { UDP_TO_CHAN_66.get(&PortAndPeerV6::new(ipv6.src(), udp.dst(), udp.src())) } .ok_or(Error::NoChannelBinding)?; let new_src = ipv6.dst(); // The IP we received the packet on will be the new source IP. + let new_dst = client_and_channel.client_ip(); let new_ipv6_total_len = ipv6.payload_len() + CdHdr::LEN as u16; - let pseudo_header = ipv6.update(new_src, client_and_channel.client_ip(), new_ipv6_total_len); + + eth.update(new_dst)?; + + let pseudo_header = ipv6.update(new_src, new_dst, new_ipv6_total_len); let udp_len = udp.len(); let new_udp_len = udp_len + CdHdr::LEN as u16; @@ -270,7 +303,12 @@ fn try_handle_ipv6_udp_to_channel_data(ctx: &XdpContext, ipv6: Ip6, udp: Udp) -> Ok(()) } -fn try_handle_ipv6_channel_data_to_udp(ctx: &XdpContext, ipv6: Ip6, udp: Udp) -> Result<(), Error> { +fn try_handle_ipv6_channel_data_to_udp( + ctx: &XdpContext, + eth: Eth, + ipv6: Ip6, + udp: Udp, +) -> Result<(), Error> { let cd = ChannelData::parse(ctx, Ipv6Hdr::LEN)?; // SAFETY: ??? @@ -279,8 +317,12 @@ fn try_handle_ipv6_channel_data_to_udp(ctx: &XdpContext, ipv6: Ip6, udp: Udp) -> .ok_or(Error::NoChannelBinding)?; let new_src = ipv6.dst(); // The IP we received the packet on will be the new source IP. + let new_dst = port_and_peer.peer_ip(); let new_ipv6_payload_len = ipv6.payload_len() - CdHdr::LEN as u16; - let pseudo_header = ipv6.update(new_src, port_and_peer.peer_ip(), new_ipv6_payload_len); + + eth.update(new_dst)?; + + let pseudo_header = ipv6.update(new_src, new_dst, new_ipv6_payload_len); let new_udp_len = udp.len() - CdHdr::LEN as u16; udp.update(