src/gen_icmp.erl

%%% @copyright 2010-2023 Michael Santos <michael.santos@gmail.com>
%%% All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions
%%% are met:
%%%
%%% 1. Redistributions of source code must retain the above copyright notice,
%%% this list of conditions and the following disclaimer.
%%%
%%% 2. Redistributions in binary form must reproduce the above copyright
%%% notice, this list of conditions and the following disclaimer in the
%%% documentation and/or other materials provided with the distribution.
%%%
%%% 3. Neither the name of the copyright holder nor the names of its
%%% contributors may be used to endorse or promote products derived from
%%% this software without specific prior written permission.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
%%% "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
%%% LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
%%% A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
%%% HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
%%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
%%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
%%% PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
%%% LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
%%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-module(gen_icmp).
-behaviour(gen_server).
-include_lib("kernel/include/inet.hrl").
-include_lib("pkt/include/pkt.hrl").

-define(SERVER, ?MODULE).

-define(PING_TIMEOUT, 5000).

-export([
    open/0, open/1, open/2,
    close/1,
    send/3,
    controlling_process/2,
    setopts/2,
    family/1,
    getfd/1,
    set_ttl/3,
    get_ttl/2,

    filter/1, filter/2,
    icmp6_filter_setpassall/0,
    icmp6_filter_setblockall/0,
    icmp6_filter_setpass/2,
    icmp6_filter_setblock/2,
    icmp6_filter_willpass/2,
    icmp6_filter_willblock/2
]).
-export([recv/2, recv/3]).
-export([ping/1, ping/2, ping/3]).
-export([
    echo/3, echo/4,
    packet/2, packet/3,
    parse/1, parse/2,

    gettime/0,
    timediff/1, timediff/2
]).
-export([addr_list/3]).

-export([start_link/2, start/2]).
%% gen_server callbacks
-export([
    init/1,
    handle_call/3,
    handle_cast/2,
    handle_info/2,
    terminate/2,
    code_change/3
]).

-record(state, {
    % Protocol family (inet, inet6)
    family = inet,
    % caller PID
    pid,
    % socket file descriptor
    fd,
    % udp socket
    s
}).

-record(ping_opt, {
    s,
    id,
    sequence,
    timeout,
    tref,
    timestamp = true
}).

-record(icmp6_pseudohdr, {
    saddr = {0, 0, 0, 0, 0, 0, 0, 0},
    daddr = {0, 0, 0, 0, 0, 0, 0, 0},
    len = 0,
    next = ?IPPROTO_ICMPV6,
    h = #icmp6{}
}).

