FreshRSS : Retrouver une interface épurée grâce au MutationObserver

Étant en train de faire de nombreux changements dans mon homelab, tant du côté de l’infrastructure que des outils, en passant par la sauvegarde. Je m’attarde aujourd’hui sur un point annexe rencontré lors de la migration de mon instance FreshRSS.

Bien occupé par d’autres préoccupations ces dernières années, la mise à jour de certains services commençaient à prendre du retard. En migrant FreshRSS, j’en ai profité pour mettre l’outil à jour et partir sur la dernière version en date. Rien à redire, l’outil fonctionne toujours aussi bien, fait ce qu’on lui demande et réponds à mes besoins pour le suivi de flux RSS. Par contre, un petit changement dans l’UX/UI m’a très vite contrarié.

Désormais, FreshRSS organise toutes les entrées par date et regroupe donc les flux par jour, avec un entête de séparation. Si ce changement ne se sent pas particulièrement en consultant le flux principal contenant les entrées de tous mes flux RSS, le résultat est plutôt perturbant lorsqu’on consulte le détail d’un flux RSS en particulier. La page devient alors une alternance entre entête de date et entrée de flux. La majeure partie des flux que je consulte publient très rarement plus d’une entrée par jour. Ma page se remplie d’entête de date qui constitue la moitié du contenu de la page et dont je n’ai pas l’utilité.

Bref, direction GitHub pour confirmer qu’un changement a eu lieu et savoir si un contournement ou une option de configuration existe. Bingo, deux issues existent:

Avant de passer à la solution que j’ai mise en place pour arriver à quelque chose de satisfaisant, voici la situation irritante en images.

Avant, un flux n’avait que trois entêtes: « Today », « Yesterday » et « Before yesterday ».

Et désormais, avec une entête par jour, on obtient quelque chose comme cela:

Quelle est donc la solution, me direz-vous ?

Les discussions sur GitHub proposent d’activer l’extension « User CSS » et d’ajouter la règle suivante:

 .transition {
    display: none;
 }

Ce qui a pour effet de faire disparaître toutes les entêtes. C’est mieux, mais insuffisant à mon goût. J’ai vraiment l’impression de perdre en lisibilité, car je ne peux plus distinguer les entrées récentes (les deux derniers jours) des entrées plus anciennes. En continuant d’expérimenter avec des règles CSS plus complexes pour tenter d’arriver à une présentation me convenant, j’aperçois un bouton permettant d’activer l’extension « User JS ». Ayant passé de nombreuses années à expérimenter et construire des solutions avec du JS, un début d’idée commence à germer dans mon esprit. Avec l’aide d’un MutationObserver pour intercepter les changements du DOM, je devrais être en mesure de détecter l’ajout des entêtes dans le DOM et de ne garder que les trois premières, retrouvant ainsi une présentation proche de la version historique à laquelle je suis habitué.

Après quelques itérations, voici donc le code que j’utilise:

const labels = ['Today', 'Yesterday', 'Before yesterday'];
let transitionCount = 0;

function processTransition(el) {
  const i = transitionCount++;
  if (i >= 3) {
    el.style.display = 'none';
  } else {
    const link = el.querySelector('.transition-value a:first-child');
    if (link) {
      link.textContent = labels[i];
    }
  }
}

const main = document.querySelector('main');
main.querySelectorAll('.transition').forEach(processTransition);

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.nodeType !== 1) {
        continue;
      }
      if (node.classList?.contains('transition')) {
        processTransition(node);
      } else {
        node.querySelectorAll?.('.transition').forEach(processTransition);
      }
    }
  }
});

observer.observe(main, { childList: true, subtree: true });

Ce qui nous donne le résultat suivant:

On constate immédiatement que l’information est bien plus condensée et qu’il y a, à mon goût, moins d’espace perdu. Je me rends compte en écrivant ces lignes, que les labels que j’utilise ne correspondent toutefois plus vraiment. En effet, dans mon exemple avec le flux xkcd, les deux entrées les plus récentes ont été reçues le lendemain de leur publication, mais à deux jours d’intervalles. Mon affichage « Aujourd’hui » (« Today ») et « Hier » (« Yesterday »), est donc inexacte, mais correspond en tout cas à un regroupement qui me convient. Après réflexion, je m’oriente donc vers un découpage « Latest », « Previous » et « Older ».

