Низкоуровневое обнаружение Wi-Fi устройств в домашней сети +41




Чтобы сделать собственное уникальное устройство для «умного дома» сейчас достаточно купить микроконтроллер и электронные компоненты. Конечно, на рынке уже есть множество «умных» устройств, но не все производители предоставляют открытое API, и уж точно единицы разрешают (или по крайней мере не запрещают) создавать собственные прошивки. Иногда наступает тот момент, когда кажется, что разработать и запрограммировать собственное устройство будет лучшим решением.

В этой статье я расскажу про несколько способов «‎научить» микроконтроллер распознавать присутствие людей дома исключительно с помощью Wi-Fi.

Предисловие


Источник изображения
Мне с детства нравились часы. Конечно, желания заполнить всю комнату звенящими часами, словно Доктор Браун, у меня не было, но часов в моей комнате было достаточно. Тем не менее, пришлось повзрослеть и желание иметь множество часов как-то приутихло.

Наступила взрослая самостоятельная жизнь и вопрос с часами стал одним из маленьких конфликтов интересов. Мне хотелось иметь светящиеся часы, которые ненавязчиво покажут время в любое время дня и ночи. Моя девушка же придерживается мнения, что никакой свет, даже слабый, не должен препятствовать засыпанию.

Компромисс находился простой: в процессе подготовки ко сну выключать часы. Правда, их нужно с утра как-то включить, что в свою очередь нерационально: проще взять телефон, там время тоже есть. Значит, нужно как-то автоматически определять моменты, подходящие для отключения и включения часов.

Быстро проанализировав подготовку ко сну, я обнаружил там повторяющееся действие, а именно, отключение Wi-Fi на телефоне. Такой триггер позволит «умным» часам выключаться не по сухому «расписанию», а в нужные моменты времени.

Давайте определимся, из чего состоят часы и какие ограничения накладываются на окружение.

Что там внутри


Подключение платы к светодиодной матрице (источник alexgyver.ru)

Набор юного «самодельщика» прост:

  • плата Wemos D1 Mini на базе чипа ESP8266 с Wi-Fi;
  • светодиодная WS2812B-совместимая матрица размером 32х8;
  • блок питания 5В, 2А;
  • для разработки прошивки используется Arduino IDE.
Разумеется, полагаться на внутренние часы микроконтроллера неразумно и нужна отдельная плата часов реального времени. Однако, согласно любительскому исследованию, скорость расхождения внутреннего времени микроконтроллера составляет примерно одну секунду в день. Это не критично для настенных часов, а при наличии доступа в интернет, синхронизация с сервером времени решит проблему.

Минимальные вложения для сборки данного устройства накладывают следующие ограничения:

  • в домашней сети отсутствуют какие-либо системы, выполняющие мониторинг сети;
  • допустимы изменения в конфигурации домашнего роутера;
  • допустимы изменения в конфигурации сетевого подключения на телефоне;
  • модификация ПО домашнего роутера запрещена;
  • модификация ПО телефона запрещена.

Под модификацией ПО подразумевается создание собственных программных решений, которые по событиям в операционной системе телефона могут отправить команду. Во-первых это «‎не спортивно»‎, а во-вторых потребует кроссплатформенной разработки, если захочется включить в поддержку чей-то iPhone, а потом и пару ноутбуков с разными операционными системами.

Современные устройства умеют подменять MAC-адрес во имя конфиденциальности
Единственный уникальный идентификатор, с помощью которого можно найти телефон в домашней сети — MAC-адрес. Однако, современная техника умеет генерировать «подставной» MAC-адрес, что усложняет определение устройства. Тем не менее, для заданных Wi-Fi-сетей эту опцию можно отключить.

Итак, у нас есть MAC-адрес, что будем делать?

Поиск устройства


Можно придумать несколько вариантов в зависимости от искомого устройства и используемого маршрутизатора/точки доступа.

Большую часть решений объединяет одно: подключение к домашнему Wi-Fi. Минимальный код, с которым будем работать.

#include <ESP8266WiFi.h>
#include <WiFiManager.h>

