diff --git a/app/controllers/cwmp_controller.ts b/app/controllers/cwmp_controller.ts index dbecf0c..1bd268c 100644 --- a/app/controllers/cwmp_controller.ts +++ b/app/controllers/cwmp_controller.ts @@ -200,13 +200,14 @@ export default class CwmpController { logger.info({ events }, 'CWMP Inform events extracted') - // Track last periodic inform timestamp + // Track last periodic inform timestamp + last remote IP (used for Connection Request fallback) const normalizedEvents = events.map((e) => normalizeEventCode(e)) const isPeriodic = normalizedEvents.some((e) => e === 'PERIODIC') const isBootstrap = normalizedEvents.some((e) => e === 'BOOTSTRAP') - if (isPeriodic || isBootstrap) { - await DeviceModel.updateOne({ identifier: hashId }, { $set: { lastPeriodicInformAt: new Date() } }).catch(() => {}) - } + const cpeIpForStorage = (xForwardedFor || '').split(',')[0]?.trim() || remoteKey.split(':')[0] + const dbUpdate: Record = { lastRemoteIp: cpeIpForStorage } + if (isPeriodic || isBootstrap) dbUpdate.lastPeriodicInformAt = new Date() + await DeviceModel.updateOne({ identifier: hashId }, { $set: dbUpdate }).catch(() => {}) // Phase 12: Crash pattern detection const hasBoot = normalizedEvents.some((e) => e === 'BOOT') diff --git a/app/domain/device/device_record.ts b/app/domain/device/device_record.ts index 36748b3..2d2b51c 100644 --- a/app/domain/device/device_record.ts +++ b/app/domain/device/device_record.ts @@ -19,6 +19,7 @@ export interface DeviceRecord { provisioningCode?: string references?: DeviceReference[] lastPeriodicInformAt?: Date + lastRemoteIp?: string updatedAt?: Date createdAt?: Date } diff --git a/app/services/device_service.ts b/app/services/device_service.ts index 9599ce1..d81d8d0 100644 --- a/app/services/device_service.ts +++ b/app/services/device_service.ts @@ -159,61 +159,74 @@ export default class DeviceService { } private async sendWakeOnWlan(connectionRequestURL: URL, device: DeviceRecord): Promise { - try { - // Use ConnectionRequest credentials from device properties/parameters - // Fallback chain: ConnectionRequestUsername > ManagementServer.Username > OUI-ProductClass-Serial - const allParams = [...(device.properties || []), ...(device.parameters || [])] + // Use ConnectionRequest credentials from device properties/parameters + // Fallback chain: ConnectionRequestUsername > ManagementServer.Username > OUI-ProductClass-Serial + const allParams = [...(device.properties || []), ...(device.parameters || [])] - const connReqUsername = allParams.find((p: any) => - p.name?.includes('ConnectionRequestUsername') - ) - const connReqPassword = allParams.find((p: any) => - p.name?.includes('ConnectionRequestPassword') - ) - const mgmtUsername = allParams.find((p: any) => p.name?.endsWith('ManagementServer.Username')) - const mgmtPassword = allParams.find((p: any) => p.name?.endsWith('ManagementServer.Password')) + const connReqUsername = allParams.find((p: any) => p.name?.includes('ConnectionRequestUsername')) + const connReqPassword = allParams.find((p: any) => p.name?.includes('ConnectionRequestPassword')) + const mgmtUsername = allParams.find((p: any) => p.name?.endsWith('ManagementServer.Username')) + const mgmtPassword = allParams.find((p: any) => p.name?.endsWith('ManagementServer.Password')) - // genieacs-sim uses ManagementServer.Username/Password for CR auth (non-standard) - // Real CPEs use ConnectionRequestUsername/Password - const username = - mgmtUsername?.value || - connReqUsername?.value || - `${device.deviceId.oui}-${device.deviceId.productClass}-${device.deviceId.serialNumber}` - const password = mgmtPassword?.value ?? connReqPassword?.value ?? '' + // genieacs-sim uses ManagementServer.Username/Password for CR auth (non-standard) + // Real CPEs use ConnectionRequestUsername/Password + const username = + mgmtUsername?.value || + connReqUsername?.value || + `${device.deviceId.oui}-${device.deviceId.productClass}-${device.deviceId.serialNumber}` + const password = mgmtPassword?.value ?? connReqPassword?.value ?? '' + const basicAuth = Buffer.from(`${username}:${password}`).toString('base64') + const headers = { Authorization: `Basic ${basicAuth}` } - logger.info({ url: connectionRequestURL.toString(), username }, 'Sending Connection Request') + // Build list of URLs to try: + // 1. Original ConnectionRequestURL (may be IPv6) + // 2. Fallback: same path but with lastRemoteIp (IPv4 public IP from last Inform) + const urlsToTry: URL[] = [connectionRequestURL] - const basicAuth = Buffer.from(`${username}:${password}`).toString('base64') - const response = await fetch(connectionRequestURL.toString(), { - method: 'GET', - headers: { - Authorization: `Basic ${basicAuth}`, - }, - }) - - if (!response.ok) { - logger.error( - { - status: response.status, - statusText: response.statusText, - url: connectionRequestURL.toString(), - }, - 'Connection Request failed with non-OK status' + const lastRemoteIp = (device as any).lastRemoteIp as string | undefined + if (lastRemoteIp && lastRemoteIp !== connectionRequestURL.hostname.replace(/^\[|\]$/g, '')) { + try { + const fallbackUrl = new URL(connectionRequestURL.toString()) + // IPv6 hostnames need brackets in URL + fallbackUrl.hostname = lastRemoteIp.includes(':') ? `[${lastRemoteIp}]` : lastRemoteIp + urlsToTry.push(fallbackUrl) + logger.info( + { original: connectionRequestURL.toString(), fallback: fallbackUrl.toString() }, + '[ConnectionRequest] IPv4 fallback URL prepared' ) - return false + } catch { + // ignore invalid fallback URL } - logger.info( - { status: response.status, url: connectionRequestURL.toString() }, - 'Connection Request successful' - ) - return true - } catch (error: any) { - logger.error( - { error: error?.message, code: error?.cause?.code, url: connectionRequestURL.toString() }, - 'Connection Request network error' - ) - return false } + + for (const url of urlsToTry) { + try { + logger.info({ url: url.toString(), username }, 'Sending Connection Request') + const response = await fetch(url.toString(), { method: 'GET', headers }) + + if (!response.ok) { + logger.error( + { status: response.status, statusText: response.statusText, url: url.toString() }, + 'Connection Request failed with non-OK status' + ) + continue // try next URL + } + logger.info({ status: response.status, url: url.toString() }, 'Connection Request successful') + return true + } catch (error: any) { + logger.warn( + { error: error?.message, code: error?.cause?.code, url: url.toString() }, + '[ConnectionRequest] URL failed, trying next if available' + ) + // try next URL + } + } + + logger.error( + { urls: urlsToTry.map((u) => u.toString()) }, + 'Connection Request failed on all URLs' + ) + return false } calculatePagination(offset: number, limit: number, total: number): PaginationData {