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 Sep 25, 2024
1 parent 4f8f507 commit fd40c6f
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 21 deletions.
73 changes: 62 additions & 11 deletions forwarder/src/socket/icmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ use std::{
os::fd::AsRawFd,
};

/// 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];

/// tracks if the icmp receiver thread is started or not, the first index
/// is for icmpv4 and the second is for icmpv6
static IS_RECEIVER_STARTED: [Mutex<bool>; 2] = [Mutex::new(false), Mutex::new(false)];
Expand Down Expand Up @@ -109,8 +114,13 @@ impl SocketTrait for IcmpSocket {
}

fn send(&self, buffer: &[u8]) -> io::Result<usize> {
assert!(
!self.is_blocking,
"IcmpSocket::send_to called on blocking socket"
);

let dst_addr = self.connected_addr.unwrap();
let packet = craft_icmp_packet(buffer, &self.local_addr()?, &dst_addr);
let packet = craft_icmp_packet(buffer, &self.local_addr()?, &dst_addr, true);
let dst_addr: SocketAddr = if dst_addr.is_ipv6() {
// in linux `send_to` on icmpv6 socket requires destination port to be zero
let mut addr_without_port = dst_addr;
Expand All @@ -130,7 +140,11 @@ impl SocketTrait for IcmpSocket {
}

fn send_to(&self, buffer: &[u8], to: &SocketAddr) -> io::Result<usize> {
let packet = craft_icmp_packet(buffer, &self.local_addr()?, to);
assert!(
self.is_blocking,
"IcmpSocket::send_to called on nonblocking socket"
);
let packet = craft_icmp_packet(buffer, &self.local_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 @@ -194,30 +208,67 @@ impl SocketTrait for IcmpSocket {
}
}

fn craft_icmp_packet(payload: &[u8], source_addr: &SocketAddr, dst_addr: &SocketAddr) -> Vec<u8> {
let echo_header = IcmpEchoHeader {
id: dst_addr.port(),
seq: source_addr.port(),
fn craft_icmp_packet(
payload: &[u8],
source_addr: &SocketAddr,
dst_addr: &SocketAddr,
request: bool,
) -> Vec<u8> {
// 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 !request {
let payload_with_magic_len = payload.len() + ECHO_REPLY_MAGIC.len();
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 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 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 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)
.unwrap()
.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);
header_and_payload
}

Expand Down
55 changes: 45 additions & 10 deletions forwarder/src/socket/icmp/receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,61 @@ pub fn parse_icmp_packet(packet: &[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
// 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
};

if !(icmp.type_u8() == echo_request || icmp.type_u8() == echo_reply) {
return None;
}
if 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 payload = &packet[packet.len() - payload_len..];
let payload = if icmp.type_u8() == echo_request {
&packet[packet.len() - payload_len..]
} else {
// filter the reply packets that doesn't have the magic bytes
let payload = &packet[packet.len() - payload_len..];
let magic_len = super::ECHO_REPLY_MAGIC.len();
if payload.len() < magic_len {
return None;
}
if payload[payload.len() - magic_len..] != super::ECHO_REPLY_MAGIC {
return None;
}
// striping magic bytes off the payload
&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 |
// 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 is reply or request
let (src_port, dst_port) = if icmp.type_u8() == echo_reply {
(seq, id)
} else {
(id, seq)
};

Some(IcmpPacket {
payload,
Expand Down

0 comments on commit fd40c6f

Please sign in to comment.