Post

DNS Nebula avec Technitium

DNS Nebula avec Technitium

Nebula a besoin de noms simples pour vos hôtes. La frappe ssh db-01.dn.example.comdans un terminal ou web-frontend.dn.example.comdans un navigateur est préféré à la mémorisation 100.100.1.43ou fdef:c0:c0:2966:d3bf:1a96:fa72:db82. Configuration avec l’API Defined Networking et le fournisseur DNS Technitium.

Technitium API

Créer un token

Depuis l’interface Web

1.Se connecter à la console Web Technitium en admin.

  1. Crée de préférence un utilisateur dédié à la synchronisation, dns-sync
  2. Donnez-lui uniquement les droits nécessaires sur la zone DNS :

    • View pour lire les enregistrements ;
    • Modify pour les créer ou modifier ;
    • Delete pour supprimer les anciennes adresses.
  3. Se connecter avec l’utilisateur. dns-sync
  4. Clique sur son nom, en haut à droite.
  5. Choisis Create API Token.
  6. Saisis le mot de passe du compte.
  7. Donne un nom au token, par exemple defined-networking-sync.
  8. Clique sur Create.
  9. Copie immédiatement le token et conserve-le dans un gestionnaire de secrets.

Le token n’est généralement affiché qu’une seule fois. La création depuis le menu utilisateur est également documentée par plusieurs intégrations Technitium.[poshac][homarr]

Export URL + Token

1
2
export TECHNITIUM_URL="https://dns.rnmkcy.eu"
export TECHNITIUM_TOKEN="le_token_copié"

Puis l’appel API doit transmettre le token ainsi :

1
2
3
4
headers: {
  Authorization: `Bearer ${TECHNITIUM_TOKEN}`,
  Accept: "application/json",
}

Technitium utilise également un endpoint de connexion /api/user/login qui retourne un token de session, mais ce token est soumis à la durée de session. Pour un script automatisé, préfère un API token permanent ou dédié plutôt qu’un token de session.[docs]

Tester le token

Teste le token avec une requête sans modification :

1
2
3
curl -i \
  -H "Authorization: Bearer ${TECHNITIUM_TOKEN}" \
  "${TECHNITIUM_URL}/api/user/whoami"

Ou teste directement la lecture de la zone :

1
2
3
4
curl -G \
  -H "Authorization: Bearer ${TECHNITIUM_TOKEN}" \
  --data-urlencode "zone=homr.arpa" \
  "${TECHNITIUM_URL}/api/zones/records/list"

Évite d’utiliser le compte admin dans le script de production : un utilisateur dns-sync limité à la zone concernée est plus sûr.### Obtenez des informations de session

Lister les sous-domaines home.arpa

1
2
3
4
curl -sS -G \
  -H "Authorization: Bearer ${TECHNITIUM_TOKEN}" \
  --data-urlencode "zone=home.arpa" \
  "${TECHNITIUM_URL}/api/zones/export"

Réponse

1
2
3
4
5
6
7
8
9
10
11
$ORIGIN home.arpa.
@                     900       IN  SOA           dns.rnmkcy.eu. hostadmin 9 900 300 604800 900
@                     14400     IN  NS            dns.rnmkcy.eu.
cwwk                  86400     IN  AAAA          2a01:e0a:95a:e2f0:aab8:e0ff:fe04:ec45
cwwk                  86400     IN  A             192.168.0.205
ged                   86400     IN  A             192.168.0.205
link                  86400     IN  A             192.168.0.205
lldap                 86400     IN  A             192.168.0.205
portainer             86400     IN  A             192.168.0.205
pve                   86400     IN  A             192.168.0.215
site                  86400     IN  A             192.168.0.205

Lister les noms DNS de la zone

1
2
3
4
5
6
7
8
9
10
curl -sS -G \
  -H "Authorization: Bearer ${TECHNITIUM_TOKEN}" \
  --data-urlencode "zone=home.arpa" \
  "${TECHNITIUM_URL}/api/zones/export" |
  awk '
    /^[[:space:]]*;/ { next }
    NF >= 2 { print $1 }
  ' |
  sed 's/\.$//' |
  sort -u

Réponse

1
2
3
4
5
6
7
8
9
@
$ORIGIN
cwwk
ged
link
lldap
portainer
pve
site

