From 62d64432afbcde11c79ef0b121bd75b37f0a0f03 Mon Sep 17 00:00:00 2001 From: slipher Date: Thu, 11 Jun 2026 12:28:16 -0500 Subject: [PATCH 1/6] Set outgoing multicast interface for IPv6 LAN discovery Correctly set the IPV6_MULTICAST_IF option on the IP6 socket. This option affects outgoing multicast packets. Those are used by the client when searching for IPv6 LAN servers. The old code setting IPV6_MULTICAST_IF was useless because it was in NET_JoinMulticast6() which is only called when starting a server. --- src/engine/qcommon/net_ip.cpp | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/engine/qcommon/net_ip.cpp b/src/engine/qcommon/net_ip.cpp index 9af11e2bd4..061b09fee5 100644 --- a/src/engine/qcommon/net_ip.cpp +++ b/src/engine/qcommon/net_ip.cpp @@ -149,6 +149,7 @@ static SOCKET socks_socket = INVALID_SOCKET; static SOCKET multicast6_socket = INVALID_SOCKET; // Keep track of currently joined multicast group. +// Also stores the multicast address info for clients though they don't join a group static struct ipv6_mreq curgroup; // And the currently bound address. @@ -1020,7 +1021,7 @@ SOCKET NET_IPSocket( const char *net_interface, int port, struct sockaddr_in *bi NET_IP6Socket ==================== */ -SOCKET NET_IP6Socket( const char *net_interface, int port, struct sockaddr_in6 *bindto, int *err ) +SOCKET NET_IP6Socket( const char *net_interface, int port, bool multicastSend, struct sockaddr_in6 *bindto, int *err ) { SOCKET newsocket; struct sockaddr_in6 address; @@ -1068,6 +1069,13 @@ SOCKET NET_IP6Socket( const char *net_interface, int port, struct sockaddr_in6 * } #endif + if ( multicastSend && curgroup.ipv6mr_interface && SOCKET_ERROR == setsockopt( + newsocket, IPPROTO_IPV6, IPV6_MULTICAST_IF, reinterpret_cast( &curgroup.ipv6mr_interface ), + sizeof( curgroup.ipv6mr_interface ) ) ) + { + Log::Warn( "NET_IP6Socket: couldn't set outbound multicast scope for socket: %s", NET_ErrorString() ); + } + if ( !net_interface || !net_interface[ 0 ] ) { memset( &address, 0, sizeof( address ) ); @@ -1170,29 +1178,13 @@ void NET_JoinMulticast6() } else { - if ( ( multicast6_socket = NET_IP6Socket( net_mcast6addr->string, ntohs( boundto.sin6_port ), nullptr, &err ) ) == INVALID_SOCKET ) + if ( ( multicast6_socket = NET_IP6Socket( net_mcast6addr->string, ntohs( boundto.sin6_port ), false, nullptr, &err ) ) == INVALID_SOCKET ) { // If the OS does not support binding to multicast addresses, like Windows XP, at least try with a non-multicast socket. multicast6_socket = ip6_socket; } } - if ( curgroup.ipv6mr_interface ) - { - if ( setsockopt( multicast6_socket, IPPROTO_IPV6, IPV6_MULTICAST_IF, - ( char * ) &curgroup.ipv6mr_interface, sizeof( curgroup.ipv6mr_interface ) ) < 0 ) - { - Log::Notice( "NET_JoinMulticast6: Couldn't set scope on multicast socket: %s", NET_ErrorString() ); - - if ( multicast6_socket != ip6_socket ) - { - closesocket( multicast6_socket ); - multicast6_socket = INVALID_SOCKET; - return; - } - } - } - if ( setsockopt( multicast6_socket, IPPROTO_IPV6, IPV6_JOIN_GROUP, ( char * ) &curgroup, sizeof( curgroup ) ) ) { Log::Notice( "NET_JoinMulticast6: Couldn't join multicast group: %s", NET_ErrorString() ); @@ -1624,7 +1616,8 @@ static void NET_OpenIP( bool serverMode ) { for ( i = ( port6 == PORT_ANY ? 1 : MAX_TRY_PORTS ); i; i-- ) { - ip6_socket = NET_IP6Socket( net_ip6->string, port6, &boundto, &err ); + bool sendingMulticast = !serverMode && !( net_enabled->integer & NET_DISABLEMCAST ); + ip6_socket = NET_IP6Socket( net_ip6->string, port6, sendingMulticast, &boundto, &err ); if ( ip6_socket != INVALID_SOCKET ) { @@ -1737,8 +1730,8 @@ void NET_EnableNetworking( bool serverMode ) networkingEnabled = true; + NET_SetMulticast6(); // just parses cvars NET_OpenIP( serverMode ); - NET_SetMulticast6(); SV_NET_Config(); } From 58b9629c209f6af9199b01678be2bbddc54f7288 Mon Sep 17 00:00:00 2001 From: slipher Date: Sun, 14 Jun 2026 18:20:48 -0500 Subject: [PATCH 2/6] Comment about IPv6 multicast address --- src/engine/qcommon/net_ip.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/engine/qcommon/net_ip.cpp b/src/engine/qcommon/net_ip.cpp index 061b09fee5..a4eedee9bb 100644 --- a/src/engine/qcommon/net_ip.cpp +++ b/src/engine/qcommon/net_ip.cpp @@ -160,6 +160,10 @@ static struct sockaddr_in6 boundto; #endif // use an admin local address per default so that network admins can decide on how to handle quake3 traffic. +// However, an admin-local address usually doesn't go anywhere besides without special network +// configuration. Could use ff02 instead of ff04 meaning a link-local address more like IPv4, but +// then it doesn't work without manually setting net_mcast6iface. +// The final 4 quartets spell "ioquake3". TODO change for Daemon? #define NET_MULTICAST_IP6 "ff04::696f:7175:616b:6533" static const int MAX_IPS = 32; From c1f04d75137f62721070e976e87981cda647545e Mon Sep 17 00:00:00 2001 From: slipher Date: Wed, 10 Jun 2026 21:50:24 -0500 Subject: [PATCH 3/6] Log index parsed from net_mcast6iface --- src/engine/qcommon/net_ip.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/qcommon/net_ip.cpp b/src/engine/qcommon/net_ip.cpp index a4eedee9bb..27a2201ce7 100644 --- a/src/engine/qcommon/net_ip.cpp +++ b/src/engine/qcommon/net_ip.cpp @@ -1152,6 +1152,7 @@ void NET_SetMulticast6() curgroup.ipv6mr_interface = net_mcast6iface->integer; #else curgroup.ipv6mr_interface = if_nametoindex( net_mcast6iface->string ); + Log::Notice( "Parsed IPv6 interface index: %d", curgroup.ipv6mr_interface ); #endif } else From 6afb97b7da92d3ee32dc79e6433e42ce6676b794 Mon Sep 17 00:00:00 2001 From: slipher Date: Mon, 28 Sep 2026 23:58:00 -0500 Subject: [PATCH 4/6] Fix /net_restart on *nix with running server Avoid leaking socket fd's into gamelogic subprocesses, which causes a 'bind: address already in use' error upon /net_restart. The same bug was fixed for Windows in f70cf74fbc3459792f3b2b0c509979ab047980a2. --- src/engine/qcommon/net_ip.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/engine/qcommon/net_ip.cpp b/src/engine/qcommon/net_ip.cpp index 27a2201ce7..0fd5aee3d1 100644 --- a/src/engine/qcommon/net_ip.cpp +++ b/src/engine/qcommon/net_ip.cpp @@ -88,6 +88,7 @@ static bool winsockInitialized = false; # endif # include +# include # include # include # include @@ -951,6 +952,7 @@ SOCKET NET_IPSocket( const char *net_interface, int port, struct sockaddr_in *bi newsocket = WSASocketW( PF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT ); #else newsocket = socket( PF_INET, SOCK_DGRAM, IPPROTO_UDP ); + fcntl( newsocket, F_SETFD, FD_CLOEXEC ); #endif if ( newsocket == INVALID_SOCKET ) @@ -1042,6 +1044,7 @@ SOCKET NET_IP6Socket( const char *net_interface, int port, bool multicastSend, s newsocket = WSASocketW( PF_INET6, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT ); #else newsocket = socket( PF_INET6, SOCK_DGRAM, IPPROTO_UDP ); + fcntl( newsocket, F_SETFD, FD_CLOEXEC ); #endif if ( newsocket == INVALID_SOCKET ) @@ -1243,6 +1246,7 @@ void NET_OpenSocks( int port ) socks_socket = WSASocketW( AF_INET, SOCK_STREAM, IPPROTO_TCP, nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT ); #else socks_socket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); + fcntl( socks_socket, F_SETFD, FD_CLOEXEC ); #endif if ( socks_socket == INVALID_SOCKET ) From 94fb3e3c69ed7531e17f89d60848d9ea248b633a Mon Sep 17 00:00:00 2001 From: slipher Date: Tue, 29 Sep 2026 21:09:51 -0500 Subject: [PATCH 5/6] Fix IP6 multicast receiving after /net_restart --- src/engine/qcommon/net_ip.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/engine/qcommon/net_ip.cpp b/src/engine/qcommon/net_ip.cpp index 0fd5aee3d1..8608a3bb88 100644 --- a/src/engine/qcommon/net_ip.cpp +++ b/src/engine/qcommon/net_ip.cpp @@ -1890,4 +1890,9 @@ void NET_Restart_f() #else NET_EnableNetworking( com_sv_running.Get() ); #endif + + if ( com_sv_running.Get() ) + { + NET_JoinMulticast6(); + } } From 91d058a7e648ba0517f9c55ee4497297786b4a13 Mon Sep 17 00:00:00 2001 From: slipher Date: Tue, 29 Sep 2026 21:53:38 -0500 Subject: [PATCH 6/6] Don't try to bind to IPv6 multicast address Internet sources say you are supposed to bind to the all-zeroes address if you want to receive multicast packets, not the multicast address. Indeed, when tested on Windows the attempt to bind to a multicast address produced an error. --- src/engine/qcommon/net_ip.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/qcommon/net_ip.cpp b/src/engine/qcommon/net_ip.cpp index 8608a3bb88..c28581bdf7 100644 --- a/src/engine/qcommon/net_ip.cpp +++ b/src/engine/qcommon/net_ip.cpp @@ -1179,14 +1179,14 @@ void NET_JoinMulticast6() return; } - if ( IN6_IS_ADDR_MULTICAST( &boundto.sin6_addr ) || IN6_IS_ADDR_UNSPECIFIED( &boundto.sin6_addr ) ) + if ( IN6_IS_ADDR_UNSPECIFIED( &boundto.sin6_addr ) ) { // The way the socket was bound does not prohibit receiving multi-cast packets. So we don't need to open a new one. multicast6_socket = ip6_socket; } else { - if ( ( multicast6_socket = NET_IP6Socket( net_mcast6addr->string, ntohs( boundto.sin6_port ), false, nullptr, &err ) ) == INVALID_SOCKET ) + if ( ( multicast6_socket = NET_IP6Socket( nullptr, ntohs( boundto.sin6_port ), false, nullptr, &err ) ) == INVALID_SOCKET ) { // If the OS does not support binding to multicast addresses, like Windows XP, at least try with a non-multicast socket. multicast6_socket = ip6_socket;