diff --git a/Cargo.lock b/Cargo.lock
index fa0d1d4..5d93403 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -868,7 +868,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "librtmp2"
version = "0.1.0"
-source = "git+https://github.com/OpenRTMP/librtmp2.git?branch=main#e80119900eaaed16a2c272a552d8c47954319d34"
+source = "git+https://github.com/OpenRTMP/librtmp2.git?branch=main#e889f8ca61dc4fd879096da4facae230aac1ef15"
dependencies = [
"cc",
"libc",
@@ -950,9 +950,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
-version = "2.8.2"
+version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "mime"
diff --git a/src/rtmp_bridge.rs b/src/rtmp_bridge.rs
index 1c667d5..54b674a 100644
--- a/src/rtmp_bridge.rs
+++ b/src/rtmp_bridge.rs
@@ -818,6 +818,38 @@ mod tests {
.unwrap();
}
+ #[test]
+ fn publish_failures_count_toward_auth_rate_limit_when_remote_ip_known() {
+ let db = Arc::new(Db::open(":memory:").unwrap());
+ let bridge = test_bridge(db);
+ let ip = "203.0.113.7:1935";
+
+ for conn in 0..RTMP_AUTH_MAX_FAILURES as u64 {
+ bridge.on_connect(conn, ip);
+ assert!(bridge.authorize_publish(conn, "live", "bogus").is_err());
+ }
+
+ bridge.on_connect(RTMP_AUTH_MAX_FAILURES as u64, ip);
+ assert!(bridge.is_auth_rate_limited(&remote_ip_of(ip)));
+ }
+
+ #[test]
+ fn publish_before_on_connect_skips_auth_failure_tracking() {
+ let db = Arc::new(Db::open(":memory:").unwrap());
+ let bridge = test_bridge(db);
+ let ip = "203.0.113.7:1935";
+
+ for conn in 0..(RTMP_AUTH_MAX_FAILURES as u64 + 2) {
+ assert!(bridge.authorize_publish(conn, "live", "bogus").is_err());
+ bridge.on_connect(conn, ip);
+ }
+
+ assert!(
+ !bridge.is_auth_rate_limited(&remote_ip_of(ip)),
+ "failed publish attempts before on_connect must not consume the per-IP auth budget"
+ );
+ }
+
#[test]
fn auth_rate_limit_survives_reconnect_from_same_ip() {
let db = Arc::new(Db::open(":memory:").unwrap());
diff --git a/src/server.rs b/src/server.rs
index 27a1eaf..9ddbc9e 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -2,6 +2,7 @@
//! and the RTMP listener(s), then runs until a shutdown signal arrives.
use parking_lot::Mutex;
+use std::cell::Cell;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex as StdMutex};
@@ -17,6 +18,22 @@ use crate::rtmp_bridge::{DbRtmpBridge, FrameInfo, FrameKind, RtmpEventHandler};
/// registered on the RTMP thread before the poll loop starts.
pub(crate) static RTMP_BRIDGE: StdMutex