В последнее время все чаще попадаются статьи на тему сетевых технологий, от вдохновения захотелось что-нибудь смастерить. Выбор пал на приложение для мониторинга трафика на локальной машине – сколько отправили/получили и кому/от кого, писать будем на Rust.
Зависимости
-
pcap – позволяет перехватывать те самые пакеты
-
etherparse – для парсинга данных из пакета
-
hickory-proto – обработка dns пакетов, понадобиться для определения доменных имен
-
humansize – для отображение объема трафика в читабельном формате
-
dashmap – concurrent hashmap для хранения статистики
-
anyhow – маппинг ошибок
Структуры данных
Нас будет интересовать следующая информация:
-
IP адрес, куда/откуда идут пакеты
-
Доменное имя по соответствующему адресу, дабы было понятнее, с кем имеем дело
-
Сколько байтов отправили
-
Сколько байтов получили
-
Сколько всего пакетов отправлено и получено
pub type HostName = String;pub type StatsMap = Arc<DashMap<IpAddr, HostStats>>;#[derive(Default, Clone)]pub struct HostStats { pub hostname: Option<HostName>, pub bytes_sent: u64, pub bytes_received: u64, pub packets: u64,}impl HostStats { pub fn total(&self) -> u64 { self.bytes_sent + self.bytes_received }}#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)]pub enum Protocol { Tcp, Udp,}
И для определения доменного имени заведем локальный кэш IP адрес –> hostname, а чтобы понимать, в какую сторону идет пакет, будем сравнивать адреса из пакета с локальными:
// Для определения hostname по IP адресу и направления трафикаpub struct Attribution { // Локальных кэш IP -> hostname dns_cache: DashMap<IpAddr, Hostname>, // IP адреса локальной машины, // с помощью них будем определять направление трафика local_ips: HashSet<IpAddr>,}impl Attribution { pub fn new(local_ips: HashSet<IpAddr>) -> Self { Self { dns_cache: DashMap::new(), local_ips, } } pub fn record_dns(&self, ip: IpAddr, hostname: Hostname) { self.dns_cache.insert(ip, hostname); } pub fn resolve(&self, ip: &IpAddr) -> Option<Hostname> { self.dns_cache.get(ip).map(|h| h.clone()) } // Из двух IP адресов определяем remote pub fn remote_ip(&self, src: IpAddr, dst: IpAddr) -> Option<IpAddr> { if self.local_ips.contains(&src) { Some(dst) } else if self.local_ips.contains(&dst) { Some(src) } else { None } }}
Main
fn main() -> Result<()> { let args: Vec<String> = env::args().collect(); if args.len() != 2 { return Err(anyhow!("Invalid arguments, provide network interface name")); } // Из аргументов получаем имя сетевого интерфейса let device_name = &args[1]; // Ищем его в списке девайсов let device = pcap::Device::list()? .into_iter() .find(|d| &d.name == device_name) .expect(&format!("Network interface {} not found", device_name)); // Инициализируем структуры данных для хранения статистики let local_ips: HashSet<IpAddr> = device.addresses.iter().map(|a| a.addr).collect(); let attribution = Arc::new(Attribution::new(local_ips)); let stats: StatsMap = Arc::new(DashMap::new()); // Запускаем отдельный поток под обработку пакетов (имплементация ниже) spawn_capture_thread(device, stats.clone(), attribution); // Рендерим статистику в терминале (имплементация ниже) run_ui(stats)?; Ok(())}
Обработка пакетов
Из каждого пакета достаем src и dst адрес, определяем направление трафика и учитываем все в общей статистике. Отдельно будем смотреть на DNS пакеты (те, что идут по UDP на 53 порт).
pub fn spawn_capture_thread(device: Device, stats: StatsMap, attribution: Arc<Attribution>) { std::thread::spawn(move || { // Инициализируем "перехватчик" для выбранного сетевого интерфейса let device_name = device.name.clone(); let mut cap = pcap::Capture::from_device(device) .expect(&format!( "Could not create Capture from device {}", device_name )) // Отключаем буфферизацию, чтобы получать пакеты сразу .immediate_mode(true) .open() .expect(&format!( "Could not activate Capture from device {}", device_name )); // Итерируемся по сетевым пакетам while let Ok(packet) = cap.next_packet() { let Ok(sliced) = SlicedPacket::from_ethernet(packet.data) else { continue; }; // Извлекаем IP адреса let (src_ip, dst_ip) = match &sliced.net { Some(NetSlice::Ipv4(v4)) => ( IpAddr::V4(v4.header().source_addr()), IpAddr::V4(v4.header().destination_addr()), ), Some(NetSlice::Ipv6(v6)) => ( IpAddr::V6(v6.header().source_addr()), IpAddr::V6(v6.header().destination_addr()), ), _ => continue, }; // Извлекаем протокол, будем смотреть только UDP и TCP трафик let (protocol, src_port, dst_port, payload, transport_len) = match &sliced.transport { Some(TransportSlice::Tcp(t)) => ( Protocol::Tcp, t.source_port(), t.destination_port(), t.payload(), packet.data.len(), ), Some(TransportSlice::Udp(u)) => ( Protocol::Udp, u.source_port(), u.destination_port(), u.payload(), packet.data.len(), ), _ => continue, }; // Отдельно смотрим на DNS пакеты, // именно из них извлекаем доменные имена и кладем в кэш if protocol == Protocol::Udp && (src_port == 53 || dst_port == 53) { // Имплементация ниже handle_dns_packet(payload, &attribution); } // Определяем направления трафика, сравнивая IP из пакета с собственными let Some(remote_ip) = attribution.remote_ip(src_ip, dst_ip) else { continue; }; let is_outgoing = remote_ip == dst_ip; // Берем имя хоста из кэша let hostname = attribution.resolve(&remote_ip); // Обновляем статистику let mut entry = stats.entry(remote_ip).or_default(); if is_outgoing { entry.bytes_sent += transport_len as u64; } else { entry.bytes_received += transport_len as u64; } entry.packets += 1; if hostname.is_some() { entry.hostname = hostname; } } });}
Заполнять кэш доменных имен будем на основе соответствующих записей в DNS пакетах:
pub fn handle_dns_packet(payload: &[u8], attribution: &Attribution) { let Ok(msg) = Message::from_vec(payload) else { return; }; // Смотрим ответы DNS запросов for answer in msg.answers { // Берем имя хоста из соответствующих DNS записей if matches!(answer.record_type(), RecordType::A | RecordType::AAAA) { if let Some(ip) = answer.data.ip_addr() { let hostname = answer.name.to_string() .trim_end_matches('.').to_string(); attribution.record_dns(ip, hostname); } } }}
Отображение данных в терминале
В live режиме отображаем таблицу с топ 30 по объему трафика, ререндерим каждые 250мс или по нажатию клавиши:
pub fn run_ui(stats: StatsMap) -> io::Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; loop { if event::poll(Duration::from_millis(250))? { if let Event::Key(key) = event::read()? { // Кнопка для выхода if key.code == KeyCode::Char('q') { break; } // Кнопка для обнуления статистики if key.code == KeyCode::Char('r') { stats.clear(); } } } // Сортируем по объему трафика let mut rows: Vec<(IpAddr, HostStats)> = stats .iter() .map(|entry| (*entry.key(), entry.value().clone())) .collect(); rows.sort_by(|a, b| b.1.total().cmp(&a.1.total())); terminal.draw(|frame| { let area = frame.area(); // Отображаем топ 30 в виде таблицы let table_rows: Vec<Row> = rows .iter() .take(30) .map(|(ip, s)| { let label = s.hostname.clone().unwrap_or_else(|| ip.to_string()); Row::new(vec![ label, ip.to_string(), format_size(s.bytes_sent, BINARY), format_size(s.bytes_received, BINARY), format_size(s.total(), BINARY), s.packets.to_string(), ]) }) .collect(); let widths = [ Constraint::Percentage(30), Constraint::Percentage(20), Constraint::Percentage(12), Constraint::Percentage(12), Constraint::Percentage(14), Constraint::Percentage(12), ]; let table = Table::new(table_rows, widths) .header( Row::new(vec!["Host", "IP", "Sent", "Received", "Total", "Packets"]) .style(Style::default().add_modifier(Modifier::BOLD)), ) .block( Block::default() .borders(Borders::ALL) .title(" Live Traffic ('q' – quit, 'r' – reset) "), ) .row_highlight_style(Style::default().fg(Color::Yellow)); frame.render_widget(table, area); })?; } disable_raw_mode()?; execute!(terminal.backend_mut(), LeaveAlternateScreen)?; Ok(())}
Результат
Запускаем под sudo (без него pcap не будет работать), передав имя интересующего нас сетевого интерфейса (в моем случае «en0»), и начинаем ходить по страницам в браузере:
Возможные улучшения:
-
учет других протоколов
-
уход от DashMap, чтобы избавиться от блокировок
-
дополнительные источники для определения доменных имен
-
возможность блокировать трафик (pcap в такое не умеет)
-
возможность обработки нескольких сетевых интерфейсов одновременно
Ссылки
-
Github проекта – https://github.com/veshutov/nets
-
Мониторинг здорового человека – https://github.com/GyulyVGC/sniffnet
ссылка на оригинал статьи https://habr.com/ru/articles/1055582/