%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2011-2017. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%
-module(inet_tls_dist).
-export([childspecs/0]).
-export([listen/1, accept/1, accept_connection/5,
setup/5, close/1, select/1, is_node_name/1]).
%% Generalized dist API
-export([gen_listen/2, gen_accept/2, gen_accept_connection/6,
gen_setup/6, gen_close/2, gen_select/2]).
-export([split_node/1, nodelay/0]).
-include_lib("kernel/include/net_address.hrl").
-include_lib("kernel/include/dist.hrl").
-include_lib("kernel/include/dist_util.hrl").
%% -undef(trace).
%% -define(trace(Fmt,Args),
%% erlang:display(
%% [erlang:convert_time_unit(
%% erlang:monotonic_time()
%% - erlang:system_info(start_time), native, microsecond),
%% node(),
%% lists:flatten(io_lib:format(Fmt, Args))])).
%% -------------------------------------------------------------------------
childspecs() ->
{ok, [{ssl_dist_sup,{ssl_dist_sup, start_link, []},
permanent, infinity, supervisor, [ssl_dist_sup]}]}.
select(Node) ->
gen_select(inet_tcp, Node).
gen_select(Driver, Node) ->
case split_node(Node) of
false ->
false;
Host ->
case Driver:getaddr(Host) of
{ok, _} -> true;
_ -> false
end
end.
%% -------------------------------------------------------------------------
is_node_name(Node) ->
case split_node(Node) of
false ->
false;
_Host ->
true
end.
%% -------------------------------------------------------------------------
listen(Name) ->
gen_listen(inet_tcp, Name).
gen_listen(Driver, Name) ->
case inet_tcp_dist:gen_listen(Driver, Name) of
{ok, {Socket, Address, Creation}} ->
inet:setopts(Socket, [{packet, 4}]),
{ok, {Socket, Address#net_address{protocol=tls}, Creation}};
Other ->
Other
end.
%% -------------------------------------------------------------------------
accept(Listen) ->
gen_accept(inet_tcp, Listen).
gen_accept(Driver, Listen) ->
Kernel = self(),
spawn_opt(
fun () ->
accept_loop(Driver, Listen, Kernel)
end,
[link, {priority, max}]).
accept_loop(Driver, Listen, Kernel) ->
?trace("~p~n",[{?MODULE, accept_loop, self()}]),
case Driver:accept(Listen) of
{ok, Socket} ->
Opts = get_ssl_options(server),
wait_for_code_server(),
case ssl:ssl_accept(
Socket, [{active, false}, {packet, 4}] ++ Opts) of
{ok, SslSocket} ->
DistCtrl = ssl_tls_dist_ctrl:start(SslSocket),
?trace("~p~n",
[{?MODULE, accept_loop, accepted,
SslSocket, DistCtrl, self()}]),
ok = ssl:controlling_process(SslSocket, DistCtrl),
Kernel !
{accept, self(), DistCtrl, Driver:family(), tls},
receive
{Kernel, controller, Pid} ->
?trace("~p~n",
[{?MODULE, accept_loop,
controller, self()}]),
ssl_tls_dist_ctrl:set_supervisor(DistCtrl, Pid),
Pid ! {self(), controller};
{Kernel, unsupported_protocol} ->
?trace("~p~n",
[{?MODULE, accept_loop,
unsupported_protocol, self()}]),
exit(unsupported_protocol)
end,
accept_loop(Driver, Listen, Kernel);
{error, {options, _}} = Error ->
%% Bad options: that's probably our fault.
%% Let's log that.
error_logger:error_msg(
"Cannot accept TLS distribution connection: ~s~n",
[ssl:format_error(Error)]),
gen_tcp:close(Socket);
_ ->
gen_tcp:close(Socket)
end;
Error ->
exit(Error)
end,
accept_loop(Driver, Listen, Kernel).
wait_for_code_server() ->
%% This is an ugly hack. Upgrading a socket to TLS requires the
%% crypto module to be loaded. Loading the crypto module triggers
%% its on_load function, which calls code:priv_dir/1 to find the
%% directory where its NIF library is. However, distribution is
%% started earlier than the code server, so the code server is not
%% necessarily started yet, and code:priv_dir/1 might fail because
%% of that, if we receive an incoming connection on the
%% distribution port early enough.
%%
%% If the on_load function of a module fails, the module is
%% unloaded, and the function call that triggered loading it fails
%% with 'undef', which is rather confusing.
%%
%% Thus, the ssl_tls_dist_proxy process will terminate, and be
%% restarted by ssl_dist_sup. However, it won't have any memory
%% of being asked by net_kernel to listen for incoming
%% connections. Hence, the node will believe that it's open for
%% distribution, but it actually isn't.
%%
%% So let's avoid that by waiting for the code server to start.
case whereis(code_server) of
undefined ->
timer:sleep(10),
wait_for_code_server();
Pid when is_pid(Pid) ->
ok
end.
%% -------------------------------------------------------------------------
accept_connection(AcceptPid, DistCtrl, MyNode, Allowed, SetupTime) ->
gen_accept_connection(
inet_tcp, AcceptPid, DistCtrl, MyNode, Allowed, SetupTime).
gen_accept_connection(
Driver, AcceptPid, DistCtrl, MyNode, Allowed, SetupTime) ->
Kernel = self(),
spawn_opt(
fun() ->
do_accept(
Driver, Kernel, AcceptPid, DistCtrl,
MyNode, Allowed, SetupTime)
end,
[link, {priority, max}]).
do_accept(Driver, Kernel, AcceptPid, DistCtrl, MyNode, Allowed, SetupTime) ->
receive
{AcceptPid, controller} ->
Timer = dist_util:start_timer(SetupTime),
case check_ip(Driver, DistCtrl) of
true ->
HSData0 = ssl_tls_dist_ctrl:hs_data_common(DistCtrl),
HSData =
HSData0#hs_data{
kernel_pid = Kernel,
this_node = MyNode,
socket = DistCtrl,
timer = Timer,
this_flags = 0,
allowed = Allowed},
dist_util:handshake_other_started(HSData);
{false,IP} ->
error_logger:error_msg(
"** Connection attempt from "
"disallowed IP ~w ** ~n", [IP]),
?shutdown(no_node)
end
end.
setup(Node, Type, MyNode, LongOrShortNames, SetupTime) ->
gen_setup(inet_tcp, Node, Type, MyNode, LongOrShortNames, SetupTime).
gen_setup(Driver, Node, Type, MyNode, LongOrShortNames, SetupTime) ->
Kernel = self(),
spawn_opt(
fun() ->
do_setup(
Driver, Kernel, Node, Type,
MyNode, LongOrShortNames, SetupTime)
end,
[link, {priority, max}]).
do_setup(Driver, Kernel, Node, Type, MyNode, LongOrShortNames, SetupTime) ->
[Name, Address] = splitnode(Driver, Node, LongOrShortNames),
case Driver:getaddr(Address) of
{ok, Ip} ->
Timer = dist_util:start_timer(SetupTime),
ErlEpmd = net_kernel:epmd_module(),
case ErlEpmd:port_please(Name, Ip) of
{port, TcpPort, Version} ->
?trace("port_please(~p) -> version ~p~n",
[Node,Version]),
Opts = connect_options(get_ssl_options(client)),
dist_util:reset_timer(Timer),
case ssl:connect(
Ip, TcpPort,
[binary, {active, false}, {packet, 4},
Driver:family(), nodelay()] ++ Opts) of
{ok, SslSocket} ->
?trace("~p~n",
[{?MODULE, do_setup,
ssl_socket, SslSocket}]),
DistCtrl = ssl_tls_dist_ctrl:start(SslSocket),
ssl_tls_dist_ctrl:set_supervisor(
DistCtrl, self()),
ok =
ssl:controlling_process(
SslSocket, DistCtrl),
HSData0 =
ssl_tls_dist_ctrl:hs_data_common(DistCtrl),
HSData =
HSData0#hs_data{
kernel_pid = Kernel,
other_node = Node,
this_node = MyNode,
socket = DistCtrl,
timer = Timer,
this_flags = 0,
other_version = Version,
request_type = Type},
?trace("~p~n",
[{?MODULE, do_setup,
handshake_we_started, HSData}]),
dist_util:handshake_we_started(HSData);
Other ->
%% Other Node may have closed since
%% port_please !
?trace("other node (~p) "
"closed since port_please.~n",
[Node]),
?shutdown2(Node,
{shutdown, {connect_failed, Other}})
end;
Other ->
?trace("port_please (~p) "
"failed.~n", [Node]),
?shutdown2(Node, {shutdown, {port_please_failed, Other}})
end;
Other ->
?trace("~w:getaddr(~p) "
"failed (~p).~n", [Driver, Address, Other]),
?shutdown2(Node, {shutdown, {getaddr_failed, Other}})
end.
close(Socket) ->
gen_close(inet, Socket).
gen_close(Driver, Socket) ->
Driver:close(Socket).
%% ------------------------------------------------------------
%% Do only accept new connection attempts from nodes at our
%% own LAN, if the check_ip environment parameter is true.
%% ------------------------------------------------------------
check_ip(Driver, DistCtrl) ->
case application:get_env(check_ip) of
{ok, true} ->
case get_ifs(DistCtrl) of
{ok, IFs, IP} ->
check_ip(Driver, IFs, IP);
_ ->
?shutdown(no_node)
end;
_ ->
true
end.
check_ip(Driver, [{OwnIP, _, Netmask}|IFs], PeerIP) ->
case {Driver:mask(Netmask, PeerIP), Driver:mask(Netmask, OwnIP)} of
{M, M} -> true;
_ -> check_ip(IFs, PeerIP)
end;
check_ip(_Driver, [], PeerIP) ->
{false, PeerIP}.
get_ifs(DistCtrl) ->
Socket = ssl_tls_dist_ctrl:get_socket(DistCtrl),
case inet:peername(Socket) of
{ok, {IP, _}} ->
%% XXX this is seriously broken for IPv6
case inet:getif(Socket) of
{ok, IFs} -> {ok, IFs, IP};
Error -> Error
end;
Error ->
Error
end.
%% If Node is illegal terminate the connection setup!!
splitnode(Driver, Node, LongOrShortNames) ->
case split_node(atom_to_list(Node), $@, []) of
[Name|Tail] when Tail =/= [] ->
Host = lists:append(Tail),
check_node(Driver, Name, Node, Host, LongOrShortNames);
[_] ->
error_logger:error_msg("** Nodename ~p illegal, no '@' character **~n",
[Node]),
?shutdown(Node);
_ ->
error_logger:error_msg("** Nodename ~p illegal **~n", [Node]),
?shutdown(Node)
end.
check_node(Driver, Name, Node, Host, LongOrShortNames) ->
case split_node(Host, $., []) of
[_] when LongOrShortNames == longnames ->
case Driver:parse_address(Host) of
{ok, _} ->
[Name, Host];
_ ->
error_logger:error_msg(
"** System running to use "
"fully qualified hostnames **~n"
"** Hostname ~s is illegal **~n",
[Host]),
?shutdown(Node)
end;
[_, _ | _] when LongOrShortNames == shortnames ->
error_logger:error_msg(
"** System NOT running to use "
"fully qualified hostnames **~n"
"** Hostname ~s is illegal **~n",
[Host]),
?shutdown(Node);
_ ->
[Name, Host]
end.
split_node(Node) when is_atom(Node) ->
case split_node(atom_to_list(Node), $@, []) of
[_, Host] ->
Host;
_ ->
false
end;
split_node(_) ->
false.
%%
split_node([Chr|T], Chr, Ack) ->
[lists:reverse(Ack)|split_node(T, Chr, [])];
split_node([H|T], Chr, Ack) ->
split_node(T, Chr, [H|Ack]);
split_node([], _, Ack) ->
[lists:reverse(Ack)].
%% -------------------------------------------------------------------------
connect_options(Opts) ->
case application:get_env(kernel, inet_dist_connect_options) of
{ok,ConnectOpts} ->
lists:ukeysort(1, ConnectOpts ++ Opts);
_ ->
Opts
end.
%% we may not always want the nodelay behaviour
%% for performance reasons
nodelay() ->
case application:get_env(kernel, dist_nodelay) of
undefined ->
{nodelay, true};
{ok, true} ->
{nodelay, true};
{ok, false} ->
{nodelay, false};
_ ->
{nodelay, true}
end.
get_ssl_options(Type) ->
case init:get_argument(ssl_dist_opt) of
{ok, Args} ->
[{erl_dist, true} | ssl_options(Type, lists:append(Args))];
_ ->
[{erl_dist, true}]
end.
ssl_options(_,[]) ->
[];
ssl_options(server, ["client_" ++ _, _Value |T]) ->
ssl_options(server,T);
ssl_options(client, ["server_" ++ _, _Value|T]) ->
ssl_options(client,T);
ssl_options(server, ["server_certfile", Value|T]) ->
[{certfile, Value} | ssl_options(server,T)];
ssl_options(client, ["client_certfile", Value | T]) ->
[{certfile, Value} | ssl_options(client,T)];
ssl_options(server, ["server_cacertfile", Value|T]) ->
[{cacertfile, Value} | ssl_options(server,T)];
ssl_options(client, ["client_cacertfile", Value|T]) ->
[{cacertfile, Value} | ssl_options(client,T)];
ssl_options(server, ["server_keyfile", Value|T]) ->
[{keyfile, Value} | ssl_options(server,T)];
ssl_options(client, ["client_keyfile", Value|T]) ->
[{keyfile, Value} | ssl_options(client,T)];
ssl_options(server, ["server_password", Value|T]) ->
[{password, Value} | ssl_options(server,T)];
ssl_options(client, ["client_password", Value|T]) ->
[{password, Value} | ssl_options(client,T)];
ssl_options(server, ["server_verify", Value|T]) ->
[{verify, atomize(Value)} | ssl_options(server,T)];
ssl_options(client, ["client_verify", Value|T]) ->
[{verify, atomize(Value)} | ssl_options(client,T)];
ssl_options(server, ["server_verify_fun", Value|T]) ->
[{verify_fun, verify_fun(Value)} | ssl_options(server,T)];
ssl_options(client, ["client_verify_fun", Value|T]) ->
[{verify_fun, verify_fun(Value)} | ssl_options(client,T)];
ssl_options(server, ["server_crl_check", Value|T]) ->
[{crl_check, atomize(Value)} | ssl_options(server,T)];
ssl_options(client, ["client_crl_check", Value|T]) ->
[{crl_check, atomize(Value)} | ssl_options(client,T)];
ssl_options(server, ["server_crl_cache", Value|T]) ->
[{crl_cache, termify(Value)} | ssl_options(server,T)];
ssl_options(client, ["client_crl_cache", Value|T]) ->
[{crl_cache, termify(Value)} | ssl_options(client,T)];
ssl_options(server, ["server_reuse_sessions", Value|T]) ->
[{reuse_sessions, atomize(Value)} | ssl_options(server,T)];
ssl_options(client, ["client_reuse_sessions", Value|T]) ->
[{reuse_sessions, atomize(Value)} | ssl_options(client,T)];
ssl_options(server, ["server_secure_renegotiate", Value|T]) ->
[{secure_renegotiate, atomize(Value)} | ssl_options(server,T)];
ssl_options(client, ["client_secure_renegotiate", Value|T]) ->
[{secure_renegotiate, atomize(Value)} | ssl_options(client,T)];
ssl_options(server, ["server_depth", Value|T]) ->
[{depth, list_to_integer(Value)} | ssl_options(server,T)];
ssl_options(client, ["client_depth", Value|T]) ->
[{depth, list_to_integer(Value)} | ssl_options(client,T)];
ssl_options(server, ["server_hibernate_after", Value|T]) ->
[{hibernate_after, list_to_integer(Value)} | ssl_options(server,T)];
ssl_options(client, ["client_hibernate_after", Value|T]) ->
[{hibernate_after, list_to_integer(Value)} | ssl_options(client,T)];
ssl_options(server, ["server_ciphers", Value|T]) ->
[{ciphers, Value} | ssl_options(server,T)];
ssl_options(client, ["client_ciphers", Value|T]) ->
[{ciphers, Value} | ssl_options(client,T)];
ssl_options(server, ["server_dhfile", Value|T]) ->
[{dhfile, Value} | ssl_options(server,T)];
ssl_options(server, ["server_fail_if_no_peer_cert", Value|T]) ->
[{fail_if_no_peer_cert, atomize(Value)} | ssl_options(server,T)];
ssl_options(Type, Opts) ->
error(malformed_ssl_dist_opt, [Type, Opts]).
atomize(List) when is_list(List) ->
list_to_atom(List);
atomize(Atom) when is_atom(Atom) ->
Atom.
termify(String) when is_list(String) ->
{ok, Tokens, _} = erl_scan:string(String ++ "."),
{ok, Term} = erl_parse:parse_term(Tokens),
Term.
verify_fun(Value) ->
case termify(Value) of
{Mod, Func, State} when is_atom(Mod), is_atom(Func) ->
Fun = fun Mod:Func/3,
{Fun, State};
_ ->
error(malformed_ssl_dist_opt, [Value])
end.