|
|
@@ -0,0 +1,253 @@
|
|
|
+-module(emqtt_networking).
|
|
|
+
|
|
|
+-export([boot/0]).
|
|
|
+
|
|
|
+-export([start_tcp_listener/1, stop_tcp_listener/1, tcp_host/1, ntoab/1]).
|
|
|
+
|
|
|
+%callback.
|
|
|
+
|
|
|
+-export([tcp_listener_started/3, tcp_listener_stopped/3, start_client/1]).
|
|
|
+
|
|
|
+-include_lib("kernel/include/inet.hrl").
|
|
|
+
|
|
|
+-define(FIRST_TEST_BIND_PORT, 10000).
|
|
|
+
|
|
|
+boot() ->
|
|
|
+ {ok, TcpListeners} = application:get_env(tcp_listeners),
|
|
|
+ [ok = start_tcp_listener(Listener) || Listener <- TcpListeners].
|
|
|
+
|
|
|
+start_tcp_listener(Listener) ->
|
|
|
+ start_listener(Listener, emqtt, "TCP Listener",
|
|
|
+ {?MODULE, start_client, []}).
|
|
|
+
|
|
|
+start_listener(Listener, Protocol, Label, OnConnect) ->
|
|
|
+ [start_listener0(Address, Protocol, Label, OnConnect) ||
|
|
|
+ Address <- tcp_listener_addresses(Listener)],
|
|
|
+ ok.
|
|
|
+
|
|
|
+start_listener0(Address, Protocol, Label, OnConnect) ->
|
|
|
+ Spec = tcp_listener_spec(emqtt_tcp_listener_sup, Address, tcp_opts(),
|
|
|
+ Protocol, Label, OnConnect),
|
|
|
+ case supervisor:start_child(emqtt_sup, Spec) of
|
|
|
+ {ok, _} -> ok;
|
|
|
+ {error, {shutdown, _}} -> {IPAddress, Port, _Family} = Address,
|
|
|
+ exit({could_not_start_tcp_listener,
|
|
|
+ {ntoa(IPAddress), Port}})
|
|
|
+ end.
|
|
|
+
|
|
|
+stop_tcp_listener(Listener) ->
|
|
|
+ [stop_tcp_listener0(Address) ||
|
|
|
+ Address <- tcp_listener_addresses(Listener)],
|
|
|
+ ok.
|
|
|
+
|
|
|
+stop_tcp_listener0({IPAddress, Port, _Family}) ->
|
|
|
+ Name = tcp_name(emqtt_tcp_listener_sup, IPAddress, Port),
|
|
|
+ ok = supervisor:terminate_child(emqtt_sup, Name),
|
|
|
+ ok = supervisor:delete_child(emqtt_sup, Name).
|
|
|
+
|
|
|
+tcp_listener_addresses(Port) when is_integer(Port) ->
|
|
|
+ tcp_listener_addresses_auto(Port);
|
|
|
+tcp_listener_addresses({"auto", Port}) ->
|
|
|
+ %% Variant to prevent lots of hacking around in bash and batch files
|
|
|
+ tcp_listener_addresses_auto(Port);
|
|
|
+tcp_listener_addresses({Host, Port}) ->
|
|
|
+ %% auto: determine family IPv4 / IPv6 after converting to IP address
|
|
|
+ tcp_listener_addresses({Host, Port, auto});
|
|
|
+tcp_listener_addresses({Host, Port, Family0})
|
|
|
+ when is_integer(Port) andalso (Port >= 0) andalso (Port =< 65535) ->
|
|
|
+ [{IPAddress, Port, Family} ||
|
|
|
+ {IPAddress, Family} <- getaddr(Host, Family0)];
|
|
|
+tcp_listener_addresses({_Host, Port, _Family0}) ->
|
|
|
+ error_logger:error_msg("invalid port ~p - not 0..65535~n", [Port]),
|
|
|
+ throw({error, {invalid_port, Port}}).
|
|
|
+
|
|
|
+tcp_listener_addresses_auto(Port) ->
|
|
|
+ lists:append([tcp_listener_addresses(Listener) ||
|
|
|
+ Listener <- port_to_listeners(Port)]).
|
|
|
+
|
|
|
+tcp_listener_spec(NamePrefix, {IPAddress, Port, Family}, SocketOpts,
|
|
|
+ Protocol, Label, OnConnect) ->
|
|
|
+ {tcp_name(NamePrefix, IPAddress, Port),
|
|
|
+ {tcp_listener_sup, start_link,
|
|
|
+ [IPAddress, Port, [Family | SocketOpts],
|
|
|
+ {?MODULE, tcp_listener_started, [Protocol]},
|
|
|
+ {?MODULE, tcp_listener_stopped, [Protocol]},
|
|
|
+ OnConnect, Label]},
|
|
|
+ transient, infinity, supervisor, [tcp_listener_sup]}.
|
|
|
+
|
|
|
+
|
|
|
+tcp_listener_started(Protocol, IPAddress, Port) ->
|
|
|
+ %% We need the ip to distinguish e.g. 0.0.0.0 and 127.0.0.1
|
|
|
+ %% We need the host so we can distinguish multiple instances of the above
|
|
|
+ %% in a cluster.
|
|
|
+ error_logger:info_msg("tcp listener started: ~p ~p:~p", [Protocol, IPAddress, Port]).
|
|
|
+
|
|
|
+tcp_listener_stopped(Protocol, IPAddress, Port) ->
|
|
|
+ error_logger:info_msg("tcp listener stopped: ~p ~p:~p", [Protocol, IPAddress, Port]).
|
|
|
+
|
|
|
+start_client(Sock) ->
|
|
|
+ {ok, Client} = supervisor:start_child(emqtt_client_sup, []),
|
|
|
+ ok = gen_tcp:controlling_process(Sock, Client),
|
|
|
+ emqtt_client:go(Client, Sock),
|
|
|
+ Client.
|
|
|
+
|
|
|
+%%--------------------------------------------------------------------
|
|
|
+tcp_host({0,0,0,0}) ->
|
|
|
+ hostname();
|
|
|
+
|
|
|
+tcp_host({0,0,0,0,0,0,0,0}) ->
|
|
|
+ hostname();
|
|
|
+
|
|
|
+tcp_host(IPAddress) ->
|
|
|
+ case inet:gethostbyaddr(IPAddress) of
|
|
|
+ {ok, #hostent{h_name = Name}} -> Name;
|
|
|
+ {error, _Reason} -> ntoa(IPAddress)
|
|
|
+ end.
|
|
|
+
|
|
|
+hostname() ->
|
|
|
+ {ok, Hostname} = inet:gethostname(),
|
|
|
+ case inet:gethostbyname(Hostname) of
|
|
|
+ {ok, #hostent{h_name = Name}} -> Name;
|
|
|
+ {error, _Reason} -> Hostname
|
|
|
+ end.
|
|
|
+
|
|
|
+tcp_opts() ->
|
|
|
+ {ok, Opts} = application:get_env(emqtt, tcp_listen_options),
|
|
|
+ Opts.
|
|
|
+
|
|
|
+%% inet_parse:address takes care of ip string, like "0.0.0.0"
|
|
|
+%% inet:getaddr returns immediately for ip tuple {0,0,0,0},
|
|
|
+%% and runs 'inet_gethost' port process for dns lookups.
|
|
|
+%% On Windows inet:getaddr runs dns resolver for ip string, which may fail.
|
|
|
+getaddr(Host, Family) ->
|
|
|
+ case inet_parse:address(Host) of
|
|
|
+ {ok, IPAddress} -> [{IPAddress, resolve_family(IPAddress, Family)}];
|
|
|
+ {error, _} -> gethostaddr(Host, Family)
|
|
|
+ end.
|
|
|
+
|
|
|
+gethostaddr(Host, auto) ->
|
|
|
+ Lookups = [{Family, inet:getaddr(Host, Family)} || Family <- [inet, inet6]],
|
|
|
+ case [{IP, Family} || {Family, {ok, IP}} <- Lookups] of
|
|
|
+ [] -> host_lookup_error(Host, Lookups);
|
|
|
+ IPs -> IPs
|
|
|
+ end;
|
|
|
+
|
|
|
+gethostaddr(Host, Family) ->
|
|
|
+ case inet:getaddr(Host, Family) of
|
|
|
+ {ok, IPAddress} -> [{IPAddress, Family}];
|
|
|
+ {error, Reason} -> host_lookup_error(Host, Reason)
|
|
|
+ end.
|
|
|
+
|
|
|
+host_lookup_error(Host, Reason) ->
|
|
|
+ error_logger:error_msg("invalid host ~p - ~p~n", [Host, Reason]),
|
|
|
+ throw({error, {invalid_host, Host, Reason}}).
|
|
|
+
|
|
|
+resolve_family({_,_,_,_}, auto) -> inet;
|
|
|
+resolve_family({_,_,_,_,_,_,_,_}, auto) -> inet6;
|
|
|
+resolve_family(IP, auto) -> throw({error, {strange_family, IP}});
|
|
|
+resolve_family(_, F) -> F.
|
|
|
+
|
|
|
+%%--------------------------------------------------------------------
|
|
|
+
|
|
|
+%% There are three kinds of machine (for our purposes).
|
|
|
+%%
|
|
|
+%% * Those which treat IPv4 addresses as a special kind of IPv6 address
|
|
|
+%% ("Single stack")
|
|
|
+%% - Linux by default, Windows Vista and later
|
|
|
+%% - We also treat any (hypothetical?) IPv6-only machine the same way
|
|
|
+%% * Those which consider IPv6 and IPv4 to be completely separate things
|
|
|
+%% ("Dual stack")
|
|
|
+%% - OpenBSD, Windows XP / 2003, Linux if so configured
|
|
|
+%% * Those which do not support IPv6.
|
|
|
+%% - Ancient/weird OSes, Linux if so configured
|
|
|
+%%
|
|
|
+%% How to reconfigure Linux to test this:
|
|
|
+%% Single stack (default):
|
|
|
+%% echo 0 > /proc/sys/net/ipv6/bindv6only
|
|
|
+%% Dual stack:
|
|
|
+%% echo 1 > /proc/sys/net/ipv6/bindv6only
|
|
|
+%% IPv4 only:
|
|
|
+%% add ipv6.disable=1 to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub then
|
|
|
+%% sudo update-grub && sudo reboot
|
|
|
+%%
|
|
|
+%% This matters in (and only in) the case where the sysadmin (or the
|
|
|
+%% app descriptor) has only supplied a port and we wish to bind to
|
|
|
+%% "all addresses". This means different things depending on whether
|
|
|
+%% we're single or dual stack. On single stack binding to "::"
|
|
|
+%% implicitly includes all IPv4 addresses, and subsequently attempting
|
|
|
+%% to bind to "0.0.0.0" will fail. On dual stack, binding to "::" will
|
|
|
+%% only bind to IPv6 addresses, and we need another listener bound to
|
|
|
+%% "0.0.0.0" for IPv4. Finally, on IPv4-only systems we of course only
|
|
|
+%% want to bind to "0.0.0.0".
|
|
|
+%%
|
|
|
+%% Unfortunately it seems there is no way to detect single vs dual stack
|
|
|
+%% apart from attempting to bind to the port.
|
|
|
+port_to_listeners(Port) ->
|
|
|
+ IPv4 = {"0.0.0.0", Port, inet},
|
|
|
+ IPv6 = {"::", Port, inet6},
|
|
|
+ case ipv6_status(?FIRST_TEST_BIND_PORT) of
|
|
|
+ single_stack -> [IPv6];
|
|
|
+ ipv6_only -> [IPv6];
|
|
|
+ dual_stack -> [IPv6, IPv4];
|
|
|
+ ipv4_only -> [IPv4]
|
|
|
+ end.
|
|
|
+
|
|
|
+ipv6_status(TestPort) ->
|
|
|
+ IPv4 = [inet, {ip, {0,0,0,0}}],
|
|
|
+ IPv6 = [inet6, {ip, {0,0,0,0,0,0,0,0}}],
|
|
|
+ case gen_tcp:listen(TestPort, IPv6) of
|
|
|
+ {ok, LSock6} ->
|
|
|
+ case gen_tcp:listen(TestPort, IPv4) of
|
|
|
+ {ok, LSock4} ->
|
|
|
+ %% Dual stack
|
|
|
+ gen_tcp:close(LSock6),
|
|
|
+ gen_tcp:close(LSock4),
|
|
|
+ dual_stack;
|
|
|
+ %% Checking the error here would only let us
|
|
|
+ %% distinguish single stack IPv6 / IPv4 vs IPv6 only,
|
|
|
+ %% which we figure out below anyway.
|
|
|
+ {error, _} ->
|
|
|
+ gen_tcp:close(LSock6),
|
|
|
+ case gen_tcp:listen(TestPort, IPv4) of
|
|
|
+ %% Single stack
|
|
|
+ {ok, LSock4} -> gen_tcp:close(LSock4),
|
|
|
+ single_stack;
|
|
|
+ %% IPv6-only machine. Welcome to the future.
|
|
|
+ {error, eafnosupport} -> ipv6_only; %% Linux
|
|
|
+ {error, eprotonosupport}-> ipv6_only; %% FreeBSD
|
|
|
+ %% Dual stack machine with something already
|
|
|
+ %% on IPv4.
|
|
|
+ {error, _} -> ipv6_status(TestPort + 1)
|
|
|
+ end
|
|
|
+ end;
|
|
|
+ %% IPv4-only machine. Welcome to the 90s.
|
|
|
+ {error, eafnosupport} -> %% Linux
|
|
|
+ ipv4_only;
|
|
|
+ {error, eprotonosupport} -> %% FreeBSD
|
|
|
+ ipv4_only;
|
|
|
+ %% Port in use
|
|
|
+ {error, _} ->
|
|
|
+ ipv6_status(TestPort + 1)
|
|
|
+ end.
|
|
|
+
|
|
|
+ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
|
|
|
+ inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
|
|
|
+ntoa(IP) ->
|
|
|
+ inet_parse:ntoa(IP).
|
|
|
+
|
|
|
+ntoab(IP) ->
|
|
|
+ Str = ntoa(IP),
|
|
|
+ case string:str(Str, ":") of
|
|
|
+ 0 -> Str;
|
|
|
+ _ -> "[" ++ Str ++ "]"
|
|
|
+ end.
|
|
|
+
|
|
|
+tcp_name(Prefix, IPAddress, Port)
|
|
|
+ when is_atom(Prefix) andalso is_number(Port) ->
|
|
|
+ list_to_atom(
|
|
|
+ format("~w_~s:~w", [Prefix, inet_parse:ntoa(IPAddress), Port])).
|
|
|
+
|
|
|
+format(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)).
|
|
|
+
|
|
|
+
|
|
|
+
|