Contexte
Hardware
J'en ai marre des Raspberry Pi. Ça fait depuis longtemps que je le dis, et le temps n'a malheureusement pas corrigé certains défauts. Je vais oser l'affirmation :
Il est impossible d'avoir un uptime décent pour faire un serveur d'un Raspberry Pi, peu importe le type de serveur ou la génération de Raspberry Pi dont on parle. Ce sont des machines d'expérimentation, et malgré les soins que je leur apporte, je ne peux pas les utiliser comme des serveurs pour deux raisons :
- le réseau est instable (en particulier en Wifi mais c'est aussi vrai en ethernet pour les RPi qui en sont dotés, en tout cas le 4 comme on va le voir)
- le système de stockage aux fraises (je ne parle pas des cartes SD mais du stockage USB)
Pour les caméras, ce n'est pas trop grave si l'une d'elles se déconnecte pendant quelques minutes. C'est beaucoup plus embêtant pour un serveur DNS, en substance pi-hole, hébergé depuis quelque temps sur un Raspberry Pi 4. La machine utilise l'ISO officielle de la fondation et il n'y a que pi-hole qui y soit installé. Aucune configuration supplémentaire n'est faite sur la machine. L'installation est parfaitement vanilla, sauf ce qui est installé par pi-hole.
Le Pi 4 est connecté en ethernet, et il boote depuis un SSD SATA en USB3. Je ne me fous pas de la gueule de cette machine quand même.
Occasionnellement, une fois par semaine ou peut-être plus, la machine disparaît du réseau. Comme ça : pouf, plus de Raspberry Pi. Plus de DNS. Plus de SSH. Plus de pi-hole. Plus rien. Sans explication : pas de log, rien, que dalle, peanuts.
Software
J'adore pi-hole. L'application fonctionne exactement comme je veux qu'elle le fasse. Je vois en temps réel les domaines demandés par le réseau, je peux black/whitelister un domaine en un clic, c'est très frugal, ça répond bien, bref, c'est un excellent logiciel.
Malgré ces éloges, j'ai trois "problèmes" :
- installer un upstream récursif sur la même machine n'est pas "simple" (c'est-à-dire que c'est facile de copier/coller des commandes, mais les comprendre — et surtout comprendre pourquoi ça ne fonctionne pas — est une autre histoire)
- installer un cluster de pi-hole est tout sauf trivial (il y a moyen mais ce n'est absolument pas propre)
- pas d'installation sous NixOS (selon ma politique personnelle, docker n'est pas une option)
Perdre pour gagner avec Blocky, Unbound et NixOS
Hors de question de re-tenter le coup avec AdGuard Home pour les raisons que j'ai déjà détaillées ici. Une alternative (parmi d'autres) est blocky. Une "petite" application écrite en Go, qui fait office de proxy DNS et de blocklist. Avec unbound en upstream, on obtient une solution similaire à pi-hole. C'est la "stack" que l'on va installer sur un "vrai" serveur.
Similaire parce que, "malheureusement", on va perdre l'UI, mais est-ce que ça sera vraiment un problème ? Pour l'instant, je ne sais pas...
Pré-requis
Partons du principe qu'on a un serveur DNS fonctionnel sur le réseau (typiquement, le routeur ou le serveur DNS à remplacer).
On va installer Blocky et Unbound sur un serveur sous NixOS.
Unbound
{
services.unbound = {
enable = true;
resolveLocalQueries = false;
settings = {
server = {
interface = "127.0.0.1@5353";
access-control = [ "127.0.0.1/0 allow" ];
verbosity = 1;
};
};
};
}
On passe resolveLocalQueries
à false
pour qu'unbound ne pirate pas le resolv.conf
: étant donné qu'il n'écoute pas sur le port standard, si on ne met pas cette directive, NixOS ne pourra plus résoudre des domaines après l'installation d'unbound.
interface
et access-control
ne servent respectivement qu'à indiquer comment le serveur doit écouter (en l'occurrence, uniquement depuis 127.0.0.1
sur le port 5353
) et n'autoriser que la machine locale à l'utiliser.
Et c'est tout : tous les autres paramètres par défaut sont suffisants, nous n'avons besoin de rien d'autre.
nix-os rebuild switch
Et on s'assure que ça fonctionne :
nix-shell -p dig # Shell temporaire pour lancer dig
dig free.fr @127.0.0.1 -p 5353
; <<>> DiG 9.18.26 <<>> free.fr @127.0.0.1 -p 5353
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22474
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;free.fr. IN A
;; ANSWER SECTION:
free.fr. 600 IN A 212.27.48.10
;; Query time: 67 msec
;; SERVER: 127.0.0.1#5353(127.0.0.1) (UDP)
;; WHEN: Mon May 20 00:49:10 CEST 2024
;; MSG SIZE rcvd: 52
On peut passer à la suite.
L'avantage avec NixOS (et plus généralement, les distros du même genre) c'est qu'à ce stade, si ça ne fonctionne pas, ça ne vient pas de la configuration 😁
Blocky
{
services.blocky = {
enable = true;
settings = {
# On peut utiliser plusieurs upstreams dans plusieurs groupes...
upstreams = {
groups = {
default = [
# Mais en l'occurrence, on va se contenter d'utiliser notre unbound
"127.0.0.1:5353"
];
};
};
conditional = {
# Si Blocky n'a pas déjà résolu un domaine, il demande à l'upstream.
# Si on mettait false, on obtiendrait une erreur de résolution.
fallbackUpstream = true;
mapping = {
# Ça c'est personnel : c'est 10.0.0.1 qui répond pour le domaine
# home.arpa sur mon réseau parce que c'est le DHCP et qu'il stocke les
# noms d'hôtes du réseau local
"home.arpa" = "10.0.0.1";
};
};
blocking = {
blackLists = {
ads = [
# Là on ajoute les listes que l'on veut, à commencer par la plus
# célèbre
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
];
};
clientGroupsBlock = {
default = [
# Sans cette ligne, rien ne sera bloqué, tout sera envoyé à
# l'upstream. Le nom doit correspondre à au moins une liste créée
# dans `blackLists`
"ads"
];
};
};
# Les ports d'écoute
ports = {
dns = 53;
};
};
};
}
Voyez que si l'on virait les lignes ne servant qu'à ouvrir et fermer des blocs de config, on n'aurait moins d'une dizaine de lignes de configuration. Si vous préférez quelque chose de plus condensé :
{
services.blocky.enable = true;
services.blocky.settings.upstreams.groups.default = [ "127.0.0.1:5353" ];
services.blocky.settings.conditional.fallbackUpstream = true;
services.blocky.settings.conditional.mapping."home.arpa" = "10.0.0.1";
services.blocky.settings.blocking.blackLists.ads = [
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
];
services.blocky.settings.clientGroupsBlock.default = [ "ads" ];
services.blocky.settings.ports.dns = 53;
}
On répète exactement la même configuration sur un deuxième serveur. On obtient deux serveurs DNS capables de bloquer des domaines sur le réseau, mais, pour obtenir un véritable effet "cluster", il faut aller un peu plus loin.
Les directives de configuration de Blocky pour NixOS se résument à enable
et settings
, cette dernière acceptant une notation équivalent à celle de Blocky.
On pourra donc utiliser "directement" la documentation de Blocky pour le configurer dans NixOS.
redis
J'utilise de toute façon redis sur mon serveur web, ce n'est pas bien compliqué de l'installer sur le reverse-proxy (qui est aussi une machine sous NixOS).
Je vais d'ailleurs considérer que le redis du serveur web (10.0.2.2
) sera le maître du réseau, tandis que celui du reverse-proxy (10.0.2.1
) sera l'esclave.
Le redis maître sera configuré comme suit :
{
services.redis.vmOverCommit = true;
services.redis.servers.blocky = {
enable = true;
bind = "0.0.0.0";
port = 16379
masterAuth = "secret";
requirePass = "secret";
};
}
Notons que nous créons un service redis spécialement dédié à blocky.
On applique la modification que redis recommande avec services.redis.vmOverCommit = true;
.
En outre, on fait écouter ce serveur sur un port différent du port original (6379
) pour pouvoir continuer d'utiliser une autre instance de redis dédiée à d'autres usages.
Enfin, n'oubliez pas de changer le mot de passe (secret
).
Et l'esclave :
{
services.redis.vmOverCommit = true;
services.redis.servers.blocky = {
enable = true;
bind = "0.0.0.0";
masterAuth = "secret";
requirePass = "secret";
slaveOf = {
ip = "10.0.2.2";
port = 16379;
};
};
}
Même configuration, mais en précisant simplement quel est le serveur maître.
Après quelques nixos rebuild switch
, on devrait voir la sortie suivante sur le serveur maître :
journalctl -u redis-blocky.service -f
May 20 10:27:08 mac-mini-m1-de-richard redis[97580]: Running mode=standalone, port=16379.
May 20 10:27:08 mac-mini-m1-de-richard redis[97580]: Server initialized
May 20 10:27:08 mac-mini-m1-de-richard redis[97580]: Loading RDB produced by version 7.2.4
May 20 10:27:08 mac-mini-m1-de-richard redis[97580]: RDB age 0 seconds
May 20 10:27:08 mac-mini-m1-de-richard redis[97580]: RDB memory usage when created 0.98 Mb
May 20 10:27:08 mac-mini-m1-de-richard redis[97580]: Done loading RDB, keys loaded: 0, keys expired: 0.
May 20 10:27:08 mac-mini-m1-de-richard redis[97580]: DB loaded from disk: 0.000 seconds
May 20 10:27:08 mac-mini-m1-de-richard redis[97580]: Ready to accept connections tcp
May 20 10:27:08 mac-mini-m1-de-richard redis[97580]: Ready to accept connections unix
May 20 10:27:08 mac-mini-m1-de-richard systemd[1]: Started Redis Server - redis-blocky.
May 20 10:27:42 mac-mini-m1-de-richard redis[97580]: Replica 10.0.2.1:16379 asks for synchronization
May 20 10:27:42 mac-mini-m1-de-richard redis[97580]: Partial resynchronization request from 10.0.2.1:16379 accepted. Sending 0 bytes of backlog starting from offset 2087.
Et sur l'esclave :
journalctl -u redis-blocky.service -f
May 20 10:27:42 reverse-proxy redis[48394]: Ready to accept connections unix
May 20 10:27:42 reverse-proxy systemd[1]: Started Redis Server - redis-blocky.
May 20 10:27:42 reverse-proxy redis[48394]: Connecting to MASTER 10.0.2.2:16379
May 20 10:27:42 reverse-proxy redis[48394]: MASTER <-> REPLICA sync started
May 20 10:27:42 reverse-proxy redis[48394]: Non blocking connect for SYNC fired the event.
May 20 10:27:42 reverse-proxy redis[48394]: Master replied to PING, replication can continue...
May 20 10:27:42 reverse-proxy redis[48394]: Trying a partial resynchronization (request c55ad3ed1037641cbbf6f8cc36a7e76bc64b59e5:2087).
May 20 10:27:42 reverse-proxy redis[48394]: Successful partial resynchronization with master.
May 20 10:27:42 reverse-proxy redis[48394]: Master replication ID changed to 00484c469b8a3de337df088cadefc5e2687f2338
May 20 10:27:42 reverse-proxy redis[48394]: MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization.
Donc, a priori tout fonctionne.
Il ne reste plus qu'à intégrer redis à Blocky en modifiant sa configuration sur les deux serveurs :
{
services.blocky = {
enable = true;
settings = {
# [...]
redis = {
address = "127.0.0.1:16379";
password = "secret";
};
};
};
}
Dans un monde idéal, on aurait trois serveurs redis et on utiliserait sentinel, mais dans le cas présent, un système maître/esclave devrait suffire : c'est pourquoi on indique à chaque serveur blocky d'utiliser son propre serveur redis qui sera de toute façon répliqué sur l'autre serveur physique.
Après quelques instants, on devrait pouvoir voir la base de données de redis se remplir :
redis-cli -p 16379 -a secret
127.0.0.1:16379> KEYS *
1) "blocky:cache:\x00\x01hosts.tweedge.net"
2) "blocky:cache:\x00\x01blocklistproject.github.io"
3) "blocky:cache:\x00\x1cgit.athaliasoft.com"
4) "blocky:cache:\x00\x01git.athaliasoft.com"
5) "blocky:cache:\x00\x1craw.githubusercontent.com"
6) "blocky:cache:\x00\x1cblocklistproject.github.io"
7) "blocky:cache:\x00\x01raw.githubusercontent.com"
8) "blocky:cache:\x00\x1chosts.tweedge.net"
Et on devrait voir plus ou moins la même chose sur les deux serveurs.
Blacklist/whitelist personnelles
Évidemment, on peut modifier la configuration de Blocky pour ajouter ses propres black/whitelists :
{
services.blocky = {
enable = true;
settings = {
# [...]
blocking = {
blackLists = {
ads = [
"someadsdomain.com"
# [...]
];
};
};
};
};
}
Mais cela impose de modifier les deux serveurs à chaque fois, puis nixos rebuild switch
, etc.
C'est chiant.
Donc, la solution, c'est d'héberger ses listes dans un dépôt git.
Du coup, on peut faire ça :
{
services.blocky = {
enable = true;
settings = {
# [...]
blocking = {
blackLists = {
externalLists = [
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
];
internalLists = [
https://git.athaliasoft.com/Infrastructure/dns/raw/branch/main/blacklist.txt
];
};
whiteLists = {
internalLists = [
https://git.athaliasoft.com/Infrastructure/dns/raw/branch/main/whitelist.txt
];
};
};
# [...]
};
};
}
Il n'y a qu'à mettre à jour le dépôt git pour que, ponctuellement, Blocky mette à jour ses listes.
Et si on est vraiment pressé, on peut toujours faire systemctl restart blocky.service
.
On écrira alors notre blacklist comme un fichier hosts
:
0.0.0.0 cache.consentframework.com
0.0.0.0 cdn.stripcash.com.c.footprint.net
0.0.0.0 consentframework.com
0.0.0.0 cookieless-data.com
# etc
Alors que la whitelist contiendra un domaine par ligne, tout simplement.
On utilisera alors le chemin vers le fichier brut, fourni par notre forge logicielle.
![Capture d'écran de l'interface web de la forge logicielle Gitea, montrant notamment le bouton nommé 'Brut' offrant un lien direct vers le fichier concerné.](https://www.richard-dern.fr/storage/8793/4699/dbc1/9ab3/61d6/82fd/a233/b305/87934699dbc19ab361d682fda233b305.webp)
Utilisez le bouton "Brut" pour obtenir l'URL du fichier à rajouter à blocky
Il y a d'autres façons de faire : blocky peut aussi interpréter des fichiers locaux1. Mais j'ai estimé que pour mon cas d'usage, stocker mes black/whitelists dans Gitea était plus intéressant.
Je regrette juste de ne pas encore avoir compris s'il était possible de fournir à blocky la liste des listes à parser : j'aurais aimé stocker dans Gitea un fichier qui contient une liste d'URLs (contenant notamment https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts mentionné dans mes exemples, mais pas seulement), mais je n'ai pas — encore — compris comment faire ni même si c'était possible.
Conclusion
La gestion des black/whitelists est un peu plus pénible que sous pi-hole : je ne peux plus afficher en temps réel la liste des domaines demandés, et cliquer sur l'un d'eux pour le (dé)bloquer. Il est possible de partiellement remédier à cela en installant une stack Prometheus/Grafana, ce que je n'ai pas envie de faire pour le moment, pas juste pour blocky.
Pour le moment, cette solution me satisfait, et, finalement, l'interface web ne me manque pas. Après tout, une fois que tout est en place, on ne touche plus à rien, et la supervision se fait aussi bien en ligne de commande. Et je ne trifouille pas tous les jours les black/whitelists.
Notons tout de même que tout cela est bien plus simple et rapide à installer, et surtout à sauvegarder et à restaurer que pi-hole sur un Raspberry Pi.
Deux instances de blocky couplées à deux instances de redis sur deux serveurs physiques "fiables" devraient m'éviter quelques déconvenues à l'avenir...