Files
ppsspp/Common/Net/Resolve.cpp
T
Henrik RydgårdandClaude Opus 5.5 66fb9f7ab4 Net: Harden the direct DNS lookup and inet_pton
- inet_pton could write outside its output for IPv6 strings with more than
  eight colons or two "::". Only reachable through AF_INET today, but it's a
  public function. Added a unit test.
- The direct DNS lookup took any datagram that arrived within its timeout as
  the answer, and cached it until restart. The socket is now connected, so the
  OS drops packets from anyone else, the query has a random ID, and replies
  with a different ID, without the response bit, truncated or with an error
  code are rejected.
- Packet fields are read and written with memcpy instead of through unaligned
  pointers, which can fault on 32-bit ARM.
- The lookup cache has a lock. Only the emu thread uses it so far.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 11:45:43 +02:00

807 lines
21 KiB
C++

#include "ppsspp_config.h"
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <map>
#include <mutex>
#include <random>
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/StringUtils.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/Net/SocketCompat.h"
#include "Common/Net/Resolve.h"
#ifndef HTTPS_NOT_AVAILABLE
#include "Common/Net/HTTPNaettRequest.h"
#include "ext/naett-lib/naett.h"
// Note: PPSSPP_PLATFORM(LINUX) is also set on Android, which needs no loader.
#if PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID)
// On Linux, naett goes through libcurl, which we load at runtime - so HTTPS support is
// only known once we've tried.
#include "ext/naett-lib/src/naett_curl.h"
#endif
#endif
#if PPSSPP_PLATFORM(ANDROID)
#include <jni.h>
extern JavaVM *gJvm;
#endif
namespace net {
static bool g_wsaInitialized;
void Init() {
#ifdef _WIN32
// WSA does its own internal reference counting, no need to keep track of if we inited or not.
WSADATA wsaData{};
if (FAILED(WSAStartup(MAKEWORD(2, 2), &wsaData))) {
ERROR_LOG(Log::Net, "WSAStartup failed");
} else {
g_wsaInitialized = true;
}
#endif
// naett ignores repeat calls the same way WSAStartup does, so there's nothing to track here
// either. HTTPSAvailable is cheap to ask twice - on Linux it's a cached dlopen result.
#ifndef HTTPS_NOT_AVAILABLE
#if PPSSPP_PLATFORM(ANDROID)
_assert_(gJvm != nullptr);
naettInit(gJvm);
#else
if (HTTPSAvailable()) {
naettInit(NULL);
}
#endif
#endif
}
bool HTTPSAvailable() {
#ifdef HTTPS_NOT_AVAILABLE
return false;
#elif PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID)
return naettCurlLoad() != 0;
#else
return true;
#endif
}
void Shutdown() {
#ifndef HTTPS_NOT_AVAILABLE
http::HTTPSShutdown();
#endif
#ifdef _WIN32
if (g_wsaInitialized) {
WSACleanup();
}
#endif
}
bool HostPortExists(const std::string &host, int port, int timeout_ms) {
if (host.empty() || (port <= 0 || port > 65535) || timeout_ms < 0) return false;
addrinfo hints;
addrinfo* res = nullptr;
std::memset(&hints, 0, sizeof(hints));
hints.ai_socktype = SOCK_STREAM; // TCP
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
int gai = getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &res);
if (gai != 0) {
// getaddrinfo failed (DNS resolve failed or bad port)
return false;
}
bool ok = false;
for (addrinfo* p = res; p != nullptr && !ok; p = p->ai_next) {
// create socket
int sockfd =
#ifdef _WIN32
(int)socket(p->ai_family, p->ai_socktype, p->ai_protocol);
#else
socket(p->ai_family, p->ai_socktype, p->ai_protocol);
#endif
if (sockfd < 0) {
continue;
}
// make non-blocking
#ifdef _WIN32
unsigned long mode = 1;
ioctlsocket((SOCKET)sockfd, FIONBIO, &mode);
#else
// On non-Windows, check if fd is too large for select()
if (sockfd >= FD_SETSIZE) {
close(sockfd);
continue;
}
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) flags = 0;
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
#endif
// try connect
int conn = connect(sockfd, p->ai_addr, (int)p->ai_addrlen);
#ifdef _WIN32
if (conn == 0) {
ok = true; // immediate success
}
else {
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK || err == WSAEINPROGRESS) {
// fall through to select
}
else {
// immediate failure
}
}
#else
if (conn == 0) {
ok = true; // immediate success
}
else {
if (errno == EINPROGRESS) {
// fall through to select
}
else {
// immediate failure
}
}
#endif
if (!ok) {
// wait for writable with timeout
fd_set writefds;
FD_ZERO(&writefds);
#ifdef _WIN32
FD_SET((SOCKET)sockfd, &writefds);
#else
FD_SET(sockfd, &writefds);
#endif
fd_set exceptfds;
FD_ZERO(&exceptfds);
#ifdef _WIN32
FD_SET((SOCKET)sockfd, &exceptfds);
#else
FD_SET(sockfd, &exceptfds);
#endif
timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
int sel = select(
#ifdef _WIN32
0,
#else
sockfd + 1,
#endif
nullptr, &writefds, &exceptfds, &tv);
if (sel > 0) {
// check for error on socket
int sock_err = 0;
socklen_t len = sizeof(sock_err);
#ifdef _WIN32
int ret = getsockopt((SOCKET)sockfd, SOL_SOCKET, SO_ERROR, (char*)&sock_err, &len);
#else
int ret = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &sock_err, &len);
#endif
#ifdef _WIN32
bool writable = FD_ISSET(static_cast<SOCKET>(sockfd), &writefds) != 0;
#else
bool writable = FD_ISSET(sockfd, &writefds) != 0;
#endif
if (ret == 0 && sock_err == 0 && writable) {
ok = true;
}
}
// else timeout or error -> try next addr
}
// close socket
#ifdef _WIN32
closesocket((SOCKET)sockfd);
#else
close(sockfd);
#endif
}
freeaddrinfo(res);
return ok;
}
// NOTE: Due to the nature of getaddrinfo, this can block indefinitely. Not good.
bool DNSResolve(const std::string &host, const std::string &service, addrinfo **res, std::string &error, DNSType type) {
#if PPSSPP_PLATFORM(SWITCH)
// Force IPv4 lookups.
if (type == DNSType::IPV6) {
*res = nullptr;
return false;
} else if (type == DNSType::ANY) {
type = DNSType::IPV4;
}
#endif
addrinfo hints = {0};
// TODO: Might be uses to lookup other values.
hints.ai_socktype = SOCK_STREAM;
#ifdef __ANDROID__
hints.ai_flags = AI_ADDRCONFIG;
#else
// AI_V4MAPPED seems to have issues on some platforms, not sure we should include it:
// http://stackoverflow.com/questions/1408030/what-is-the-purpose-of-the-ai-v4mapped-flag-in-getaddrinfo
hints.ai_flags = /*AI_V4MAPPED |*/ AI_ADDRCONFIG;
#endif
hints.ai_protocol = 0;
if (type == DNSType::IPV4)
hints.ai_family = AF_INET;
else if (type == DNSType::IPV6)
hints.ai_family = AF_INET6;
const char *servicep = service.empty() ? nullptr : service.c_str();
*res = nullptr;
int result = getaddrinfo(host.c_str(), servicep, &hints, res);
if (result == EAI_AGAIN) {
// Temporary failure. Since this already blocks, let's just try once more.
sleep_ms(1, "dns-resolve-poll");
result = getaddrinfo(host.c_str(), servicep, &hints, res);
}
if (result != 0) {
#ifdef _WIN32
error = ConvertWStringToUTF8(gai_strerror(result));
#else
error = gai_strerror(result);
#endif
if (*res != nullptr)
freeaddrinfo(*res);
*res = nullptr;
return false;
}
return true;
}
void DNSResolveFree(addrinfo *res) {
if (res)
freeaddrinfo(res);
}
bool GetLocalIP4List(std::vector<std::string> &IP4s) {
char ipstr[INET6_ADDRSTRLEN]; // We use IPv6 length since it's longer than IPv4
// getifaddrs first appeared in glibc 2.3, On Android officially supported since __ANDROID_API__ >= 24
#if defined(_IFADDRS_H_) || (__GLIBC__ > 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 3) || (__ANDROID_API__ >= 24)
INFO_LOG(Log::Net, "GetIPList from getifaddrs");
struct ifaddrs* ifAddrStruct = NULL;
struct ifaddrs* ifa = NULL;
getifaddrs(&ifAddrStruct);
if (ifAddrStruct != NULL) {
for (ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
if (!ifa->ifa_addr) {
continue;
}
if (ifa->ifa_addr->sa_family == AF_INET) {
// is a valid IP4 Address
if (inet_ntop(AF_INET, &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr, ipstr, sizeof(ipstr)) != 0) {
IP4s.push_back(ipstr);
}
}
/*else if (ifa->ifa_addr->sa_family == AF_INET6) {
// is a valid IP6 Address
if (inet_ntop(AF_INET6, &((struct sockaddr_in6*)ifa->ifa_addr)->sin6_addr, ipstr, sizeof(ipstr)) != 0) {
IP6s.push_back(ipstr);
}
}*/
}
freeifaddrs(ifAddrStruct);
return true;
}
#elif defined(SIOCGIFCONF) // Better detection on Linux/UNIX/MacOS/some Android
INFO_LOG(Log::Net, "GetIPList from SIOCGIFCONF");
static struct ifreq ifreqs[32];
struct ifconf ifc{};
ifc.ifc_req = ifreqs;
ifc.ifc_len = sizeof(ifreqs);
int sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
ERROR_LOG(Log::Net, "GetIPList failed to create socket (result = %i, errno = %i)", sd, socket_errno);
return false;
}
int r = ioctl(sd, SIOCGIFCONF, (char*)&ifc);
if (r != 0) {
ERROR_LOG(Log::Net, "GetIPList failed ioctl/SIOCGIFCONF (result = %i, errno = %i)", r, socket_errno);
return false;
}
struct ifreq* item;
struct sockaddr* addr;
for (int i = 0; i < ifc.ifc_len / sizeof(struct ifreq); ++i)
{
item = &ifreqs[i];
addr = &(item->ifr_addr);
// Get the IP address
r = ioctl(sd, SIOCGIFADDR, item);
if (r != 0)
{
ERROR_LOG(Log::Net, "GetIPList failed ioctl/SIOCGIFADDR (i = %i, result = %i, errno = %i)", i, r, socket_errno);
}
if (ifreqs[i].ifr_addr.sa_family == AF_INET) {
// is a valid IP4 Address
if (inet_ntop(AF_INET, &((struct sockaddr_in*)addr)->sin_addr, ipstr, sizeof(ipstr)) != 0) {
IP4s.emplace_back(ipstr);
}
}
/*else if (ifreqs[i].ifr_addr.sa_family == AF_INET6) {
// is a valid IP6 Address
if (inet_ntop(AF_INET6, &((struct sockaddr_in6*)addr)->sin6_addr, ipstr, sizeof(ipstr)) != 0) {
IP6s.push_back(ipstr);
}
}*/
}
close(sd);
return true;
#else // Fallback to POSIX/Cross-platform way but may not work well on Linux (ie. only shows 127.0.0.1)
DEBUG_LOG(Log::Net, "GetIPList from fallback method");
struct addrinfo hints, * res, * p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
hints.ai_socktype = SOCK_DGRAM;
// Get local host name
char szHostName[256] = "";
if (::gethostname(szHostName, sizeof(szHostName))) {
// Error handling
}
int status;
if ((status = getaddrinfo(szHostName, NULL, &hints, &res)) != 0) {
//fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return false;
}
for (p = res; p != NULL; p = p->ai_next) {
if (p->ai_family == AF_INET) {
// is a valid IP4 Address
if (inet_ntop(p->ai_family, &(((struct sockaddr_in*)p->ai_addr)->sin_addr), ipstr, sizeof(ipstr)) != 0) {
IP4s.push_back(ipstr);
}
}
/*else if (p->ai_family == AF_INET6) {
// is a valid IP6 Address
if (inet_ntop(p->ai_family, &(((struct sockaddr_in6*)p->ai_addr)->sin6_addr), ipstr, sizeof(ipstr)) != 0) {
IP6s.push_back(ipstr);
}
}*/
}
freeaddrinfo(res); // free the linked list
return true;
#endif
return false;
}
// IP address parser
int inet_pton(int af, const char* src, void* dst)
{
if (af == AF_INET)
{
unsigned char *ip = (unsigned char *)dst;
int k = 0, x = 0;
char ch;
for (int i = 0; (ch = src[i]) != 0; i++)
{
if (ch == '.')
{
ip[k++] = x;
if (k == 4)
return 0;
x = 0;
}
else if (ch < '0' || ch > '9')
return 0;
else
x = x * 10 + ch - '0';
if (x > 255)
return 0;
}
ip[k++] = x;
if (k != 4)
return 0;
}
else if (af == AF_INET6)
{
unsigned short* ip = ( unsigned short* )dst;
int i;
for (i = 0; i < 8; i++) ip[i] = 0;
int k = 0;
unsigned int x = 0;
char ch;
int marknum = 0;
bool sawDoubleColon = false;
for (i = 0; src[i] != 0; i++)
{
if (src[i] == ':')
marknum++;
}
// Eight groups have at most seven colons, eight with a leading or trailing "::".
if (marknum > 8)
return 0;
for (i = 0; (ch = src[i]) != 0; i++)
{
if (ch == ':')
{
// "::" below can skip k ahead, so check before every write.
if (k >= 8)
return 0;
x = ((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8);
ip[k++] = x;
if (k == 8)
return 0;
x = 0;
if (i > 0 && src[i - 1] == ':') {
// Only one "::" is allowed. With marknum <= 8 the skip keeps k in range.
if (sawDoubleColon)
return 0;
sawDoubleColon = true;
k += 7 - marknum;
}
}
else if (ch >= '0' && ch <= '9')
x = x * 16 + ch - '0';
else if (ch >= 'a' && ch <= 'f')
x = x * 16 + ch - 'a' + 10;
else if (ch >= 'A' && ch <= 'F')
x = x * 16 + ch - 'A' + 10;
else
return 0;
if (x > 0xFFFF)
return 0;
}
if (k != 7)
return 0;
x = ((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8);
ip[k++] = x;
}
return 1;
}
// Structs for implementing DNS are available here:
// https://web.archive.org/web/20201204080751/https://www.binarytides.com/dns-query-code-in-c-with-winsock/
#define DNS_PORT 53
#define DNS_QUERY_TYPE_A 1
#define DNS_QUERY_CLASS_IN 1
#define DNS_FLAG_RESPONSE 0x8000
#define DNS_FLAG_TRUNCATED 0x0200
#define DNS_RCODE_MASK 0x000F
// The packet has no alignment, so read and write fields with memcpy.
static uint16_t ReadBE16(const unsigned char *p) {
uint16_t value;
memcpy(&value, p, sizeof(value));
return ntohs(value);
}
static uint32_t ReadBE32(const unsigned char *p) {
uint32_t value;
memcpy(&value, p, sizeof(value));
return ntohl(value);
}
static void WriteBE16(unsigned char *p, uint16_t value) {
value = htons(value);
memcpy(p, &value, sizeof(value));
}
// DNS header structure
struct DNSHeader {
uint16_t id; // Identifier
uint16_t flags; // Flags
uint16_t q_count; // Number of questions
uint16_t ans_count; // Number of answers
uint16_t auth_count; // Number of authority records
uint16_t add_count; // Number of additional records
};
// Function to convert a domain name to DNS query format
// http://www.tcpipguide.com/free/t_DNSNameNotationandMessageCompressionTechnique.htm
static bool encode_domain_name(const char *domain, unsigned char *encoded, size_t max_len) {
const char *pos = domain;
unsigned char *ptr = encoded;
const unsigned char *end = encoded + max_len;
while (*pos) {
const char *start = pos;
while (*pos && *pos != '.') {
pos++;
}
size_t label_len = pos - start;
if (label_len > 63 || ptr + label_len + 1 >= end) {
return false; // Label too long or buffer overflow
}
*ptr++ = (unsigned char)label_len; // length field
memcpy(ptr, start, label_len);
ptr += label_len;
if (*pos == '.') {
pos++;
}
}
if (ptr >= end) {
return false;
}
*ptr = 0; // End of domain name
return true;
}
// Function to parse and print the DNS response
static bool parse_dns_response(const unsigned char *buffer, size_t response_len, uint16_t expected_id, uint32_t *output) {
if (response_len < sizeof(DNSHeader)) {
ERROR_LOG(Log::Net, "DNS response too short");
return false;
}
const uint16_t id = ReadBE16(buffer + offsetof(DNSHeader, id));
const uint16_t flags = ReadBE16(buffer + offsetof(DNSHeader, flags));
const unsigned char *ptr = buffer + sizeof(struct DNSHeader);
const unsigned char *end = buffer + response_len;
DEBUG_LOG(Log::Net, "DNS Response:");
DEBUG_LOG(Log::Net, "ID: 0x%x", id);
DEBUG_LOG(Log::Net, "Flags: 0x%x", flags);
DEBUG_LOG(Log::Net, "Questions: %d", ReadBE16(buffer + offsetof(DNSHeader, q_count)));
DEBUG_LOG(Log::Net, "Answers: %d", ReadBE16(buffer + offsetof(DNSHeader, ans_count)));
DEBUG_LOG(Log::Net, "Authority Records: %d", ReadBE16(buffer + offsetof(DNSHeader, auth_count)));
DEBUG_LOG(Log::Net, "Additional Records: %d", ReadBE16(buffer + offsetof(DNSHeader, add_count)));
if (id != expected_id || !(flags & DNS_FLAG_RESPONSE)) {
ERROR_LOG(Log::Net, "DNS response doesn't answer our query (ID 0x%04x, expected 0x%04x, flags 0x%04x)", id, expected_id, flags);
return false;
}
if (flags & DNS_FLAG_TRUNCATED) {
// We'd have to retry over TCP to get the rest.
ERROR_LOG(Log::Net, "DNS response truncated");
return false;
}
if ((flags & DNS_RCODE_MASK) != 0) {
WARN_LOG(Log::Net, "DNS server returned error %d", flags & DNS_RCODE_MASK);
return false;
}
// Skip over the question section
const int q_count = ReadBE16(buffer + offsetof(DNSHeader, q_count));
for (int i = 0; i < q_count; i++) {
while (ptr < end && *ptr != 0) {
int jump = *ptr;
ptr += jump + 1;
if (ptr >= end) {
ERROR_LOG(Log::Net, "DNS response malformed (question section)");
return false;
}
}
ptr += 5; // Null byte + QTYPE (2 bytes) + QCLASS (2 bytes)
if (ptr > end) {
ERROR_LOG(Log::Net, "DNS response malformed (question section end)");
return false;
}
}
*output = 0;
// Parse the answer section
const int ans_count = ReadBE16(buffer + offsetof(DNSHeader, ans_count));
for (int i = 0; i < ans_count; i++) {
DEBUG_LOG(Log::Net, "Answer %d:\n", i + 1);
// Skip the name (can be a pointer or a sequence)
if (ptr >= end) {
ERROR_LOG(Log::Net, "DNS response malformed (answer %d name)", i);
return false;
}
if ((*ptr & 0xC0) == 0xC0) {
if (ptr + 2 > end) {
ERROR_LOG(Log::Net, "DNS response malformed (answer %d name pointer)", i);
return false;
}
ptr += 2; // Pointer (2 bytes)
} else {
while (ptr < end && *ptr != 0) {
int jump = *ptr;
ptr += jump + 1;
if (ptr >= end) {
ERROR_LOG(Log::Net, "DNS response malformed (answer %d name loop)", i);
return false;
}
}
ptr++;
}
if (ptr + 10 > end) {
ERROR_LOG(Log::Net, "DNS response too short for answer %d header", i);
return false;
}
uint16_t type = ReadBE16(ptr);
ptr += 2;
uint16_t clazz = ReadBE16(ptr);
ptr += 2;
uint32_t ttl = ReadBE32(ptr);
ptr += 4;
uint16_t data_len = ReadBE16(ptr);
ptr += 2;
DEBUG_LOG(Log::Net, " Type: %d", type);
DEBUG_LOG(Log::Net, " Class: %d", clazz);
DEBUG_LOG(Log::Net, " TTL: %u", ttl);
DEBUG_LOG(Log::Net, " Data length: %d", (int)data_len);
if (ptr + data_len > end) {
ERROR_LOG(Log::Net, "DNS response data exceeds buffer");
return false;
}
if (type == DNS_QUERY_TYPE_A && data_len == 4) {
// IPv4 address
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, ptr, ip, sizeof(ip));
DEBUG_LOG(Log::Net, " IPV4 Address: %s", ip);
memcpy(output, ptr, 4);
// Skipping further responses.
return true;
}
ptr += data_len;
}
return false;
}
// This was written by ChatGPT, although not much of that remains, after all the cleanup and fixing...
// Specialized cache for the direct DNS lookups
struct DNSCacheEntry {
uint32_t ipv4Address;
};
// Only used from the emu thread currently, but cheap to protect.
static std::mutex g_directDNSCacheLock;
static std::map<std::string, DNSCacheEntry> g_directDNSCache;
bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t *ipv4_addr) {
if (!strlen(dns_server_ip)) {
WARN_LOG(Log::Net, "Direct lookup: DNS server not specified");
return false;
}
if (!strlen(domain)) {
ERROR_LOG(Log::Net, "Direct lookup: Can't look up an empty domain");
return false;
}
std::string key = StringFromFormat("%s:%s", dns_server_ip, domain);
{
std::lock_guard<std::mutex> guard(g_directDNSCacheLock);
auto iter = g_directDNSCache.find(key);
if (iter != g_directDNSCache.end()) {
INFO_LOG(Log::Net, "Returning cached response from direct DNS request for '%s' to DNS server '%s", domain, dns_server_ip);
*ipv4_addr = iter->second.ipv4Address;
return true;
}
}
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// Create UDP socket
if (sockfd == INVALID_SOCKET) {
ERROR_LOG(Log::Net, "Socket creation for direct DNS failed");
return false;
}
#ifndef _WIN32
// On non-Windows, we can't use select() if fd >= FD_SETSIZE
// For DNS, just set a socket timeout instead
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
WARN_LOG(Log::Net, "Failed to set socket timeout for DNS query");
}
#else
// Windows version
DWORD timeout = 5000; // 5 seconds
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)) < 0) {
WARN_LOG(Log::Net, "Failed to set socket timeout for DNS query");
}
#endif
struct sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(DNS_PORT);
if (net::inet_pton(AF_INET, dns_server_ip, &server_addr.sin_addr) <= 0) {
ERROR_LOG(Log::Net,"Invalid DNS server IP address %s", dns_server_ip);
closesocket(sockfd);
return false;
}
// Connecting a UDP socket makes the OS drop datagrams from anyone but the server.
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
ERROR_LOG(Log::Net, "Failed to connect to DNS server %s", dns_server_ip);
closesocket(sockfd);
return false;
}
// A random ID, so a stale or stray reply isn't mistaken for the answer.
std::random_device rd;
const uint16_t query_id = (uint16_t)rd();
// Build DNS query
unsigned char buffer[1024]{};
WriteBE16(buffer + offsetof(DNSHeader, id), query_id);
WriteBE16(buffer + offsetof(DNSHeader, flags), 0x0100); // Standard query, recursion desired
WriteBE16(buffer + offsetof(DNSHeader, q_count), 1); // One question
unsigned char *qname = buffer + sizeof(DNSHeader);
size_t qname_space = sizeof(buffer) - sizeof(DNSHeader) - 4; // Reserve 4 bytes for qtype and qclass
if (!encode_domain_name(domain, qname, qname_space)) {
ERROR_LOG(Log::Net, "Domain name too long or invalid: %s", domain);
closesocket(sockfd);
return false;
}
unsigned char *qinfo = qname + strlen((const char *)qname) + 1;
WriteBE16(qinfo, DNS_QUERY_TYPE_A);
WriteBE16(qinfo + 2, DNS_QUERY_CLASS_IN);
// Send DNS query
size_t query_len = (qinfo + 4) - buffer;
if (send(sockfd, (const char *)buffer, (int)query_len, 0) < 0) {
ERROR_LOG(Log::Net, "Failed to send DNS query");
closesocket(sockfd);
return false;
}
// Receive DNS response
int response_len = recv(sockfd, (char *)buffer, sizeof(buffer), 0);
if (response_len < 0) {
ERROR_LOG(Log::Net, "Failed to receive DNS response (timeout or error)");
closesocket(sockfd);
return false;
}
// Close socket
closesocket(sockfd);
// Done communicating, time to parse.
if (!parse_dns_response(buffer, (size_t)response_len, query_id, ipv4_addr)) {
return false;
}
std::lock_guard<std::mutex> guard(g_directDNSCacheLock);
g_directDNSCache[key] = DNSCacheEntry{ *ipv4_addr };
return true;
}
} // namespace net