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.

%% @doc gen_icmp is an interface for using ICMP and ICMPv6 sockets
%%
%% gen_icmp uses raw sockets and abuses gen_udp for the socket
%% handling. gen_icmp should work on Linux and BSDs.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:ping("hex.pm").
%% [{ok,"hex.pm",
%%      {35,241,14,39},
%%      {35,241,14,39},
%%      {61261,0,116,215},
%%      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO">>}]
%% '''
-module(gen_icmp).
-behaviour(gen_server).
-include_lib("kernel/include/inet.hrl").
-include_lib("pkt/include/pkt.hrl").

-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
]).

-type uint8_t() :: 0..16#ff.
-type uint16_t() :: 0..16#ffff.
-type uint32_t() :: 0..16#ffffffff.

-type int32_t() :: -16#7fffffff..16#7fffffff.

-type fd() :: int32_t().
-type socket() :: pid().
-type icmp6_filter() :: <<_:256>>.

% ping details
-type id() :: uint16_t().
-type sequence() :: uint16_t().
-type ttlx() :: uint8_t().
% ICMP packet time to live (renamed to avoid conflict with ttl() defined in the pkt header file)
-type elapsed() :: int32_t() | undefined.

-type packet_option() ::
    {type, atom | uint8_t()}
    | {code, atom | uint8_t()}
    | {id, id()}
    | {sequence, sequence()}
    | {gateway, pkt:in_addr()}
    | {un, binary()}
    | {mtu, uint16_t()}
    | {pointer, uint8_t() | uint32_t()}
    | {ts_orig, uint32_t()}
    | {ts_recv, uint32_t()}
    | {ts_tx, uint32_t()}
    | {maxdelay, uint16_t()}
    | {saddr, pkt:in6_addr()}
    | {daddr, pkt:in6_addr()}
    | {len, non_neg_integer()}
    | {next, uint8_t()}.

-export_type([
    uint8_t/0,
    uint16_t/0,
    uint32_t/0,

    int32_t/0,

    fd/0,
    socket/0,
    icmp6_filter/0,

    id/0,
    sequence/0,
    ttlx/0,
    elapsed/0,
    packet_option/0
]).

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

-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{}
}).

%% @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.
%%
%% 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
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:open().
%% {ok,<0.299.0>}
%% '''
-spec open() -> {ok, socket()} | {error, system_limit | inet:posix()}.
open() ->
    open([], []).

%% @doc Open an ICMP socket with raw socket options.
%%
%% See the https://github.com/msantos/procket for the raw socket options
%% and for instructions on setting up the setuid helper.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:open([{ttl, 1}, inet6]).
%% {ok,<0.302.0>}
%% '''
-spec open(proplists:proplist()) -> {ok, socket()} | {error, system_liimt | inet:posix()}.
open(RawOpts) ->
    open(RawOpts, []).

%% @doc Open an ICMP socket with options.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:open([{ttl, 1}, inet6], [list]).
%% {ok,<0.302.0>}
%% '''
-spec open(proplists:proplist(), [inet:inet_backend() | gen_udp:open_option()]) ->
    {ok, socket()} | {error, system_liimt | inet:posix()}.
open(RawOpts, SockOpts) ->
    start_link(RawOpts, SockOpts).

%% @doc Close the ICMP socket.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, S} = gen_icmp:open().
%% {ok,<0.224.0>}
%% 2> gen_icmp:close(S).
%% ok
%% '''
-spec close(socket()) -> ok.
close(Socket) when is_pid(Socket) ->
    gen_server:call(Socket, 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.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, S} = gen_icmp:open().
%% {ok,<0.224.0>}
%% 2>gen_icmp:send(S, "google.com", gen_icmp:echo(inet, 0, 0)).
%% ok
%% 3> gen_icmp:recv(S, 1).
%% {ok,{{142,251,32,78},
%%      <<69,0,0,84,0,0,0,0,116,1,214,38,142,251,32,78,100,115,
%%             92,198,0,0,19,125,0,...>>}}
%% 4> gen_icmp:close(S).
%% ok
%% '''
-spec send(
    socket(),
    {inet:ip_address(), inet:port_number()}
    | inet:family_address()
    | socket:sockaddr_in()
    | socket:sockaddr_in6(),
    iodata()
) -> ok | {error, not_owner | inet:posix()}.
send(Socket, Address, Packet) when is_pid(Socket) ->
    gen_server:call(Socket, {send, Address, Packet}, infinity).

%% @doc Read data from an ICMP socket.
%%
%% This function receives a packet from a socket in passive mode.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, S} = gen_icmp:open().
%% {ok,<0.224.0>}
%% 2>gen_icmp:send(S, "google.com", gen_icmp:echo(inet, 0, 0)).
%% ok
%% 3> gen_icmp:recv(S, 1).
%% {ok,{{142,251,32,78},
%%      <<69,0,0,84,0,0,0,0,116,1,214,38,142,251,32,78,100,115,
%%             92,198,0,0,19,125,0,...>>}}
%% 4> gen_icmp:close(S).
%% ok
%% '''
-spec recv(socket(), non_neg_integer()) ->
    {ok, inet:ip_address() | inet:returned_non_ip_address(), string() | binary()}
    | {error, not_owner | timeout | inet:posix()}.
recv(Socket, Length) ->
    recv(Socket, Length, infinity).

%% @doc Read data from an ICMP socket with timeout.
%%
%% The optional Timeout parameter specifies a timeout in
%% milliseconds. The default value is infinity.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, S} = gen_icmp:open().
%% {ok,<0.299.0>}
%% 2> gen_icmp:send(S, "google.com", gen_icmp:echo(inet, 0, 0)).
%% ok
%% 3> gen_icmp:recv(S, 1, 5000).
%% {ok,{{142,251,41,78},
%%           <<69,0,0,84,0,0,0,0,116,1,205,38,142,251,41,78,100,115,
%%                    92,198,0,0,100,77,0,...>>}}
%% 4> gen_icmp:recv(S, 1, 5000).
%% {error,timeout}
%% 5> gen_icmp:close(S).
%% ok
%% '''
-spec recv(socket(), non_neg_integer(), timeout()) ->
    {ok, inet:ip_address() | inet:returned_non_ip_address(), string() | binary()}
    | {error, not_owner | timeout | inet:posix()}.
