Skip to content

Commit 0d21523

Browse files
AmnaSneneFrançois-Simon Fauteux-Chapleau
authored and
François-Simon Fauteux-Chapleau
committed
upnp: add IGD discovery phase.
When the protocols start searching for IGD, the IGD discovery phase begins. During this phase, all requested mappings will be marked as pending. If an IGD is found, the pending mappings will be requested from the router. If the phase ends due to a timeout, the pending mappings will be marked as failed GitLab: #36 Change-Id: Icb19b05e40968d619cb032a949a8fe8acfef311b
1 parent 6f45861 commit 0d21523

File tree

5 files changed

+117
-33
lines changed

5 files changed

+117
-33
lines changed

include/upnp/upnp_context.h

+19
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class UpnpMappingObserver
7575
virtual void onMappingRequestFailed(const Mapping& map) = 0;
7676
virtual void onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map) = 0;
7777
virtual void onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& map) = 0;
78+
virtual void onIgdDiscoveryStarted() = 0;
7879
};
7980

8081
class UPnPContext : public UpnpMappingObserver
@@ -133,6 +134,11 @@ class UPnPContext : public UpnpMappingObserver
133134
});
134135
}
135136

137+
// Set the timeout for the IGD discovery process.
138+
// If the timeout expires and no valid IGD has been discovered,
139+
// then the state of all pending mappings is set to FAILED.
140+
void setIgdDiscoveryTimeout(std::chrono::milliseconds timeout);
141+
136142
private:
137143
// Initialization
138144
void init();
@@ -242,6 +248,9 @@ class UPnPContext : public UpnpMappingObserver
242248
// Callback used to report remove request status.
243249
void onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& map) override;
244250

251+
// Callback used to report the start of the discovery process: search for IGDs.
252+
void onIgdDiscoveryStarted() override;
253+
245254
private:
246255
UPnPContext(const UPnPContext&) = delete;
247256
UPnPContext(UPnPContext&&) = delete;
@@ -304,6 +313,16 @@ class UPnPContext : public UpnpMappingObserver
304313
// Shutdown synchronization
305314
bool shutdownComplete_ {false};
306315
bool shutdownTimedOut_ {false};
316+
317+
// IGD Discovery synchronization. This boolean indicates if the IGD discovery is in progress.
318+
bool igdDiscoveryInProgress_ {true};
319+
std::mutex igdDiscoveryMutex_;
320+
std::chrono::milliseconds igdDiscoveryTimeout_ {std::chrono::milliseconds(500)};
321+
322+
// End of the discovery process.
323+
void _endIgdDiscovery();
324+
325+
asio::steady_timer igdDiscoveryTimer_;
307326
};
308327

309328
} // namespace upnp

