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:
Scott Reimers 2026-03-16 18:37:24 -04:00
parent e6f55fb1d6
commit 8fad30cf95
8 changed files with 136 additions and 17 deletions

View file

@ -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)
}
}
});