Nebula DNS zone home.arpa

Ce guide passe à travers la récupération de vos hôtes de l’API et la synchronisation en tant qu’enregistrements DNS à Netlify DNS. Le même concept fonctionne avec n’importe quel fournisseur DNS qui possède une API, par ex. Cloudflare, Route 53 ou Google Cloud DNS. Un résolveur auto-hébergé comme CoreDNS fonctionne aussi, avec un chemin d’écriture différent; plus sur cela à la fin.

Remarque: Ce guide synchronise les noms d’hôte avec les noms DNS publics. Une énumération de votre domaine pourrait découvrir vos noms d’hôte, mais avec Nebula, votre trafic est crypté et les adresses IP internes ne sont d’aucune utilité pour les étrangers. Envisagez de consulter vos noms d’hôtes pour obtenir des renseignements personnels.

Ce dont vous aurez besoin

  • Quelque part pour exécuter la synchronisation qui a Node.js 20 ou plus récent disponible (nous allons d’abord tester localement, puis mettre en place un calendrier). Un flux de travail GitHub Actions, un travail de cron ou une autre solution 1
  • Un compte de réseau défini avec les hôtes inscrits
  • Une clé API avec le hosts:listet hosts:updatechamps d’application
  • Un domaine géré parTechnitium, home.arpa
  • Un jeton d’accès personnel Technitium

Clé API Nebula

Dans le panneau d’administration, créez une clé API avec les portées hosts:list et hosts:update. hosts:listpermet au script de lire vos hôtes; hosts:updatepermet à la balise de script chaque hôte qui a un enregistrement DNS, ce qui permet de voir facilement quels hôtes ont été traités avec succès. Cette deuxième portée signifie que la clé peut modifier les hôtes, pas seulement les lister, alors gardez-le dans un magasin secret 22. Si vous préférez que la clé reste en lecture seule, créez-la avec hosts:listseul et supprimer la boucle de marquage à la fin du script. Laissez l’expiration aussi. Le formulaire est par défaut à 30 jours et la date ne peut pas être changée plus tard; une clé qui fuit ne reste utile que jusqu’à son expiration, et une synchronisation qui fonctionne sur un horaire vous indiquera haut et fort quand il est temps d’en faire un nouveau. Copiez la clé dans un endroit sûr; vous ne la verrez qu’une fois. Enregistrez-le dans une variable pour les étapes suivantes: Fenêtre terminale

1
export DN_API_KEY="dnkey-XXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXX..."

Répertorier les hôtes

1
2
curl -s -H "Authorization: Bearer $DN_API_KEY" \
  https://api.defined.net/v2/hosts | jq '.data[] | {name, ipAddresses}'

Résultat

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
{
  "name": "PC1",
  "ipAddresses": [
    "10.139.100.28",
    "fdef:c0:c0:f38:3a0f:8e11:7a0:12e8"
  ]
}
{
  "name": "a52s",
  "ipAddresses": [
    "10.139.100.26",
    "fdef:c0:c0:f38:3a0f:47fd:8445:3760"
  ]
}
{
  "name": "cwwk",
  "ipAddresses": [
    "10.139.100.1",
    "fdef:c0:c0:f38:3a0f:d5f9:79a8:1"
  ]
}
{
  "name": "e6230",
  "ipAddresses": [
    "10.139.100.25",
    "fdef:c0:c0:f38:3a0f:dd5b:fb1d:4153"
  ]
}
{
  "name": "s8",
  "ipAddresses": [
    "10.139.100.27",
    "fdef:c0:c0:f38:3a0f:9645:5127:2a54"
  ]
}
{
  "name": "skrime",
  "ipAddresses": [
    "fdef:c0:c0:f38:3a0f:84c1:b98b:21c6"
  ]
}
{
  "name": "sxb",
  "ipAddresses": [
    "10.139.100.20",
    "fdef:c0:c0:f38:3a0f:1d3a:1951:4b7c"
  ]
}
{
  "name": "xoyaz",
  "ipAddresses": [
    "10.139.100.21",
    "fdef:c0:c0:f38:3a0f:8e4e:c20d:75a3"
  ]
}
{
  "name": "yannig",
  "ipAddresses": [
    "10.139.100.23",
    "fdef:c0:c0:f38:3a0f:7662:d71c:ab60"
  ]
}
{
  "name": "yannir",
  "ipAddresses": [
    "10.139.100.22",
    "fdef:c0:c0:f38:3a0f:2288:21e2:190b"
  ]
}
{
  "name": "yiak",
  "ipAddresses": [
    "10.139.100.24",
    "fdef:c0:c0:f38:3a0f:f1da:c483:c423"
  ]
}

