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)

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:443 et [::]:443) : les IP Tailscale sont donc déjà couvertes.
  • Des lignes listen pour 100.64.0.2 et [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.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;

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}}’

Cet article est sous licence CC BY 4.0 par l'auteur.