Current section

Files

Jump to
erldns src erldns_zone_cache.erl
Raw

src/erldns_zone_cache.erl

%% Copyright (c) 2012-2020, DNSimple Corporation
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% @doc A cache holding all of the zone data.
%%
%% Write operations occur through the cache process mailbox, whereas read
%% operations may occur either through the mailbox or directly through the
%% underlying data store, depending on performance requirements.
-module(erldns_zone_cache).
-behavior(gen_server).
-include_lib("dns_erlang/include/dns.hrl").
-include("erldns.hrl").
-export([start_link/0]).
% Read APIs
-export([
find_zone/1,
find_zone/2,
get_zone/1,
get_authority/1,
get_delegations/1,
get_zone_records/1,
get_records_by_name/1,
get_records_by_name_and_type/2,
in_zone/1,
record_name_in_zone/2,
zone_names_and_versions/0,
get_rrset_sync_counter/3
]).
% Deprecated APIs
-export([get_zone_with_records/1]).
% Write APIs
-export([
put_zone/1,
put_zone/2,
delete_zone/1,
update_zone_records_and_digest/3,
put_zone_rrset/4,
delete_zone_rrset/5
]).
% Gen server hooks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-define(SERVER, ?MODULE).
-record(state, {parsers, tref = none}).
%% @doc Start the zone cache process.
-spec start_link() -> any().
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
% ----------------------------------------------------------------------------------------------------
% Read API
%% @doc Find a zone for a given qname.
-spec find_zone(dns:dname()) -> #zone{} | {error, zone_not_found} | {error, not_authoritative}.
find_zone(Qname) ->
find_zone(erldns:normalize_name(Qname), get_authority(Qname)).
%% @doc Find a zone for a given qname.
-spec find_zone(dns:dname(), {error, any()} | {ok, dns:rr()} | [dns:rr()] | dns:rr()) ->
#zone{} | {error, zone_not_found} | {error, not_authoritative}.
find_zone(Qname, {error, _}) ->
find_zone(Qname, []);
find_zone(Qname, {ok, Authority}) ->
find_zone(Qname, Authority);
find_zone(_Qname, []) ->
{error, not_authoritative};
find_zone(Qname, Authorities) when is_list(Authorities) ->
find_zone(Qname, lists:last(Authorities));
find_zone(Qname, Authority) when is_record(Authority, dns_rr) ->
Name = erldns:normalize_name(Qname),
case dns:dname_to_labels(Name) of
[] ->
{error, zone_not_found};
[_ | Labels] ->
case get_zone(Name) of
{ok, Zone} ->
Zone;
{error, zone_not_found} ->
case Name =:= Authority#dns_rr.name of
true ->
{error, zone_not_found};
false ->
find_zone(dns:labels_to_dname(Labels), Authority)
end
end
end.
%% @doc Get a zone for the specific name. This function will not attempt to resolve
%% the dname in any way, it will simply look up the name in the underlying data store.
-spec get_zone(dns:dname()) -> {ok, #zone{}} | {error, zone_not_found}.
get_zone(Name) ->
NormalizedName = erldns:normalize_name(Name),
case erldns_storage:select(zones, NormalizedName) of
[{NormalizedName, Zone}] ->
{ok, Zone#zone{
name = NormalizedName,
records = [],
records_by_name = trimmed
}};
_ ->
{error, zone_not_found}
end.
%% @doc Get a zone for the specific name, including the records for the zone.
%% @deprecated Use {@link erldns_zone_cache:get_zone/1} to get the zone meta data and
%% {@link erldns_zone_cache:get_zone_records/1} to get the records for the zone.
-spec get_zone_with_records(dns:dname()) -> {ok, #zone{}} | {error, zone_not_found}.
get_zone_with_records(Name) ->
NormalizedName = erldns:normalize_name(Name),
case erldns_storage:select(zones, NormalizedName) of
[{NormalizedName, Zone}] ->
{ok, Zone};
_ ->
{error, zone_not_found}
end.
%% @doc Find the SOA record for the given DNS question.
-spec get_authority(dns:message() | dns:dname()) -> {error, no_question} | {error, authority_not_found} | {ok, dns:authority()}.
get_authority(Message) when is_record(Message, dns_message) ->
case Message#dns_message.questions of
[] ->
{error, no_question};
Questions ->
Question = lists:last(Questions),
get_authority(Question#dns_query.name)
end;
get_authority(Name) ->
case find_zone_in_cache(erldns:normalize_name(Name)) of
{ok, Zone} ->
{ok, Zone#zone.authority};
_ ->
{error, authority_not_found}
end.
%% @doc Get the list of NS and glue records for the given name. This function
%% will always return a list, even if it is empty.
-spec get_delegations(dns:dname()) -> [dns:rr()] | [].
get_delegations(Name) ->
case find_zone_in_cache(Name) of
{ok, Zone} ->
Records =
lists:flatten(
erldns_storage:select(
zone_records_typed,
[{{{erldns:normalize_name(Zone#zone.name), erldns:normalize_name(Name), ?DNS_TYPE_NS}, '$1'}, [], ['$$']}],
infinite
)
),
lists:filter(erldns_records:match_delegation(Name), Records);
_ ->
[]
end.
%% @doc Get all records for the given zone.
-spec get_zone_records(dns:dname()) -> [dns:rr()].
get_zone_records(Name) ->
case find_zone_in_cache(Name) of
{ok, Zone} ->
lists:flatten(
erldns_storage:select(
zone_records_typed, [{{{erldns:normalize_name(Zone#zone.name), '_', '_'}, '$1'}, [], ['$$']}], infinite
)
);
_ ->
[]
end.
%% @doc Get all records for the given type and given name.
-spec get_records_by_name_and_type(dns:dname(), dns:type()) -> [dns:rr()].
get_records_by_name_and_type(Name, Type) ->
case find_zone_in_cache(Name) of
{ok, Zone} ->
lists:flatten(
erldns_storage:select(
zone_records_typed,
[{{{erldns:normalize_name(Zone#zone.name), erldns:normalize_name(Name), Type}, '$1'}, [], ['$$']}],
infinite
)
);
_ ->
[]
end.
%% @doc Return the record set for the given dname.
-spec get_records_by_name(dns:dname()) -> [dns:rr()].
get_records_by_name(Name) ->
case find_zone_in_cache(Name) of
{ok, Zone} ->
lists:flatten(
erldns_storage:select(
zone_records_typed,
[{{{erldns:normalize_name(Zone#zone.name), erldns:normalize_name(Name), '_'}, '$1'}, [], ['$$']}],
infinite
)
);
_ ->
[]
end.
%% @doc Check if the name is in a zone.
-spec in_zone(binary()) -> boolean().
in_zone(Name) ->
case find_zone_in_cache(Name) of
{ok, Zone} ->
is_name_in_zone(Name, Zone);
_ ->
false
end.
%% @doc Check if the record name is in the zone. Will also return true if a wildcard is present at the node.
-spec record_name_in_zone(binary(), dns:dname()) -> boolean().
record_name_in_zone(ZoneName, Name) ->
case find_zone_in_cache(Name) of
{ok, Zone} ->
case
lists:flatten(
erldns_storage:select(
zone_records_typed, [{{{ZoneName, erldns:normalize_name(Name), '_'}, '$1'}, [], ['$$']}], infinite
)
)
of
[] ->
is_name_in_zone_with_wildcard(Name, Zone);
_ ->
true
end;
_ ->
false
end.
%% @doc Return a list of tuples with each tuple as a name and the version SHA
%% for the zone.
-spec zone_names_and_versions() -> [{dns:dname(), binary()}].
zone_names_and_versions() ->
% ETS and Mnesia foldl/3 differ in return values -> Mnesia's version is missing key
case erldns_config:storage_type() of
erldns_storage_json ->
erldns_storage:foldl(fun({_, Zone}, NamesAndShas) -> NamesAndShas ++ [{Zone#zone.name, Zone#zone.version}] end, [], zones);
erldns_storage_mnesia ->
erldns_storage:foldl(fun(Zone, NamesAndShas) -> NamesAndShas ++ [{Zone#zone.name, Zone#zone.version}] end, [], zones)
end.
%% @doc Return current sync counter
-spec get_rrset_sync_counter(dns:dname(), dns:dname(), dns:type()) -> integer().
get_rrset_sync_counter(ZoneName, RRFqdn, Type) ->
case
erldns_storage:select(
sync_counters, [{{erldns:normalize_name(ZoneName), erldns:normalize_name(RRFqdn), Type, '$1'}, [], ['$_']}], infinite
)
of
[{ZoneName, RRFqdn, Type, Counter}] ->
Counter;
[] ->
% return default value of 0
0
end.
%% @doc Update the RRSet sync counter for the given RR set name and type in the given zone.
-spec write_rrset_sync_counter({dns:dname(), dns:dname(), dns:type(), integer()}) -> ok.
write_rrset_sync_counter({ZoneName, RRFqdn, Type, Counter}) ->
erldns_storage:insert(sync_counters, {ZoneName, RRFqdn, Type, Counter}).
% ----------------------------------------------------------------------------------------------------
% Write API
%% @doc Put a name and its records into the cache, along with a SHA which can be
%% used to determine if the zone requires updating.
%%
%% This function will build the necessary Zone record before inserting.
-spec put_zone({Name, Sha, Records, Keys} | {Name, Sha, Records}) -> ok | {error, Reason :: term()} when
Name :: binary(),
Sha :: binary(),
Records :: [dns:rr()],
Keys :: [erldns:keyset()].
put_zone({Name, Sha, Records}) ->
put_zone({Name, Sha, Records, []});
put_zone({Name, Sha, Records, Keys}) ->
SignedZone = sign_zone(build_zone(Name, Sha, Records, Keys)),
NamedRecords = build_named_index(SignedZone#zone.records),
delete_zone_records(erldns:normalize_name(Name)),
case put_zone(erldns:normalize_name(Name), SignedZone#zone{records = trimmed}) of
ok ->
put_zone_records(erldns:normalize_name(Name), NamedRecords);
{error, Reason} ->
{error, Reason}
end.
%% @doc Put a zone into the cache and wait for a response.
-spec put_zone(dns:name(), erldns:zone()) -> ok | {error, Reason :: term()}.
put_zone(Name, Zone) ->
erldns_storage:insert(zones, {erldns:normalize_name(Name), Zone}).
-spec put_zone_records(dns:name(), map()) -> ok.
put_zone_records(Name, RecordsByName) ->
put_zone_records_entry(Name, maps:next(maps:iterator(RecordsByName))).
%% @doc Put zone RRSet
-spec put_zone_rrset(
{dns:dname(), binary(), [dns:rr()]} | {dns:dname(), binary(), [dns:rr()], [any()]}, dns:dname(), dns:type(), integer()
) ->
ok | {error, Reason :: term()}.
put_zone_rrset({ZoneName, Digest, Records}, RRFqdn, Type, Counter) ->
put_zone_rrset({ZoneName, Digest, Records, []}, RRFqdn, Type, Counter);
put_zone_rrset({ZoneName, Digest, Records, _Keys}, RRFqdn, Type, Counter) ->
case find_zone_in_cache(erldns:normalize_name(ZoneName)) of
{ok, Zone} ->
% TODO: remove debug
lager:debug("Putting RRSet (~p) with Type: ~p for Zone (~p): ~p", [RRFqdn, Type, ZoneName, Records]),
KeySets = Zone#zone.keysets,
SignedRRSet = sign_rrset(ZoneName, Records, KeySets),
{RRSigRecsCovering, RRSigRecsNotCovering} = filter_rrsig_records_with_type_covered(RRFqdn, Type),
% RRSet records + RRSIG records for the type + the rest of RRSIG records for FQDN
TypedRecords = build_typed_index(Records ++ SignedRRSet ++ RRSigRecsNotCovering),
CurrentRRSetRecords = get_records_by_name_and_type(RRFqdn, Type),
ZoneRecordsCount = Zone#zone.record_count,
% put zone_records_typed records first then create the records in zone_records
put_zone_records_typed_entry(ZoneName, RRFqdn, maps:next(maps:iterator(TypedRecords))),
UpdatedZoneRecordsCount =
ZoneRecordsCount +
(length(Records) - length(CurrentRRSetRecords)) +
(length(SignedRRSet) - length(RRSigRecsCovering)),
update_zone_records_and_digest(ZoneName, UpdatedZoneRecordsCount, Digest),
write_rrset_sync_counter({ZoneName, RRFqdn, Type, Counter}),
lager:debug("RRSet update completed for FQDN: ~p, Type: ~p", [RRFqdn, Type]),
ok;
% if zone is not in cache, return error
_ ->
{error, zone_not_found}
end.
put_zone_records_entry(_, none) ->
ok;
put_zone_records_entry(Name, {K, V, I}) ->
put_zone_records_typed_entry(Name, K, maps:next(maps:iterator(build_typed_index(V)))),
put_zone_records_entry(Name, maps:next(I)).
put_zone_records_typed_entry(_, _, none) ->
ok;
put_zone_records_typed_entry(ZoneName, Fqdn, {K, V, I}) ->
erldns_storage:insert(zone_records_typed, {{erldns:normalize_name(ZoneName), erldns:normalize_name(Fqdn), K}, V}),
put_zone_records_typed_entry(ZoneName, Fqdn, maps:next(I)).
%% @doc Remove a zone from the cache without waiting for a response.
-spec delete_zone(binary()) -> any().
delete_zone(Name) ->
gen_server:cast(?SERVER, {delete, Name}).
-spec delete_zone_records(binary()) -> any().
delete_zone_records(Name) ->
erldns_storage:select_delete(zone_records_typed, [{{{erldns:normalize_name(Name), '_', '_'}, '_'}, [], [true]}]).
%% @doc Remove zone RRSet
-spec delete_zone_rrset(binary(), binary(), binary(), integer(), integer()) -> any().
delete_zone_rrset(ZoneName, Digest, RRFqdn, Type, Counter) ->
case find_zone_in_cache(erldns:normalize_name(ZoneName)) of
{ok, Zone} ->
Zone,
CurrentCounter = get_rrset_sync_counter(ZoneName, RRFqdn, Type),
case Counter of
N when N =:= 0; CurrentCounter < N ->
lager:debug("Removing RRSet (~p) with type ~p", [RRFqdn, Type]),
ZoneRecordsCount = Zone#zone.record_count,
CurrentRRSetRecords = get_records_by_name_and_type(RRFqdn, Type),
erldns_storage:select_delete(
zone_records_typed,
[{{{erldns:normalize_name(ZoneName), erldns:normalize_name(RRFqdn), Type}, '_'}, [], [true]}]
),
% remove the RRSIG for the given record type
{RRSigsCovering, RRSigsNotCovering} =
lists:partition(
erldns_records:match_type_covered(Type), get_records_by_name_and_type(RRFqdn, ?DNS_TYPE_RRSIG_NUMBER)
),
erldns_storage:insert(
zone_records_typed,
{{erldns:normalize_name(ZoneName), erldns:normalize_name(RRFqdn), ?DNS_TYPE_RRSIG_NUMBER}, RRSigsNotCovering}
),
% only write counter if called explicitly with Counter value i.e. different than 0.
% this will not write the counter if called by put_zone_rrset/3 as it will prevent subsequent delete ops
case Counter of
N when N > 0 ->
% DELETE RRSet command has been sent
% we need to update the zone digest as the zone content changes
UpdatedZoneRecordsCount =
ZoneRecordsCount -
length(CurrentRRSetRecords) -
length(RRSigsCovering),
update_zone_records_and_digest(ZoneName, UpdatedZoneRecordsCount, Digest),
write_rrset_sync_counter({ZoneName, RRFqdn, Type, Counter});
_ ->
ok
end;
N when CurrentCounter > N ->
lager:debug("Not processing delete operation for RRSet (~p): counter (~p) provided is lower than system", [
RRFqdn, Counter
])
end;
_ ->
{error, zone_not_found}
end.
%% @doc Given a zone name, list of records, and a digest, update the zone metadata in cache.
-spec update_zone_records_and_digest(dns:dname(), integer(), binary()) -> ok | {error, Reason :: term()}.
update_zone_records_and_digest(ZoneName, RecordsCount, Digest) ->
case find_zone_in_cache(erldns:normalize_name(ZoneName)) of
{ok, Zone} ->
Zone,
UpdatedZone =
Zone#zone{
version = Digest,
authority = get_records_by_name_and_type(ZoneName, ?DNS_TYPE_SOA),
record_count = RecordsCount
},
put_zone(Zone#zone.name, UpdatedZone);
_ ->
{error, zone_not_found}
end.
%% @doc Filter RRSig records for FQDN, removing type covered.
-spec filter_rrsig_records_with_type_covered(dns:dname(), dns:type()) -> {[dns:rr()], [dns:rr()]} | {[], []}.
filter_rrsig_records_with_type_covered(RRFqdn, TypeCovered) ->
% guards below do not allow fun calls to prevent side effects
case find_zone_in_cache(erldns:normalize_name(RRFqdn)) of
{ok, _Zone} ->
% {RRSigsCovering, RRSigsNotCovering} =
lists:partition(
erldns_records:match_type_covered(TypeCovered),
get_records_by_name_and_type(RRFqdn, ?DNS_TYPE_RRSIG_NUMBER)
);
_ ->
{[], []}
end.
% ----------------------------------------------------------------------------------------------------
% Gen server init
%% @doc Initialize the zone cache.
-spec init([]) -> {ok, #state{}}.
init([]) ->
erldns_storage:create(schema),
erldns_storage:create(zones),
erldns_storage:create(zone_records_typed),
erldns_storage:create(authorities),
erldns_storage:create(sync_counters),
{ok, #state{parsers = []}}.
% ----------------------------------------------------------------------------------------------------
% gen_server callbacks
handle_call(Message, _From, State) ->
lager:debug("Received unsupported call (message: ~p)", [Message]),
{reply, ok, State}.
handle_cast({delete, Name}, State) ->
erldns_storage:delete(zones, erldns:normalize_name(Name)),
delete_zone_records(Name),
{noreply, State};
handle_cast(Message, State) ->
lager:debug("Received unsupported cast (message: ~p)", [Message]),
{noreply, State}.
handle_info(_Message, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_PreviousVersion, State, _Extra) ->
{ok, State}.
% Internal API
is_name_in_zone(Name, Zone) ->
ZoneName = erldns:normalize_name(Zone#zone.name),
case
lists:flatten(
erldns_storage:select(zone_records_typed, [{{{ZoneName, erldns:normalize_name(Name), '_'}, '$1'}, [], ['$$']}], infinite)
)
of
[] ->
case dns:dname_to_labels(Name) of
[] ->
false;
[_] ->
false;
[_ | Labels] ->
is_name_in_zone(dns:labels_to_dname(Labels), Zone)
end;
_ ->
true
end.
is_name_in_zone_with_wildcard(Name, Zone) ->
ZoneName = erldns:normalize_name(Zone#zone.name),
WildcardName = erldns:normalize_name(erldns_records:wildcard_qname(Name)),
case lists:flatten(erldns_storage:select(zone_records_typed, [{{{ZoneName, WildcardName, '_'}, '$1'}, [], ['$$']}], infinite)) of
[] ->
case dns:dname_to_labels(Name) of
[] ->
false;
[_] ->
false;
[_ | Labels] ->
is_name_in_zone_with_wildcard(dns:labels_to_dname(Labels), Zone)
end;
_ ->
true
end.
find_zone_in_cache(Qname) ->
Name = erldns:normalize_name(Qname),
find_zone_in_cache(Name, dns:dname_to_labels(Name)).
find_zone_in_cache(_Name, []) ->
{error, zone_not_found};
find_zone_in_cache(Name, [_ | Labels]) ->
case erldns_storage:select(zones, Name) of
[{Name, Zone}] ->
{ok, Zone};
_ ->
case Labels of
[] ->
{error, zone_not_found};
_ ->
find_zone_in_cache(dns:labels_to_dname(Labels))
end
end.
build_zone(Qname, Version, Records, Keys) ->
Authorities = lists:filter(erldns_records:match_type(?DNS_TYPE_SOA), Records),
#zone{
name = Qname,
version = Version,
record_count = length(Records),
authority = Authorities,
records = Records,
records_by_name = trimmed,
keysets = Keys
}.
-spec build_named_index([#dns_rr{}]) -> #{binary() => [#dns_rr{}]}.
build_named_index(Records) ->
NamedIndex =
lists:foldl(
fun(R, Idx) ->
Name = erldns:normalize_name(R#dns_rr.name),
maps:update_with(Name, fun(RR) -> [R | RR] end, [R], Idx)
end,
#{},
Records
),
maps:map(fun(_K, V) -> lists:reverse(V) end, NamedIndex).
-spec build_typed_index([#dns_rr{}]) -> #{dns:type() => [#dns_rr{}]}.
build_typed_index(Records) ->
TypedIndex = lists:foldl(fun(R, Idx) -> maps:update_with(R#dns_rr.type, fun(RR) -> [R | RR] end, [R], Idx) end, #{}, Records),
maps:map(fun(_K, V) -> lists:reverse(V) end, TypedIndex).
-spec sign_zone(erldns:zone()) -> erldns:zone().
sign_zone(Zone = #zone{keysets = []}) ->
Zone;
sign_zone(Zone) ->
DnskeyRRs = lists:filter(erldns_records:match_type(?DNS_TYPE_DNSKEY), Zone#zone.records),
KeyRRSigRecords = lists:flatten(lists:map(erldns_dnssec:key_rrset_signer(Zone#zone.name, DnskeyRRs), Zone#zone.keysets)),
% TODO: remove wildcard signatures as they will not be used but are taking up space
ZoneRRSigRecords =
lists:flatten(
lists:map(
erldns_dnssec:zone_rrset_signer(
Zone#zone.name,
lists:filter(fun(RR) -> RR#dns_rr.type =/= ?DNS_TYPE_DNSKEY end, Zone#zone.records)
),
Zone#zone.keysets
)
),
Records =
Zone#zone.records ++
KeyRRSigRecords ++
rewrite_soa_rrsig_ttl(Zone#zone.records, ZoneRRSigRecords -- lists:filter(erldns_records:match_wildcard(), ZoneRRSigRecords)),
#zone{
name = Zone#zone.name,
version = Zone#zone.version,
record_count = length(Records),
authority = Zone#zone.authority,
records = Records,
records_by_name = build_named_index(Records),
keysets = Zone#zone.keysets
}.
% Sign RRSet
-spec sign_rrset(binary(), [dns:rr()], [erldns:keyset()]) -> [dns:rr()].
sign_rrset(Name, Records, KeySets) ->
ZoneRecords = get_records_by_name_and_type(Name, ?DNS_TYPE_SOA),
RRSigRecords =
rewrite_soa_rrsig_ttl(
ZoneRecords,
lists:flatten(
lists:map(
erldns_dnssec:zone_rrset_signer(
Name,
lists:filter(
fun(RR) -> RR#dns_rr.type =/= ?DNS_TYPE_DNSKEY end,
Records
)
),
KeySets
)
)
),
RRSigRecords.
% Rewrite the RRSIG TTL so it follows the same rewrite rules as the SOA TTL.
rewrite_soa_rrsig_ttl(ZoneRecords, RRSigRecords) ->
SoaRR = lists:last(lists:filter(erldns_records:match_type(?DNS_TYPE_SOA), ZoneRecords)),
lists:map(
fun(RR) ->
case RR#dns_rr.type of
?DNS_TYPE_RRSIG ->
case RR#dns_rr.data#dns_rrdata_rrsig.type_covered of
?DNS_TYPE_SOA -> erldns_records:minimum_soa_ttl(RR, SoaRR#dns_rr.data);
_ -> RR
end;
_ ->
RR
end
end,
RRSigRecords
).