hello-dns/tdns/tdns.cc
2018-04-18 09:24:57 +02:00

517 lines
18 KiB
C++

/* Goal: a fully standards compliant basic authoritative server. In <1000 lines.
Non-goals: notifications, slaving zones, name compression, edns,
performance
*/
/*!
@file
@brief This is the main file of the tdns authoritative server
*/
#include <cstdint>
#include <vector>
#include <map>
#include <stdexcept>
#include "sclasses.hh"
#include <thread>
#include <signal.h>
#include "record-types.hh"
#include "dns-storage.hh"
using namespace std;
/*! \mainpage Welcome to tdns
\section Introduction
tdns is a simple authoritative nameserver that is fully faithful to the
DNS storage model as outlined in RFC 1034.
An introduction can be found on https://powerdns.org/hello-dns/tdns/intro.md.html
## Object relations
DNSMessageReader is used to read DNS messages. A UDP DNS Packet is also a DNS message.
DNSMessageWriter is used to create DNS messages.
A DNS name is stored in a DNSName object and internally consists of DNSLabel's.
DNS messages also mostly have a query name, which is a DNSName and a query type which is a DNSType. They also have a DNSClass but we don't do much with that.
To insert resource records into DNSMessageWriter, use DNSMessageWriter::putRR, to
read them from DNSMessageReader, use DNSMessageReader::getRR.
Resource records are stored as RRGen instances. The RRGen object is able to serialize
itself to/from a DNSMessageWriter or DNSMessageReader. In addition, this object has a
RRGen::toString method for 'human consumption' output.
## Next steps in understanding `tdns`
The basics of tdns can best be understood by first reading tdig.cc
Details of how DNSMessageReader/DNSMessageWriter and the RRGen objects interact
are in record-types.hh
*/
void addAdditional(const DNSNode* bestzone, const DNSName& zone, const vector<DNSName>& toresolve, DNSMessageWriter& response);
/** \brief This is the main DNS logic function
This is the main 'DNS logic' function. It receives a set of zones,
a readable DNS query from a certain IP address, and a writable
DNS response.
This function is called by both UDP and TCP listeners. It therefore
does not do any IXFR/AXFR. It does however perform several sanity checks.
Returns false if no response should be sent.
This function implements "the algorithm" from RFC 1034 and is key to
unstanding DNS */
bool processQuestion(const DNSNode& zones, DNSMessageReader& dm, const ComboAddress& remote, DNSMessageWriter& response)
{
if(dm.dh.qr) {
cerr<<"Dropping non-query from "<<remote.toStringWithPort()<<endl;
return false; // should not send ANY kind of response, loop potential
}
DNSName qname;
DNSType qtype;
dm.getQuestion(qname, qtype);
DNSName origname=qname; // we need this for error reporting, we munch the original name
cout<<"Received a query from "<<remote.toStringWithPort()<<" for "<<qname<<" and type "<<qtype<<endl;
try {
response.dh.id = dm.dh.id; response.dh.rd = dm.dh.rd;
response.dh.ad = response.dh.ra = response.dh.aa = 0;
response.dh.qr = 1; response.dh.opcode = dm.dh.opcode;
uint16_t newsize; bool doBit;
if(dm.getEDNS(&newsize, &doBit)) {
cout<<"\tHave EDNS, buffer size = "<<newsize<<", DO bit = "<<doBit<<endl;
if(dm.d_ednsVersion != 0) {
cout<<"\tBad EDNS version: "<<(int)dm.d_ednsVersion<<endl;
response.setEDNS(newsize, doBit, RCode::Badvers);
return true;
}
response.setEDNS(newsize, doBit);
}
if(qtype == DNSType::AXFR || qtype == DNSType::IXFR) {
cout<<"\tQuery was for AXFR or IXFR over UDP, can't do that"<<endl;
response.dh.rcode = (int)RCode::Servfail;
return true;
}
if(dm.dh.opcode != 0) {
cout<<"\tQuery had non-zero opcode "<<dm.dh.opcode<<", sending NOTIMP"<<endl;
response.dh.rcode = (int)RCode::Notimp;
return true;
}
// find the best zone for this query
DNSName zonename;
auto fnd = zones.find(qname, zonename);
if(!fnd || !fnd->zone) { // check if we found an actual zone
cout<<"\tNo zone matched"<<endl;
response.dh.rcode = (uint8_t)RCode::Refused;
return true;
}
// qname is now relative to the zonename
cout<<"\tFound best zone: "<<zonename<<", qname now "<<qname<<endl;
response.dh.aa = 1;
auto bestzone = fnd->zone.get(); // this loads a pointer to the zone contents
DNSName searchname(qname), lastnode, zonecutname;
const DNSNode* passedZonecut=0;
int CNAMELoopCount = 0;
loopCNAME:;
/* search for the best node, where we want to benefit from wildcard synthesis
note that this is the same 'find' we used to find the best zone, but we did not
want any wildcard procssing there */
auto node = bestzone->find(searchname, lastnode, true, &passedZonecut, &zonecutname);
if(passedZonecut) {
response.dh.aa = false;
cout<<"\tThis is a delegation, zonecutname: '"<<zonecutname<<"'"<<endl;
auto iter = passedZonecut->rrsets.find(DNSType::NS); // is there an NS record here? should be!
if(iter != passedZonecut->rrsets.end()) {
const auto& rrset = iter->second;
vector<DNSName> toresolve;
for(const auto& rr : rrset.contents) {
/* add the NS records to the authority section. Note that for this we have to make
the name absolute again: zonecutname + zonename */
response.putRR(DNSSection::Authority, zonecutname+zonename, DNSType::NS, rrset.ttl, rr);
// and add for additional processing
toresolve.push_back(dynamic_cast<NSGen*>(rr.get())->d_name);
}
addAdditional(bestzone, zonename, toresolve, response);
}
}
else if(!searchname.empty()) { // we had parts of the qname that did not match
cout<<"\tThis is an NXDOMAIN situation"<<endl;
if(!CNAMELoopCount) // RFC 1034, 4.3.2, step 3.c
response.dh.rcode = (int)RCode::Nxdomain;
const auto& rrset = bestzone->rrsets[DNSType::SOA]; // fetch the SOA record to indicate NXDOMAIN ttl
auto ttl = min(rrset.ttl, dynamic_cast<SOAGen*>(rrset.contents[0].get())->d_minimum); // 2308 3
response.putRR(DNSSection::Authority, zonename, DNSType::SOA, ttl, rrset.contents[0]);
}
else {
cout<<"\tFound node in zone '"<<zonename<<"' for lhs '"<<qname<<"', searchname now '"<<searchname<<"', lastnode '"<<lastnode<<"', passedZonecut="<<passedZonecut<<endl;
decltype(node->rrsets)::const_iterator iter;
vector<DNSName> additional;
// first we always check for a CNAME, which should be the only RRType at a node if present
if(iter = node->rrsets.find(DNSType::CNAME), iter != node->rrsets.end()) {
cout<<"\tNo CNAME"<<endl;
const auto& rrset = iter->second;
response.putRR(DNSSection::Answer, lastnode+zonename, DNSType::CNAME, rrset.ttl, rrset.contents[0]);
DNSName target=dynamic_cast<CNAMEGen*>(rrset.contents[0].get())->d_name;
// we'll only follow in-zone CNAMEs, which is not quite per-RFC, but a good idea
if(target.makeRelative(zonename)) {
cout<<"\tFound CNAME, chasing to "<<target<<endl;
searchname = target;
if(qtype != DNSType::CNAME && CNAMELoopCount++ < 10) { // do not loop if they *wanted* the CNAME
lastnode.clear();
zonecutname.clear();
goto loopCNAME;
}
}
} // we have a node, and it might even have RRSets we want
else if(iter = node->rrsets.find(qtype), iter != node->rrsets.end() || (!node->rrsets.empty() && qtype==DNSType::ANY)) {
auto range = make_pair(iter, iter);
if(qtype == DNSType::ANY) // if ANY, loop over all types
range = make_pair(node->rrsets.begin(), node->rrsets.end());
else
++range.second; // only the qtype they wanted
for(auto i2 = range.first; i2 != range.second; ++i2) {
const auto& rrset = i2->second;
for(const auto& rr : rrset.contents) {
cout<<"\tAdding a " << i2->first <<" RR\n";
response.putRR(DNSSection::Answer, lastnode+zonename, i2->first, rrset.ttl, rr);
if(i2->first == DNSType::MX)
additional.push_back(dynamic_cast<MXGen*>(rr.get())->d_name);
}
}
}
else {
cout<<"\tNode exists, qtype doesn't, NOERROR situation, inserting SOA"<<endl;
const auto& rrset = bestzone->rrsets[DNSType::SOA];
auto ttl = min(rrset.ttl, dynamic_cast<SOAGen*>(rrset.contents[0].get())->d_minimum); // 2308 3
response.putRR(DNSSection::Authority, zonename, DNSType::SOA, ttl, rrset.contents[0]);
}
addAdditional(bestzone, zonename, additional, response);
}
return true;
}
catch(std::out_of_range& e) { // exceeded packet size
cout<<"\tQuery for '"<<origname<<"'|"<<qtype<<" got truncated"<<endl;
response.clearRRs();
response.dh.aa = 0; response.dh.tc = 1;
return true;
}
catch(std::exception& e) {
cout<<"\tError processing query: "<<e.what()<<endl;
return false;
}
}
/* this is where all UDP questions come in. Note that 'zones' is const,
which protects us from accidentally changing anything */
void udpThread(ComboAddress local, Socket* sock, const DNSNode* zones)
{
DNSName qname;
DNSType qtype;
for(;;) {
ComboAddress remote(local);
try {
string message = SRecvfrom(*sock, 512, remote);
DNSMessageReader dm(message);
dm.getQuestion(qname, qtype);
DNSMessageWriter response(qname, qtype);
if(processQuestion(*zones, dm, remote, response)) {
if(response.dh.rcode)
cout<<"\tSending response with rcode "<<(RCode)response.dh.rcode <<endl;
SSendto(*sock, response.serialize(), remote);
}
}
catch(std::exception& e) {
cerr<<"Query from "<<remote.toStringWithPort()<<" caused an error: "<<e.what()<<endl;
}
}
}
/** \brief Looks up additional records
This function is called to do additional processing on records we encountered
earlier that would benefit. This includes MX and NS records.
Note that this function will only ook within 'bestzone', the best zone we had
for the original query. This means we will not look at potentially helpful
records in other zones. RFCs tell us that resolvers should not use/trust such
out of zone data anyhow, but no RFC tells us we should not add that data.
But we don't */
void addAdditional(const DNSNode* bestzone, const DNSName& zone, const vector<DNSName>& toresolve, DNSMessageWriter& response)
try
{
for(auto addname : toresolve ) {
if(!addname.makeRelative(zone)) {
// cout<<addname<<" is not within our zone, not doing glue"<<endl;
continue;
}
DNSName wuh;
auto addnode = bestzone->find(addname, wuh);
if(!addnode || !addname.empty()) {
continue;
}
for(auto& type : {DNSType::A, DNSType::AAAA}) {
auto iter2 = addnode->rrsets.find(type);
if(iter2 != addnode->rrsets.end()) {
const auto& rrset = iter2->second;
for(const auto& rr : rrset.contents) {
response.putRR(DNSSection::Additional, wuh+zone, type, rrset.ttl, rr);
}
}
}
}
}
catch(std::out_of_range& e) { // exceeded packet size
cout<<"\tAdditional records would have overflowed the packet, stopped adding them, not truncating yet\n";
}
/*! \brief Writes a DNSMessageWriter to a TCP/IP socket, with length envelope
helper function which encapsulates a DNS message within an 'envelope'
Note that it is highly recommended to send the envelope (with length)
as a single call. This saves packets and works around implementation bugs
over at resolvers */
static void writeTCPMessage(int sock, DNSMessageWriter& response)
{
string ser="00"+response.serialize();
uint16_t len = htons(ser.length()-2);
ser[0] = *((char*)&len);
ser[1] = *(((char*)&len) + 1);
SWriten(sock, ser);
}
/*! helper to read a 16 bit length in network order. Returns 0 on EOF */
uint16_t tcpGetLen(int sock)
{
string message = SRead(sock, 2);
if(message.empty())
return 0;
if(message.size() != 2) {
throw std::runtime_error("Incomplete TCP/IP message");
}
uint16_t len;
memcpy(&len, &message.at(1)-1, 2);
return htons(len);
}
/*! spawned for each new TCP/IP client. In actual production this is not a good idea. */
void tcpClientThread(ComboAddress remote, int s, const DNSNode* zones)
try
{
signal(SIGPIPE, SIG_IGN);
Socket sock(s); // this will close for us
cout<<"TCP Connection from "<<remote.toStringWithPort()<<endl;
// multiple questions can come in over a single TCP/IP connection
for(;;) {
uint16_t len=tcpGetLen(sock);
if(!len) // likely EOF
return;
if(len > 512) {
cerr<<"Remote "<<remote.toStringWithPort()<<" sent question that was too big"<<endl;
return;
}
if(len < sizeof(dnsheader)) {
cerr<<"Dropping query from "<<remote.toStringWithPort()<<", too short"<<endl;
return;
}
std::string message = SRead(sock, len);
DNSMessageReader dm(message);
DNSName name;
DNSType type;
dm.getQuestion(name, type);
DNSMessageWriter response(name, type, 16384);
if(type == DNSType::AXFR || type == DNSType::IXFR) {
if(dm.dh.opcode || dm.dh.qr) {
cerr<<"Dropping non-query AXFR from "<<remote.toStringWithPort()<<endl; // too weird
return;
}
cout<<"AXFR requested for "<<name<<endl;
response.dh.id = dm.dh.id;
response.dh.ad = response.dh.ra = response.dh.aa = 0;
response.dh.qr = 1;
DNSName zone;
// as in processQuestion, find the best zone
auto fnd = zones->find(name, zone);
if(!fnd || !fnd->zone || !name.empty() || !fnd->zone->rrsets.count(DNSType::SOA)) {
cout<<" This was not a zone, or zone had no SOA"<<endl;
response.dh.rcode = (int)RCode::Refused;
writeTCPMessage(sock, response);
continue;
}
auto node = fnd->zone.get();
// send SOA, which is how an AXFR must start
response.putRR(DNSSection::Answer, zone, DNSType::SOA, node->rrsets[DNSType::SOA].ttl, node->rrsets[DNSType::SOA].contents[0]);
writeTCPMessage(sock, response);
response.clearRRs();
// send all other records
node->visit([&response,&sock,&name,&type,&zone](const DNSName& nname, const DNSNode* n) {
for(const auto& p : n->rrsets) {
if(p.first == DNSType::SOA) // skip the SOA, as it indicates end of AXFR
continue;
for(const auto& rr : p.second.contents) {
retry:
try {
response.putRR(DNSSection::Answer, nname, p.first, p.second.ttl, rr);
}
catch(std::out_of_range& e) { // exceeded packet size
writeTCPMessage(sock, response);
response.clearRRs();
goto retry;
}
}
}
}, zone);
writeTCPMessage(sock, response);
response.clearRRs();
// send SOA again
response.putRR(DNSSection::Answer, zone, DNSType::SOA, node->rrsets[DNSType::SOA].ttl, node->rrsets[DNSType::SOA].contents[0]);
writeTCPMessage(sock, response);
return;
}
else {
if(processQuestion(*zones, dm, remote, response)) {
writeTCPMessage(sock, response);
}
else
return;
}
}
}
catch(std::exception &e) {
cerr<<"TCP client thread spawned for "<<remote.toStringWithPort()<<" exiting: "<<e.what()<<endl;
}
//! connects to an authoritative server, retrieves a zone, returns it as a smart pointer
std::unique_ptr<DNSNode> retrieveZone(const ComboAddress& remote, const DNSName& zone)
{
cout<<"Attempting to retrieve zone "<<zone<<" from "<<remote.toStringWithPort()<<endl;
Socket tcp(remote.sin4.sin_family, SOCK_STREAM);
SConnect(tcp, remote);
DNSMessageWriter dmw(zone, DNSType::AXFR);
writeTCPMessage(tcp, dmw);
auto ret = std::make_unique<DNSNode>();
int soaCount=0;
uint32_t rrcount=0;
for(;;) {
uint16_t len = tcpGetLen(tcp);
string message = SRead(tcp, len);
DNSMessageReader dmr(message);
if(dmr.dh.rcode != (int)RCode::Noerror) {
cout<<"Got error "<<dmr.dh.rcode<<" from auth "<<remote.toStringWithPort()<< " when attempting to retrieve "<<zone<<endl;
return std::unique_ptr<DNSNode>();
}
DNSName rrname;
DNSType rrtype;
DNSSection rrsection;
uint32_t ttl;
std::unique_ptr<RRGen> rr;
while(dmr.getRR(rrsection, rrname, rrtype, ttl, rr)) {
++rrcount;
if(!rrname.makeRelative(zone))
continue;
if(rrtype == DNSType::SOA && ++soaCount==2)
goto done;
ret->add(rrname)->addRRs(std::move(rr));
ret->add(rrname)->rrsets[rrtype].ttl = ttl;
}
}
done:
cout<<"Done with AXFR of "<<zone<<" from "<<remote.toStringWithPort()<<", retrieved "<<rrcount<<" records"<<endl;
return ret;
}
//! This is the main tdns function
int main(int argc, char** argv)
try
{
if(argc != 2) {
cerr<<"Syntax: tdns ipaddress:port"<<endl;
return(EXIT_FAILURE);
}
cout<<"Hello and welcome to tdns, the teaching authoritative nameserver"<<endl;
signal(SIGPIPE, SIG_IGN);
ComboAddress local(argv[1], 53);
Socket udplistener(local.sin4.sin_family, SOCK_DGRAM);
SBind(udplistener, local);
Socket tcplistener(local.sin4.sin_family, SOCK_STREAM);
SSetsockopt(tcplistener, SOL_SOCKET, SO_REUSEPORT, 1);
SBind(tcplistener, local);
SListen(tcplistener, 10);
DNSNode zones;
cout<<"Loading & retrieving zone data"<<endl;
loadZones(zones);
cout<<"Listening on TCP & UDP on "<<local.toStringWithPort()<<endl;
thread udpServer(udpThread, local, &udplistener, &zones);
cout<<"Server is live"<<endl;
for(;;) {
ComboAddress remote(local); // this sets the family correctly
int client = SAccept(tcplistener, remote);
thread t(tcpClientThread, remote, client, &zones);
t.detach();
}
}
catch(std::exception& e)
{
cerr<<"Fatal error: "<<e.what()<<endl;
return EXIT_FAILURE;
}