lucidiot's cybrecluster

Identifier les constructeurs des routeurs Wi-Fi

Lucidiot Informatique 2021-08-01
Une tentative d'étude statistique basée sur notre warwalking…


Parmi les choses un peu plus étranges qu'on peut faire avec nos données de scans Wi-Fi, c'est essayer d'identifier les fabricants des routeurs qui fournissent chaque réseau Wi-Fi. Cela peut nous permettre d'avoir une idée de qui est le fournisseur d'accès majoritaire, ou juste en savoir plus sur les "objets connectés" qu'on observe de plus en plus, tels que des robots aspirateurs, des purificateurs d'air, des imprimantes, des Chromecast, etc., souvent ouverts, sans aucun mot de passe.

Pour identifier qui émet un réseau Wi-Fi, on peut essayer d'utiliser le BSSID du réseau, qui n'est autre qu'une adresse MAC.

Les adresses MAC

Les adresses MAC, ou adresses Media Access Control, sont des identifiants uniques associés à des cartes réseau Ethernet, Wi-Fi, Bluetooth, etc. ; en fait, quasiment tous les standards de communication IEEE 802. Elles font 48 bits, soit 6 octets, et sont représentées sous une forme hexadécimale, souvent en séparant chaque octet par des deux-points ou par des tirets : 01:23:45:67:89:AB. Cette numérotation provient d'une suite de protocoles réseau définis par Xerox, et qui ont donné lieu au modèle OSI, qui n'a eu que très peu d'influence sur TCP/IP et ne devrait pas être enseigné dans les cours de réseau de nos jours.

Une adresse MAC débute la plupart du temps par un OUI, ou Organizationally Unique Identifier, un identifiant unique attribué par l'IEEE à quiconque accepte de leur faire un chèque. L'IEEE maintient plusieurs registres pour plusieurs types de ces identifiants, et les registres ont évolué au fil du temps ; on peut aujourd'hui s'attendre à rencontrer ceux-là :

MA-L
MAC Address Block-Large, le tout premier registre, parfois appelé directement OUI. On y attribue des blocs de 16 777 216 adresses en donnant des préfixes à 24 bits.
MA-M
MAC Address Block-Medium ou OUI-28, où on attribue des blocs de 1 048 576 adresses avec des préfixes à 28 bits (par exemple de B0:C5:CA:F0:00:00 à B0:C5:CA:FF:FF:FF). Un ou plusieurs blocs peuvent être assignés dans MA-L à l'IEEE lui-même pour qu'il puisse assigner des blocs MA-M.
MA-S
Mac Address Block-Small ou OUI-36, qui permet 4096 adresses avec des préfixes à 36 bits (par exemple de 8C:1F:64:4D:B0:00 à 8C:1F:64:4D:BF:FF). Un ou plusieurs blocs peuvent être assignés dans MA-L à l'IEEE lui-même pour qu'il puisse assigner des blocs MA-S.
IAB
Individual Address Block, un registre désormais fermé qui utilisait lui aussi des préfixes à 36 bits. Les préfixes restent valides malgré la fermeture et n'ont pas de collisions avec MA-S.
CID
Dans les adresses MAC, le second bit le moins significatif du premier octet indique si on parle d'un identifiant globalement unique d'organisation ou un identifiant local. Autrement dit, 00:00:02 est un identifiant local non géré par l'IEEE et qu'on peut utiliser comme on veut, et 00:00:00 est un identifiant global réservé et assigné par l'IEEE contre un chèque. C'est un peu comme les blocs d'adresses locales dans IP (10/8, 172.16/12, 192.168/16). Le registre Company ID, maintenant fermé, permettait de maintenir une liste de ces identifiants-là.

