Implement IPv6 support

This commit is contained in:
Cameron Gutman 2023-08-26 14:10:29 -05:00
parent 7662fe8616
commit 62a5cd959a
10 changed files with 214 additions and 27 deletions

View File

@ -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
^^^^

View File

@ -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);

View File

@ -155,6 +155,8 @@ namespace config {
} cmd;
std::uint16_t port;
std::string address_family;
std::string log_file;
std::vector<prep_cmd_t> prep_cmds;

View File

@ -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<bool>(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) {

View File

@ -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

View File

@ -6,6 +6,8 @@
#include <tuple>
#include <boost/asio.hpp>
#include <enet/enet.h>
#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

View File

@ -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.<xmlattr>.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.<xmlattr>.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<SimpleWeb::HTTP>;
@ -1035,7 +1051,7 @@ namespace nvhttp {
http_server.resource["^/pin/([0-9]+)$"]["GET"] = pin<SimpleWeb::HTTP>;
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) {

View File

@ -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);

View File

@ -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();

View File

@ -607,6 +607,19 @@
</div>
</div>
<div v-if="currentTab === 'advanced'" class="config-page">
<!--Address family-->
<div class="mb-3">
<label for="address_family" class="form-label">Address Family</label>
<select
id="address_family"
class="form-select"
v-model="config.address_family"
>
<option value="ipv4">IPv4 only</option>
<option value="both">IPv4+IPv6</option>
</select>
<div class="form-text">Set the address family used by Sunshine</div>
</div>
<!--Port family-->
<div class="mb-3">
<label for="port" class="form-label">Port</label>
@ -1027,6 +1040,7 @@
<script>
// create dictionary for defaultConfig
const defaultConfig = {
"address_family": "ipv4",
"always_send_scancodes": "enabled",
"amd_coder": "auto",
"amd_preanalysis": "disabled",