%%% @copyright 2009-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 An Erlang port interface to libpcap. %% %% == SETUP == %% %% ``` %% # Allow your user to epcap with root privs %% sudo visudo %% youruser ALL = NOPASSWD: /path/to/epcap/priv/epcap %% # And if requiretty is enabled, disable it by using one of these %% Defaults!/path/to/epcap/priv/epcap !requiretty %% Defaults:youruser !requiretty %% ''' %% %% == EXAMPLES == %% %% To run the `sniff' example: %% %% ``` %% $ mkdir -p ebin %% $ erlc +debug_info -I _build/default/lib -o ebin examples/*.erl %% ''' %% %% ``` %% rebar3 shell %% %% % Start the sniffer process %% sniff:start_link(). %% %% % Use your interface, or leave it out and trust in pcap %% sniff:start([{interface, "eth0"}]). %% %% % To change the filter %% sniff:start([{filter, "icmp or (tcp and port 80)"},{interface, "eth0"}]). %% %% % To stop sniffing %% sniff:stop(). %% ''' %% %% == PF_RING == %% %% In case you want to compile epcap with PF_RING support, just specify %% the path to the libpfring and modified libpcap libraries via shell %% variable PFRING. %% %% ``` %% PFRING=/home/user/pfring rebar3 do clean, compile %% ''' %% %% To complete the configuration you need to set up the cluster_id %% option. The value of the cluster_id option is integer and should be in %% range between 0 and 255. %% %% ``` %% epcap:start_link([{interface, "lo"}, {cluster_id, 2}]). %% ''' %% %% You can also specify the option cpu_affinity to set up CPU affinity for %% epcap port: %% %% ``` %% epcap:start_link([{interface, "lo"}, {cluster_id, 2}, {cpu_affinity, "1,3,5-7"}]). %% ''' %% %% == PROCESS RESTRICTIONS == %% %% Setting the `RESTRICT_PROCESS' environment variable controls which %% mode of process restriction is used. The available modes are: %% %% * seccomp: linux %% %% * pledge: openbsd (default) %% %% * capsicum: freebsd (default) %% %% * rlimit: all (default: linux) %% %% * null: all %% %% For example, to force using the seccomp process restriction on linux: %% %% ``` %% RESTRICT_PROCESS=rlimit rebar3 do clean, compile %% ''' %% %% The `null' mode disables process restrictions and can be used for debugging. %% %% ``` %% RESTRICT_PROCESS=null rebar3 do clean, compile %% %% epcap:start([{exec, "sudo strace -f -s 4096 -o rlimit.trace"}, {filter, "port 9997"}]). %% %% RESTRICT_PROCESS=seccomp rebar3 do clean, compile %% %% epcap:start([{exec, "sudo strace -f -s 4096 -o seccomp.trace"}, {filter, "port 9997"}]). %% ''' -module(epcap). -behaviour(gen_server). %% API -export([start_link/0, start_link/1, start_link/2, stop/1]). -export([start/0, start/1, start/2]). -export([send/2]). -export([getopts/1]). %% gen_server callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). -export_type([time_unit/0, options/0]). -record(state, {pid :: pid(), port :: port()}). -type time_unit() :: timestamp | microsecond. %% @doc Start the epcap port process. %% @see start_link/2 -spec start() -> ignore | {error, _} | {ok, pid()}. start() -> start(self(), []). %% @doc Start the epcap port process with options. %% @see start_link/2 -spec start(options()) -> ignore | {error, _} | {ok, pid()}. start(Options) -> start(self(), Options). %% @doc Start the epcap port process with owner and options. %% @see start_link/2 -spec start(pid(), options()) -> ignore | {error, _} | {ok, pid()}. start(Pid, Options) when is_pid(Pid), is_list(Options) -> gen_server:start(?MODULE, [Pid, Options], []). %% @doc Start and link with the epcap port process. %% @see start_link/2 -spec start_link() -> ignore | {error, _} | {ok, pid()}. start_link() -> start_link(self(), []). %% @doc Start and link with the epcap port process. %% @see start_link/2 -spec start_link(options()) -> ignore | {error, _} | {ok, pid()}. start_link(Options) -> start_link(self(), Options). %% @doc Start and link with the epcap port process. %% %% Packets are delivered as messages: %% %% ``` %% {packet, DataLinkType, Time, Length, Packet} %% ''' %% %% The DataLinkType is an integer representing the link layer, %% e.g., ethernet, Linux cooked socket. %% %% The Time can be either in microseconds or a timestamp in the same %% format as erlang:now/0 depending on the value of the time_unit %% option (default: timestamp): %% %% ``` %% {MegaSecs, Secs, MicroSecs} %% ''' %% %% The Length corresponds to the actual packet length on the %% wire. The captured packet may have been truncated. To get the %% captured packet length, use byte_size(Packet). %% %% The Packet is a binary holding the captured data. %% %% If the version of the pcap library supports it, the pcap buffer %% size can be set to avoid dropped packets by using the 'buffer' %% option. The buffer size must be larger than the snapshot %% length (default: 65535) plus some overhead for the pcap data %% structures. Using some multiple of the snapshot length is %% suggested. %% %% == Examples == %% %% ``` %% 1> epcap:start([{filter, "icmp or (tcp and port 80)"},{interface, "eth0"}]). %% {ok,<0.197.0>} %% 2> flush(). %% Shell got {packet,1, %% {1693,57279,417781}, %% 98, %% <<0,22,62,179,157,38,0,22,62,91,72,102,8,0,69,0,0,84,231,22, %% 64,0,64,1,130,73,100,115,92,198,8,8,8,8,8,0,164,231,23, %% 127,0,1,255,0,234,100,0,0,0,0,141,95,6,0,0,0,0,0,16,17,18, %% 19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37, %% 38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55>>} %% Shell got {packet,1, %% {1693,57279,450447}, %% 98, %% <<0,22,62,91,72,102,0,22,62,179,157,38,8,0,69,0,0,84,0,0,0, %% 0,115,1,118,96,8,8,8,8,100,115,92,198,0,0,172,231,23,127, %% 0,1,255,0,234,100,0,0,0,0,141,95,6,0,0,0,0,0,16,17,18,19, %% 20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38, %% 39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55>>} %% ok %% ''' -spec start_link(pid(), options()) -> ignore | {error, _} | {ok, pid()}. start_link(Pid, Options) -> gen_server:start_link(?MODULE, [Pid, Options], []). %% @doc Inject a packet on the network interface. %% %% To enable sending packets, start_link/1 must be called with the %% `{inject, true}' option (default: `{inject, false}'). When disabled, %% any data sent to the epcap port is silently discarded. Packet injection %% failures are treated as fatal errors terminating the epcap port. Partial %% writes are not considered to be errors and are ignored (an error message %% will be printed to stderr if the verbose option is used). %% %% == Examples == %% %% ``` %% 1> {ok, Port} = epcap:start([inject]). %% {ok,<0.197.0>} %% 2> {packet, _, _, _, Packet} = receive N -> N end. %% {packet,1, %% {1693,58033,222196}, %% 74, %% <<0,22,62,179,157,38,0,22,62,91,72,102,8,0,69,0,0,60,205, %% 40,64,0,64,6,...>>} %% 3> epcap:send(Port, Packet). %% ok %% ''' -spec send(pid(), iodata()) -> ok. send(Pid, Packet) when is_pid(Pid) -> case iolist_size(Packet) < 65535 of true -> gen_server:call(Pid, {send, Packet}, infinity); false -> erlang:error(badarg) end. %% @doc Stop the epcap port. %% %% == Examples == %% %% ``` %% 1> {ok, Port} = epcap:start([{filter, "icmp or (tcp and port 80)"},{interface, "eth0"}]). %% {ok,<0.205.0>} %% 2> epcap:stop(Port). %% ok %% ''' -spec stop(pid()) -> ok. stop(Pid) -> catch gen_server:call(Pid, stop), ok. %% @private init([Pid, Options0]) -> process_flag(trap_exit, true), Options = setopts( [ {chroot, filename:join([basedir(), "tmp"])}, {timeout, timeout()}, {direction, inout} ], Options0 ), ok = filelib:ensure_dir( filename:join( proplists:get_value(chroot, Options), "." ) ), [Cmd | Argv] = getopts(Options), Port = open_port( {spawn_executable, Cmd}, [{args, Argv}, {packet, 2}, binary, exit_status] ), % Block until the port has fully initialized receive {Port, {data, Data}} -> {epcap, ready} = binary_to_term(Data), {ok, #state{pid = Pid, port = Port}}; {'EXIT', Port, normal} -> {stop, {error, port_init_failed}}; {'EXIT', Port, Reason} -> {stop, {error, Reason}} end. %% @private handle_call({send, Packet}, _From, #state{port = Port} = State) -> Reply = try erlang:port_command(Port, Packet) of true -> ok catch error:badarg -> {error, closed} end, {reply, Reply, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. %% @private handle_cast(_Msg, State) -> {noreply, State}. %% @private terminate(_Reason, #state{port = Port}) -> catch erlang:port_close(Port), ok. %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Port communication %%-------------------------------------------------------------------- %% @private handle_info({Port, {data, Data}}, #state{port = Port, pid = Pid} = State) -> Pid ! binary_to_term(Data), {noreply, State}; handle_info({'EXIT', Port, Reason}, #state{port = Port} = State) -> {stop, {shutdown, Reason}, State}; handle_info({Port, {exit_status, Status}}, #state{port = Port} = State) -> case Status of 0 -> {stop, normal, State}; _ when 128 < Status -> {stop, {port_terminated, Status - 128}, State}; _ -> {stop, {port_terminated, Status}, State} end; handle_info( Info, %% WTF State ) -> error_logger:error_report([{wtf, Info}]), {noreply, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -type arg_num() :: string() | non_neg_integer(). -type options() :: [ inject | monitor | promiscuous | verbose | {buffer, arg_num()} | {chroot, string()} | {cluster_id, arg_num()} | {direction, in | out | inout} | {env, string()} | {exec, string()} | {file, string()} | {filter, string()} | {group, string()} | {interface, string()} | {progname, string()} | {snaplen, arg_num()} | {time_unit, time_unit()} | {timeout, arg_num() | infinity | immediate} | {user, string()} | {verbose, arg_num()} ]. -spec setopts(options(), options()) -> options(). setopts([], Options) -> proplists:compact(Options); setopts([{Key, Val} | Rest], Options) -> case proplists:get_value(Key, Options) of undefined -> setopts(Rest, [{Key, Val} | Options]); _ -> setopts(Rest, Options) end. %% @private -spec getopts(options()) -> [string()]. getopts(Options) when is_list(Options) -> Exec = exec(Options), Progname = proplists:get_value(progname, Options, progname()), Cpu_affinity = cpu_affinity(Options), Filter = proplists:get_value(filter, Options, ""), Switches0 = lists:append([optarg(Opt) || Opt <- Options]), Switches = Switches0 ++ [Filter], [Cmd | Argv] = [ N || N <- string:tokens(Exec, " ") ++ [Cpu_affinity, Progname | Switches], N /= "" ], [find_executable(Cmd) | Argv]. -spec optarg(atom() | tuple()) -> string(). optarg({buffer, Arg}) -> switch("b", maybe_string(Arg)); optarg({chroot, Arg}) -> switch("d", Arg); optarg({cluster_id, Arg}) -> switch("e", env("PCAP_PF_RING_CLUSTER_ID", Arg)); optarg({env, Arg}) -> switch("e", Arg); optarg({file, Arg}) -> switch("f", Arg); optarg({group, Arg}) -> switch("g", Arg); optarg({interface, Arg}) -> switch("i", Arg); optarg(monitor) -> switch("M"); optarg(promiscuous) -> switch("P"); optarg({snaplen, Arg}) -> switch("s", maybe_string(Arg)); optarg({time_unit, Arg}) -> switch("T", time_unit(Arg)); optarg({timeout, immediate}) -> switch("t", "0"); optarg({timeout, infinity}) -> switch("t", "-1"); optarg({timeout, Arg}) -> switch("t", maybe_string(Arg)); optarg({user, Arg}) -> switch("u", Arg); optarg(verbose) -> switch("v"); optarg({verbose, 0}) -> ""; optarg({verbose, Arg}) -> switch(string:copies("v", Arg)); optarg(inject) -> switch("X"); optarg({direction, Arg}) -> switch("Q", maybe_string(Arg)); optarg(_) -> "". switch(Switch) -> [lists:concat(["-", Switch])]. switch(Switch, Arg) -> [lists:concat(["-", Switch]), Arg]. env(Key, Val) -> lists:concat([Key, "=", maybe_string(Val)]). -spec basedir() -> string(). basedir() -> case code:priv_dir(?MODULE) of {error, bad_name} -> filename:join([filename:dirname(code:which(?MODULE)), "..", "priv", ?MODULE]); Dir -> Dir end. -spec progname() -> string(). progname() -> filename:join([basedir(), ?MODULE]). -spec exec(proplists:proplist()) -> string(). exec(Options) -> Exec = proplists:get_value(exec, Options, "sudo"), case proplists:is_defined(file, Options) of true -> ""; false -> Exec end. -spec cpu_affinity(proplists:proplist()) -> string(). cpu_affinity(Options) -> case proplists:get_value(cpu_affinity, Options) of undefined -> ""; CPUs -> "taskset -c " ++ CPUs end. -spec timeout() -> 0 | 500. timeout() -> case os:type() of {unix, linux} -> 0; _ -> 500 end. %-spec time_unit(time_unit()) -> "0" | "1". time_unit(timestamp) -> "0"; time_unit(microsecond) -> "1". find_executable(Exe) -> case os:find_executable(Exe) of false -> erlang:error(badarg, [Exe]); N -> N end. maybe_string(T) when is_list(T) -> T; maybe_string(T) when is_integer(T) -> integer_to_list(T); maybe_string(T) when is_atom(T) -> atom_to_list(T); maybe_string(T) when is_binary(T) -> binary_to_list(T).