Skip to content

Commit 41829d7

Browse files
committed
add bpf filter to icmp sockets
1 parent aba89d3 commit 41829d7

6 files changed

Lines changed: 76 additions & 40 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

forwarder/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ socket2 = { version = "0.5.5", features = ["all"] }
1010
mio = { version = "1.0.2", features = ["net", "os-poll"] }
1111
parking_lot = "0.12.3"
1212
smoltcp = { version = "0.12.0", default-features = false, features = ["proto-ipv4", "proto-ipv6"] }
13+
libc = "0.2.158"

forwarder/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ pub fn run(listen_uri: Uri, remote_uri: Uri, passphrase: Option<String>) -> anyh
3535
let socket = Arc::new(socket);
3636
log::info!("listen on '{listen_addr}'");
3737

38-
let poll = poll::new(remote_uri.protocol, remote_uri.addr.is_ipv6())
39-
.with_context(|| "couldn't create poll")?;
38+
let poll =
39+
poll::new(remote_uri.protocol, remote_uri.addr).with_context(|| "couldn't create poll")?;
4040
let registry = poll
4141
.get_registry()
4242
.with_context(|| "couldn't create poll registry")?;

forwarder/src/poll.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
};
66
use icmp::IcmpPoll;
77
use parking_lot::RwLock;
8-
use std::sync::Arc;
8+
use std::{net::SocketAddr, sync::Arc};
99
use udp::UdpPoll;
1010

