Skip to content

Commit

Permalink
support nat on icmp sockets
Browse files Browse the repository at this point in the history
  • Loading branch information
Arian8j2 committed Nov 22, 2024
1 parent 53acda5 commit 3f41b35
Showing 1 changed file with 96 additions and 22 deletions.
118 changes: 96 additions & 22 deletions forwarder/src/socket/icmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ use std::{
net::{SocketAddr, SocketAddrV6},
};

/// magic bytes that are injected to end of icmp echo reply packets that
/// we craft and it get discarded later when parsing, it's purpose is to
/// detect automatic echo reply packets of kernel and ignore them
const ECHO_REPLY_MAGIC: [u8; 3] = [0x24, 0x74, 0x33];

/// `IcmpSocket` that is very similiar to `UdpSocket`
#[derive(Debug)]
pub struct IcmpSocket {
Expand Down Expand Up @@ -50,7 +55,7 @@ impl IcmpSocket {

impl SocketTrait for IcmpSocket {
fn send_to(&self, buffer: &[u8], to: &SocketAddr) -> io::Result<usize> {
let packet = craft_icmp_packet(buffer, &self.udp_socket_addr, to)?;
let packet = craft_icmp_packet(buffer, &self.udp_socket_addr, to, false)?;
let mut to_addr = *to;
// in linux `send_to` on icmpv6 socket requires destination port to be zero
to_addr.set_port(0);
Expand Down Expand Up @@ -115,7 +120,7 @@ impl NonBlockingSocketTrait for NonBlockingIcmpSocket {
let dst_addr = self
.connected_addr
.ok_or_else(|| Into::<io::Error>::into(io::ErrorKind::NotConnected))?;
let packet = craft_icmp_packet(buffer, &self.icmp_socket.udp_socket_addr, &dst_addr)?;
let packet = craft_icmp_packet(buffer, &self.icmp_socket.udp_socket_addr, &dst_addr, true)?;
self.icmp_socket.socket.send(&packet)
}

Expand All @@ -137,30 +142,64 @@ fn craft_icmp_packet(
payload: &[u8],
source_addr: &SocketAddr,
dst_addr: &SocketAddr,
is_echo_request: bool,
) -> io::Result<Vec<u8>> {
let echo_header = IcmpEchoHeader {
id: dst_addr.port(),
seq: source_addr.port(),
// when we are sending echo reply we inject few magic bytes to the
// end of payload so when receiving reply packets we can determine
// if the echo reply packet is automatically sent from kernel
// (in case /proc/sys/net/ipv4/icmp_echo_ignore_all is not turned off)
// or we actually sent it
let payload = if !is_echo_request {
let payload_with_magic_len = payload.len() + ECHO_REPLY_MAGIC.len();
// TODO: this allocation is really bad, find another way for it
let mut buffer = vec![0u8; payload_with_magic_len];
buffer[..payload.len()].copy_from_slice(payload);
buffer[payload.len()..].copy_from_slice(&ECHO_REPLY_MAGIC);
buffer
} else {
payload.to_vec()
};

// read comments on `receiver::parse_icmp_packet` on why the
// source and destination place changes based on echo reply or request
let echo_header = if is_echo_request {
IcmpEchoHeader {
id: source_addr.port(),
seq: dst_addr.port(),
}
} else {
IcmpEchoHeader {
id: dst_addr.port(),
seq: source_addr.port(),
}
};

let icmp_header = if source_addr.is_ipv4() {
let icmp_type = Icmpv4Type::EchoRequest(echo_header);
Icmpv4Header::with_checksum(icmp_type, payload)
let icmp_type = if is_echo_request {
Icmpv4Type::EchoRequest(echo_header)
} else {
Icmpv4Type::EchoReply(echo_header)
};
Icmpv4Header::with_checksum(icmp_type, &payload)
.to_bytes()
.to_vec()
} else {
let icmp_type = Icmpv6Type::EchoRequest(echo_header);
let icmp_type = if is_echo_request {
Icmpv6Type::EchoRequest(echo_header)
} else {
Icmpv6Type::EchoReply(echo_header)
};
let source_ip = as_socket_addr_v6(*source_addr).ip().octets();
let destination_ip = as_socket_addr_v6(*dst_addr).ip().octets();
Icmpv6Header::with_checksum(icmp_type, source_ip, destination_ip, payload)
Icmpv6Header::with_checksum(icmp_type, source_ip, destination_ip, &payload)
.map_err(|_| Into::<io::Error>::into(io::ErrorKind::InvalidInput))?
.to_bytes()
.to_vec()
};

let mut header_and_payload = Vec::with_capacity(icmp_header.len() + payload.len());
header_and_payload.extend_from_slice(&icmp_header);
header_and_payload.extend_from_slice(payload);
header_and_payload.extend_from_slice(&payload);
Ok(header_and_payload)
}

Expand All @@ -187,29 +226,64 @@ pub fn parse_icmp_packet(packet: &mut [u8], is_ipv6: bool) -> Option<IcmpPacket<
};

let icmp = IcmpSlice::from_slice(is_ipv6, &packet[payload_start_index..])?;
// we only work with icmp echo requests so if any other type of icmp
// we only work with icmp echo requests and replies so if any other type of icmp
// packet we receive we just ignore it
let correct_icmp_type = if is_ipv6 {
let echo_request = if is_ipv6 {
etherparse::icmpv6::TYPE_ECHO_REQUEST
} else {
etherparse::icmpv4::TYPE_ECHO_REQUEST
};
if icmp.type_u8() != correct_icmp_type || icmp.code_u8() != 0 {
let echo_reply = if is_ipv6 {
etherparse::icmpv6::TYPE_ECHO_REPLY
} else {
etherparse::icmpv4::TYPE_ECHO_REPLY
};

let is_echo_request = icmp.type_u8() == echo_request;
let is_echo_reply = icmp.type_u8() == echo_reply;

let is_correct_icmp_type = is_echo_request || is_echo_reply;
if !is_correct_icmp_type || icmp.code_u8() != 0 {
return None;
}

let bytes5to8 = icmp.bytes5to8();
// icmp is on layer 3 so it has no idea about ports
// we use identification part of icmp packet as destination port
// to identify packets that are really meant for us
let dst_port = u16::from_be_bytes([bytes5to8[0], bytes5to8[1]]);

// we also use sequence part of icmp packet as source port
let src_port = u16::from_be_bytes([bytes5to8[2], bytes5to8[3]]);
let id = u16::from_be_bytes([bytes5to8[0], bytes5to8[1]]);
let seq = u16::from_be_bytes([bytes5to8[2], bytes5to8[3]]);

let payload_len = icmp.payload().len();
let total_len = packet.len();
let payload = &mut packet[total_len - payload_len..];
let packet_len = packet.len();

let payload = if is_echo_request {
&mut packet[packet_len - payload_len..]
} else {
// filter the reply packets that doesn't have the magic bytes
let payload = &mut packet[packet_len - payload_len..];
let magic_len = ECHO_REPLY_MAGIC.len();
if payload_len < magic_len {
return None;
}
if payload[payload_len - magic_len..] != ECHO_REPLY_MAGIC {
return None;
}
// striping magic bytes off the payload
&mut payload[..payload_len - magic_len]
};

// icmp is on layer 3 so it has no idea about ports so we use
// identification and sequence part of icmp packet as src and dst port
// but the problem is that if for example port 1010 sends a packet to 8000
// the id and seq is like this:
// | ID: 1010 | SEQ: 8000 |
// now if the server wants to send echo reply from 8000 to 1010 the packet
// will be like this:
// | ID: 8000 | SEQ: 1010 |
// because now the sender is 8000 and the receiver is 1010
// the important part is the ID, if the ID of echo reply is different than
// the echo request then NAT has no clue how to forward this packet, so we
// swap the id and seq position based on the packet being reply or request
// so the ID field will not get changed
let (src_port, dst_port) = if is_echo_reply { (seq, id) } else { (id, seq) };

Some(IcmpPacket {
payload,
Expand Down

0 comments on commit 3f41b35

Please sign in to comment.