Skip to content

Commit 2608566

Browse files
committed
fix(upnp): add exponential backoff for failed port mappings
1 parent 1755bca commit 2608566

File tree

2 files changed

+104
-33
lines changed

2 files changed

+104
-33
lines changed

protocols/upnp/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
checks active mappings on the gateway.
77
See [PR 6127](https://github.com/libp2p/rust-libp2p/pull/6127).
88

9+
- Fix excessive retry attempts for failed port mappings by implementing exponential backoff.
10+
Failed mappings now retry up to 5 times with increasing delays (30s to 480s) before giving up.
11+
This prevents continuous retry loops.
12+
See [PR 6128](https://github.com/libp2p/rust-libp2p/pull/6128).
13+
914
## 0.5.0
1015

1116
- update igd-next to 0.16.1

protocols/upnp/src/behaviour.rs

Lines changed: 99 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ const MAPPING_DURATION: u32 = 3600;
5353
/// Renew the Mapping every half of `MAPPING_DURATION` to avoid the port being unmapped.
5454
const MAPPING_TIMEOUT: u64 = MAPPING_DURATION as u64 / 2;
5555

56+
/// Maximum number of retry attempts for failed mappings.
57+
const MAX_RETRY_ATTEMPTS: u32 = 5;
58+
59+
/// Base delay in seconds for exponential backoff (will be multiplied by 2^retry_count).
60+
const BASE_RETRY_DELAY_SECS: u64 = 30;
61+
62+
/// Maximum delay in seconds between retry attempts.
63+
const MAX_RETRY_DELAY_SECS: u64 = 1800;
64+
5665
/// A [`Gateway`] Request.
5766
#[derive(Debug)]
5867
pub(crate) enum GatewayRequest {
@@ -125,8 +134,13 @@ enum MappingState {
125134
Pending,
126135
/// Port mapping is active with the inner timeout.
127136
Active(Delay),
128-
/// Port mapping failed, we will try again.
129-
Failed,
137+
/// Port mapping failed with retry information.
138+
Failed {
139+
/// Number of times we've tried to map this port.
140+
retry_count: u32,
141+
/// When we should try again (None means no more retries).
142+
next_retry: Option<Delay>,
143+
},
130144
}
131145

132146
/// Current state of the UPnP [`Gateway`].
@@ -174,7 +188,7 @@ impl MappingList {
174188
fn renew(&mut self, gateway: &mut Gateway, cx: &mut Context<'_>) {
175189
for (mapping, state) in self.iter_mut() {
176190
match state {
177-
MappingState::Inactive | MappingState::Failed => {
191+
MappingState::Inactive => {
178192
let duration = MAPPING_DURATION;
179193
if let Err(err) = gateway.sender.try_send(GatewayRequest::AddMapping {
180194
mapping: mapping.clone(),
@@ -185,8 +199,32 @@ impl MappingList {
185199
"could not request port mapping for multiaddress on the gateway: {}",
186200
err
187201
);
202+
} else {
203+
*state = MappingState::Pending;
204+
}
205+
}
206+
MappingState::Failed {
207+
retry_count,
208+
next_retry,
209+
} => {
210+
if let Some(delay) = next_retry {
211+
if Pin::new(delay).poll(cx).is_ready() {
212+
let duration = MAPPING_DURATION;
213+
if let Err(err) = gateway.sender.try_send(GatewayRequest::AddMapping {
214+
mapping: mapping.clone(),
215+
duration,
216+
}) {
217+
tracing::debug!(
218+
multiaddress=%mapping.multiaddr,
219+
retry_count=%retry_count,
220+
"could not retry port mapping for multiaddress on the gateway: {}",
221+
err
222+
);
223+
} else {
224+
*state = MappingState::Pending;
225+
}
226+
}
188227
}
189-
*state = MappingState::Pending;
190228
}
191229
MappingState::Active(timeout) => {
192230
if Pin::new(timeout).poll(cx).is_ready() {
@@ -453,36 +491,64 @@ impl NetworkBehaviour for Behaviour {
453491
}
454492
}
455493
GatewayEvent::MapFailure(mapping, err) => {
456-
match self
457-
.mappings
458-
.insert(mapping.clone(), MappingState::Failed)
459-
.expect("mapping should exist")
460-
{
461-
MappingState::Active(_) => {
462-
tracing::debug!(
463-
address=%mapping.internal_addr,
464-
protocol=%mapping.protocol,
465-
"failed to remap UPnP mapped for protocol: {err}"
466-
);
467-
let external_multiaddr =
468-
mapping.external_addr(gateway.external_addr);
469-
self.pending_events.push_back(Event::ExpiredExternalAddr(
470-
external_multiaddr.clone(),
471-
));
472-
return Poll::Ready(ToSwarm::ExternalAddrExpired(
473-
external_multiaddr,
474-
));
475-
}
476-
MappingState::Pending => {
477-
tracing::debug!(
478-
address=%mapping.internal_addr,
479-
protocol=%mapping.protocol,
480-
"failed to map UPnP mapped for protocol: {err}"
481-
);
482-
}
483-
_ => {
484-
unreachable!()
494+
let prev_state =
495+
self.mappings.get(&mapping).expect("mapping should exist");
496+
497+
let (retry_count, was_active) = match prev_state {
498+
MappingState::Active(_) => (0, true),
499+
MappingState::Pending => (0, false),
500+
MappingState::Failed { retry_count, .. } => {
501+
(*retry_count, false)
485502
}
503+
_ => unreachable!(),
504+
};
505+
506+
let new_retry_count = retry_count + 1;
507+
let next_retry = if new_retry_count < MAX_RETRY_ATTEMPTS {
508+
let delay_secs = std::cmp::min(
509+
BASE_RETRY_DELAY_SECS
510+
.saturating_mul(2_u64.pow(retry_count)),
511+
MAX_RETRY_DELAY_SECS,
512+
);
513+
Some(Delay::new(Duration::from_secs(delay_secs)))
514+
} else {
515+
tracing::warn!(
516+
address=%mapping.internal_addr,
517+
protocol=%mapping.protocol,
518+
"giving up on UPnP mapping after {new_retry_count} attempts"
519+
);
520+
None
521+
};
522+
523+
self.mappings.insert(
524+
mapping.clone(),
525+
MappingState::Failed {
526+
retry_count: new_retry_count,
527+
next_retry,
528+
},
529+
);
530+
531+
if was_active {
532+
tracing::debug!(
533+
address=%mapping.internal_addr,
534+
protocol=%mapping.protocol,
535+
"failed to remap UPnP mapped for protocol: {err}"
536+
);
537+
let external_multiaddr =
538+
mapping.external_addr(gateway.external_addr);
539+
self.pending_events.push_back(Event::ExpiredExternalAddr(
540+
external_multiaddr.clone(),
541+
));
542+
return Poll::Ready(ToSwarm::ExternalAddrExpired(
543+
external_multiaddr,
544+
));
545+
} else {
546+
tracing::debug!(
547+
address=%mapping.internal_addr,
548+
protocol=%mapping.protocol,
549+
retry_count=%new_retry_count,
550+
"failed to map UPnP mapped for protocol: {err}"
551+
);
486552
}
487553
}
488554
GatewayEvent::Removed(mapping) => {

0 commit comments

Comments
 (0)