Script de synchronisation

Ce script récupère tous vos hôtes à partir de l’API Defined Networking, convertit les noms d’hôtes en étiquettes DNS 3 valides3. Il les compare ensuite aux enregistrements DNS actuels dans Netlify, et crée ou supprime des enregistrements pour les garder synchronisés, après quoi il balise chaque hôte qui obtient un enregistrement dns:synced. Les hôtes qui n’ont pas été vus par le script ou dont les noms ne peuvent pas être convertis en étiquettes DNS n’obtiendront pas la balise.

Il faut remplacer les appels Netlify par des appels à l’API HTTP Technitium. Ton nsupdate fonctionne pour une mise à jour ponctuelle, mais pour reproduire le comportement complet du script — ajout des nouvelles adresses et suppression des anciennes — l’API Technitium est plus adaptée.

Technitium expose notamment les endpoints /api/zones/records/add et /api/zones/records/delete, avec authentification par jeton Bearer.[github]

Voici le script Node.js complet avec :

  • synchronisation Defined Networking → Technitium ;
  • création et suppression des enregistrements A/AAAA ;
  • synchronisation du tag dns:synced ;
  • mode --dry-run ;
  • protection supplémentaire avec APPLY=1 ;
  • gestion de la réponse Technitium sous response.records ;
  • support des réponses texte pour l’export, même si l’export n’est pas utilisé ici.

Le serveur Technitium fournit une API HTTP permettant d’effectuer les mêmes opérations que sa console Web, notamment la gestion des enregistrements DNS.[github][deepwiki]

Enregistrer sous sync-dns.mjs :

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
#!/usr/bin/env node

import { domainToASCII, domainToUnicode } from "node:url";

/*
 * Utilisation :
 *
 * Dry-run, recommandé en premier :
 *   node sync-dns.mjs --dry-run
 *
 * Application réelle :
 *   APPLY=1 node sync-dns.mjs
 *
 * Variables obligatoires :
 *   DN_API_KEY
 *   TECHNITIUM_TOKEN
 *   TECHNITIUM_URL
 *   DOMAIN
 *
 * Variables facultatives :
 *   SUBDOMAIN=dn
 *   TTL=300
 */

const DRY_RUN =
  process.argv.includes("--dry-run") ||
  process.env.APPLY !== "1";

const DN_API_KEY = required(
  "DN_API_KEY",
  "your Defined Networking API key"
);

const TECHNITIUM_TOKEN = required(
  "TECHNITIUM_TOKEN",
  "your Technitium API token"
);

const TECHNITIUM_URL = (
  process.env.TECHNITIUM_URL || "http://127.0.0.1:5380"
).replace(/\/+$/, "");

const DOMAIN = required(
  "DOMAIN",
  "the authoritative Technitium DNS zone"
);

const SUBDOMAIN = process.env.SUBDOMAIN || "dn";
const TTL = parsePositiveInteger(process.env.TTL || "300", "TTL");

const SYNCED_TAG = "dns:synced";

console.log(
  DRY_RUN
    ? "Mode dry-run activé : aucune modification ne sera appliquée."
    : "Mode réel activé : les modifications seront appliquées."
);

function required(name, description) {
  const value = process.env[name];

  if (!value) {
    console.error(`Variable ${name} absente : définis ${description}`);
    process.exit(1);
  }

  return value;
}

function parsePositiveInteger(value, name) {
  const number = Number(value);

  if (!Number.isInteger(number) || number <= 0) {
    console.error(`${name} doit être un entier positif`);
    process.exit(1);
  }

  return number;
}

function errorMessage(body, fallback = "erreur inconnue") {
  if (!body) return fallback;

  if (typeof body === "string") {
    return body;
  }

  return (
    body.errorMessage ||
    body.message ||
    body.error ||
    fallback
  );
}

