Skip to content

Commit

Permalink
Implement DNS query support
Browse files Browse the repository at this point in the history
Implement support for dns request / response with the goal of allowing a asyncronous host -> ip resolution mechanism.

* Created a example tool for dns query:
```
$ bin/toolbox-dns -s 8.8.8.8 www.reactivemarkets.com
```
* Added an additional hex_dump debug facility

Close reactivemarkets#57
  • Loading branch information
rfernandes committed Oct 29, 2019
1 parent c294b74 commit 712e1a2
Show file tree
Hide file tree
Showing 14 changed files with 600 additions and 4 deletions.
6 changes: 3 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ install_libraries("${Boost_DATE_TIME_LIBRARY}")

add_definitions(-DBOOST_NO_AUTO_PTR=1 -DBOOST_NO_RTTI=1 -DBOOST_NO_TYPEID=1)
add_definitions(-DBOOST_ASIO_DISABLE_THREADS=1 -DBOOST_ASIO_HEADER_ONLY=1)
if(NOT "${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}" MATCHES "\\.a$")
add_definitions(-DBOOST_TEST_DYN_LINK)
endif()
# if(NOT "${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}" MATCHES "\\.a$")
# add_definitions(-DBOOST_TEST_DYN_LINK)
# endif()

find_package(Doxygen) # Optional.

Expand Down
3 changes: 3 additions & 0 deletions example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ target_link_libraries(toolbox-echo-serv ${toolbox_LIBRARY})

add_executable(toolbox-http-serv HttpServ.cpp)
target_link_libraries(toolbox-http-serv ${toolbox_LIBRARY})

add_executable(toolbox-dns Dns.cpp)
target_link_libraries(toolbox-dns ${toolbox_LIBRARY})
61 changes: 61 additions & 0 deletions example/Dns.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// The Reactive C++ Toolbox.
// Copyright (C) 2013-2019 Swirly Cloud Limited
// Copyright (C) 2019 Reactive Markets Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <toolbox/io.hpp>
#include <toolbox/net.hpp>
#include <toolbox/sys.hpp>
#include <toolbox/util.hpp>

#include <fstream>
#include <iostream>

using namespace std;
using namespace toolbox;
using namespace toolbox::net;