void setup() {
  Serial.begin(115200);

  WiFiManager wifiManager;
  wifiManager.setDebugOutput(false);
  wifiManager.autoConnect("habr-example", "supergeneral");
  Serial.print("Connected! IP address: ");
  Serial.println(WiFi.localIP());

  /* Здесь также инициализация для FastLED и других библиотек,
   * которые не важны для данного примера 
   **/ 
}

void loop() {
  // Основной код
  ledTick();
}

Для упрощения работы с Wi-Fi используется библиотека WiFiManager. Если в памяти микроконтроллера нет информации о известных точках или они недоступны, WiFiManager запустит собственную точку доступа с веб-интерфейсом для быстрого подключения к новому Wi-Fi.

Кто там


Самое простое решение всегда на поверхности: давайте «пинганем» телефон. На поверку, в мире микроконтроллеров протокол ICMP используется неохотно. Так, в lwIP (lightweight IP, реализации стека TCP/IP для встраиваемых систем) есть минимальная поддержка протокола ICMP, но этого недостаточно. Для наших целей придется поставить библиотеку ESP8266-ping.

Для уведомления об успешном или неуспешном пинге библиотека использует функции обратного вызова. Напишем функцию с простой логикой:

  • если устройство было недоступно, а сейчас доступно — устройство появилось в сети;
  • если устройство недоступно MAX_PING попыток подряд — устройство ушло из сети.

#define MAX_PING 5

boolean current_state = false;
unsigned char attempts = 0;

void* responseCallback(const PingerResponse& response) {
  if(response.ReceivedResponse) {
    if(current_state == false) {
      attempts = 0;
      current_state = true;
      Serial.println("Device on");
    }
  } else {
    if(current_state == true) {
      attempts++;
      if(attempts > MAX_PING) {
        current_state = false;
        Serial.println("Device off");
      }
    }
  }
  return (void*)true;
}

Инициализируем библиотеку ESP8266-ping:

#define PING_INTERVAL 1000

Pinger pinger;

void setup() {
  // Общая инициализация опущена

  pinger.OnReceive(&responseCallback);
}

Так как пинг — не единственная наша задача, создаем функцию, которая раз в PING_INTERVAL миллисекунд отправляет ICMP-пакет.

unsigned long previousTime = 0;
void pingTick() {
  if(millis() - previousTime > PING_INTERVAL) {
    previousTime = millis();
    pinger.Ping("192.168.88.148", 1, PING_INTERVAL / 2);
  }
}

void loop() {
  // Другие Tick() функции опущены
  pingTick();
}

Полный исходный текст
#include <ESP8266WiFi.h>
#include <WiFiManager.h>

#include <Pinger.h>

#define MAX_PING 5
#define PING_INTERVAL 1000

Pinger pinger;
boolean current_state = false;
unsigned char attempts = 0;

void* responseCallback(const PingerResponse& response) {
  if(response.ReceivedResponse) {
    if(current_state == false) {
      attempts = 0;
      current_state = true;
      Serial.println("Device on");
    }
  } else {
    if(current_state == true) {
      attempts++;
      if(attempts > MAX_PING) {
        current_state = false;
        Serial.println("Device off");
      }
    }
  }
  return (void*)true;
}

unsigned long previousTime = 0;
void pingTick() {
  if(millis() - previousTime > PING_INTERVAL) {
    previousTime = millis();
    pinger.Ping("192.168.88.148", 1, 1000);
  }
}

void setup() {
  Serial.begin(115200);

  WiFiManager wifiManager;
  wifiManager.setDebugOutput(false);
  wifiManager.autoConnect("habr-example", "supergeneral");
  Serial.print("Connected! IP address: ");
  Serial.println(WiFi.localIP());

  pinger.OnReceive(&responseCallback);
}

void loop() {
  pingTick();
}

Третий аргумент функции Ping задает время ожидания ответа и ему стоит быть меньше, чем промежутки между пингами. Однако, здесь фигурирует только IP-адрес, еще и явно прописанный в прошивке. Есть два решения данной ситуации:

  1. в настройках DHCP-сервера явно «прибить» адрес к MAC-адресу искомого устройства;
  2. пинговать все адреса подсети и проверять MAC-адрес.

