Detección de presencia por ARP, o cómo saber si tu hijo está en casa

Los adolescentes prefieren morir antes que salir de casa sin su móvil y la condenación eterna antes que permitir que se quede sin batería, así que comprobar si el móvil de tu hijo está conectado a tu red WIFI es una buena forma de saber si está o no en casa.

El siguiente programa te avisa por Telegram cuando tu hijo entra o sale de casa. De modo general, te avisa cuando un determinado dispositivo pasa a estar presente o desaparece de tu red doméstica.

Este programa puede ejecutarse en cualquier máquina Unix-like de nuestra red local que esté encendida permanentemente. En mi caso utilizo una Raspberry PI que tiene otros usos adicionales.

arp-20180622.py (Código fuente)

#!/usr/bin/env python3

# (c) 2018 Jesús Cea Avión - jcea@jcea.es - https://www.jcea.es/
# This code is licensed under AGPLv3.

import time
import subprocess
import socket

import telegram.ext


MAC = 'XX:XX:XX:XX:XX:XX'

# Esperamos a tener red porque no queremos que se pierda la
# primera notificación de arranque.
# Además, no queremos un falso negativo cuando aún no tenemos red.
while True:
    try:
        socket.gethostbyname('api.telegram.org')
        break
    except socket.gaierror:
        time.sleep(1)

TELEGRAM_TOKEN = '0000:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

updater = telegram.ext.updater.Updater(TELEGRAM_TOKEN)
updater.start_polling(timeout=60)
job_queue = updater.job_queue


presencia = None
prefix = ' <b>Arranque del sistema:</b>'

current_message = ''

# Usuarios autorizados
# jcea y L*****
usuarios = (00000000, 00000000)


def callback(bot, update):
    usuario = update.effective_user['id']
    if usuario in usuarios:
        bot.send_message(usuario, text=current_message, parse_mode='HTML')


updater.dispatcher.add_handler(
        telegram.ext.MessageHandler(
            filters=(telegram.ext.filters.Filters.user(user_id=usuarios) &
                     telegram.ext.filters.Filters.text),
            callback=callback,
            ))


def msg(bot, job):
    texto = job.context['msg']
    for i in usuarios:
        bot.send_message(i, text=texto, parse_mode='HTML')


while True:
    r = subprocess.run(
            [('for i in `/usr/bin/seq 1 254`; '
              'do /bin/ping -c 1 192.168.1.$i & done; wait')],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
            shell=True, timeout=60, check=True)
    r = subprocess.run(
            ['/usr/sbin/arp', '-n'],
            universal_newlines=True,
            stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
            timeout=60, check=True)
    presente = r.stdout.find(MAC) != -1
    if presencia != presente:
        if presente:
            current_message = ('%s:%s La luz está encendida'
                               % (time.ctime(), prefix))
        else:
            current_message = ('%s:%s La luz <b>ESTÁ APAGADA</b>'
                               % (time.ctime(), prefix))
        print(current_message)

        job_queue.run_once(msg, when=0, context={'msg': current_message})

        presencia = presente
        prefix = ''
    time.sleep(60)

La configuración está en el propio código fuente:

  • Línea 13: Indicamos la dirección MAC del dispositivo que queremos detectar.
  • Línea 25: Indicamos el token de acceso que nos ha proporcionado Telegram.
  • Líneas 37-39: Listamos los identificadores de usuario que van a recibir las notificaciones de cambio de estado y que están autorizados para enviar mensajes al Bot que tenemos funcionando en Telegram.
  • Línea 65: Indicamos la red /24 en la que nos encontramos.

El funcionamiento del servicio es el siguiente:

Al lanzar el programa, esperamos en las líneas 18-23 hasta que nuestro dispositivo tenga red. Esto se hace por dos motivos: a) para evitar un falso positivo intentando localizar el dispositivo de interés cuando nuestro equipo de detección todavía está levantando la red y b) para que no se pierda la notificación inicial si intentamos enviarla cuando aún no hemos conectado a Telegram [1].

[1] Tal vez esto último no sea necesario y la biblioteca Telegram que empleamos encole localmente los mensajes mientras no esté conectada a Telegram. No recuerdo los detalles.

Una vez que tenemos red, nos conectamos a Telegram con las credenciales correspondientes. La biblioteca que empleamos lanzará hilos de fondo que se encargan de gestionar los detalles de comunicación con Telegram, se reconecta si es necesario, etc.

Cuando arrancamos el programa no sabemos cuáles han sido los cambios de presencia del dispositivo de interés, así que vamos a enviar un mensaje de arranque con el estado actual de presencia y un indicador de que el sistema se ha arrancado ahora. Los detalles están en las líneas 32-35.

El bucle principal en sí empieza en la línea 62. Lo primero que hacemos es intentar enviar un datagrama ICMP Ping a todas las máquinas de la red local (subred /24). Solo enviamos un paquete por máquina, aunque el hecho de usar un comando externo por cada dirección IP y ejecutarlos todos a la vez es algo muy poco elegante y genera picos de carga puntuales en el detector. Lo cierto es que en la Raspberry PI en la que funciona esto no ha dado ningún problema, pero es algo a mejorar en el futuro.

Los 254 comandos se ejecutan en paralelo y se espera a que terminen. Se detectan errores y nos aseguramos de que el proceso entero no supere 60 segundos, como medida de seguridad. Lo cierto es que no estamos gestionando los errores, así que si el comando falla o supera ese tiempo de ejecución, el detector moriría.

