diff --git a/apps/api/src/services/vps-tracker-sync.ts b/apps/api/src/services/vps-tracker-sync.ts index 8b04ce9..69f5098 100644 --- a/apps/api/src/services/vps-tracker-sync.ts +++ b/apps/api/src/services/vps-tracker-sync.ts @@ -68,13 +68,15 @@ async function resolveIpsViaDns(hostname: string): Promise { * IP для матчинга в VPS Tracker: * 1) A-записи binding * 2) разворот локальной CNAME-цепочки по другим bindings - * 3) публичный DNS (resolve4) - * 4) IP сервиса + * 3) origin A/AAAA из dns_records CFDM (даже proxied — content = origin) + * 4) публичный DNS (resolve4) — часто CF anycast, слабый сигнал + * 5) IP сервиса */ export async function resolveBindingIpsForSync( binding: ServiceBindingView, serviceIps: string[], index: BindingIpIndex, + db?: Db, ): Promise { if (binding.target_ips.length > 0) { return [...binding.target_ips]; @@ -86,6 +88,11 @@ export async function resolveBindingIpsForSync( const local = resolveIpsLocally(index, targetFqdn); if (local.length > 0) return local; + if (db) { + const fromTable = repos.listOriginIpsForFqdn(db, targetFqdn); + if (fromTable.length > 0) return fromTable; + } + const viaDns = await resolveIpsViaDns(targetFqdn); if (viaDns.length > 0) return viaDns; } @@ -94,6 +101,13 @@ export async function resolveBindingIpsForSync( return []; } +function cnameTargetForSync(binding: ServiceBindingView): string | undefined { + const raw = binding.cname_target?.trim(); + if (!raw) return undefined; + const normalized = normalizeCnameHost(raw, binding.zone_name); + return normalized || undefined; +} + export async function buildServiceSyncBindingsAsync( db: Db, serviceId: number, @@ -107,7 +121,7 @@ export async function buildServiceSyncBindingsAsync( const items: CfdmBindingSyncItem[] = []; for (const binding of bindings) { - const ips = await resolveBindingIpsForSync(binding, serviceIps, index); + const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db); items.push({ bindingId: binding.id, serviceId: service.id, @@ -117,6 +131,7 @@ export async function buildServiceSyncBindingsAsync( zoneName: binding.zone_name, hostname: binding.hostname, ips, + cnameTarget: cnameTargetForSync(binding), }); } @@ -151,7 +166,7 @@ export async function buildAllSyncBindings( serviceIps = repos.listServiceIps(db, binding.service_id); serviceIpCache.set(binding.service_id, serviceIps); } - const ips = await resolveBindingIpsForSync(binding, serviceIps, index); + const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db); items.push({ bindingId: binding.id, serviceId: binding.service_id, @@ -161,6 +176,7 @@ export async function buildAllSyncBindings( zoneName: binding.zone_name, hostname: binding.hostname, ips, + cnameTarget: cnameTargetForSync(binding), }); } return items; diff --git a/packages/db/src/repos.ts b/packages/db/src/repos.ts index c330393..293d19d 100644 --- a/packages/db/src/repos.ts +++ b/packages/db/src/repos.ts @@ -533,6 +533,49 @@ export function listDnsByDomain(db: Db, domainId: number): DnsRecord[] { .map(mapDnsRecord); } +/** + * Origin A/AAAA из локальной таблицы dns_records для FQDN. + * content в CFDM — origin IP даже при proxied=true (в отличие от публичного DNS). + * Следует по CNAME-записям в той же БД. + */ +export function listOriginIpsForFqdn( + db: Db, + fqdn: string, + depth = 0, +): string[] { + if (depth > 8) return []; + const normalized = fqdn.trim().toLowerCase().replace(/\.+$/, ""); + if (!normalized) return []; + + const aIps: string[] = []; + let cnameNext: string | null = null; + + for (const domain of listAllDomains(db)) { + const zone = domain.zone_name.trim().toLowerCase().replace(/\.+$/, ""); + if (!zone) continue; + if (normalized !== zone && !normalized.endsWith(`.${zone}`)) continue; + + const hostLabel = + normalized === zone ? "@" : normalized.slice(0, -(zone.length + 1)); + + for (const record of listDnsByDomain(db, domain.id)) { + const type = record.record_type.toUpperCase(); + if (!dnsRecordNamesMatch(record.name, hostLabel, zone)) continue; + if (type === "A" || type === "AAAA") { + const ip = record.content.trim(); + if (ip) aIps.push(ip); + } else if (type === "CNAME" && !cnameNext) { + const target = record.content.trim().replace(/\.+$/, ""); + if (target) cnameNext = target.includes(".") ? target : `${target}.${zone}`; + } + } + } + + if (aIps.length > 0) return [...new Set(aIps)]; + if (cnameNext) return listOriginIpsForFqdn(db, cnameNext, depth + 1); + return []; +} + export function findDnsByCfId( db: Db, domainId: number, diff --git a/packages/shared/dist/index.d.ts b/packages/shared/dist/index.d.ts index 0cf6d34..3df0602 100644 --- a/packages/shared/dist/index.d.ts +++ b/packages/shared/dist/index.d.ts @@ -1467,6 +1467,7 @@ declare const cfdmBindingSyncItemSchema: z.ZodObject<{ zoneName: z.ZodString; hostname: z.ZodString; ips: z.ZodArray; + cnameTarget: z.ZodOptional; deleted: z.ZodOptional; }, z.core.$strip>; declare const cfdmSyncBindingsBodySchema: z.ZodObject<{ @@ -1479,6 +1480,7 @@ declare const cfdmSyncBindingsBodySchema: z.ZodObject<{ zoneName: z.ZodString; hostname: z.ZodString; ips: z.ZodArray; + cnameTarget: z.ZodOptional; deleted: z.ZodOptional; }, z.core.$strip>>; fullSync: z.ZodOptional; diff --git a/packages/shared/dist/index.js b/packages/shared/dist/index.js index 68f3816..149b6b1 100644 --- a/packages/shared/dist/index.js +++ b/packages/shared/dist/index.js @@ -579,6 +579,8 @@ var cfdmBindingSyncItemSchema = z3.object({ zoneName: z3.string().min(1), hostname: z3.string(), ips: z3.array(z3.string()), + /** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */ + cnameTarget: z3.string().optional(), deleted: z3.boolean().optional() }); var cfdmSyncBindingsBodySchema = z3.object({ diff --git a/packages/shared/src/integration-vps-tracker.ts b/packages/shared/src/integration-vps-tracker.ts index ad661e0..6c67bd7 100644 --- a/packages/shared/src/integration-vps-tracker.ts +++ b/packages/shared/src/integration-vps-tracker.ts @@ -9,6 +9,8 @@ export const cfdmBindingSyncItemSchema = z.object({ zoneName: z.string().min(1), hostname: z.string(), ips: z.array(z.string()), + /** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */ + cnameTarget: z.string().optional(), deleted: z.boolean().optional(), });