Skip to content

Commit

Permalink
Fix #2558: Add a client DNS resolver
Browse files Browse the repository at this point in the history
This will allow Agora to resolves validators public key to IP address and protocols,
bridging the gap between the HTTP and DNS capabilities of the name registry.

Co-Authored-By: Muhammed Kadir Yücel <[email protected]>
  • Loading branch information
Geod24 and Muhammed Kadir Yücel committed Jan 17, 2022
1 parent 37ff7ef commit 7fdff99
Show file tree
Hide file tree
Showing 2 changed files with 265 additions and 0 deletions.
8 changes: 8 additions & 0 deletions dub.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@
"source/agora/utils/gc/*"
]
},
{
"name": "dns-resolver",
"targetName": "dns-resolver",
"versions": [ "AgoraStandaloneDNSResolver" ],
"excludedSourceFiles": [
"source/agora/utils/gc/*"
]
},
{
"name": "vanity",
"targetName": "agora-vanity-generator",
Expand Down
257 changes: 257 additions & 0 deletions source/agora/network/DNSResolver.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
/*******************************************************************************
A client-side DNS resolver
This client is a caching, recursive DNS resolver, written in Dlang.
Copyright:
Copyright (c) 2019-2022 BOSAGORA Foundation
All rights reserved.
License:
MIT License. See LICENSE for details.
*******************************************************************************/

module agora.network.DNSResolver;

import agora.common.DNS;
import agora.common.Ensure;
import agora.common.Types;
import agora.serialization.Serializer;
import agora.utils.Log;

import vibe.core.net;

import std.array;
import std.algorithm;
import std.format;
import std.random;
static import std.socket;
import std.stdio;
import core.time;

shared static this ()
{
// Cannot be a manifest constant as `Address` will read the common schema
// when instantiated.
DefaultDNS = [
// Cloudflare
Address("dns://1.1.1.1"), Address("dns://1.0.0.1"),
// Google public DNS
Address("dns://8.8.8.8"), Address("dns://8.8.4.4"),
// CISCO OpenDNS
Address("dns://208.67.222.222"), Address("dns://208.67.220.220"),
// Quad9
Address("dns://9.9.9.9"), Address("dns://149.112.112.112"),
];
}

///
public final class DNSResolver
{
/// Logger instance
private Logger log;

/// The list of base DNS resolver (e.g. read from resolv.conf)
private PeerInfo[] resolvers;

/***************************************************************************
Instantiate a new object of this type
Params:
peers = The 'seed' peers, which are the first point of contact
of the resolver.
***************************************************************************/

public this (const Address[] peers = DefaultDNS)
{
this.log = Log.lookup(__MODULE__);
this.log.enableConsole();

this.resolvers.length = peers.length;
// "Connect" to each of the resolvers
// UDP is not connection-oriented, but this binds the socket to the
// underlying address.
foreach (idx, ref res; this.resolvers)
{
assert(peers[idx].host.guessAddressType != TYPE.CNAME,
"Address '" ~ peers[idx].host ~
"' is not an IP address and can't be used for DNS resolution");

res.address = resolveHost(peers[idx].host);
res.address.port = peers[idx].port;
}
}

/***************************************************************************
Returns the `ResourceRecord` matching `type` associated with `name`
This low-level function will query the registered resolvers for the
records matching `name`. `type` is an optional argument indicating what
kind of RR is expected. While it defaults to `ALL`, it is recommended
to provide a different value, as many servers might refuse to answer
for queries on which they are not authoritative.
Params:
name = The name to resolve
type = Type of reecord to query
***************************************************************************/

public ResourceRecord[] query (const(char)[] name, QTYPE type = QTYPE.ALL) @trusted
{
Message msg;
msg.questions ~= Question(Domain.fromString(name), type, QCLASS.IN);

msg.header.ID = uniform!short;
// Don't do the whole recursion dance, assume our resolvers are recursive.
msg.header.RD = true;
// Default:
// QR set to 0 (query), OPCODE set to 0 (Query), AA set to false,
// TC set to false, RA set to false, Z set to 0, RCODE set to 0 (NoError)
msg.header.QDCOUNT = 1;
// Rest is set to 0
msg.additionals ~= OPTRR.init.record;
msg.header.ARCOUNT = 1;

ubyte[16384] buffer;
foreach (ref peer; this.resolvers)
{
auto conn = listenUDP(0, "0.0.0.0",
UDPListenOptions.reuseAddress | UDPListenOptions.reusePort);
conn.connect(peer.address);

try
{
conn.send(msg.serializeFull);
auto response = conn.recv(5.seconds, buffer);
auto answer = response.deserializeFull!Message();
log.trace("Got response from '{}' for '{}'({}): {}",
peer.address, name, type, answer);
if (answer.header.RCODE == Header.RCode.NoError)
return answer.answers;
}
catch (Exception exc)
{
log.warn("Network error while resolving '{}' ({}) using '{}': {}",
name, type, peer.address, exc);
continue;
}
}
return null;
}

/// Resolves an address
public Address resolve (Address address)
{
// Fast path: If the address is already resolved,
// do not do network communication
try
{
scope res = std.socket.parseAddress(address.host);
return address;
}
catch (std.socket.SocketException) {}

// We need to execute a query, and parse the resulting DNS record
auto results = this.query(address.host);
ensure(results.length > 0, "Could not resolve host name '{}' (address: '{}')",
address.host, address);

// Our DNS record will primarily be of 3 types: A, CNAME, AAAA
// CNAME just means we have to recurse, while the other ones are the end result.
size_t ipv6_index = results.length;
foreach (index, const ref res; results)
{
// If it's a CNAME we need to recurse, although the server should
// have done that for us.
if (res.type == TYPE.CNAME)
{
ensure(res.name.value.length > 0,
"Found a CNAME to the root domain (empty) while resolving '{}'", address);
log.trace("Address '{}': Found CNAME '{}'", address, res.name);
address.host = cast(string) res.name.value;
return this.resolve(address);
}

// Prefer IPv4 as it's faster and less surprising to users
if (res.type == TYPE.A)
{
// Ignore broken record
if (res.rdata.a.length < 1) continue;
// Just pick the first record.
// TODO: What is expected when there's more than one record ?
// Should we use it as a fall back, round robin, something else?
const straddr = format("%d.%d.%d.%d",
res.rdata.a[0] >> 24 & 0xFF, res.rdata.a[0] >> 16 & 0xFF,
res.rdata.a[0] >> 8 & 0xFF, res.rdata.a[0] >> 0 & 0xFF);
log.trace("Address '{}' resolved to A record '{}'", address, straddr);
address.host = straddr;
return address;
}
// Save the IPv6 address if encountered
else if (res.type == TYPE.AAAA && ipv6_index == results.length)
ipv6_index = index;
}
ensure(false, "Could not resolve host name '{}': No address in result",
address.host);
assert(0);
}

public Domain[] getNameServers (const(char)[] name) @safe
{
if (auto answers = this.query(name, QTYPE.NS))
return answers.map!(answ => answ.rdata.name).array;
return null;
}
}

/*******************************************************************************
An `Address` where the `hostname` part has been resolved
*******************************************************************************/

/// Hold information about DNS resolvers
private struct PeerInfo
{
/// Address of the peer (IP)
public NetworkAddress address;

/// Number of messages sent to this peer
public size_t queries;
}

/// The 'default' DNS resolver if none is provided
public immutable Address[] DefaultDNS;

version (AgoraStandaloneDNSResolver)
{
import vibe.core.core;

import std.stdio;

private int main (string[] args)
{
assert(args.length > 1, "Need at least one argument");

runTask(() @trusted nothrow {
scope (failure) assert(0);
DNSResolver resolver = new DNSResolver();

foreach (host; args[1 .. $])
{
try
writeln(resolver.query(host, QTYPE.NS));
catch (Exception exc)
writeln("Exception caught while try to resolve addresses: ", exc);
}
});

return runEventLoop();
}
}

0 comments on commit 7fdff99

Please sign in to comment.