Created
July 30, 2025 07:48
-
-
Save kr4uzi/91d05200432ae32d99294ccd8c46b3f7 to your computer and use it in GitHub Desktop.
Backup of my gamespy-emulator dns implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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