v0.3.3: Rate limiting, IPv6 fix, schema versioning, video preload, engagement propagation
Security & stability: - Incoming auth-fail rate limiting per source IP (3 attempts, then exponential backoff) - Schema versioning via PRAGMA user_version with migration framework Networking: - IPv6 http_addr fix: advertise actual public IPv6 instead of 0.0.0.0 - N2/N3 TTL reduced from 7 days to 5 hours - Full N1/N2 state re-broadcast every 4 hours - Bootstrap isolation recovery: 24h check with sticky N1 advertising - Bidirectional engagement propagation (upstream + downstream) - Auto downstream registration on pull sync and push notification - post_upstream table for CDN tree traversal Media & UI: - Video preload="auto" for share links and in-app blob URLs - Following: Online/Offline split with last-seen timestamps - DMs filtered from My Posts tab - Image lightbox, audio player, file attachments with download prompt - Share link unroutable address filtering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e6f55fb1d6
commit
8fad30cf95
8 changed files with 136 additions and 17 deletions
|
|
@ -360,9 +360,14 @@ impl Network {
|
|||
}
|
||||
return Some(bind.to_string());
|
||||
}
|
||||
// For public IPv6, the external addr is the bound socket addr
|
||||
// For public IPv6, use the actual public address from iroh + bound port
|
||||
if self.has_public_v6 {
|
||||
return self.endpoint.bound_sockets().first().map(|s| s.to_string());
|
||||
let port = self.endpoint.bound_sockets().first().map(|s| s.port()).unwrap_or(0);
|
||||
if let Some(public_v6) = self.endpoint.addr().ip_addrs()
|
||||
.find(|s| matches!(s.ip(), std::net::IpAddr::V6(_)) && is_publicly_routable(s))
|
||||
{
|
||||
return Some(std::net::SocketAddr::new(public_v6.ip(), port).to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -383,15 +388,55 @@ impl Network {
|
|||
/// only InitialExchange triggers mesh slot allocation.
|
||||
pub async fn run_accept_loop(self: Arc<Self>) -> anyhow::Result<()> {
|
||||
info!("Accepting incoming connections (v3 ephemeral)...");
|
||||
|
||||
// Rate limit: track auth failures per source IP
|
||||
let fail_tracker: Arc<tokio::sync::Mutex<std::collections::HashMap<std::net::IpAddr, (u32, tokio::time::Instant)>>> =
|
||||
Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new()));
|
||||
// Cleanup stale entries every 60s
|
||||
let ft_cleanup: Arc<tokio::sync::Mutex<std::collections::HashMap<std::net::IpAddr, (u32, tokio::time::Instant)>>> = Arc::clone(&fail_tracker);
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let mut ft = ft_cleanup.lock().await;
|
||||
let now = tokio::time::Instant::now();
|
||||
ft.retain(|_, (_, last)| now.duration_since(*last).as_secs() < 300);
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(incoming) = self.endpoint.accept().await {
|
||||
let this = Arc::clone(&self);
|
||||
let remote_sock = crate::connection::normalize_addr(incoming.remote_address());
|
||||
let ft = Arc::clone(&fail_tracker);
|
||||
|
||||
// Check if this IP is rate-limited before spawning
|
||||
{
|
||||
let tracker = ft.lock().await;
|
||||
if let Some((count, last)) = tracker.get(&remote_sock.ip()) {
|
||||
let elapsed = tokio::time::Instant::now().duration_since(*last);
|
||||
// Exponential backoff: block for 2^(failures-3) seconds after 3+ failures
|
||||
if *count >= 3 {
|
||||
let block_secs = 1u64 << (*count - 3).min(8); // max ~256s
|
||||
if elapsed.as_secs() < block_secs {
|
||||
// Silently drop — don't even log to avoid log spam
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
match incoming.await {
|
||||
Ok(conn) => {
|
||||
let remote = conn.remote_id();
|
||||
let remote_node_id = *remote.as_bytes();
|
||||
|
||||
// Successful connection — clear failure count
|
||||
{
|
||||
let mut tracker = ft.lock().await;
|
||||
tracker.remove(&remote_sock.ip());
|
||||
}
|
||||
|
||||
// Store peer with their address
|
||||
{
|
||||
let storage = this.storage.lock().await;
|
||||
|
|
@ -414,7 +459,16 @@ impl Network {
|
|||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = %e, "Failed to accept connection");
|
||||
// Track auth failure for this IP
|
||||
let mut tracker = ft.lock().await;
|
||||
let entry = tracker.entry(remote_sock.ip())
|
||||
.or_insert((0, tokio::time::Instant::now()));
|
||||
entry.0 += 1;
|
||||
entry.1 = tokio::time::Instant::now();
|
||||
if entry.0 <= 3 {
|
||||
warn!(error = %e, addr = %remote_sock.ip(), failures = entry.0, "Failed to accept connection");
|
||||
}
|
||||
// After 3 failures, stop logging (rate limited silently)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue