-
Notifications
You must be signed in to change notification settings - Fork 98
/
erldns_zone_cache.erl
317 lines (271 loc) · 11.4 KB
/
erldns_zone_cache.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
%% Copyright (c) 2012-2015, Aetrion LLC
%%
%% 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/include/dns.hrl").
-include("erldns.hrl").
-export([start_link/0]).
% Read APIs
-export([
find_zone/1,
find_zone/2,
get_zone/1,
get_zone_with_records/1,
get_authority/1,
get_delegations/1,
get_records_by_name/1,
in_zone/1,
zone_names_and_versions/0
]).
% Write APIs
-export([
put_zone/1,
put_zone/2,
delete_zone/1
]).
% 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.
-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:rr()}.
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} ->
lists:filter(fun(R) -> apply(erldns_records:match_type(?DNS_TYPE_NS), [R]) and apply(erldns_records:match_delegation(Name), [R]) end, Zone#zone.records);
_ ->
[]
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} ->
case dict:find(erldns:normalize_name(Name), Zone#zone.records_by_name) of
{ok, RecordSet} -> RecordSet;
_ -> []
end;
_ ->
[]
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 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() ->
erldns_storage:foldl(fun({_, Zone}, NamesAndShas) -> NamesAndShas ++ [{Zone#zone.name, Zone#zone.version}] end, [], zones).
% ----------------------------------------------------------------------------------------------------
% 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 interting.
-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}) ->
put_zone(erldns:normalize_name(Name), build_zone(Name, Sha, Records, Keys)).
%% @doc Put a zone into the cache and wait for a response.
-spec put_zone(binary(), erldns:zone()) -> ok | {error, Reason :: term()}.
put_zone(Name, Zone) ->
erldns_storage:insert(zones, {erldns:normalize_name(Name), sign_zone(Zone)}).
%% @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}).
% ----------------------------------------------------------------------------------------------------
% Gen server init
%% @doc Initialize the zone cache.
-spec init([]) -> {ok, #state{}}.
init([]) ->
erldns_storage:create(schema),
erldns_storage:create(zones),
erldns_storage:create(authorities),
{ok, #state{parsers = []}}.
% ----------------------------------------------------------------------------------------------------
% gen_server callbacks
handle_call(Message, _From, State) ->
lager:debug("Received unsupported call: ~p", [Message]),
{reply, ok, State}.
handle_cast({delete, Name}, State) ->
erldns_storage:delete(zones, erldns:normalize_name(Name)),
{noreply, State};
handle_cast(Message, State) ->
lager:debug("Received unsupported cast: ~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) ->
case dict:is_key(erldns:normalize_name(Name), Zone#zone.records_by_name) of
true -> true;
false ->
case dns:dname_to_labels(Name) of
[] -> false;
[_] -> false;
[_|Labels] -> is_name_in_zone(dns:labels_to_dname(Labels), Zone)
end
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) ->
RecordsByName = build_named_index(Records),
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 = RecordsByName, keysets = Keys}.
-spec(build_named_index([#dns_rr{}]) -> dict:dict(binary(), [#dns_rr{}])).
build_named_index(Records) -> build_named_index(Records, dict:new()).
build_named_index([], Idx) -> Idx;
build_named_index([R|Rest], Idx) ->
case dict:find(R#dns_rr.name, Idx) of
{ok, Records} ->
build_named_index(Rest, dict:store(erldns:normalize_name(R#dns_rr.name), Records ++ [R], Idx));
error ->
build_named_index(Rest, dict:store(erldns:normalize_name(R#dns_rr.name), [R], Idx))
end.
-spec(sign_zone(erldns:zone()) -> erldns:zone()).
sign_zone(Zone = #zone{keysets = []}) ->
Zone;
sign_zone(Zone) ->
lager:debug("Signing zone ~p", [Zone#zone.name]),
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)),
verify_zone(Zone, DnskeyRRs, KeyRRSigRecords),
% 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)),
build_zone(Zone#zone.name, Zone#zone.version, Zone#zone.records ++ KeyRRSigRecords ++ rewrite_soa_rrsig_ttl(Zone#zone.records, ZoneRRSigRecords -- lists:filter(erldns_records:match_wildcard(), ZoneRRSigRecords)), Zone#zone.keysets).
-spec(verify_zone(erldns:zone(), [dns:rr()], [dns:rr()]) -> boolean()).
verify_zone(Zone, DnskeyRRs, KeyRRSigRecords) ->
lager:debug("Verify zone ~p", [Zone#zone.name]),
case lists:filter(fun(RR) -> RR#dns_rr.data#dns_rrdata_dnskey.flags =:= 257 end, DnskeyRRs) of
[] -> false;
KSKs ->
lager:debug("KSKs: ~p", [KSKs]),
KSKDnskey = lists:last(KSKs),
RRSig = lists:last(KeyRRSigRecords),
lager:debug("Attempting to verify RRSIG with ~p", [KSKDnskey]),
VerifyResult = dnssec:verify_rrsig(RRSig, DnskeyRRs, [KSKDnskey], []),
lager:debug("KSK verified? ~p", [VerifyResult]),
VerifyResult
end.
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 -> erldns_records:minimum_soa_ttl(RR, SoaRR#dns_rr.data);
_ -> RR
end
end, RRSigRecords).