%%-------------------------------------------------------------------------
%%% API
%%-------------------------------------------------------------------------
%% @doc Open an ICMP socket
%%
%% By default, the ICMP socket is opened in {active,false} mode. No
%% packets will be received by the socket. setopts/2 can be used
%% to place the socket in {active,true} mode.
%%
%% See the procket README for the raw socket options and for
%% instructions on setting up the setuid helper.
%%
%% gen_icmp first attempts to natively open the socket and falls
%% back to forking the setuid helper program if beam does not have
%% the appropriate privileges.  Privileges to open a raw socket can
%% be given by, for example, running as root or, on Linux, granting
%% the CAP_NET_RAW capability to beam:
%%
%%     setcap cap_net_raw=ep /usr/local/lib/erlang/erts-5.8.3/bin/beam.smp
%%
%% Only the owning process will receive ICMP packets (see
%% controlling_process/2 to change the owner). The process owning the
%% raw socket will receive all ICMP packets sent to the host.
%%
%% Messages sent to the controlling process are:
%%
%% {icmp, Socket, Address, TTL, Packet}
%%
%% Where:
%%
%%     * Socket is the pid of the gen_icmp process
%%
%%     * Address is a tuple representing the IPv4 or IPv6 source address
%%
%%     * TTL: IPv4: TTL taken from the IP header
%%
%%     * TTL: IPv6: the socket's hop limit returned from
%%       getsockopt(IPV6_UNICAST_HOPS) (this is not the packet's
%%       TTL, it is the socket's max TTL)
%%
%%     * Packet is the complete ICMP packet including the ICMP headers
open() ->
    open([], []).
open(RawOpts) ->
    open(RawOpts, []).
open(RawOpts, SockOpts) ->
    start_link(RawOpts, SockOpts).

%% @doc Close the ICMP socket
close(Ref) when is_pid(Ref) ->
    gen_server:call(Ref, close, infinity).

%% @doc Send data via an ICMP socket
%%
%% Like the gen_udp and gen_tcp modules, any process can send ICMP
%% packets but only the owner will receive the responses.
send(Ref, Address, Packet) when is_pid(Ref) ->
    gen_server:call(Ref, {send, Address, Packet}, infinity).

%% @doc Read data from an ICMP socket
%%
%% This function receives a packet from a socket in passive mode.
%%
%% The optional Timeout parameter specifies a timeout in
%% milliseconds. The default value is infinity.
recv(Ref, Length) ->
    recv(Ref, Length, infinity).
recv(Ref, Length, Timeout) ->
    gen_server:call(Ref, {recv, Length, Timeout}, infinity).

%% @doc Change the controlling process of the ICMP socket
%%
%% Change the process owning the socket. Allows another process to
%% receive the ICMP responses.
controlling_process(Ref, Pid) when is_pid(Ref), is_pid(Pid) ->
    gen_server:call(Ref, {controlling_process, Pid}, infinity).

%% @doc Set socket options
%%
%% For options, see the inet man page. Simply calls inet:setopts/2 on
%% the gen_udp socket.
%%
%% setopts/2 can be used to toggle the socket between passive and
%% active mode:
%%
%% ```
%% {ok, Socket} = gen_icmp:open(), % socket is {active,false}
%% ok = gen_icmp:setopts(Socket, [{active, true}]),
%% % do stuff with the socket
%% ok = gen_icmp:setopts(Socket, [{active, false}]).
%% '''
setopts(Ref, Options) when is_pid(Ref), is_list(Options) ->
    gen_server:call(Ref, {setopts, Options}, infinity).

family(Ref) when is_pid(Ref) ->
    gen_server:call(Ref, family, infinity).

getfd(Ref) when is_pid(Ref) ->
    gen_server:call(Ref, getfd, infinity).

filter(Ref) when is_pid(Ref) ->
    gen_server:call(Ref, filter, infinity).
filter(Ref, Filter) when is_pid(Ref) ->
    gen_server:call(Ref, {filter, Filter}, infinity).

ping(Host) ->
    ping(Host, []).

ping(Host, Options) when is_tuple(Host) ->
    ping([Host], Options);
ping([Char | _] = Host, Options) when is_integer(Char) ->
    ping([Host], Options);
ping(Hosts, Options) ->
    {ok, Socket} = gen_icmp:open(Options),
    Res = ping(Socket, Hosts, Options),
    gen_icmp:close(Socket),
    Res.

%% @doc Send an ICMP ECHO_REQUEST
%%
%% ping/1 is a convenience function to send a single ping
%% packet. The argument to ping/1 can be either a hostname or a
%% list of hostnames.
%%
%% To prevent the process mailbox from being flooded with ICMP
%% messages, ping/3 will put the socket into {active,false} mode
%% after completing.
%%
%% The ping/3 function blocks until either an ICMP ECHO REPLY is
%% received from all hosts or Timeout is reached.
%%
%% Id and sequence are used to differentiate ping responses. Usually,
%% the sequence is incremented for each ping in one run.
%%
%% A list of responses is returned. If the ping was successful,
%% the elapsed time in milliseconds is included (calculated by
%% subtracting the current time from the time we sent in the ICMP
%% ECHO packet and returned to us in the ICMP ECHOREPLY payload)
%% where:
%%
%% * Host: the provided hostname
%%
%% * Address: the resolved IPv4 or IPv6 network address represented
%%   as a 4 or 8-tuple used in the ICMP echo request
%%
%% * ReplyAddr: the IPv4 or IPv6 network address originating the
%%   ICMP echo reply
%%
%% The timeout is set for all ICMP packets and is set after all
%% packets have been sent out.
%%
%% ping/1 and ping/2 open and close an ICMP socket for the user. For
%% best performance, ping/3 should be used instead, with the socket
%% being maintained between runs.
%%
%% By default only one address per hostname is pinged. To
%% enable pinging all addresses per hostname pass {multi, true}
%% to options.
%%
%% A ping payload contains an 8 byte timestamp in microseconds. When
%% creating a custom payload, the first 8 bytes of the ICMP echo
%% reply payload will be used for calculating the elapsed time. To
%% disable this behaviour, use the option {timestamp,false} (the
%% elapsed time in the return value will be set to 0).
%%
%% The timeout defaults to 5 seconds.
%%
%% ICMPv6 sockets can restrict which ICMPv6 types are received by the
%% socket using the filter option.  The filter argument is a binary
%% generated using the icmp6_filter functions described below.
%%
%% The default filter allows: ICMP6_ECHO_REPLY, ICMP6_DST_UNREACH,
%% ICMP6_PACKET_TOO_BIG, ICMP6_TIME_EXCEEDED and ICMP6_PARAM_PROB.
%% Note: ping/3 does not restore the original filter on the socket.
ping(Socket, Hosts, Options) when is_pid(Socket), is_list(Hosts), is_list(Options) ->
    ok = setopts(Socket, [{active, true}]),

    Family = family(Socket),

    Id = proplists:get_value(id, Options, erlang:phash2(self(), 16#FFFF)),
    Seq = proplists:get_value(sequence, Options, 0),
    Data = proplists:get_value(data, Options, payload(echo)),
    Timeout = proplists:get_value(timeout, Options, ?PING_TIMEOUT),
    Timestamp = proplists:get_value(timestamp, Options, true),
    Multi = proplists:get_value(multi, Options, false),

    ICMP6_filter = lists:foldl(
        fun(T, X) ->
            gen_icmp:icmp6_filter_setpass(T, X)
        end,
        gen_icmp:icmp6_filter_setblockall(),
        [
            echo_reply,
            dst_unreach,
            packet_too_big,
            time_exceeded,
            param_prob
        ]
    ),

    Filter = proplists:get_value(filter, Options, ICMP6_filter),

    ok =
        case Family of
            inet6 ->
                filter(Socket, Filter);
            _ ->
                ok
        end,

    Hosts2 = addr_list(Family, Hosts, Multi),

    {Addresses, Errors, _} = lists:foldl(
        fun
            ({ok, Host, Addr}, {NHosts, Nerr, NSeq}) ->
                {[{ok, Host, Addr, NSeq} | NHosts], Nerr, NSeq + 1};
            (Err, {NHosts, Nerr, NSeq}) ->
                {NHosts, [Err | Nerr], NSeq}
        end,
        {[], [], Seq},
        Hosts2
    ),

    Result =
        case Addresses of
            [] ->
                Errors;
            _ ->
                [
                    spawn(fun() ->
                        gen_icmp:send(Socket, Addr, gen_icmp:echo(Family, Id, S, Data))
                    end)
                 || {ok, _Host, Addr, S} <- Addresses
                ],
                {Timeouts, Replies} = ping_reply(Addresses, #ping_opt{
                    s = Socket,
                    id = Id,
                    timeout = Timeout,
                    timestamp = Timestamp
                }),
                Errors ++ Timeouts ++ Replies
        end,

    ok = setopts(Socket, [{active, false}]),
    flush_events(Socket),
    Result.

%%-------------------------------------------------------------------------
%%% Callbacks
%%-------------------------------------------------------------------------
start_link(RawOpts, SockOpts) ->
    Pid = self(),
    gen_server:start_link(?MODULE, [Pid, RawOpts, SockOpts], []).

start(RawOpts, SockOpts) ->
    Pid = self(),
    case gen_server:start(?MODULE, [Pid, RawOpts, SockOpts], []) of
        {ok, Socket} -> {ok, Socket};
        {error, Error} -> Error
    end.

init([Pid, RawOpts, SockOpts]) ->
    process_flag(trap_exit, true),

    {Protocol, Family} =
        case proplists:get_value(inet6, RawOpts, false) of
            false -> {icmp, inet};
            true -> {'ipv6-icmp', inet6}
        end,

    Result =
        case procket:socket(Family, raw, Protocol) of
            {error, eperm} ->
                procket:open(0, RawOpts ++ [{protocol, Protocol}, {type, raw}, {family, Family}]);
            N ->
                N
        end,

    init_1(Pid, Family, RawOpts, SockOpts, Result).

init_1(Pid, Family, RawOpts, SockOpts0, {ok, FD}) ->
    TTL = proplists:get_value(ttl, RawOpts),

    case TTL of
        undefined -> ok;
        _ -> set_ttl(FD, Family, TTL)
    end,

    SockOpts =
        case proplists:is_defined(active, SockOpts0) of
            true -> SockOpts0;
            false -> SockOpts0 ++ [{active, false}]
        end,

    case gen_udp:open(0, SockOpts ++ [binary, {fd, FD}, Family]) of
        {ok, Socket} ->
            {ok, #state{
                family = Family,
                pid = Pid,
                fd = FD,
                s = Socket
            }};
        Error ->
            Error
    end;
init_1(_Pid, _Family, _RawOpts, _SockOpts, Error) ->
    {stop, Error}.

handle_call(close, {Pid, _}, #state{pid = Pid, s = Socket} = State) ->
    {stop, normal, gen_udp:close(Socket), State};
handle_call({send, IP, Packet}, _From, #state{s = Socket} = State) ->
    {reply, gen_udp:send(Socket, IP, 0, Packet), State};
handle_call({recv, Length, Timeout}, {Pid, _}, #state{pid = Pid, s = Socket} = State) ->
    Reply =
        case gen_udp:recv(Socket, Length, Timeout) of
            {ok, {Address, _Port, Packet}} -> {ok, {Address, Packet}};
            N -> N
        end,
    {reply, Reply, State};
handle_call({controlling_process, Pid}, {Owner, _}, #state{pid = Owner} = State) ->
    {reply, ok, State#state{pid = Pid}};
handle_call({setopts, Options}, {Pid, _}, #state{pid = Pid, s = Socket} = State) ->
    {reply, inet:setopts(Socket, Options), State};
handle_call(family, _From, #state{family = Family} = State) ->
    {reply, Family, State};
handle_call(getfd, _From, #state{fd = Socket} = State) ->
    {reply, Socket, State};
handle_call(filter, _From, #state{family = inet6, fd = Socket} = State) ->
    Reply = procket:getsockopt(Socket, ?IPPROTO_ICMPV6, icmp6_filter(), <<0:256>>),
    {reply, Reply, State};
handle_call(filter, _From, State) ->
    {reply, unsupported, State};
handle_call({filter, Filter}, _From, #state{family = inet6, fd = Socket} = State) ->
    Reply = procket:setsockopt(Socket, ?IPPROTO_ICMPV6, icmp6_filter(), Filter),
    {reply, Reply, State};
handle_call({filter, _Filter}, _From, State) ->
    {reply, unsupported, State};
handle_call(Request, From, State) ->
    error_logger:info_report([{call, Request}, {from, From}, {state, State}]),
    {reply, error, State}.

handle_cast(Msg, State) ->
    error_logger:info_report([{cast, Msg}, {state, State}]),
    {noreply, State}.

% IPv4 ICMP
handle_info(
    {udp, Socket, {_, _, _, _} = Saddr, 0,
        <<4:4, HL:4, _ToS:8, _Len:16, _Id:16, 0:1, _DF:1, _MF:1, _Off:13, TTL:8, ?IPPROTO_ICMP:8,
            _Sum:16, _SA1:8, _SA2:8, _SA3:8, _SA4:8, _DA1:8, _DA2:8, _DA3:8, _DA4:8, Data/binary>>},
    #state{pid = Pid, s = Socket} = State
) ->
    N = (HL - 5) * 4,
    Opt =
        if
            N > 0 -> N;
            true -> 0
        end,

    <<_:Opt/bits, Payload/bits>> = Data,
    Pid ! {icmp, self(), Saddr, TTL, Payload},
    {noreply, State};
% IPv6 ICMP
handle_info(
    {udp, Socket, {_, _, _, _, _, _, _, _} = Saddr, 0, Data},
    #state{pid = Pid, fd = FD, s = Socket} = State
) ->
    {ok, TTL} = get_ttl(FD, inet6),
    Pid ! {icmp, self(), Saddr, TTL, Data},
    {noreply, State};
handle_info({'EXIT', _, normal}, State) ->
    {noreply, State};
handle_info(Info, State) ->
    error_logger:info_report([{info, Info}, {state, State}]),
    {noreply, State}.

terminate(_Reason, #state{fd = Socket}) ->
    procket:close(Socket),
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%-------------------------------------------------------------------------
%%% Utility Functions
%%-------------------------------------------------------------------------

%% Create an ICMP packet
packet(#icmp{} = Header, Payload) when is_binary(Payload) ->
    Sum = pkt:makesum(
        list_to_binary([
            pkt:icmp(Header),
            Payload
        ])
    ),
    list_to_binary([
        pkt:icmp(Header#icmp{checksum = Sum}),
        Payload
    ]);
packet(
    #icmp6_pseudohdr{
        saddr = {SA1, SA2, SA3, SA4, SA5, SA6, SA7, SA8},
        daddr = {DA1, DA2, DA3, DA4, DA5, DA6, DA7, DA8},
        len = Len,
        next = Next,
        h = Header
    },
    Payload
) when is_binary(Payload) ->
    Sum = pkt:makesum(
        list_to_binary([
            <<SA1, SA2, SA3, SA4, SA5, SA6, SA7, SA8, DA1, DA2, DA3, DA4, DA5, DA6, DA7, DA8,
                Len:32, 0:24, Next:8>>,
            pkt:icmp6(Header),
            Payload
        ])
    ),
    list_to_binary([
        pkt:icmp6(Header#icmp6{checksum = Sum}),
        Payload
    ]);
packet(Header, Payload) ->
    packet(inet, Header, Payload).

% IPv4 ICMP packet
packet(inet, Header, Payload) when is_list(Header), is_binary(Payload) ->
    Default = #icmp{},

    Type = icmp_message:type_to_uint8(
        proplists:get_value(type, Header, Default#icmp.type)
    ),
    Code = icmp_message:code_to_uint8(
        proplists:get_value(code, Header, Default#icmp.code)
    ),

    Id = proplists:get_value(id, Header, Default#icmp.id),
    Seq = proplists:get_value(sequence, Header, Default#icmp.sequence),
    GW = proplists:get_value(gateway, Header, Default#icmp.gateway),
    UN = proplists:get_value(un, Header, Default#icmp.un),
    MTU = proplists:get_value(mtu, Header, Default#icmp.mtu),
    Pointer = proplists:get_value(pointer, Header, Default#icmp.pointer),
    TS_orig = proplists:get_value(ts_orig, Header, Default#icmp.ts_orig),
    TS_recv = proplists:get_value(ts_recv, Header, Default#icmp.ts_recv),
    TS_tx = proplists:get_value(ts_tx, Header, Default#icmp.ts_tx),

    ICMP = #icmp{
        type = Type,
        code = Code,
        id = Id,
        sequence = Seq,
        gateway = GW,
        un = UN,
        mtu = MTU,
        pointer = Pointer,
        ts_orig = TS_orig,
        ts_recv = TS_recv,
        ts_tx = TS_tx
    },
    packet(ICMP, Payload);
% IPv6 ICMP packet
packet(inet6, Header, Payload) when is_list(Header), is_binary(Payload) ->
    Default = #icmp6{},

    Type = icmp6_message:type_to_uint8(
        proplists:get_value(type, Header, Default#icmp6.type)
    ),
    Code = icmp6_message:code_to_uint8(
        proplists:get_value(code, Header, Default#icmp6.code)
    ),

    Id = proplists:get_value(id, Header, Default#icmp6.id),
    Seq = proplists:get_value(sequence, Header, Default#icmp6.seq),
    UN = proplists:get_value(un, Header, Default#icmp6.un),
    MTU = proplists:get_value(mtu, Header, Default#icmp6.mtu),
    Pointer = proplists:get_value(pointer, Header, Default#icmp6.pptr),
    Maxdelay = proplists:get_value(maxdelay, Header, Default#icmp6.maxdelay),

    % IPv6 pseudoheader
    Saddr = proplists:get_value(saddr, Header, {0, 0, 0, 0, 0, 0, 0, 0}),
    Daddr = proplists:get_value(daddr, Header, {0, 0, 0, 0, 0, 0, 0, 0}),
    Len = proplists:get_value(len, Header, 0),
    Next = proplists:get_value(next, Header, ?IPPROTO_ICMPV6),

    Pseudo = #icmp6_pseudohdr{
        saddr = Saddr,
        daddr = Daddr,
        len = Len,
        next = Next,
        h = #icmp6{
            type = Type,
            code = Code,
            id = Id,
            seq = Seq,
            un = UN,
            mtu = MTU,
            pptr = Pointer,
            maxdelay = Maxdelay
        }
    },

    packet(Pseudo, Payload).

echo(Family, Id, Seq) ->
    % Pad packet to 64 bytes
    echo(Family, Id, Seq, payload(echo)).
echo(Family, Id, Seq, Payload) when
    is_integer(Id),
    Id >= 0,
    Id < 16#FFFF,
    is_integer(Seq),
    Seq >= 0,
    Seq < 16#FFFF,
    is_binary(Payload)
->
    Echo =
        case Family of
            inet -> echo;
            inet6 -> echo_request
        end,

    packet(
        Family,
        [
            {type, Echo},
            {id, Id},
            {sequence, Seq}
        ],
        Payload
    ).

% Default ICMP echo payload
payload(echo) ->
    USec = gettime(),
    <<USec:8/signed-integer-unit:8, (list_to_binary(lists:seq($\s, $O)))/binary>>.

% Set the TTL on a socket
set_ttl(FD, inet, TTL) ->
    procket:setsockopt(FD, ?IPPROTO_IP, ip_ttl(), <<TTL:32/native>>);
set_ttl(FD, inet6, TTL) ->
    procket:setsockopt(FD, ?IPPROTO_IPV6, ipv6_unicast_hops(), <<TTL:32/native>>).

% Get the socket TTL
get_ttl(FD, inet) ->
    case procket:getsockopt(FD, ?IPPROTO_IP, ip_ttl(), <<0:32>>) of
        {ok, <<TTL:32/native>>} -> {ok, TTL};
        Error -> Error
    end;
get_ttl(FD, inet6) ->
    case procket:getsockopt(FD, ?IPPROTO_IPV6, ipv6_unicast_hops(), <<0:32>>) of
        {ok, <<TTL:32/native>>} -> {ok, TTL};
        Error -> Error
    end.

%%-------------------------------------------------------------------------
%%% Internal Functions
%%-------------------------------------------------------------------------

%%
%% ping
%%
addr_list(Family, Hosts, Multi) ->
    lists:flatmap(
        fun(Host) ->
            case parse(Family, Host) of
                {ok, IPs} when Multi == true ->
                    [{ok, Host, IP} || IP <- IPs];
                {ok, [IP | _]} ->
                    [{ok, Host, IP}];
                {error, Error} ->
                    [{error, Error, Host, undefined}]
            end
        end,
        Hosts
    ).

parse(Addr) ->
    parse(inet, Addr).

parse(Family, Addr) when is_list(Addr) ->
    parse_or_resolve(Family, Addr, inet_parse:address(Addr));
parse(_Family, Addr) when is_tuple(Addr) ->
    {ok, [Addr]}.

parse_or_resolve(_Family, _Addr, {ok, IP}) ->
    {ok, [IP]};
parse_or_resolve(Family, Addr, {error, einval}) ->
    case inet:gethostbyname(Addr, Family) of
        {ok, #hostent{h_addr_list = IPs}} ->
            {ok, IPs};
        Error ->
            Error
    end.

ping_reply(Hosts, #ping_opt{s = Socket, timeout = Timeout} = Opt) ->
    Pid = self(),
    TRef =
        case Timeout of
            infinity ->
                infinity;
            _ ->
                erlang:send_after(Timeout, Pid, {icmp, Socket, timeout})
        end,
    ping_loop(Hosts, [], Opt#ping_opt{tref = TRef}).

cancel_timeout(infinity) ->
    false;
cancel_timeout(TRef) ->
    erlang:cancel_timer(TRef).

ping_loop([], Acc, #ping_opt{tref = TRef}) ->
    cancel_timeout(TRef),
    {[], Acc};
ping_loop(
    Hosts,
    Acc,
    #ping_opt{
        tref = TRef,
        s = Socket,
        id = Id,
        timestamp = Timestamp
    } = Opt
) ->
    receive
        % IPv4 ICMP Echo Reply
        {icmp, Socket, {_, _, _, _} = Reply, TTL,
            <<?ICMP_ECHOREPLY:8, 0:8, _Checksum:16, Id:16, Seq:16, Data/binary>>} ->
            {Elapsed, Payload} =
                case Timestamp of
                    true ->
                        <<USec:8/signed-integer-unit:8, Data1/binary>> = Data,
                        {timediff(USec) div 1000, Data1};
                    false ->
                        {0, Data}
                end,
            {Hosts2, Result} =
                case lists:keytake(Seq, 4, Hosts) of
                    {value, {ok, Addr, Address, Seq}, NHosts} ->
                        {NHosts, [
                            {ok, Addr, Address, Reply, {Id, Seq, TTL, Elapsed}, Payload} | Acc
                        ]};
                    false ->
                        {Hosts, Acc}
                end,
            ping_loop(Hosts2, Result, Opt);
        % IPv4 ICMP Error
        {icmp, Socket, {_, _, _, _} = Reply, TTL,
            <<Type:8, Code:8, _Checksum1:16, _Unused:32, 4:4, 5:4, _ToS:8, _Len:16, _Id:16, 0:1,
                _DF:1, _MF:1, _Off:13, _TTL:8, ?IPPROTO_ICMP:8, _Sum:16, _SA1:8, _SA2:8, _SA3:8,
                _SA4:8, DA1:8, DA2:8, DA3:8, DA4:8, ?ICMP_ECHO:8, 0:8, _Checksum2:16, Id:16, Seq:16,
                _/binary>> = Data} ->
            <<_ICMPHeader:8/bytes, Payload/binary>> = Data,
            DA = {DA1, DA2, DA3, DA4},
            {Hosts2, Result} =
                case lists:keytake(Seq, 4, Hosts) of
                    {value, {ok, Addr, DA, Seq}, NHosts} ->
                        {NHosts, [
                            {error, icmp_message:code({Type, Code}), Addr, DA, Reply,
                                {Id, Seq, TTL, undefined}, Payload}
                            | Acc
                        ]};
                    false ->
                        {Hosts, Acc}
                end,
            ping_loop(Hosts2, Result, Opt);
        % IPv6 ICMP Echo Reply
        {icmp, Socket, {_, _, _, _, _, _, _, _} = Reply, TTL,
            <<?ICMP6_ECHO_REPLY:8, 0:8, _Checksum:16, Id:16, Seq:16, Data/binary>>} ->
            {Elapsed, Payload} =
                case Timestamp of
                    true ->
                        <<USec:8/signed-integer-unit:8, Data1/binary>> = Data,
                        {timediff(USec) div 1000, Data1};
                    false ->
                        {0, Data}
                end,
            {Hosts2, Result} =
                case lists:keytake(Seq, 4, Hosts) of
                    {value, {ok, Addr, Address, Seq}, NHosts} ->
                        {NHosts, [
                            {ok, Addr, Address, Reply, {Id, Seq, TTL, Elapsed}, Payload} | Acc
                        ]};
                    false ->
                        {Hosts, Acc}
                end,
            ping_loop(Hosts2, Result, Opt);
        % IPv6 ICMP Error
        {icmp, Socket, {_, _, _, _, _, _, _, _} = Reply, TTL,
            <<Type:8, Code:8, _Checksum1:16, _Unused:32, 6:4, _Class:8, _Flow:20, _Len:16,
                ?IPPROTO_ICMPV6:8, _Hop:8, _SA1:16, _SA2:16, _SA3:16, _SA4:16, _SA5:16, _SA6:16,
                _SA7:16, _SA8:16, DA1:16, DA2:16, DA3:16, DA4:16, DA5:16, DA6:16, DA7:16, DA8:16,
                ?ICMP6_ECHO_REQUEST:8, 0:8, _Checksum2:16, Id:16, Seq:16, _/binary>> = Data} ->
            <<_ICMPHeader:8/bytes, Payload/binary>> = Data,
            DA = {DA1, DA2, DA3, DA4, DA5, DA6, DA7, DA8},
            {value, {ok, Addr, DA, Seq}, Hosts2} = lists:keytake(Seq, 4, Hosts),
            {Hosts2, Result} =
                case lists:keytake(Seq, 4, Hosts) of
                    {value, {ok, Addr, DA, Seq}, NHosts} ->
                        {NHosts, [
                            {error, icmp_message:code({Type, Code}), Addr, DA, Reply,
                                {Id, Seq, TTL, undefined}, Payload}
                            | Acc
                        ]};
                    false ->
                        {Hosts, Acc}
                end,
            ping_loop(Hosts2, Result, Opt);
        % IPv4/IPv6 timeout on socket
        {icmp, Socket, timeout} ->
            cancel_timeout(TRef),
            Timeouts = [{error, timeout, Addr, IP} || {ok, Addr, IP, _Seq} <- Hosts],
            {Timeouts, Acc}
    end.

% TTL
ip_ttl() ->
    case os:type() of
        {unix, linux} -> 2;
        {unix, _} -> 4
    end.

ipv6_unicast_hops() ->
    case os:type() of
        {unix, linux} -> 16;
        {unix, _} -> 4
    end.

icmp6_filter() ->
    case erlang:system_info(os_type) of
        {unix, linux} ->
            1;
        {unix, _} ->
            18
    end.

% IPv6 ICMP filtering
%
% Linux reverses the meaining macros in RFC3542
icmp6_filter_setpassall() ->
    case erlang:system_info(os_type) of
        {unix, linux} ->
            <<0:256>>;
        {unix, _} ->
            <<16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff,
                16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff,
                16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff>>
    end.

icmp6_filter_setblockall() ->
    case erlang:system_info(os_type) of
        {unix, linux} ->
            <<16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff,
                16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff,
                16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff, 16#ff>>;
        {unix, _} ->
            <<0:256>>
    end.

%#define ICMP6_FILTER_SETPASS(type, filterp) \
%            (((filterp)->icmp6_filt[(type) >> 5]) |= (1 << ((type) & 31)))
icmp6_filter_setpass(Type0, <<_:256>> = Filter) ->
    Type = icmp6_message:type_to_uint8(Type0),
    Offset = Type bsr 5,
    Value = 1 bsl (Type band 31),
    Fun =
        case erlang:system_info(os_type) of
            {unix, linux} ->
                fun(N) -> N band bnot Value end;
            {unix, _} ->
                fun(N) -> N bor Value end
        end,
    array_set(Offset, Fun, Filter).

%#define ICMP6_FILTER_SETBLOCK(type, filterp) \
%            (((filterp)->icmp6_filt[(type) >> 5]) &= ~(1 << ((type) & 31)))
icmp6_filter_setblock(Type0, <<_:256>> = Filter) ->
    Type = icmp6_message:type_to_uint8(Type0),
    Offset = Type bsr 5,
    Value = 1 bsl (Type band 31),
    Fun =
        case erlang:system_info(os_type) of
            {unix, linux} ->
                fun(N) -> N bor Value end;
            {unix, _} ->
                fun(N) -> N band bnot Value end
        end,
    array_set(Offset, Fun, Filter).

%#define ICMP6_FILTER_WILLPASS(type, filterp) \
%            ((((filterp)->icmp6_filt[(type) >> 5]) & (1 << ((type) & 31))) != 0)
icmp6_filter_willpass(Type0, <<_:256>> = Filter) ->
    Type = icmp6_message:type_to_uint8(Type0),
    Offset = Type bsr 5,
    Value = 1 bsl (Type band 31),
    El = array_get(Offset, Filter),
    case erlang:system_info(os_type) of
        {unix, linux} -> El band Value =:= 0;
        {unix, _} -> El band Value =/= 0
    end.

%#define ICMP6_FILTER_WILLBLOCK(type, filterp) \
%            ((((filterp)->icmp6_filt[(type) >> 5]) & (1 << ((type) & 31))) == 0)
icmp6_filter_willblock(Type0, <<_:256>> = Filter) ->
    Type = icmp6_message:type_to_uint8(Type0),
    Offset = Type bsr 5,
    Value = 1 bsl (Type band 31),
    El = array_get(Offset, Filter),
    case erlang:system_info(os_type) of
        {unix, linux} -> El band Value =/= 0;
        {unix, _} -> El band Value =:= 0
    end.

% Offset starts at 0
array_set(Offset, Fun, Bin) ->
    Array = array:from_list([N || <<N:4/native-unsigned-integer-unit:8>> <= Bin]),
    Value = Fun(array:get(Offset, Array)),
    <<
        <<N:4/native-unsigned-integer-unit:8>>
     || N <- array:to_list(array:set(Offset, Value, Array))
    >>.

array_get(Offset, Bin) ->
    Array = array:from_list([N || <<N:4/native-unsigned-integer-unit:8>> <= Bin]),
    array:get(Offset, Array).

flush_events(Ref) ->
    receive
        {icmp, Ref, _Addr, _TTL, _Data} ->
            flush_events(Ref)
    after 0 -> ok
    end.

gettime() ->
    try erlang:monotonic_time(micro_seconds) of
        N ->
            N
    catch
        error:undef ->
            timestamp_to_microseconds(os:timestamp())
    end.

timediff(T) ->
    timediff(gettime(), T).

timediff(T1, T2) ->
    T1 - T2.

timestamp_to_microseconds({MegaSecs, Secs, MicroSecs}) ->
    (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs.