Rewrite UPnP support
- Fixes port mappings expiring after a day - Fixes support for IGDv1 gateways - Fixes handling of gateway reboots/resets
This commit is contained in:
+232
-128
@@ -20,6 +20,9 @@ using namespace std::literals;
|
|||||||
namespace upnp {
|
namespace upnp {
|
||||||
constexpr auto INET6_ADDRESS_STRLEN = 46;
|
constexpr auto INET6_ADDRESS_STRLEN = 46;
|
||||||
|
|
||||||
|
constexpr auto PORT_MAPPING_LIFETIME = 3600s;
|
||||||
|
constexpr auto REFRESH_INTERVAL = 120s;
|
||||||
|
|
||||||
constexpr auto IPv4 = 0;
|
constexpr auto IPv4 = 0;
|
||||||
constexpr auto IPv6 = 1;
|
constexpr auto IPv6 = 1;
|
||||||
|
|
||||||
@@ -33,50 +36,10 @@ namespace upnp {
|
|||||||
struct {
|
struct {
|
||||||
std::string wan;
|
std::string wan;
|
||||||
std::string lan;
|
std::string lan;
|
||||||
|
std::string proto;
|
||||||
} port;
|
} port;
|
||||||
|
|
||||||
std::string description;
|
std::string description;
|
||||||
bool tcp;
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
unmap(
|
|
||||||
const urls_t &urls,
|
|
||||||
const IGDdatas &data,
|
|
||||||
std::vector<mapping_t>::const_reverse_iterator begin,
|
|
||||||
std::vector<mapping_t>::const_reverse_iterator end) {
|
|
||||||
BOOST_LOG(debug) << "Unmapping UPNP ports"sv;
|
|
||||||
|
|
||||||
for (auto it = begin; it != end; ++it) {
|
|
||||||
auto status = UPNP_DeletePortMapping(
|
|
||||||
urls->controlURL,
|
|
||||||
data.first.servicetype,
|
|
||||||
it->port.wan.c_str(),
|
|
||||||
it->tcp ? "TCP" : "UDP",
|
|
||||||
nullptr);
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
BOOST_LOG(warning) << "Failed to unmap port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class deinit_t: public platf::deinit_t {
|
|
||||||
public:
|
|
||||||
using iter_t = std::vector<mapping_t>::const_reverse_iterator;
|
|
||||||
deinit_t(urls_t &&urls, IGDdatas data, std::vector<mapping_t> &&mapping):
|
|
||||||
urls { std::move(urls) }, data { data }, mapping { std::move(mapping) } {}
|
|
||||||
|
|
||||||
~deinit_t() {
|
|
||||||
BOOST_LOG(info) << "Unmapping UPNP ports..."sv;
|
|
||||||
unmap(urls, data, std::rbegin(mapping), std::rend(mapping));
|
|
||||||
}
|
|
||||||
|
|
||||||
urls_t urls;
|
|
||||||
IGDdatas data;
|
|
||||||
|
|
||||||
std::vector<mapping_t> mapping;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::string_view
|
static std::string_view
|
||||||
@@ -95,98 +58,239 @@ namespace upnp {
|
|||||||
return "Unknown status"sv;
|
return "Unknown status"sv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class deinit_t: public platf::deinit_t {
|
||||||
|
public:
|
||||||
|
deinit_t() {
|
||||||
|
auto rtsp = std::to_string(::map_port(rtsp_stream::RTSP_SETUP_PORT));
|
||||||
|
auto video = std::to_string(::map_port(stream::VIDEO_STREAM_PORT));
|
||||||
|
auto audio = std::to_string(::map_port(stream::AUDIO_STREAM_PORT));
|
||||||
|
auto control = std::to_string(::map_port(stream::CONTROL_PORT));
|
||||||
|
auto gs_http = std::to_string(::map_port(nvhttp::PORT_HTTP));
|
||||||
|
auto gs_https = std::to_string(::map_port(nvhttp::PORT_HTTPS));
|
||||||
|
auto wm_http = std::to_string(::map_port(confighttp::PORT_HTTPS));
|
||||||
|
|
||||||
|
mappings.assign({
|
||||||
|
{ { rtsp, rtsp, "TCP"s }, "Sunshine - RTSP"s },
|
||||||
|
{ { video, video, "UDP"s }, "Sunshine - Video"s },
|
||||||
|
{ { audio, audio, "UDP"s }, "Sunshine - Audio"s },
|
||||||
|
{ { control, control, "UDP"s }, "Sunshine - Control"s },
|
||||||
|
{ { gs_http, gs_http, "TCP"s }, "Sunshine - Client HTTP"s },
|
||||||
|
{ { gs_https, gs_https, "TCP"s }, "Sunshine - Client HTTPS"s },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only map port for the Web Manager if it is configured to accept connection from WAN
|
||||||
|
if (net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) {
|
||||||
|
mappings.emplace_back(mapping_t { { wm_http, wm_http, "TCP"s }, "Sunshine - Web UI"s });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the mapping thread
|
||||||
|
upnp_thread = std::thread { &deinit_t::upnp_thread_proc, this };
|
||||||
|
}
|
||||||
|
|
||||||
|
~deinit_t() {
|
||||||
|
upnp_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Maps a port via UPnP.
|
||||||
|
* @param data IGDdatas from UPNP_GetValidIGD()
|
||||||
|
* @param urls urls_t from UPNP_GetValidIGD()
|
||||||
|
* @param lan_addr Local IP address to map to
|
||||||
|
* @param mapping Information about port to map
|
||||||
|
* @return `true` on success.
|
||||||
|
*/
|
||||||
|
bool
|
||||||
|
map_port(const IGDdatas &data, const urls_t &urls, const std::string &lan_addr, const mapping_t &mapping) {
|
||||||
|
char intClient[16];
|
||||||
|
char intPort[6];
|
||||||
|
char desc[80];
|
||||||
|
char enabled[4];
|
||||||
|
char leaseDuration[16];
|
||||||
|
bool indefinite = false;
|
||||||
|
|
||||||
|
// First check if this port is already mapped successfully
|
||||||
|
BOOST_LOG(debug) << "Checking for existing UPnP port mapping for "sv << mapping.port.wan;
|
||||||
|
auto err = UPNP_GetSpecificPortMappingEntry(
|
||||||
|
urls->controlURL,
|
||||||
|
data.first.servicetype,
|
||||||
|
// In params
|
||||||
|
mapping.port.wan.c_str(),
|
||||||
|
mapping.port.proto.c_str(),
|
||||||
|
nullptr,
|
||||||
|
// Out params
|
||||||
|
intClient, intPort, desc, enabled, leaseDuration);
|
||||||
|
if (err == 714) { // NoSuchEntryInArray
|
||||||
|
BOOST_LOG(debug) << "Mapping entry not found for "sv << mapping.port.wan;
|
||||||
|
}
|
||||||
|
else if (err == UPNPCOMMAND_SUCCESS) {
|
||||||
|
// Some routers change the description, so we can't check that here
|
||||||
|
if (!std::strcmp(intClient, lan_addr.c_str())) {
|
||||||
|
if (std::atoi(leaseDuration) == 0) {
|
||||||
|
BOOST_LOG(debug) << "Static mapping entry found for "sv << mapping.port.wan;
|
||||||
|
|
||||||
|
// It's a static mapping, so we're done here
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG(debug) << "Mapping entry found for "sv << mapping.port.wan << " ("sv << leaseDuration << " seconds remaining)"sv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG(warning) << "UPnP conflict detected with: "sv << intClient;
|
||||||
|
|
||||||
|
// Some UPnP IGDs won't let unauthenticated clients delete other conflicting port mappings
|
||||||
|
// for security reasons, but we will give it a try anyway.
|
||||||
|
err = UPNP_DeletePortMapping(
|
||||||
|
urls->controlURL,
|
||||||
|
data.first.servicetype,
|
||||||
|
mapping.port.wan.c_str(),
|
||||||
|
mapping.port.proto.c_str(),
|
||||||
|
nullptr);
|
||||||
|
if (err) {
|
||||||
|
BOOST_LOG(error) << "Unable to delete conflicting UPnP port mapping: "sv << err;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG(error) << "UPNP_GetSpecificPortMappingEntry() failed: "sv << err;
|
||||||
|
|
||||||
|
// If we get a strange error from the router, we'll assume it's some old broken IGDv1
|
||||||
|
// device and only use indefinite lease durations to hopefully avoid confusing it.
|
||||||
|
if (err != 606) { // Unauthorized
|
||||||
|
indefinite = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update the port mapping
|
||||||
|
auto mapping_period = std::to_string(indefinite ? 0 : PORT_MAPPING_LIFETIME.count());
|
||||||
|
err = UPNP_AddPortMapping(
|
||||||
|
urls->controlURL,
|
||||||
|
data.first.servicetype,
|
||||||
|
mapping.port.wan.c_str(),
|
||||||
|
mapping.port.lan.c_str(),
|
||||||
|
lan_addr.data(),
|
||||||
|
mapping.description.c_str(),
|
||||||
|
mapping.port.proto.c_str(),
|
||||||
|
nullptr,
|
||||||
|
mapping_period.c_str());
|
||||||
|
|
||||||
|
if (err != UPNPCOMMAND_SUCCESS && !indefinite) {
|
||||||
|
// This may be an old/broken IGD that doesn't like non-static mappings.
|
||||||
|
BOOST_LOG(debug) << "Trying static mapping after failure: "sv << err;
|
||||||
|
err = UPNP_AddPortMapping(
|
||||||
|
urls->controlURL,
|
||||||
|
data.first.servicetype,
|
||||||
|
mapping.port.wan.c_str(),
|
||||||
|
mapping.port.lan.c_str(),
|
||||||
|
lan_addr.data(),
|
||||||
|
mapping.description.c_str(),
|
||||||
|
mapping.port.proto.c_str(),
|
||||||
|
nullptr,
|
||||||
|
"0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
BOOST_LOG(error) << "Failed to map "sv << mapping.port.proto << ' ' << mapping.port.lan << ": "sv << err;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_LOG(debug) << "Successfully mapped "sv << mapping.port.proto << ' ' << mapping.port.lan;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unmaps all ports.
|
||||||
|
* @param data IGDdatas from UPNP_GetValidIGD()
|
||||||
|
* @param data urls_t from UPNP_GetValidIGD()
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
unmap_all_ports(const urls_t &urls, const IGDdatas &data) {
|
||||||
|
for (auto it = std::begin(mappings); it != std::end(mappings); ++it) {
|
||||||
|
auto status = UPNP_DeletePortMapping(
|
||||||
|
urls->controlURL,
|
||||||
|
data.first.servicetype,
|
||||||
|
it->port.wan.c_str(),
|
||||||
|
it->port.proto.c_str(),
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
if (status && status != 714) { // NoSuchEntryInArray
|
||||||
|
BOOST_LOG(warning) << "Failed to unmap "sv << it->port.proto << ' ' << it->port.lan << ": "sv << status;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG(debug) << "Successfully unmapped "sv << it->port.proto << ' ' << it->port.lan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Maintains UPnP port forwarding rules
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
upnp_thread_proc() {
|
||||||
|
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||||
|
bool mapped = false;
|
||||||
|
IGDdatas data;
|
||||||
|
urls_t mapped_urls;
|
||||||
|
|
||||||
|
// Refresh UPnP rules every few minutes. They can be lost if the router reboots,
|
||||||
|
// WAN IP address changes, or various other conditions.
|
||||||
|
do {
|
||||||
|
int err = 0;
|
||||||
|
device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) };
|
||||||
|
if (!device || err) {
|
||||||
|
BOOST_LOG(warning) << "Couldn't discover any UPNP devices"sv;
|
||||||
|
mapped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto dev = device.get(); dev != nullptr; dev = dev->pNext) {
|
||||||
|
BOOST_LOG(debug) << "Found device: "sv << dev->descURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<char, INET6_ADDRESS_STRLEN> lan_addr;
|
||||||
|
|
||||||
|
urls_t urls;
|
||||||
|
auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size());
|
||||||
|
if (status != 1 && status != 2) {
|
||||||
|
BOOST_LOG(error) << status_string(status);
|
||||||
|
mapped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string lan_addr_str { lan_addr.data() };
|
||||||
|
|
||||||
|
BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL;
|
||||||
|
|
||||||
|
for (auto it = std::begin(mappings); it != std::end(mappings) && !shutdown_event->peek(); ++it) {
|
||||||
|
map_port(data, urls, lan_addr_str, *it);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mapped) {
|
||||||
|
BOOST_LOG(info) << "Completed UPnP port mappings to "sv << lan_addr_str << " via "sv << urls->rootdescURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped = true;
|
||||||
|
mapped_urls = std::move(urls);
|
||||||
|
} while (!shutdown_event->view(REFRESH_INTERVAL));
|
||||||
|
|
||||||
|
if (mapped) {
|
||||||
|
// Unmap ports upon termination
|
||||||
|
BOOST_LOG(info) << "Unmapping UPNP ports..."sv;
|
||||||
|
unmap_all_ports(mapped_urls, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<mapping_t> mappings;
|
||||||
|
std::thread upnp_thread;
|
||||||
|
};
|
||||||
|
|
||||||
std::unique_ptr<platf::deinit_t>
|
std::unique_ptr<platf::deinit_t>
|
||||||
start() {
|
start() {
|
||||||
if (!config::sunshine.flags[config::flag::UPNP]) {
|
if (!config::sunshine.flags[config::flag::UPNP]) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
int err {};
|
return std::make_unique<deinit_t>();
|
||||||
|
|
||||||
device_t device { upnpDiscover(2000, nullptr, nullptr, 0, IPv4, 2, &err) };
|
|
||||||
if (!device || err) {
|
|
||||||
BOOST_LOG(error) << "Couldn't discover any UPNP devices"sv;
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto dev = device.get(); dev != nullptr; dev = dev->pNext) {
|
|
||||||
BOOST_LOG(debug) << "Found device: "sv << dev->descURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::array<char, INET6_ADDRESS_STRLEN> lan_addr;
|
|
||||||
std::array<char, INET6_ADDRESS_STRLEN> wan_addr;
|
|
||||||
|
|
||||||
urls_t urls;
|
|
||||||
IGDdatas data;
|
|
||||||
|
|
||||||
auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size());
|
|
||||||
if (status != 1 && status != 2) {
|
|
||||||
BOOST_LOG(error) << status_string(status);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOST_LOG(debug) << "Found valid IGD device: "sv << urls->rootdescURL;
|
|
||||||
|
|
||||||
if (UPNP_GetExternalIPAddress(urls->controlURL, data.first.servicetype, wan_addr.data())) {
|
|
||||||
BOOST_LOG(warning) << "Could not get external ip"sv;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
BOOST_LOG(debug) << "Found external ip: "sv << wan_addr.data();
|
|
||||||
if (config::nvhttp.external_ip.empty()) {
|
|
||||||
config::nvhttp.external_ip = wan_addr.data();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto rtsp = std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT));
|
|
||||||
auto video = std::to_string(map_port(stream::VIDEO_STREAM_PORT));
|
|
||||||
auto audio = std::to_string(map_port(stream::AUDIO_STREAM_PORT));
|
|
||||||
auto control = std::to_string(map_port(stream::CONTROL_PORT));
|
|
||||||
auto gs_http = std::to_string(map_port(nvhttp::PORT_HTTP));
|
|
||||||
auto gs_https = std::to_string(map_port(nvhttp::PORT_HTTPS));
|
|
||||||
auto wm_http = std::to_string(map_port(confighttp::PORT_HTTPS));
|
|
||||||
|
|
||||||
std::vector<mapping_t> mappings {
|
|
||||||
{ { rtsp, rtsp }, "RTSP setup port"s, true },
|
|
||||||
{ { video, video }, "Video stream port"s, false },
|
|
||||||
{ { audio, audio }, "Control stream port"s, false },
|
|
||||||
{ { control, control }, "Audio stream port"s, false },
|
|
||||||
{ { gs_http, gs_http }, "Gamestream http port"s, true },
|
|
||||||
{ { gs_https, gs_https }, "Gamestream https port"s, true },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only map port for the Web Manager if it is configured to accept connection from WAN
|
|
||||||
if (net::from_enum_string(config::nvhttp.origin_web_ui_allowed) > net::LAN) {
|
|
||||||
mappings.emplace_back(mapping_t { { wm_http, wm_http }, "Sunshine Web UI port"s, true });
|
|
||||||
}
|
|
||||||
|
|
||||||
auto it = std::begin(mappings);
|
|
||||||
|
|
||||||
status = 0;
|
|
||||||
for (; it != std::end(mappings); ++it) {
|
|
||||||
status = UPNP_AddPortMapping(
|
|
||||||
urls->controlURL,
|
|
||||||
data.first.servicetype,
|
|
||||||
it->port.wan.c_str(),
|
|
||||||
it->port.lan.c_str(),
|
|
||||||
lan_addr.data(),
|
|
||||||
it->description.c_str(),
|
|
||||||
it->tcp ? "TCP" : "UDP",
|
|
||||||
nullptr,
|
|
||||||
"86400");
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
BOOST_LOG(error) << "Failed to map port ["sv << it->port.wan << "] to port ["sv << it->port.lan << "]: error code ["sv << status << ']';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
unmap(urls, data, std::make_reverse_iterator(it), std::rend(mappings));
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::make_unique<deinit_t>(std::move(urls), data, std::move(mappings));
|
|
||||||
}
|
}
|
||||||
} // namespace upnp
|
} // namespace upnp
|
||||||
|
|||||||
Reference in New Issue
Block a user