537 lines
19 KiB
C++
537 lines
19 KiB
C++
#include <fstream>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <stdexcept>
|
|
#include "sclasses.hh"
|
|
#include <signal.h>
|
|
#include <random>
|
|
#include "record-types.hh"
|
|
#include <thread>
|
|
|
|
/*!
|
|
@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
|
|
|
|
ofstream g_dot;
|
|
|
|
/** Helper function that extracts a useable IP address from an
|
|
A or AAAA resource record. Returns sin_family == 0 if it didn't work */
|
|
static 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 > 300) // 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, 1500 byte buffer size
|
|
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 describes a single resource record
|
|
struct ResolveRR
|
|
{
|
|
DNSName name;
|
|
uint32_t ttl;
|
|
std::unique_ptr<RRGen> rr;
|
|
};
|
|
|
|
//! This is the end result of our resolving work
|
|
struct ResolveResult
|
|
{
|
|
vector<ResolveRR> res; //!< what you asked for
|
|
vector<ResolveRR> intermediate; //!< a CNAME chain that gets you there
|
|
void clear()
|
|
{
|
|
res.clear();
|
|
intermediate.clear();
|
|
}
|
|
};
|
|
|
|
/** This takes a list of servers (in a specific order) and shuffles them to a vector.
|
|
This is to spread the load across nameservers
|
|
*/
|
|
|
|
static auto randomizeServers(const multimap<DNSName, ComboAddress>& mservers)
|
|
{
|
|
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);
|
|
return servers;
|
|
}
|
|
|
|
static void dotQuery(const DNSName& auth, const DNSName& server)
|
|
{
|
|
g_dot << '"' << auth << "\" -> \"" << server << "\"" << endl;
|
|
}
|
|
|
|
static void dotAnswer(const DNSName& dn, const DNSType& rrdt, const DNSName& server)
|
|
{
|
|
g_dot <<"\"" << dn << "/"<<rrdt<<"\" [shape=box]\n";
|
|
g_dot << '"' << server << "\" -> \"" << dn << "/"<<rrdt<<"\"\n";
|
|
}
|
|
|
|
static void dotCNAME(const DNSName& target, const DNSName& server, const DNSName& dn)
|
|
{
|
|
g_dot << '"' << target << "\" [shape=box]"<<endl;
|
|
g_dot << '"' << server << "\" -> \"" << dn << "/CNAME\" -> \"" << target <<"\"\n";
|
|
}
|
|
|
|
static void dotDelegation(const DNSName& rrdn, const DNSName& server)
|
|
{
|
|
g_dot << '"' << rrdn << "\" [shape=diamond]\n";
|
|
g_dot << '"' << server << "\" -> \"" << rrdn << "\"\n";
|
|
}
|
|
|
|
/** 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.
|
|
*/
|
|
|
|
ResolveResult 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)+" ";
|
|
cout << prefix << "Starting query at authority = "<<auth<< ", have "<<mservers.size() << " addresses to try"<<endl;
|
|
|
|
// it is good form to sort the servers in order of response time
|
|
// for tres, this is not done (since we have no memory), but we do randomize:
|
|
|
|
auto servers = randomizeServers(mservers);
|
|
ResolveResult ret;
|
|
for(auto& sp : servers) {
|
|
dotQuery(auth, sp.first);
|
|
|
|
ret.clear();
|
|
ComboAddress server=sp.second;
|
|
server.sin4.sin_port = htons(53); // just to be sure
|
|
|
|
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 into rrdn and rrdt
|
|
|
|
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) { // authoritative answer. We trust this.
|
|
if(rrsection == DNSSection::Answer && dn == rrdn && dt == rrdt) {
|
|
cout << prefix<<"We got an answer to our question!"<<endl;
|
|
dotAnswer(dn, rrdt, sp.first);
|
|
ret.res.push_back({dn, ttl, std::move(rr)});
|
|
}
|
|
else if(dn == rrdn && rrdt == DNSType::CNAME) {
|
|
DNSName target = dynamic_cast<CNAMEGen*>(rr.get())->d_name;
|
|
ret.intermediate.push_back({dn, ttl, std::move(rr)}); // rr is DEAD now!
|
|
cout << prefix<<"We got a CNAME to " << target <<", chasing"<<endl;
|
|
dotCNAME(target, sp.first, dn);
|
|
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.res.push_back({dn, ttl, 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;
|
|
}
|
|
|
|
auto chaseres=resolveAt(target, dt, depth + 1);
|
|
ret.res = std::move(chaseres.res);
|
|
for(auto& rr : chaseres.intermediate) // add up their intermediates to ours
|
|
ret.intermediate.push_back(std::move(rr));
|
|
return ret;
|
|
}
|
|
}
|
|
else {
|
|
// this picks up nameserver records. We check if glue records are within the authority
|
|
// of what we approached this server for.
|
|
if(rrsection == DNSSection::Authority && rrdt == DNSType::NS) {
|
|
if(dn.isPartOf(rrdn)) {
|
|
DNSName nsname = dynamic_cast<NSGen*>(rr.get())->d_name;
|
|
|
|
if(!dmr.dh.aa && (newAuth != rrdn || nsses.empty())) {
|
|
dotDelegation(rrdn, sp.first);
|
|
}
|
|
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)) {
|
|
// this only picks up addresses for NS records we've seen already
|
|
// but that is ok: NS is in Authority section
|
|
if(rrdn.isPartOf(auth))
|
|
addresses.insert({rrdn, getIP(rr)});
|
|
else
|
|
cout << prefix << "Not accepting IP address of " << rrdn <<": out of authority of this server"<<endl;
|
|
}
|
|
}
|
|
}
|
|
if(!ret.res.empty()) {
|
|
// the answer is in!
|
|
cout << prefix<<"Done, returning "<<ret.res.size()<<" results, "<<ret.intermediate.size()<<" intermediate\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.res.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 from "<<nsses.size()<<" unresolved 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.res.size()<<" nameserver " << qtype <<" addresses, adding to list"<<endl;
|
|
for(const auto& res : result.res)
|
|
newns.insert({name, getIP(res.rr)});
|
|
}
|
|
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.res.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;
|
|
}
|
|
|
|
//! This is a thread that will create an answer to the query in `dmr`
|
|
void processQuery(int sock, ComboAddress client, DNSMessageReader dmr)
|
|
try
|
|
{
|
|
g_numqueries = 0;
|
|
DNSName dn;
|
|
DNSType dt;
|
|
dmr.getQuestion(dn, dt);
|
|
|
|
DNSMessageWriter dmw(dn, dt);
|
|
dmw.dh.rd = dmr.dh.rd;
|
|
dmw.dh.ra = true;
|
|
dmw.dh.qr = true;
|
|
dmw.dh.id = dmr.dh.id;
|
|
|
|
ResolveResult res;
|
|
try {
|
|
res = resolveAt(dn, dt);
|
|
|
|
cout<<"Result of query for "<< dn <<"|"<<toString(dt)<<endl;
|
|
for(const auto& r : res.intermediate) {
|
|
cout<<r.name <<" "<<r.ttl<<" "<<r.rr->getType()<<" " << r.rr->toString()<<endl;
|
|
}
|
|
|
|
for(const auto& r : res.res) {
|
|
cout<<r.name <<" "<<r.ttl<<" "<<r.rr->getType()<<" "<<r.rr->toString()<<endl;
|
|
}
|
|
}
|
|
catch(NodataException& nd)
|
|
{
|
|
SSendto(sock, dmw.serialize(), client);
|
|
return;
|
|
}
|
|
catch(NxdomainException& nx)
|
|
{
|
|
dmw.dh.rcode = (int)RCode::Nxdomain;
|
|
SSendto(sock, dmw.serialize(), client);
|
|
return;
|
|
}
|
|
// Put in the CNAME chain
|
|
for(const auto& rr : res.intermediate)
|
|
dmw.putRR(DNSSection::Answer, rr.name, rr.ttl, rr.rr);
|
|
for(const auto& rr : res.res) // and the actual answer
|
|
dmw.putRR(DNSSection::Answer, rr.name, rr.ttl, rr.rr);
|
|
string resp = dmw.serialize();
|
|
SSendto(sock, resp, client); // and send it!
|
|
}
|
|
catch(exception& e)
|
|
{
|
|
cerr << "Thread died: " << e.what() << endl;
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
try
|
|
{
|
|
if(argc != 2 && argc != 3) {
|
|
cerr<<"Syntax: tres name type\n";
|
|
cerr<<"Syntax: tres ip:port\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 root 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;
|
|
|
|
// XXX should check if response name and type match query
|
|
// 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;
|
|
|
|
if(argc == 2) { // be a server
|
|
ComboAddress local(argv[1], 53);
|
|
Socket sock(local.sin4.sin_family, SOCK_DGRAM);
|
|
SBind(sock, local);
|
|
string packet;
|
|
ComboAddress client;
|
|
|
|
for(;;) {
|
|
try {
|
|
packet = SRecvfrom(sock, 1500, client);
|
|
cout<<"Received packet from "<< client.toStringWithPort() << endl;
|
|
DNSMessageReader dmr(packet);
|
|
if(dmr.dh.qr) {
|
|
cout << "Packet from " << client.toStringWithPort()<< " was not a query"<<endl;
|
|
continue;
|
|
}
|
|
std::thread t(processQuery, (int)sock, client, dmr);
|
|
t.detach();
|
|
}
|
|
catch(exception& e) {
|
|
cout << "Processing packet from " << client.toStringWithPort() <<": "<<e.what() << endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
// single shot operation
|
|
g_dot.open("plot.dot");
|
|
g_dot << "digraph { "<<endl;
|
|
DNSName dn = makeDNSName(argv[1]);
|
|
DNSType dt = makeDNSType(argv[2]);
|
|
|
|
auto res = resolveAt(dn, dt);
|
|
cout<<"Result of query for "<< dn <<"|"<<toString(dt)<<endl;
|
|
for(const auto& r : res.intermediate) {
|
|
cout<<r.name <<" "<<r.ttl<<" "<<r.rr->getType()<<" " << r.rr->toString()<<endl;
|
|
}
|
|
|
|
for(const auto& r : res.res) {
|
|
cout<<r.name <<" "<<r.ttl<<" "<<r.rr->getType()<<" "<<r.rr->toString()<<endl;
|
|
}
|
|
cout<<"Used "<<g_numqueries << " queries"<<endl;
|
|
|
|
g_dot << "}"<<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;
|
|
}
|