Il est possible avec beaucoup de matériels réseau de modifier l'adresse MAC à tout moment, ce qui peut permettre de se faire passer pour quelqu'un d'autre à tout moment ou de masquer un peu son identité. Il n'est donc pas certain qu'on puisse identifier l'intégralité de nos réseaux, mais sachant qu'en France la majorité d'entre nous utilisera des "box" (connues sous le nom de STB pour les anglophones et qui combinent ensemble un routeur, un modem, un switch, etc.) qui ne laissent généralement pas le choix de l'adresse MAC, ou des objets connectés sur lesquels on a encore moins de contrôle, on peut espérer en identifier une bonne partie.

Importer les organisations connues

On pourrait essayer de traiter les divers fichiers TXT et CSV fournis par l'IEEE pour importer tous les fabricants connus, mais pourquoi faire ça quand il y a des gens qui fournissent un format un peu plus simple où tout est dans un seul fichier ? Le service macaddress.io permet de télécharger toute leur base de données au format CSV, XML ou JSON. On peut importer directement tout le CSV dans une table :

CREATE TYPE mac_assignment_block_size AS ENUM (
  'CID',
  'IAB',
  'MA-L',
  'MA-M',
  'MA-S'
);

CREATE TABLE mac_vendor (
  oui VARCHAR(17) NOT NULL PRIMARY KEY,
  private BOOLEAN NOT NULL,
  name VARCHAR,
  address TEXT,
  country_code VARCHAR(2),
  assignment_block_size mac_assignment_block_size NOT NULL,
  created DATE NOT NULL,
  updated DATE NOT NULL
);

Ensuite, on peut utiliser psql pour importer directement depuis l'URL :

curl -s --fail https://macaddress.io/database/macaddress.io-db.csv |
# Remove the first line with column names
tail -n+2 |
psql -c '\copy mac_vendor FROM STDIN WITH (FORMAT CSV)'

Identifier les points d'accès

Dans le fichier CSV qui nous est fourni, on n'a que la portion de l'adresse MAC qui a été réservée ; par exemple, AA:BB:CC signifie que l'organisation dispose de toutes les adresses de AA:BB:CC:00:00:00 jusque AA:BB:CC:FF:FF:FF. La façon bête et méchante que j'ai au départ trouvé pour trouver les réseaux est donc d'utiliser une comparaison de chaîne de caractères :

CREATE VIEW wifi_vendors AS
SELECT name, COUNT(*)
FROM accesspoint
INNER JOIN mac_vendor ON LOWER(bssid::text) LIKE LOWER(oui) || '%'
GROUP BY name
ORDER BY COUNT(*) DESC;

Le bssid d'un point d'accès étant de type macaddr, je le convertis d'abord en text pour obtenir une représentation sous la forme 01:23:45:67:89:ab. Pour être sûr qu'il n'y ait pas de soucis entre ab et AB, j'utilise LOWER pour tout mettre en minuscules. L'opérateur LIKE avec le % à la fin permet d'indiquer qu'on cherche, pour chaque BSSID de point d'accès, tous les blocs d'adresses MAC qui représentent le début du BSSID.

Cette requête prend plusieurs minutes à s'exécuter, donc j'en avais fait une vue matérialisée qu'on mettait à jour après chaque nouvel import de données suite à une nouvelle balade. PostgreSQL ignore complètement mes tentatives d'ajouter des index sur les tables pour accélérer cette requête ; ça passe toujours par deux scans séquentiels, c'est-à-dire des lectures complètes des deux tables (36 000 lignes de points d'accès et 43 000 lignes de blocs d'adresses), puis une jointure à base de boucles imbriquées, soit plus d'un milliard et demi de comparaisons de chaînes de caractères. C'est loin d'être optimal.

Mais ce n'était pas le seul problème de cette requête ; on n'arrivait à identifier que moins de 10% des réseaux. Ça m'étonnait beaucoup, sachant que WiFiAnalyzer parvenait à identifier la majorité des réseaux qu'il trouvait quand je l'utilisais pendant le warwalking. Il identifiait notamment tous les réseaux d'Orange, Free, ou SFR, ce qui constitue déjà une bonne majorité de nos points d'accès. J'avais en tête une solution à tester, mais jouer avec les adresses MAC n'a pas été notre priorité pendant un bon moment puisqu'on en était encore aux balbutiements du projet et qu'on peaufinait encore notre collecte de données.