Pour avoir utilisé cette solution au quotidien depuis maintenant plusieurs semaines, je suis tout à fait satisfait du résultat. Bien sûr, il y a parfois quelques clignotements dans l’interface, le temps que le code s’exécute et que les entêtes soient cachées ou que leur titre change, mais rien qui ne soit gênant.

V6 s’invite au déménagement

Un déménagement. Une idée simple quand on y pense: il suffit de déplacer des choses d’un point A vers un point B. Dans les faits, des objets à ranger dans des cartons en vu du transport, des meubles à vider, à déplacer, de l’aide à trouver (un meuble même vide, pèse son poids), le tout à planifier et à ordonnancer pour terminer à temps. Et bien sûr, au milieu de tout ça, des ordinateurs, switchs et router, bref, l’infrastructure qui héberge mon lot de service à arrêter, déplacer et à reconnecter ailleurs, possiblement dans un nouveau réseau. Au moment où j’écris ces lignes, pas loin de 24 mois se sont écoulés depuis que j’ai eu à déplacer mon homelab.

Occupé par l’organisation de mon déménagement, je n’avais pas pris le temps d’anticiper le déplacement de mon infra, en la migrant temporairement dans une machine virtuelle chez un hébergeur. Bien qu’ayant automatisé la majeure partie du déploiement, je savais que cela nécessiterait plusieurs vérifications, notamment que les processus de sauvegarde se soient bien remis en place. Même chose pour le renouvellement des certificats, sans parler des adaptations nécessaires sur les scripts ansible, si j’en avais profité pour passer sur une version plus récente de Debian. Bref, pas suffisamment de temps à dédier au sujet. J’ai donc attendu jusqu’au dernier moment, avant d’arrêter mon homelab et de déplacer ses composants, avec bien entendu, un temps d’indisponibilité dans l’intervalle, jusqu’à reconnecter le tout dans le nouveau réseau.

La bonne nouvelle, c’est que tout a redémarré correctement, pas de casse pendant le déplacement, pas de stockage corrompu, tout a fonctionné à merveille. La mauvaise, c’est que le réseau d’arrivée comportait des nouveautés ! Le réseau que je quittais était entièrement sous mon contrôle. En effet, j’avais réussi à utiliser mon propre router à la place de la livebox du FAI et je disposais alors d’une IPv4 dédiée. Dynamique, mais relativement fixe; avec un dynamique DNS bien configuré pour pouvoir contacter mes services, même en cas de changement d’IP. En changeant de réseau, je me retrouvais à devoir exposer mon homelab derrière une box de FAI et, oh surprise, j’ai vu arriver de l’IPv6, et découvert que le point d’entrée dans le réseau, la box du FAI, ne disposait pas d’une adresse IPv4 pour son seul usage, mais d’une IPv4 partagée entre plusieurs box. Cette technique portant le nom de CGNAT pour Carrier-Grade Network Address Translation.

Impossible donc de contacter mes services en v4, j’ai adapté mes plans pour faire fonctionner le tout en v6. Après quelques essais, la connectivité vers mon homelab était rétablie. Tout aurait pu s’arrêter là si je n’avais pas constaté, quelques jours plus tard, que mes services étaient inaccessibles depuis un réseau que j’utilisais régulièrement durant cette période. La raison la plus probable: pas d’IPv6 sur ce réseau et aucun mécanisme de compatibilité entre v6 et v4. Mes cours de réseaux sur IPv6 semblaient bien loin. Ni une, ni deux, j’ai donc enfourché mon renard de feu à la recherche de solutions à mon problème.

Après plusieurs lectures pour comprendre ce fameux CGNAT, je suis arrivé à la conclusion qu’il allait me falloir un point relais capable de recevoir du trafic en IPv4 et de le transférer vers mon réseau via IPv6. Je me suis alors souvenu que Google, dans sa plateforme cloud, propose des network load balancers intervenant au niveau de la couche TCP, soit bien plus bas que le proxy dont je me sers dans mon infrastructure pour faire de la terminaison SSL sur du trafic HTTPS. En continuant mes recherches autour de la notion de proxy passthrough, j’ai découvert que je pouvais configurer un serveur HAProxy, de telle sorte qu’il puisse recevoir du trafic HTTPS en IPv4 et IPv6 et le rediriger vers une adresse IPv6, sans que celui-ci ait besoin de toucher à la couche HTTPS, et donc sans avoir à reconfigurer les certificats de mes noms de domaines au niveau de ce proxy. Une fois connu, cela semble évident.