recv(Socket, Length, Timeout) ->
    gen_server:call(Socket, {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.
-spec controlling_process(socket(), pid()) -> ok.
controlling_process(Socket, Pid) when is_pid(Socket), is_pid(Pid) ->
    gen_server:call(Socket, {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.
%%
%% == Examples ==
%%
%% ```
%% {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}]).
%% '''
-spec setopts(socket(), [inet:socket_setopt()]) -> ok | {error, inet:posix()}.
setopts(Socket, Options) when is_pid(Socket), is_list(Options) ->
    gen_server:call(Socket, {setopts, Options}, infinity).

%% @doc Get socket family.
%%
%% Returns the socket family: `inet' (IPv4), `inet6' (IPv6)
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, S} = gen_icmp:open([inet6]).
%% {ok,<0.224.0>}
%% 2> gen_icmp:family(S).
%% inet6
%% '''
-spec family(socket()) -> inet | inet6.
family(Socket) when is_pid(Socket) ->
    gen_server:call(Socket, family, infinity).

%% @doc Get underlying file descriptor for socket.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, S} = gen_icmp:open().
%% {ok,<0.221.0>}
%% 2> gen_icmp:getfd(S).
%% 20
%% '''
-spec getfd(socket()) -> fd().
getfd(Socket) when is_pid(Socket) ->
    gen_server:call(Socket, getfd, infinity).

%% @doc Get ICMPv6 socket filter.
%%
%% Retrieves the ICMPv6 filter for a socket. For ICMPv4
%% sockets, the atom `unsupported' is returned.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, S} = gen_icmp:open([inet6]).
%% {ok,<0.299.0>}
%% 2> gen_icmp:filter(S).
%% {ok,<<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
%%       0,0,...>>}
%% '''
-spec filter(socket()) -> {ok, icmp6_filter()} | {error, unsupported | inet:posix()}.
filter(Socket) when is_pid(Socket) ->
    gen_server:call(Socket, filter, infinity).

%% @doc Set ICMPv6 socket filter.
%%
%% Sets an ICMPv6 filter on a socket. For ICMPv4 sockets, the atom
%% `unsupported' is returned.
%%
%% Filters can be generated by using the icmp6_filter functions.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, S} = gen_icmp:open([inet6]).
%% {ok,<0.299.0>}
%% 2> gen_icmp:filter(S, gen_icmp:icmp6_filter_setpassall()).
%% ok
%% '''
-spec filter(socket(), icmp6_filter()) -> ok | {error, unsupported | inet:posix()}.
filter(Socket, Filter) when is_pid(Socket) ->
    gen_server:call(Socket, {filter, Filter}, infinity).

%% @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.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:ping("google.com").
%% [{ok,"google.com",
%%      {142,251,41,46},
%%      {142,251,41,46},
%%      {61261,0,116,84},
%%      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO">>}]
%% '''
%%
%% @see ping/3
-spec ping(inet:socket_address() | inet:hostname() | [inet:socket_address() | inet:hostname()]) ->
    [
        {ok, inet:socket_address() | inet:hostname(), inet:ip_address(), inet:ip_address(),
            {id(), sequence(), ttlx(), elapsed()}, binary()}
        | {error, unreach_host | timxceed_intrans, [inet:socket_address() | inet:hostname()],
            inet:ip_address(), inet:ip_address(), {id(), sequence(), ttlx(), elapsed()}, binary()}
        | {error, timeout | inet:posix(), [inet:socket_address() | inet:hostname()],
            inet:ip_address()}
    ].
ping(Host) ->
    ping(Host, []).

%% @doc Send an ICMP ECHO_REQUEST with options.
%%
%% Ping a host or a list of hosts.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:ping("google.com", [inet6]).
%% [{ok,"google.com",
%%      {9735,63664,16395,2052,0,0,0,8206},
%%      {9735,63664,16395,2052,0,0,0,8206},
%%      {61261,0,64,53},
%%      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO">>}]
%% '''
%%
%% @see ping/3
-spec ping(
    inet:socket_address() | inet:hostname() | [inet:socket_address() | inet:hostname()],
    proplists:proplist()
) ->
    [
        {ok, inet:socket_address() | inet:hostname(), inet:ip_address(), inet:ip_address(),
            {id(), sequence(), ttlx(), elapsed()}, binary()}
        | {error, unreach_host | timxceed_intrans, [inet:socket_address() | inet:hostname()],
            inet:ip_address(), inet:ip_address(), {id(), sequence(), ttlx(), elapsed()}, binary()}
        | {error, timeout | inet:posix(), [inet:socket_address() | inet:hostname()],
            inet:ip_address()}
    ].
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.
%%
%% 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.
%%
%% 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.
%%
%% == Examples ==
%%
%% ```
%% 1> {ok, S} = gen_icmp:open([inet6]).
%% {ok,<0.299.0>}
%% 2> gen_icmp:ping(S, ["google.com"], []).
%% [{ok,"google.com",
%%      {9735,63664,16395,2051,0,0,0,8206},
%%      {9735,63664,16395,2051,0,0,0,8206},
%%      {61261,0,64,110},
%%      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO">>}]
%% 3> gen_icmp:ping(S, ["2001:4860:4860::8888"], []).
%% [{ok,"2001:4860:4860::8888",
%%      {8193,18528,18528,0,0,0,0,34952},
%%      {8193,18528,18528,0,0,0,0,34952},
%%      {61261,0,64,20},
%%      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO">>}]
%% 4> gen_icmp:ping(S, ["2001:4860:4860::8888", "google.com"], []).
%% [{ok,"google.com",
%%      {9735,63664,16395,2051,0,0,0,8206},
%%      {9735,63664,16395,2051,0,0,0,8206},
%%      {61261,1,64,23},
%%      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO">>},
%%  {ok,"2001:4860:4860::8888",
%%      {8193,18528,18528,0,0,0,0,34952},
%%      {8193,18528,18528,0,0,0,0,34952},
%%      {61261,0,64,23},
%%      <<" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNO">>}]
%% 5> gen_icmp:close(S).
%% ok
%% '''
-spec ping(socket(), [inet:socket_address() | inet:hostname()], proplists:proplist()) ->
    [
        {ok, inet:socket_address() | inet:hostname(), inet:ip_address(), inet:ip_address(),
            {id(), sequence(), ttlx(), elapsed()}, binary()}
        | {error, unreach_host | timxceed_intrans, [inet:socket_address() | inet:hostname()],
            inet:ip_address(), inet:ip_address(), {id(), sequence(), ttlx(), elapsed()}, binary()}
        | {error, timeout | inet:posix(), [inet:socket_address() | inet:hostname()],
            inet:ip_address()}
    ].
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
%%-------------------------------------------------------------------------
%% @private
start_link(RawOpts, SockOpts) ->
    Pid = self(),
    gen_server:start_link(?MODULE, [Pid, RawOpts, SockOpts], []).

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

%% @private
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}.

%% @private
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}.

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

%% @private
% 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}.

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

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

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

%% @doc Create an ICMP packet.
%%
%% Convenience function for creating arbitrary ICMP packets. This
%% function will calculate the ICMP checksum and insert it into the
%% packet.
%%
%% == Examples ==
%%
%% ```
%% 1> rr("_build/default/lib/pkt/include/pkt_icmp.hrl").
%% [icmp]
%% 2> gen_icmp:packet(#icmp{}, <<"abc">>).
%% <<8,0,51,157,0,0,0,0,97,98,99>>
%% '''
-spec packet(#icmp{} | #icmp6_pseudohdr{} | [packet_option()], binary()) -> binary().
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) when is_list(Header) ->
    packet(inet, Header, Payload).

%% @doc Construct an ICMP or ICMPv6 packet payload.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:packet(inet, [{type, dest_unreach}, {code, unreach_port}], <<"payload goes here">>).
%% <<3,3,135,239,0,0,0,0,112,97,121,108,111,97,100,32,103,
%%   111,101,115,32,104,101,114,101>>
%% '''
-spec packet(inet | inet6, [packet_option()], binary()) -> binary().
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).

%% @doc Generate ICMP ECHO_REQUEST payload.
%%
%% Creates an ICMP echo packet with an 8 byte timestamp and a
%% payload consisting of ASCII 32 to 79.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:echo(inet, 0, 0).
%% <<8,0,166,49,0,0,0,0,255,253,243,182,71,2,233,209,32,33,
%%   34,35,36,37,38,39,40,41,42,43,44,...>>
%% '''
-spec echo(inet | inet6, id(), sequence()) -> binary().
echo(Family, Id, Seq) ->
    % Pad packet to 64 bytes
    echo(Family, Id, Seq, payload(echo)).

%% @doc Generate ICMP ECHO_REQUEST with user specified payload.
%%
%% Creates an ICMP echo packet with the results of
%% erlang:monotonic_time(micro_seconds) used as the timestamp and a user
%% specified payload (padded to 64 bytes).
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:echo(inet, 0, 0, <<"ping", 0:(60*8)>>).
%% <<8,0,25,47,0,0,0,0,112,105,110,103,0,0,0,0,0,0,0,0,0,0,0,
%%   0,0,0,0,0,0,...>>
%% '''
-spec echo(inet | inet6, id(), sequence(), <<_:64>>) -> binary().
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>>.

%% @doc Set the TTL on a file descriptor.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:set_ttl(gen_icmp:getfd(S), gen_icmp:family(S), 1).
%% ok
%% 2> gen_icmp:get_ttl(gen_icmp:getfd(S), gen_icmp:family(S)).
%% {ok,1}
%% '''
-spec set_ttl(fd(), inet | inet6, int32_t()) -> ok | {error, inet:posix()}.
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>>).

%% @doc Get the TTL for a file descriptor.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:get_ttl(gen_icmp:getfd(S), gen_icmp:family(S)).
%% {ok,64}
%% '''
-spec get_ttl(fd(), inet | inet6) -> {ok, int32_t()} | {error, inet:posix()}.
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.

%% @doc Resolve a host list.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:addr_list(inet, ["8.8.8.8", "google.com", "cloudflare.com", {8,8,8,8}, "8.8.8.8"], false).
%% [{ok,"8.8.8.8",{8,8,8,8}},
%%  {ok,"google.com",{172,217,165,14}},
%%  {ok,"cloudflare.com",{104,16,133,229}},
%%  {ok,{8,8,8,8},{8,8,8,8}},
%%  {ok,"8.8.8.8",{8,8,8,8}}]
%%
%% 2> [{ok,"8.8.8.8",{8,8,8,8}},
%%  {ok,"google.com",{172,217,165,14}},
%%  {ok,"cloudflare.com",{104,16,132,229}},
%%  {ok,"cloudflare.com",{104,16,133,229}},
%%  {ok,{8,8,8,8},{8,8,8,8}},
%%  {ok,"8.8.8.8",{8,8,8,8}}]
%% '''
-spec addr_list(inet | inet6, [inet:socket_address() | inet:hostname()], boolean()) ->
    [
        {ok, inet:hostname(), inet:socket_address()}
        | {error, inet:posix(), inet:hostname(), undefined}
    ].
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
    ).

%% @doc Parse or resolve an IPv4 host identifier.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:parse("8.8.8.8").
%% {ok,[{8,8,8,8}]}
%%
%% 2> gen_icmp:parse({8,8,8,8}).
%% {ok,[{8,8,8,8}]}
%%
%% 3> gen_icmp:parse("2001:4860:4860::8888").
%% {ok,[{8193,18528,18528,0,0,0,0,34952}]}
%%
%% 4> gen_icmp:parse("cloudflare.com").
%% {ok,[{104,16,132,229},{104,16,133,229}]}
%%
%% 5> gen_icmp:parse("foo").
%% {error,nxdomain}
%% '''
-spec parse(inet:socket_address() | inet:hostname()) ->
    {ok, [inet:socket_address()]} | {error, inet:posix()}.
parse(Addr) ->
    parse(inet, Addr).

%% @doc Parse or resolve a host identifier.
%%
%% == Examples ==
%%
%% ```
%% 1> gen_icmp:parse(inet, "cloudflare.com").
%% {ok,[{104,16,133,229},{104,16,132,229}]}
%%
%% 2> gen_icmp:parse(inet6, "cloudflare.com").
%% {ok,[{9734,18176,0,0,0,0,26640,34021},
%%      {9734,18176,0,0,0,0,26640,34277}]}
%% '''
-spec parse(inet | inet6, inet:socket_address() | inet:hostname()) ->
    {ok, [inet:socket_address()]} | {error, inet:posix()}.
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]}.

%% @doc IPv6 ICMP filtering: allow all ICMP types.
%%
%% @see icmp6_filter_setblock/2
-spec icmp6_filter_setpassall() -> icmp6_filter().
icmp6_filter_setpassall() ->
    case os:type() of
        {unix, linux} ->
            % Linux reverses the RFC3542 macros
            <<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.

%% @doc ICMPv6 filtering: block all ICMP types.
%%
%% @see icmp6_filter_setpass/2
-spec icmp6_filter_setblockall() -> icmp6_filter().
icmp6_filter_setblockall() ->
    case os:type() of
        {unix, linux} ->
            % Linux reverses the RFC3542 macros
            <<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.

%% @doc Update a filter to allow an ICMPv6 type.
%%
%% == Examples ==
%%
%% To generate a filter that allowed only ICMP6_ECHO_REPLY messages:
%%
%% ```
%% {ok, Socket} = gen_icmp:open([inet6]),
%% Filter = gen_icmp:icmp6_filter_setpass(echo_reply, gen_icmp:icmp6_filter_setblockall()),
%% ok = gen_icmp:filter(Socket, Filter).
%% '''
%%
%% @see filter/2

%#define ICMP6_FILTER_SETPASS(type, filterp) \
%            (((filterp)->icmp6_filt[(type) >> 5]) |= (1 << ((type) & 31)))
-spec icmp6_filter_setpass(atom() | uint8_t(), icmp6_filter()) -> icmp6_filter().
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 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).

