fix: rebind CWMP session when CPE sends empty POST on new TCP port
Build & Deploy / build (push) Successful in 33s
Build & Deploy / docker (push) Successful in 1m1s
Build & Deploy / deploy (push) Successful in 33s

TR-069 CPEs routinely open a new TCP connection (new port) for the empty
POST that follows InformResponse. ensureSessionForRemote() only did an
exact remoteKey match, so hasSession() returned false and the state
machine was never continued — GPN discovery was never triggered.

Add IP-only fallback in ensureSessionForRemote(): when the exact
remoteKey is not found, scan RUNNING machines for a unique IP match,
rebind the new remoteKey, and remap sessionMachines to the new sessionId.
Ambiguous cases (NAT/CGNAT with multiple active devices on same IP)
return false safely.
This commit is contained in:
Kevin L'Hereec committed 2026-07-17 16:14:38 +02:00
1 parent 7835f99657
commit 61bfb4e762
1 file changed
+43 -6
+43 -6
View File
@@ -322,11 +322,46 @@ export class StateMachineManager extends EventEmitter {
}
/**
* Ensure session is bound when TCP connection changes
* Ensure session is bound when TCP connection changes.
*
* First tries an exact remoteKey match. If that fails (CPE opened a new TCP
* port for the empty POST — standard TR-069 behaviour), falls back to an
* IP-only match among RUNNING machines (single unambiguous candidate only).
* In both cases the new sessionId is registered in sessionMachines so that
* hasSession() returns true and continue() can run.
*/
ensureSessionForRemote(remoteKey: string, newSession: string): boolean {
const deviceKey = this.remoteToDevice.get(remoteKey)
if (!deviceKey) return false
// --- Exact match path ---
let deviceKey = this.remoteToDevice.get(remoteKey)
// --- IP-only fallback (CPE reconnected on a new TCP port) ---
if (!deviceKey) {
const lastColon = remoteKey.lastIndexOf(':')
const remoteIp = lastColon > 0 ? remoteKey.slice(0, lastColon).replace(/^\[|\]$/g, '') : remoteKey
const candidates: string[] = []
for (const [key, dkJson] of this.remoteToDevice.entries()) {
const kLastColon = key.lastIndexOf(':')
const keyIp = (kLastColon > 0 ? key.slice(0, kLastColon) : key).replace(/^\[|\]$/g, '')
if (keyIp !== remoteIp) continue
const existSession = this.deviceSessions.get(dkJson)
if (!existSession) continue
const mid = this.sessionMachines.get(existSession)
if (!mid) continue
const m = this.machines.get(mid)
if (!m || m.status !== MachineStatus.RUNNING) continue
if (!candidates.includes(dkJson)) candidates.push(dkJson)
}
// Refuse to guess if ambiguous (NAT/CGNAT with multiple active devices)
if (candidates.length !== 1) return false
deviceKey = candidates[0]
// Register the new remoteKey so future lookups (exact or fallback) hit immediately
this.bindRemoteToDevice(remoteKey, JSON.parse(deviceKey))
}
const existingSession = this.deviceSessions.get(deviceKey)
if (!existingSession) return false
@@ -338,15 +373,17 @@ export class StateMachineManager extends EventEmitter {
if (!machine) return false
// Don't rebind to a completed/failed machine
if (machine.status !== MachineStatus.RUNNING) {
return false
}
if (machine.status !== MachineStatus.RUNNING) return false
if (existingSession !== newSession) {
this.sessionMachines.delete(existingSession)
this.sessionMachines.set(newSession, machineId)
this.deviceSessions.set(deviceKey, newSession)
machine.session = newSession
logger.debug(
{ oldSession: existingSession, newSession, deviceKey },
'[StateMachineManager] Session rebound via IP fallback (CPE new TCP port)'
)
}
return true