Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add support for loading zones remotely and from a zone server.

  • Loading branch information...
commit 3fb014486e57c5755b7c684294509fdf085102fb 1 parent d5750c6
@aeden aeden authored
View
8 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]}]}.
View
2  erldns.config.example
@@ -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},
View
3  rebar.config
@@ -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"}}
]}.
View
5 src/erldns.app.src
@@ -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, []}]}
+ ]}.
View
3  src/erldns.erl
@@ -3,6 +3,9 @@
-export([start/0]).
start() ->
+ inets:start(),
+ crypto:start(),
+ ssl:start(),
lager:start(),
folsom:start(),
application:start(erldns).
View
13 src/erldns_app.erl
@@ -2,7 +2,7 @@
-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]),
@@ -10,6 +10,17 @@ start(Type, Args) ->
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.
View
4 src/erldns_metrics.erl
@@ -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]).
@@ -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]).
View
7 src/erldns_sup.erl
@@ -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}}.
View
17 src/erldns_udp_server.erl
@@ -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) ->
@@ -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]).
View
115 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()}.
+
View
20 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.
View
86 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]),
+ {}.
+
Please sign in to comment.
Something went wrong with that request. Please try again.