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
|
|
@ -474,7 +474,7 @@ pub fn render_post_html(post: &crate::types::Post, _post_id: &[u8; 32], author_n
|
|||
let cid_hex = hex::encode(&att.cid);
|
||||
if att.mime_type.starts_with("video/") {
|
||||
attachments_html.push_str(&format!(
|
||||
r#"<video src="/b/{}" controls style="max-width:100%;margin:0.5rem 0;border-radius:8px"></video>"#,
|
||||
r#"<video src="/b/{}" controls preload="auto" playsinline style="max-width:100%;margin:0.5rem 0;border-radius:8px"></video>"#,
|
||||
cid_hex
|
||||
));
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,16 +28,72 @@ pub struct Storage {
|
|||
conn: Connection,
|
||||
}
|
||||
|
||||
/// Current schema version. Bump this when making schema or data changes
|
||||
/// that require migration. Old databases with a lower version will be migrated.
|
||||
/// If the gap is too large (major version mismatch), the DB is reset instead.
|
||||
const SCHEMA_VERSION: u32 = 2;
|
||||
|
||||
/// Minimum schema version we can migrate from. Anything older gets a full reset.
|
||||
const MIN_MIGRATABLE_VERSION: u32 = 1;
|
||||
|
||||
impl Storage {
|
||||
pub fn open(path: impl AsRef<Path>) -> anyhow::Result<Self> {
|
||||
let conn = Connection::open(path)?;
|
||||
let conn = Connection::open(path.as_ref())?;
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL;")?;
|
||||
|
||||
// Check schema version
|
||||
let db_version: u32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
|
||||
|
||||
if db_version > 0 && db_version < MIN_MIGRATABLE_VERSION {
|
||||
// Too old to migrate — reset the database
|
||||
tracing::warn!(
|
||||
db_version, current = SCHEMA_VERSION,
|
||||
"Database schema too old to migrate, resetting"
|
||||
);
|
||||
drop(conn);
|
||||
std::fs::remove_file(path.as_ref())?;
|
||||
let conn = Connection::open(path.as_ref())?;
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL;")?;
|
||||
let storage = Self { conn };
|
||||
storage.init_tables()?;
|
||||
storage.set_schema_version(SCHEMA_VERSION)?;
|
||||
return Ok(storage);
|
||||
}
|
||||
|
||||
let storage = Self { conn };
|
||||
storage.init_tables()?;
|
||||
storage.migrate()?;
|
||||
|
||||
if db_version < SCHEMA_VERSION {
|
||||
// Run version-specific data migrations
|
||||
storage.migrate_data(db_version)?;
|
||||
storage.set_schema_version(SCHEMA_VERSION)?;
|
||||
tracing::info!(from = db_version, to = SCHEMA_VERSION, "Database schema upgraded");
|
||||
}
|
||||
|
||||
Ok(storage)
|
||||
}
|
||||
|
||||
fn set_schema_version(&self, version: u32) -> anyhow::Result<()> {
|
||||
self.conn.pragma_update(None, "user_version", version)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Data migrations that run once when upgrading between schema versions.
|
||||
fn migrate_data(&self, from_version: u32) -> anyhow::Result<()> {
|
||||
if from_version < 2 {
|
||||
// v1 → v2: Clear stale N2/N3 entries and mesh_peers that may prevent
|
||||
// bootstrap reconnection. The node will rediscover peers on next startup.
|
||||
tracing::info!("Schema v2: clearing stale network state for fresh discovery");
|
||||
let _ = self.conn.execute_batch(
|
||||
"DELETE FROM reachable_n2;
|
||||
DELETE FROM reachable_n3;
|
||||
DELETE FROM mesh_peers;"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_tables(&self) -> anyhow::Result<()> {
|
||||
self.conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS posts (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "itsgoin-desktop"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"productName": "itsgoin",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"identifier": "com.itsgoin.app",
|
||||
"build": {
|
||||
"frontendDist": "../../frontend",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue