feat: v0.7.2 — portmapper (UPnP+PCP+NAT-PMP), session-relay opt-in, URL Phase 1
Network/reachability improvements + a relay-privacy fix. Wire-compatible with v0.7.0/v0.7.1; no protocol changes. - Replace hand-rolled UPnP (igd-next) with the portmapper crate. All three protocols (UPnP-IGD / NAT-PMP / PCP) run in parallel, auto-renew internally. PCP adds IPv6 firewall pinholes and works on iOS without the multicast entitlement. Pulled in transitively via iroh already, no net dep growth. - Android: UPnP/PCP/NAT-PMP attempted on WiFi/Ethernet with a WifiManager.MulticastLock acquired for the lifetime of the mapping. Cellular skipped early (no UPnP/PCP gateway, avoid 3s discovery waste). - TCP port-mapping gate removed for mobile — phones with permissive NAT can now serve HTTP for direct browser fetches. - Anchor reachability watcher (bidirectional): clears is_anchor after >5min of no port mapping; restores it when the mapping comes back. Network roams self-heal without restart. Mobile never auto-anchors. - Session relay opt-in restored. relay.session_relay_enabled setting defaults OFF (anchors included — servers shouldn't silently burn bandwidth either). Gates both serving (can_accept_relay_pipe) and using (auto-fallback in node.rs). UI toggle in Settings. Relay-style signaling (RelayIntroduce / worm_lookup / N1-N3 shares) unaffected. - URL Phase 1: share links now contain only the post ID (itsgoin.net/p/<post>). Anchor handler already supported post-ID-only URLs (author was optional); just dropped the author hex from the generator. Older URLs with author hex continue to work. - Quick app close button in header (with confirm) — useful for stopping network activity between sessions on mobile. - JNI null-pointer guards on ndk_context handles in android_wifi.rs. MEMORY rule sharpened to distinguish session relay (byte pipe, opt-in) from relay-style signaling/discovery (always on). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
069257c2d8
commit
4706e81603
16 changed files with 696 additions and 340 deletions
|
|
@ -3692,8 +3692,14 @@ impl Node {
|
|||
}
|
||||
}
|
||||
|
||||
// Step 7: Session relay fallback — if intro was accepted but hole punch failed
|
||||
if let (Some(intro_id), Some(relay_peer)) = (last_intro_id, last_relay_peer) {
|
||||
// Step 7: Session relay fallback — only if BOTH the introducer
|
||||
// signaled relay availability AND this node has opted in to
|
||||
// using session relay (`relay.session_relay_enabled`). Default
|
||||
// is opt-out: hole-punch failure does NOT silently fall back
|
||||
// to byte-relaying through a third party.
|
||||
if !self.network.conn_handle().is_session_relay_enabled().await {
|
||||
debug!(target = hex::encode(peer_id), "Session relay opt-out — skipping relay fallback");
|
||||
} else if let (Some(intro_id), Some(relay_peer)) = (last_intro_id, last_relay_peer) {
|
||||
if last_relay_available {
|
||||
info!(
|
||||
target = hex::encode(peer_id),
|
||||
|
|
@ -4577,44 +4583,16 @@ impl Node {
|
|||
})
|
||||
}
|
||||
|
||||
/// Start UPnP lease renewal cycle. Renews every lease_secs/2.
|
||||
/// On 3 consecutive failures: clears is_anchor and logs a warning.
|
||||
/// No-op since v0.7.2: the `portmapper::Client` held inside the active
|
||||
/// `PortMapping` auto-renews internally in its own background service.
|
||||
/// Retained for API compatibility with callers in CLI and Tauri until
|
||||
/// they're cleaned up. Returns `None`.
|
||||
///
|
||||
/// TODO(v0.7.x): wire a watcher on `mapping.watch_external()` to clear
|
||||
/// anchor mode if the external address stays `None` for more than ~5min
|
||||
/// (parity with the old "3 renewal failures" behavior).
|
||||
pub fn start_upnp_renewal_cycle(&self) -> Option<tokio::task::JoinHandle<()>> {
|
||||
let mapping = self.network.upnp_mapping()?;
|
||||
let local_port = mapping.local_port;
|
||||
let external_port = mapping.external_addr.port();
|
||||
let interval_secs = (mapping.lease_secs / 2) as u64;
|
||||
let network = Arc::clone(&self.network);
|
||||
let alog = Arc::clone(&self.activity_log);
|
||||
|
||||
Some(tokio::spawn(async move {
|
||||
let mut interval =
|
||||
tokio::time::interval(std::time::Duration::from_secs(interval_secs));
|
||||
let mut consecutive_failures: u32 = 0;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if crate::upnp::renew_upnp_mapping(local_port, external_port).await {
|
||||
consecutive_failures = 0;
|
||||
debug!("UPnP: lease renewed (port {})", external_port);
|
||||
} else {
|
||||
consecutive_failures += 1;
|
||||
warn!("UPnP: renewal failed ({}/3)", consecutive_failures);
|
||||
if consecutive_failures >= 3 {
|
||||
network.clear_anchor();
|
||||
if let Ok(mut log) = alog.try_lock() {
|
||||
log.log(
|
||||
ActivityLevel::Warn,
|
||||
ActivityCategory::Connection,
|
||||
"UPnP lease lost after 3 renewal failures, auto-anchor disabled".into(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
warn!("UPnP: 3 consecutive renewal failures, auto-anchor disabled");
|
||||
return; // stop the cycle
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
None
|
||||
}
|
||||
|
||||
// --- HTTP Post Delivery ---
|
||||
|
|
@ -4669,34 +4647,21 @@ impl Node {
|
|||
})
|
||||
}
|
||||
|
||||
/// Start UPnP TCP lease renewal cycle alongside the UDP renewal.
|
||||
/// No-op since v0.7.2 — the TCP `portmapper::Client` auto-renews internally.
|
||||
pub fn start_upnp_tcp_renewal_cycle(&self) -> Option<tokio::task::JoinHandle<()>> {
|
||||
if !self.network.has_upnp_tcp() {
|
||||
return None;
|
||||
}
|
||||
let mapping = self.network.upnp_mapping()?;
|
||||
let local_port = mapping.local_port;
|
||||
let external_port = mapping.external_addr.port();
|
||||
let interval_secs = (mapping.lease_secs / 2) as u64;
|
||||
|
||||
Some(tokio::spawn(async move {
|
||||
let mut interval =
|
||||
tokio::time::interval(std::time::Duration::from_secs(interval_secs));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if !crate::upnp::renew_upnp_tcp_mapping(local_port, external_port).await {
|
||||
warn!("UPnP: TCP lease renewal failed");
|
||||
// Don't stop the cycle — TCP is best-effort
|
||||
}
|
||||
}
|
||||
}))
|
||||
None
|
||||
}
|
||||
|
||||
/// Generate a share link URL for a public post.
|
||||
/// Returns None if post is not public or not found.
|
||||
///
|
||||
/// URL Phase 1 (v0.7.2): the link contains only the post ID — no author
|
||||
/// hex, no node addresses. The receiving anchor (itsgoin.net) does the
|
||||
/// holder lookup itself and serves via redirect or QUIC-proxy fallback.
|
||||
/// Older URLs with `/{post_hex}/{author_hex}` continue to work — the
|
||||
/// web handler parses the author hex as optional.
|
||||
pub async fn generate_share_link(&self, post_id: &PostId) -> anyhow::Result<Option<String>> {
|
||||
// Look up the post to verify it's public and get the author
|
||||
let (post, visibility) = {
|
||||
let (_post, visibility) = {
|
||||
let store = self.storage.get().await;
|
||||
match store.get_post_with_visibility(post_id)? {
|
||||
Some(pv) => pv,
|
||||
|
|
@ -4709,8 +4674,7 @@ impl Node {
|
|||
}
|
||||
|
||||
let post_hex = hex::encode(post_id);
|
||||
let author_hex = hex::encode(post.author);
|
||||
Ok(Some(format!("https://itsgoin.net/p/{}/{}", post_hex, author_hex)))
|
||||
Ok(Some(format!("https://itsgoin.net/p/{}", post_hex)))
|
||||
}
|
||||
|
||||
// --- Engagement API ---
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue