La Lanterne Rouge

Warning: Geek Inside

NeufboxWatcher : contrôlez votre box avec PHP

- Posted in Sans catégorie by

Tout d'abord comme c'est la coutume, je vous souhaite une bonne année 2012. Que tous vos voeux se réalisent, que la crise et l'HADOPI vous épargnent, et que le voisin qui vous fait profiter de la mélodie de son aspirateur tous les dimanches à 8h fasse un cancer des ongles (ou déménage, c'est vous qui voyez).

C'est le bel ennui de Noël...

Moi à Noël, pendant que toute la famille fait la sieste règlementaire, je m'ennuie un peu. Alors je cherche des occupations geekiennes. Et des fois, j'en trouve.

Cette fois-ci tout est parti d'un problème de déconnexions intempestives de la box de mon hébergeur(1). En effet depuis quelques semaines l'ADSL perd la synchro très régulièrement (parfois toutes les 10-20 minutes) et la box – pour une raison que j'ignore, l'âge ou un bug du firmware peut-être – n'arrive pas toujours à se reconnecter seule et nécessite un reboot manuel (soit en utilisant la prise d'alimentation, soit depuis l'interface d'administration).

neufbox4-sfr.jpg

Évidemment, si pas d'ADSL, pas moyen d'accéder au serveur derrière pour contrôler la box via le VPN. L'idée est donc de mettre en place un script automatisé de surveillance sur le serveur qui va régulièrement tester l'état de connexion de la box et la rebooter si nécessaire via le LAN.

La question qui se pose alors est : comment contrôler la box via un script ?

Analyse et exploitation de l'interface web de la box

Il n'y a, à ma connaissance, qu'un seul canal de contrôle sur l'appareil qui soit ouvert aux pauvres utilisateurs/clients/pigeons que nous sommes : la fameuse interface d'administration accessible via un navigateur à l'adresse http://192.168.1.1/.

Bien que prévue à la base pour être manipulable par souris/clavier, elle n'en reste pas moins un enchaînement de pages récupérées grâce à quelques requêtes GET et POST. On peut donc considérer ça comme un webservice dont la structure des message serait juste un peu "polluée".

La première étape a alors été de faire un peu de rétro-ingénierie sur cette interface au moyen de Firefox et l'indispensable Firebug.

neufbox_firebug2.png

Bons points : les requêtes sont simples, la structure des pages très propre (ce n'est pas du Valid XHTML 1.0 Strict mais on pardonnera) et j'entrevois déjà comment factoriser le parsing de nombreux éléments tels que les indicateurs de statut et les tableaux HTML.

Mais avant d'accéder à ces pages, il faut pouvoir être authentifié. Je regarde donc le fonctionnement du formulaire de login et suis curieux d'y voir un fonctionnement plus complexe que je ne l'aurais pensé de prime abord.

Phase 1 : l'authentification

L'interface étant acccessible via une simple connexion HTTP (donc non chiffrée), les informations saisies dans les formulaires (et plus généralements, échangées entre la box et le client) circulent en clair.

Si la liaison est filaire, pas d'inquiétude à avoir (à moins d'être parano). Mais la plupart des gens préfèrent désormais le Wifi pour ne pas s'encombrer de fils inesthétiques quand ils vont "surfer sur Internet" (sic). Lorsqu'on connait la résistance des systèmes de "sécurité" du Wifi(2), on se rend bien compte qu'une connexion non chiffrée représente un risque non négligeable de donner les accès de sa box au premier voisin venu testant son Backtrack.

L'authentification sur la box via le formulaire est donc fort justement protégée grâce à l'utilisation d'un HMAC (Keyed-Hash Message Authentication Code : code d'authentification d'une empreinte cryptographique de message avec clé) basé sur SHA-256. Le protocole est le suivant et se découvre en analysant le Javascript exécuté lors de la validation du formulaire et les requêtes qu'il effectue vers le serveur :

1. Lors de la validation du formulaire (par clic sur le bouton ou appui sur Entrée), le Javascript intercepte l'action (empêche l'envoi de la requête POST "normale") et envoie à la place une requête contenant uniquement le champ POST suivant :

action=challenge

vers le chemin /login et avec les entêtes suivants :

X-Requested-With: XMLHttpRequest
X-Requested-Handler: ajax

2. Le serveur renvoi alors la structure XML suivante :

<rsp stat="ok">
<challenge>25d6cbfb759fde5cd02d4d144952deee6e7da89a</challenge>
</rsp>

Le champ "challenge" est généré (pseudo-)aléatoirement par le serveur et servira par la suite d'identifiant de session.

