fix(relay): keep-alive on WebSocket, cap delivery attempts, honest receipts
Messages arrived late or not at all, for two independent reasons. Dead connections: neither side sent a ping, and after AUTH the server waited for a frame without a timeout. Mobile NAT drops idle TCP silently, so the client wrote into a void while the relay still held the device in its online map — and therefore never sent a push wake-up either. The ping is a protocol-level one rather than v1::Ping, because dart:io answers those on its own; the fix works with clients already in the wild. Poisoned queue: a client decrypts an envelope (ratchet advances), stores it and acknowledges it. If that ACK is lost, a second decryption attempt can only fail, so the envelope is never acknowledged — and was redelivered on every connect until its 30-day TTL, without ever reaching the recipient. attempt_count existed from the first migration but was never counted; it now bounds redelivery. Receipts reported DELIVERED_ONLINE whenever a write into the bounded mpsc channel succeeded, which it does even for a long-dead socket. Online is now claimed only for devices that recently sent something. Push send failures were logged at debug while the relay runs at info, so a rejected wake-up looked exactly like a working one.
This commit is contained in:
@@ -85,13 +85,23 @@ pub async fn send_wake(
|
||||
/// Samotný POST budíčku (bez guardu — ten řeší volající). Oddělené kvůli
|
||||
/// testovatelnosti proti loopback mocku (guard by loopback jinak odmítl).
|
||||
async fn post_wake(client: &Client, url: &str) {
|
||||
let _ = client
|
||||
let resp = client
|
||||
.post(url)
|
||||
.header("content-type", "application/json")
|
||||
.header("ttl", "2419200") // WebPush TTL (28 dní), ntfy ignoruje
|
||||
.body(WAKE_BODY)
|
||||
.send()
|
||||
.await;
|
||||
// Výsledek se dřív zahazoval (`let _ =`), takže nešlo poznat, jestli
|
||||
// budíčky vůbec odcházejí. Logujeme jen stavový kód — endpoint je
|
||||
// identifikátor uživatele a do logu nesmí.
|
||||
match resp {
|
||||
Ok(r) if !r.status().is_success() => {
|
||||
tracing::warn!(status = %r.status(), "UnifiedPush budíček odmítnut");
|
||||
}
|
||||
Err(e) => tracing::warn!("UnifiedPush budíček selhal: {e}"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Povolit jen bezpečné cíle. https vždy; http jen pro .onion (samo šifruje).
|
||||
@@ -191,7 +201,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn verejne_adresy_projdou() {
|
||||
for ip in ["1.1.1.1", "8.8.8.8", "203.0.113.10", "2606:4700:4700::1111"] {
|
||||
for ip in ["1.1.1.1", "8.8.8.8", "9.9.9.9", "2606:4700:4700::1111"] {
|
||||
assert!(!is_internal_ip(&ip.parse().unwrap()), "{ip} nemělo být interní");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user