Files
hello-dns/tdns/tres.cc
2018-10-16 00:18:00 +02:00

381 lines
14 KiB
C++

#include <cstdint>
#include <vector>
#include <map>
#include <stdexcept>
#include "sclasses.hh"
#include <signal.h>
#include <random>
#include "record-types.hh"
/*!
@file
@brief Teachable resolver
*/
using namespace std;
multimap<DNSName, ComboAddress> g_root;
unsigned int g_numqueries;
bool g_skipIPv6{false}; //!< set this if you have no functioning IPv6
/** Helper function that extracts a useable IP address from an
A or AAAA resource record. Returns sin_family == 0 if it didn't work */
ComboAddress getIP(const std::unique_ptr<RRGen>& rr)
{
ComboAddress ret;
ret.sin4.sin_family = 0;
if(auto ptr = dynamic_cast<AGen*>(rr.get()))
ret=ptr->getIP();
else if(auto ptr = dynamic_cast<AAAAGen*>(rr.get()))
ret=ptr->getIP();
ret.sin4.sin_port = htons(53);
return ret;
}
//! Thrown if too many queries have been sent.
struct TooManyQueriesException{};
/** This function guarantees that you will get an answer from this server. It will drop EDNS for you
and eventually it will even fall back to TCP for you. If nothing works, an exception is thrown.
Note that this function does not think about actual DNS errors, you get those back verbatim.
Only the TC bit is checked.
This function does check if the ID field of the response matches the query, but the caller should
check qname and qtype.
*/
DNSMessageReader getResponse(const ComboAddress& server, const DNSName& dn, const DNSType& dt, int depth=0)
{
std::string prefix(depth, ' ');
prefix += dn.toString() + "|"+toString(dt)+" ";
bool doEDNS=true, doTCP=false;
for(int tries = 0; tries < 4 ; ++tries) {
if(++g_numqueries > 30) // there is the possibility our algorithm will loop
throw TooManyQueriesException(); // and send out thousands of queries, so let's not
DNSMessageWriter dmw(dn, dt);
dmw.dh.rd = false;
dmw.randomizeID();
if(doEDNS)
dmw.setEDNS(1500, false); // no DNSSEC for now
string resp;
double timeout=1.0;
if(doTCP) {
Socket sock(server.sin4.sin_family, SOCK_STREAM);
SConnect(sock, server);
string ser = dmw.serialize();
uint16_t len = htons(ser.length());
string tmp((char*)&len, 2);
SWrite(sock, tmp);
SWrite(sock, ser);
int err = waitForData(sock, &timeout);
if( err <= 0) {
throw std::runtime_error("Error waiting for data from "+server.toStringWithPort()+": "+ (err ? string(strerror(errno)): string("Timeout")));
}
tmp=SRead(sock, 2);
len = ntohs(*((uint16_t*)tmp.c_str()));
// so yes, you need to check for a timeout here again!
err = waitForData(sock, &timeout);
if( err <= 0) {
throw std::runtime_error("Error waiting for data from "+server.toStringWithPort()+": "+ (err ? string(strerror(errno)): string("Timeout")));
}
// and even this is not good enough, an authoritative server could be trickling us bytes
resp = SRead(sock, len);
}
else {
Socket sock(server.sin4.sin_family, SOCK_DGRAM);
SConnect(sock, server);
SWrite(sock, dmw.serialize());
int err = waitForData(sock, &timeout);
// so one could simply retry on a timeout, but here we don't
if( err <= 0) {
throw std::runtime_error("Error waiting for data from "+server.toStringWithPort()+": "+ (err ? string(strerror(errno)): string("Timeout")));
}
ComboAddress ign=server;
resp = SRecvfrom(sock, 65535, ign);
}
DNSMessageReader dmr(resp);
if(dmr.dh.id != dmw.dh.id) {
cout << prefix << "ID mismatch on answer" << endl;
continue;
}
if(!dmr.dh.qr) { // for security reasons, you really need this
cout << prefix << "What we received was not a response, ignoring"<<endl;
continue;
}
if((RCode)dmr.dh.rcode == RCode::Formerr) { // XXX this should check that there is no OPT in the response
cout << prefix <<"Got a Formerr, resending without EDNS"<<endl;
doEDNS=false;
continue;
}
if(dmr.dh.tc) {
cout << prefix <<"Got a truncated answer, retrying over TCP"<<endl;
doTCP=true;
continue;
}
return dmr;
}
// should never get here
return DNSMessageReader(""); // just to make compiler happy
}
//! this is a different kind of error: we KNOW your name does not exist
struct NxdomainException{};
//! Or if your type does not exist
struct NodataException{};
/** This attempts to look up the name dn with type dt. The depth parameter is for
trace output.
the 'auth' field describes the authority of the servers we will be talking to. Defaults to root ('believe everything')
The multimap specifies the servers to try with. Defaults to a list of
root-servers.
*/
vector<std::unique_ptr<RRGen>> resolveAt(const DNSName& dn, const DNSType& dt, int depth=0, const DNSName& auth={}, const multimap<DNSName, ComboAddress>& mservers=g_root)
{
std::string prefix(depth, ' ');
prefix += dn.toString() + "|"+toString(dt)+" ";
vector<std::unique_ptr<RRGen>> ret;
// it is good form to sort the servers in order of response time
// for tres, this is not done, but we do randomize
cout << prefix << "Starting query at authority = "<<auth<< ", have "<<mservers.size() << " addresses to try"<<endl;
vector<pair<DNSName, ComboAddress> > servers;
for(auto& sp : mservers)
servers.push_back(sp);
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(servers.begin(), servers.end(), g);
for(auto& sp : servers) {
ret.clear();
ComboAddress server=sp.second;
server.sin4.sin_port = htons(53);
if(g_skipIPv6 && server.sin4.sin_family == AF_INET6)
continue;
try {
cout << prefix<<"Sending to server "<<sp.first<<" on "<<server.toString()<<endl;
DNSMessageReader dmr = getResponse(server, dn, dt, depth); // takes care of EDNS and TCP for us
DNSSection rrsection;
uint32_t ttl;
DNSName rrdn, newAuth;
DNSType rrdt;
dmr.getQuestion(rrdn, rrdt); // parse
cout << prefix<<"Received response with RCode "<<(RCode)dmr.dh.rcode<<", qname " <<dn<<", qtype "<<dt<<", aa: "<<dmr.dh.aa << endl;
if(rrdn != dn || dt != rrdt) {
cout << prefix << "Got a response to a different question or different type than we asked for!"<<endl;
continue; // see if another server wants to work with us
}
// in a real resolver, you must ignore NXDOMAIN in case of a CNAME. Because that is how the internet rolls.
if((RCode)dmr.dh.rcode == RCode::Nxdomain) {
cout << prefix<<"Got an Nxdomain, it does not exist"<<endl;
throw NxdomainException();
}
else if((RCode)dmr.dh.rcode != RCode::Noerror) {
throw std::runtime_error(string("Answer from authoritative server had an error: ") + toString((RCode)dmr.dh.rcode));
}
if(dmr.dh.aa) {
cout << prefix<<"Answer says it is authoritative!"<<endl;
}
std::unique_ptr<RRGen> rr;
set<DNSName> nsses;
multimap<DNSName, ComboAddress> addresses;
/* here we loop over records. Perhaps the answer is there, perhaps
there is a CNAME we should follow, perhaps we get a delegation.
And if we do get a delegation, there might even be useful glue */
while(dmr.getRR(rrsection, rrdn, rrdt, ttl, rr)) {
cout << prefix << rrsection<<" "<<rrdn<< " IN " << rrdt << " " << ttl << " " <<rr->toString()<<endl;
if(dmr.dh.aa==1) {
if(dn == rrdn && dt == rrdt) {
cout << prefix<<"We got an answer to our question!"<<endl;
ret.push_back(std::move(rr));
}
if(dn == rrdn && rrdt == DNSType::CNAME) {
DNSName target = dynamic_cast<CNAMEGen*>(rr.get())->d_name;
cout << prefix<<"We got a CNAME to " << target <<", chasing"<<endl;
if(target.isPartOf(auth)) { // this points to something we consider this server auth for
cout << prefix << "target " << target << " is within " << auth<<", harvesting from packet"<<endl;
bool hadMatch=false; // perhaps the answer is in this DNS message
while(dmr.getRR(rrsection, rrdn, rrdt, ttl, rr)) {
if(rrsection==DNSSection::Answer && rrdn == target && rrdt == dt) {
hadMatch=true;
ret.push_back(std::move(rr));
}
}
if(hadMatch) { // if it worked, great, otherwise actual chase
cout << prefix << "in-message chase worked, we're done"<<endl;
return ret;
}
else
cout <<prefix<<"in-message chase not successful, will do new query for "<<target<<endl;
}
return resolveAt(target, dt, depth + 1);
}
}
else {
// this picks up nameserver records, and we even believe your glue.. but ONLY for this query
// from a security perspective, all an auth can do is ruin the result, since we don't cache
// if an auth serves confused glue, resolution will suffer
// (so in other words, if you have an out of zone NS record, we will believe your glue)
if(rrsection == DNSSection::Authority && rrdt == DNSType::NS) {
if(dn.isPartOf(rrdn)) {
DNSName nsname = dynamic_cast<NSGen*>(rr.get())->d_name;
nsses.insert(nsname);
newAuth = rrdn;
}
else
cout<< prefix << "Authoritative server gave us NS record to which this query does not belong" <<endl;
}
else if(rrsection == DNSSection::Additional && nsses.count(rrdn) && (rrdt == DNSType::A || rrdt == DNSType::AAAA)) {
addresses.insert({rrdn, getIP(rr)}); // this only picks up addresses for NS records we've seen already
// but that is ok: NS is in Authority section
}
}
}
if(!ret.empty()) {
// the answer is in!
cout << prefix<<"Done, returning "<<ret.size()<<" results\n";
return ret;
}
else if(dmr.dh.aa) {
cout << prefix <<"No data response"<<endl;
throw NodataException();
}
// we got a delegation
cout << prefix << "We got delegated to " << nsses.size() << " " << newAuth << " nameserver names " << endl;
if(!addresses.empty()) {
// in addresses are nameservers for which we have IP or IPv6 addresses
cout << prefix<<"Have "<<addresses.size()<<" IP addresses to iterate to: ";
for(const auto& p : addresses)
cout << p.first <<"="<<p.second.toString()<<" ";
cout <<endl;
auto res2=resolveAt(dn, dt, depth+1, newAuth, addresses);
if(!res2.empty())
return res2;
cout << prefix<<"The IP addresses we had did not provide a good answer"<<endl;
}
// well we could not make it work using the servers we had addresses for. Let's try
// to get addresses for the rest
cout << prefix<<"Don't have a resolved nameserver to ask anymore, trying to resolve "<<nsses.size()<<" names"<<endl;
for(const auto& name: nsses) {
multimap<DNSName, ComboAddress> newns;
cout << prefix<<"Attempting to resolve NS "<<name<<endl;
for(const DNSType& qtype : {DNSType::A, DNSType::AAAA}) {
try {
auto result = resolveAt(name, qtype, depth+1);
cout << prefix<<"Got "<<result.size()<<" nameserver IPv4 addresses, adding to list"<<endl;
for(const auto& res : result)
newns.insert({name, getIP(res)});
}
catch(...)
{
cout << prefix <<"Failed to resolve name for "<<name<<"|"<<qtype<<endl;
}
}
cout << prefix<<"We now have "<<newns.size()<<" resolved addresses to try"<<endl;
if(newns.empty())
continue;
// we have a new (set) of addresses to try
auto res2 = resolveAt(dn, dt, depth+1, newAuth, newns);
if(!res2.empty()) // it worked!
return res2;
// it didn't, let's move on to the next server
}
}
catch(std::exception& e) {
cout << prefix <<"Error resolving: " << e.what() << endl;
}
}
// if we get here, we have no results for you.
return ret;
}
int main(int argc, char** argv)
try
{
if(argc != 3) {
cerr<<"Syntax: tres name type\n";
return(EXIT_FAILURE);
}
signal(SIGPIPE, SIG_IGN); // TCP, so we need this
// configure some hints
multimap<DNSName, ComboAddress> hints = {{makeDNSName("a.root-servers.net"), ComboAddress("198.41.0.4", 53)},
{makeDNSName("f.root-servers.net"), ComboAddress("192.5.5.241", 53)},
{makeDNSName("k.root-servers.net"), ComboAddress("193.0.14.129", 53)},
};
// retrieve the actual live NSSET from the hints
for(const auto& h : hints) {
try {
DNSMessageReader dmr = getResponse(h.second, makeDNSName("."), DNSType::NS);
DNSSection rrsection;
DNSName rrdn;
DNSType rrdt;
uint32_t ttl;
std::unique_ptr<RRGen> rr;
// this assumes the root will only send us relevant NS records
// we could check with the NS records if we wanted
// but if a root wants to mess with us, it can
while(dmr.getRR(rrsection, rrdn, rrdt, ttl, rr)) {
if(rrdt == DNSType::A || rrdt == DNSType::AAAA)
g_root.insert({rrdn, getIP(rr)});
}
break;
}
catch(...){}
}
cout<<"Retrieved . NSSET from hints, have "<<g_root.size()<<" addresses"<<endl;
DNSName dn = makeDNSName(argv[1]);
DNSType dt = makeDNSType(argv[2]);
auto res = resolveAt(dn, dt);
cout<<"Result or query for "<< dn <<"|"<<toString(dt)<<endl;
for(const auto& r : res) {
cout<<r->toString()<<endl;
}
cout<<"Used "<<g_numqueries << " queries"<<endl;
}
catch(std::exception& e)
{
cerr<<"Fatal error: "<<e.what()<<endl;
return EXIT_FAILURE;
}
catch(NxdomainException& e)
{
cout<<"Name does not exist"<<endl;
return EXIT_FAILURE;
}
catch(NodataException& e)
{
cout<<"Name does not have datatype requested"<<endl;
return EXIT_FAILURE;
}