Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add support for loading zones remotely and from a zone server.
  • Loading branch information
aeden committed Mar 11, 2013
1 parent d5750c6 commit 3fb0144
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 15 deletions.
8 changes: 5 additions & 3 deletions ebin/erldns.app
@@ -1,12 +1,14 @@
{application,erldns,
[{description,"Erlang Authoritative DNS Server"},
{vsn,"de616b1"},
{vsn,"d5750c6"},
{mod,{erldns_app,[]}},
{applications,[kernel,stdlib]},
{applications,[kernel,stdlib,inets,crypto,ssl]},
{start_phases,[{post_start,[]}]},
{modules,[erldns,erldns_app,erldns_axfr,erldns_config,
erldns_dnssec,erldns_edns,erldns_encoder,
erldns_handler,erldns_metrics,erldns_packet_cache,
erldns_query_throttle,erldns_records,erldns_sup,
erldns_tcp_server,erldns_udp_server,erldns_worker,
erldns_zone_cache,gen_nb_server,
erldns_zone_cache,erldns_zone_client,
erldns_zone_loader,erldns_zone_parser,gen_nb_server,
sample_custom_handler]}]}.
2 changes: 2 additions & 0 deletions erldns.config.example
Expand Up @@ -4,6 +4,8 @@
{inet4, "127.0.0.1"}},
{inet6, "::1"},
{catch_exceptions, false},
{credentials, {"indentifier", "token"}},
{zone_server_host, "your-zone-server.example.com"}
{pools, [
{tcp_worker_pool, [
{size, 10},
Expand Down
3 changes: 2 additions & 1 deletion rebar.config
Expand Up @@ -13,8 +13,9 @@

{deps, [
{poolboy, ".*", {git, "git://github.com/devinus/poolboy.git", "HEAD"}},
{epgsql, ".*", {git, "git://github.com/wg/epgsql.git", "HEAD"}},
{jsx, ".*", {git, "git://github.com/talentdeficit/jsx.git", "HEAD"}},
{dns, ".*", {git, "git://github.com/andrewtj/dns_erlang.git", "HEAD"}},
{websocket_client, ".*", {git, "git://github.com/jeremyong/websocket_client.git", "HEAD"}},
{lager, ".*", {git, "git://github.com/basho/lager.git", "HEAD"}},
{folsom, ".*", {git, "git://github.com/boundary/folsom.git", "HEAD"}}
]}.
5 changes: 3 additions & 2 deletions src/erldns.app.src
Expand Up @@ -2,5 +2,6 @@
[{description, "Erlang Authoritative DNS Server"},
{vsn, git},
{mod, { erldns_app, []}},
{applications, [kernel, stdlib]}]}.

{applications, [kernel, stdlib, inets, crypto, ssl]},
{start_phases, [{post_start, []}]}
]}.
3 changes: 3 additions & 0 deletions src/erldns.erl
Expand Up @@ -3,6 +3,9 @@
-export([start/0]).

start() ->
inets:start(),
crypto:start(),
ssl:start(),
lager:start(),
folsom:start(),
application:start(erldns).
13 changes: 12 additions & 1 deletion src/erldns_app.erl
Expand Up @@ -2,14 +2,25 @@
-behavior(application).

% Application hooks
-export([start/2, stop/1]).
-export([start/2, start_phase/3, stop/1]).

start(Type, Args) ->
lager:info("~p:start(~p, ~p)", [?MODULE, Type, Args]),
random:seed(erlang:now()),
enable_metrics(),
erldns_sup:start_link().

start_phase(post_start, _StartType, _PhaseArgs) ->
lager:info("Loading zones from local file"),
erldns_metrics:measure(none, erldns_zone_loader, load_zones, []),
case application:get_env(erldns, zone_server_host) of
{ok, _} ->
lager:info("Loading zones from remote server"),
erldns_metrics:measure(none, erldns_zone_client, fetch_zones, []);
_ -> not_fetching
end,
ok.

stop(State) ->
lager:info("~p:stop(~p)~n", [?MODULE, State]),
ok.
Expand Down
4 changes: 2 additions & 2 deletions src/erldns_metrics.erl
Expand Up @@ -33,7 +33,7 @@ slowest() ->

measure(_, Module, FunctionName, Args) when is_list(Args) ->
{T, R} = timer:tc(Module, FunctionName, Args),
lager:debug("~p:~p took ~p ms", [Module, FunctionName, T / 1000]),
lager:debug([{tag, timer_result}], "~p:~p took ~p ms", [Module, FunctionName, T / 1000]),
R;
measure(Name, Module, FunctionName, Arg) -> measure(Name, Module, FunctionName, [Arg]).

Expand Down Expand Up @@ -65,4 +65,4 @@ code_change(_PreviousVersion, State, _Extra) ->

% Internal API

display_list({Name, T}) -> lager:debug("~p: ~p ms", [Name, T / 1000]).
display_list({Name, T}) -> lager:debug([{tag, timer_result}], "~p: ~p ms", [Name, T / 1000]).
7 changes: 6 additions & 1 deletion src/erldns_sup.erl
Expand Up @@ -40,4 +40,9 @@ init(_Args) ->
{tcp_inet6, {erldns_tcp_server, start_link, [tcp_inet6, inet6]}, permanent, 5000, worker, [erldns_tcp_server]}
],

