mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
fix(eBPF): correctly set Ethernet addresses (#8601)
At present, the eBPF code assumes that the incoming packet needs to be sent back to the same MAC address that it came from. This is only true if there is at least one IP layer hop in-between the relay and the Client / Gateway. When setting up Firezone in my local LAN to debug the eBPF code, all components are within the same subnet and thus can send packets directly to each other, without having to go through the router. In such a scenario, simply swapping the Ethernet addresses is not correct. As part of witnessing traffic coming in via the network, we can build up a mapping of IP to MAC address. This mapping can then later be used to set the correct MAC address for a given destination IP. All of this functions entirely without interaction from userspace. Unless you are running in a LAN environment, most if not all IPs will point to the same MAC address (the one of the next IP layer hop, i.e. the router). For the very first packet that we want to relay, we will not have a MAC address for the destination IP. This doesn't matter though, we simply pass that packet up to userspace and handle it there. Pretty much all communication on the Internet is bi-directional because you need some kind of ACK. As soon as we receive the first ACK, e.g. the response to a binding request, we will learn the MAC address for the given target IP and the eBPF router can kick in for all packets going forward. Related: #7518
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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<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)?,
|
||||
};
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,17 @@ pub struct Ip4<'a> {
|
||||
impl<'a> Ip4<'a> {
|
||||
#[inline(always)]
|
||||
pub fn parse(ctx: &'a XdpContext) -> Result<Self, Error> {
|
||||
let hdr = slice_mut_at::<Ipv4Hdr>(ctx, EthHdr::LEN)?;
|
||||
let ip4_hdr = slice_mut_at::<Ipv4Hdr>(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 {
|
||||
|
||||
@@ -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<u32, Error> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user