241 lines
8.6 KiB
Erlang
241 lines
8.6 KiB
Erlang
%%-----------------------------------------------------------------------------
|
|
%% Copyright (c) 2012-2015, Feng Lee <feng@emqtt.io>
|
|
%%
|
|
%% Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
%% of this software and associated documentation files (the "Software"), to deal
|
|
%% in the Software without restriction, including without limitation the rights
|
|
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
%% copies of the Software, and to permit persons to whom the Software is
|
|
%% furnished to do so, subject to the following conditions:
|
|
%%
|
|
%% The above copyright notice and this permission notice shall be included in all
|
|
%% copies or substantial portions of the Software.
|
|
%%
|
|
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
%% SOFTWARE.
|
|
%%------------------------------------------------------------------------------
|
|
|
|
-module(emqtt_net).
|
|
|
|
-author('feng@emqtt.io').
|
|
|
|
-export([tcp_name/3, tcp_host/1, getopts/2, setopts/2, getaddr/2, port_to_listeners/1]).
|
|
|
|
-export([peername/1, sockname/1, peer_string/1, connection_string/2]).
|
|
|
|
-include_lib("kernel/include/inet.hrl").
|
|
|
|
-define(FIRST_TEST_BIND_PORT, 10000).
|
|
|
|
%%--------------------------------------------------------------------
|
|
|
|
%% 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.
|
|
|
|
tcp_name(Prefix, IPAddress, Port)
|
|
when is_atom(Prefix) andalso is_number(Port) ->
|
|
list_to_atom(
|
|
lists:flatten(
|
|
io_lib:format(
|
|
"~w_~s:~w", [Prefix, inet_parse:ntoa(IPAddress), Port]))).
|
|
|
|
connection_string(Sock, Direction) ->
|
|
case socket_ends(Sock, Direction) of
|
|
{ok, {FromAddress, FromPort, ToAddress, ToPort}} ->
|
|
{ok, lists:flatten(
|
|
io_lib:format(
|
|
"~s:~p -> ~s:~p",
|
|
[maybe_ntoab(FromAddress), FromPort,
|
|
maybe_ntoab(ToAddress), ToPort]))};
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
socket_ends(Sock, Direction) ->
|
|
{From, To} = sock_funs(Direction),
|
|
case {From(Sock), To(Sock)} of
|
|
{{ok, {FromAddress, FromPort}}, {ok, {ToAddress, ToPort}}} ->
|
|
{ok, {rdns(FromAddress), FromPort,
|
|
rdns(ToAddress), ToPort}};
|
|
{{error, _Reason} = Error, _} ->
|
|
Error;
|
|
{_, {error, _Reason} = Error} ->
|
|
Error
|
|
end.
|
|
|
|
maybe_ntoab(Addr) when is_tuple(Addr) -> ntoab(Addr);
|
|
maybe_ntoab(Host) -> Host.
|
|
|
|
rdns(Addr) -> Addr.
|
|
|
|
sock_funs(inbound) -> {fun peername/1, fun sockname/1};
|
|
sock_funs(outbound) -> {fun sockname/1, fun peername/1}.
|
|
|
|
getopts(Sock, Options) when is_port(Sock) ->
|
|
inet:getopts(Sock, Options).
|
|
|
|
setopts(Sock, Options) when is_port(Sock) ->
|
|
inet:setopts(Sock, Options).
|
|
|
|
sockname(Sock) when is_port(Sock) -> inet:sockname(Sock).
|
|
|
|
peer_string(Sock) ->
|
|
case peername(Sock) of
|
|
{ok, {Addr, Port}} ->
|
|
{ok, lists:flatten(io_lib:format("~s:~p", [maybe_ntoab(Addr), Port]))};
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
peername(Sock) when is_port(Sock) -> inet:peername(Sock).
|
|
|
|
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_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.
|
|
|
|
|