Skip to content

Commit 3f41b35

Browse files
committed
support nat on icmp sockets
1 parent 53acda5 commit 3f41b35

File tree

1 file changed

+96
-22
lines changed

1 file changed

+96
-22
lines changed

forwarder/src/socket/icmp.rs

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ use std::{
1313
net::{SocketAddr, SocketAddrV6},
1414
};
1515

16+
/// magic bytes that are injected to end of icmp echo reply packets that
17+
/// we craft and it get discarded later when parsing, it's purpose is to
18+
/// detect automatic echo reply packets of kernel and ignore them
19+
const ECHO_REPLY_MAGIC: [u8; 3] = [0x24, 0x74, 0x33];
20+
1621
/// `IcmpSocket` that is very similiar to `UdpSocket`
1722
#[derive(Debug)]
1823
pub struct IcmpSocket {
@@ -50,7 +55,7 @@ impl IcmpSocket {
5055

5156
impl SocketTrait for IcmpSocket {
5257
fn send_to(&self, buffer: &[u8], to: &SocketAddr) -> io::Result<usize> {
53-
let packet = craft_icmp_packet(buffer, &self.udp_socket_addr, to)?;
58+
let packet = craft_icmp_packet(buffer, &self.udp_socket_addr, to, false)?;
5459
let mut to_addr = *to;
5560
// in linux `send_to` on icmpv6 socket requires destination port to be zero
5661
to_addr.set_port(0);
@@ -115,7 +120,7 @@ impl NonBlockingSocketTrait for NonBlockingIcmpSocket {
115120
let dst_addr = self
116121
.connected_addr
117122
.ok_or_else(|| Into::<io::Error>::into(io::ErrorKind::NotConnected))?;
118-
let packet = craft_icmp_packet(buffer, &self.icmp_socket.udp_socket_addr, &dst_addr)?;
123+
let packet = craft_icmp_packet(buffer, &self.icmp_socket.udp_socket_addr, &dst_addr, true)?;
119124
self.icmp_socket.socket.send(&packet)
120125
}
121126

@@ -137,30 +142,64 @@ fn craft_icmp_packet(
137142
payload: &[u8],
138143
source_addr: &SocketAddr,
139144
dst_addr: &SocketAddr,
145+
is_echo_request: bool,
140146
) -> io::Result<Vec<u8>> {
141-
let echo_header = IcmpEchoHeader {
142-
id: dst_addr.port(),
143-
seq: source_addr.port(),
147+
// when we are sending echo reply we inject few magic bytes to the
148+
// end of payload so when receiving reply packets we can determine
149+
// if the echo reply packet is automatically sent from kernel
150+
// (in case /proc/sys/net/ipv4/icmp_echo_ignore_all is not turned off)
151+
// or we actually sent it
152+
let payload = if !is_echo_request {
153+
let payload_with_magic_len = payload.len() + ECHO_REPLY_MAGIC.len();
154+
// TODO: this allocation is really bad, find another way for it
155+
let mut buffer = vec![0u8; payload_with_magic_len];
156+
buffer[..payload.len()].copy_from_slice(payload);
157+
buffer[payload.len()..].copy_from_slice(&ECHO_REPLY_MAGIC);
158+
buffer
159+
} else {
160+
payload.to_vec()
161+
};
162+
163+
// read comments on `receiver::parse_icmp_packet` on why the
164+
// source and destination place changes based on echo reply or request
165+
let echo_header = if is_echo_request {
166+
IcmpEchoHeader {
167+
id: source_addr.port(),
168+
seq: dst_addr.port(),
169+
}
170+
} else {
171+
IcmpEchoHeader {
172+
id: dst_addr.port(),
173+
seq: source_addr.port(),
174+
}
144175
};
145176

146177
let icmp_header = if source_addr.is_ipv4() {
147-
let icmp_type = Icmpv4Type::EchoRequest(echo_header);
148-
Icmpv4Header::with_checksum(icmp_type, payload)
178+
let icmp_type = if is_echo_request {
179+
Icmpv4Type::EchoRequest(echo_header)
180+
} else {
181+
Icmpv4Type::EchoReply(echo_header)
182+
};
183+
Icmpv4Header::with_checksum(icmp_type, &payload)
149184
.to_bytes()
150185
.to_vec()
151186
} else {
152-
let icmp_type = Icmpv6Type::EchoRequest(echo_header);
187+
let icmp_type = if is_echo_request {
188+
Icmpv6Type::EchoRequest(echo_header)
189+
} else {
190+
Icmpv6Type::EchoReply(echo_header)
191+
};
153192
let source_ip = as_socket_addr_v6(*source_addr).ip().octets();
154193
let destination_ip = as_socket_addr_v6(*dst_addr).ip().octets();
155-
Icmpv6Header::with_checksum(icmp_type, source_ip, destination_ip, payload)
194+
Icmpv6Header::with_checksum(icmp_type, source_ip, destination_ip, &payload)
156195
.map_err(|_| Into::<io::Error>::into(io::ErrorKind::InvalidInput))?
157196
.to_bytes()
158197
.to_vec()
159198
};
160199

161200
let mut header_and_payload = Vec::with_capacity(icmp_header.len() + payload.len());
162201
header_and_payload.extend_from_slice(&icmp_header);
163-
header_and_payload.extend_from_slice(payload);
202+
header_and_payload.extend_from_slice(&payload);
164203
Ok(header_and_payload)
165204
}
166205

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

189228
let icmp = IcmpSlice::from_slice(is_ipv6, &packet[payload_start_index..])?;
190-
// we only work with icmp echo requests so if any other type of icmp
229+
// we only work with icmp echo requests and replies so if any other type of icmp
191230
// packet we receive we just ignore it
192-
let correct_icmp_type = if is_ipv6 {
231+
let echo_request = if is_ipv6 {
193232
etherparse::icmpv6::TYPE_ECHO_REQUEST
194233
} else {
195234
etherparse::icmpv4::TYPE_ECHO_REQUEST
196235
};
197-
if icmp.type_u8() != correct_icmp_type || icmp.code_u8() != 0 {
236+
let echo_reply = if is_ipv6 {
237+
etherparse::icmpv6::TYPE_ECHO_REPLY
238+
} else {
239+
etherparse::icmpv4::TYPE_ECHO_REPLY
240+
};
241+
242+
let is_echo_request = icmp.type_u8() == echo_request;
243+
let is_echo_reply = icmp.type_u8() == echo_reply;
244+
245+
let is_correct_icmp_type = is_echo_request || is_echo_reply;
246+
if !is_correct_icmp_type || icmp.code_u8() != 0 {
198247
return None;
199248
}
200249

201250
let bytes5to8 = icmp.bytes5to8();
202-
// icmp is on layer 3 so it has no idea about ports
203-
// we use identification part of icmp packet as destination port
204-
// to identify packets that are really meant for us
205-
let dst_port = u16::from_be_bytes([bytes5to8[0], bytes5to8[1]]);
206-
207-
// we also use sequence part of icmp packet as source port
208-
let src_port = u16::from_be_bytes([bytes5to8[2], bytes5to8[3]]);
251+
let id = u16::from_be_bytes([bytes5to8[0], bytes5to8[1]]);
252+
let seq = u16::from_be_bytes([bytes5to8[2], bytes5to8[3]]);
209253

210254
let payload_len = icmp.payload().len();
211-
let total_len = packet.len();
212-
let payload = &mut packet[total_len - payload_len..];
255+
let packet_len = packet.len();
256+
257+
let payload = if is_echo_request {
258+
&mut packet[packet_len - payload_len..]
259+
} else {
260+
// filter the reply packets that doesn't have the magic bytes
261+
let payload = &mut packet[packet_len - payload_len..];
262+
let magic_len = ECHO_REPLY_MAGIC.len();
263+
if payload_len < magic_len {
264+
return None;
265+
}
266+
if payload[payload_len - magic_len..] != ECHO_REPLY_MAGIC {
267+
return None;
268+
}
269+
// striping magic bytes off the payload
270+
&mut payload[..payload_len - magic_len]
271+
};
272+
273+
// icmp is on layer 3 so it has no idea about ports so we use
274+
// identification and sequence part of icmp packet as src and dst port
275+
// but the problem is that if for example port 1010 sends a packet to 8000
276+
// the id and seq is like this:
277+
// | ID: 1010 | SEQ: 8000 |
278+
// now if the server wants to send echo reply from 8000 to 1010 the packet
279+
// will be like this:
280+
// | ID: 8000 | SEQ: 1010 |
281+
// because now the sender is 8000 and the receiver is 1010
282+
// the important part is the ID, if the ID of echo reply is different than
283+
// the echo request then NAT has no clue how to forward this packet, so we
284+
// swap the id and seq position based on the packet being reply or request
285+
// so the ID field will not get changed
286+
let (src_port, dst_port) = if is_echo_reply { (seq, id) } else { (id, seq) };
213287

214288
Some(IcmpPacket {
215289
payload,

0 commit comments

Comments
 (0)