%%% @copyright 2011-2023 Michael Santos %%% 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 Perform a traceroute. %% %% Send a probe packet with the time to live set from 1. Monitor %% an ICMP socket for ICMP responses or timeout. %% %% == Examples == %% %% ``` %% 1> tracert:host("hex.pm"). %% ''' -module(tracert). -behaviour(gen_server). -include_lib("pkt/include/pkt.hrl"). -export([ host/1, host/2, host/3, path/1 ]). -export([ open/0, open/1, close/1, socket/4, proplist_to_record/1, probe/5, response/1 ]). -export([start_link/1]). %% gen_server callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). -record(state, { pid, family = inet, protocol = icmp, ttl = 1, max_hops = 31, % 1 second timeout = 1000, packet, handler, dport = 0, sport = 0, next_port, saddr = {0, 0, 0, 0}, daddr, ws, rs }). -type socket() :: pid(). -type option() :: {family, inet | inet6} | {protocol, icmp | udp} | {ttl, non_neg_integer()} | {max_hops, non_neg_integer()} | {timeout, non_neg_integer() | infinity} | {packet, binary()} | {dport, gen_icmp:uint16_t()} | {sport, gen_icmp:uint16_t()} | {next_port, gen_icmp:uint16_t()} | {saddr, inet:socket_address()} | {daddr, inet:socket_address()}. -export_type([ option/0, socket/0 ]). -ifndef(PF_INET6). -define(PF_INET6, family(inet6)). -endif. %% @doc Perform an ICMP traceroute to a destination. %% %% == Examples == %% %% ``` %% 1> tracert:host("google.com"). %% [timeout, %% {{142,250,57,128}, %% 1174, %% {icmp,<<11,0,111,150,0,0,0,0,69,96,0,84,128,116,64,0,1, %% 1,125,44,10,128,...>>}}, %% {{172,253,79,98}, %% 3418, %% {icmp,<<11,0,244,238,0,17,0,0,69,96,0,84,128,117,64,0,1, %% 1,125,43,10,...>>}}, %% {{192,178,45,217}, %% 4103, %% {icmp,<<11,0,111,113,0,17,0,0,69,96,0,84,128,118,64,0,1, %% 1,125,42,...>>}}, %% {{216,239,57,177}, %% 2233, %% {icmp,<<11,0,111,113,0,17,0,0,69,96,0,84,128,119,64,0,1, %% 1,125,...>>}}, %% timeout,timeout,timeout,timeout,timeout,timeout,timeout, %% timeout,timeout, %% {{173,194,195,100}, %% 975, %% {icmp,<<0,0,184,52,205,52,0,0,32,...>>}}] %% ''' -spec host(inet:socket_address() | inet:hostname()) -> [{inet:socket_address(), MicroSeconds :: integer(), {icmp | udp, binary()}} | timeout]. host(Host) -> host(Host, []). %% @doc Perform a traceroute to a destination with options. %% %% == Examples == %% %% ``` %% 1> tracert:host("google.com", [{protocol, udp}]). %% [timeout, %% {{142,250,57,212}, %% 3671, %% {icmp,<<11,0,0,16,0,0,0,0,69,96,0,28,1,92,64,0,1,17,109, %% 31,10,128,...>>}}, %% {{142,250,231,47}, %% 2081, %% {icmp,<<11,0,255,254,0,17,0,0,69,96,0,28,1,93,64,0,1,17, %% 109,30,10,...>>}}, %% {{142,251,236,131}, %% 1322, %% {icmp,<<11,0,255,254,0,17,0,0,69,96,0,28,1,94,64,0,1,17, %% 109,29,...>>}}, %% {{108,170,234,239}, %% 3121, %% {icmp,<<11,0,255,254,0,17,0,0,69,96,0,28,1,95,64,0,1,17, %% 109,...>>}}, %% timeout,timeout,timeout,timeout,timeout,timeout,timeout, %% timeout,timeout, %% {{64,233,191,139},917,{icmp,<<3,3,8,13,0,0,0,0,69,...>>}}] %% ''' -spec host(inet:socket_address() | inet:hostname(), [option()]) -> [{inet:socket_address(), MicroSeconds :: integer(), {icmp | udp, binary()}} | timeout]. host(Host, Options) -> {ok, Socket} = open(Options), Path = host(Socket, Host, Options), close(Socket), Path. %% @doc Perform a traceroute to a destination. %% %% ICMP and UDP probes are supported. ICMP probes are the default. %% %% max_hops is the maximum TTL (default: 30) %% %% Set the time in milliseconds to wait for a response using the %% timeout option (default: 1000 ms). WARNING: if the response %% arrives after the timeout, tracert will insert spurious entries %% into the path. %% %% tracert will not spawn the setuid helper if the `{setuid, false}' %% option is used. In this case, beam must either be running as %% root or have the cap_net_raw privileges under Linux. %% %% The {sport, Port} option sets the initial source port for UDP %% probes. The port will be incremented by 1 for each subsequent %% probe (default: random high port). For ICMP probes, the ICMP %% ID field will be set to this value. %% %% The return value is an ordered list of tuples: %% %% * Address: the source address responding to the probe %% %% * MicroSeconds: time elapsed between the probe and receiving %% the response %% %% * Protocol: icmp or udp %% %% * Protocol data: a binary representing the received packet %% contents %% %% == Examples == %% %% ``` %% 1> {ok, Socket} = tracert:open(). %% {ok,<0.164.0>} %% 2> Path = tracert:host(Socket, "8.8.8.8", []). %% [timeout,timeout,timeout,timeout,timeout,timeout,timeout, %% timeout,timeout,timeout, %% {{8,8,8,8}, %% 750, %% {icmp,<<0,0,231,24,158,80,0,0,32,33,34,35,36,...>>}}] %% 3> tracert:close(Socket). %% ok %% 4> tracert:path(Path). %% [timeout,timeout,timeout,timeout,timeout,timeout,timeout, %% timeout,timeout,timeout, %% {{8,8,8,8},750,echoreply}] %% ''' -spec host(socket(), inet:socket_address() | inet:hostname(), [option()]) -> [{inet:socket_address(), MicroSeconds :: integer(), {icmp | udp, binary()}} | timeout]. host(Socket, Host, Options) -> State = proplist_to_record(Options), #state{family = Family} = State, {ok, [Daddr | _]} = gen_icmp:parse(Family, Host), ok = gen_server:call(Socket, {handler, State#state.handler}, infinity), trace(Socket, State#state{daddr = Daddr}). trace(Ref, State) -> flush_events(Ref), Sport = gen_server:call(Ref, sport), Path = trace(Ref, State#state{sport = Sport}, []), flush_events(Ref), Path. % Traceroute complete trace(_Ref, #state{ttl = 0}, Acc) -> lists:reverse(Acc); % Max hops reached trace(_Ref, #state{ttl = TTL, max_hops = TTL}, Acc) -> lists:reverse(Acc); trace( Ref, #state{ daddr = Daddr, dport = Dport, saddr = Saddr, sport = Sport, ttl = TTL, packet = Fun, next_port = Next, timeout = Timeout } = State0, Acc ) -> State = State0#state{dport = Next(Dport)}, Packet = Fun({Saddr, Sport}, {Daddr, Dport}, TTL), ok = probe(Ref, Daddr, Dport, TTL, Packet), Now = gen_icmp:gettime(), % No catch all match because packets may be received after the timeout receive % Response from destination {icmp, Ref, Daddr, {_, Data}} -> trace( Ref, State#state{ttl = 0}, [{Daddr, gen_icmp:timediff(Now), {icmp, Data}} | Acc] ); % Response from intermediate host % IPv4 ICMP payload {icmp, Ref, {_, _, _, _} = Addr, {icmp, <<_ICMPHeader:8/bytes, _IPv4Header:20/bytes, _Type:8, _Code:8, _Checksum:16, Sport:16, _/binary>> = Data}} -> trace( Ref, State#state{ttl = TTL + 1}, [{Addr, gen_icmp:timediff(Now), {icmp, Data}} | Acc] ); % IPv6 ICMP payload {icmp, Ref, {_, _, _, _, _, _, _, _} = Addr, {icmp, <<_ICMPHeader:8/bytes, _IPv6Header:40/bytes, _Type:8, _Code:8, _Checksum:16, Sport:16, _/binary>> = Data}} -> trace( Ref, State#state{ttl = TTL + 1}, [{Addr, gen_icmp:timediff(Now), {icmp, Data}} | Acc] ); % IPv4 UDP payload {icmp, Ref, {_, _, _, _} = Addr, {udp, <<_ICMPHeader:8/bytes, _IPv4Header:20/bytes, Sport:16, _/binary>> = Data}} -> trace( Ref, State#state{ttl = TTL + 1}, [{Addr, gen_icmp:timediff(Now), {icmp, Data}} | Acc] ); % IPv6 UDP payload {icmp, Ref, {_, _, _, _, _, _, _, _} = Addr, {udp, <<_ICMPHeader:8/bytes, _IPv6Header:40/bytes, _Sport:16, _/binary>> = Data}} -> trace( Ref, State#state{ttl = TTL + 1}, [{Addr, gen_icmp:timediff(Now), {icmp, Data}} | Acc] ); % Response from protocol handler {tracert, Ref, Saddr, Data} -> trace( Ref, State#state{ttl = 0}, [{Saddr, gen_icmp:timediff(Now), Data} | Acc] ) after Timeout -> trace(Ref, State#state{ttl = TTL + 1}, [timeout | Acc]) end. %% @doc Send a traceroute probe packet. -spec probe(socket(), inet:socket_address(), inet:socket_address(), non_neg_integer(), binary()) -> ok | {error, inet:posix()}. probe(Socket, Daddr, Dport, TTL, Packet) when is_binary(Packet) -> gen_server:call(Socket, {send, Daddr, Dport, TTL, Packet}, infinity). %% @doc Open an ICMP socket for traceroute %% %% == Examples == %% %% ``` %% 1> {ok, Socket} = tracert:open(). %% {ok,<0.164.0>} %% ''' -spec open() -> {ok, socket()} | {error, system_limit | inet:posix()}. open() -> open([]). %% @doc Open an ICMP socket for traceroute. %% %% == Examples == %% %% ``` %% 1> {ok, Socket} = tracert:open([inet6, {protocol, udp}]). %% {ok,<0.289.0>} %% ''' -spec open([option()]) -> {ok, socket()} | {error, system_limit | inet:posix()}. open(Options) -> start_link(Options). %% @doc Close the ICMP socket. %% %% == Examples == %% %% ``` %% 1> {ok, Socket} = tracert:open(). %% {ok,<0.164.0>} %% 2> tracert:close(Socket). %% ok %% ''' -spec close(socket()) -> ok. close(Socket) -> gen_server:call(Socket, close). %% @doc Convert trace response to atoms. %% %% Convert the list of binaries returned by host/1,2,3 to atoms %% representing the ICMP response codes and UDP errors. %% %% == Examples == %% %% ``` %% 1> {ok, Socket} = tracert:open(). %% {ok,<0.164.0>} %% 2> Path = tracert:host(Socket, "8.8.8.8", []). %% [timeout,timeout,timeout,timeout,timeout,timeout,timeout, %% timeout,timeout,timeout, %% {{8,8,8,8}, %% 750, %% {icmp,<<0,0,231,24,158,80,0,0,32,33,34,35,36,...>>}}] %% 3> tracert:close(Socket). %% ok %% 4> tracert:path(Path). %% [timeout,timeout,timeout,timeout,timeout,timeout,timeout, %% timeout,timeout,timeout, %% {{8,8,8,8},750,echoreply}] %% ''' path(Path) when is_list(Path) -> path(Path, [response(icmp)]). path(Path, []) -> Path; path(Path, [Fun | Funs]) when is_list(Path), is_function(Fun) -> Mapped = lists:map(Fun, Path), path(Mapped, Funs). %% @private response(icmp) -> fun ({{_, _, _, _} = Saddr, Microsec, {icmp, Packet}}) -> ICMP = icmp_to_atom(inet, Packet), {Saddr, Microsec, ICMP}; ({{_, _, _, _, _, _, _, _} = Saddr, Microsec, {icmp, Packet}}) -> ICMP = icmp_to_atom(inet6, Packet), {Saddr, Microsec, ICMP}; (N) -> N end. %%------------------------------------------------------------------------- %%% Callbacks %%------------------------------------------------------------------------- %% @private start_link(Options) -> Pid = self(), gen_server:start_link(?MODULE, [Pid, Options], []). %% @private init([Pid, Options]) -> process_flag(trap_exit, true), State = proplist_to_record(Options), #state{ family = Family, protocol = Protocol, saddr = Saddr, sport = Sport } = State, % Read socket: ICMP trace {ok, RS} = gen_icmp:open([Family], [{active, true}]), % Write socket: probes {ok, WS} = socket( Family, Protocol, Saddr, Sport ), {ok, State#state{ pid = Pid, ws = WS, rs = RS }}. %% @private handle_call(close, {Pid, _}, #state{pid = Pid} = State) -> {stop, normal, ok, State}; handle_call(sport, _From, #state{sport = Sport} = State) -> {reply, Sport, State}; handle_call( {send, {DA1, DA2, DA3, DA4}, Dport, TTL, Packet}, _From, #state{ws = Socket} = State ) -> Sockaddr = << (procket:sockaddr_common(?PF_INET, 16))/binary, % Destination Port Dport:16, % IPv4 address DA1, DA2, DA3, DA4, 0:64 >>, ok = gen_icmp:set_ttl(Socket, inet, TTL), {reply, procket:sendto(Socket, Packet, 0, Sockaddr), State}; handle_call( {send, {DA1, DA2, DA3, DA4, DA5, DA6, DA7, DA8}, Dport, TTL, Packet}, _From, #state{ws = Socket} = State ) -> Sockaddr = << (procket:sockaddr_common(?PF_INET6, 16))/binary, % Destination Port Dport:16, % Flow info 0:32, % IPv6 address DA1:16, DA2:16, DA3:16, DA4:16, DA5:16, DA6:16, DA7:16, DA8:16, % Scope ID 0:32 >>, ok = gen_icmp:set_ttl(Socket, inet6, TTL), {reply, procket:sendto(Socket, Packet, 0, Sockaddr), State}; handle_call({handler, _Handler}, _From, State) -> {reply, ok, State}; handle_call(Request, From, State) -> error_logger:info_report([{call, Request}, {from, From}, {state, State}]), {reply, ok, State}. %% @private handle_cast(Msg, State) -> error_logger:info_report([{cast, Msg}, {state, State}]), {noreply, State}. %% @private handle_info( {icmp, Socket, Daddr, _TTL, Data}, #state{ pid = Pid, rs = Socket, protocol = Protocol } = State ) -> Pid ! {icmp, self(), Daddr, {Protocol, Data}}, {noreply, State}; handle_info({tracert, Daddr, Data}, #state{pid = Pid} = State) -> Pid ! {tracert, self(), Daddr, 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{rs = RS, ws = WS}) -> procket:close(WS), gen_icmp:close(RS), ok. %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. %%------------------------------------------------------------------------- %%% Utility Functions %%------------------------------------------------------------------------- %% @private socket(Family, Protocol0, Saddr, Sport) -> {Protocol, Type, Port} = case {Family, Protocol0} of {inet, icmp} -> {icmp, raw, 0}; {inet6, icmp} -> {'ipv6-icmp', raw, 0}; {_, udp} -> {udp, dgram, Sport} end, open_socket(Family, Type, Protocol, Saddr, Port). open_socket(Family, Type, Protocol, Saddr, Sport) -> case procket:socket(Family, Type, Protocol) of {error, eperm} -> procket:open(Sport, [ {ip, Saddr}, {family, Family}, {type, Type}, {protocol, Protocol} ]); {ok, Socket} -> bind_socket(Socket, Family, Saddr, Sport); Error -> Error end. bind_socket(Socket, inet, {SA1, SA2, SA3, SA4}, Sport) -> Sockaddr = << (procket:sockaddr_common(?PF_INET, 16))/binary, % Source port Sport:16, % IPv4 address SA1, SA2, SA3, SA4, 0:64 >>, case procket:bind(Socket, Sockaddr) of ok -> {ok, Socket}; Error -> Error end; bind_socket(Socket, inet6, {SA1, SA2, SA3, SA4, SA5, SA6, SA7, SA8}, Sport) -> Sockaddr = << (procket:sockaddr_common(?PF_INET6, 16))/binary, % Source port Sport:16, % IPv6 flow information 0:32, % IPv6 address SA1:16, SA2:16, SA3:16, SA4:16, SA5:16, SA6:16, SA7:16, SA8:16, % IPv6 scope id 0:32 >>, case procket:bind(Socket, Sockaddr) of ok -> {ok, Socket}; Error -> Error end. %% @private proplist_to_record(Options) -> Default = #state{}, {Family, Saddr} = case proplists:get_value(inet6, Options, false) of true -> {inet6, {0, 0, 0, 0, 0, 0, 0, 0}}; false -> {Default#state.family, Default#state.saddr} end, Protocol = proplists:get_value(protocol, Options, Default#state.protocol), Packet = proplists:get_value(packet, Options, protocol(Protocol)), Handler = proplists:get_value(handler, Options, Default#state.handler), Initial_ttl = proplists:get_value(ttl, Options, Default#state.ttl), Max_hops = proplists:get_value(max_hops, Options, Default#state.max_hops), Timeout = proplists:get_value(timeout, Options, Default#state.timeout), Saddr = proplists:get_value(saddr, Options, Saddr), Sport = proplists:get_value(sport, Options, 16#7FFF + rand:uniform(16#8000)), Dport = proplists:get_value(dport, Options, dport(Protocol)), Next_port = proplists:get_value(next_port, Options, next_port(Protocol)), true = Initial_ttl < Max_hops, true = Initial_ttl > 0, #state{ family = Family, protocol = Protocol, packet = Packet, handler = Handler, ttl = Initial_ttl, max_hops = Max_hops, timeout = Timeout, saddr = Saddr, sport = Sport, dport = Dport, next_port = Next_port }. %%------------------------------------------------------------------------- %%% Internal Functions %%------------------------------------------------------------------------- icmp_to_atom(inet, ICMP) when is_binary(ICMP) -> { #icmp{ type = Type, code = Code }, _Payload } = pkt:icmp(ICMP), icmp_message:code({Type, Code}); icmp_to_atom(inet6, ICMP) when is_binary(ICMP) -> { #icmp6{ type = Type, code = Code }, _Payload } = pkt:icmp6(ICMP), icmp6_message:code({Type, Code}). %% %% Construct the protocol headers for the probe %% % Default UDP packet protocol(udp) -> fun({_Saddr, _Sport}, {_Daddr, _Dport}, _TTL) -> <<>> end; % Default ICMP echo packet protocol(icmp) -> fun ({{_, _, _, _}, Sport}, {_Daddr, _Dport}, _TTL) -> gen_icmp:echo(inet, Sport, 0, <<(list_to_binary(lists:seq($\s, $W)))/binary>>); ({{_, _, _, _, _, _, _, _}, Sport}, {_Daddr, _Dport}, _TTL) -> gen_icmp:echo(inet6, Sport, 0, <<(list_to_binary(lists:seq($\s, $W)))/binary>>) end. %% %% Calculate the port for different protocol types %% dport(udp) -> 1 bsl 15 + 666; dport(icmp) -> 0. next_port(udp) -> fun(N) -> N + 1 end; next_port(_) -> fun(N) -> N end. flush_events(Ref) -> receive {Event, Ref, _Addr, _Data} when Event == icmp; Event == tracert -> flush_events(Ref) after 0 -> ok end. family(inet6) -> case os:type() of {unix, darwin} -> 30; {unix, freebsd} -> 28; {unix, linux} -> 10; {unix, netbsd} -> 24; {unix, openbsd} -> 24 end.