Optimiser avec des intervalles

J'avais utilisé le type macaddr pour le BSSID des points d'accès parce que c'est un type optimisé par Postgres et qui stocke véritablement seulement 48 bits en base de données, et pas une représentation textuelle de 138 bits. C'est assez dommage de ne pas profiter de ça dans cette table des blocs d'adresses ! Mais il nous faudrait alors pouvoir indiquer deux adresses : celle du début et de la fin du bloc. On se retrouverait alors avec quelque chose comme WHERE lower_mac <= bssid AND bssid <= upper_mac. Bien que j'aurais pu directement implémenter ça, PostgreSQL fournit aussi une façon plus optimisée de gérer ce genre d'intervalles, en déclarant directement un intervalle.

Les types d'intervalle permettent d'avoir des intervalles avec bords exclusifs ou inclusifs, d'avoir des intervalles qui vont vers l'infini, etc., mais surtout permettent d'optimiser beaucoup d'opérations, notamment avec un index GiST. Il existe des intervalles prédéfinis comme int4range, mais grâce à [btree_gist][btree_gist], l'extension que j'avais mentionné précédemment pour nous permettre de faire correspondre les coordonnées GPS aux données Wi-Fi, on peut définir un nouveau type d'intervalle pour les adresses MAC :

CREATE TYPE macaddrrange AS RANGE (SUBTYPE = MACADDR);

On peut ensuite modifier notre table existante pour que oui ne soit plus un VARCHAR mais un intervalle :

DROP VIEW IF EXISTS wifi_vendors;

ALTER TABLE mac_vendor
ALTER COLUMN oui
TYPE macaddrrange
USING macaddrrange(
  RPAD(REPLACE(oui, ':', ''), 12, '0')::macaddr,
  RPAD(REPLACE(oui, ':', ''), 12, 'F')::macaddr,
  '[]'
);

On utilise USING pour expliquer à PostgreSQL comment changer de type depuis la valeur originale. Cela nous permet d'éviter d'ajouter une colonne temporaire supplémentaire puis de faire un échange. On utilise la fonction macaddrrange(lower, upper, bounds) qui permet d'indiquer les bornes inférieures et supérieures, et si les bornes sont inclusives [ ou exclusives ( ; ici elles sont toutes les deux inclusives donc on a []. On construit les bornes inférieures depuis notre chaîne de caractères comme ceci :

  1. On part de la chaîne de caractères, par exemple B0:C5:CA:F
  2. On retire tous les deux-points avec REPLACE : B0C5CAF
  3. On complète avec des zéros pour obtenir une adresse MAC valide, soit 12 chiffres hexadécimaux, avec RPAD : B0C5CAF00000
  4. On convertit explicitement en une adresse MAC avec ::macaddr ; PostgreSQL va donc interpréter les chiffres hexadécimaux en une adresse MAC valide
  5. On répète ça avec des F à la place des 0 pour la borne supérieure.

On obtient ainsi des intervalles décrivant vraiment les blocs, par exemple [B0C5CAF00000, B0C5CAFFFFFF], et on peut utiliser des opérateurs et fonctions d'intervalles comme l'opérateur @>, qui permet de tester si un intervalle contient une valeur :

CREATE VIEW wifi_vendors AS
SELECT name, COUNT(*)
FROM accesspoint
INNER JOIN mac_vendor ON oui @> bssid
GROUP BY name
ORDER BY COUNT(*) DESC;

Enfin, on peut optimiser encore un peu cette requête en ajoutant un index GiST. GiST est conçu pour les données géographiques ou sous forme de tableaux ou d'intervalles, et c'est une situation idéale pour l'utiliser :

CREATE INDEX mac_vendor_oui ON mac_vendor USING GIST (oui);

