@@ -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 |
|
@@ -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})$/);
|
@@ -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
|