# $Id: interconn.bro,v 1.12 2002/10/13 21:07:59 vern Exp $
#
# interconn - generic detection of interactive connections.

@load port-name
@load demux

# The following must be defined for the event engine to generate
# interconn events.
redef interconn_min_interarrival = 0.01 sec;
redef interconn_max_interarrival = 2.0 sec;
redef interconn_max_keystroke_pkt_size = 20;
redef interconn_default_pkt_size = 512;
redef interconn_stat_period = 15.0 sec;
redef interconn_stat_backoff = 1.5;

const interconn_min_num_pkts = 10 &redef; # min num of pkts sent
const interconn_min_duration = 2.0 sec &redef; # min duration for the connection

const interconn_ssh_len_disabled = T &redef;
const interconn_min_ssh_pkts_ratio = 0.6 &redef;

const interconn_min_bytes = 10 &redef;
const interconn_min_7bit_ascii_ratio = 0.75 &redef;

const interconn_min_num_lines = 2 &redef;
const interconn_min_normal_line_ratio = 0.5 &redef;

# alpha: portion of interarrival times within range 
#	[interconn_min_interarrival, interconn_max_interarrival]
#
#	alpha should be >= interconn_min_alpha
#
# gamma: num_keystrokes_two_in_row / num_pkts
#	gamma indicates the portion of keystrokes in the overall traffic
#
#	gamma should be >= interconn_min_gamma

const interconn_min_alpha = 0.2 &redef; # minimum required alpha
const interconn_min_gamma = 0.2 &redef; # minimum required gamma

const interconn_standard_ports = { telnet, rlogin, ftp, ssh, smtp, 143/tcp, 110/tcp  } &redef;
const interconn_ignore_standard_ports = F &redef;

const interconn_demux_disabled = T &redef;

const INTERCONN_UNKNOWN = 0;	# direction/interactivity is unknown

const INTERCONN_FORWARD = 1;	# forward: a conn's orig is true originator
const INTERCONN_BACKWARD = 2;	# backward: a conn's resp is true originator

const INTERCONN_INTERACTIVE = 1;	# a conn is interactive
const INTERCONN_STANDARD_PORT = 2;	# conn involves a standard port to ignore

type conn_info : record {
	interactive: count; # interactivity: unknown/interactive/standard_port
	dir: count; # direction: unknown/forward/backward
};

global interconn_conns: table [conn_id] of conn_info; # table for all connections

# Table for resp_endp's of those established (non-partial) conn's.
# If a partial conn connects to one of such resp's, we can infer
# its direction.
global interconn_resps: table [addr, port] of count &default = 0;

global interconn_log = open_log_file("interconn") &redef;

global num_interconns = 0;

function interconn_conn_string(c: connection): string
	{
	return fmt("%.6f %s.%d > %s.%d",
		c$start_time,
		c$id$orig_h, c$id$orig_p,
		c$id$resp_h, c$id$resp_p);
	}

function interconn_weird(c: connection, s: string): bool
	{
	print fmt("%s interconn_weird: %s %s", network_time(), interconn_conn_string(c), s);
	}

function get_direction(c: connection): count
	{
	local id = c$id;

	if ( interconn_conns[id]$dir != INTERCONN_UNKNOWN )
		return interconn_conns[id]$dir;

	# The connection is not established yet, but one endpoint
	# is a known resp_endp
	if ( [id$resp_h, id$resp_p] in interconn_resps )
		{
		interconn_conns[id]$dir = INTERCONN_FORWARD;
		++interconn_resps[id$resp_h, id$resp_p];

		return INTERCONN_FORWARD;
		}

	else if ( [id$orig_h, id$orig_p] in interconn_resps )
		{
		interconn_conns[id]$dir = INTERCONN_BACKWARD;
		++interconn_resps[id$orig_h, id$orig_p];

		return INTERCONN_BACKWARD;
		}

	return INTERCONN_UNKNOWN;
	}

function comp_gamma(s: interconn_endp_stats): double
	{
	return s$num_pkts >= interconn_min_num_pkts ?
		(1.0 * s$num_keystrokes_two_in_row) / s$num_pkts : 0.0;
	}

function comp_alpha(s: interconn_endp_stats) : double
	{
	return ( s$num_keystrokes_two_in_row > 0 ) ?
		(1.0 * s$num_normal_interarrivals / s$num_keystrokes_two_in_row) : 0.0;
	}

function log_interconn(c: connection, tag: string): bool
	{
	print interconn_log, fmt("%s %s", interconn_conn_string(c), tag);

	local id = c$id;

	if ( interconn_demux_disabled )
		{
		if ( active_connection(id) )
			skip_further_processing(id);
		}
	else 
		demux_conn(id, tag, "orig", "resp");
	}