Lo importante aquí es generar tráfico con todas las direcciones de la red local. El contenido de dicho tráfico es irrelevante y también es irrelevante que esas direcciones estén en uso o no. Lo que nos interesa, la clave de todo este proceso es que la tabla ARP [2] del sistema operativo se actualice.

[2]

A nivel físico los datagrama en la red local se direccionan utilizando 48 bits, las direcciones físicas. Cuando enviamos un datagrama a una dirección IP de la red local, hay que descubrir la dirección MAC que se corresponde en este momento a esa dirección IP. El Sistema Operativo guardará en una caché esa correspondencia durante unos segundos para no tener que realizar una resolución nueva en cada datagrama transmitido.

Este protocolo de resolución se llama Address Resolution Protocol (ARP). Para IPv4 el protocolo está definido en el RFC 826 y sus actualizaciones. Esa caché de resoluciones dirección IP -> dirección MAC se llama tabla ARP.

En las líneas 68-72 ejecutamos un comando externo para obtener la tabla ARP del sistema operativo. En la línea 73 determinamos si el dispositivo que nos interesa monitorizar está o no está en la red.

Como solo nos interesan los cambios de estado de ese dispositivo (salvo cuando arrancamos el sistema, que nos interesa conocer el estado actual), no hacemos nada si el estado no ha cambiado (línea 74).

Si el estado ha cambiado, en las líneas 75-80 generamos un mensaje apropiado según el dispositivo haya aparecido o desaparecido de la red. Mostramos el estado actual en el terminal y enviamos un mensaje por Telegram en la línea 83.

Por último, esperamos un minuto y repetimos el proceso.

Este es el bucle principal: nos envía una notificación de estado en el arranque y luego nos notifica los cambios de estado, comprobándolo una vez por minuto.

Existe una tarea secundaria en las líneas 48-53: ahí indicamos a Telegram que nos envíe los mensajes de texto provenientes de los usuarios autorizados. Cualquier otro usuario será ignorado. Si nos llega un mensaje de texto cualquiera, se ejecutará la rutina en las líneas 42-45 que simplemente responderá a cualquier texto con el estado actual del dispositivo monitorizado. Esto es útil si nos tememos que hemos perdido mensajes o para asegurarnos de que el Bot está funcionando correctamente y no se ha quedado colgado o ha perdido la red.

En funcionamiento veremos algo parecido a:

20180622-presencia_arp.png

Aquí vemos los eufemismos "La luz ESTÁ APAGADA" y "La luz está encendida" para identificar cuando nuestro hijo está o no en casa. No es plan de que se entere de que lo estemos controlando si se tropieza con nuestro móvil por casualidad... :-)

Es interesante seguir la historia que nos cuenta el Bot. La Raspberry PI se reinició a las 18:27 y el chico estaba en casa (de hecho, la reinició él porque el Kodi no le estaba funcionando correctamente). A las 21:24 salió de casa a tirar la basura. Se pierde la conexión en el ascensor. Desde la calle se capta la WIFI de casa, así que al salir recuperó la conexión (21:25). Estuvo un ratillo fuera, volvió a subirse al ascensor a las 21:30 y a las 21:31 ya estaba en casa de vuelta.

Al día siguiente se fue al colegio a las 07:50 y regresó a las 17:28. Un día largo. Dejó las cosas del cole y volvió a salir a las 17:42.

El porqué de todo esto

Realmente yo hablaría con el chaval y le instalaría de forma consensuada un monitor GPS en el móvil. Sería más exacto y más útil cuando está fuera de casa. Saber simplemente que su hijo no está en casa ayuda poco a una madre preocupada.

Por desgracia, en el caso que nos ocupa la persona afectada se oponía completamente a la idea de compartir su posición GPS en momentos inoportunos, el nivel técnico de la persona supervisora era inferior al de la persona supervisada e instalar software de seguimiento sin el consentimiento del primero tiene barreras legales y éticas que preferí no cruzar.

El sistema actual es muy incompleto, pero el chico es casero (de momento) y la madre soltera está más tranquila en la oficina sabiendo que su hijo está a salvo en casa.

Por supuesto, la efectividad de esta monitorización, ya de por sí regulera, se va al cuerno si el chico sabe qué está ocurriendo. Dejar el teléfono en casa o apagarlo, aún yendo contra el ADN de un adolescente de hoy en día, es siempre una opción si el premio es lo bastante jugoso.

Otros usos de esta tecnología

Por supuesto, hay otros usos de esta tecnología:

  1. Comprobar si aparecen direcciones MAC desconocidas en nuestra red, síntoma de que algún vecino se nos está colando.
  2. Activar sensores y alarmas automáticamente cuando no estamos en casa.
  3. Recibir avisos si ciertos dispositivos IoT están fallando o se han quedado sin baterías.
  4. Saber con unos segundos de antelación que cierta persona está llegando a casa para escondernos detrás de la puerta y darle un susto de muerte. Comprobado, funciona :-).
  5. Si en vez de monitorizar la tabla ARP de nuestra red WIFI controlamos las direcciones MAC que nos llegan por radio (poniendo la interfaz de red en modo promiscuo), podemos realizar tareas similares con nuestros vecinos y también detectar la aparición de dispositivos móviles desconocidos en las inmediaciones.

Actualización 20180927: Tengo una actualización del código en Detección de presencia por ARP, o cómo saber si tu hijo está en casa (II): Ahora con AsyncIO.