async function request(url, init = {}) {
  const response = await fetch(url, init);
  const text = await response.text();

  let body = null;

  if (text) {
    try {
      body = JSON.parse(text);
    } catch {
      body = text;
    }
  }

  if (!response.ok) {
    throw new Error(
      `HTTP ${response.status} pour ${url}: ${errorMessage(body)}`
    );
  }

  if (
    body &&
    typeof body === "object" &&
    body.status &&
    body.status !== "ok"
  ) {
    throw new Error(
      `Erreur API pour ${url}: ${errorMessage(body, body.status)}`
    );
  }

  return body;
}

async function definedNetworking(path, init = {}) {
  return request(`https://api.defined.net${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${DN_API_KEY}`,
      "Content-Type": "application/json",
      Accept: "application/json",
      ...(init.headers || {}),
    },
  });
}

async function technitium(path, params = {}) {
  const url = new URL(
    `/api/${path}`,
    `${TECHNITIUM_URL}/`
  );

  for (const [key, value] of Object.entries(params)) {
    if (value !== undefined && value !== null) {
      url.searchParams.set(key, String(value));
    }
  }

  return request(url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${TECHNITIUM_TOKEN}`,
      Accept: "application/json",
    },
  });
}

function warning(message) {
  console.warn(`::warning::${message}`);
}

function shown(hostname) {
  const unicode = domainToUnicode(hostname);

  return unicode && unicode !== hostname
    ? `${hostname} (${unicode})`
    : hostname;
}

function toLabel(name) {
  const APOSTROPHES = /['‘’ʼ]/g;
  const JUNK =
    /[^\p{L}\p{N}\p{Extended_Pictographic}-]+/gu;
  const LEGAL_LABEL =
    /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;

  let kept = String(name)
    .normalize("NFKC")
    .toLowerCase();

  if (!kept.startsWith("xn--")) {
    kept = kept
      .replace(APOSTROPHES, "")
      .replace(JUNK, (run, offset, source) => {
        const atStart = offset === 0;
        const atEnd = offset + run.length === source.length;
        const nextToExistingDash =
          source[offset - 1] === "-" ||
          source[offset + run.length] === "-";

        return atStart || atEnd || nextToExistingDash
          ? ""
          : "-";
      });
  }

  const label = /^[a-z0-9-]*$/.test(kept)
    ? kept
    : domainToASCII(kept);

  return LEGAL_LABEL.test(label) ? label : "";
}

function absoluteHostname(label) {
  return `${label}.${SUBDOMAIN}.${DOMAIN}`
    .replace(/\.+$/, "")
    .toLowerCase();
}

function recordKey(record) {
  return [
    record.type.toUpperCase(),
    record.domain.toLowerCase().replace(/\.$/, ""),
    record.value,
  ].join(" ");
}

function normalizeRecord(record) {
  const domain = String(
    record.domain ||
    record.name ||
    record.nameServer ||
    ""
  )
    .replace(/\.$/, "")
    .toLowerCase();

  const type = String(record.type || "").toUpperCase();

  let value =
    record.value ||
    record.ipAddress ||
    record.address ||
    "";

  if (typeof value !== "string") {
    value = String(value);
  }

  return {
    domain,
    type,
    value,
  };
}

function extractRecords(response) {
  const records =
    response?.response?.records ||
    response?.records ||
    [];

  if (!Array.isArray(records)) {
    throw new Error(
      "Réponse Technitium inattendue : response.records absent ou invalide"
    );
  }

  return records
    .map(normalizeRecord)
    .filter(
      (record) =>
        record.domain &&
        (record.type === "A" || record.type === "AAAA") &&
        record.value
    );
}

function getHostAddresses(host) {
  const addresses = Array.isArray(host.ipAddresses)
    ? host.ipAddresses
    : [];

  return addresses
    .filter(Boolean)
    .map((value) => String(value).trim())
    .filter(Boolean);
}

function addressType(value) {
  return value.includes(":") ? "AAAA" : "A";
}

async function getRecordsForDomain(domain) {
  const response = await technitium("zones/records/get", {
    zone: DOMAIN,
    domain,
  });

  return extractRecords(response);
}

async function addRecord(record) {
  console.log(
    `${DRY_RUN ? "[dry-run] " : ""}` +
    `Création: ${record.type} ${shown(record.domain)} -> ${record.value}`
  );

  if (DRY_RUN) {
    return;
  }

  const params = {
    zone: DOMAIN,
    domain: record.domain,
    type: record.type,
    ttl: TTL,
  };

  if (record.type === "A" || record.type === "AAAA") {
    params.ipAddress = record.value;
  }

  await technitium("zones/records/add", params);
}

async function deleteRecord(record) {
  console.log(
    `${DRY_RUN ? "[dry-run] " : ""}` +
    `Suppression: ${record.type} ` +
    `${shown(record.domain)} -> ${record.value}`
  );

  if (DRY_RUN) {
    return;
  }

  await technitium("zones/records/delete", {
    zone: DOMAIN,
    domain: record.domain,
    type: record.type,
    value: record.value,
  });
}

async function updateHostTags(host, tags, shouldBeTagged) {
  console.log(
    `${DRY_RUN ? "[dry-run] " : ""}` +
    `${shouldBeTagged ? "Ajout" : "Suppression"} du tag ` +
    `"${SYNCED_TAG}" pour "${host.name}"`
  );

  if (DRY_RUN) {
    return;
  }

  const {
    name,
    roleID,
    staticAddresses,
    listenPort,
    configOverrides,
  } = host;

  await definedNetworking(`/v3/hosts/${host.id}`, {
    method: "PUT",
    body: JSON.stringify({
      name,
      roleID,
      staticAddresses,
      listenPort,
      configOverrides,
      tags,
    }),
  });
}

// -----------------------------------------------------------------------------
// 1. Récupération des hôtes Defined Networking avec pagination
// -----------------------------------------------------------------------------

const hosts = [];
let cursor = "";

do {
  const path = cursor
    ? `/v2/hosts?cursor=${encodeURIComponent(cursor)}`
    : "/v2/hosts";

  const page = await definedNetworking(path);

  if (!Array.isArray(page?.data)) {
    throw new Error(
      "Réponse Defined Networking inattendue : data absent ou invalide"
    );
  }

  hosts.push(...page.data);

  cursor = page.metadata?.hasNextPage
    ? page.metadata.nextCursor || ""
    : "";
} while (cursor);

console.log(`${hosts.length} hôte(s) Defined Networking récupéré(s)`);

// -----------------------------------------------------------------------------
// 2. Conversion des noms en labels DNS
// -----------------------------------------------------------------------------

const labeled = hosts.map((host) => ({
  ...host,
  label: toLabel(host.name),
}));

for (const host of labeled.filter((item) => !item.label)) {
  warning(
    `"${host.name}" ne produit pas un label DNS valide : hôte ignoré`
  );
}

// Détection des collisions de labels.
const labels = new Map();

for (const host of labeled.filter((item) => item.label)) {
  const names = labels.get(host.label) || [];
  names.push(host.name);
  labels.set(host.label, names);
}

for (const [label, names] of labels) {
  if (names.length > 1) {
    warning(
      `${names.map((name) => `"${name}"`).join(", ")} ` +
      `produisent tous le label DNS "${label}"`
    );
  }
}

// -----------------------------------------------------------------------------
// 3. Construction de l'ensemble DNS désiré
// -----------------------------------------------------------------------------

const desired = labeled
  .filter((host) => host.label)
  .flatMap((host) => {
    const domain = absoluteHostname(host.label);

    return getHostAddresses(host).map((value) => ({
      domain,
      type: addressType(value),
      value,
    }));
  });

const desiredByDomain = new Map();

for (const record of desired) {
  const records = desiredByDomain.get(record.domain) || [];
  records.push(record);
  desiredByDomain.set(record.domain, records);
}

console.log(
  `${desired.length} enregistrement(s) DNS désiré(s)`
);

// -----------------------------------------------------------------------------
// 4. Lecture des enregistrements existants
//
// Technitium retourne les enregistrements du domaine demandé, mais pas
// nécessairement tous les descendants. On interroge donc chaque domaine géré.
// -----------------------------------------------------------------------------

const managedDomains = [
  ...new Set(desired.map((record) => record.domain)),
];

const existing = [];

for (const domain of managedDomains) {
  const records = await getRecordsForDomain(domain);
  existing.push(...records);
}

// Déduplication.
const existingByKey = new Map();

for (const record of existing) {
  existingByKey.set(recordKey(record), record);
}

const uniqueExisting = [...existingByKey.values()];

const desiredKeys = new Set(desired.map(recordKey));
const existingKeys = new Set(uniqueExisting.map(recordKey));

// -----------------------------------------------------------------------------
// 5. Suppression des anciennes adresses
// -----------------------------------------------------------------------------

let additions = 0;
let deletions = 0;
let tagChanges = 0;

for (const record of uniqueExisting) {
  if (!desiredKeys.has(recordKey(record))) {
    deletions++;
    await deleteRecord(record);
  }
}

// -----------------------------------------------------------------------------
// 6. Création des nouvelles adresses
// -----------------------------------------------------------------------------

for (const record of desired) {
  if (!existingKeys.has(recordKey(record))) {
    additions++;
    await addRecord(record);
  }
}

// -----------------------------------------------------------------------------
// 7. Synchronisation des tags Defined Networking
// -----------------------------------------------------------------------------

for (const host of labeled) {
  const shouldBeTagged = Boolean(host.label);
  const currentTags = Array.isArray(host.tags)
    ? host.tags
    : [];

  const isTagged = currentTags.includes(SYNCED_TAG);

  if (isTagged === shouldBeTagged) {
    continue;
  }

  const tags = shouldBeTagged
    ? [...new Set([...currentTags, SYNCED_TAG])]
    : currentTags.filter((tag) => tag !== SYNCED_TAG);

  tagChanges++;

  await updateHostTags(
    host,
    tags,
    shouldBeTagged
  );
}

// -----------------------------------------------------------------------------
// 8. Résumé
// -----------------------------------------------------------------------------

console.log("");
console.log(
  DRY_RUN
    ? "Dry-run terminé."
    : "Synchronisation terminée."
);
console.log(`Enregistrements à créer : ${additions}`);
console.log(`Enregistrements à supprimer : ${deletions}`);
console.log(`Tags à modifier : ${tagChanges}`);

if (DRY_RUN) {
  console.log("Aucune modification n'a été appliquée.");
}

Configuration

Les variables exportées

1
2
3
4
5
6
export DN_API_KEY="votre_cle_defined_networking"
export TECHNITIUM_URL="https://dns.rnmkcy.eu"
export TECHNITIUM_TOKEN="votre_token_technitium"
export DOMAIN="home.arpa"
export SUBDOMAIN="dn"
export TTL="300"

Le serveur Technitium sera alors interrogé pour des noms comme :

1
2
3
laptop.dn.home.arpa
server.dn.home.arpa
phone.dn.home.arpa

Exécution recommandée

Commencer par un dry-run :

1
node sync-dns.mjs --dry-run

Le script lira les hôtes et les enregistrements, mais n’exécutera aucun add, delete ni PUT.

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
Mode dry-run activé : aucune modification ne sera appliquée.
11 hôte(s) Defined Networking récupéré(s)
21 enregistrement(s) DNS désiré(s)
[dry-run] Création: A pc1.dn.home.arpa -> 10.139.100.28
[dry-run] Création: AAAA pc1.dn.home.arpa -> fdef:c0:c0:f38:3a0f:8e11:7a0:12e8
[dry-run] Création: A a52s.dn.home.arpa -> 10.139.100.26
[dry-run] Création: AAAA a52s.dn.home.arpa -> fdef:c0:c0:f38:3a0f:47fd:8445:3760
[dry-run] Création: A cwwk.dn.home.arpa -> 10.139.100.1
[dry-run] Création: AAAA cwwk.dn.home.arpa -> fdef:c0:c0:f38:3a0f:d5f9:79a8:1
[dry-run] Création: A e6230.dn.home.arpa -> 10.139.100.25
[dry-run] Création: AAAA e6230.dn.home.arpa -> fdef:c0:c0:f38:3a0f:dd5b:fb1d:4153
[dry-run] Création: A s8.dn.home.arpa -> 10.139.100.27
[dry-run] Création: AAAA s8.dn.home.arpa -> fdef:c0:c0:f38:3a0f:9645:5127:2a54
[dry-run] Création: AAAA skrime.dn.home.arpa -> fdef:c0:c0:f38:3a0f:84c1:b98b:21c6
[dry-run] Création: A sxb.dn.home.arpa -> 10.139.100.20
[dry-run] Création: AAAA sxb.dn.home.arpa -> fdef:c0:c0:f38:3a0f:1d3a:1951:4b7c
[dry-run] Création: A xoyaz.dn.home.arpa -> 10.139.100.21
[dry-run] Création: AAAA xoyaz.dn.home.arpa -> fdef:c0:c0:f38:3a0f:8e4e:c20d:75a3
[dry-run] Création: A yannig.dn.home.arpa -> 10.139.100.23
[dry-run] Création: AAAA yannig.dn.home.arpa -> fdef:c0:c0:f38:3a0f:7662:d71c:ab60
[dry-run] Création: A yannir.dn.home.arpa -> 10.139.100.22
[dry-run] Création: AAAA yannir.dn.home.arpa -> fdef:c0:c0:f38:3a0f:2288:21e2:190b
[dry-run] Création: A yiak.dn.home.arpa -> 10.139.100.24
[dry-run] Création: AAAA yiak.dn.home.arpa -> fdef:c0:c0:f38:3a0f:f1da:c483:c423
[dry-run] Ajout du tag "dns:synced" pour "PC1"
[dry-run] Ajout du tag "dns:synced" pour "a52s"
[dry-run] Ajout du tag "dns:synced" pour "cwwk"
[dry-run] Ajout du tag "dns:synced" pour "e6230"
[dry-run] Ajout du tag "dns:synced" pour "s8"
[dry-run] Ajout du tag "dns:synced" pour "skrime"
[dry-run] Ajout du tag "dns:synced" pour "sxb"
[dry-run] Ajout du tag "dns:synced" pour "xoyaz"
[dry-run] Ajout du tag "dns:synced" pour "yannig"
[dry-run] Ajout du tag "dns:synced" pour "yannir"
[dry-run] Ajout du tag "dns:synced" pour "yiak"

Dry-run terminé.
Enregistrements à créer : 21
Enregistrements à supprimer : 0
Tags à modifier : 11
Aucune modification n'a été appliquée.

Pour appliquer les modifications :

1
APPLY=1 node sync-dns.mjs

La variable APPLY=1 est volontairement obligatoire pour le mode réel. Sans elle, le script reste en dry-run même si l’option --dry-run n’est pas fournie.

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
Mode réel activé : les modifications seront appliquées.
11 hôte(s) Defined Networking récupéré(s)
21 enregistrement(s) DNS désiré(s)
Création: A pc1.dn.home.arpa -> 10.139.100.28
Création: AAAA pc1.dn.home.arpa -> fdef:c0:c0:f38:3a0f:8e11:7a0:12e8
Création: A a52s.dn.home.arpa -> 10.139.100.26
Création: AAAA a52s.dn.home.arpa -> fdef:c0:c0:f38:3a0f:47fd:8445:3760
Création: A cwwk.dn.home.arpa -> 10.139.100.1
Création: AAAA cwwk.dn.home.arpa -> fdef:c0:c0:f38:3a0f:d5f9:79a8:1
Création: A e6230.dn.home.arpa -> 10.139.100.25
Création: AAAA e6230.dn.home.arpa -> fdef:c0:c0:f38:3a0f:dd5b:fb1d:4153
Création: A s8.dn.home.arpa -> 10.139.100.27
Création: AAAA s8.dn.home.arpa -> fdef:c0:c0:f38:3a0f:9645:5127:2a54
Création: AAAA skrime.dn.home.arpa -> fdef:c0:c0:f38:3a0f:84c1:b98b:21c6
Création: A sxb.dn.home.arpa -> 10.139.100.20
Création: AAAA sxb.dn.home.arpa -> fdef:c0:c0:f38:3a0f:1d3a:1951:4b7c
Création: A xoyaz.dn.home.arpa -> 10.139.100.21
Création: AAAA xoyaz.dn.home.arpa -> fdef:c0:c0:f38:3a0f:8e4e:c20d:75a3
Création: A yannig.dn.home.arpa -> 10.139.100.23
Création: AAAA yannig.dn.home.arpa -> fdef:c0:c0:f38:3a0f:7662:d71c:ab60
Création: A yannir.dn.home.arpa -> 10.139.100.22
Création: AAAA yannir.dn.home.arpa -> fdef:c0:c0:f38:3a0f:2288:21e2:190b
Création: A yiak.dn.home.arpa -> 10.139.100.24
Création: AAAA yiak.dn.home.arpa -> fdef:c0:c0:f38:3a0f:f1da:c483:c423
Ajout du tag "dns:synced" pour "PC1"
Ajout du tag "dns:synced" pour "a52s"
Ajout du tag "dns:synced" pour "cwwk"
Ajout du tag "dns:synced" pour "e6230"
Ajout du tag "dns:synced" pour "s8"
Ajout du tag "dns:synced" pour "skrime"
Ajout du tag "dns:synced" pour "sxb"
Ajout du tag "dns:synced" pour "xoyaz"
Ajout du tag "dns:synced" pour "yannig"
Ajout du tag "dns:synced" pour "yannir"
Ajout du tag "dns:synced" pour "yiak"

Synchronisation terminée.
Enregistrements à créer : 21
Enregistrements à supprimer : 0
Tags à modifier : 11

Technitium: Lister les zones après modification

1
2
3
4
curl -sS -G \
  -H "Authorization: Bearer ${TECHNITIUM_TOKEN}" \
  --data-urlencode "zone=home.arpa" \
  "${TECHNITIUM_URL}/api/zones/export"

home.arpa

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
$ORIGIN home.arpa.
@                     900       IN  SOA           dns.rnmkcy.eu. hostadmin 30 900 300 604800 900
@                     14400     IN  NS            dns.rnmkcy.eu.
cwwk                  86400     IN  AAAA          2a01:e0a:95a:e2f0:aab8:e0ff:fe04:ec45
cwwk                  86400     IN  A             192.168.0.205
a52s.dn               300       IN  AAAA          fdef:c0:c0:f38:3a0f:47fd:8445:3760
a52s.dn               300       IN  A             10.139.100.26
cwwk.dn               300       IN  AAAA          fdef:c0:c0:f38:3a0f:d5f9:79a8:1
cwwk.dn               300       IN  A             10.139.100.1
e6230.dn              300       IN  AAAA          fdef:c0:c0:f38:3a0f:dd5b:fb1d:4153
e6230.dn              300       IN  A             10.139.100.25
pc1.dn                300       IN  AAAA          fdef:c0:c0:f38:3a0f:8e11:7a0:12e8
pc1.dn                300       IN  A             10.139.100.28
s8.dn                 300       IN  AAAA          fdef:c0:c0:f38:3a0f:9645:5127:2a54
s8.dn                 300       IN  A             10.139.100.27
skrime.dn             300       IN  AAAA          fdef:c0:c0:f38:3a0f:84c1:b98b:21c6
sxb.dn                300       IN  AAAA          fdef:c0:c0:f38:3a0f:1d3a:1951:4b7c
sxb.dn                300       IN  A             10.139.100.20
xoyaz.dn              300       IN  AAAA          fdef:c0:c0:f38:3a0f:8e4e:c20d:75a3
xoyaz.dn              300       IN  A             10.139.100.21
yannig.dn             300       IN  AAAA          fdef:c0:c0:f38:3a0f:7662:d71c:ab60
yannig.dn             300       IN  A             10.139.100.23
yannir.dn             300       IN  AAAA          fdef:c0:c0:f38:3a0f:2288:21e2:190b
yannir.dn             300       IN  A             10.139.100.22
yiak.dn               300       IN  AAAA          fdef:c0:c0:f38:3a0f:f1da:c483:c423
yiak.dn               300       IN  A             10.139.100.24
ged                   86400     IN  A             192.168.0.205
link                  86400     IN  A             192.168.0.205
lldap                 86400     IN  A             192.168.0.205
portainer             86400     IN  A             192.168.0.205
pve                   86400     IN  A             192.168.0.215
site                  86400     IN  A             192.168.0.205

Point important sur les enregistrements existants

Cette version interroge Technitium avec :

1
/api/zones/records/get

pour chaque nom d’hôte désiré. C’est nécessaire parce que la réponse que tu as obtenue pour dn.home.arpa était :

1
"records": []

Même si des descendants comme machine.dn.home.arpa existent, records/get ne les retourne pas forcément lorsqu’on demande uniquement dn.home.arpa.

Pour un grand nombre d’hôtes, il faudra optimiser cette partie avec un export de zone ou une lecture récursive adaptée à la version de Technitium.

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