Avec tout ça, on a maintenant une vue qui s'exécute en moins de deux secondes, et nous renvoie des identifications pour plus de 80% des points d'accès connus.

Dédupliquer les organisations connues

J'ai fait quelques requêtes supplémentaires pour vérifier s'il n'y avait pas d'erreurs avec ces statistiques, et je me suis rendu compte d'un problème :

SELECT accesspoint.*, COUNT(DISTINCT oui)
FROM accesspoint
INNER JOIN mac_vendor ON oui @> bssid
GROUP BY bssid
HAVING COUNT(DISTINCT oui) > 1;

Cette requête me permet de savoir s'il y a des réseaux qui semblent appartenir à plusieurs blocs en même temps, ce qui n'est pas vraiment censé arriver puisque ça voudrait dire qu'il y a plusieurs fabricants en même temps. Et j'ai eu une dizaine de résultats où j'avais deux fabricants ! Regardons ça de plus près…

WITH matched AS (
  SELECT *
  FROM accesspoint
  INNER JOIN mac_vendor ON oui @> bssid
)
SELECT bssid, ssid, oui, name, assignment_block_size
FROM matched
WHERE bssid IN (
  SELECT bssid
  FROM matched
  GROUP BY bssid
  HAVING COUNT(DISTINCT oui) > 1
);

Cette requête est un peu plus compliquée, mais elle me permet de voir tous les fabricants associés à tous les réseaux qui ont des fabricants en double. Je me rends compte alors que pour chaque réseau, il y a en fait un bloc MA-M et un bloc MA-L. Le bloc MA-L est associé à l'IEEE lui-même, et le bloc MA-M est associé au véritable fabricant. C'est logique, vu que l'IEEE a décidé d'assigner ses propres blocs depuis MA-L à MA-M.

oui name assignment_block_size
[c0:d3:91:00:00:00,c0:d3:91:ff:ff:ff] Ieee Registration Authority MA-L
[c0:d3:91:e0:00:00,c0:d3:91:ef:ff:ff] Samsara Networks Inc MA-M
[14:1f:ba:00:00:00,14:1f:ba:ff:ff:ff] Ieee Registration Authority MA-L
[14:1f:ba:70:00:00,14:1f:ba:7f:ff:ff] Wisnetworks Tech Co, Ltd MA-M

Comme je ne m'intéresse pas vraiment au fonctionnement un peu "interne" de l'assignation de ces blocs, je vais juste enlever les blocs d'adresses qui contiennent eux-mêmes d'autres blocs d'adresses :

DELETE FROM mac_vendor
USING mac_vendor inner_vendor
WHERE mac_vendor.oui @> inner_vendor.oui
AND mac_vendor.oui != inner_vendor.oui;

Notez le mac_vendor.oui != inner_vendor.oui ; l'opérateur @> considère qu'un intervalle s'inclut très bien lui-même, mais tous les blocs se contiendraient du coup eux-mêmes et je n'ai pas envie de supprimer tous les blocs.

Enfin, je peux utiliser une contrainte d'exclusion pour m'assurer que plus jamais je n'aurai ce problème : je peux interdire les intersections entre tous les blocs en utilisant l'opérateur d'intersection d'intervalles && :

DROP INDEX mac_vendor_oui;

ALTER TABLE mac_vendor
ADD CONSTRAINT distinct_ranges
EXCLUDE USING GIST (oui WITH &&);

Notez qu'on supprime l'index GiST créé précédemment, car cette contrainte d'exclusion va elle-même créer son propre index GiST. Ce sera exactement le même que le nôtre, et ça pourra aussi bien être utilisé par PostgreSQL dans les requêtes.

Avec ces changements, il n'y a d'un seul coup plus du tout de duplications dans nos requêtes.

Le biais des hotspots

On peut assez rapidement agrémenter notre vue avec un affichage en pourcentage :

SELECT name, ROUND(count::numeric / (SELECT COUNT(*) FROM accesspoint) * 100, 1) AS percent
FROM wifi_vendors;

Voici donc les trois vainqueurs :

name percent
Freebox Sas 24.6
Sagemcom Broadband Sas 21.3
Sfr 4.6

Sagemcom correspond principalement aux Livebox ; c'est le fabricant de la quasi-totalité des modèles de Livebox, mis à part la Livebox Inventel qu'on ne voit quasiment plus. Ça m'étonne que Free, Orange et SFR soient autant présents !

En y réfléchissant un peu, je me suis rendu compte que c'est à cause d'un des prérequis des BSSID : ils doivent être uniques dans une zone donnée pour éviter des collisions avec des réseaux alentours. Ça compte aussi pour deux points d'accès distincts qui seraient fournis par le même matériel physique. Autrement dit, si une Freebox fournit à la fois le réseau de son propriétaire ainsi que les hotspots FreeWifi et FreeWifi_secure, il y a 3 réseaux pour la même box !

On peut minimiser ce biais (mais pas forcément totalement l'éliminer) en utilisant une table toute simple que nous avons ajouté à notre base de données à la main. Les hotspots nous ont déjà posé problème par le passé, notamment en polluant beaucoup les cartes, donc nous avons créé une table blocklist et nous l'utilisons pour filtrer nos requêtes :

CREATE TABLE blocklist (ssid VARCHAR PRIMARY KEY);

INSERT INTO blocklist VALUES
  ('FreeWifi'),
  ('FreeWifi_secure'),
  ('SFR WiFi FON'),
  ('SFR WiFi Mobile'),
  ('orange'),
  ('FlixBus Wifi'),
  ('WiFi BlaBlaBus'),
  ('eduroam');

Cette liste noire inclut des réseaux Wi-Fi fournis par des cars, parce qu'on les croise parfois durant nos promenades et qu'ils ne sont pas vraiment localisables sur une carte vu qu'ils bougent beaucoup, ainsi que eduroam, le réseau des universités européennes, qui apparaît comme un réseau ouvert qu'on pourrait ajouter sur OpenStreetMap alors qu'il est considéré comme privé selon les règles d'OpenStreetMap.

On peut utiliser cette liste assez facilement dans notre vue :

CREATE OR REPLACE VIEW wifi_vendors AS
SELECT name, COUNT(*)
FROM accesspoint
INNER JOIN mac_vendor ON oui @> bssid
WHERE ssid NOT IN (SELECT ssid FROM blocklist)
GROUP BY name
ORDER BY COUNT(*) DESC;

On peut ensuite relancer nos statistiques, et voici les véritables parts de marché sur les ondes Wi-Fi des trois FAI les plus courants :

name percent
Sagemcom Broadband Sas 20.3
Freebox Sas 13.6
Sfr 4.5

Orange représente 20% des réseaux qu'on a pu observer, même s'il est probablement le FAI le plus cher de France, et ça pourrait s'expliquer par le fait qu'Orange aie dans beaucoup d'endroits l'exclusivité pour la fibre. On remarquera aussi que Bouygues est totalement absent de ces statistiques ; il semblerait que ce soit dû au fait que ses box n'utilisent pas d'adresses avec des OUI enregistrés à l'IEEE, mais uniquement des addresses avec des identifiants locaux, donc elles font toutes partie des 15 à 20% de réseaux non identifiés.

On peut donc déduire de cette expérience que tenter d'identifier le fournisseur d'accès à partir de l'identifiant d'un réseau est à peu près aussi fiable que de juste utiliser le nom du réseau, quand leur propriétaire n'essaie même pas de modifier celui par défaut (comme NUMERICABLE-255B) ou quand des hotspots existent (par exemple FreeWifi). Mais ça reste un essai intéressant et instructif au moins dans les fonctionnalités de PostgreSQL.


Commentaires

Il n'y a pour l'instant aucun commentaire. Soyez le premier !