{ok, {{one_for_one, 20, 10}, Procs ++ AppPoolSpecs}}.
OptionalProcs = case application:get_env(erldns, zone_server_host) of
{ok, _} -> [?CHILD(erldns_zone_client, worker, [])];
_ -> []
end,

{ok, {{one_for_one, 20, 10}, Procs ++ OptionalProcs ++ AppPoolSpecs}}.
17 changes: 12 additions & 5 deletions src/erldns_udp_server.erl
Expand Up @@ -30,12 +30,11 @@ handle_call(_Request, _From, State) ->
{ok, State}.
handle_cast(_Message, State) ->
{noreply, State}.
handle_info(timeout, State) ->
lager:info("UDP instance timed out"),
{noreply, State};
handle_info({udp, Socket, Host, Port, Bin}, State) ->
lager:debug("Received UDP Request ~p ~p ~p", [Socket, Host, Port]),
poolboy:transaction(udp_worker_pool, fun(Worker) ->
gen_server:call(Worker, {udp_query, Socket, Host, Port, Bin})
end),
% handle_dns_query(Socket, Host, Port, Bin),
do_work(Socket, Host, Port, Bin),
inet:setopts(State#state.socket, [{active, once}]),
{noreply, State};
handle_info(_Message, State) ->
Expand All @@ -57,3 +56,11 @@ start(Port, InetFamily) ->
lager:error("Failed to open UDP socket. Need to run as sudo?"),
{error, eacces}
end.

do_work(Socket, Host, Port, Bin) ->
lager:debug("Received UDP Request ~p ~p ~p", [Socket, Host, Port]),
poolboy:transaction(udp_worker_pool, fun(Worker) ->
lager:debug("Processing UDP request with worker ~p ~p ~p", [Socket, Host, Port]),
gen_server:call(Worker, {udp_query, Socket, Host, Port, Bin})
end),
lager:debug("Completed UDP request processing ~p ~p ~p", [Socket, Host, Port]).
115 changes: 115 additions & 0 deletions src/erldns_zone_client.erl
@@ -0,0 +1,115 @@
-module(erldns_zone_client).

-behaviour(websocket_client_handler).

-include("dns.hrl").
-include("erldns.hrl").

-export([
start_link/0,
fetch_zones/0,
fetch_zone/1,
init/1,
websocket_handle/2,
websocket_info/2,
websocket_terminate/2
]).

% Public API
start_link() ->
lager:info("Starting websocket client"),
StartLinkResult = websocket_client:start_link(?MODULE, wss, zone_server_host(), 443, "/ws", []),
{ok, StartLinkResult}.

fetch_zones() ->
case httpc:request(get, {zones_url(), [auth_header()]}, [], [{body_format, binary}]) of
{ok, {{_Version, 200, _ReasonPhrase}, _Headers, Body}} ->
lager:info("Parsing zones JSON"),
Zones = erldns_zone_parser:zones_to_erlang(jsx:decode(Body)),
lists:foreach(
fun(Zone) ->
erldns_zone_cache:put_zone(Zone)
end, Zones),
lager:info("Put ~p zones into cache", [length(Zones)]),
{ok, length(Zones)};
{_, {{_Version, Status, ReasonPhrase}, _Headers, _Body}} ->
lager:error("Failed to load zones: ~p (status: ~p)", [ReasonPhrase, Status]),
{err, Status, ReasonPhrase}
end.

fetch_zone(Name) ->
fetch_zone(Name, zones_url() ++ binary_to_list(Name)).

fetch_zone(Name, Url) ->
case httpc:request(get, {Url, [auth_header()]}, [], [{body_format, binary}]) of
{ok, {{_Version, 200, _ReasonPhrase}, _Headers, Body}} ->
lager:info("Parsing zone JSON"),
Zone = erldns_zone_parser:zone_to_erlang(jsx:decode(Body)),
lager:info("Putting ~p into zone cache", [Name]),
erldns_zone_cache:put_zone(Zone);
{_, {{_Version, Status, ReasonPhrase}, _Headers, _Body}} ->
lager:error("Failed to load zone: ~p (status: ~p)", [ReasonPhrase, Status]),
{err, Status, ReasonPhrase}
end.

% Websocket Callbacks

init([]) ->
lager:info("init() websocket client"),
self() ! authenticate,
{ok, 2}.

websocket_handle({_Type, Msg}, State) ->
ZoneNotification = jsx:decode(Msg),
lager:info("Zone notification received: ~p", [ZoneNotification]),
case ZoneNotification of
[{<<"name">>, Name}, {<<"url">>, Url}, {<<"action">>, Action}] ->
case Action of
<<"create">> ->
lager:debug("Creating zone ~p", [Name]),
fetch_zone(Name, binary_to_list(Url));
<<"update">> ->
lager:debug("Updating zone ~p", [Name]),
fetch_zone(Name, binary_to_list(Url));
<<"delete">> ->
erldns_zone_cache:delete_zone(Name),
lager:debug("Deleting zone ~p", [Name]);
_ ->
lager:error("Unsupported action: ~p", [Action])
end;
_ ->
lager:error("Unsupported zone notification message: ~p", [ZoneNotification])
end,
{ok, State}.

websocket_info(authenticate, State) ->
EncodedCredentials = encoded_credentials(),
lager:debug("Authenticating with ~p", [EncodedCredentials]),
{reply, {text, list_to_binary("Authorization: " ++ EncodedCredentials)}, State};

websocket_info(Atom, State) ->
lager:debug("websocket_info(~p, ~p)", [Atom, State]),
{ok, State}.

websocket_terminate(Message, State) ->
lager:debug("websocket_terminate(~p, ~p)", [Message, State]),
ok.

%% Internal functions
zone_server_host() ->
{ok, ZoneServerHost} = application:get_env(erldns, zone_server_host),
ZoneServerHost.

zones_url() ->
"https://" ++ zone_server_host() ++ "/zones/".

encoded_credentials() ->
case application:get_env(erldns, credentials) of
{ok, {Username, Password}} ->
lager:debug("Sending ~p:~p for authentication", [Username, Password]),
base64:encode_to_string(lists:append([Username,":",Password]))
end.

auth_header() ->
{"Authorization","Basic " ++ encoded_credentials()}.

20 changes: 20 additions & 0 deletions src/erldns_zone_loader.erl
@@ -0,0 +1,20 @@
-module(erldns_zone_loader).

-export([load_zones/0]).

-define(FILENAME, "zones.json").

load_zones() ->
case file:read_file(?FILENAME) of
{ok, Binary} ->
Zones = erldns_zone_parser:zones_to_erlang(jsx:decode(Binary)),
lists:foreach(
fun(Zone) ->
erldns_zone_cache:put_zone(Zone)
end, Zones),
lager:info("Loaded ~p zones", [length(Zones)]),
{ok, length(Zones)};
{error, Reason} ->
lager:error("Failed to load zones: ~p", [Reason]),
{err, Reason}
end.
86 changes: 86 additions & 0 deletions src/erldns_zone_parser.erl
@@ -0,0 +1,86 @@
-module(erldns_zone_parser).

-export([zones_to_erlang/1, zone_to_erlang/1]).

-include("dns.hrl").
-include("erldns.hrl").

zones_to_erlang(Zones) -> zones_to_erlang(Zones, []).

% Internal
zones_to_erlang([], Zones) -> Zones;

zones_to_erlang([Zone|Rest], Zones) ->
ParsedZone = zone_to_erlang(Zone),
zones_to_erlang(Rest, Zones ++ [ParsedZone]).

%% Takes a JSON zone and turns it into the tuple {Name, Records}.
zone_to_erlang([{<<"name">>, Name}, {<<"records">>, JsonRecords}]) ->
Records = lists:map(
fun(JsonRecord) ->
json_record_to_erlang(JsonRecord)
end, JsonRecords),

FilteredRecords = lists:filter(
fun(R) ->
case R of
{} -> false;
_ -> true
end
end, Records),

{Name, FilteredRecords}.

% Internal converters
json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"SOA">>}, {<<"data">>, [{<<"mname">>, Mname}, {<<"rname">>, Rname}, {<<"serial">>, Serial}, {<<"refresh">>, Refresh}, {<<"retry">>, Retry}, {<<"expire">>, Expire},{<<"minimum">>, Minimum}]}, {<<"ttl">>, Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_SOA, data = #dns_rrdata_soa{mname = Mname, rname = Rname, serial = Serial, refresh = Refresh, retry = Retry, expire = Expire, minimum = Minimum}, ttl = Ttl};

json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"NS">>}, {<<"data">>, [{<<"dname">>, Dname}]}, {<<"ttl">>, Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_NS, data = #dns_rrdata_ns{dname = Dname}, ttl = Ttl};

json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"A">>}, {<<"data">>, [{<<"ip">>, Ip}]}, {<<"ttl">>, Ttl}]) ->
case inet_parse:address(binary_to_list(Ip)) of
{ok, Address} ->
#dns_rr{name = Name, type = ?DNS_TYPE_A, data = #dns_rrdata_a{ip = Address}, ttl = Ttl};
{error, Reason} ->
lager:error("Failed to parse A record address ~p: ~p", [Ip, Reason]),
{}
end;

