Headscale Tailscale Technitium - Split Horizon Technitium pour `*.home.arpa` (LAN et tailnet) (Partie II)
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:…) sont conservées.
1. Résumé
| Sujet | État |
|---|---|
Réponse DNS de *.home.arpa selon l’origine du client |
En place : IP LAN pour le LAN, IP Tailscale pour le tailnet |
Wildcard * |
Converti en CNAME vers cwwk.home.arpa (l’APP ne peut pas être posé sur *) |
HTTPS *.home.arpa par le tunnel |
Fonctionnel (politique Headscale étendue au 443, CA installée sur les clients) |
| Exposition depuis Internet | DNS, NFS, console et API Technitium bloqués ; SSH par clé uniquement |
| Validation | Tests DNS et HTTPS sur 3 clients ; « tout est clean » confirmé après les derniers correctifs |
Objectif atteint : nom.home.arpa se résout vers une adresse joignable quel que soit l’endroit où se trouve le client.
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 → 100.64.0.2 et fd7a:115c:a1e0::2 ; aucun résolveur global |
| Clients de test | pc1 (LAN, Tailscale), yiak (distant), skrime (VPS IPv6 seul) |
Pare-feu de cwwk |
ufw : deny (incoming), allow (outgoing), deny (routed) |
Sur cwwk lui-même : 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).
3. Configuration finale
3.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.
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 appartiennent à la traduction d’adresses ("enableAddressTranslation": false) et n’ont aucun effet ici.
3.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 autres noms servis par nginx) |
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. 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.
3.3 nginx (cwwk)
- Écoute sur toutes les adresses (
0.0.0.0:443et[::]:443) : les IP Tailscale sont donc déjà couvertes. - Des lignes
listenpour100.64.0.2et[fd7a:115c:a1e0::2]ont été ajoutées dans les blocs concernés (sans effet négatif). - 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;
3.4 Headscale
Politique étendue pour autoriser le port 443 vers cwwk pour les machines du tailnet. Sans cette autorisation, curl renvoyait 000 : la connexion TCP n’aboutissait pas, alors que le DNS et le port 53 fonctionnaient déjà.
3.5 Autorité de certification
Le certificat racine homearpaCA.crt (public) a été installé dans le magasin système des clients qui font du HTTPS vers *.home.arpa : yiak, skrime et l’Alpine concernée, en plus des machines déjà équipées. 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 |
| Arch | sudo trust anchor --store homearpaCA.crt |
4. Validation
4.1 Résultats relevés pendant la session
| Test | Client | Résultat |
|---|---|---|
Résolution système de cwwk, dns, nom du wildcard (test-xxxxx) |
pc1 |
fd7a:115c:a1e0::2 et 100.64.0.2 |
| Idem | yiak |
fd7a:115c:a1e0::2 et 100.64.0.2 |
| Idem | skrime (VPS IPv6) |
fd7a:115c:a1e0::2 et 100.64.0.2 |
| TTL des réponses | tous | 299 à 300 s |
HTTPS dns.home.arpa, certificat vérifié |
pc1 |
200, ssl_verify=0, via l’IPv6 Tailscale |
HTTPS dns.home.arpa, certificat vérifié |
yiak, skrime |
ssl_verify=20 (CA absente), corrigé par l’installation de la CA |
| Requête DoH depuis le LAN | poste LAN | 192.168.0.205 |
| Source vue par Technitium pour l’UDP | pc1, yiak |
192.168.0.20, 100.64.0.6 (vraies adresses) : aucun besoin de network_mode: host |
4.2 Confirmation globale
Après les correctifs (politique Headscale, CA, écoute et ordre de démarrage de nginx), la validation d’ensemble a été confirmée (« tout est clean »). Les contrôles suivants font partie de la checklist de référence (section 7) et sont à reproduire après tout changement.
5. 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 ufw compte : les DENY sur 5380 et 8053 passent avant l’autorisation générale du LAN.
6. Enseignements et pièges rencontrés
| Piège | Explication |
|---|---|
networkGroupMap n’est pas ce qu’on croit |
Dans la config de l’app, c’est le bloc networks qui définit les réseaux nommés utilisables dans l’enregistrement APP. networkGroupMap ne sert qu’à la traduction d’adresses, désactivée ici. |
| Réponse vide de l’app | Un client classé dans le groupe par défaut (private/public), absent du Record Data, ne reçoit aucune adresse. |
APP impossible sur * |
Contournement : APP sur cwwk, puis * et les autres noms en CNAME vers cwwk.home.arpa. Le CNAME du wildcard est bien évalué par Split Horizon. |
dig avec deux types |
dig nom A AAAA ne garde que le dernier type (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. Un ssl_verify=20 signale seulement l’absence de la CA. |
| Test côté LAN lancé depuis un client distant | dig @192.168.0.205 depuis yiak expire : normal, il n’a pas de route vers le LAN. |
Client LAN avec accept-dns=true |
Il reçoit l’IP Tailscale de cwwk comme un client distant, et passe donc par le tunnel. Une connexion directe sur le LAN reste vérifiable avec tailscale ping. |
7. Checklist de vérification (à rejouer après un changement)
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env bash
echo "== Résolution système =="
for n in cwwk dns "test-$RANDOM"; do
printf '%-14s ' "$n"
getent ahosts "$n.home.arpa" | awk '$2=="STREAM"{printf "%s ", $1}'
echo
done
echo; echo "== HTTPS (certificat vérifié) =="
curl -s -m 5 -o /dev/null \
-w 'code=%{http_code} ip=%{remote_ip} ssl_verify=%{ssl_verify_result}\n' \
https://dns.home.arpa/
echo; echo "== TTL =="
dig +noall +answer cwwk.home.arpa A | awk '{print $2" s"}'
| Client | Résolution attendue | HTTPS attendu |
|---|---|---|
Tailnet (yiak, pc1, skrime) |
100.64.0.2 et fd7a:115c:a1e0::2 |
code ≠ 000, ssl_verify=0 |
| Poste du LAN sans Tailscale | 192.168.0.205 |
code ≠ 000, ip=192.168.0.205, ssl_verify=0 |
Autres contrôles utiles :
1
2
3
4
5
6
tailscale ping cwwk # "via 192.168.x.x" (direct) et non "via DERP"
dig @100.64.0.2 cwwk.home.arpa A +short # 100.64.0.2
dig @100.64.0.2 cwwk.home.arpa AAAA +short # fd7a:115c:a1e0::2
dig @192.168.0.205 cwwk.home.arpa A +short # 192.168.0.205 (depuis le LAN)
sudo ss -tlnp | grep ':443' # nginx à l'écoute
sudo tail -n 5 /var/log/nginx/access.log # source réelle des requêtes
Après un redémarrage de cwwk : nginx et Technitium actifs,
docker inspect -f ‘liquid{{.HostConfig.RestartPolicy.Name}}’ technitium
en unless-stopped ou always, et l’app Split Horizon avec ses réseaux et enregistrements toujours présents.
8. Points ouverts et évolutions possibles
| Sujet | Détail |
|---|---|
| DoH distant | Technitium voit l’IP du proxy (classée lan) pour les requêtes DoH, donc un client DoH distant recevrait l’IP LAN. Sans objet tant que le DoH reste local. Pour le distinguer : proxy_set_header X-Real-IP $remote_addr; côté nginx, et 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 des machines du LAN autres que cwwk doivent être joignables à distance : --advertise-routes=192.168.0.0/24, approbation Headscale, ip_forward, et règle ufw route allow in on tailscale0 out on <interface_lan> (ufw est en deny (routed)). |
| Nouveaux noms | Tout nouveau nom servi par nginx passe par le wildcard sans rien créer. Un nom qui a besoin de son propre enregistrement doit être en CNAME vers cwwk.home.arpa. Brave exige que le nom figure explicitement dans le SAN du certificat : à vérifier lors du renouvellement. |
| Noms inconnus | Le wildcard répond à tout nom : une faute de frappe donne une réponse valide (nginx répond) au lieu de NXDOMAIN. |
| TTL | Fixé à 300 s : bon compromis entre réactivité et charge. À remonter si les changements sont rares. |
| Sauvegarde | Faire une sauvegarde de Technitium (Settings → Backup Settings, zone et apps) après chaque évolution, et la conserver hors de cwwk. |
Retour arrière
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. Les clients retrouvent alors la même réponse partout.
Annexe : commandes utiles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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'
# SSH
sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin|pubkeyauthentication'
# DNS
dig @100.64.0.2 <nom>.home.arpa A +short
getent ahosts <nom>.home.arpa
Pare-feu et ports (cwwk)
docker ps –format ‘{{.Names}} {{.Ports}}’
