Showing 3 changed files with 79 additions and 21 deletions
+2 -2
.doc/local-hosts.md
@@ -28,10 +28,10 @@ Implementarea versionată este:
28 28
 |--------|-----|
29 29
 | `var/host-manager.sqlite` | sursa de adevăr runtime pentru registry și Work Orders |
30 30
 | `config/hosts.yaml` | seed/snapshot export pentru hosturi și FQDN-uri canonice |
31
-| `config/local-hosts.tsv` | manifest DNS generat, cu un singur IP canonic pe host și aliasuri scurte derivate |
31
+| `config/local-hosts.tsv` | manifest DNS generat, cu A records pentru hosturi/aliasuri de host și CNAME records pentru vhosturi |
32 32
 | `scripts/sync_local_hosts.sh` | generează și sincronizează `/etc/hosts`, `cloaking-rules.txt` și `/ip dns static` |
33 33
 
34
-`madagascar.xdev.ro` este domeniul implicit. Pentru orice nume `*.madagascar.xdev.ro`, aliasul scurt este derivat automat. De exemplu, `autonas01.madagascar.xdev.ro` publică și `autonas01`, iar `pmx.baobab.madagascar.xdev.ro` publică și `pmx.baobab`. Aliasurile derivate nu se declară separat în registry.
34
+`madagascar.xdev.ro` este domeniul implicit. Pentru orice host real `*.madagascar.xdev.ro`, aliasul scurt este derivat automat. De exemplu, `autonas01.madagascar.xdev.ro` publică și `autonas01`. Vhosturile, de exemplu `pmx.baobab.madagascar.xdev.ro`, se publică drept CNAME către hostul care le servește; aliasul scurt derivat, de exemplu `pmx.baobab`, este tot CNAME. Aliasurile derivate nu se declară separat în registry.
35 35
 
36 36
 ## Ierarhia surselor
37 37
 
+40 -18
scripts/host_manager.pl
@@ -489,8 +489,6 @@ SELECT
489 489
     v.status AS vhost_status,
490 490
     v.certificate_id,
491 491
     h.legacy_id,
492
-    h.hosts_ip,
493
-    h.dns_ip,
494 492
     h.monitoring,
495 493
     h.status AS host_status,
496 494
     c.common_name,
@@ -520,7 +518,6 @@ SQL
520 518
             vhost_fqdn => $row->{vhost_fqdn},
521 519
             host_id => $row->{legacy_id} || '',
522 520
             host_fqdn => $row->{host_fqdn},
523
-            ip => $row->{hosts_ip} || $row->{dns_ip} || '',
524 521
             derived_aliases => short_alias_for_fqdn($row->{vhost_fqdn}) ? [ short_alias_for_fqdn($row->{vhost_fqdn}) ] : [],
525 522
             monitoring => $row->{monitoring} || '',
526 523
             status => $row->{host_status} || $row->{vhost_status} || '',
@@ -943,6 +940,30 @@ sub effective_names {
943 940
     return unique_preserve(@names);
944 941
 }
945 942
 
