std::unique_ptr<NetmaskGroup> SyncRes::s_dontQuery{nullptr};
NetmaskGroup SyncRes::s_ednssubnets;
SuffixMatchNode SyncRes::s_ednsdomains;
+EDNSSubnetOpts SyncRes::s_ecsScopeZero;
string SyncRes::s_serverID;
SyncRes::LogMode SyncRes::s_lm;
return -1;
}
+void SyncRes::setIncomingECS(boost::optional<const EDNSSubnetOpts&> incomingECS)
+{
+ d_incomingECS = incomingECS;
+ if (incomingECS) {
+ if (d_incomingECS->source.getBits() == 0) {
+ /* RFC7871 says we MUST NOT send any ECS if the source scope is 0.
+ But using an empty ECS in that case would mean inserting
+ a non ECS-specific entry into the cache, preventing any further
+ ECS-specific query to be sent.
+ So instead we use the trick described in section 7.1.2:
+ "The subsequent Recursive Resolver query to the Authoritative Nameserver
+ will then either not include an ECS option or MAY optionally include
+ its own address information, which is what the Authoritative
+ Nameserver will almost certainly use to generate any Tailored
+ Response in lieu of an option. This allows the answer to be handled
+ by the same caching mechanism as other queries, with an explicit
+ indicator of the applicable scope. Subsequent Stub Resolver queries
+ for /0 can then be answered from this cached response.
+ */
+ d_incomingECS = s_ecsScopeZero;
+ d_incomingECSNetwork = s_ecsScopeZero.source.getMaskedNetwork();
+ }
+ else {
+ uint8_t bits = std::min(incomingECS->source.getBits(), (incomingECS->source.isIpv4() ? s_ecsipv4limit : s_ecsipv6limit));
+ d_incomingECS->source = Netmask(incomingECS->source.getNetwork(), bits);
+ d_incomingECSNetwork = d_incomingECS->source.getMaskedNetwork();
+ }
+ }
+ else {
+ d_incomingECSNetwork = ComboAddress();
+ }
+}
+
boost::optional<Netmask> SyncRes::getEDNSSubnetMask(const ComboAddress& local, const DNSName&dn, const ComboAddress& rem)
{
boost::optional<Netmask> result;
ComboAddress trunc;
uint8_t bits;
if(d_incomingECSFound) {
- if (d_incomingECS->source.getBits() == 0) {
- /* RFC7871 says we MUST NOT send any ECS if the source scope is 0 */
- return result;
- }
trunc = d_incomingECSNetwork;
bits = d_incomingECS->source.getBits();
}
else if(!local.isIPv4() || local.sin4.sin_addr.s_addr) { // detect unset 'requestor'
trunc = local;
bits = local.isIPv4() ? 32 : 128;
+ bits = std::min(bits, (trunc.isIPv4() ? s_ecsipv4limit : s_ecsipv6limit));
}
else {
/* nothing usable */
}
if(s_ednsdomains.check(dn) || s_ednssubnets.match(rem)) {
- bits = std::min(bits, (trunc.isIPv4() ? s_ecsipv4limit : s_ecsipv6limit));
trunc.truncate(bits);
return boost::optional<Netmask>(Netmask(trunc, bits));
}
emptyECSText = 'No ECS received'
nameECS = 'ecs-echo.example.'
ttlECS = 60
+ecsReactorRunning = False
class ECSTest(RecursorTest):
_config_template_default = """
@classmethod
def startResponders(cls):
+ global ecsReactorRunning
print("Launching responders..")
address = cls._PREFIX + '.21'
port = 53
- if not reactor.running:
+ if not ecsReactorRunning:
reactor.listenUDP(port, UDPECSResponder(), interface=address)
+ ecsReactorRunning = True
+ if not reactor.running:
cls._UDPResponder = threading.Thread(name='UDP ECS Responder', target=reactor.run, args=(False,))
cls._UDPResponder.setDaemon(True)
cls._UDPResponder.start()
- @classmethod
- def tearDownResponders(cls):
- reactor.stop()
-
@classmethod
def setUpClass(cls):
cls.setUpSockets()
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', emptyECSText)
+
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected)
+
class testIncomingNoECS(ECSTest):
_confdir = 'IncomingNoECS'
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', emptyECSText)
+
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected)
+
class testECSByName(ECSTest):
_confdir = 'ECSByName'
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '127.0.0.0/24')
+
+ # the request for no ECS is ignored because use-incoming-edns-subnet is not set
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected)
+
class testECSByNameLarger(ECSTest):
_confdir = 'ECSByNameLarger'
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '127.0.0.1/32')
+
+ # the request for no ECS is ignored because use-incoming-edns-subnet is not set
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected)
+
class testECSByNameSmaller(ECSTest):
_confdir = 'ECSByNameLarger'
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '127.0.0.0/16')
+
+ # the request for no ECS is ignored because use-incoming-edns-subnet is not set
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected)
+
class testIncomingECSByName(ECSTest):
_confdir = 'ECSIncomingByName'
_config_template = """edns-subnet-whitelist=ecs-echo.example.
use-incoming-edns-subnet=yes
forward-zones=ecs-echo.example=%s.21
+ecs-scope-zero-address=2001:db8::42
""" % (os.environ['PREFIX'])
def testSendECS(self):
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected, ttlECS)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', "2001:db8::42/128")
+
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected, ttlECS)
+
class testIncomingECSByNameLarger(ECSTest):
_confdir = 'ECSIncomingByNameLarger'
use-incoming-edns-subnet=yes
ecs-ipv4-bits=32
forward-zones=ecs-echo.example=%s.21
+ecs-scope-zero-address=192.168.0.1
""" % (os.environ['PREFIX'])
def testSendECS(self):
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected, ttlECS)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '192.168.0.1/32')
+
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected, ttlECS)
+
class testIncomingECSByNameSmaller(ECSTest):
_confdir = 'ECSIncomingByNameSmaller'
use-incoming-edns-subnet=yes
ecs-ipv4-bits=16
forward-zones=ecs-echo.example=%s.21
+ecs-scope-zero-address=192.168.0.1
""" % (os.environ['PREFIX'])
def testSendECS(self):
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '192.168.0.1/32')
+
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected, ttlECS)
+
class testIncomingECSByNameV6(ECSTest):
_confdir = 'ECSIncomingByNameV6'
use-incoming-edns-subnet=yes
ecs-ipv6-bits=128
forward-zones=ecs-echo.example=%s.21
+query-local-address6=::1
""" % (os.environ['PREFIX'])
def testSendECS(self):
res = self.sendUDPQuery(query)
self.sendECSQuery(query, expected, ttlECS)
+ def testRequireNoECS(self):
+ # we should get ::1/128 because neither ecs-scope-zero-addr nor query-local-address are set,
+ # but query-local-address6 is set to ::1
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', "::1/128")
+
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected, ttlECS)
+
class testECSNameMismatch(ECSTest):
_confdir = 'ECSNameMismatch'
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', emptyECSText)
+
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected)
+
class testECSByIP(ECSTest):
_confdir = 'ECSByIP'
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '127.0.0.0/24')
+
+ # the request for no ECS is ignored because use-incoming-edns-subnet is not set
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected)
+
class testIncomingECSByIP(ECSTest):
_confdir = 'ECSIncomingByIP'
_config_template = """edns-subnet-whitelist=%s.21
use-incoming-edns-subnet=yes
forward-zones=ecs-echo.example=%s.21
+ecs-scope-zero-address=::1
""" % (os.environ['PREFIX'], os.environ['PREFIX'])
def testSendECS(self):
query = dns.message.make_query(nameECS, 'TXT')
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ # we will get ::1 because ecs-scope-zero-addr is set to ::1
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', '::1/128')
+
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected, ttlECS)
+
class testECSIPMismatch(ECSTest):
_confdir = 'ECSIPMismatch'
res = self.sendUDPQuery(query)
self.sendECSQuery(query, expected)
+ def testRequireNoECS(self):
+ expected = dns.rrset.from_text(nameECS, ttlECS, dns.rdataclass.IN, 'TXT', emptyECSText)
+
+ ecso = clientsubnetoption.ClientSubnetOption('0.0.0.0', 0)
+ query = dns.message.make_query(nameECS, 'TXT', 'IN', use_edns=True, options=[ecso], payload=512)
+ self.sendECSQuery(query, expected)
+
class UDPECSResponder(DatagramProtocol):
@staticmethod
def ipToStr(option):