# $Id: icmp.bro 5892 2008-07-01 02:37:03Z vern $

@load hot
@load weird
@load conn
@load scan

global icmp_file = open_log_file("icmp");

redef capture_filters += { ["icmp"] = "icmp" };

module ICMP;

export {

	redef enum Notice += {
		ICMPAsymPayload,	# payload in echo req-resp not the same
		ICMPConnectionPair,	# too many ICMPs between hosts
		ICMPAddressScan,

		# The following isn't presently sufficiently useful due
		# to cold start and packet drops.
		# ICMPUnpairedEchoReply,	# no EchoRequest seen for EchoReply
	};

	# Whether to log detailed information icmp.log.
	const log_details = T &redef;

	# ICMP scan detection.
	const detect_scans = T &redef;
	const scan_threshold = 25 &redef;

	# Analysis of connection pairs.
	const detect_conn_pairs = F &redef;	# switch for connection pair
	const detect_payload_asym = F &redef;	# switch for echo payload
	const conn_pair_threshold = 200 &redef;
}

global conn_pair:table[addr] of set[addr] &create_expire = 1 day;
global conn_pair_thresh_reached: table[addr] of bool &default=F;



type flow_id: record {
	orig_h: addr;
	resp_h: addr;
	id: count;
};

type flow_info: record {
	start_time: time;
	last_time: time;
	orig_bytes: count;
	resp_bytes: count;
	payload: string;
};

const names: table[count] of string = {
	[0] = "echo_reply",
	[3] = "unreach",
	[4] = "quench",
	[5] = "redirect",
	[8] = "echo_req",
	[9] = "router_adv",
	[10] = "router_sol",
	[11] = "time_xcd",
	[12] = "param_prob",
	[13] = "tstamp_req",
	[14] = "tstamp_reply",
	[15] = "info_req",
	[16] = "info_reply",
	[17] = "mask_req",
	[18] = "mask_reply",
} &default = function(n: count): string { return fmt("icmp-%d", n); };


# Map IP protocol number to the protocol's name.
const IP_proto_name: table[count] of string = {
	[1]  = "ICMP",
	[2]  = "IGMP",
	[6]  = "TCP",
	[17] = "UDP",
	[41] = "IPV6",
} &default = function(n: count): string { return fmt("%s", n); }
  &redef;


global flows: table[flow_id] of flow_info &read_expire = 45 sec;

event icmp_sent(c: connection, icmp: icmp_conn)
	{
	print icmp_file, fmt("%.6f %.6f %s %s %s %s %s %s %s %s %s",
		network_time(), 0.0, icmp$orig_h, icmp$resp_h,
		names[icmp$itype], icmp$itype, icmp$icode, "icmp",
		icmp$len, "0", "SH");
	}

event flow_summary(flow: flow_id, last_time: time)
	{
	if ( flow !in flows )
		return;

	local fi = flows[flow];

	if ( fi$last_time == last_time )
		{
		local local_init = is_local_addr(flow$orig_h);
		local local_addr = local_init ? flow$orig_h : flow$resp_h;
		local remote_addr = local_init ? flow$resp_h : flow$orig_h;
		local flags = local_init ? "L" : "";

		local state: string;
		if ( fi$orig_bytes > 0 )
			{
			if ( fi$resp_bytes > 0 )
				state = "SF";
			else
				state = "SH";
			}
		else if ( fi$resp_bytes > 0 )
			state = "SHR";
		else
			state = "OTH";

		print icmp_file, fmt("%.6f %.6f %s %s %s %s %s %s %s",
			fi$start_time, fi$last_time - fi$start_time,
			flow$orig_h, flow$resp_h, "icmp_echo",
			fi$orig_bytes, fi$resp_bytes, state, flags);

		delete flows[flow];
		}
	}

function update_flow(icmp: icmp_conn, id: count, is_orig: bool, payload: string)
	{
	local fid: flow_id;
	fid$orig_h = is_orig ? icmp$orig_h : icmp$resp_h;
	fid$resp_h = is_orig ? icmp$resp_h : icmp$orig_h;
	fid$id = id;

	if ( fid !in flows )
		{
		local info: flow_info;
		info$start_time = network_time();
		info$orig_bytes = info$resp_bytes = 0;
		info$payload = payload;	# checked in icmp_echo_reply
		flows[fid] = info;
		}

	local fi = flows[fid];

	fi$last_time = network_time();

	if ( is_orig )
		fi$orig_bytes = fi$orig_bytes + byte_len(payload);
	else
		fi$resp_bytes = fi$resp_bytes + byte_len(payload);

	schedule +30sec { flow_summary(fid, fi$last_time) };
	}

