const IPV4 = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ /** Нормализация IP/endpoint из RouterOS (CIDR, zone id) — как в network-map-layout. */ export function normalizeGreEndpointAddr(addr: string): string { return addr.trim().split("/")[0].split("%")[0].trim() } /** Явный IPv4 (для отличия от FQDN в remote-address GRE). */ export function isIpv4String(s: string): boolean { return IPV4.test(s.trim()) } /** * Сопоставление IP WAN в каталоге с outer GRE: прямой IP или FQDN, * зарезолвенный в API (hostname lower-case → A-запись). */ export function wanUplinkIpMatchesGreOuter( wanIp: string, greOuter: string, resolvedIpv4ByHost?: ReadonlyMap, ): boolean { const a = normalizeGreEndpointAddr(wanIp) const b = normalizeGreEndpointAddr(greOuter) if (!a || !b) return false if (a === b) return true if (resolvedIpv4ByHost) { const rb = resolvedIpv4ByHost.get(b.toLowerCase()) if (rb && a === normalizeGreEndpointAddr(rb)) return true const ra = resolvedIpv4ByHost.get(a.toLowerCase()) if (ra && b === normalizeGreEndpointAddr(ra)) return true if (ra && rb && normalizeGreEndpointAddr(ra) === normalizeGreEndpointAddr(rb)) return true } return false } /** Все варианты внешнего IP для сопоставления с каталогом (IP и зарезолвенный FQDN). */ export function greOuterComparableIps( outer: string, resolvedIpv4ByHost?: ReadonlyMap, ): string[] { const n = normalizeGreEndpointAddr(outer) if (!n || n === "0.0.0.0") return [] const out: string[] = [] if (isIpv4String(n)) out.push(n) const r = resolvedIpv4ByHost?.get(n.toLowerCase()) if (r) { const ip = normalizeGreEndpointAddr(r) if (ip && isIpv4String(ip) && !out.includes(ip)) out.push(ip) } return out } /** Собрать уникальные FQDN из local/remote-address (без чистых IPv4). */ export function collectGreEndpointHostnames( tunnels: Array<{ localAddress: string; remoteAddress: string }>, ): string[] { const set = new Set() for (const t of tunnels) { for (const a of [t.localAddress, t.remoteAddress]) { const n = normalizeGreEndpointAddr(a) if (!n || n === "0.0.0.0") continue if (!isIpv4String(n)) set.add(n.toLowerCase()) } } return [...set] } /** Ответ POST /api/network/resolve-hosts → Map для матчинга. */ export function greResolvedMapFromApi( results: Record | undefined, ): ReadonlyMap { const m = new Map() if (!results) return m for (const [k, v] of Object.entries(results)) { if (v && isIpv4String(v)) m.set(k.toLowerCase(), normalizeGreEndpointAddr(v)) } return m } /** Подпись outer на карте: при FQDN дополнительно показываем разрешённый IPv4. */ export function formatGreOuterForDisplay( addr: string, resolvedIpv4ByHost?: ReadonlyMap, ): string { const n = normalizeGreEndpointAddr(addr) || "?" if (isIpv4String(n)) return n const r = resolvedIpv4ByHost?.get(n.toLowerCase()) return r ? `${n} (${r})` : n }