src/upnp/protocol/natpmp/nat_pmp.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,14 @@ void
189189
NatPmp::searchForIgd()
190190
{
191191
if (not initialized_) {
192+
observer_->onIgdDiscoveryStarted();
192193
initNatPmp();
193194
}
194195

195196
// Schedule a retry in case init failed.
196197
if (not initialized_) {
197198
if (igdSearchCounter_++ < MAX_RESTART_SEARCH_RETRIES) {
198199
if (logger_) logger_->debug("NAT-PMP: Start search for IGDs. Attempt {}", igdSearchCounter_);
199-
200200
// Cancel the current timer (if any) and re-schedule.
201201
searchForIgdTimer_.expires_after(NATPMP_SEARCH_RETRY_UNIT * igdSearchCounter_);
202202
searchForIgdTimer_.async_wait([w=weak()](const asio::error_code& ec) {

src/upnp/protocol/pupnp/pupnp.cpp

+34-28
Original file line numberDiff line numberDiff line change
@@ -240,41 +240,46 @@ PUPnP::terminate()
240240
if (logger_) logger_->debug("PUPnP: Instance {} terminated", fmt::ptr(this));
241241
}
242242

243+
void
244+
PUPnP::searchForDeviceAsync(const std::string& deviceType)
245+
{
246+
// Despite its name and the claim in the libupnp documentation that it "returns immediately",
247+
// the UpnpSearchAsync function isn't really async. This is because it tries to send multiple
248+
// copies of each search message and waits for a certain amount of time after sending each
249+
// copy. The number of copies is given by the NUM_SSDP_COPY macro, whose default value is 2,
250+
// and the waiting time is determined by the SSDP_PAUSE macro, whose default value is 100 (ms).
251+
// If both IPv4 and IPv6 are enabled, then UpnpSearchAsync sends 3 distinct messages (2 for IPv6
252+
// and 1 for IPv4), resulting in a total of 3 * 2 * 100 = 600 ms spent waiting by default.
253+
// This is why we put the call to UpnpSearchAsync on its own thread.
254+
dht::ThreadPool::io().run([w = weak_from_this(), deviceType] {
255+
auto sthis = std::static_pointer_cast<PUPnP>(w.lock());
256+
if (!sthis)
257+
return;
258+
259+
auto err = UpnpSearchAsync(sthis->ctrlptHandle_,
260+
SEARCH_TIMEOUT,
261+
deviceType.c_str(),
262+
sthis.get());
263+
if (err != UPNP_E_SUCCESS) {
264+
if (sthis->logger_)
265+
sthis->logger_->warn("PUPnP: Send search for {} failed. Error {:d}: {}",
266+
deviceType,
267+
err,
268+
UpnpGetErrorMessage(err));
269+
}
270+
});
271+
}
243272
void
244273
PUPnP::searchForDevices()
245274
{
246275
if (logger_) logger_->debug("PUPnP: Send IGD search request");
247276

248277
// Send out search for multiple types of devices, as some routers may possibly
249278
// only reply to one.
250-
251-
auto err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_ROOT_DEVICE, this);
252-
if (err != UPNP_E_SUCCESS) {
253-
if (logger_) logger_->warn("PUPnP: Send search for UPNP_ROOT_DEVICE failed. Error {:d}: {}",
254-
err,
255-
UpnpGetErrorMessage(err));
256-
}
257-
258-
err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_IGD_DEVICE, this);
259-
if (err != UPNP_E_SUCCESS) {
260-
if (logger_) logger_->warn("PUPnP: Send search for UPNP_IGD_DEVICE failed. Error {:d}: {}",
261-
err,
262-
UpnpGetErrorMessage(err));
263-
}
264-
265-
err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANIP_SERVICE, this);
266-
if (err != UPNP_E_SUCCESS) {
267-
if (logger_) logger_->warn("PUPnP: Send search for UPNP_WANIP_SERVICE failed. Error {:d}: {}",
268-
err,
269-
UpnpGetErrorMessage(err));
270-
}
271-
272-
err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANPPP_SERVICE, this);
273-
if (err != UPNP_E_SUCCESS) {
274-
if (logger_) logger_->warn("PUPnP: Send search for UPNP_WANPPP_SERVICE failed. Error {:d}: {}",
275-
err,
276-
UpnpGetErrorMessage(err));
277-
}
279+
searchForDeviceAsync(UPNP_ROOT_DEVICE);
280+
searchForDeviceAsync(UPNP_IGD_DEVICE);
281+
searchForDeviceAsync(UPNP_WANIP_SERVICE);
282+
searchForDeviceAsync(UPNP_WANPPP_SERVICE);
278283
}
279284

280285
void
@@ -340,6 +345,7 @@ PUPnP::searchForIgd()
340345
if (clientRegistered_) {
341346
assert(initialized_);
342347
searchForDevices();
348+
observer_->onIgdDiscoveryStarted();
343349
} else {
344350
if (logger_) logger_->warn("PUPnP: PUPNP not fully setup. Skipping the IGD search");
345351
}

src/upnp/protocol/pupnp/pupnp.h

+3
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ class PUPnP : public UPnPProtocol
134134
// Start search for UPNP devices
135135
void searchForDevices();
136136

137+
// Start search for UPNP device in a different thread
138+
void searchForDeviceAsync(const std::string& deviceType);
139+
137140
// Return true if it has at least one valid IGD.
138141
bool hasValidIgd() const;
139142