event icmp_echo_request(c: connection, icmp: icmp_conn, id: count, seq: count, payload: string)
	{
	update_flow(icmp, id, T, payload);

	local orig = icmp$orig_h;
	local resp = icmp$resp_h;

	# Simple ping scan detector.
	if ( detect_scans &&
	     (orig !in Scan::distinct_peers ||
	      resp !in Scan::distinct_peers[orig]) )
		{
		if ( orig !in Scan::distinct_peers )
			{
			local empty_peer_set: set[addr] &mergeable;
			Scan::distinct_peers[orig] = empty_peer_set;
			}

		if ( resp !in Scan::distinct_peers[orig] )
			add Scan::distinct_peers[orig][resp];

		if ( ! Scan::shut_down_thresh_reached[orig] &&
		     orig !in Scan::skip_scan_sources &&
		     orig !in Scan::skip_scan_nets &&
		     |Scan::distinct_peers[orig]| >= scan_threshold )
			{
			NOTICE([$note=ICMPAddressScan, $src=orig,
				$msg=fmt("%s has icmp echo scanned %s hosts",
				orig, scan_threshold)]);

			Scan::shut_down_thresh_reached[orig] = T;
			}
		}

	if ( detect_conn_pairs )
		{
		if ( orig !in conn_pair )
			{
			local empty_peer_set2: set[addr] &mergeable;
			conn_pair[orig] = empty_peer_set2;
			}

		if ( resp !in conn_pair[orig] )
			add conn_pair[orig][resp];

		if ( ! conn_pair_thresh_reached[orig] &&
		     |conn_pair[orig]| >= conn_pair_threshold )
			{
			NOTICE([$note=ICMPConnectionPair,
				$msg=fmt("ICMP connection threshold exceeded : %s -> %s",
				orig, resp)]);
			conn_pair_thresh_reached[orig] = T;
			}
		}
	}

event icmp_echo_reply(c: connection, icmp: icmp_conn, id: count,
			seq: count, payload: string)
	{
	# Check payload with the associated flow.

	local fid: flow_id;
	fid$orig_h = icmp$resp_h;	# We know the expected results since
	fid$resp_h = icmp$orig_h;	# it's an echo reply.
	fid$id = id;

	if ( fid !in flows )
		{
# 		NOTICE([$note=ICMPUnpairedEchoReply,
# 			$msg=fmt("ICMP echo reply w/o request: %s -> %s",
# 				icmp$orig_h, icmp$resp_h)]);
		}
	else
		{
		if ( detect_payload_asym )
			{
			local fi = flows[fid];
			local pl = fi$payload;

			if ( pl != payload )
				{
				NOTICE([$note=ICMPAsymPayload,
					$msg=fmt("ICMP payload inconsistancy: %s(%s) -> %s(%s)",
					icmp$orig_h, byte_len(fi$payload),
					icmp$resp_h, byte_len(payload))]);
				}
			}
		}

	update_flow(icmp, id, F, payload);
	}

event icmp_unreachable(c: connection, icmp: icmp_conn, code: count,
			context: icmp_context)
	{
	if ( active_connection(context$id) )
		{
		# This section allows Bro to act on ICMP-unreachable packets
		# that happen in the context of an active connection.  It is
		# not currently used.
		local c2 = connection_record(context$id);
		local os = c2$orig$state;
		local rs = c2$resp$state;
		local is_attempt =
			is_tcp_port(c2$id$orig_p) ?
				(os == TCP_SYN_SENT && rs == TCP_INACTIVE) :
				(os == UDP_ACTIVE && rs == UDP_INACTIVE);

		# Insert action here.
		}

	if ( log_details )
		{
		# ICMP unreachable packets are the only ones currently
		# logged.  Due to the connection data contained *within*
		# them, each log line will contain two connections' worth
		# of data.  The initial ICMP connection info is the same
		# as logged for connections.
		print icmp_file, fmt("%.6f %.6f %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s",
			network_time(), 0.0, icmp$orig_h, icmp$resp_h,
			names[icmp$itype], icmp$itype, icmp$icode, "icmp",
			icmp$len, "0", "EncapPkt:",
				# This is the encapsulated packet:
				context$id$orig_h, context$id$orig_p,
				context$id$resp_h, context$id$resp_p,
				context$len, IP_proto_name[context$proto],
				context$len, context$bad_hdr_len,
				context$bad_checksum);
		}
	}