Au final, la solution aura été relativement simple et rapide à mettre en place. Commander un petit VPS disposant d’une IPv4, modifier les DNS pour faire pointer les noms de domaines vers ce serveur pour le trafic en v4, installer HAProxy et le configurer en mode proxy passthrough. Pour le trafic en v6, on peut au choix utiliser le même proxy, ou déclarer directement l’IPv6 du serveur cible dans les DNS. Cette solution m’aura donc permis de rétablir rapidement l’accès à mes services quel que soit le réseau (v4 ou v6), et surtout, avec pratiquement pas de modification du côté de mon infrastructure auto-hébergé.

Pour terminer, voici la configuration utilisée au niveau de HAProxy (/etc/haproxy/haproxy.cfg) pour mettre en œuvre la solution trouvée:

global
	log /dev/log	local0
	log /dev/log	local1 notice
	chroot /var/lib/haproxy
	stats socket /run/haproxy/admin.sock mode 660 level admin
	stats timeout 30s
	user haproxy
	group haproxy
	daemon

	# Default SSL material locations
	ca-base /etc/ssl/certs
	crt-base /etc/ssl/private

	# See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate
	ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
	ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
	ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets

defaults
	log	global
	mode	http
	option	httplog
	option	dontlognull
	timeout connect 5000
	timeout client  50000
	timeout server  50000
	errorfile 400 /etc/haproxy/errors/400.http
	errorfile 403 /etc/haproxy/errors/403.http
	errorfile 408 /etc/haproxy/errors/408.http
	errorfile 500 /etc/haproxy/errors/500.http
	errorfile 502 /etc/haproxy/errors/502.http
	errorfile 503 /etc/haproxy/errors/503.http
	errorfile 504 /etc/haproxy/errors/504.http

frontend www_https
	bind *:443 v4v6
	mode tcp
	option tcplog
	default_backend backend_servers

