fix(integrations): отдавать origin IP и cnameTarget для CNAME sync
Build and Push CFDM Docker Image / build-and-push (push) Successful in 2m6s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 6s

Публичный DNS даёт CF anycast; для матча в VPS Tracker нужны origin из dns_records и цель CNAME.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-01 01:16:26 +07:00
co-authored by Cursor
parent c01c75116a
commit ec70104085
5 changed files with 69 additions and 4 deletions
+20 -4
View File
@@ -68,13 +68,15 @@ async function resolveIpsViaDns(hostname: string): Promise<string[]> {
* IP для матчинга в VPS Tracker: * IP для матчинга в VPS Tracker:
* 1) A-записи binding * 1) A-записи binding
* 2) разворот локальной CNAME-цепочки по другим bindings * 2) разворот локальной CNAME-цепочки по другим bindings
* 3) публичный DNS (resolve4) * 3) origin A/AAAA из dns_records CFDM (даже proxied — content = origin)
* 4) IP сервиса * 4) публичный DNS (resolve4) — часто CF anycast, слабый сигнал
* 5) IP сервиса
*/ */
export async function resolveBindingIpsForSync( export async function resolveBindingIpsForSync(
binding: ServiceBindingView, binding: ServiceBindingView,
serviceIps: string[], serviceIps: string[],
index: BindingIpIndex, index: BindingIpIndex,
db?: Db,
): Promise<string[]> { ): Promise<string[]> {
if (binding.target_ips.length > 0) { if (binding.target_ips.length > 0) {
return [...binding.target_ips]; return [...binding.target_ips];
@@ -86,6 +88,11 @@ export async function resolveBindingIpsForSync(
const local = resolveIpsLocally(index, targetFqdn); const local = resolveIpsLocally(index, targetFqdn);
if (local.length > 0) return local; 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); const viaDns = await resolveIpsViaDns(targetFqdn);
if (viaDns.length > 0) return viaDns; if (viaDns.length > 0) return viaDns;
} }
@@ -94,6 +101,13 @@ export async function resolveBindingIpsForSync(
return []; 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( export async function buildServiceSyncBindingsAsync(
db: Db, db: Db,
serviceId: number, serviceId: number,
@@ -107,7 +121,7 @@ export async function buildServiceSyncBindingsAsync(
const items: CfdmBindingSyncItem[] = []; const items: CfdmBindingSyncItem[] = [];
for (const binding of bindings) { for (const binding of bindings) {
const ips = await resolveBindingIpsForSync(binding, serviceIps, index); const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db);
items.push({ items.push({
bindingId: binding.id, bindingId: binding.id,
serviceId: service.id, serviceId: service.id,
@@ -117,6 +131,7 @@ export async function buildServiceSyncBindingsAsync(
zoneName: binding.zone_name, zoneName: binding.zone_name,
hostname: binding.hostname, hostname: binding.hostname,
ips, ips,
cnameTarget: cnameTargetForSync(binding),
}); });
} }
@@ -151,7 +166,7 @@ export async function buildAllSyncBindings(
serviceIps = repos.listServiceIps(db, binding.service_id); serviceIps = repos.listServiceIps(db, binding.service_id);
serviceIpCache.set(binding.service_id, serviceIps); serviceIpCache.set(binding.service_id, serviceIps);
} }
const ips = await resolveBindingIpsForSync(binding, serviceIps, index); const ips = await resolveBindingIpsForSync(binding, serviceIps, index, db);
items.push({ items.push({
bindingId: binding.id, bindingId: binding.id,
serviceId: binding.service_id, serviceId: binding.service_id,
@@ -161,6 +176,7 @@ export async function buildAllSyncBindings(
zoneName: binding.zone_name, zoneName: binding.zone_name,
hostname: binding.hostname, hostname: binding.hostname,
ips, ips,
cnameTarget: cnameTargetForSync(binding),
}); });
} }
return items; return items;
+43
View File
@@ -533,6 +533,49 @@ export function listDnsByDomain(db: Db, domainId: number): DnsRecord[] {
.map(mapDnsRecord); .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( export function findDnsByCfId(
db: Db, db: Db,
domainId: number, domainId: number,
+2
View File
@@ -1467,6 +1467,7 @@ declare const cfdmBindingSyncItemSchema: z.ZodObject<{
zoneName: z.ZodString; zoneName: z.ZodString;
hostname: z.ZodString; hostname: z.ZodString;
ips: z.ZodArray<z.ZodString>; ips: z.ZodArray<z.ZodString>;
cnameTarget: z.ZodOptional<z.ZodString>;
deleted: z.ZodOptional<z.ZodBoolean>; deleted: z.ZodOptional<z.ZodBoolean>;
}, z.core.$strip>; }, z.core.$strip>;
declare const cfdmSyncBindingsBodySchema: z.ZodObject<{ declare const cfdmSyncBindingsBodySchema: z.ZodObject<{
@@ -1479,6 +1480,7 @@ declare const cfdmSyncBindingsBodySchema: z.ZodObject<{
zoneName: z.ZodString; zoneName: z.ZodString;
hostname: z.ZodString; hostname: z.ZodString;
ips: z.ZodArray<z.ZodString>; ips: z.ZodArray<z.ZodString>;
cnameTarget: z.ZodOptional<z.ZodString>;
deleted: z.ZodOptional<z.ZodBoolean>; deleted: z.ZodOptional<z.ZodBoolean>;
}, z.core.$strip>>; }, z.core.$strip>>;
fullSync: z.ZodOptional<z.ZodBoolean>; fullSync: z.ZodOptional<z.ZodBoolean>;
+2
View File
@@ -579,6 +579,8 @@ var cfdmBindingSyncItemSchema = z3.object({
zoneName: z3.string().min(1), zoneName: z3.string().min(1),
hostname: z3.string(), hostname: z3.string(),
ips: z3.array(z3.string()), ips: z3.array(z3.string()),
/** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */
cnameTarget: z3.string().optional(),
deleted: z3.boolean().optional() deleted: z3.boolean().optional()
}); });
var cfdmSyncBindingsBodySchema = z3.object({ var cfdmSyncBindingsBodySchema = z3.object({
@@ -9,6 +9,8 @@ export const cfdmBindingSyncItemSchema = z.object({
zoneName: z.string().min(1), zoneName: z.string().min(1),
hostname: z.string(), hostname: z.string(),
ips: z.array(z.string()), ips: z.array(z.string()),
/** CNAME-цель (FQDN), если binding — CNAME; для матчинга в VPS Tracker. */
cnameTarget: z.string().optional(),
deleted: z.boolean().optional(), deleted: z.boolean().optional(),
}); });