1111
type OnPeerRecvCallback = dyn Fn(&Peer, &mut [u8]);
@@ -34,9 +34,9 @@ pub trait Registry: Send + Sync {
3434
mod icmp;
3535
mod udp;
3636

37-
pub fn new(protocol: Protocol, is_ipv6: bool) -> anyhow::Result<Box<dyn Poll>> {
37+
pub fn new(protocol: Protocol, remote_addr: SocketAddr) -> anyhow::Result<Box<dyn Poll>> {
3838
Ok(match protocol {
3939
Protocol::Udp => Box::new(UdpPoll::new()?),
40-
Protocol::Icmp => Box::new(IcmpPoll { is_ipv6 }),
40+
Protocol::Icmp => Box::new(IcmpPoll { remote_addr }),
4141
})
4242
}

forwarder/src/poll/icmp.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use crate::{
77
MAX_PACKET_SIZE,
88
};
99
use parking_lot::RwLock;
10-
use std::sync::Arc;
10+
use std::{net::SocketAddr, sync::Arc};
1111

1212
#[derive(Debug)]
1313
pub struct IcmpPoll {
14-
pub is_ipv6: bool,
14+
pub remote_addr: SocketAddr,
1515
}
1616

1717
impl Poll for IcmpPoll {
@@ -24,17 +24,27 @@ impl Poll for IcmpPoll {
2424
peers: Arc<RwLock<PeerManager>>,
2525
on_peer_recv: Box<dyn Fn(&Peer, &mut [u8])>,
2626
) -> anyhow::Result<()> {
27-
let listen_addr = crate::peer::create_any_addr(self.is_ipv6);
27+
let is_ipv6 = self.remote_addr.is_ipv6();
28+
let listen_addr = crate::peer::create_any_addr(is_ipv6);
2829
let socket: socket2::Socket = IcmpSocket::inner_bind(listen_addr)?;
30+
31+
#[cfg(target_os = "linux")]
32+
{
33+
let filter =
34+
crate::socket::icmp::create_bfp_seq_filter(is_ipv6, self.remote_addr.port());
35+
if let Err(error) = socket.attach_filter(&filter) {
36+
log::warn!("couldn't attach bpf filter: {error:?}");
37+
}
38+
}
39+
2940
let mut buffer = [0u8; MAX_PACKET_SIZE];
30-
let header_offset = header_offset(self.is_ipv6);
41+
let header_offset = header_offset(is_ipv6);
3142

3243
loop {
3344
let Ok(size) = socket.recv(cast_maybe_uninit(&mut buffer)) else {
3445
continue;
3546
};
36-
let Some(icmp_packet) =
37-
parse_icmp_packet(&buffer[header_offset..size], self.is_ipv6, true)
47+
let Some(icmp_packet) = parse_icmp_packet(&buffer[header_offset..size], is_ipv6, true)
3848
else {
3949
continue;
4050
};

forwarder/src/socket/icmp.rs

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use socket2::{Domain, Protocol, Type};
44
use std::{
55
io,
66
mem::MaybeUninit,
7-
net::{IpAddr, Ipv6Addr, SocketAddr},
7+
net::{IpAddr, Ipv6Addr, SocketAddr, UdpSocket},
88
slice,
99
};
1010

@@ -18,20 +18,27 @@ pub struct IcmpSocket {
1818
/// actual underlying icmp socket
1919
socket: socket2::Socket,
2020
/// udp socket that is kept alive for avoiding duplicate port
21-
_udp_socket: std::net::UdpSocket,
22-
/// address of udp socket same as `udp_socket.local_addr()`
23-
udp_socket_addr: SocketAddr,
21+
_udp_socket: UdpSocket,
22+
addr: SocketAddr,
2423
}
2524

2625
impl IcmpSocket {
2726
pub fn bind(addr: &SocketAddr) -> io::Result<Self> {
28-
let udp_socket = std::net::UdpSocket::bind(addr)?;
27+
let udp_socket = UdpSocket::bind(addr)?;
2928
let udp_socket_addr = udp_socket.local_addr()?;
3029
let socket = IcmpSocket::inner_bind(*addr)?;
3130

31+
#[cfg(target_os = "linux")]
32+
{
33+
let filter = create_bfp_seq_filter(udp_socket_addr.is_ipv6(), udp_socket_addr.port());
34+
if let Err(error) = socket.attach_filter(&filter) {
35+
log::warn!("couldn't attach bpf filter: {error:?}");
36+
}
37+
}
38+
3239
Ok(IcmpSocket {
3340
_udp_socket: udp_socket,
34-
udp_socket_addr,
41+
addr: udp_socket_addr,
3542
socket,
3643
})
3744
}
@@ -50,30 +57,29 @@ impl IcmpSocket {
5057
impl SocketTrait for IcmpSocket {
5158
fn send_to(&self, buffer: &mut [u8], to: &SocketAddr) -> io::Result<usize> {
5259
let buffer_with_header = unsafe { slice_sub(buffer, ICMP_HEADER_LEN) };
53-
craft_icmp_packet(buffer_with_header, &self.udp_socket_addr, to, true);
60+
craft_icmp_packet(buffer_with_header, &self.addr, to, true);
5461
let mut to_addr = *to;
5562
// in linux `send_to` on icmpv6 socket requires destination port to be zero
5663
to_addr.set_port(0);
5764
self.socket.send_to(buffer_with_header, &to_addr.into())
5865
}
5966

6067
fn recv_from(&self, buffer: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
61-
let local_addr = self.udp_socket_addr;
62-
loop {
63-
let icmp_header_offset = header_offset(local_addr.is_ipv6());
68+
let is_ipv6 = self.addr.is_ipv6();
69+
let icmp_header_offset = header_offset(is_ipv6);
70+
// aligning in a way that the icmp payload gets written into buffer
71+
let payload_offset = icmp_header_offset + ICMP_HEADER_LEN;
6472

65-
// aligning in a way that the icmp payload gets written into buffer
66-
let payload_offset = icmp_header_offset + ICMP_HEADER_LEN;
73+
loop {
6774
let buffer_with_header = unsafe { slice_sub(buffer, payload_offset) };
68-
6975
let (size, from_addr) = self
7076
.socket
7177
.recv_from(cast_maybe_uninit(buffer_with_header))?;
7278
let icmp_packet = &mut buffer_with_header[icmp_header_offset..size];
73-
let Some(packet) = parse_icmp_packet(icmp_packet, local_addr.is_ipv6(), false) else {
79+
let Some(packet) = parse_icmp_packet(icmp_packet, is_ipv6, false) else {
7480
continue;
7581
};
76-
if packet.dst_port != local_addr.port() {
82+
if packet.dst_port != self.addr.port() {
7783
continue;
7884
}
7985
// doesn't panic because from_addr is either ipv6 or ipv4
@@ -85,13 +91,16 @@ impl SocketTrait for IcmpSocket {
8591
}
8692

8793
fn local_addr(&self) -> io::Result<SocketAddr> {
88-
Ok(self.udp_socket_addr)
94+
Ok(self.addr)
8995
}
9096
}
9197

9298
#[derive(Debug)]
9399
pub struct NonBlockingIcmpSocket {
94-
icmp_socket: IcmpSocket,
100+
socket: socket2::Socket,
101+
/// udp socket that is kept alive for avoiding duplicate port
102+
_udp_socket: UdpSocket,
103+
addr: SocketAddr,
95104
// we need to have a copy of connected addr because we
96105
// need it to craft packet, in ipv6 we need addr + port and
97106
// in ipv4 we need port
@@ -100,11 +109,15 @@ pub struct NonBlockingIcmpSocket {
100109

101110
impl NonBlockingIcmpSocket {
102111
pub fn bind(addr: &SocketAddr) -> io::Result<Self> {
103-
let icmp_socket = IcmpSocket::bind(addr)?;
104-
icmp_socket.socket.set_nonblocking(true)?;
112+
let udp_socket = UdpSocket::bind(addr)?;
113+
let addr = udp_socket.local_addr()?;
114+
let socket = IcmpSocket::inner_bind(addr)?;
115+
socket.set_nonblocking(true)?;
105116
Ok(Self {
106-
icmp_socket,
117+
socket,
107118
connected_addr: None,
119+
_udp_socket: udp_socket,
120+
addr,
108121
})
109122
}
110123
}
@@ -116,26 +129,21 @@ impl NonBlockingSocketTrait for NonBlockingIcmpSocket {
116129
.ok_or_else(|| Into::<io::Error>::into(io::ErrorKind::NotConnected))?;
117130
// it's safe because the main buffer has reserved bytes
118131
let buffer_with_header = unsafe { slice_sub(buffer, ICMP_HEADER_LEN) };
119-
craft_icmp_packet(
120-
buffer_with_header,
121-
&self.icmp_socket.udp_socket_addr,
122-
&dst_addr,
123-
false,
124-
);
125-
self.icmp_socket.socket.send(buffer_with_header)
132+
craft_icmp_packet(buffer_with_header, &self.addr, &dst_addr, false);
133+
self.socket.send(buffer_with_header)
126134
}
127135

128136
fn connect(&mut self, addr: &SocketAddr) -> io::Result<()> {
129137
self.connected_addr = Some(*addr);
130138
let mut addr = *addr;
131139
// in linux icmpv6 socket requires destination port to be zero
132140
addr.set_port(0);
133-
self.icmp_socket.socket.connect(&addr.into())?;
141+
self.socket.connect(&addr.into())?;
134142
Ok(())
135143
}
136144

137145
fn local_addr(&self) -> io::Result<SocketAddr> {
138-
self.icmp_socket.local_addr()
146+
Ok(self.addr)
139147
}
140148
}
141149

@@ -241,3 +249,19 @@ fn as_ipv6(ip: &IpAddr) -> &Ipv6Addr {
241249
_ => panic!(),
242250
}
243251
}
252+
253+
// bpf filter is really useful when having multiple forwarder instances
254+
// on same kernel, by default all icmp sockets receives all packets and then
255+
// we in user mode filter them but with this the packets get filtered on kernel
256+
#[cfg(target_os = "linux")]
257+
pub fn create_bfp_seq_filter(is_ipv6: bool, value: u16) -> [libc::sock_filter; 4] {
258+
let icmp_header_offset = header_offset(is_ipv6);
259+
let offset = icmp_header_offset + 6;
260+
[
261+
(0x28, 0, 0, offset as u32), // ldh [offset]
262+
(0x15, 0, 1, value as u32), // jne val, drop
263+
(0x06, 0, 0, 0xffffffff), // ret #-1
264+
(0x06, 0, 0, 0000000000), // drop: ret #0
265+
]
266+
.map(|(code, jt, jf, k)| libc::sock_filter { code, jt, jf, k })
267+
}

0 commit comments

Comments
 (0)