Skip to content

Commit fd40c6f

Browse files
committed
support nat on icmp sockets
1 parent 4f8f507 commit fd40c6f

File tree

2 files changed

+107
-21
lines changed

2 files changed

+107
-21
lines changed

forwarder/src/socket/icmp.rs

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ use std::{
1515
os::fd::AsRawFd,
1616
};
1717

18+
/// magic bytes that are injected to end of icmp echo reply packets that
19+
/// we craft and it get discarded later when parsing, it's purpose is to
20+
/// detect automatic echo reply packets of kernel and ignore them
21+
const ECHO_REPLY_MAGIC: [u8; 3] = [0x24, 0x74, 0x33];
22+
1823
/// tracks if the icmp receiver thread is started or not, the first index
1924
/// is for icmpv4 and the second is for icmpv6
2025
static IS_RECEIVER_STARTED: [Mutex<bool>; 2] = [Mutex::new(false), Mutex::new(false)];
@@ -109,8 +114,13 @@ impl SocketTrait for IcmpSocket {
109114
}
110115

111116
fn send(&self, buffer: &[u8]) -> io::Result<usize> {
117+
assert!(
118+
!self.is_blocking,
119+
"IcmpSocket::send_to called on blocking socket"
120+
);
121+
112122
let dst_addr = self.connected_addr.unwrap();
113-
let packet = craft_icmp_packet(buffer, &self.local_addr()?, &dst_addr);
123+
let packet = craft_icmp_packet(buffer, &self.local_addr()?, &dst_addr, true);
114124
let dst_addr: SocketAddr = if dst_addr.is_ipv6() {
115125
// in linux `send_to` on icmpv6 socket requires destination port to be zero
116126
let mut addr_without_port = dst_addr;
@@ -130,7 +140,11 @@ impl SocketTrait for IcmpSocket {
130140
}
131141

132142
fn send_to(&self, buffer: &[u8], to: &SocketAddr) -> io::Result<usize> {
133-
let packet = craft_icmp_packet(buffer, &self.local_addr()?, to);
143+
assert!(
144+
self.is_blocking,
145+
"IcmpSocket::send_to called on nonblocking socket"
146+
);
147+
let packet = craft_icmp_packet(buffer, &self.local_addr()?, to, false);
134148
let mut to_addr = *to;
135149
// in linux `send_to` on icmpv6 socket requires destination port to be zero
136150
to_addr.set_port(0);
@@ -194,30 +208,67 @@ impl SocketTrait for IcmpSocket {
194208
}
195209
}
196210

197-
fn craft_icmp_packet(payload: &[u8], source_addr: &SocketAddr, dst_addr: &SocketAddr) -> Vec<u8> {
198-
let echo_header = IcmpEchoHeader {
199-
id: dst_addr.port(),
200-
seq: source_addr.port(),
211+
fn craft_icmp_packet(
212+
payload: &[u8],
213+
source_addr: &SocketAddr,
214+
dst_addr: &SocketAddr,
215+
request: bool,
216+
) -> Vec<u8> {
217+
// when we are sending echo reply we inject few magic bytes to the
218+
// end of payload so when receiving reply packets we can determine
219+
// if the echo reply packet is automatically sent from kernel
220+
// (in case /proc/sys/net/ipv4/icmp_echo_ignore_all is not turned off)
221+
// or we actually sent it
222+
let payload = if !request {
223+
let payload_with_magic_len = payload.len() + ECHO_REPLY_MAGIC.len();
224+
let mut buffer = vec![0u8; payload_with_magic_len];
225+
buffer[..payload.len()].copy_from_slice(payload);
226+
buffer[payload.len()..].copy_from_slice(&ECHO_REPLY_MAGIC);
227+
buffer
228+
} else {
229+
payload.to_vec()
230+
};
231+
232+
// read comments on `receiver::parse_icmp_packet` on why the
233+
// source and destination place changes based on echo reply or request
234+
let echo_header = if request {
235+
IcmpEchoHeader {
236+
id: source_addr.port(),
237+
seq: dst_addr.port(),
238+
}
239+
} else {
240+
IcmpEchoHeader {
241+
id: dst_addr.port(),
242+
seq: source_addr.port(),
243+
}
201244
};
202245

203246
let icmp_header = if source_addr.is_ipv4() {
204-
let icmp_type = Icmpv4Type::EchoRequest(echo_header);
205-
Icmpv4Header::with_checksum(icmp_type, payload)
247+
let icmp_type = if request {
248+
Icmpv4Type::EchoRequest(echo_header)
249+
} else {
250+
Icmpv4Type::EchoReply(echo_header)
251+
};
252+
Icmpv4Header::with_checksum(icmp_type, &payload)
206253
.to_bytes()
207254
.to_vec()
208255
} else {
209-
let icmp_type = Icmpv6Type::EchoRequest(echo_header);
256+
let icmp_type = if request {
257+
Icmpv6Type::EchoRequest(echo_header)
258+
} else {
259+
Icmpv6Type::EchoReply(echo_header)
260+
};
210261
let source_ip = as_socket_addr_v6(*source_addr).ip().octets();
211262
let destination_ip = as_socket_addr_v6(*dst_addr).ip().octets();
212-
Icmpv6Header::with_checksum(icmp_type, source_ip, destination_ip, payload)
263+
Icmpv6Header::with_checksum(icmp_type, source_ip, destination_ip, &payload)
213264
.unwrap()
214265
.to_bytes()
215266
.to_vec()
216267
};
217268

218269
let mut header_and_payload = Vec::with_capacity(icmp_header.len() + payload.len());
219270
header_and_payload.extend_from_slice(&icmp_header);
220-
header_and_payload.extend_from_slice(payload);
271+
header_and_payload.extend_from_slice(&payload);
221272
header_and_payload
222273
}
223274

forwarder/src/socket/icmp/receiver.rs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,26 +62,61 @@ pub fn parse_icmp_packet(packet: &[u8], is_ipv6: bool) -> Option<IcmpPacket<'_>>
6262
let icmp = IcmpSlice::from_slice(is_ipv6, &packet[payload_start_index..])?;
6363
// we only work with icmp echo requests so if any other type of icmp
6464
// packet we receive we just ignore it
65-
let correct_icmp_type = if is_ipv6 {
65+
let echo_request = if is_ipv6 {
6666
etherparse::icmpv6::TYPE_ECHO_REQUEST
6767
} else {
6868
etherparse::icmpv4::TYPE_ECHO_REQUEST
6969
};
70-
if icmp.type_u8() != correct_icmp_type || icmp.code_u8() != 0 {
70+
let echo_reply = if is_ipv6 {
71+
etherparse::icmpv6::TYPE_ECHO_REPLY
72+
} else {
73+
etherparse::icmpv4::TYPE_ECHO_REPLY
74+
};
75+
76+
if !(icmp.type_u8() == echo_request || icmp.type_u8() == echo_reply) {
77+
return None;
78+
}
79+
if icmp.code_u8() != 0 {
7180
return None;
7281
}
7382

7483
let bytes5to8 = icmp.bytes5to8();
75-
// icmp is on layer 3 so it has no idea about ports
76-
// we use identification part of icmp packet as destination port
77-
// to identify packets that are really meant for us
78-
let dst_port = u16::from_be_bytes([bytes5to8[0], bytes5to8[1]]);
79-
80-
// we also use sequence part of icmp packet as source port
81-
let src_port = u16::from_be_bytes([bytes5to8[2], bytes5to8[3]]);
84+
let id = u16::from_be_bytes([bytes5to8[0], bytes5to8[1]]);
85+
let seq = u16::from_be_bytes([bytes5to8[2], bytes5to8[3]]);
8286

8387
let payload_len = icmp.payload().len();
84-
let payload = &packet[packet.len() - payload_len..];
88+
let payload = if icmp.type_u8() == echo_request {
89+
&packet[packet.len() - payload_len..]
90+
} else {
91+
// filter the reply packets that doesn't have the magic bytes
92+
let payload = &packet[packet.len() - payload_len..];
93+
let magic_len = super::ECHO_REPLY_MAGIC.len();
94+
if payload.len() < magic_len {
95+
return None;
96+
}
97+
if payload[payload.len() - magic_len..] != super::ECHO_REPLY_MAGIC {
98+
return None;
99+
}
100+
// striping magic bytes off the payload
101+
&payload[..payload.len() - magic_len]
102+
};
103+
104+
// icmp is on layer 3 so it has no idea about ports so we use
105+
// identification and sequence part of icmp packet as src and dst port
106+
// but the problem is that if for example port 1010 sends a packet to 8000
107+
// the id and seq is like this:
108+
// | ID: 1010 | SEQ: 8000 |
109+
// now if the server wants to send echo reply from 8000 to 1010 the packet
110+
// will be like this:
111+
// | ID: 8000 | SEQ: 1010 |
112+
// the important part is the ID, if the ID of echo reply is different than
113+
// the echo request then NAT has no clue how to forward this packet, so we
114+
// swap the id and seq position based on the packet is reply or request
115+
let (src_port, dst_port) = if icmp.type_u8() == echo_reply {
116+
(seq, id)
117+
} else {
118+
(id, seq)
119+
};
85120

86121
Some(IcmpPacket {
87122
payload,

0 commit comments

Comments
 (0)