From 62a5cd959a86f5a70a42290c7859775857235e6f Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 26 Aug 2023 14:10:29 -0500 Subject: [PATCH] Implement IPv6 support --- docs/source/about/advanced_usage.rst | 24 +++++++ src/config.cpp | 5 +- src/config.h | 2 + src/confighttp.cpp | 9 +-- src/network.cpp | 90 +++++++++++++++++++++++- src/network.h | 43 ++++++++++- src/nvhttp.cpp | 30 ++++++-- src/rtsp.cpp | 8 +-- src/stream.cpp | 16 +++-- src_assets/common/assets/web/config.html | 14 ++++ 10 files changed, 214 insertions(+), 27 deletions(-) diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index b8d15289..9730b374 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -557,6 +557,30 @@ port port = 47989 +address_family +^^^^^^^^^^^^^^ + +**Description** + Set the address family that Sunshine will use. + +.. table:: + :widths: auto + + ===== =========== + Value Description + ===== =========== + ipv4 IPv4 only + both IPv4+IPv6 + ===== =========== + +**Default** + ``ipv4`` + +**Example** + .. code-block:: text + + address_family = both + pkey ^^^^ diff --git a/src/config.cpp b/src/config.cpp index f15eacb4..ecb3d232 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -511,7 +511,8 @@ namespace config { {}, // Password Salt platf::appdata().string() + "/sunshine.conf", // config file {}, // cmd args - 47989, + 47989, // Base port number + "ipv4", // Address family platf::appdata().string() + "/sunshine.log", // log file {}, // prep commands }; @@ -1110,6 +1111,8 @@ namespace config { int_f(vars, "port"s, port); sunshine.port = (std::uint16_t) port; + string_restricted_f(vars, "address_family", sunshine.address_family, { "ipv4"sv, "both"sv }); + bool upnp = false; bool_f(vars, "upnp"s, upnp); diff --git a/src/config.h b/src/config.h index b23b59cc..23673e1e 100644 --- a/src/config.h +++ b/src/config.h @@ -155,6 +155,8 @@ namespace config { } cmd; std::uint16_t port; + std::string address_family; + std::string log_file; std::vector prep_cmds; diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 6e8b2393..ba7c21ee 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -76,7 +76,7 @@ namespace confighttp { void send_unauthorized(resp_https_t response, req_https_t request) { - auto address = request->remote_endpoint().address().to_string(); + auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; const SimpleWeb::CaseInsensitiveMultimap headers { { "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" } @@ -86,7 +86,7 @@ namespace confighttp { void send_redirect(resp_https_t response, req_https_t request, const char *path) { - auto address = request->remote_endpoint().address().to_string(); + auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; const SimpleWeb::CaseInsensitiveMultimap headers { { "Location", path } @@ -96,7 +96,7 @@ namespace confighttp { bool authenticate(resp_https_t response, req_https_t request) { - auto address = request->remote_endpoint().address().to_string(); + auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); auto ip_type = net::from_address(address); if (ip_type > http::origin_web_ui_allowed) { @@ -731,6 +731,7 @@ namespace confighttp { auto shutdown_event = mail::man->event(mail::shutdown); auto port_https = map_port(PORT_HTTPS); + auto address_family = net::af_from_enum_string(config::sunshine.address_family); https_server_t server { config::nvhttp.cert, config::nvhttp.pkey }; server.default_resource["GET"] = not_found; @@ -758,7 +759,7 @@ namespace confighttp { server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage; server.resource["^/node_modules\\/.+$"]["GET"] = getNodeModules; server.config.reuse_address = true; - server.config.address = "0.0.0.0"s; + server.config.address = net::af_to_any_address_string(address_family); server.config.port = port_https; auto accept_and_run = [&](auto *server) { diff --git a/src/network.cpp b/src/network.cpp index f530b0f3..87090015 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -102,12 +102,96 @@ namespace net { return "wan"sv; } + /** + * @brief Returns the `af_e` enum value for the `address_family` config option value. + * @param view The config option value. + * @return The `af_e` enum value. + */ + af_e + af_from_enum_string(const std::string_view &view) { + if (view == "ipv4") { + return IPV4; + } + if (view == "both") { + return BOTH; + } + + // avoid warning + return BOTH; + } + + /** + * @brief Returns the wildcard binding address for a given address family. + * @param af Address family. + * @return Normalized address. + */ + std::string_view + af_to_any_address_string(af_e af) { + switch (af) { + case IPV4: + return "0.0.0.0"sv; + case BOTH: + return "::"sv; + } + + // avoid warning + return "::"sv; + } + + /** + * @brief Converts an address to a normalized form. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize. + * @return Normalized address. + */ + boost::asio::ip::address + normalize_address(boost::asio::ip::address address) { + // Convert IPv6-mapped IPv4 addresses into regular IPv4 addresses + if (address.is_v6()) { + auto v6 = address.to_v6(); + if (v6.is_v4_mapped()) { + return boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, v6); + } + } + + return address; + } + + /** + * @brief Returns the given address in normalized string form. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize. + * @return Normalized address in string form. + */ + std::string + addr_to_normalized_string(boost::asio::ip::address address) { + return normalize_address(address).to_string(); + } + + /** + * @brief Returns the given address in a normalized form for in the host portion of a URL. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize and escape. + * @return Normalized address in URL-escaped string. + */ + std::string + addr_to_url_escaped_string(boost::asio::ip::address address) { + address = normalize_address(address); + if (address.is_v6()) { + return "["s + address.to_string() + ']'; + } + else { + return address.to_string(); + } + } + host_t - host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port) { - enet_address_set_host(&addr, "0.0.0.0"); + host_create(af_e af, ENetAddress &addr, std::size_t peers, std::uint16_t port) { + auto any_addr = net::af_to_any_address_string(af); + enet_address_set_host(&addr, any_addr.data()); enet_address_set_port(&addr, port); - return host_t { enet_host_create(AF_INET, &addr, peers, 0, 0, 0) }; + return host_t { enet_host_create(af == IPV4 ? AF_INET : AF_INET6, &addr, peers, 0, 0, 0) }; } void diff --git a/src/network.h b/src/network.h index e1ca36c7..b54f63ce 100644 --- a/src/network.h +++ b/src/network.h @@ -6,6 +6,8 @@ #include +#include + #include #include "utility.h" @@ -24,6 +26,11 @@ namespace net { WAN }; + enum af_e : int { + IPV4, + BOTH + }; + net_e from_enum_string(const std::string_view &view); std::string_view @@ -33,5 +40,39 @@ namespace net { from_address(const std::string_view &view); host_t - host_create(ENetAddress &addr, std::size_t peers, std::uint16_t port); + host_create(af_e af, ENetAddress &addr, std::size_t peers, std::uint16_t port); + + /** + * @brief Returns the `af_e` enum value for the `address_family` config option value. + * @param view The config option value. + * @return The `af_e` enum value. + */ + af_e + af_from_enum_string(const std::string_view &view); + + /** + * @brief Returns the wildcard binding address for a given address family. + * @param af Address family. + * @return Normalized address. + */ + std::string_view + af_to_any_address_string(af_e af); + + /** + * @brief Returns the given address in normalized string form. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize. + * @return Normalized address in string form. + */ + std::string + addr_to_normalized_string(boost::asio::ip::address address); + + /** + * @brief Returns the given address in a normalized form for in the host portion of a URL. + * @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses. + * @param address The address to normalize and escape. + * @return Normalized address in URL-escaped string. + */ + std::string + addr_to_url_escaped_string(boost::asio::ip::address address); } // namespace net diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index f52ddc07..962310f3 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -591,7 +591,7 @@ namespace nvhttp { response->close_connection_after_response = true; - auto address = request->remote_endpoint().address().to_string(); + auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); auto ip_type = net::from_address(address); if (ip_type > http::origin_pin_allowed) { BOOST_LOG(info) << "/pin: ["sv << address << "] -- denied"sv; @@ -639,9 +639,24 @@ namespace nvhttp { tree.put("root.uniqueid", http::unique_id); tree.put("root.HttpsPort", map_port(PORT_HTTPS)); tree.put("root.ExternalPort", map_port(PORT_HTTP)); - tree.put("root.mac", platf::get_mac_address(local_endpoint.address().to_string())); + tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0"); - tree.put("root.LocalIP", local_endpoint.address().to_string()); + + // Moonlight clients track LAN IPv6 addresses separately from LocalIP which is expected to + // always be an IPv4 address. If we return that same IPv6 address here, it will clobber the + // stored LAN IPv4 address. To avoid this, we need to return an IPv4 address in this field + // when we get a request over IPv6. + // + // HACK: We should return the IPv4 address of local interface here, but we don't currently + // have that implemented. For now, we will emulate the behavior of GFE+GS-IPv6-Forwarder, + // which returns 127.0.0.1 as LocalIP for IPv6 connections. Moonlight clients with IPv6 + // support know to ignore this bogus address. + if (local_endpoint.address().is_v6() && !local_endpoint.address().to_v6().is_v4_mapped()) { + tree.put("root.LocalIP", "127.0.0.1"); + } + else { + tree.put("root.LocalIP", net::addr_to_normalized_string(local_endpoint.address())); + } uint32_t codec_mode_flags = SCM_H264; if (video::active_hevc_mode >= 2) { @@ -800,7 +815,7 @@ namespace nvhttp { rtsp_stream::launch_session_raise(launch_session); tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.sessionUrl0", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.gamesession", 1); } @@ -871,7 +886,7 @@ namespace nvhttp { rtsp_stream::launch_session_raise(make_launch_session(host_audio, args)); tree.put("root..status_code", 200); - tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); + tree.put("root.sessionUrl0", "rtsp://"s + net::addr_to_url_escaped_string(request->local_endpoint().address()) + ':' + std::to_string(map_port(rtsp_stream::RTSP_SETUP_PORT))); tree.put("root.resume", 1); } @@ -934,6 +949,7 @@ namespace nvhttp { auto port_http = map_port(PORT_HTTP); auto port_https = map_port(PORT_HTTPS); + auto address_family = net::af_from_enum_string(config::sunshine.address_family); bool clean_slate = config::sunshine.flags[config::flag::FRESH_STATE]; @@ -1026,7 +1042,7 @@ namespace nvhttp { https_server.resource["^/cancel$"]["GET"] = cancel; https_server.config.reuse_address = true; - https_server.config.address = "0.0.0.0"s; + https_server.config.address = net::af_to_any_address_string(address_family); https_server.config.port = port_https; http_server.default_resource["GET"] = not_found; @@ -1035,7 +1051,7 @@ namespace nvhttp { http_server.resource["^/pin/([0-9]+)$"]["GET"] = pin; http_server.config.reuse_address = true; - http_server.config.address = "0.0.0.0"s; + http_server.config.address = net::af_to_any_address_string(address_family); http_server.config.port = port_http; auto accept_and_run = [&](auto *http_server) { diff --git a/src/rtsp.cpp b/src/rtsp.cpp index d0552fae..6e06c1fd 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -225,7 +225,7 @@ namespace rtsp_stream { } int - bind(std::uint16_t port, boost::system::error_code &ec) { + bind(net::af_e af, std::uint16_t port, boost::system::error_code &ec) { { auto lg = _session_slots.lock(); @@ -233,14 +233,14 @@ namespace rtsp_stream { _slot_count = config::stream.channels; } - acceptor.open(tcp::v4(), ec); + acceptor.open(af == net::IPV4 ? tcp::v4() : tcp::v6(), ec); if (ec) { return -1; } acceptor.set_option(boost::asio::socket_base::reuse_address { true }); - acceptor.bind(tcp::endpoint(tcp::v4(), port), ec); + acceptor.bind(tcp::endpoint(af == net::IPV4 ? tcp::v4() : tcp::v6(), port), ec); if (ec) { return -1; } @@ -766,7 +766,7 @@ namespace rtsp_stream { server.map("PLAY"sv, &cmd_play); boost::system::error_code ec; - if (server.bind(map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { + if (server.bind(net::af_from_enum_string(config::sunshine.address_family), map_port(rtsp_stream::RTSP_SETUP_PORT), ec)) { BOOST_LOG(fatal) << "Couldn't bind RTSP server to port ["sv << map_port(rtsp_stream::RTSP_SETUP_PORT) << "], " << ec.message(); shutdown_event->raise(true); diff --git a/src/stream.cpp b/src/stream.cpp index 036f635a..8b8497e2 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -257,8 +257,8 @@ namespace stream { class control_server_t { public: int - bind(std::uint16_t port) { - _host = net::host_create(_addr, config::stream.channels, port); + bind(net::af_e address_family, std::uint16_t port) { + _host = net::host_create(address_family, _addr, config::stream.channels, port); return !(bool) _host; } @@ -1442,39 +1442,41 @@ namespace stream { int start_broadcast(broadcast_ctx_t &ctx) { + auto address_family = net::af_from_enum_string(config::sunshine.address_family); + auto protocol = address_family == net::IPV4 ? udp::v4() : udp::v6(); auto control_port = map_port(CONTROL_PORT); auto video_port = map_port(VIDEO_STREAM_PORT); auto audio_port = map_port(AUDIO_STREAM_PORT); - if (ctx.control_server.bind(control_port)) { + if (ctx.control_server.bind(address_family, control_port)) { BOOST_LOG(error) << "Couldn't bind Control server to port ["sv << control_port << "], likely another process already bound to the port"sv; return -1; } boost::system::error_code ec; - ctx.video_sock.open(udp::v4(), ec); + ctx.video_sock.open(protocol, ec); if (ec) { BOOST_LOG(fatal) << "Couldn't open socket for Video server: "sv << ec.message(); return -1; } - ctx.video_sock.bind(udp::endpoint(udp::v4(), video_port), ec); + ctx.video_sock.bind(udp::endpoint(protocol, video_port), ec); if (ec) { BOOST_LOG(fatal) << "Couldn't bind Video server to port ["sv << video_port << "]: "sv << ec.message(); return -1; } - ctx.audio_sock.open(udp::v4(), ec); + ctx.audio_sock.open(protocol, ec); if (ec) { BOOST_LOG(fatal) << "Couldn't open socket for Audio server: "sv << ec.message(); return -1; } - ctx.audio_sock.bind(udp::endpoint(udp::v4(), audio_port), ec); + ctx.audio_sock.bind(udp::endpoint(protocol, audio_port), ec); if (ec) { BOOST_LOG(fatal) << "Couldn't bind Audio server to port ["sv << audio_port << "]: "sv << ec.message(); diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index e0bb751d..b4fc94be 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -607,6 +607,19 @@
+ +
+ + +
Set the address family used by Sunshine
+
@@ -1027,6 +1040,7 @@