943
+sub host_dns_names {
944
+    my ($host) = @_;
945
+    my @names;
946
+    my $fqdn = canonical_host_fqdn($host);
947
+    push @names, $fqdn if length $fqdn;
948
+    push @names, declared_alias_names($host), derived_alias_names($host);
949
+    return unique_preserve(@names);
950
+}
951
+
952
+sub vhost_cname_records {
953
+    my ($host) = @_;
954
+    my $target = canonical_host_fqdn($host);
955
+    return () unless length $target;
956
+    my @records;
957
+    for my $vhost (declared_vhost_names($host)) {
958
+        push @records, [ $vhost, $target ];
959
+        if (my $short = short_alias_for_fqdn($vhost)) {
960
+            push @records, [ $short, $target ];
961
+        }
962
+    }
963
+    my %seen;
964
+    return grep { !$seen{$_->[0]}++ } @records;
965
+}
966
+
946 967
 sub declared_dns_names {
947 968
     my ($host) = @_;
948 969
     my @names;
@@ -1078,6 +1099,7 @@ sub render_local_hosts_tsv {
1078 1099
     $out .= "#\n";
1079 1100
     $out .= "# Format:\n";
1080 1101
     $out .= "# ip<TAB>name [aliases...]\n";
1102
+    $out .= "# CNAME<TAB>alias<TAB>target\n";
1081 1103
     $out .= "#\n";
1082 1104
     $out .= "# Priority rule:\n";
1083 1105
     $out .= "# - DHCP lease/reservation on 192.168.2.1 is canonical for LAN IP allocation.\n";
@@ -1087,9 +1109,12 @@ sub render_local_hosts_tsv {
1087 1109
         next unless ($host->{status} || 'active') eq 'active';
1088 1110
         my $ip = canonical_ip($host);
1089 1111
         next unless $ip;
1090
-        my @names = effective_names($host);
1112
+        my @names = host_dns_names($host);
1091 1113
         next unless @names;
1092 1114
         $out .= join("\t", $ip, join(' ', @names)) . "\n";
1115
+        for my $record (vhost_cname_records($host)) {
1116
+            $out .= join("\t", 'CNAME', @$record) . "\n";
1117
+        }
1093 1118
     }
1094 1119
     return $out;
1095 1120
 }
@@ -3369,9 +3394,8 @@ sub app_html {
3369 3394
               <thead>
3370 3395
                 <tr>
3371 3396
                   <th style="width: 22%">Vhost</th>
3372
-                  <th style="width: 24%">Host</th>
3373
-                  <th style="width: 10%">IP</th>
3374
-                  <th style="width: 30%">Certificate</th>
3397
+                  <th style="width: 28%">Host</th>
3398
+                  <th style="width: 34%">Certificate</th>
3375 3399
                   <th style="width: 8%">Monitoring</th>
3376 3400
                   <th style="width: 6%">Status</th>
3377 3401
                 </tr>
@@ -4051,7 +4075,7 @@ sub app_html {
4051 4075
 
4052 4076
     function renderHostCertificateCell(host) {
4053 4077
       const cert = host.certificate || {};
4054
-      const certId = host.certificate_id || certId(cert) || '';
4078
+      const certId = host.certificate_id || certificateIdOf(cert) || '';
4055 4079
       const row = hostCertificateRow(host);
4056 4080
       const links = certId ? `<div class="vhost-cert-links">
4057 4081
         <a class="linkbtn" href="/download/ca/cert/${encodeURIComponent(certId)}.crt">crt</a>
@@ -4085,7 +4109,6 @@ sub app_html {
4085 4109
         vhost,
4086 4110
         host_id: host.id || '',
4087 4111
         host_fqdn: host.fqdn || '',
4088
-        ip: host.ip || '',
4089 4112
         derived_aliases: shortAliasForFqdn(vhost) ? [shortAliasForFqdn(vhost)] : [],
4090 4113
         monitoring: host.monitoring || '',
4091 4114
         status: host.status || '',
@@ -4113,11 +4136,10 @@ sub app_html {
4113 4136
             </select>
4114 4137
           </div>
4115 4138
         </td>
4116
-        <td>${escapeHtml(row.ip)}</td>
4117 4139
         <td>${renderVhostCertificateCell(row)}</td>
4118 4140
         <td><span class="pill">${escapeHtml(row.monitoring)}</span></td>
4119 4141
         <td>${escapeHtml(row.status)}</td>
4120
-      </tr>`).join('') : '<tr><td colspan="6" class="muted">No vhosts.</td></tr>';
4142
+      </tr>`).join('') : '<tr><td colspan="5" class="muted">No vhosts.</td></tr>';
4121 4143
       document.querySelectorAll('[data-vhost-select]').forEach(select => {
4122 4144
         select.addEventListener('change', () => {
4123 4145
           reassignVhostFromSelect(select).catch(e => {
@@ -4201,15 +4223,15 @@ sub app_html {
4201 4223
     function renderCertificateOptions(selectedCertificateId, row) {
4202 4224
       const byId = new Map();
4203 4225
       (state.certificates || []).forEach(cert => {
4204
-        const id = certId(cert);
4226
+        const id = certificateIdOf(cert);
4205 4227
         if (id) byId.set(id, cert);
4206 4228
       });
4207 4229
       if (row && row.certificate) {
4208
-        const id = certId(row.certificate);
4230
+        const id = certificateIdOf(row.certificate);
4209 4231
         if (id && !byId.has(id)) byId.set(id, row.certificate);
4210 4232
       }
4211 4233
       const certs = Array.from(byId.values())
4212
-        .filter(cert => certMatchesRow(cert, row) || certId(cert) === selectedCertificateId)
4234
+        .filter(cert => certMatchesRow(cert, row) || certificateIdOf(cert) === selectedCertificateId)
4213 4235
         .sort((a, b) => {
4214 4236
           const ar = certRelevance(a, row);
4215 4237
           const br = certRelevance(b, row);
@@ -4217,7 +4239,7 @@ sub app_html {
4217 4239
           return String(a.name || a.id || '').localeCompare(String(b.name || b.id || ''));
4218 4240
         });
4219 4241
       const options = ['<option value="">no certificate</option>'].concat(certs.map(cert => {
4220
-        const id = certId(cert);
4242
+        const id = certificateIdOf(cert);
4221 4243
         const label = compactCertificateLabel(cert, row);
4222 4244
         const selected = id === selectedCertificateId ? ' selected' : '';
4223 4245
         return `<option value="${escapeHtml(id)}"${selected}>${escapeHtml(label)}</option>`;
@@ -4225,7 +4247,7 @@ sub app_html {
4225 4247
       return options.join('');
4226 4248
     }
4227 4249
 
4228
-    function certId(cert) {
4250
+    function certificateIdOf(cert) {
4229 4251
       return cert ? (cert.id || cert.name || '') : '';
4230 4252
     }
4231 4253
 
@@ -4238,7 +4260,7 @@ sub app_html {
4238 4260
     function certRelevance(cert, row) {
4239 4261
       if (!row) return 9;
4240 4262
       const names = new Set(certDnsNames(cert));
4241
-      const id = String(certId(cert)).toLowerCase();
4263
+      const id = String(certificateIdOf(cert)).toLowerCase();
4242 4264
       const commonName = String(cert.common_name || '').toLowerCase();
4243 4265
       const vhost = String(row.vhost || '').toLowerCase();
4244 4266
       const host = String(row.host_fqdn || row.fqdn || '').toLowerCase();
@@ -4265,7 +4287,7 @@ sub app_html {
4265 4287
 
4266 4288
     function compactCertificateLabel(cert, row) {
4267 4289
       const relevance = certRelevance(cert, row);
4268
-      const id = String(certId(cert));
4290
+      const id = String(certificateIdOf(cert));
4269 4291
       const days = daysUntil(cert.not_after);
4270 4292
       const suffix = days === null ? '' : ` (${certStatusLabel(days)})`;
4271 4293
       const timestamp = id.match(/-(\d{14})$/);
+37 -1
scripts/sync_local_hosts.sh
@@ -68,8 +68,9 @@ CLOAK_ROWS="$WORK_DIR/cloak.rows"
68 68
 NAMES_FILE="$WORK_DIR/names.txt"
69 69
 MIKROTIK_RSC="$WORK_DIR/as01.rsc"
70 70
 VERIFY_ROWS="$WORK_DIR/verify.rows"
71
+CNAME_VERIFY_ROWS="$WORK_DIR/cname-verify.rows"
71 72
 
72
-touch "$HOSTS_ROWS" "$CLOAK_ROWS" "$NAMES_FILE" "$MIKROTIK_RSC" "$VERIFY_ROWS"
73
+touch "$HOSTS_ROWS" "$CLOAK_ROWS" "$NAMES_FILE" "$MIKROTIK_RSC" "$VERIFY_ROWS" "$CNAME_VERIFY_ROWS"
73 74
 
74 75
 quote_ros() {
75 76
     printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
@@ -98,6 +99,27 @@ while IFS= read -r line || [[ -n "$line" ]]; do
98 99
 
99 100
     line="${line//$'\r'/}"
100 101
     IFS=$'\t' read -r col1 col2 col3 _ <<< "$line"
102
+    record_type="$(printf '%s' "$col1" | tr '[:lower:]' '[:upper:]')"
103
+    if [[ "$record_type" == "CNAME" ]]; then
104
+        name="${col2:-}"
105
+        target="${col3:-}"
106
+        [[ -n "$name" && -n "$target" ]] || die "Invalid CNAME row: $line"
107
+
108
+        printf '%s\n' "$name" >> "$NAMES_FILE"
109
+        printf '%-40s %s\n' "$name" "$target" >> "$CLOAK_ROWS"
110
+        if [[ "$name" == *.xdev.ro ]]; then
111
+            printf '%s %s\n' "$name" "$target" >> "$CNAME_VERIFY_ROWS"
112
+        fi
113
+
114
+        ros_name="$(quote_ros "$name")"
115
+        ros_target="$(quote_ros "$target")"
116
+        {
117
+            printf '/ip dns static remove [find name="%s"]\n' "$ros_name"
118
+            printf '/ip dns static add name="%s" type=CNAME cname="%s" comment="xdev-local managed"\n' "$ros_name" "$ros_target"
119
+        } >> "$MIKROTIK_RSC"
120
+        continue
121
+    fi
122
+
101 123
     if [[ -n "${col3:-}" ]]; then
102 124
         ip="$col2"
103 125
         names="$col3"
@@ -285,6 +307,20 @@ verify_resolver() {
285 307
         fi
286 308
     done < "$VERIFY_ROWS"
287 309
 
310
+    while read -r name expected_target; do
311
+        answers="$(dig @"$resolver" "$name" +short || true)"
312
+        target_answers="$(dig @"$resolver" "$expected_target" +short || true)"
313
+        expected_target_dot="${expected_target%.}."
314
+        if ! grep -Fxq "$expected_target" <<< "$answers" \
315
+            && ! grep -Fxq "$expected_target_dot" <<< "$answers" \
316
+            && ! grep -Fxf <(printf '%s\n' "$target_answers") <<< "$answers" >/dev/null; then
317
+            compact_answers="$(paste -s -d ',' - <<< "$answers")"
318
+            compact_target_answers="$(paste -s -d ',' - <<< "$target_answers")"
319
+            printf '[FAIL] %s %s expected CNAME %s or flattened %s got %s\n' "$resolver" "$name" "$expected_target" "${compact_target_answers:-<empty>}" "${compact_answers:-<empty>}" >&2
320
+            failures=$((failures + 1))
321
+        fi
322
+    done < "$CNAME_VERIFY_ROWS"
323
+
288 324
     dns_status="$(dig @"$resolver" "$NEGATIVE_NAME" +noall +comments | awk '/status:/ {print $6}' | tr -d ',' || true)"
289 325
     if [[ "$dns_status" != "NXDOMAIN" ]]; then
290 326
         printf '[FAIL] %s %s expected NXDOMAIN got %s\n' "$resolver" "$NEGATIVE_NAME" "${dns_status:-<empty>}" >&2