json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"AAAA">>}, {<<"data">>, [{<<"ip">>, Ip}]}, {<<"ttl">>, Ttl}]) ->
case inet_parse:address(binary_to_list(Ip)) of
{ok, Address} ->
#dns_rr{name = Name, type = ?DNS_TYPE_AAAA, data = #dns_rrdata_aaaa{ip = Address}, ttl = Ttl};
{error, Reason} ->
lager:error("Failed to parse AAAA record address ~p: ~p", [Ip, Reason]),
{}
end;

json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"CNAME">>}, {<<"data">>, [{<<"dname">>, Dname}]}, {<<"ttl">>, Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_CNAME, data = #dns_rrdata_cname{dname = Dname}, ttl = Ttl};

json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"MX">>}, {<<"data">>, [{<<"preference">>, Preference}, {<<"exchange">>, Exchange}]}, {<<"ttl">>, Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_MX, data = #dns_rrdata_mx{exchange = Exchange, preference = Preference}, ttl = Ttl};

json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"TXT">>}, {<<"data">>, [{<<"txt">>, Text}]}, {<<"ttl">>, Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_TXT, data = #dns_rrdata_txt{txt = [Text]}, ttl = Ttl};

json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"SPF">>}, {<<"data">>, [{<<"spf">>, Spf}]}, {<<"ttl">>, Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_SPF, data = #dns_rrdata_spf{spf = [Spf]}, ttl = Ttl};

json_record_to_erlang([{<<"name">>,Name},{<<"type">>,<<"PTR">>},{<<"data">>,[{<<"dname">>, Dname}]},{<<"ttl">>, Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_PTR, data = #dns_rrdata_ptr{dname = Dname}, ttl = Ttl};

json_record_to_erlang([{<<"name">>,Name},{<<"type">>,<<"SSHFP">>},{<<"data">>,[{<<"alg">>,Alg},{<<"fptype">>,Fptype},{<<"fp">>,Fp}]},{<<"ttl">>,Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_SSHFP, data = #dns_rrdata_sshfp{alg = Alg, fp_type = Fptype, fp = Fp}, ttl = Ttl};

json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"SRV">>}, {<<"data">>, [{<<"priority">>, Priority}, {<<"weight">>, Weight}, {<<"port">>, Port}, {<<"target">>, Target}]}, {<<"ttl">>, Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_SRV, data = #dns_rrdata_srv{priority = Priority, weight = Weight, port = Port, target = Target}, ttl = Ttl};

json_record_to_erlang([{<<"name">>, Name}, {<<"type">>, <<"NAPTR">>}, {<<"data">>, [{<<"order">>, Order}, {<<"preference">>, Preference}, {<<"flags">>, Flags}, {<<"services">>, Services}, {<<"regexp">>, Regexp}, {<<"replacement">>, Replacement}]}, {<<"ttl">>, Ttl}]) ->
#dns_rr{name = Name, type = ?DNS_TYPE_NAPTR, data = #dns_rrdata_naptr{order = Order, preference = Preference, flags = Flags, services = Services, regexp = Regexp, replacement = Replacement}, ttl = Ttl};

json_record_to_erlang(JsonRecord) ->
lager:info("Unsupported record ~p", [JsonRecord]),
{}.

0 comments on commit 3fb0144

Please sign in to comment.