3. Lors de la réception de la réponse, la fonction Javascript récupère cette structure, vérifie qu'elle ne contient pas d'erreur, puis génère la chaîne pour authentification selon la formule suivante (extraite du script) :

HMAC_SHA256_MAC(challenge, SHA256_hash(login))
+ HMAC_SHA256_MAC(challenge, SHA256_hash(password))

Le résultat est donc la concaténation des HMAC des hashs obtenus avec SHA-256 du login et du mot de passe. On remarque que c'est l'identifiant de session (nommé ici "challenge") qui sert de clé "secrète" pour le HMAC (pas si secrète que ça puisqu'elle sera passée en tant que donnée POST par la suite, mais ce n'est pas important ici).

4. Le script remplit alors respectivement les champs cachés "zsid" et "hash" avec la valeur préalablement appelée "challenge" et le résultat de la concaténation des hashs précédente. Il efface ensuite les champs "login" et "password" (qui sinon seraient transmis par POST en clair) et soumet finalement le formulaire. Un cookie "zsid" contenant là encore l'identifiant de session est envoyé avec la requête POST ainsi créée.

Pour savoir si le login a réussi, il suffit de tester le code HTTP retourné :

  • avec 200, on peut garder l'identifiant de session pour le faire passer en cookie (sous le nom "sid" cette fois-ci) pour les prochaines requêtes,
  • tout autre code doit être considéré comme une erreur (jusque là, rien d'exceptionnel !).

(Mise à jour 2012-02-11 : En fait c'est en testant l'entête Set-Cookie que nous savons si l'authentification a réussi, car le serveur peut renvoyer une redirection en 302 en cas de succès mais aussi d'erreur. Voir version 0.1.1 ici)

Phase 2 : l'extraction des informations des pages

Dès que nous avons notre cookie valide, on peut accéder à n'importe quelle page de l'interface via de simples requêtes GET. Les chemins sont particulièrement simples et propres, on retrouve par exemple :

  • "/state" pour la page des statuts généraux (indicateurs de connexion IPv4, IPv6, téléphonie et TV)
  • "/state/wan" pour la page des détails de la connexion ADSL
  • "/state/voip" pour la page des détails de la téléphonie, avec l'historique des appels si activé
  • "/network" pour la page des statut du LAN
  • "/network/nat" pour la liste des règles de routage
  • etc.

Comme écrit plus haut, bien que les sources ne passent pas un test de validation XHTML 1.0, tous les élements de l'interface sont très bien organisés et chacun ou presque possède un attribut "id" facilement utilisable. Il suffit donc ensuite d'une requête XPath bien préparée pour récupérer très exactement les noeuds et leurs informations.

Pour récupérer les informations de connexion IPv4 contenues dans un tableau, on utilisera :

//table[@id="wan_info"]

pour accéder à cet élément et on itèrera simplement sur les noeuds enfants.

icon_disabled.png icon_enabled.png icon unused

J'en ai rapidement profité pour factoriser dans une fonction la "traduction" des indicateurs de statut (les feux rouges/jaunes/verts visibles sur différentes pages de l'interface) en constantes numériques plus faciles à réutiliser par un script.

neufbox_wan_info.png

De même pour les tableaux à deux colonnes, une fonction se charge de retourner toutes les valeurs textuelles trouvées dans un array PHP propre. Pour les tableaux à double entrée, une fonction récupère tout d'abord les entêtes puis remplit là encore un array PHP avec les valeurs de chaque cellule de chaque ligne en utilisant le titre de la colonne comme clé.

Une fois ces quelques fonctions "outils" achevées, il ne reste plus qu'à les appeler avec les bons paramètres (URL et XPath) pour récupérer très simplement la quasi-totalité des informations présentées dans l'interface d'administration.

La classe Neufbox4

Afin de favoriser la réutilisation, la maintenance et l'extension des fonctionnalités, j'ai regroupé toute la gestion de la communication avec la Neufbox dans une classe PHP (nommée Neufbox4, car elle ne s'applique qu'à la version 4 et non aux modèles Trio3C/D ou Evolution).

Un exemple d'utilisation se résume à l'appel des méthodes suivantes (ici, pour récupérer les informations de connexion ADSL) :

$neufbox = new Neufbox4('192.168.1.1');
$neufbox->login('admin', 'lemotdepassecompliquearetenirmarqueaudosdelabox');
print_r($neufbox->getIpv4ConnectionInfo());

La liste des méthodes accessibles par la classe Neufbox4 (à la version 0.1.0) est la suivante :

