diff --git a/src/upnp.cpp b/src/upnp.cpp index 34c8d406..6fc5a130 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -20,6 +20,9 @@ using namespace std::literals; namespace upnp { constexpr auto INET6_ADDRESS_STRLEN = 46; + constexpr auto PORT_MAPPING_LIFETIME = 3600s; + constexpr auto REFRESH_INTERVAL = 120s; + constexpr auto IPv4 = 0; constexpr auto IPv6 = 1; @@ -33,50 +36,10 @@ namespace upnp { struct { std::string wan; std::string lan; + std::string proto; } port; std::string description; - bool tcp; - }; - - void - unmap( - const urls_t &urls, - const IGDdatas &data, - std::vector::const_reverse_iterator begin, - std::vector::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::const_reverse_iterator; - deinit_t(urls_t &&urls, IGDdatas data, std::vector &&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; }; static std::string_view @@ -95,98 +58,239 @@ namespace upnp { 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(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 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 mappings; + std::thread upnp_thread; + }; + std::unique_ptr start() { if (!config::sunshine.flags[config::flag::UPNP]) { return nullptr; } - int err {}; - - 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 lan_addr; - std::array 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 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(std::move(urls), data, std::move(mappings)); + return std::make_unique(); } } // namespace upnp