From 34b09674425642e8dead2fa94685ff50b9630e37 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Fri, 6 Feb 2026 13:07:47 +0100 Subject: [PATCH 1/2] fix(netwatch): fix socket rebind recovery and AddrInUse on Wine Two fixes for socket rebinding: 1. Allow rebinding from Closed state: previously, if a rebind failed (e.g. AddrInUse), the socket was permanently stuck in Closed state and all subsequent rebind attempts would fail with 'socket is closed and cannot be rebound'. Now Closed state stores the bind address and allows retry. 2. Set SO_REUSEADDR before binding: on Wine, socket close may not be instantaneous, so rebinding to the same port immediately after dropping the old socket can fail with AddrInUse (OS error 10048). SO_REUSEADDR allows the new socket to bind even if the old one hasn't fully released the port yet. --- netwatch/src/udp.rs | 48 ++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/netwatch/src/udp.rs b/netwatch/src/udp.rs index bbc1a8f..bbc0c78 100644 --- a/netwatch/src/udp.rs +++ b/netwatch/src/udp.rs @@ -688,6 +688,8 @@ enum SocketState { addr: SocketAddr, }, Closed { + /// The addr to rebind to when recovering. + addr: SocketAddr, last_max_gso_segments: NonZeroUsize, last_gro_segments: NonZeroUsize, last_may_fragment: bool, @@ -736,6 +738,10 @@ impl SocketState { socket.set_only_v6(true)?; } + // Allow rebinding to the same port if the previous socket hasn't fully closed yet. + // This is particularly needed under Wine where socket close may not be instantaneous. + socket.set_reuse_address(true)?; + // Binding must happen before calling quinn, otherwise `local_addr` // is not yet available on all OSes. socket.bind(&addr.into())?; @@ -768,25 +774,34 @@ impl SocketState { } fn rebind(&mut self) -> io::Result<()> { - let (addr, closed_state) = match self { - Self::Connected { state, addr, .. } => { - let s = SocketState::Closed { - last_max_gso_segments: state.max_gso_segments(), - last_gro_segments: state.gro_segments(), - last_may_fragment: state.may_fragment(), - }; - (*addr, s) - } - Self::Closed { .. } => { - return Err(io::Error::other("socket is closed and cannot be rebound")); - } + let addr = match self { + Self::Connected { addr, .. } => *addr, + Self::Closed { addr, .. } => *addr, }; debug!("rebinding {}", addr); - *self = closed_state; - *self = Self::bind(addr)?; + // Transition to Closed first to drop the old socket. + // This is needed so the port is released before we try to bind again. + if let Self::Connected { state, .. } = self { + *self = SocketState::Closed { + addr, + last_max_gso_segments: state.max_gso_segments(), + last_gro_segments: state.gro_segments(), + last_may_fragment: state.may_fragment(), + }; + } - Ok(()) + match Self::bind(addr) { + Ok(new_state) => { + *self = new_state; + Ok(()) + } + Err(err) => { + // Stay in Closed state but allow future rebind attempts + debug!("rebind failed, will retry on next attempt: {}", err); + Err(err) + } + } } fn is_closed(&self) -> bool { @@ -795,8 +810,9 @@ impl SocketState { fn close(&mut self) -> Option<(tokio::net::UdpSocket, quinn_udp::UdpSocketState)> { match self { - Self::Connected { state, .. } => { + Self::Connected { state, addr, .. } => { let s = SocketState::Closed { + addr: *addr, last_max_gso_segments: state.max_gso_segments(), last_gro_segments: state.gro_segments(), last_may_fragment: state.may_fragment(), From c0883a74a091e73811cbbbc4d23a141885f91a5d Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Fri, 6 Feb 2026 13:10:45 +0100 Subject: [PATCH 2/2] fix(netwatch): remove SO_REUSEADDR, rely on Closed-state retry SO_REUSEADDR on UDP sockets has security implications: it allows other processes to bind to the same port and steal packets. Instead, rely on the Closed-state recovery fix to retry rebinding on subsequent network change events. --- netwatch/src/udp.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/netwatch/src/udp.rs b/netwatch/src/udp.rs index bbc0c78..6cfc278 100644 --- a/netwatch/src/udp.rs +++ b/netwatch/src/udp.rs @@ -738,10 +738,6 @@ impl SocketState { socket.set_only_v6(true)?; } - // Allow rebinding to the same port if the previous socket hasn't fully closed yet. - // This is particularly needed under Wine where socket close may not be instantaneous. - socket.set_reuse_address(true)?; - // Binding must happen before calling quinn, otherwise `local_addr` // is not yet available on all OSes. socket.bind(&addr.into())?;