public function login($login, $password)
public function logout()
public function getHost()
public function getIpv4Status()
public function getIpv6Status()
public function getPhoneStatus()
public function getTelevisionStatus()
public function getWifiStatus()
public function getModemInfo()
public function getIpv4ConnectionInfo()
public function getIpv6ConnectionInfo()
public function getAdslInfo()
public function getPppInfo()
public function getConnectedHosts()
public function getPortsInfo(
public function getWifiInfo()
public function getNatConfig()
public function getPhoneCallHistory()
public function reboot()
public function getFullReport()

La dernière de la liste réalise en fait un appel à tous getters de la classe et retourne la totalité des informations dans un tableau.

Les fonctions suffixées par "Status" retournent toujours un entier à comparer avec les constantes STATUS_* définies dans la classe.

Les fonctions suffixées par "Info" retournent toujours un tableau d'informations textuelles.

Pour les autres, il faut vérifier dans le docblock d'entête.

La classe NeufboxWatcher

Comme mon besoin final ici n'était pas de créer simplement une API pour accéder à une Neufbox mais bien de vérifier son état et de prendre les mesures nécessaires pour assurer la continuité du service (par la force s'il le faut !), j'ai créé une seconde classe chargée de ce rôle. Elle utilise une instance de Neufbox4 pour fonctionner, qui est passée en paramètre au constructeur.

Les méthodes disponibles sur cette seconde classe sont les suivantes :

public function setRebootWaitDelay($sec)
public function checkAdslAndReboot()
public function checkAdslAndRebootOnMultipleFail($failCountBeforeReboot = self::DEFAULT_FAIL_COUNT_BEFORE_REBOOT)

La dernière méthode utilise un fichier texte pour conserver le compte des constats d'échecs (connexion ADSL absente au moment de la vérification) et lance uniquement un ordre de reboot si le compteur atteint le nombre passé en paramètre (3 par défaut). La seconde n'attend pas cette limite et tente de rebooter à la première défaillance constatée.

Le script de reboot automatique : 9boxwatcher.php

Dans ma configuration, un job CRON est exécuté toutes les heures pour tester l'état de la connexion ADSL (le statut plus exactement : connecté ou non) et lancer un reboot si l'ADSL est down. J'ai donc adjoint à mes deux classes un petit script destiné à être exécuté en ligne de commande et qui prend en paramètre de simples options pour faciliter le log des exécutions.

La commande finale ressemble à :

php -f /var/9boxwatcher/9boxwatcher.php -- --action checkandreboot --silent-success

Mon script est placé dans le dossier /var/9boxwatcher avec les deux classes PHP.

L'option "--action" admet les valeurs suivantes :

  • checkandreboot : Vérifie l'état de la connexion et reboote si nécessaire,
  • reboot : Reboote directement la box
  • fullreport : Affiche le résultat de l'appel à la méthode Neufbox4::getFullReport()

L'option "--silent-success" joue simplement avec le niveau de log pour cacher les messages dont la criticité est inférieure à WARNING, ce qui évite d'avoir un retour dans la console si la connexion ADSL est opérationnelle et par conséquent un mail toutes les heures avec les retours du script. De cette manière, seules les exécutions entraînant un ordre de reboot se retrouvent dans ma boîte mail.

Téléchargements

Mon SVN personnel étant – comme son nom l'indique – personnel, et son accès limité, je vous propose à la place de télécharger les sources à l'adresse suivante :

http://nanawel.free.fr/dev/9boxwatcher/9boxwatcher-0.1.1.zip

J'ai également posté la version 0.1.0 de la classe Neufbox4 sur pastebin.com :

http://pastebin.com/UJyz9m7k

MISE À JOUR 18/07/2013 : J'ai créé un dépôt GitHub

https://github.com/nanawel/9boxwatcher

Si ces fichiers vous sont d'une quelconque utilité, n'hésitez pas à les compléter, à les modifier et à les redistribuer.

Un petit rappel sur la configuration nécessaire pour faire tourner le script :

  • PHP > 5.1.2
  • Extension PHP cURL
  • Extension PHP DOM
  • Extension PHP SimpleXML

Et la suite ?

Pour le moment, seule une poignée de méthodes est disponible sur la classe Neufbox4. La seule effectuant une action est reboot(), les autres étant de simples accesseurs. Mais le plus dur est fait : réaliser l'authentication et fournir une méthode permettant de faire abstraction des paramètres cURL à passer à chaque fois.

J'envisage d'ajouter d'autres méthodes, notamment pour contrôler l'état du Wifi, voire changer le SSID ou d'autres paramètres, et éventuellement gérer les règles de NAT à la volée.

Pour le moment, le challenge passé, mon intérêt est évidemment un peu retombé. Mais qui sait, je pourrais m'y repencher dessus très prochainement.

(1) Voir .

(2) La dernière preuve date d'il y a à peine quelques jours (et son exploit est ici).

Fork me on GitHub