src/upnp/upnp_context.cpp

+60-4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ UPnPContext::UPnPContext(const std::shared_ptr<asio::io_context>& ioContext, con
5151
, renewalSchedulingTimer_(*ctx)
5252
, syncTimer_(*ctx)
5353
, connectivityChangedTimer_(*ctx)
54+
, igdDiscoveryTimer_(*ctx)
55+
5456
{
5557
if (logger_) logger_->debug("Creating UPnPContext instance [{}]", fmt::ptr(this));
5658

@@ -1095,6 +1097,51 @@ UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& ma
10951097
map->getNotifyCallback()(map);
10961098
}
10971099

1100+
void
1101+
UPnPContext::onIgdDiscoveryStarted()
1102+
{
1103+
std::lock_guard lock(igdDiscoveryMutex_);
1104+
igdDiscoveryInProgress_ = true;
1105+
if (logger_) logger_->debug("IGD Discovery started");
1106+
igdDiscoveryTimer_.expires_after(igdDiscoveryTimeout_);
1107+
igdDiscoveryTimer_.async_wait([this] (const asio::error_code& ec) {
1108+
if (ec != asio::error::operation_aborted && igdDiscoveryInProgress_) {
1109+
_endIgdDiscovery();
1110+
}
1111+
});
1112+
}
1113+
1114+
void
1115+
UPnPContext::_endIgdDiscovery()
1116+
{
1117+
std::lock_guard lockDiscovery_(igdDiscoveryMutex_);
1118+
igdDiscoveryInProgress_ = false;
1119+
if (logger_) logger_->debug("IGD Discovery ended");
1120+
if (isReady()) {
1121+
return;
1122+
}
1123+
// if there is no valid IGD, the pending mapping requests will be changed to failed
1124+
std::lock_guard lockMappings_(mappingMutex_);
1125+
PortType types[2] {PortType::TCP, PortType::UDP};
1126+
for (auto& type : types) {
1127+
const auto& mappingList = getMappingList(type);
1128+
for (auto const& [_, map] : mappingList) {
1129+
updateMappingState(map, MappingState::FAILED);
1130+
// Do not unregister the mapping, it's up to the controller to decide. It will be unregistered when the controller releases it.
1131+
// unregisterMapping(map) here will cause a deadlock because of the lock on mappingMutex_.
1132+
if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
1133+
map->toString());
1134+
}
1135+
}
1136+
}
1137+
1138+
void
1139+
UPnPContext::setIgdDiscoveryTimeout(std::chrono::milliseconds timeout)
1140+
{
1141+
std::lock_guard lock(igdDiscoveryMutex_);
1142+
igdDiscoveryTimeout_ = timeout;
1143+
}
1144+
10981145
Mapping::sharedPtr_t
10991146
UPnPContext::registerMapping(Mapping& map)
11001147
{
@@ -1123,14 +1170,23 @@ UPnPContext::registerMapping(Mapping& map)
11231170
assert(mapPtr);
11241171
}
11251172

1126-
// No available IGD. The pending mapping requests will be processed
1127-
// when an IGD becomes available
11281173
if (not isReady()) {
1129-
if (logger_) logger_->warn("No IGD available. Mapping will be requested when an IGD becomes available");
1174+
// There is no valid IGD available
1175+
std::lock_guard lock(igdDiscoveryMutex_);
1176+
// IGD discovery is in progress, the mapping request will be made once an IGD becomes available
1177+
if (igdDiscoveryInProgress_) {
1178+
if (logger_) logger_->debug("Request for mapping {} will be requested when an IGD becomes available",
1179+
map.toString());
1180+
} else {
1181+
// it's not in the IGD discovery phase, the mapping request will fail
1182+
if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
1183+
map.toString());
1184+
updateMappingState(mapPtr, MappingState::FAILED);
1185+
}
11301186
} else {
1187+
// There is a valid IGD available, request the mapping.
11311188
requestMapping(mapPtr);
11321189
}
1133-
11341190
return mapPtr;
11351191
}
11361192

0 commit comments

Comments
 (0)