From fe2b8b345673d196d0428ec9d17223e0540d3eda Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 16 Oct 2025 23:28:32 +0200 Subject: [PATCH] TP-Link: Space out HTTP requests a bit, retry connection for sending files On firmware M7350(EU)_V9_9.0.2 Build 241021 (but not sooner), entryId=2 was being sent before entryId=1. entryId=2 is invalid if entryId=1 does not exist yet. The reason it works is due to both requests firing simultaneously, so sometimes entryId=1 is indeed being registered first. We may also be hitting random race conditions on the backend, not 100% sure. Try to alleviate them by sleeping 1 second between started requests and waiting until the DOM is ready. Also, on sluggish devices, it can happen that nc is not ready within 100ms. Fixing that with exponential backoff. --- installer/src/tplink.rs | 41 ++++++++++++++++++++++++++++++----------- installer/src/util.rs | 28 +++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/installer/src/tplink.rs b/installer/src/tplink.rs index 7046a22..b0b85d8 100644 --- a/installer/src/tplink.rs +++ b/installer/src/tplink.rs @@ -265,6 +265,14 @@ async fn handler(state: State, mut req: Request) -> Result, mut req: Request) -> Result { - // Intentionally register rayhunter-daemon before rayhunter-root so that we are less - // likely to run into race conditions where rayhunter-root is launched, and the - // installer kills the server. In practice both HTTP requests may execute concurrently - // anyway. - Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 2, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"}); - Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 1, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"}); + data.extend(br#";document.addEventListener("DOMContentLoaded", () => { + console.log("rayhunter: start polling"); + + var rayhunterSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + + var rayhunterPoll = window.setInterval(async () => { + Globals.models.PTModel.add({applicationName: "rayhunter-daemon", enableState: 1, entryId: 1, openPort: "2400-2500", openProtocol: "TCP", triggerPort: "$(/etc/init.d/rayhunter_daemon start)", triggerProtocol: "TCP"}); + console.log("rayhunter: first request succeeded, stopping rayhunter poll loop"); + window.clearInterval(rayhunterPoll); + + // PTModel.add actually does not wait for the request to finsh. + // Wait 1 second for the request to finish. + // Running both requests concurrently can get one of the two requests rejected, as + // sending a request with entryId: 2 is invalid if entryId 1 does not exist (yet) + // This only happens starting with firmware M7350(EU)_V9_9.0.2 Build 241021, earlier + // versions are not affected. + await rayhunterSleep(1000); + + console.log("rayhunter: running second request"); + Globals.models.PTModel.add({applicationName: "rayhunter-root", enableState: 1, entryId: 2, openPort: "2300-2400", openProtocol: "TCP", triggerPort: "$(busybox telnetd -l /bin/sh)", triggerProtocol: "TCP"}); // Do not use alert(), instead replace page with success message. Using alert() will // block the event loop in such a way that any background promises are blocked from // progress too. For example: The HTTP requests to register our port triggers! document.body.innerHTML = "

Success! You can go back to the rayhunter installer.

"; - - // We can stop polling now, presumably both requests are already inflight. - window.clearInterval(window.rayhunterPoll); - }, 1000);"#); + }, 1000); + });"#); response = Response::from_parts(parts, Body::from(Bytes::from(data))); response.headers_mut().remove("Content-Length"); } diff --git a/installer/src/util.rs b/installer/src/util.rs index d0f04a9..0ded952 100644 --- a/installer/src/util.rs +++ b/installer/src/util.rs @@ -91,7 +91,7 @@ pub async fn telnet_send_file( payload: &[u8], wait_for_prompt: bool, ) -> Result<()> { - echo!("Sending file {filename} ... "); + echo!("Sending file {filename}... "); let nc_output = { let filename = filename.to_owned(); let handle = tokio::spawn(async move { @@ -102,14 +102,31 @@ pub async fn telnet_send_file( ) .await }); - // wait for nc to become available. if the installer fails with connection refused, this - // likely is not high enough. - sleep(Duration::from_millis(100)).await; + let mut addr = addr; addr.set_port(8081); + let mut stream; + let mut attempts = 0; + + loop { + // wait for nc to become available, with exponential backoff. + // + // if the installer fails with connection refused, this + // likely is not high enough. + sleep(Duration::from_millis(100 * (1 << attempts))).await; + + stream = TcpStream::connect(addr).await; + attempts += 1; + if stream.is_ok() || attempts > 3 { + break; + } + + echo!("attempt {attempts}... "); + } + { - let mut stream = TcpStream::connect(addr).await?; + let mut stream = stream?; stream.write_all(payload).await?; // if the orbic is sluggish, we need for nc to write the data to disk before @@ -122,6 +139,7 @@ pub async fn telnet_send_file( sleep(Duration::from_millis(1000)).await; // ensure that stream is dropped before we wait for nc to terminate. + drop(stream); } handle.await??