При условии, что часы — это домашнее устройство, а домашнюю сеть и телефоны не меняют пять раз на дню, то первое решение выглядит достойно. При этом время реакции часов на выход устройства из сети — MAX_PING * PING_INTERVAL миллисекунд.

Но случаются вредные устройства, которые не отвечают на ICMP-запросы.

Открывайте! Мы знаем, что вы тут


Далеко за примером ходить не надо: операционная система Microsoft Windows по умолчанию игнорирует ICMP-запросы. Такой расклад дел не сильно усложняет жизнь. Устройство может игнорировать ICMP-запросы, но ARP-запросы ему проигнорировать не получится. Поэтому для «особо вредных» устройств у нас более хитрый план: очищаем ARP-таблицу, отправляем несколько пингов, проверяем ARP-таблицу.
ARP (англ. Address Resolution Protocol — протокол определения адреса) — протокол в компьютерных сетях, предназначенный для определения MAC-адреса по IP-адресу другого компьютера. © Википедия

Особенность ARP-протокола заключается в том, что он работает только в пределах одного Ethernet-сегмента. Тем не менее, ожидается, что домашняя сеть не должна быть сложной.

Доступ к ARP-таблицам на ESP8266 возможен через функции lwIP. Эти функции — для смелых и простым смертным не нужны, поэтому примеров и объяснений достаточно мало, нужно читать еще и комментарии к коду. Добавляем в проект включение заголовочных файлов lwip:

#include <lwip/etharp.h>

Удаляем функцию обратного вызова и изменяем pingTick() следующим образом:

void pingTick() {
  if(millis() - previousTime > PING_INTERVAL) {
    previousTime = millis();

    // IP-адрес искомого устройства, может быть глобальным
    IPAddress addr = IPAddress(192,168,88,148);

    // Итерация по ARP-таблице
    ip4_addr_t *ip;
    struct netif *netif;
    struct eth_addr *ethaddr;
    bool found = false;
    for(int i=0; i<ARP_TABLE_SIZE; i++) {
      if (etharp_get_entry(i, &ip, &netif, &ethaddr)) {
        if(addr[0] == (ip->addr & 0xFF) &&
           addr[1] == (ip->addr >> 8 & 0xFF) &&
           addr[2] == (ip->addr >> 16 & 0xFF) &&
           addr[3] == (ip->addr >> 24 & 0xFF)) {
          found = true;
        }
      }
    }

    // Очищаем ARP-таблицу 
    etharp_cleanup_netif(netif);
    
    // Запускаем следующий раунд пингов
    pinger.Ping(addr, 5, 100);

    // Обрабатываем информацию
    if(found) {
      Serial.println("Device on");
    } else {
      Serial.println("Device off");
    }
  }
}

Время реакции этого способа равно PING_INTERVAL, в моем случае я увеличил это число до пяти секунд. Способ потенциально хороший, но в тестах в моей домашней сети он постоянно сбоил и способ с ICMP-ответами работал стабильнее. Поэтому если ваше устройство не скупится отвечать на пинг, то лучше использовать предыдущий способ.

Полный исходный код
#include <ESP8266WiFi.h>
#include <WiFiManager.h>

#include <Pinger.h>
#include <lwip/etharp.h>

#define MAX_PING 5
#define PING_INTERVAL 5000

Pinger pinger;
boolean current_state = false;
unsigned char attempts = 0;

