Post

Headscale Tailscale Technitium - Split Horizon Technitium pour `*.home.arpa` (LAN et tailnet) (Partie II)

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:443 et [::]:443) : les IP Tailscale sont déjà couvertes, et nginx sert aussi des sites publics sans lien avec home.arpa.
  • Des lignes listen pour 100.64.0.2 et [fd7a:115c:a1e0::2] ont été ajoutées dans les blocs concernés. Elles ne sont pas nécessaires (l’écoute sur 0.0.0.0 et [::] couvre déjà ces adresses) et peuvent empêcher nginx de démarrer si l’interface tailscale0 n’a pas encore son adresse au démarrage : à retirer, ou à compenser avec net.ipv4.ip_nonlocal_bind=1 et net.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.arpa restreignent 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 colonne Mesh, 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
Cet article est sous licence CC BY 4.0 par l'auteur.