Skip to content

Instantly share code, notes, and snippets.

@kr4uzi
Created July 30, 2025 07:48
Show Gist options
  • Select an option

  • Save kr4uzi/91d05200432ae32d99294ccd8c46b3f7 to your computer and use it in GitHub Desktop.

Select an option

Save kr4uzi/91d05200432ae32d99294ccd8c46b3f7 to your computer and use it in GitHub Desktop.
Backup of my gamespy-emulator dns implementation
// the dns implementation was removed from the gamespy-emulator because i found that
// redirecting calls from ::gethostbyname requires less technical boiler plate and
// who'd want to change their dns configuration anyways?
// dns_details.h
#include <string>
#include <cstdint>
#include <vector>
#include <span>
#include <map>
#include <expected>
#include <chrono>
namespace dns
{
// https://www.rfc-editor.org/rfc/rfc6895.html
enum class ParseError
{
INCOMPLETE,
INVALID
};
struct dns_question {
std::string name;
// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4
enum class QTYPE : std::uint16_t {
A = 1, // Host Address
NS = 2, // Authoritative Name Server
MD = 3, // Mail Destination (OBSOLETE - use MX)
MF = 4, // MAIL Forwarder (OBSOLETE - use MX)
CNAME = 5, // Canonical Name for an Alias
SOA = 6, // Start of a Zone of Authoritiy
MB = 7, // Mailbox Domain Name (EXPERIMENTAL)
MG = 8, // Mial Group Member (EXPERIMENTAL)
MR = 9, // Mail Rename Domain Name (EXPERIMENTAL)
NUL = 10, // null RR (EXPERIMENTAL)
WKS = 11, // Well Known Service Descriptor
PTR = 12, // Domain Name Pointer
HINFO = 13, // Host Information
MINFO = 14, // Mailbox or Mail List Information
MX = 15, // Mail Exchange
TEXT = 16, // Text
AAAA = 28, // IPv6 Address
SRV = 33, // Service Record
NAPTR = 35, // Naming AUthority Pointer
} type;
// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-2
enum class QCLASS : std::uint16_t {
// 0 - assignment requires an IETF Standards Action.
INTERNET = 1, // IN
// 2 - available for assignment by IETF Consensus as a data CLASS.
CHAOS = 3, // Moon 1981
HESIOD = 4, // Dyer 1987
// (5 - 127) available for assignment by IETF Consensus as data CLASSes only.
// (128 - 253) available for assignment by IETF Consensus as QCLASSes only.
NONE = 254,
ANY = 255,
// 256 - 32767 assigned by IETF Consensus.
// 32768 - 65280 assigned based on Specification Required as defined in[RFC 2434].
// 65280 - 65534 Private Use.
// 65535 can only be assigned by an IETF Standards Action.
} klass;
std::vector<std::uint8_t> to_bytes(std::map<std::string, std::size_t>& nameMap, std::size_t currentOffset) const;
static std::expected<dns_question, ParseError> from_bytes(const std::span<std::uint8_t>& bytes, std::span<std::uint8_t>::const_iterator& i);
};
struct dns_resource : public dns_question {
std::chrono::seconds ttl; // time to live
std::vector<std::uint8_t> data;
std::vector<std::uint8_t> to_bytes(std::map<std::string, std::size_t>& nameMap, std::size_t currentOffset) const;
static std::expected<dns_resource, ParseError> from_bytes(const std::span<std::uint8_t>& bytes, std::span<std::uint8_t>::const_iterator& i);
};
struct dns_packet {
std::uint16_t id;
bool response : 1;
// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-5
enum class OPCODE : std::uint8_t {
Query = 0,
IQuery = 1, // Inverse Query (OBSOLETE)
Status = 2,
// 3 unassigned
Notify = 4,
UPupdate = 5,
// 6-15 unassinged
} query_type : 4;
bool authoritative_answer : 1;
bool truncated : 1;
bool recursion_desired : 1;
bool recursion_available : 1;
std::uint8_t : 1; // Z
bool authentic_data : 1;
bool checking_disabled : 1;
// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6
enum class RCODE : std::uint8_t {
NoError = 0, // No Error
FormErr = 1, // Format Error
ServFail = 2, // Server Failure
NXDomain = 3, // Non-Existent Domain
NotImp = 4, // Not Implemented
Refused = 5, // Query Refused
YXDomain = 6, // Name Exists when it should not
YXRRSet = 7, // RR Set Exists when it should not
NXRRSet = 8, // RR Set that should exist does not
NotAuth = 9, // Server Not Authoritative for zone / Not Authorized
NotZone = 10, // Name not contained in zone
DSOTYPENI = 11, // DSO-TYPE Not Implemented
// 12 - 15 Unassigned
BADVERS = 16, // Bad OPT Version
BADSIG = 16, // TSIG Signature Failure
BADKEY = 17, // Key not recognized
BADTIME = 18, // Signature out of time window
BADMODE = 19, // Bad TKEY Mode
BADNAME = 20, // Duplicate key name
BADALG = 21, // Algorithm not supported
BADTRUNC = 22, // Bad Truncation
BADCOOKIE = 23 // Bad/missing Server Cookie
// 24-3840 Unassigned
} response_type : 4;
std::vector<dns_question> questions;
std::vector<dns_resource> answers;
// name server records
// additional records
std::vector<std::uint8_t> to_bytes() const;
static std::expected<dns_packet, ParseError> from_bytes(const std::span<std::uint8_t>& bytes);
};
}
// dns.h
#include "asio.h"
namespace gamespy
{
class GameDB;
class DNSServer
{
static constexpr boost::asio::ip::port_type PORT = 53;
boost::asio::ip::udp::socket m_Socket;
GameDB& m_DB;
public:
DNSServer(boost::asio::io_context& context, GameDB& db);
~DNSServer();
boost::asio::awaitable<void> AcceptConnections();
};
}
// dns_details.cpp
#include "dns_details.h"
#include <stdexcept>
#include <optional>
#include <ranges>
using namespace dns;
std::vector<std::uint8_t> dns_question::to_bytes(std::map<std::string, std::size_t>& nameMap, std::size_t currentOffset) const
{
if (name.empty())
throw std::runtime_error("empty name is not allowed");
auto bytes = std::vector<std::uint8_t>{};
if (nameMap.contains(name)) {
auto pos = nameMap[name];
bytes.push_back(0b11000000 | ((pos >> 8) & 0xFF));
bytes.push_back(pos & 0xFF);
}
else {
nameMap.emplace(name, currentOffset);
for (const auto& domain : name | std::views::split('.')) {
if (domain.size() > 63) throw std::overflow_error("domain too long");
bytes.push_back(domain.size() & 0xFF);
bytes.append_range(domain);
}
bytes.push_back(0x00);
}
auto _type = std::to_underlying(type);
bytes.push_back((_type >> 8) & 0xFF);
bytes.push_back(_type & 0xFF);
auto _klass = std::to_underlying(klass);
bytes.push_back((_klass >> 8) & 0xFF);
bytes.push_back(_klass & 0xFF);
return bytes;
}
std::expected<dns_question, ParseError> dns_question::from_bytes(const std::span<std::uint8_t>& bytes, std::span<std::uint8_t>::const_iterator& i)
{
//auto i = std::vector<std::uint8_t>::iterator{ begin };
if (std::ranges::distance(i, bytes.end()) < 2)
return std::unexpected(ParseError::INCOMPLETE);
auto typeBegin = std::optional<std::span<std::uint8_t>::const_iterator>{};
auto name = std::vector<std::uint8_t>{};
for (std::size_t len = *i++; len;) {
if ((len & 0b11000000) == 0b11000000) {
// compression: pos stores the offset (relative to bytes::begin)
// -> the i-iterator is set to the offset position
// -> to "return" back to the parsing position, the current cursor needs to be saved (in typeBegin)
auto pos = ((len & 0b00111111) << 8) | *i;
typeBegin = ++i;
i = std::ranges::next(bytes.begin(), pos, bytes.end());
if (i == bytes.end())
return std::unexpected(ParseError::INVALID);
len = *i++;
}
auto domainEnd = std::ranges::next(i, len, bytes.end());
if (domainEnd == bytes.end())
return std::unexpected(ParseError::INCOMPLETE);
name.insert(name.end(), i, domainEnd);
i = domainEnd;
len = *i++; // note: we're guaranteed to not dereference bytes.end() here!
if (len != 0)
name.push_back('.');
}
// in case of compression the i-iterator has not been traversed and needs to be restored
if (typeBegin)
i = *typeBegin;
if (std::ranges::distance(i, bytes.end()) < 4)
return std::unexpected(ParseError::INCOMPLETE);
if (name.empty())
return std::unexpected(ParseError::INVALID);
std::uint16_t qType = (*i++ << 8) | *i++;
std::uint16_t qClass = (*i++ << 8) | *i++;
return dns_question{
.name = std::string{name.begin(), name.end()},
.type = static_cast<QTYPE>(qType),
.klass = static_cast<QCLASS>(qClass)
};
}
std::vector<std::uint8_t> dns_resource::to_bytes(std::map<std::string, std::size_t>& nameMap, std::size_t currentOffset) const
{
auto time_to_live = ttl.count();
if (time_to_live > std::numeric_limits<std::uint16_t>::max())
throw std::overflow_error("ttl does not fit in uint16_t");
auto rdLength = data.size();
if (data.size() > std::numeric_limits<std::uint16_t>::max())
throw std::overflow_error("dns_answer data is too large");
auto bytes = dns_question::to_bytes(nameMap, currentOffset);
bytes.push_back((time_to_live >> 24) & 0xFF);
bytes.push_back((time_to_live >> 16) & 0xFF);
bytes.push_back((time_to_live >> 8) & 0xFF);
bytes.push_back(time_to_live & 0xFF);
bytes.push_back((rdLength >> 8) & 0xFF);
bytes.push_back(rdLength & 0xFF);
bytes.append_range(data);
return bytes;
}
std::expected<dns_resource, ParseError> dns_resource::from_bytes(const std::span<std::uint8_t>& bytes, std::span<std::uint8_t>::const_iterator& i)
{
auto question = dns_question::from_bytes(bytes, i);
if (question) {
if (std::ranges::distance(i, bytes.end()) < 4)
return std::unexpected(ParseError::INCOMPLETE);
auto _ttl = static_cast<std::uint16_t>((*i++ << 8) | *i++);
auto size = static_cast<std::uint16_t>((*i++ << 8) | *i++);
if (std::ranges::distance(i, bytes.end()) < size)
return std::unexpected(ParseError::INCOMPLETE);
auto dataEnd = std::ranges::next(i, size, bytes.end());
auto data = std::vector<std::uint8_t>{ i, dataEnd };
i = dataEnd;
return dns_resource{
{
.name = question->name,
.type = question->type,
.klass = question->klass
},
std::chrono::seconds(_ttl),
std::move(data)
};
}
return std::unexpected(question.error());
}
std::vector<std::uint8_t> dns_packet::to_bytes() const
{
auto bytes = std::vector<std::uint8_t>{};
bytes.append_range(std::array{ (id >> 8) & 0xFF, id & 0xFF });
bytes.push_back(
static_cast<std::uint8_t>(response) << 7
| std::to_underlying(query_type) << 3
| static_cast<std::uint8_t>(authoritative_answer) << 2
| static_cast<std::uint8_t>(truncated) << 1
| static_cast<std::uint8_t>(recursion_desired)
);
bytes.push_back(
recursion_available << 7
| authentic_data << 5
| checking_disabled << 4
| std::to_underlying(response_type)
);
bytes.push_back((questions.size() >> 8) & 0xFF);
bytes.push_back(questions.size() & 0xFF);
bytes.push_back((answers.size() >> 8) & 0xFF);
bytes.push_back(answers.size() & 0xFF);
bytes.append_range(std::array{ 0x00, 0x00 }); // name server count
bytes.append_range(std::array{ 0x00, 0x00 }); // additional resource count
std::map<std::string, std::size_t> nameMap;
for (const auto& q : questions)
bytes.append_range(q.to_bytes(nameMap, bytes.size()));
for (const auto& a : answers)
bytes.append_range(a.to_bytes(nameMap, bytes.size()));
return bytes;
}
std::expected<dns_packet, ParseError> dns_packet::from_bytes(const std::span<std::uint8_t>& bytes)
{
if (bytes.size() < 12)
return std::unexpected(ParseError::INCOMPLETE);
std::uint16_t qdCount = (bytes[4] << 8) | bytes[5];
std::uint16_t anCount = (bytes[6] << 8) | bytes[7];
std::uint16_t nsCount = (bytes[8] << 8) | bytes[9];
std::uint16_t arCount = (bytes[10] << 8) | bytes[11];
std::vector<dns_question> questions;
auto i = bytes.cbegin() + 12;
for (std::uint16_t q = 0; q < qdCount; q++) {
auto question = dns_question::from_bytes(bytes, i);
if (question)
questions.push_back(*question);
else
return std::unexpected(question.error());
}
std::vector<dns_resource> answers;
for (std::uint16_t a = 0; a < anCount; a++) {
auto answer = dns_resource::from_bytes(bytes, i);
if (answer)
answers.push_back(*answer);
else
return std::unexpected(answer.error());
}
return dns_packet{
.id = static_cast<std::uint16_t>(bytes[0] << 8 | bytes[1]),
.response = static_cast<bool>( bytes[2] & 0b10000000),
.query_type = static_cast<OPCODE>(bytes[2] & 0b01111000),
.authoritative_answer = static_cast<bool>( bytes[2] & 0b00000100),
.truncated = static_cast<bool> ( bytes[2] & 0b00000010),
.recursion_desired = static_cast<bool>( bytes[2] & 0b00000001),
.recursion_available = static_cast<bool>( bytes[3] & 0b10000000),
.authentic_data = static_cast<bool>( bytes[3] & 0x00100000),
.checking_disabled = static_cast<bool>( bytes[3] & 0x00010000),
.response_type = static_cast<RCODE>( bytes[3] & 0b00001111),
.questions = std::move(questions),
.answers = std::move(answers)
};
}
// dns.cpp
#include "dns.h"
#include "dns_details.h"
#include <array>
#include <print>
using boost::asio::ip::udp;
using namespace gamespy;
void HandlePacket(dns::dns_packet& packet)
{
for (const auto& q : packet.questions) {
if ((q.type == dns::dns_question::QTYPE::A || q.type == dns::dns_question::QTYPE::AAAA) && q.klass == dns::dns_question::QCLASS::INTERNET) {
if (q.name.ends_with("gamespy.com") || q.name.ends_with("dice.se")) {
auto data = std::vector<std::uint8_t>{};
if (q.type == dns::dns_question::QTYPE::A)
data.append_range(boost::asio::ip::address_v4::loopback().to_bytes());
else
data.append_range(boost::asio::ip::address_v6::loopback().to_bytes());
packet.answers.push_back(dns::dns_resource{
{
.name = q.name,
.type = q.type,
.klass = q.klass,
},
std::chrono::seconds(180),
std::move(data)
});
}
packet.authoritative_answer = true;
}
else {
packet.response_type = dns::dns_packet::RCODE::NXDomain;
break;
}
}
packet.response = true;
packet.recursion_available = false;
}
DNSServer::DNSServer(boost::asio::io_context& context, GameDB& db)
: m_Socket(context, udp::endpoint(udp::v6(), PORT)), m_DB(db)
{
std::println("[dns] listening on {} for *.gamespy.com", PORT);
}
DNSServer::~DNSServer()
{
}
boost::asio::awaitable<void> DNSServer::AcceptConnections()
{
auto buff = std::array<std::uint8_t, 512>{};
while (m_Socket.is_open()) {
udp::endpoint client;
const auto& [error, length] = co_await m_Socket.async_receive_from(boost::asio::buffer(buff), client, boost::asio::as_tuple(boost::asio::use_awaitable));
if (error)
break;
else if (length == 0)
continue;
auto packet = dns::dns_packet::from_bytes(buff);
if (!packet || packet->questions.size() == 0)
continue;
HandlePacket(*packet);
co_await m_Socket.async_send_to(boost::asio::buffer(packet->to_bytes()), client, boost::asio::use_awaitable);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment