diff --git a/docs/configuration.md b/docs/configuration.md
index d060c4e04..662a07f19 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1502,6 +1502,46 @@ editing the `conf` file in a text editor. Use the examples as reference.
+### bind_address
+
+
+
+
Description
+
+ 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).
+
+ Note: 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".
+
+
+
+
Default
+
@code{}
+ (empty - bind to all interfaces)
+ @endcode
+
+
+
Example (IPv4)
+
@code{}
+ bind_address = 192.168.1.100
+ @endcode
+
+
+
Example (IPv6)
+
@code{}
+ bind_address = 2001:db8::1
+ @endcode
+
+
+
Example (Loopback)
+
@code{}
+ bind_address = 127.0.0.1
+ @endcode
+
+
+
### port
diff --git a/src/config.cpp b/src/config.cpp
index 8e231a1f8..f65102e17 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -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);
diff --git a/src/config.h b/src/config.h
index 7ba1bd234..e8d1594fb 100644
--- a/src/config.h
+++ b/src/config.h
@@ -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;
diff --git a/src/confighttp.cpp b/src/confighttp.cpp
index 5fad47bbd..b706d4102 100644
--- a/src/confighttp.cpp
+++ b/src/confighttp.cpp
@@ -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) {
diff --git a/src/network.cpp b/src/network.cpp
index fedee2b83..aad65a7a0 100644
--- a/src/network.cpp
+++ b/src/network.cpp
@@ -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
diff --git a/src/network.h b/src/network.h
index 99aa0189a..1b99a1c14 100644
--- a/src/network.h
+++ b/src/network.h
@@ -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.
diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp
index 716c16f16..d4e5ba73f 100644
--- a/src/nvhttp.cpp
+++ b/src/nvhttp.cpp
@@ -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;
@@ -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) {
diff --git a/src/rtsp.cpp b/src/rtsp.cpp
index f5909eb87..45444b8c8 100644
--- a/src/rtsp.cpp
+++ b/src/rtsp.cpp
@@ -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;
}
diff --git a/src/stream.cpp b/src/stream.cpp
index 6bfd5d977..0a342082a 100644
--- a/src/stream.cpp
+++ b/src/stream.cpp
@@ -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();
diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html
index 887062d6c..c85192b87 100644
--- a/src_assets/common/assets/web/config.html
+++ b/src_assets/common/assets/web/config.html
@@ -191,6 +191,7 @@
options: {
"upnp": "disabled",
"address_family": "ipv4",
+ "bind_address": "",
"port": 47989,
"origin_web_ui_allowed": "lan",
"external_ip": "",
diff --git a/src_assets/common/assets/web/configs/tabs/Network.vue b/src_assets/common/assets/web/configs/tabs/Network.vue
index 4fac049db..69b7fd17a 100644
--- a/src_assets/common/assets/web/configs/tabs/Network.vue
+++ b/src_assets/common/assets/web/configs/tabs/Network.vue
@@ -33,6 +33,13 @@ const effectivePort = computed(() => +config.value?.port ?? defaultMoonlightPort
{{ $t('config.address_family_desc') }}
+
+
+
+
+
{{ $t('config.bind_address_desc') }}
+
+
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json
index 1a9e8fa79..331dc6315 100644
--- a/src_assets/common/assets/web/public/assets/locale/en.json
+++ b/src_assets/common/assets/web/public/assets/locale/en.json
@@ -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",
diff --git a/tests/unit/test_network.cpp b/tests/unit/test_network.cpp
index 227358652..ef150539b 100644
--- a/tests/unit/test_network.cpp
+++ b/tests/unit/test_network.cpp
@@ -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), "::");
+}