Add SRV record support for server connections

Resolve _minecraft._tcp.<hostname> SRV records before connecting,
matching Java Edition behavior. Players can connect using just a domain
name and the client will look up the actual server address and port
from DNS. Falls back to the original hostname/port if no SRV record
exists or the address is a numeric IP.
This commit is contained in:
itsRevela 2026-03-26 15:45:24 -05:00
parent f1310abe08
commit fbe14e7755
2 changed files with 63 additions and 5 deletions

View file

@ -14,6 +14,8 @@
#endif
#include "..\..\..\Minecraft.World\DisconnectPacket.h"
#include "..\..\Minecraft.h"
#include <windns.h>
#pragma comment(lib, "Dnsapi.lib")
#include "..\4JLibs\inc\4J_Profile.h"
#include <string>
@ -310,6 +312,44 @@ bool WinsockNetLayer::HostGame(int port, const char* bindIp)
return true;
}
// Resolve a Minecraft SRV record (_minecraft._tcp.<hostname>) to get the actual host and port.
// Returns true if an SRV record was found and outHost/outPort were updated.
// Returns false (no changes) for numeric IPs, missing records, or DNS errors.
static bool ResolveSRV(const char* hostname, char* outHost, size_t outHostSize, int* outPort)
{
// Skip numeric IPs (starts with digit, contains no letters)
if (hostname[0] >= '0' && hostname[0] <= '9')
{
bool hasLetter = false;
for (const char* p = hostname; *p; ++p)
if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')) { hasLetter = true; break; }
if (!hasLetter) return false;
}
char srvName[300];
sprintf_s(srvName, "_minecraft._tcp.%s", hostname);
DNS_RECORD* records = nullptr;
DNS_STATUS status = DnsQuery_A(srvName, DNS_TYPE_SRV, DNS_QUERY_STANDARD, nullptr, &records, nullptr);
if (status != 0 || records == nullptr)
return false;
// Find the first SRV record
for (DNS_RECORD* r = records; r != nullptr; r = r->pNext)
{
if (r->wType == DNS_TYPE_SRV)
{
strncpy_s(outHost, outHostSize, r->Data.SRV.pNameTarget, _TRUNCATE);
*outPort = r->Data.SRV.wPort;
DnsRecordListFree(records, DnsFreeRecordList);
return true;
}
}
DnsRecordListFree(records, DnsFreeRecordList);
return false;
}
bool WinsockNetLayer::JoinGame(const char* ip, int port)
{
if (!s_initialized && !Initialize()) return false;
@ -336,6 +376,13 @@ bool WinsockNetLayer::JoinGame(const char* ip, int port)
s_clientRecvThread = nullptr;
}
// Try SRV record resolution for hostnames
char resolvedHost[256];
int resolvedPort = port;
strncpy_s(resolvedHost, sizeof(resolvedHost), ip, _TRUNCATE);
if (ResolveSRV(ip, resolvedHost, sizeof(resolvedHost), &resolvedPort))
app.DebugPrintf("SRV resolved %s -> %s:%d\n", ip, resolvedHost, resolvedPort);
struct addrinfo hints = {};
struct addrinfo* result = nullptr;
@ -344,9 +391,9 @@ bool WinsockNetLayer::JoinGame(const char* ip, int port)
hints.ai_protocol = IPPROTO_TCP;
char portStr[16];
sprintf_s(portStr, "%d", port);
sprintf_s(portStr, "%d", resolvedPort);
int iResult = getaddrinfo(ip, portStr, &hints, &result);
int iResult = getaddrinfo(resolvedHost, portStr, &hints, &result);
if (iResult != 0)
{
app.DebugPrintf("getaddrinfo failed for %s:%d - %d\n", ip, port, iResult);
@ -539,17 +586,24 @@ bool WinsockNetLayer::BeginJoinGame(const char* ip, int port)
DWORD WINAPI WinsockNetLayer::JoinThreadProc(LPVOID param)
{
// Try SRV record resolution for hostnames
char resolvedHost[256];
int resolvedPort = s_joinPort;
strncpy_s(resolvedHost, sizeof(resolvedHost), s_joinIP, _TRUNCATE);
if (ResolveSRV(s_joinIP, resolvedHost, sizeof(resolvedHost), &resolvedPort))
app.DebugPrintf("SRV resolved %s -> %s:%d\n", s_joinIP, resolvedHost, resolvedPort);
struct addrinfo hints = {}, *result = nullptr;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
char portStr[16];
sprintf_s(portStr, "%d", s_joinPort);
sprintf_s(portStr, "%d", resolvedPort);
if (getaddrinfo(s_joinIP, portStr, &hints, &result) != 0)
if (getaddrinfo(resolvedHost, portStr, &hints, &result) != 0)
{
app.DebugPrintf("getaddrinfo failed for %s:%d\n", s_joinIP, s_joinPort);
app.DebugPrintf("getaddrinfo failed for %s:%d\n", resolvedHost, resolvedPort);
s_joinState = eJoinState_Failed;
return 0;
}

View file

@ -14,6 +14,10 @@ This project is based on source code of Minecraft Legacy Console Edition v1.6.05
## Latest:
SRV record support and async join refactor:
- Added DNS SRV record resolution (`_minecraft._tcp.<hostname>`), matching Java Edition behavior. Players can connect using just a domain name (e.g. `play.example.com`) and the client will automatically look up the correct server address and port from DNS
- Refactored the async server joining system: replaced boolean flags with a clean `eJoinState` enum state machine, moved connection progress handling into a dedicated `UIScene_ConnectingProgress` class with attempt counter and cancel support, and added a `FinalizeJoin()` separation so the recv thread only starts after the UI confirms success
Piston fix for dedicated servers:
- Fixed a bug where pistons would permanently break server-wide on dedicated servers when a redstone clock ran long enough. The piston update lock (`ignoreUpdate`) was set at the start of `triggerEvent` but never cleared on three early-return paths, permanently blocking all piston neighbor updates for the rest of the session. A fast clock would eventually hit one of these paths (e.g. signal state changing between event queuing and processing), locking out every piston in the world