From 9b72e23e13d9bb46d3302ccf67497cd4f5530bc4 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 18 Feb 2019 15:25:13 +0100 Subject: [PATCH] Fix #192 - Resolve as(-sets) contained in route-sets (#204) --- docs/users/queries.rst | 2 + irrd/server/whois/query_parser.py | 25 ++++++--- irrd/server/whois/tests/test_query_parser.py | 53 +++++++++++++++++++- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/docs/users/queries.rst b/docs/users/queries.rst index 337025d33..7fd196bd3 100644 --- a/docs/users/queries.rst +++ b/docs/users/queries.rst @@ -51,6 +51,8 @@ IRRd style queries (and possibly names of other sets, if the search was not recursive), separated by spaces. For example: ``!iRS-EXAMPLE,1`` returns all members of `RS-EXAMPLE`, recursively. + If a `route-set` has `as-sets` or AS number as members, the response includes + the prefixes originating from that AS, or the ASes in that set. If the ``compatibility.ipv4_only_route_set_members`` setting is enabled, IPv6 prefixes will not be returned. * ``!j`` returns the serial range for each source, along with the most diff --git a/irrd/server/whois/query_parser.py b/irrd/server/whois/query_parser.py index 9c13822c7..82aeaec51 100644 --- a/irrd/server/whois/query_parser.py +++ b/irrd/server/whois/query_parser.py @@ -272,6 +272,7 @@ def _recursive_set_resolve(self, members: Set[str], sets_seen=None) -> Set[str]: sets_seen.update(members) set_members = set() + resolved_as_members = set() sub_members, leaf_members = self._find_set_members(members) for sub_member in sub_members: @@ -282,15 +283,21 @@ def _recursive_set_resolve(self, members: Set[str], sets_seen=None) -> Set[str]: continue except ValueError: pass - if self._current_set_root_object_class is None or self._current_set_root_object_class == 'as-set': - try: - parse_as_number(sub_member) + # AS numbers are permitted in route-sets and as-sets, per RFC 2622 5.3. + # When an AS number is encountered as part of route-set resolving, + # the prefixes originating from that AS should be added to the response. + try: + as_number_formatted, _ = parse_as_number(sub_member) + if self._current_set_root_object_class == 'route-set': + set_members.update(self.preloader.routes_for_origins([as_number_formatted])) + resolved_as_members.add(sub_member) + else: set_members.add(sub_member) - continue - except ValueError: - pass + continue + except ValueError: + pass - further_resolving_required = sub_members - set_members - sets_seen + further_resolving_required = sub_members - set_members - sets_seen - resolved_as_members new_members = self._recursive_set_resolve(further_resolving_required, sets_seen) set_members.update(new_members) @@ -316,7 +323,9 @@ def _find_set_members(self, set_names: Set[str]) -> Tuple[Set[str], Set[str]]: query = self._prepare_query(column_names=columns) object_classes = ['as-set', 'route-set'] - if self._current_set_root_object_class: + # Per RFC 2622 5.3, route-sets can refer to as-sets, + # but as-sets can only refer to other as-sets. + if self._current_set_root_object_class == 'as-set': object_classes = [self._current_set_root_object_class] query = query.object_classes(object_classes).rpsl_pks(set_names) diff --git a/irrd/server/whois/tests/test_query_parser.py b/irrd/server/whois/tests/test_query_parser.py index 4be797ac4..f1bb7d536 100644 --- a/irrd/server/whois/tests/test_query_parser.py +++ b/irrd/server/whois/tests/test_query_parser.py @@ -595,7 +595,7 @@ def test_handle_irrd_routes_for_as_set(self, prepare_parser, monkeypatch): assert not mock_dq.mock_calls - def test_as_route_set_members(self, prepare_parser): + def test_as_set_members(self, prepare_parser): mock_dq, mock_dh, mock_preloader, parser = prepare_parser mock_query_result1 = [ @@ -656,7 +656,6 @@ def test_as_route_set_members(self, prepare_parser): assert response.response_type == WhoisQueryResponseType.SUCCESS assert response.mode == WhoisQueryResponseMode.IRRD assert response.result == 'AS65544 AS65545 AS65547' - print(flatten_mock_calls(mock_dq)) assert flatten_mock_calls(mock_dq) == [ ['object_classes', (['as-set', 'route-set'],), {}], ['rpsl_pks', ({'AS-FIRSTLEVEL'},), {}], @@ -679,6 +678,56 @@ def test_as_route_set_members(self, prepare_parser): ['rpsl_pks', ({'AS-NOTEXIST'},), {}] ] + def test_route_set_members(self, prepare_parser): + mock_dq, mock_dh, mock_preloader, parser = prepare_parser + + mock_query_result1 = [ + { + 'pk': uuid.uuid4(), + 'rpsl_pk': 'RS-FIRSTLEVEL', + 'parsed_data': {'as-set': 'RS-FIRSTLEVEL', + 'members': ['RS-SECONDLEVEL', 'RS-2nd-UNKNOWN']}, + 'object_text': 'text', + 'object_class': 'route-set', + 'source': 'TEST1', + }, + ] + mock_query_result2 = [ + { + 'pk': uuid.uuid4(), + 'rpsl_pk': 'RS-SECONDLEVEL', + 'parsed_data': {'as-set': 'RS-SECONDLEVEL', 'members': ['AS-REFERRED', '192.0.2.0/25']}, + 'object_text': 'text', + 'object_class': 'route-set', + 'source': 'TEST1', + }, + ] + mock_query_result3 = [ + { + 'pk': uuid.uuid4(), + 'rpsl_pk': 'AS-REFERRED', + 'parsed_data': {'as-set': 'AS-REFERRED', + 'members': ['AS65545']}, + 'object_text': 'text', + 'object_class': 'as-set', + 'source': 'TEST2', + }, + ] + mock_query_iterator = iter([mock_query_result1, mock_query_result2, mock_query_result3, []]) + mock_dh.execute_query = lambda query: iter(next(mock_query_iterator)) + mock_preloader.routes_for_origins = Mock(return_value=['192.0.2.128/25']) + + response = parser.handle_query('!iRS-FIRSTLEVEL,1') + assert response.response_type == WhoisQueryResponseType.SUCCESS + assert response.mode == WhoisQueryResponseMode.IRRD + assert response.result == '192.0.2.0/25 192.0.2.128/25' + assert flatten_mock_calls(mock_dq) == [ + ['object_classes', (['as-set', 'route-set'],), {}], ['rpsl_pks', ({'RS-FIRSTLEVEL'},), {}], + ['object_classes', (['as-set', 'route-set'],), {}], + ['rpsl_pks', ({'RS-SECONDLEVEL', 'RS-2nd-UNKNOWN'},), {}], + ['object_classes', (['as-set', 'route-set'],), {}], ['rpsl_pks', ({'AS-REFERRED'},), {}], + ] + def test_as_route_set_mbrs_by_ref(self, prepare_parser): mock_dq, mock_dh, mock_preloader, parser = prepare_parser