backend backend_servers
	mode tcp
	# balance roundrobin
	option ssl-hello-chk
	# TODO: replace ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff with the target IPv6
	server server1 [ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:443 verify none

frontend http_in
	bind :::80 v4v6
	option forwardfor
	default_backend http_back

backend http_back
	# TODO: replace ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff with the target IPv6
	server server2 [ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:80

Sources:

Certifié

Depuis ce week-end, j’ai une nouvelle certification à mon actif, à savoir, la Google Professional Cloud DevOps Engineer. Le format de l’examen est classique, 50-60 questions sur le sujet, pas toujours des plus simples, 2 heures de temps, surveillance en ligne. Une jolie réussite pour ce début d’année 2025 et qui porte désormais mon nombre de certifications à 4.

Mon entrée dans le monde des certificats s’est fait en 2022, avec la Google Professional Cloud Developer. Après 3 ans de pratique pour mettre en place la nouvelle plateforme cloud d’un client, c’était la candidate idéale pour démarrer. Puis, avance rapide début 2024 où, après plusieurs semaines d’auto-formation pour améliorer ma compréhension de la technologie et pour atteindre mes objectifs annuels, je passe la Certified Kubernetes Application Developer (CKAD pour les intimes) de la Linux Foundation.

Plus tard à la fin août 2024, ce sera le renouvellement de ma certification Google Professional Cloud Developer. Avec seulement 2 ans de validité, le moment de repasser l’examen arrive plus rapidement qu’il n’y paraît. Pour ne pas refaire la même erreur qu’en 2022, je décide de capitaliser sur le travail déjà effectué pour rafraîchir ma mémoire et je passe à la fin novembre la certification Google Professional Cloud Architect.

Motivé par ces réussites, je découvre qu’en me formant pour ces deux certifications Google, j’ai déjà effectué presque 80% du parcours de formation recommandé pour la certification DevOps. Qu’à cela ne tienne, quelques heures d’études supplémentaires, me permettent d’empocher une nouvelle certification: la Google Professional Cloud DevOps Engineer.

Alors que j’écris ces quelques lignes, je me prends à sourire. Il y a quelques années, je ne voyais que peu d’intérêt à l’obtention de certifications et considérais, et considère toujours, l’expérience pratique comme bien supérieure. Qu’est-ce qui a changé me demanderez-vous ?

Continuer la lecture de « Certifié »

Contourner le filtrage web en entreprise

Je suis fatigué et irrité par les politiques de filtrage web des grandes entreprises qui, pour des raisons de sécurité ou par manque de confiance envers leurs employés, appliquent un filtrage bien trop agressif sur leur trafic web de sortie.

J’avais rencontré ce problème sur mon premier poste en sortant d’école. Tous les employés étaient d’ailleurs pris pour des étudiants, puisque l’entreprise utilisait des listes de filtrages en usage dans les universités françaises. Impossible donc de consulter la carte de l’enseigne de restauration du coin (Jamais compris le gain de sécurité). Plus embêtant était le blocage pur et simple de gist, ou d’outils similaires – ce qui est quand même fort dommage quand, au détour d’une discussion web sur un problème technique, tout le monde pointe vers une solution à laquelle vous n’avez pas accès.

Comme d’habitude dans ces cas-là, certains employés ont trouvé une solution pour simplement pouvoir faire leur travail sans qu’on leur mette constamment des bâtons dans les roues. A l’époque, l’intégralité de mon équipe avait configuré son poste de travail pour que le trafic passe par le même proxy que celui utilisé par les machines qui hébergeaient nos environnements de développement et de test. Proxy qui était pas ou peu filtré.

Je pensais naïvement qu’en presque 10 ans, les choses s’étaient améliorées, et pourtant, je suis à nouveau confronté aux mêmes problèmes idiots. Un exemple parmi d’autres: récemment, le système de surveillance du trafic réseau du client chez lequel j’interviens a décidé de bloquer le site web de npm, à savoir https://www.npmjs.com. Plus d’accès à la documentation (sauf à trouver le dépôt des sources). Terminé la récupération des dépendances en local… Le blocage fut heureusement de courte durée, mais bon, c’est rapidement une journée de travail perdue pour les personnes concernées. 

Autre exemple, impossible de faire des recherches sur les technologies WebGL, et tout ce qui tourne autour des modèles 3D dans le navigateur. Pourquoi donc, me direz-vous ? Où est le danger ? Ce coup là, le blocage est effectué car ces sujets sont assimilés au domaine du jeu vidéo et donc bloqués de peur que les employées passent leur journée à jouer. Bravo, vous venez par la même occasion de vous assurer que l’entreprise n’innovera pas dans ce domaine, ou avec difficulté.

En fait, le problème est en général tellement courant, qu’on finit par apprendre que pour pouvoir travailler, une équipe entière s’est fait installer une connexion internet domestique pour son propre usage.

Revenons à nos moutons, filtrage excessif du trafic web: ras-le-bol. Pas de droits d’admin sur ma machine, et installations limitées au catalogue de logiciels autorisés… Je dispose par contre d’une machine virtuelle chez l’un des grands fournisseurs mondiaux de cloud (il faut bien trouver une manière de travailler quand tout est bloqué). La solution: utiliser cette machine comme nœud de sortie pour mon trafic web.

Pour ce faire, je dispose bien entendu d’une entrée dans mon fichier .ssh/config, de la forme:

Host proxy
  HostName <ip_machine_distante>
  User <username>
  IdentityFile <chemin_vers_le_fichier_pem>

A partir de là, je vais donc lancer un proxy SOCKS via ssh avec la commande: ssh proxy -N -D 9090.
-N précise qu’aucune commande ne sera exécutée sur l’hôte distant et -D pour utiliser, ici le port 9090, comme port sur la machine local exposant le proxy SOCKS.

Deuxième étape: télécharger Firefox en version portable pour disposer d’un navigateur sur lequel j’ai la main sur la configuration des paramètres de proxy: https://portableapps.com/apps/internet/firefox_portable .

Enfin, configurer notre Firefox en version portable pour utiliser notre proxy SOCKS. Dans la partie préférence, rechercher le terme proxy. Choisir ensuite “Manual proxy configuration”, et renseigner “127.0.0.1” dans “SOCKS Host” et “9090” dans “Port”. Conserver la case “SOCKS v5” cochée et valider.

Enfin, vérifier que l’url précédemment bloquée est accessible.

Dernière étape, aller chercher un café et le siroter en parcourant un web non filtré. Hourra !

Pour essayer de simplifier le lancement du proxy, j’ai créé 2 scripts: proxy.bat et start_proxy.sh. proxy.bat me sert de point d’entrée pour lancer le script principal start_proxy.sh via git bash, simplement en cliquant sur le fichier dans l’explorateur Windows.

"C:\Program Files\Git\bin\bash.exe" --login -i -c "C:/Users/<username>/APPS/start_proxy.sh"
# pause

Le script start_proxy.sh est quant à lui une copie inspirée d’un script partagé au détour d’un gist.

#! /bin/bash
# Inspired by: https://gist.github.com/scy/6781836?permalink_comment_id=2777535#gistcomment-2777535

set -eu
SOCKS_PROXY_PORT=9090
HOST=proxy

socket=$(mktemp -t deploy-ssh-socket.XXXXXX)
rm ${socket} # delete socket file so path can be used by ssh

exit_code=0

cleanup () {
    # Stop SSH port forwarding process, this function may be
    # called twice, so only terminate port forwarding if the
    # socket still exists
    if [ -S ${socket} ]; then
        echo
        echo "Sending exit signal to SSH process"
        ssh -S ${socket} -O exit ${HOST}
    fi
    exit $exit_code
}

trap cleanup EXIT ERR INT TERM

echo "Starting SOCKS proxy on port ${SOCKS_PROXY_PORT}"
# https://mpharrigan.com/2016/05/17/background-ssh.html
# Start SSH port forwarding process
ssh -M -S ${socket} -fNT -o ExitOnForwardFailure=yes -D ${SOCKS_PROXY_PORT} ${HOST}

# -f : Run in the background before command execution.
# -N : Do not execute a remote command; this is useful for just forwarding ports.
# -T : Disable pseudo-tty allocation.
# -S <socketname>: Use a control socket.
# -M : Put control socket in master mode.
# ExitOnForwardFailure : if set to "yes", the connection shall be terminated 
#   if ssh cannot set up all requested dynamic, tunnel, local, and remote port forwardings,
#   (e.g. if either end is unable to bind and listen on a specified port).
# -D <port>: Port on which to expose on the local machine.

ssh -S ${socket} -O check ${HOST}
# -O check,exit : Control command

# launching a shell here causes the script to not exit and allows you
#   to keep the forwarding running for as long as you want.
#   I also like to customise the prompt to indicate that this isn't a normal shell.

bash --rcfile <(echo 'PS1="\nwith-ports> "')

Le tout fonctionne plutôt, avec parfois une perte de connexion en revenant de la pause déjeuner, après une interruption trop longue de la connexion internet, ou une période d’inactivité. Dans cette situation, je ferme simplement la fenêtre “cmd” où tournait le script, et je relance le tout par un double clic sur mon premier fichier.

Simple, efficace et surtout fonctionnel.

Déploiement d’un routeur R7000 supplémentaire sous FreshTomato

Ca y est, le domicile parentale est enfin équipé en fibre optique, en remplacement d’une connexion Adsl vieillissante. Si la qualité et la stabilité du signal s’est donc grandement améliorée, l’organisation physique du réseau a quelque peu changée. La box fibre du fournisseur d’accès se retrouve à la cave, avec l’impact que vous devinez sur la couverture du signal WiFi.

Pour palier au problème, et plutôt que de souscrire à une option de répétiteur WiFi facturée mensuellement, je me suis procuré un routeur R7000 d’occasion. Comme d’habitude, plutôt que de rester sur le firmware Netgear, j’ai immédiatement procédé à l’installation de FreshTomato, pour disposer d’un système bien plus à jour et de nombreuses fonctionnalités supplémentaires, en particulier: des règles de filtrage DNS directement au niveau du routeur.

Sans rentrer dans les détails, la procédure d’installation tient en quelques étapes:

Principales difficultés dans l’opération: ne pas oublier qu’il faut commencer par installer l’image initial et ensuite, trouver ou retrouver les couples utilisateur/mot de passe à utiliser pour chaque image. Ce qui nous donne:

  • initial: admin/@newdig ou root/admin
  • AIO: root/admin

Et pour de plus amples détails, direction la documentation en anglais: Installing FreshTomato.