int main(int argc, char* argv[])
{
std::ios::sync_with_stdio(false);
try {

std::string nameserver;
std::string hostname;

Options opts{"Unit Test options [OPTIONS] [COMMAND]"};
// clang-format off
opts('s', Value{nameserver}, "ShortOption Description")
('h', "help", Help{}, "Help")
(Value{hostname}.required(), "Hostname")
;
// clang-format on

opts.parse(argc, argv);

if (nameserver.empty()) {
std::ifstream resolv_conf{"/etc/resolv.conf"};
DnsServers dns_servers{resolv_conf};
nameserver = dns_servers.server();
}

const auto ip = toolbox::net::get_host_by_name(nameserver, hostname, DnsRequest::A);
cout << ip << endl;

} catch (const std::exception& e) {
TOOLBOX_ERROR << "exception: " << e.what();
return 1;
}
return 0;
}
2 changes: 2 additions & 0 deletions toolbox/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ set(lib_SOURCES
net/McastSock.cpp
net/Protocol.cpp
net/Resolver.cpp
net/Resolver2.cpp
net/Runner.cpp
net/Socket.cpp
net/StreamAcceptor.cpp
Expand Down Expand Up @@ -218,6 +219,7 @@ set(test_SOURCES
net/Endpoint.ut.cpp
net/IoSock.ut.cpp
net/Resolver.ut.cpp
net/Resolver2.ut.cpp
net/Runner.ut.cpp
net/Socket.ut.cpp
sys/Date.ut.cpp
Expand Down
1 change: 1 addition & 0 deletions toolbox/net.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "net/McastSock.hpp"
#include "net/Protocol.hpp"
#include "net/Resolver.hpp"
#include "net/Resolver2.hpp"
#include "net/Runner.hpp"
#include "net/Socket.hpp"
#include "net/StreamAcceptor.hpp"
Expand Down
281 changes: 281 additions & 0 deletions toolbox/net/Resolver2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
// The Reactive C++ Toolbox.
// Copyright (C) 2013-2019 Swirly Cloud Limited
// Copyright (C) 2019 Reactive Markets Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "Resolver2.hpp"

#include "Endpoint.hpp"
#include "Socket.hpp"

#include <toolbox/net/DgramSock.hpp>

#include <toolbox/util/Stream.hpp>

#include <fstream>
#include <iomanip>
#include <iostream>

namespace toolbox {
inline namespace net {

char* to_dns(const std::string& host, char* buff)
{
std::size_t pos{0};
std::string_view view{host};
while ((pos = view.find_first_of('.'), pos) != std::string_view::npos) {
*buff = static_cast<char>(pos);
std::memcpy(++buff, view.data(), pos);
view.remove_prefix(pos + 1); // Skip the dot
buff += pos;
}
*buff = static_cast<char>(view.size());
std::memcpy(++buff, view.data(), view.size() + 1); // Include null terminator
return buff += view.size() + 1;
}

DnsServers::DnsServers(std::istream& resolv_conf, std::vector<std::string> additional_nameservers)
: servers_{std::move(additional_nameservers)}
{
std::string line;
const std::string nameserver{"nameserver"};
while (std::getline(resolv_conf, line)) {
// C++20 Replace with starts_with
if (line.compare(0, nameserver.size(), nameserver) == 0) {
auto pos = line.find_first_not_of(' ', nameserver.size() + 1);
if (pos != std::string::npos) {
servers_.emplace_back(line.substr(pos));
}
}
}

if (servers_.empty()) {
throw std::runtime_error("No servers available");
}
}

const DnsServers::Servers& DnsServers::servers() const noexcept
{
return servers_;
}

const std::string& DnsServers::server() const
{
return servers_[0];
}

#pragma pack(push, 1)

class Dns {
public:
Dns(const std::string& hostname, DnsRequest::Type query_type)
{
auto res = to_dns(hostname, query_name());
hostname_size_ = res - query_name();
question().qtype = htons(query_type);
question().class_ = htons(1); // Internet
}

class Header {

public:
Header()
: id_{(unsigned short)htons(getpid())}
, recursion_desired_{1}
, truncated_{0}
, authorative_answer_{0}
, opcode_{0} // Standard query
, qr_{0} // Query
, rcode_{0}
, cd_{0}
, ad_{0}
, z_{0}
, recursion_available_{0}
, questions_{htons(1)} // Single question
, ans_count_{0}
, auth_count_{0}
, add_count_{0}
{
}

bool recursion_desired() const noexcept { return recursion_desired_; }
bool truncated() const noexcept { return truncated_; }
bool authorative_answer() const noexcept { return authorative_answer_; }
auto opcode() const noexcept { return opcode_; }
bool qr() const noexcept { return qr_; }
auto rcode() const noexcept { return rcode_; }
bool cd() const noexcept { return cd_; }
bool ad() const noexcept { return ad_; }
bool z() const noexcept { return z_; }
bool recursion_available() const noexcept { return recursion_available_; }

auto questions() const noexcept { return ntohs(questions_); }
auto ans_count() const noexcept { return ntohs(ans_count_); }
auto auth_count() const noexcept { return ntohs(auth_count_); }
auto add_count() const noexcept { return ntohs(add_count_); }

private:
// C++20 will allow bitfield initialization on declaration
uint16_t id_; // message id
uint8_t recursion_desired_ : 1; // recursion desired
uint8_t truncated_ : 1; // truncated message
uint8_t authorative_answer_ : 1; // authoritive answer
uint8_t opcode_ : 4; // purpose of message
uint8_t qr_ : 1; // query/response flag

uint8_t rcode_ : 4; // response code
uint8_t cd_ : 1; // disable checking
uint8_t ad_ : 1; // authenticated data
uint8_t z_ : 1; // its z! reserveddns_servers
uint8_t recursion_available_ : 1; // recursion available

uint16_t questions_; // number of question entries
uint16_t ans_count_; // number of answer entries
uint16_t auth_count_; // number of authority entries
uint16_t add_count_; // number of resource entries
};

struct Question {
uint16_t qtype;
uint16_t class_;
};

class RData {
public:
enum class Type : uint16_t {
A_RECORD = 0x0001,
NAME_SERVER = 0x0002,
CNAME = 0x0005,
MAIL_SERVER = 0x000f
};

Type type() const noexcept { return static_cast<Type>(ntohs(type_)); }
auto class_() const noexcept { return ntohs(class__); }
auto ttl() const noexcept { return ntohs(ttl_); }
auto length() const noexcept { return ntohs(length_); }

private:
uint16_t type_;
uint16_t class__;
uint32_t ttl_;
uint16_t length_;
};

const auto& header() const noexcept { return header_; }
auto& header() noexcept { return header_; }

char* query_name() noexcept { return reinterpret_cast<char*>(&header_) + sizeof(Header); }

int hostname_size() const noexcept { return hostname_size_; }

Question& question() noexcept
{
return *reinterpret_cast<Question*>(query_name() + hostname_size_);
}

int size() const noexcept { return sizeof(Header) + hostname_size_ + sizeof(Question); }

private:
// bookkeeping
int64_t hostname_size_;

Header header_;
};
#pragma pack(pop)

/// Convert strings like www.google.com to 3www6google3com, where numbers are bytes that follow
std::string dns_format(const std::string& host)
{
std::size_t pos = 0;
std::stringstream out;
std::string_view view{host};
while ((pos = view.find_first_of('.'), pos) != std::string_view::npos) {
out << static_cast<char>(pos) << view.substr(0, pos);
view.remove_prefix(pos + 1);
}
out << static_cast<char>(view.size()) << view;
return out.str();
}

toolbox::net::IpAddrV4 TOOLBOX_API get_host_by_name(const std::string& nameserver,
const std::string& hostname,
DnsRequest::Type query_type)
{
sockaddr_in dest;
int sckt = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //UDP packet for DNS queries
dest.sin_family = AF_INET;
dest.sin_port = htons(53);
dest.sin_addr.s_addr = inet_addr(nameserver.c_str());

//Set the DNS structure to standard queries
char send_buffer[2048];
Dns* dns = new (send_buffer) Dns{hostname, query_type};

//point to the query portion
int query_name_size = dns->hostname_size();

if (sendto(sckt, reinterpret_cast<const char*>(&dns->header()), dns->size(), 0,
(struct sockaddr*)&dest, sizeof(dest))
< 0) {
throw std::runtime_error{"Error sending dns request"};
}

// TODO: Split request / response through reactor to make it asynchronous.
// use id field to track them if required.

//Receive the answer
int dest_size = sizeof dest;
char recv_buffer[2048];

if (recvfrom(sckt, recv_buffer, sizeof(recv_buffer), 0, (sockaddr*)&dest,
(socklen_t*)&dest_size)
< 0) {
throw std::runtime_error{"Error receiving dns response"};
}

// Adjust dns to struct
dns = reinterpret_cast<Dns*>(&recv_buffer);

//move ahead of the dns header and the query field
char* reader = &recv_buffer[sizeof(Dns::Header) + query_name_size + sizeof(Dns::Question)];

//Start reading answers
for (int i = 0; i < dns->header().ans_count(); i++) {
if (*reinterpret_cast<unsigned char*>(reader) >= 192) // Name is in the header (skip)
{
reader += 2;
} else {
throw std::runtime_error("Implement in answer full name decoding");
}

const auto& r_data = *reinterpret_cast<Dns::RData*>(reader);
reader = reader + sizeof(Dns::RData);

assert(r_data._class() == 1);

switch (r_data.type()) {
case Dns::RData::Type::A_RECORD: {
const auto& bytes = *reinterpret_cast<boost::asio::ip::address_v4::bytes_type*>(reader);
return boost::asio::ip::make_address_v4(bytes);
}
default: { //Skip all other records
reader = reader + r_data.length();
}
}
}
throw std::runtime_error{"Could not resolve addr"};
}

} // namespace net
} // namespace toolbox
Loading

0 comments on commit 712e1a2

Please sign in to comment.