function is_interactive_endp(s: interconn_endp_stats): bool
	{
	# Criteria 1: num_pkts >= interconn_min_num_pkts.
	if ( s$num_pkts < interconn_min_num_pkts )
		return F;

	# Criteria 2: gamma >= interconn_min_gamma.
	if ( comp_gamma(s) < interconn_min_gamma )
		return F;

	# Criteria 3: alpha >= interconn_min_alpha.
	if ( comp_alpha(s) < interconn_min_alpha )
		return F;

	return T;
	}

event connection_established(c: connection)
	{
	local id = c$id;
	local dir = interconn_conns[id]$dir;

	if ( dir == INTERCONN_FORWARD )
		return;

	if ( dir == INTERCONN_BACKWARD )
		{
		interconn_weird(c, "inconsistent direction");
		return;
		}

	interconn_conns[id]$dir = INTERCONN_FORWARD;
	++interconn_resps[id$resp_h, id$resp_p];
	}

event new_connection(c: connection)
	{
	local id = c$id;

	local info: conn_info;
	info$dir = INTERCONN_UNKNOWN;

	if ( interconn_ignore_standard_ports &&
		(id$orig_p in interconn_standard_ports || id$resp_p in interconn_standard_ports) )
		{
		info$interactive = INTERCONN_STANDARD_PORT;
		if ( active_connection(id) )
			skip_further_processing(id);
		}

	else
		info$interactive = INTERCONN_UNKNOWN;

	interconn_conns[id] = info;
	}

event interconn_remove_conn(c: connection)
	{
	local id = c$id;
	local dir = interconn_conns[id]$dir;

	delete interconn_conns[id];
	delete demuxed_conn[c$id];

	if ( dir == INTERCONN_FORWARD )
		{
		if ( --interconn_resps[id$resp_h, id$resp_p] == 0 )
			delete interconn_resps[id$resp_h, id$resp_p];
		}

	else if ( dir == INTERCONN_BACKWARD )
		{
		if ( --interconn_resps[id$orig_h, id$orig_p] == 0 )
			delete interconn_resps[id$orig_h, id$orig_p];
		}
	}

event interconn_stats(c: connection,
			os: interconn_endp_stats, rs: interconn_endp_stats)
	{
	local id = c$id;

	if ( id !in interconn_conns )
		return;

	if ( interconn_conns[id]$interactive != INTERCONN_UNKNOWN )
		return; # already classified

	if ( c$duration < interconn_min_duration )
		# forget about excessively short connections
		return;

	local dir = get_direction(c);

	# Criteria:
	#
	#	if ( dir == FORWARD )
	#		(os) is interactive
	#	else if ( dir == BACKWARD )
	#		(rs) is interactive
	#	else
	#		either (os) or (rs) is interactive
	if ( dir == INTERCONN_FORWARD )
		{
		if ( ! is_interactive_endp(os) )
			return;
		}

	else if ( dir == INTERCONN_BACKWARD )
		{
		if ( ! is_interactive_endp(rs) )
			return;
		}

	else
		{
		if ( ! is_interactive_endp(os) && ! is_interactive_endp(rs) )
			return;
		}

	local tag: string;

	if ( ! interconn_ssh_len_disabled && (os$is_partial || rs$is_partial) )
		{
		local num_pkts = os$num_pkts + rs$num_pkts;
		local num_8k0_pkts = os$num_8k0_pkts + rs$num_8k0_pkts;
		local num_8k4_pkts = os$num_8k4_pkts + rs$num_8k4_pkts;

		if ( num_8k0_pkts > num_pkts * interconn_min_ssh_pkts_ratio )
			{
			# c now considered as interactive.
			interconn_conns[id]$interactive = INTERCONN_INTERACTIVE;
			tag = fmt("interconn.%d.ssh2", ++num_interconns);
			}
		else if ( num_8k4_pkts > num_pkts * interconn_min_ssh_pkts_ratio ) 
			{
			# c now considered as interactive.
			interconn_conns[id]$interactive = INTERCONN_INTERACTIVE;
			tag = fmt("interconn.%d.ssh1", ++num_interconns);
			}
		}

	# Criteria 4:	num_7bit_ascii / num_bytes is big enough; AND
	#		enough number of normal lines
	if ( interconn_conns[id]$interactive != INTERCONN_INTERACTIVE )
		{
		local num_bytes = os$num_bytes + rs$num_bytes;
		local num_7bit_ascii = os$num_7bit_ascii + rs$num_7bit_ascii;

		if ( num_bytes < interconn_min_bytes ||
		     num_7bit_ascii < num_bytes * interconn_min_7bit_ascii_ratio )
			return;

		local num_lines = os$num_lines + rs$num_lines;
		local num_normal_lines = os$num_normal_lines + 
					 rs$num_normal_lines;

		if ( num_lines < interconn_min_num_lines ||
		     num_normal_lines < num_lines * interconn_min_normal_line_ratio )
			return;

		# c now considered as interactive.
		interconn_conns[id]$interactive = INTERCONN_INTERACTIVE;

		tag = fmt("interconn.%d", ++num_interconns);
		}

	log_interconn(c, tag);
	}
