Which to use?
epcap and procket sort of overlap in functionality. The main differences, at the moment, are:- portability
epcap: should work on any Unix with pcap installed
procket: for sniffing, procket uses Linux's PF_PACKET socket option, so Linux only. I plan to add support for BPF someday, so maybe in the future procket will support BSD as well.
- safety
epcap: runs as a separate system process. Any bugs in epcap will not affect the Erlang VM.
procket: linked into the Erlang VM using the NIF interface. Bugs may stall or crash the VM.
- packet generation
epcap: can only sniff packets
procket: can generate whole packets. Again, currently Linux only, but should work under BSD's, like Mac OS X, when BPF is supported.
Raw sockets (for example, generating ICMP echo packets) work under BSD as well.
In fact, it should be possible to combine the power of procket, epcap, and BSD to send and receive arbitrary TCP or UDP packets now (since TCP/UDP raw sockets can send data only, we need to use epcap to sniff the response).
- filtering
epcap: packet filtering rules are processed in C, either in the kernel or in a library.
procket: all packets are received and must be filtered by an Erlang process
Decapsulating Packets
procket and epcap have different ways of being started and reading packets but once the raw packets are received by an Erlang process, they can be decapsulated with a small module ("epcap_net.erl") distributed with epcap. Say, for example, we wanted to monitor http requests and write out a file containing just the client side:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-module(pdump). | |
-include("epcap_net.hrl"). | |
-export([start/0, start/1]). | |
-export([filename/1]). | |
-record(state, { | |
s, % PF_PACKET socket | |
c % monitored connections | |
}). | |
-define(TMPDIR, "/tmp/"). | |
start() -> | |
start("eth0"). | |
start(Dev) -> | |
{ok, Socket} = packet:socket(), | |
ok = packet:promiscuous(Socket, packet:ifindex(Socket, Dev)), | |
loop(#state{ | |
s = Socket, | |
c = orddict:new() | |
}). | |
loop(#state{s = Socket} = State) -> | |
case procket:recvfrom(Socket, 65535) of | |
nodata -> | |
timer:sleep(10), | |
loop(State); | |
{ok, Data} -> | |
P = epcap_net:decapsulate(Data), | |
State1 = match(P, State), | |
loop(State1); | |
{'DOWN', Pid, process, _Object, _Info} -> | |
loop(State#state{ | |
c = orddict:filter( | |
fun (_,V) when V == Pid -> false; | |
(_,_) -> true end, | |
State#state.c) | |
}); | |
Error -> | |
error_logger:error_report(Error) | |
end. | |
% closed connections | |
match([ #ether{}, | |
#ipv4{ | |
saddr = Saddr, | |
daddr = Daddr | |
}, | |
#tcp{ | |
sport = 80, | |
dport = Dport, | |
rst = RST, | |
fin = FIN | |
}, | |
_Payload ], | |
#state{ | |
c = Connections | |
} = State) when RST =:= 1; FIN =:= 1 -> | |
Info = {{Saddr, 80}, {Daddr, Dport}}, | |
case orddict:find(Info, Connections) of | |
{ok, Pid} -> | |
Pid ! eof, | |
State#state{ | |
c = orddict:erase(Info, Connections) | |
}; | |
error -> | |
State | |
end; | |
% connections in ESTABLISHED state | |
match([ #ether{}, | |
#ipv4{ | |
saddr = Saddr, | |
daddr = Daddr | |
}, | |
#tcp{ | |
sport = 80, | |
dport = Dport, | |
syn = 0, | |
rst = 0, | |
fin = 0, | |
ack = 1 | |
}, | |
Payload ], | |
#state{ | |
c = Connections | |
} = State) -> | |
Info = {{Saddr, 80}, {Daddr, Dport}}, | |
case orddict:find(Info, Connections) of | |
{ok, Pid} -> | |
Pid ! {data, Payload}, | |
State; | |
error -> | |
Pid = spawn(fun() -> dumper(Info) end), | |
State#state{c = orddict:store(Info, Pid, Connections)} | |
end; | |
match(_, State) -> | |
State. | |
dumper(Info) -> | |
dumper(Info, []). | |
dumper(Info, Payload) -> | |
receive | |
{data, Data} -> | |
dumper(Info, [Data|Payload]); | |
eof -> | |
filer(Info, Payload) | |
after | |
5000 -> | |
error_logger:info_report([{timeout, Info}]), | |
filer(Info, Payload) | |
end. | |
filer(Info, Data) -> | |
Payload = list_to_binary(lists:reverse(Data)), | |
Name = filename(Info), | |
file:write_file(Name, Payload, [append]). | |
filename({{Saddr, Sport}, {Daddr, Dport}}) -> | |
?TMPDIR ++ "ip-" ++ | |
inet_parse:ntoa(Saddr) ++ "-" ++ integer_to_list(Sport) ++ "_" ++ | |
inet_parse:ntoa(Daddr) ++ "-" ++ integer_to_list(Dport). |
Matching on Payloads
A similar example with epcap: match on all http requests and write the complete transaction to a file based on the etag.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-module(dumpetag). | |
-include("epcap_net.hrl"). | |
-export([start/0, start/1]). | |
-export([filename/1]). | |
-record(state, { | |
c % monitored connections | |
}). | |
-define(TMPDIR, "/tmp/"). | |
start() -> | |
start([{filter, "tcp and port 80"}, | |
{interface, "eth0"}, | |
{chroot, "priv/tmp"}]). | |
start(Arg) -> | |
epcap:start(Arg), | |
loop(#state{ | |
c = orddict:new() | |
}). | |
loop(State) -> | |
receive | |
[{pkthdr, _}, {packet, Packet}] -> | |
P = epcap_net:decapsulate(Packet), | |
State1 = match(P, State), | |
loop(State1); | |
{'DOWN', Pid, process, _Object, _Info} -> | |
loop(State#state{ | |
c = orddict:filter( | |
fun (_,V) when V == Pid -> false; | |
(_,_) -> true end, | |
State#state.c) | |
}); | |
Error -> | |
error_logger:info_report([{error, Error}]) | |
end. | |
% closed connections | |
match([ #ether{}, | |
#ipv4{ | |
saddr = Saddr, | |
daddr = Daddr | |
}, | |
#tcp{ | |
sport = Sport, | |
dport = Dport, | |
rst = RST, | |
fin = FIN | |
}, | |
_Payload], | |
#state{ | |
c = Connections | |
} = State) when RST =:= 1; FIN =:= 1 -> | |
Info = make_key({{Saddr, Sport}, {Daddr, Dport}}), | |
case orddict:find(Info, Connections) of | |
{ok, Pid} -> | |
Pid ! eof, | |
State#state{ | |
c = orddict:erase(Info, Connections) | |
}; | |
error -> | |
State | |
end; | |
% connections in ESTABLISHED state | |
match([ #ether{}, | |
#ipv4{ | |
saddr = Saddr, | |
daddr = Daddr | |
}, | |
#tcp{ | |
sport = Sport, | |
dport = Dport, | |
syn = 0, | |
rst = 0, | |
fin = 0, | |
ack = 1 | |
}, | |
Payload], | |
#state{ | |
c = Connections | |
} = State) -> | |
Info = make_key({{Saddr, Sport}, {Daddr, Dport}}), | |
case orddict:find(Info, Connections) of | |
{ok, Pid} -> | |
Pid ! {data, Payload}, | |
State; | |
error -> | |
{Pid, _Ref} = spawn_monitor(fun() -> dumper() end), | |
State#state{c = orddict:store(Info, Pid, Connections)} | |
end; | |
match(_, State) -> | |
State. | |
% canononical representation of the TCP connection | |
make_key({{_IP, 80}, _} = Key) -> | |
Key; | |
make_key({K2, {_IP, 80} = K1}) -> | |
{K1, K2}. | |
dumper() -> | |
dumper([]). | |
dumper(Payload) -> | |
receive | |
{data, Data} -> | |
dumper([Data|Payload]); | |
eof -> | |
filer(Payload) | |
after | |
5000 -> | |
filer(Payload) | |
end. | |
filer(Data) -> | |
Payload = list_to_binary(lists:reverse(Data)), | |
Name = filename(Payload), | |
file:write_file(Name, Payload, [append]). | |
filename(Payload) -> | |
{match, [Name]} = re:run(Payload, "ETag: \"([a-zA-Z0-9-]+)\"", [{capture, [1], list}]), | |
?TMPDIR ++ "etag-" ++ Name. |
Similar to the procket example, we loop, blocking in receive. When data is received, we check if the connection is in the established state, spawning a process to accumulate the data if we haven't seen this session before.
Finally, we write out the data to the file system when the connection is closed, using the value of the "ETag" header for the file name. For succintness, I used a regular expression to match on the payload. Probably better to write a parser.
Thanks, Zabrane, for suggesting this post!
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.