mirror of
https://github.com/LizardByte/Sunshine.git
synced 2026-02-04 04:45:32 +00:00
feat(network): allow binding to specific interface (#4481)
This commit is contained in:
@ -1502,6 +1502,46 @@ editing the `conf` file in a text editor. Use the examples as reference.
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### bind_address
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
Set the IP address to bind Sunshine to. This is useful when you have multiple network interfaces
|
||||
and want to restrict Sunshine to a specific one. If not set, Sunshine will bind to all available
|
||||
interfaces (0.0.0.0 for IPv4 or :: for IPv6).
|
||||
<br><br>
|
||||
<strong>Note:</strong> The address must be valid for the system and must match the address family
|
||||
being used. When using IPv6, you can specify an IPv6 address even with address_family set to "both".
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td colspan="2">@code{}
|
||||
(empty - bind to all interfaces)
|
||||
@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example (IPv4)</td>
|
||||
<td colspan="2">@code{}
|
||||
bind_address = 192.168.1.100
|
||||
@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example (IPv6)</td>
|
||||
<td colspan="2">@code{}
|
||||
bind_address = 2001:db8::1
|
||||
@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example (Loopback)</td>
|
||||
<td colspan="2">@code{}
|
||||
bind_address = 127.0.0.1
|
||||
@endcode</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### port
|
||||
|
||||
<table>
|
||||
|
||||
@ -576,6 +576,7 @@ namespace config {
|
||||
{}, // cmd args
|
||||
47989, // Base port number
|
||||
"ipv4", // Address family
|
||||
{}, // Bind address
|
||||
platf::appdata().string() + "/sunshine.log", // log file
|
||||
false, // notify_pre_releases
|
||||
true, // system_tray
|
||||
@ -1242,6 +1243,7 @@ namespace config {
|
||||
sunshine.port = (std::uint16_t) port;
|
||||
|
||||
string_restricted_f(vars, "address_family", sunshine.address_family, {"ipv4"sv, "both"sv});
|
||||
string_f(vars, "bind_address", sunshine.bind_address);
|
||||
|
||||
bool upnp = false;
|
||||
bool_f(vars, "upnp"s, upnp);
|
||||
|
||||
@ -253,6 +253,7 @@ namespace config {
|
||||
|
||||
std::uint16_t port;
|
||||
std::string address_family;
|
||||
std::string bind_address;
|
||||
|
||||
std::string log_file;
|
||||
bool notify_pre_releases;
|
||||
|
||||
@ -1219,7 +1219,7 @@ namespace confighttp {
|
||||
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
|
||||
server.resource["^/assets\\/.+$"]["GET"] = getNodeModules;
|
||||
server.config.reuse_address = true;
|
||||
server.config.address = net::af_to_any_address_string(address_family);
|
||||
server.config.address = net::get_bind_address(address_family);
|
||||
server.config.port = port_https;
|
||||
|
||||
auto accept_and_run = [&](auto *server) {
|
||||
|
||||
@ -105,7 +105,7 @@ namespace net {
|
||||
return BOTH;
|
||||
}
|
||||
|
||||
std::string_view af_to_any_address_string(af_e af) {
|
||||
std::string_view af_to_any_address_string(const af_e af) {
|
||||
switch (af) {
|
||||
case IPV4:
|
||||
return "0.0.0.0"sv;
|
||||
@ -117,6 +117,16 @@ namespace net {
|
||||
return "::"sv;
|
||||
}
|
||||
|
||||
std::string get_bind_address(const af_e af) {
|
||||
// If bind_address is configured, use it
|
||||
if (!config::sunshine.bind_address.empty()) {
|
||||
return config::sunshine.bind_address;
|
||||
}
|
||||
|
||||
// Otherwise use the wildcard address for the given address family
|
||||
return std::string(af_to_any_address_string(af));
|
||||
}
|
||||
|
||||
boost::asio::ip::address normalize_address(boost::asio::ip::address address) {
|
||||
// Convert IPv6-mapped IPv4 addresses into regular IPv4 addresses
|
||||
if (address.is_v6()) {
|
||||
@ -159,8 +169,8 @@ namespace net {
|
||||
enet_initialize();
|
||||
});
|
||||
|
||||
auto any_addr = net::af_to_any_address_string(af);
|
||||
enet_address_set_host(&addr, any_addr.data());
|
||||
const auto bind_addr = net::get_bind_address(af);
|
||||
enet_address_set_host(&addr, bind_addr.c_str());
|
||||
enet_address_set_port(&addr, port);
|
||||
|
||||
// Maximum of 128 clients, which should be enough for anyone
|
||||
|
||||
@ -65,6 +65,13 @@ namespace net {
|
||||
*/
|
||||
std::string_view af_to_any_address_string(af_e af);
|
||||
|
||||
/**
|
||||
* @brief Get the binding address to use based on config.
|
||||
* @param af Address family.
|
||||
* @return The configured bind address or wildcard if not configured.
|
||||
*/
|
||||
std::string get_bind_address(af_e af);
|
||||
|
||||
/**
|
||||
* @brief Convert an address to a normalized form.
|
||||
* @details Normalization converts IPv4-mapped IPv6 addresses into IPv4 addresses.
|
||||
|
||||
@ -1158,7 +1158,7 @@ namespace nvhttp {
|
||||
https_server.resource["^/cancel$"]["GET"] = cancel;
|
||||
|
||||
https_server.config.reuse_address = true;
|
||||
https_server.config.address = net::af_to_any_address_string(address_family);
|
||||
https_server.config.address = net::get_bind_address(address_family);
|
||||
https_server.config.port = port_https;
|
||||
|
||||
http_server.default_resource["GET"] = not_found<SimpleWeb::HTTP>;
|
||||
@ -1168,7 +1168,7 @@ namespace nvhttp {
|
||||
};
|
||||
|
||||
http_server.config.reuse_address = true;
|
||||
http_server.config.address = net::af_to_any_address_string(address_family);
|
||||
http_server.config.address = net::get_bind_address(address_family);
|
||||
http_server.config.port = port_http;
|
||||
|
||||
auto accept_and_run = [&](auto *http_server) {
|
||||
|
||||
@ -412,7 +412,14 @@ namespace rtsp_stream {
|
||||
|
||||
acceptor.set_option(boost::asio::socket_base::reuse_address {true});
|
||||
|
||||
acceptor.bind(tcp::endpoint(af == net::IPV4 ? tcp::v4() : tcp::v6(), port), ec);
|
||||
auto bind_addr_str = net::get_bind_address(af);
|
||||
const auto bind_addr = boost::asio::ip::make_address(bind_addr_str, ec);
|
||||
if (ec) {
|
||||
BOOST_LOG(error) << "Invalid bind address: "sv << bind_addr_str << " - " << ec.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
acceptor.bind(tcp::endpoint(bind_addr, port), ec);
|
||||
if (ec) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -1721,7 +1721,14 @@ namespace stream {
|
||||
BOOST_LOG(error) << "Failed to set video socket send buffer size (SO_SENDBUF)";
|
||||
}
|
||||
|
||||
ctx.video_sock.bind(udp::endpoint(protocol, video_port), ec);
|
||||
auto bind_addr_str = net::get_bind_address(address_family);
|
||||
const auto bind_addr = boost::asio::ip::make_address(bind_addr_str, ec);
|
||||
if (ec) {
|
||||
BOOST_LOG(fatal) << "Invalid bind address: "sv << bind_addr_str << " - " << ec.message();
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx.video_sock.bind(udp::endpoint(bind_addr, video_port), ec);
|
||||
if (ec) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind Video server to port ["sv << video_port << "]: "sv << ec.message();
|
||||
|
||||
@ -1735,7 +1742,7 @@ namespace stream {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx.audio_sock.bind(udp::endpoint(protocol, audio_port), ec);
|
||||
ctx.audio_sock.bind(udp::endpoint(bind_addr, audio_port), ec);
|
||||
if (ec) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind Audio server to port ["sv << audio_port << "]: "sv << ec.message();
|
||||
|
||||
|
||||
@ -191,6 +191,7 @@
|
||||
options: {
|
||||
"upnp": "disabled",
|
||||
"address_family": "ipv4",
|
||||
"bind_address": "",
|
||||
"port": 47989,
|
||||
"origin_web_ui_allowed": "lan",
|
||||
"external_ip": "",
|
||||
|
||||
@ -33,6 +33,13 @@ const effectivePort = computed(() => +config.value?.port ?? defaultMoonlightPort
|
||||
<div class="form-text">{{ $t('config.address_family_desc') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Bind address -->
|
||||
<div class="mb-3">
|
||||
<label for="bind_address" class="form-label">{{ $t('config.bind_address') }}</label>
|
||||
<input type="text" class="form-control" id="bind_address" v-model="config.bind_address" />
|
||||
<div class="form-text">{{ $t('config.bind_address_desc') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Port family -->
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">{{ $t('config.port') }}</label>
|
||||
|
||||
@ -138,6 +138,8 @@
|
||||
"av1_mode_desc": "Allows the client to request AV1 Main 8-bit or 10-bit video streams. AV1 is more CPU-intensive to encode, so enabling this may reduce performance when using software encoding.",
|
||||
"back_button_timeout": "Home/Guide Button Emulation Timeout",
|
||||
"back_button_timeout_desc": "If the Back/Select button is held down for the specified number of milliseconds, a Home/Guide button press is emulated. If set to a value < 0 (default), holding the Back/Select button will not emulate the Home/Guide button.",
|
||||
"bind_address": "Bind address",
|
||||
"bind_address_desc": "Set the specific IP address Sunshine will bind to. If left blank, Sunshine will bind to all available addresses.",
|
||||
"capture": "Force a Specific Capture Method",
|
||||
"capture_desc": "On automatic mode Sunshine will use the first one that works. NvFBC requires patched nvidia drivers.",
|
||||
"cert": "Certificate",
|
||||
|
||||
@ -26,3 +26,115 @@ INSTANTIATE_TEST_SUITE_P(
|
||||
std::make_tuple(std::string(128, 'a'), std::string(63, 'a'))
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Test fixture for bind_address tests with setup/teardown
|
||||
*/
|
||||
class BindAddressTest: public ::testing::Test {
|
||||
protected:
|
||||
std::string original_bind_address;
|
||||
|
||||
void SetUp() override {
|
||||
// Save the original bind_address config
|
||||
original_bind_address = config::sunshine.bind_address;
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Restore the original bind_address config
|
||||
config::sunshine.bind_address = original_bind_address;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Test that get_bind_address returns wildcard when bind_address is not configured
|
||||
*/
|
||||
TEST_F(BindAddressTest, DefaultBehaviorIPv4) {
|
||||
// Clear bind_address to test the default behavior
|
||||
config::sunshine.bind_address = "";
|
||||
|
||||
const auto bind_addr = net::get_bind_address(net::af_e::IPV4);
|
||||
ASSERT_EQ(bind_addr, "0.0.0.0");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that get_bind_address returns wildcard when bind_address is not configured (IPv6)
|
||||
*/
|
||||
TEST_F(BindAddressTest, DefaultBehaviorIPv6) {
|
||||
// Clear bind_address to test the default behavior
|
||||
config::sunshine.bind_address = "";
|
||||
|
||||
const auto bind_addr = net::get_bind_address(net::af_e::BOTH);
|
||||
ASSERT_EQ(bind_addr, "::");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that get_bind_address returns configured IPv4 address
|
||||
*/
|
||||
TEST_F(BindAddressTest, ConfiguredIPv4Address) {
|
||||
// Set a specific IPv4 address
|
||||
config::sunshine.bind_address = "192.168.1.100";
|
||||
|
||||
const auto bind_addr = net::get_bind_address(net::af_e::IPV4);
|
||||
ASSERT_EQ(bind_addr, "192.168.1.100");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that get_bind_address returns configured IPv6 address
|
||||
*/
|
||||
TEST_F(BindAddressTest, ConfiguredIPv6Address) {
|
||||
// Set a specific IPv6 address
|
||||
config::sunshine.bind_address = "::1";
|
||||
|
||||
const auto bind_addr = net::get_bind_address(net::af_e::BOTH);
|
||||
ASSERT_EQ(bind_addr, "::1");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that get_bind_address returns configured address regardless of address family
|
||||
*/
|
||||
TEST_F(BindAddressTest, ConfiguredAddressOverridesFamily) {
|
||||
// Set a specific IPv6 address but request IPv4 family
|
||||
// The configured address should still be returned
|
||||
config::sunshine.bind_address = "2001:db8::1";
|
||||
|
||||
const auto bind_addr = net::get_bind_address(net::af_e::IPV4);
|
||||
ASSERT_EQ(bind_addr, "2001:db8::1");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test with loopback addresses
|
||||
*/
|
||||
TEST_F(BindAddressTest, LoopbackAddresses) {
|
||||
// Test IPv4 loopback
|
||||
config::sunshine.bind_address = "127.0.0.1";
|
||||
const auto bind_addr_v4 = net::get_bind_address(net::af_e::IPV4);
|
||||
ASSERT_EQ(bind_addr_v4, "127.0.0.1");
|
||||
|
||||
// Test IPv6 loopback
|
||||
config::sunshine.bind_address = "::1";
|
||||
const auto bind_addr_v6 = net::get_bind_address(net::af_e::BOTH);
|
||||
ASSERT_EQ(bind_addr_v6, "::1");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test with link-local addresses
|
||||
*/
|
||||
TEST_F(BindAddressTest, LinkLocalAddresses) {
|
||||
// Test IPv4 link-local
|
||||
config::sunshine.bind_address = "169.254.1.1";
|
||||
const auto bind_addr_v4 = net::get_bind_address(net::af_e::IPV4);
|
||||
ASSERT_EQ(bind_addr_v4, "169.254.1.1");
|
||||
|
||||
// Test IPv6 link-local
|
||||
config::sunshine.bind_address = "fe80::1";
|
||||
const auto bind_addr_v6 = net::get_bind_address(net::af_e::BOTH);
|
||||
ASSERT_EQ(bind_addr_v6, "fe80::1");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test that af_to_any_address_string still works correctly
|
||||
*/
|
||||
TEST_F(BindAddressTest, WildcardAddressFunction) {
|
||||
ASSERT_EQ(net::af_to_any_address_string(net::af_e::IPV4), "0.0.0.0");
|
||||
ASSERT_EQ(net::af_to_any_address_string(net::af_e::BOTH), "::");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user