Headscale Tailscale Technitium - Split Horizon Technitium pour `*.home.arpa` (LAN et tailnet) (Partie II)
Rapport complet : DNS Technitium en Split Horizon, Tailscale/Headscale et postes clients
Date : 27 octobre 2026
Les adresses IPv6 publiques sont masquées (
<IPV6_PUBLIQUE_CWWK>,<PREFIXE_IPV6_PUBLIC>). Les adresses privées (192.168.x.x, 100.64.x.x, fd7a:…, fd0f:…) sont conservées. Aucune clé ni secret ne figure dans ce document.
1. Résumé
| Sujet | État |
|---|---|
Réponse DNS de *.home.arpa selon l’origine du client |
En place : IP LAN pour les clients du LAN, IP Tailscale pour les clients du tailnet |
| Structure des enregistrements | cwwk en APP (Split Horizon) ; * et dns en CNAME vers cwwk.home.arpa |
HTTPS *.home.arpa par le tunnel |
Fonctionnel : politique Headscale étendue au 443, CA installée dans le magasin système des clients |
| Exposition depuis Internet | Seuls 443 et 55205 répondent ; DNS, NFS, console et API de Technitium bloqués ; SSH par clé uniquement |
| Poste de bureau (EndeavourOS) | systemd-resolved avec NetworkManager ; DNS 192.168.0.205 seul (IPv4 fixé, IPv6 du routeur ignorée) |
| Portable (CachyOS, Mullvad) | Vérifié et laissé tel quel, par choix |
| DHCP du routeur | Ne distribue pas Technitium ; non modifié (trois appareils seulement) |
| Clients du mesh (VM Debian et Alpine) | Contrôlés avec un script : tout est OK |
| Hôtes Proxmox | CA système volontairement non traitée (gestion des certificats propre à Proxmox) |
Objectif atteint : un nom *.home.arpa se résout vers une adresse joignable où que soit le client, et les postes concernés ont un DNS stable après une coupure réseau.
2. Architecture
| Élément | Valeur |
|---|---|
| Coordination | Headscale (VPS Debian) |
| Suffixe MagicDNS | tailscale.xoyize.xyz |
| Nœud DNS et reverse proxy | cwwk : Technitium (docker compose) et nginx |
IP LAN de cwwk |
192.168.0.205 |
IP Tailscale de cwwk |
100.64.0.2 et fd7a:115c:a1e0::2 |
| Zone locale | home.arpa |
| Split DNS Headscale | home.arpa vers 100.64.0.2 et fd7a:115c:a1e0::2 ; aucun résolveur global (override_local_dns: false) |
| Réseau local | 192.168.0.0/24 (routeur 192.168.0.254) ; un second réseau 192.168.10.0/24 derrière un autre routeur |
| Postes | PC de bureau pc1 (EndeavourOS/KDE), portable e6230 (CachyOS, Wi-Fi), yiak (distant), skrime (VPS IPv6 seul), deux nœuds Proxmox, VM Debian et Alpine |
Pare-feu de cwwk |
ufw : deny (incoming), allow (outgoing), deny (routed) |
Sur cwwk : accept-dns=false, pour éviter une boucle DNS (Tailscale ferait pointer le système vers 100.100.100.100, qui relaierait vers Technitium sur ce même nœud). Les forwarders de Technitium ne doivent jamais contenir 100.100.100.100.
3. Lire la configuration d’un client Tailscale
1
2
3
4
tailscale debug prefs | jq .CorpDNS # true = accept-dns activé (droits root requis)
tailscale get accept-dns # versions récentes
tailscale dns status # DNS, MagicDNS, split DNS, domaines de recherche
tailscale status --json
tailscale dns status doit être lancé en root pour afficher la dernière section (Access denied sinon, sans conséquence sur le fonctionnement).
Lecture type sur un client (yiak) :
| Élément | Interprétation |
|---|---|
Tailscale DNS: enabled |
accept-dns=true |
MagicDNS: enabled, suffixe tailscale.xoyize.xyz |
Les pairs sont joignables par nom.tailscale.xoyize.xyz ; ping cwwk marche grâce au domaine de recherche |
Resolvers: (no resolvers configured) |
Pas de résolveur global : le DNS système reste utilisé pour tout le reste |
home.arpa -> 100.64.0.2 et fd7a:115c:a1e0::2 |
Split DNS vers Technitium, en double pile |
Configuration Headscale correspondante :
1
2
3
4
5
6
7
8
9
10
dns:
magic_dns: true
base_domain: tailscale.xoyize.xyz # distinct de home.arpa
override_local_dns: false
nameservers:
global: []
split:
home.arpa:
- 100.64.0.2
- fd7a:115c:a1e0::2
4. Le problème de départ
Technitium renvoyait la même réponse à tous les clients : cwwk.home.arpa donnait 192.168.0.205 (A) et une IPv6 globale (AAAA). Un client distant recevait donc une adresse qu’il ne pouvait pas joindre, ou tentait une connexion directe par l’IPv6 publique, sans passer par le tunnel. Trois options ont été étudiées :
| Option | Principe | Limites |
|---|---|---|
| 1. Subnet router | cwwk annonce 192.168.0.0/24 au tailnet |
Donne accès à tout le LAN ; ufw étant en deny (routed), il faut une règle ufw route allow in on tailscale0 out on <interface_lan> ; non retenue pour l’instant |
| 2. Split Horizon dans Technitium | La réponse dépend de l’IP source du client | Retenue |
3. Nom MagicDNS (cwwk.tailscale.xoyize.xyz) |
Aucun réglage | Deux noms selon l’endroit ; le certificat de la CA home.arpa ne couvre pas ce nom |
5. Configuration finale
5.1 Technitium : app Split Horizon
Apps → Split Horizon → Config. Seul le bloc networks compte : ses clés sont les noms de réseaux utilisables dans les enregistrements APP.
1
2
3
4
5
6
7
8
9
10
11
12
"networks": {
"lan": [
"127.0.0.1",
"::1",
"192.168.0.0/24",
"192.168.10.0/24"
],
"tailscale": [
"100.64.0.0/10",
"fd7a:115c:a1e0::/48"
]
},
Les blocs networkGroupMap, groups et domainGroupMap du fichier de configuration appartiennent à la traduction d’adresses ("enableAddressTranslation": false) et n’ont aucun effet ici.
5.2 Technitium : zone home.arpa
| Nom | Type | Valeur | TTL |
|---|---|---|---|
cwwk |
APP (Split Horizon, SplitHorizon.SimpleAddress) |
voir ci-dessous | 300 |
* |
CNAME |
cwwk.home.arpa |
300 |
dns (et chaque nom servi par nginx qui avait son propre A) |
CNAME |
cwwk.home.arpa |
300 |
Record Data de l’enregistrement APP :
1
2
3
4
{
"lan": ["192.168.0.205"],
"tailscale": ["100.64.0.2", "fd7a:115c:a1e0::2"]
}
L’IPv6 publique n’est volontairement pas servie : les clients du LAN utilisent l’IPv4 et aucun client n’est dirigé vers l’adresse globale.
Fonctionnement : un client du tunnel interroge Technitium avec une source 100.64.0.x, il est classé tailscale. Un client du LAN sans Tailscale arrive avec une source 192.168.x.x, il est classé lan. Le CNAME du wildcard est bien évalué par Split Horizon (l’APP ne pouvant pas être posé sur *). Les noms qui ont leur propre enregistrement ne sont pas couverts par le wildcard : ils doivent être en CNAME vers cwwk.home.arpa ou porter leur propre APP. Les machines autres que cwwk gardent leur IP LAN et ne sont joignables à distance que par un subnet router ou leur propre IP Tailscale.
La source des requêtes UDP est correctement vue par Technitium (192.168.0.x pour le LAN, 100.64.0.x pour le tunnel) : le conteneur n’a pas besoin de network_mode: host.
5.3 nginx (cwwk)
- Écoute sur toutes les adresses (
0.0.0.0:443et[::]:443) : les IP Tailscale sont déjà couvertes, et nginx sert aussi des sites publics sans lien avechome.arpa. - Des lignes
listenpour100.64.0.2et[fd7a:115c:a1e0::2]ont été ajoutées dans les blocs concernés. Elles ne sont pas nécessaires (l’écoute sur0.0.0.0et[::]couvre déjà ces adresses) et peuvent empêcher nginx de démarrer si l’interfacetailscale0n’a pas encore son adresse au démarrage : à retirer, ou à compenser avecnet.ipv4.ip_nonlocal_bind=1etnet.ipv6.ip_nonlocal_bind=1. - Démarrage ordonné après Tailscale :
1
2
3
4
# sudo systemctl edit nginx
[Unit]
After=tailscaled.service
Wants=tailscaled.service
- Les blocs
home.arparestreignent l’accès :
1
2
3
4
5
allow 192.168.0.0/24;
allow 100.64.0.0/10;
allow fd7a:115c:a1e0::/48;
allow <PREFIXE_IPV6_PUBLIC>/64;
deny all;
5.4 Headscale
Politique étendue pour autoriser le port 443 vers cwwk pour les machines du tailnet. Avant cette modification, curl renvoyait 000 : la connexion TCP n’aboutissait pas, alors que le DNS (port 53) fonctionnait déjà.
5.5 Autorité de certification
Le certificat racine homearpaCA.crt (public) est installé dans le magasin système des clients qui font du HTTPS vers *.home.arpa. La clé de la CA reste uniquement dans le coffre Cryptomator.
| Système | Installation |
|---|---|
| Debian, Alpine | copier dans /usr/local/share/ca-certificates/ (extension .crt), puis update-ca-certificates (Alpine : paquet ca-certificates installé) |
| Arch | sudo trust anchor --store homearpaCA.crt |
Les hôtes Proxmox ne sont pas concernés : leur gestion des certificats est propre à Proxmox, et le magasin système n’y est utile que si l’hôte appelle lui-même un service HTTPS signé par cette CA.
6. Exposition depuis Internet (audit)
Test réalisé avec un mobile (Termux) en données mobiles, Tailscale coupé, sur l’IPv6 publique de cwwk.
| Élément | Résultat |
|---|---|
| Ports 53, 2049, 5380, 8053 (TCP, IPv6) | bloqués (timed out) |
| DNS UDP 53 (IPv6) | timed out |
| Ports 5380 et 8053 en IPv4 depuis le LAN | timed out : pas de contournement par Docker |
| Ports 443 et 55205 | ouverts (attendu) |
nginx dns.home.arpa depuis un VPS extérieur |
403 |
SSH (sshd -T) |
passwordauthentication no, pubkeyauthentication yes, permitrootlogin without-password |
Règles ufw : seuls 443 et 55205 sont ouverts à tous ; le DNS est servi au LAN et au tailnet ; NFS (2049) est autorisé sur tailscale0 et refusé ailleurs ; 5380 et 8053 sont refusés (IPv4 et IPv6). L’ordre des règles compte : les DENY sur 5380 et 8053 passent avant l’autorisation générale du LAN. Docker peut contourner ufw pour les ports publiés, mais le test montre que ce n’est pas le cas ici ; lier 5380 et 8053 à 127.0.0.1 dans le compose reste une bonne pratique.
7. Poste de bureau (EndeavourOS/KDE, accept-dns=true)
7.1 Incident
Après une coupure internet, un arrêt, puis un redémarrage au retour du réseau, plus aucun lien ne fonctionnait : Tailscale avait réécrit /etc/resolv.conf. Au démarrage sans internet, aucun DNS n’avait encore été obtenu ; Tailscale, qui n’était pas intégré à systemd-resolved, a écrit son propre resolv.conf en mémorisant une configuration d’origine vide. Au retour du réseau, rien n’a remis le DNS du LAN.
7.2 Correction appliquée
1
2
3
4
5
6
7
8
9
sudo systemctl enable --now systemd-resolved
sudo install -d /etc/NetworkManager/conf.d
sudo tee /etc/NetworkManager/conf.d/dns.conf >/dev/null <<'EOF'
[main]
dns=systemd-resolved
EOF
sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved NetworkManager
sudo systemctl restart tailscaled
/etc/resolv.conf contient désormais le stub (nameserver 127.0.0.53) : c’est normal, les vrais serveurs se lisent avec resolvectl status. Tailscale ne touche plus au fichier ; il déclare seulement ses domaines sur tailscale0.
7.3 État constaté
| Lien | Contenu |
|---|---|
enp7s0 (profil Lan 2.5Ghz (192.168.0.0)) |
DNS Servers: 192.168.0.205, Default Route: yes |
tailscale0 |
100.100.100.100, domaines MagicDNS et ~home.arpa, Default Route: no |
enp8s0 |
aucun lien, sans conséquence |
Le DNS IPv4 est fixé à la main dans le profil NetworkManager (ipv4.dns = 192.168.0.205, ipv4.ignore-auto-dns = yes) : il ne dépend donc pas du DHCP du routeur. Le DNS IPv6 fd0f:ee:b0::1 annoncé par le réseau (probablement le routeur) a été écarté :
1
2
3
sudo nmcli con mod "Lan 2.5Ghz (192.168.0.0)" ipv6.ignore-auto-dns yes
sudo nmcli con up "Lan 2.5Ghz (192.168.0.0)"
resolvectl status enp7s0 | grep -E 'Current DNS|DNS Servers' # 192.168.0.205 seul
Conséquence : sans second serveur, ce poste n’a plus de DNS si cwwk est éteint.
7.4 Durcissement optionnel : DNS de secours
systemd-resolved possède par défaut des DNS de secours (Quad9, Cloudflare, Google), utilisés seulement quand aucun DNS n’est connu, par exemple au démarrage avant le DHCP. Pour limiter les fuites hors de Technitium :
1
2
3
4
5
6
sudo install -d /etc/systemd/resolved.conf.d
sudo tee /etc/systemd/resolved.conf.d/fallback.conf >/dev/null <<'EOF'
[Resolve]
FallbackDNS=9.9.9.9#dns.quad9.net 2620:fe::9#dns.quad9.net
EOF
sudo systemctl restart systemd-resolved
FallbackDNS= vide supprime toute alternative. Facultatif, non confirmé comme appliqué.
7.5 Test de robustesse
1
2
3
4
5
6
sudo nmcli dev disconnect enp7s0
ls -l /etc/resolv.conf # lien vers stub-resolv.conf, inchangé
sudo systemctl restart tailscaled
ls -l /etc/resolv.conf # toujours inchangé : c'est la correction
sudo nmcli dev connect enp7s0
resolvectl status | grep -A4 'Link 2' # 192.168.0.205 revient
8. Portable (CachyOS, Wi-Fi, Mullvad)
Le DHCP du Wi-Fi distribue 192.168.0.254 (le routeur), 1.1.1.1 et, en IPv6, fd0f:ee:b0::1 : pas Technitium. Avec Mullvad connecté (fonction LAN Sharing active) :
| Élément | Lecture |
|---|---|
wg0-mullvad : DNS Servers: 10.64.0.1, DNS Domain: ~., Default Route: yes |
Les noms ordinaires passent par Mullvad (example.org répond via wg0-mullvad) |
Configuration globale : DNS Servers: 192.168.0.205, DNS Domain: ~home.arpa |
home.arpa est envoyé à Technitium ; 192.168.0.205 reste joignable grâce à LAN Sharing |
wlan0 : DNS du routeur et 1.1.1.1 |
Utilisés seulement si Mullvad est déconnecté (le kill switch bloque alors le trafic, sauf s’il est désactivé) |
tailscale0 |
100.100.100.100, Default Route: no |
Décision : ne rien modifier. Deux choix auraient été possibles : garder le DNS de Mullvad pour tout le reste (retenu : les requêtes DNS ordinaires restent dans le tunnel), ou utiliser Technitium même avec Mullvad (mullvad dns set custom 192.168.0.205 et mullvad lan set allow, mais les requêtes sortent alors par la connexion domestique selon les forwarders de Technitium). Pour revenir en arrière : mullvad dns set default.
Contrôle en lecture seule, chez soi et hors de chez soi :
1
2
resolvectl domain
resolvectl query dns.home.arpa
9. DHCP du routeur et autres appareils du LAN
| Constat | Détail |
|---|---|
| DHCP distribué | 192.168.0.254 et 1.1.1.1 (IPv4), fd0f:ee:b0::1 (IPv6) : un poste en configuration automatique contourne Technitium |
| Appareils concernés | Trois : le PC de bureau et le portable (déjà gérés à la main) et un poste EndeavourOS éteint, sans Tailscale |
| Décision | Ne pas modifier le DHCP du routeur |
Si l’on voulait le corriger à la source : distribuer 192.168.0.205 à la place de 192.168.0.254 et 1.1.1.1, et retirer ou remplacer le DNS IPv6 annoncé. Contrepartie : si cwwk s’arrête, tout le réseau perd sa résolution DNS.
Au prochain démarrage du poste éteint, lui appliquer la configuration du poste de bureau :
1
2
sudo nmcli con mod "<profil>" ipv4.dns 192.168.0.205 ipv4.ignore-auto-dns yes ipv6.ignore-auto-dns yes
sudo nmcli con up "<profil>"
Sans Tailscale, il recevra l’IP LAN de cwwk (groupe lan), ce qui est le comportement voulu.
10. Vérification des clients du mesh
Deux scripts (en annexe B) automatisent le contrôle :
verif-client-tailscale.sh, en shell POSIX (Debian et Alpine), s’exécute sur un client ;verif-mesh.sh, sur le poste Arch, le lance sur toutes les machines du CSV SSH qui ont une IP dans la colonneMesh, sans rien installer sur les clients.
1
2
3
4
~/verif-mesh.sh # toutes les machines avec une IP Mesh
~/verif-mesh.sh alias # une seule machine
~/verif-mesh.sh | grep -E '^==|KO|WARN' # uniquement les anomalies
SUDO='sudo -n ' ~/verif-mesh.sh # si les préférences Tailscale exigent root
| Étape | Contrôle | Résultat attendu |
|---|---|---|
| 1. Tailscale | BackendState, IP, service activé et démarré |
Running, actif |
| 2. DNS | accept-dns, gestionnaire de /etc/resolv.conf |
true partout, sauf cwwk (false) ; un nœud du LAN en false dont le resolv.conf pointe vers Technitium est valide |
| 3. Résolution | cwwk, dns et un nom aléatoire (wildcard) |
100.64.0.2 et son IPv6 ; 192.168.0.205 sur cwwk ou un nœud du LAN en DNS direct |
| 4. HTTPS | curl avec certificat vérifié, présence de la CA |
ssl_verify=0, code différent de 000 |
| 5. Chemin | tailscale ping cwwk |
pong, sans relais DERP |
Résultats de la première exécution :
| Machine | Résultat | Suite |
|---|---|---|
pve (Proxmox, 100.64.0.10) |
accept-dns=false, DNS direct vers Technitium : valide pour un nœud du LAN ; échec HTTPS (ssl_verify=20) |
Non traité : gestion des certificats propre à Proxmox |
tiny (Proxmox, 100.64.0.12) |
DNS Tailscale correct ; CA absente | Non traité, même raison |
| VM Debian et Alpine | Tout OK avec la version d’origine du script | — |
La version du script en annexe corrige deux points : un nœud du LAN en DNS direct n’est plus signalé en KO, et un hôte Proxmox (présence de /etc/pve) affiche un échec HTTPS en simple avertissement. Elle affiche aussi correctement « certificat non reconnu » au lieu de « connexion impossible » quand la CA manque.
Avertissement fréquent : pas de systemd-resolved. Sans systemd-resolved, Tailscale réécrit /etc/resolv.conf : c’est la situation de l’incident du poste de bureau. Sur un serveur au DNS statique, le risque est faible, car la configuration d’origine existe déjà au démarrage. Pour le réduire sur Debian :
1
2
3
4
# sudo systemctl edit tailscaled
[Unit]
After=network-online.target
Wants=network-online.target
Sur Alpine, vérifier que le service dépend de net dans /etc/init.d/tailscale.
11. Pièges rencontrés
| Piège | Explication |
|---|---|
networkGroupMap n’est pas ce qu’on croit |
Dans la configuration de l’app, c’est le bloc networks qui définit les réseaux nommés utilisables dans l’APP. networkGroupMap ne sert qu’à la traduction d’adresses, désactivée ici. |
| Réponse vide de l’app | Un client classé dans un groupe absent du Record Data (par défaut private/public) ne reçoit aucune adresse. |
APP impossible sur * |
Contournement : APP sur cwwk, puis * et les autres noms en CNAME vers cwwk.home.arpa. |
| Zone avec un wildcard | test.home.arpa ne renvoie pas NXDOMAIN : la réponse du wildcard est donnée. Une faute de frappe dans un nom donne donc une réponse valide. |
dig nom A AAAA |
Un seul type est gardé (extra type option) : une commande par type. |
dig @IP nom CNAME autre.nom |
Pose deux questions distinctes : les deux réponses se mélangent. |
| Test sans nom d’hôte | curl https://IP/ tombe sur le serveur nginx par défaut (404) : il faut envoyer le bon Host. |
000 côté HTTPS |
Aucune connexion TCP : filtre réseau (ici la politique Headscale) ou nginx qui n’écoute pas. ssl_verify=20 signale seulement l’absence de la CA. |
| Test « LAN » lancé depuis un client distant | dig @192.168.0.205 depuis un nœud sans route vers le LAN expire : normal. |
Client du LAN avec accept-dns=true |
Il reçoit l’IP Tailscale de cwwk comme un client distant et passe par le tunnel ; vérifier avec tailscale ping que la liaison reste directe. |
/etc/resolv.conf en 127.0.0.53 |
Normal avec systemd-resolved : les vrais serveurs se lisent dans resolvectl status. |
| DNS IPv6 annoncé par le routeur | Il s’ajoute au DNS IPv4 choisi : ipv4.ignore-auto-dns ne l’écarte pas, il faut ipv6.ignore-auto-dns yes. |
| Mullvad actif | Il impose ses serveurs DNS avec une route par défaut ; le DNS du réseau ne sert alors plus aux noms ordinaires. |
| DoH derrière nginx | Technitium voit l’IP du proxy (classée lan) : un client DoH distant recevrait l’IP LAN. Sans objet tant que le DoH reste local. |
12. Points ouverts et évolutions possibles
| Sujet | Détail |
|---|---|
| Sauvegarde de Technitium | Settings → Backup Settings (zone et apps), à conserver hors de cwwk ; à faire après chaque évolution |
Redémarrage de cwwk |
Contrôle de l’ordre nginx/Tailscale et de la persistance de l’app Split Horizon ; la politique de redémarrage du conteneur se vérifie avec la commande de l’annexe A (unless-stopped ou always attendu) |
| DoH distant | Pour distinguer LAN et tailnet derrière nginx : proxy_set_header X-Real-IP $remote_addr; côté nginx, puis réglage de l’IP réelle et de l’ACL des reverse proxies dans Technitium (Settings → Optional Protocols) |
| Subnet router | Non configuré : à prévoir si d’autres machines du LAN doivent être joignables à distance |
| Nouveaux noms | Un nom servi par nginx passe par le wildcard sans rien créer. Un nom avec son propre enregistrement doit être en CNAME vers cwwk.home.arpa. Brave exige que chaque nom figure explicitement dans le SAN du certificat : à contrôler au renouvellement. |
| DNS de secours du poste de bureau | Section 7.4, facultatif |
| Poste EndeavourOS éteint | Appliquer la section 9 à son premier démarrage |
| TTL | Fixé à 300 s : à remonter si les changements sont rares |
Retour arrière du Split Horizon
Supprimer l’enregistrement APP de cwwk, recréer ses A (192.168.0.205) et AAAA, puis remettre * et les noms convertis en enregistrements A. Tous les clients retrouvent alors la même réponse partout.
Annexe A : commandes utiles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Tailscale
tailscale debug prefs | jq .
tailscale dns status
tailscale status --json
tailscale ping <nœud>
# Headscale
headscale nodes list
headscale nodes list-routes
headscale policy get
# Pare-feu et ports (cwwk)
sudo ufw status verbose
sudo ufw status numbered
sudo ss -tlnp | grep -E ':(53|443|5380|8053)\b'
docker ps
docker inspect technitium | grep -A2 '"RestartPolicy"' # unless-stopped ou always attendu
# SSH
sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin|pubkeyauthentication'
# DNS
dig @100.64.0.2 <nom>.home.arpa A +short
dig @192.168.0.205 <nom>.home.arpa A +short # depuis le LAN
getent ahosts <nom>.home.arpa
resolvectl status
resolvectl query <nom>
nmcli -g ipv4.dns,ipv4.ignore-auto-dns,ipv6.ignore-auto-dns con show "<profil>"
# Mullvad (portable)
mullvad status
mullvad dns get
Annexe B : scripts de vérification
verif-client-tailscale.sh (à exécuter sur un client, shell POSIX)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/bin/sh
# verif-client-tailscale.sh : contrôle d'un client Tailscale (Debian, Alpine...) pour home.arpa
# En local : sh verif-client-tailscale.sh
# À distance : ssh utilisateur@hote 'sh -s' < verif-client-tailscale.sh
# POSIX (ash/dash) : fonctionne sur Alpine (busybox) comme sur Debian.
LAN4="192.168.0.205" # IP LAN de cwwk
TS4="100.64.0.2" # IP Tailscale de cwwk
TS6="fd7a:115c:a1e0::2" # IPv6 Tailscale de cwwk
RAND="test-$$" # nom aléatoire pour tester le wildcard
bad=0
direct=0 # 1 = nœud du LAN qui interroge Technitium directement (accept-dns=false)
ok() { printf ' [OK] %s\n' "$1"; }
warn() { printf ' [WARN] %s\n' "$1"; }
ko() { printf ' [KO] %s\n' "$1"; bad=$((bad + 1)); }
have() { command -v "$1" >/dev/null 2>&1; }
# kw : KO partout, sauf sur un hôte Proxmox (/etc/pve) où le magasin de certificats est géré à part
kw() { if [ -d /etc/pve ]; then warn "$1 [hôte Proxmox : non bloquant]"; else ko "$1"; fi; }
echo "== $(hostname) =="
if [ -r /etc/os-release ]; then
. /etc/os-release
echo " système : $PRETTY_NAME"
fi
# --- 1. Tailscale -----------------------------------------------------------
echo "-- 1. Tailscale"
if ! have tailscale; then
ko "commande tailscale absente"
echo " => $bad point(s) KO"
exit 0
fi
state=$(tailscale status --json 2>/dev/null | grep -m1 '"BackendState"' | sed 's/.*: *"\([^"]*\)".*/\1/')
if [ "$state" = "Running" ]; then
ok "BackendState : Running"
else
ko "BackendState : ${state:-illisible}"
fi
me4=$(tailscale ip -4 2>/dev/null | head -1)
me6=$(tailscale ip -6 2>/dev/null | head -1)
echo " IP Tailscale : ${me4:-?} ${me6:-}"
if have systemctl; then
echo " service tailscaled : $(systemctl is-enabled tailscaled 2>/dev/null) / $(systemctl is-active tailscaled 2>/dev/null)"
elif have rc-status; then
rc-status -a 2>/dev/null | grep -i tailscale | sed 's/^/ service : /'
fi
# --- 2. Configuration DNS ---------------------------------------------------
echo "-- 2. Configuration DNS"
prefs=$(tailscale debug prefs 2>/dev/null)
if [ -n "$prefs" ]; then
corp=$(printf '%s\n' "$prefs" | grep -m1 '"CorpDNS"' | tr -d ' ,')
case "$corp" in *true*) val=true ;; *false*) val=false ;; *) val= ;; esac
if [ "$me4" = "$TS4" ]; then
if [ "$val" = "false" ]; then ok "accept-dns désactivé (nœud Technitium : attendu)"; else ko "accept-dns=$val sur le nœud Technitium (attendu false)"; fi
elif [ "$val" = "true" ]; then
ok "accept-dns activé"
elif [ "$val" = "false" ] && grep -q "^nameserver $LAN4" /etc/resolv.conf 2>/dev/null; then
ok "accept-dns désactivé, DNS direct vers Technitium ($LAN4) : valide pour un nœud du LAN"
direct=1
else
ko "accept-dns=${val:-?} (attendu true)"
fi
else
warn "préférences illisibles (tailscale debug prefs demande les droits root)"
fi
echo " /etc/resolv.conf -> $(readlink -f /etc/resolv.conf 2>/dev/null)"
if have systemctl && systemctl is-active systemd-resolved >/dev/null 2>&1; then
ok "systemd-resolved actif"
else
warn "pas de systemd-resolved : Tailscale réécrit /etc/resolv.conf (à surveiller si le réseau n'est pas prêt au démarrage)"
fi
grep -E '^(nameserver|search)' /etc/resolv.conf 2>/dev/null | sed 's/^/ /'
tailscale dns status 2>/dev/null | grep -E 'home\.arpa|MagicDNS:' | sed 's/^/ /'
# --- 3. Résolution home.arpa ------------------------------------------------
echo "-- 3. Résolution home.arpa"
if [ "$me4" = "$TS4" ] || [ "$direct" = 1 ]; then want="$LAN4"; else want="$TS4"; fi
resolve() {
if getent ahosts "$1" >/dev/null 2>&1; then
getent ahosts "$1" | awk '$2=="STREAM"{printf "%s ", $1}'
else
getent hosts "$1" 2>/dev/null | awk '{printf "%s ", $1}'
fi
}
for n in cwwk dns "$RAND"; do
r=$(resolve "$n.home.arpa")
case "$r" in
"") ko "$n.home.arpa : aucune réponse" ;;
*"$want"*) ok "$n.home.arpa -> $r" ;;
*) ko "$n.home.arpa -> $r (attendu $want)" ;;
esac
done
# --- 4. HTTPS ---------------------------------------------------------------
echo "-- 4. HTTPS dns.home.arpa (certificat vérifié)"
if have curl; then
out=$(curl -s -m 6 -o /dev/null -w 'code=%{http_code} ip=%{remote_ip} ssl_verify=%{ssl_verify_result}' https://dns.home.arpa/ 2>/dev/null)
case "$out" in
*ssl_verify=20*) kw "$out (certificat non reconnu : CA absente ou différente de celle qui a signé)" ;;
*code=000*) kw "$out (connexion impossible)" ;;
*ssl_verify=0*) ok "$out" ;;
*) warn "$out" ;;
esac
else
warn "curl absent, test ignoré"
fi
if ls /etc/ssl/certs 2>/dev/null | grep -qi homearpa; then
ok "CA home.arpa présente dans /etc/ssl/certs"
else
warn "CA home.arpa non trouvée dans /etc/ssl/certs"
fi
# --- 5. Chemin vers cwwk ----------------------------------------------------
echo "-- 5. Chemin vers cwwk"
if [ "$me4" = "$TS4" ]; then
echo " (ce nœud est cwwk)"
else
if have timeout; then
p=$(timeout 12 tailscale ping -c 2 cwwk 2>&1 | tail -1)
else
p=$(tailscale ping -c 2 cwwk 2>&1 | tail -1)
fi
echo " $p"
case "$p" in
*DERP*) warn "passage par un relais DERP (pas de connexion directe)" ;;
*pong*) ok "réponse reçue" ;;
*) ko "pas de réponse" ;;
esac
fi
echo " => $bad point(s) KO"
verif-mesh.sh (à exécuter sur le poste Arch, lit le CSV SSH)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env bash
# verif-mesh.sh : lance verif-client-tailscale.sh sur chaque machine du CSV SSH qui a une IP Mesh
# Structure du CSV : Alias;User;IPouDomain;Mesh;Port_SSH;SSH_Key;Ping;Opt;IP4_6
#
# Usage :
# ./verif-mesh.sh # toutes les machines ayant une IP Mesh
# ./verif-mesh.sh alias # une seule machine
# SUDO='sudo -n ' ./verif-mesh.sh # exécute le script distant avec sudo sans mot de passe
# ./verif-mesh.sh | grep -E '^==|KO|WARN' # n'affiche que les problèmes
CSV="${CSV:-$HOME/.ssh/.ssh_servers}"
REMOTE="${REMOTE:-$HOME/verif-client-tailscale.sh}"
ONLY="${1:-}"
SUDO="${SUDO:-}"
[ -r "$CSV" ] || { echo "CSV introuvable : $CSV" >&2; exit 1; }
[ -r "$REMOTE" ] || { echo "Script distant introuvable : $REMOTE" >&2; exit 1; }
tail -n +2 "$CSV" | while IFS=';' read -r alias user host mesh port key ping opt ipv; do
[ -z "$alias" ] && continue
case "$alias" in \#*) continue ;; esac
[ -n "$ONLY" ] && [ "$alias" != "$ONLY" ] && continue
if [ -z "$mesh" ]; then
echo "== $alias : pas d'IP Mesh dans le CSV, ignoré"
continue
fi
key="${key/\$HOME/$HOME}"
key="${key/#\~/$HOME}"
echo
if ! ssh -o BatchMode=yes -o ConnectTimeout=6 -p "$port" -i "$key" \
"$user@$mesh" "${SUDO}sh -s" < "$REMOTE" 2>&1; then
echo "== $alias : connexion SSH impossible ($user@$mesh:$port)"
fi
done