%% @doc Update a filter to block an ICMPv6 type.
%%
%% == Examples ==
%%
%% To generate a filter that allowed only ICMP6_PACKET_TOO_BIG messages:
%%
%% ```
%% 1> {ok, Socket} = gen_icmp:open([inet6]).
%% {ok,<0.300.0>}
%% 2> Filter = gen_icmp:icmp6_filter_setblock(packet_too_big, gen_icmp:icmp6_filter_setpassall()).
%% <<4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
%%     0,...>>
%% 3> gen_icmp:filter(Socket, Filter).
%% ok
%% '''
%%
%% @see filter/2

%#define ICMP6_FILTER_SETBLOCK(type, filterp) \
%            (((filterp)->icmp6_filt[(type) >> 5]) &= ~(1 << ((type) & 31)))
-spec icmp6_filter_setblock(atom() | uint8_t(), icmp6_filter()) -> icmp6_filter().
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 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).

%% @doc Test if a filter allows an ICMPv6 type.
%%
%% == Examples ==
%%
%% ```
%% 1> Filter = gen_icmp:icmp6_filter_setblock(packet_too_big, gen_icmp:icmp6_filter_setpassall()).
%% <<4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
%%   0,...>>
%% 2> gen_icmp:icmp6_filter_willpass(echo_reply, Filter).
%% true
%% 3> gen_icmp:icmp6_filter_willpass(packet_too_big, Filter).
%% false
%% '''

%#define ICMP6_FILTER_WILLPASS(type, filterp) \
%            ((((filterp)->icmp6_filt[(type) >> 5]) & (1 << ((type) & 31))) != 0)
-spec icmp6_filter_willpass(atom() | uint8_t(), icmp6_filter()) -> boolean().
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 os:type() of
        {unix, linux} -> El band Value =:= 0;
        {unix, _} -> El band Value =/= 0
    end.

%% @doc Test if a filter blocks an ICMPv6 type.
%%
%% == Examples ==
%%
%% ```
%% 1> Filter = gen_icmp:icmp6_filter_setblock(packet_too_big, gen_icmp:icmp6_filter_setpassall()).
%% <<4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
%%   0,...>>
%% 2> gen_icmp:icmp6_filter_willblock(echo_reply, Filter).
%% false
%% 3> gen_icmp:icmp6_filter_willblock(packet_too_big, Filter).
%% true
%% '''

%#define ICMP6_FILTER_WILLBLOCK(type, filterp) \
%            ((((filterp)->icmp6_filt[(type) >> 5]) & (1 << ((type) & 31))) == 0)
-spec icmp6_filter_willblock(atom() | uint8_t(), icmp6_filter()) -> boolean().
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 os:type() of
        {unix, linux} -> El band Value =/= 0;
        {unix, _} -> El band Value =:= 0
    end.

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

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 os:type() of
        {unix, linux} ->
            1;
        {unix, _} ->
            18
    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(Socket) ->
    receive
        {icmp, Socket, _Addr, _TTL, _Data} ->
            flush_events(Socket)
    after 0 -> ok
    end.

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

%% @private
-spec timediff(integer()) -> integer().
timediff(T) ->
    timediff(gettime(), T).

%% @private
-spec timediff(integer(), integer()) -> integer().
timediff(T1, T2) ->
    T1 - T2.

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