unsigned long previousTime = 0;
void pingTick() {
  if(millis() - previousTime > PING_INTERVAL) {
    previousTime = millis();

    // IP-адрес искомого устройства, может быть глобальным
    IPAddress addr = IPAddress(192,168,88,148);

    // Итерация по ARP-таблице
    ip4_addr_t *ip;
    struct netif *netif;
    struct eth_addr *ethaddr;
    bool found = false;
    for(int i=0; i<ARP_TABLE_SIZE; i++) {
      if (etharp_get_entry(i, &ip, &netif, &ethaddr)) {
        if(addr[0] == (ip->addr & 0xFF) &&
           addr[1] == (ip->addr >> 8 & 0xFF) &&
           addr[2] == (ip->addr >> 16 & 0xFF) &&
           addr[3] == (ip->addr >> 24 & 0xFF)) {
          found = true;
        }
      }
    }

    // Очищаем ARP-таблицу 
    etharp_cleanup_netif(netif);
    
    // Запускаем следующий раунд пингов
    pinger.Ping(addr, 5, PING_INTERVAL / 10);

    // Обрабатываем информацию
    if(found) {
      if(current_state == false) {
        current_state = true;
        Serial.println("Device on");
      }
    } else {
      if(current_state == true) {
        current_state = false;
        Serial.println("Device off");
      }
    }
  }
}

void setup() {
  Serial.begin(115200);

  WiFiManager wifiManager;
  wifiManager.setDebugOutput(false);
  wifiManager.autoConnect("habr-example", "supergeneral");
  Serial.print("Connected! IP address: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  pingTick();
}

Но что делать, если эти варианты по каким-то причинам не подходят?

Чуткий нюх


Микроконтроллер на базе ESP8266 может быть Wi-Fi-сниффером. У него можно включить неразборчивый режим (promiscuous mode) и собирать пролетающие мимо пакеты. Существует несколько репозиториев, в которых есть код запускающий сниффер на вашем ESP8266.

Телефоны с включенным Wi-Fi будут постоянно рассылать разные пакеты, и часть из них не будет иметь шифрования. Таким образом, можно определять наличие или отсутствие телефона в сети.

Несмотря на то, что способ надежный как швейцарские часы, у данного решения есть ряд проблем:

  • если рядом множество Wi-Fi сетей, то поток пакетов будет большим, что потребует самодельного фильтра. Возможно этот фильтр будет медленнее, чем в lwIP.
  • В этом режиме ESP8266 не имеет доступа в интернет, так как не подключена к Wi-Fi. Если вы хотели добавить погоду или синхронизацию с NTP — это будет затруднительно.
  • Микроконтроллер может «не услышать» пакет от вашего устройства в силу физических причин, а так как пакет не предназначался микроконтроллеру, повторения не будет.
  • Сниффер может не понравиться соседям, их друзьям или местным законам.

Но существует более простой, надежный и быстрый способ получения информации о подключенных устройствах.

Уведомления


Этот способ требует соответствующего сетевого оборудования. Если у ваш домашний роутер работает на OpenWRT или RouterOS, то он точно подойдет.

Данный способ построен на парсинге логов маршрутизатора. Маршрутизатор всегда знает MAC-адрес подключившегося и в большинстве случаев выдает адрес с помощью DHCP-сервера. Поэтому, логи маршрутизатора — это самый быстрый и самый надежный способ.

Для моего Mikrotik hAP ac lite лог подключения и отключения выглядит следующим образом. MAC-адреса вымышлены.

wireless,info 80:35:XX:XX:XX:XX@wlan2: disconnected, received deauth: sending station leaving (3)
wireless,info 80:35:XX:XX:XX:X@wlan2: connected, signal strength -44

Настраиваем логирование по метке wireless,info в удаленный порт. Для ускорения обработки на микроконтроллере задействуем протокол UDP. Настраиваем UDP-сервер следующим образом:

#include <WiFiUdp.h>

WiFiUDP syslog;
void setup() {
  // Общая инициализация опущена

  syslog.begin(514);
}

Далее периодически опрашиваем UDP-сервер на предмет пришедших пакетов.

#define BUF_SIZE 4096
char str[BUF_SIZE];
String masterMac = "80:35:XX:XX:XX:XX";
void syslogTick() {
  int packetSize = syslog.parsePacket();
  if(packetSize > 0) {
    int n = syslog.read(str, BUF_SIZE);
    str[n] = '\0';

    String syslog_str = String(str);
    String mac = syslog_str.substring(14, 31);
    String reason = syslog_str.substring(39);
    
    bool connected = true;
    if(reason.startsWith("disconnected")) {
      connected = false;
    }

    if(mac != masterMac) {
      return;
    }

    if(connected) {
      Serial.println("Device connected!");
    } else {
      Serial.println("Device disconnected!");
    }
  }
}

Пакет содержит MAC-адрес и причину события. Достаточно «‎разобрать»‎ пришедшую строку и записать состояние.

Этот способ, конечно, тоже обладает недостатком. Так, при перезагрузке микроконтроллера, потребуется узнать текущее состояние искомого устройства. Но для этого можно использовать способ с ICMP-запросом.

Полный исходный код
#include <ESP8266WiFi.h>
#include <WiFiManager.h>

#include <WiFiUdp.h>

WiFiUDP syslog;
void setup() {
Serial.begin(115200);

WiFiManager wifiManager;
wifiManager.setDebugOutput(false);
wifiManager.autoConnect(«habr-example», «supergeneral»);
Serial.print(«Connected! IP address: „);
Serial.println(WiFi.localIP());

syslog.begin(514);
}

#define BUF_SIZE 4096
char str[BUF_SIZE];
String masterMac = “80:35:XX:XX:XX:XX»;
void syslogTick() {
int packetSize = syslog.parsePacket();
if(packetSize > 0) {
int n = syslog.read(str, BUF_SIZE);
str[n] = '\0';
String syslog_str = String(str);
String mac = syslog_str.substring(14, 31);
String reason = syslog_str.substring(39);

bool connected = true;
if(reason.startsWith(«disconnected»)) {
connected = false;
}

if(connected) {
Serial.println(«Device connected!»);
} else {
Serial.println(«Device disconnected!»);
}
}
}

void loop() {
syslogTick();
}






Комментарии (18):

  1. gecube
    /#23784775 / +1

    не отключаю вайфай на телефоне - это бессмысленно, пускай качает обновы по ночам. А вот "ночной" режим "без уведомлений" - то что надо

  2. ZekaVasch
    /#23784859 / +4

    Слышал, что есть семьи которые для хорошего сна выключают еще и роутер. А зачем выключать вайфай в телефоне это вопрос

    • FirsofMaxim
      /#23784873 / +2

      Хороший роутер с мобильной аппой для управления повышает успеваемость балбесов (личный опыт).

    • lab412
      /#23784951 / +1

      выключаешь роутер и телефон переключается на сотовую сеть. где профит? а если выключил телефон - то в принципе уже роутер нет смысла выключать. ну если только нет ощущения что WiFi сигнал влияет на клетки головного мозга и засылает туда ___ (вставить нужное) покаты ты спишь...

      • cck7777
        /#23786345

        Как-то для проверки платы от дистанционного пульта медоборудования с неизвестным (проприетарным? но по обозначению одной из микросхем -- на базе Wi-Fi) СВЧ протоколом сделал я себе простейший датчик излучения на СВЧ-диоде. Был весьма удивлён тому, насколько роутер мало излучает, если нет трафика (по сравнению с приёмом/передачей).

      • Kvakosavrus
        /#23787587

        Нет, wifi не нужен. Чипы вакцины получают команды через 5G, это установленный факт!

  3. delphinpro
    /#23785153 / +5

    Голосовой помощник решает все проблемы =)
    «Алиса, сколько времени» – и даже глаза открывать не надо.

  4. Amor-roma
    /#23785817

    Все вышеозначенные проблемы автора достойны похвалы!

    Но решаются не техническими, а организационными методами.

    0. Повесить те часы которые нравятся

    1. Купить девушке повязку для глаз

    2. Сменить девушку

    Profit!

    • SnakeSolid
      /#23787263 / +2

      sarcasm:on

      Изолировать девушку в отдельной комнате и доставать при необходимости.

      sarcasm:off

  5. MillaBren
    /#23786663 / +2

    Заголовок статьи про низкоуровневое обнаружение мне намекал на анализатор спектра, ибо ниже PHY в OSI уже некуда, а тут сюрпрайз.

    "Сниффер может не понравиться соседям, их друзьям или местным законам" - не совсем так. Сниффер WiFi невозможно обнаружить, если мониторинг идёт в полностью пассивном режиме (не считая варианта с человеком, который ходит везде с ноутом, внешним адаптером и анализатором спектра:))

    Про часы и темноту, кстати, актуально. Купила себе "почти умные" часы с режимом светового будильника, а у них есть WiFi, но даже в гостевую сетку не хотелось их пускать. Часы же продолжали мерзко моргать значком настройки WiFi даже ночью!
    Первая мысль была все-таки поставить приложение, найти там настройки, запустить или отключить WiFi (что с кнопочного интерфейса часов было невозможным). В итоге победила лень и белая изолента на моргающую иконку.

  6. anonimNO
    /#23786915 / +1

    У меня была такая же проблема с часами. Умный дом уже есть и я сделал так:

    • если выключился весь свет в комнате и компьютер с ноутом и после заката - выключить часы

    • если рассвет или на телефоне выключился режим не беспокоить (выключается вместе с будильником), то включить часы

    И при выключении вайфая на телефоне - интернет начинает работать через сотовую сеть, что дают большую силу излучения. Это для свидетелей секты вредных излучений.

  7. fk01
    /#23787253

    Что, спрашивается, мешало использовать для обнаружения наличия устройств в сети ARP-протокол. Что-то наподобии ArpPing. Идея в том, что любое устройство работающее в IP-сети обязано отвечать на ARP-запросы (только в пределах одной локальной сети). Иначе с ним другие никак и ни по каким протоколам работать не смогут. И сетевые экраны ARP обычно не блокируют, да и делается это немного сложней чем через iptables (см. arptables).

  8. unsignedchar
    /#23787291

    Сниффер может не понравиться соседям, их друзьям или местным законам.

    WAT?? Как может кому-то (не) нравиться обычный радиоприемник? Который и так есть в любом телефоне?

  9. ZaitsXL
    /#23789589

    По моему часы в спальне нужны для того, чтобы проснувшись среди ночи по какой-либо причине понять, спать ли дальше или нет. Если часы ночью выключены, то какой от них толк?

    Про выключение вайфая выше уже написали.

    Ну и умный дом - это который сам учится например по ряду показателей когда вы обычно ложитесь спать и отключает часы, вы же фактически сами выключаете часы, просто хитроумно

  10. balamutang
    /#23789801

    Да, надо тоже такие часы запилить, чтобы настроить на них свои хотелки

    Я купил на али светодиодные, выяснилось что они светят как прожектор, даже запихнул их за гобеленовую картину и то они просвечиваются слишком ярко через нее. И к тому же время когда у них ночной режим включается мне не подходит, жестко задано 6 утра и 7 вечера, а мне больше подошло бы 7 утра и 10 вечера, но поменять нельзя.

  11. dbond
    /#23792175

    Статья безусловно интересна, но на али есть довольно бюджетные часы с ЖКИ дисплеем. Я пару лет назад приобрел такие во все комнаты. Дисплей имеет 3 уровня подсветки, самый слабый из которых настолько слабый, что заставляет присматриваться в темноте. Не дает отсветов на предметах и не мешает спать. Второй уровень - нормальный, а третий включается по нажатию кнопки сверху - светит довольно ярко, видно даже днем. Батареек хватает примерно на год, но есть еще и сетевое питание.
    Аналогичные ищутся по запросу "настольные часы backlight" рекомендую те у которых белый фон дисплея.

    А по поводу присутствия устройств в сети вайфай, автор при своих навыках рано или поздно придет к умному дому, вот там, например в Home Assistant есть трекеры устройств ассоциированные с юзером оттуда можно отслеживать телефон например по мак адресу или по трекеру приложения. В конце концов можно написать любой сценарий к примеру - в темное время суток, при выключенном освещении, телевизоре и присутствии девушки в квартире - отключать часы. Например у меня при падении CO2 ниже 400 (это бывает через пару часов, когда дома никого нет) отключаются все телевизоры и освещение в комнатах.

  12. ITMatika
    /#23793179

    Простите, не удержался

    Подключение синей изоленты к Arduino (c) Alex Gyver