#!/usr/bin/perl
# $Id: flow-ga.pl,v 1.91 2014/12/09 07:30:05 stubbim Exp $
# $Source: /home/CVSROOT/netflow_ga/bin/flow-ga.pl,v $

# Reporterskript: hole Floweinträge aus der angegebenen Datei und sende an
# Google Analytics über das "Measurement Protocol"

# Dieses Skript erwartet einen Dateinamen als Argument, z.B.
#   perl flow-ga.pl --format nfdump /var/flows/nfcapd.201411112240
#   perl flow-ga.pl --format flowtools /var/flow-tools/myrouter/2014/2014-08/2014-08-14/ft-v05.2014-08-14.201501+0200


# hier den eigenen Google Analytics Account eintragen
my $GA_ACCOUNT	= "UA-12345678-1";
my $FLOWTOOLS_BINDIR = "/usr/bin";

# hier können die NetFlow-Beiträge der Exporter (Router,Switche,Firewall)
# an unterschiedliche GA-Properties versendet werden.
# Wenn der primÃ¤re Account verwendet werden soll, bleibt diese Variable leer.
my %NF_GA_ACCOUNT = (
#	'10.15.3.190'	=> "UA-12345678-2",
);

# IP-Adressen und -netze, die *nicht* an Google Analytics berichtet werden
# dürfen, gehören in diese Liste.
# Sinnvoll sind v.a. die Adressen der NetFlow-Exporter (Router,Switche) und
# der NetFlow-Collector
my @hide_ip_from_reporting = ( qw!10.61.27.11 10.14.0.71 10.37.182.0/24! );

# In dieser Liste werden alle DNS Domänennamen eingetragen die zum internen
# Netzwerk gehören und vom voll qualifizierten Hostnamen (FQDN) entfernt werden.
# Liste leer lassen wenn Hostnamen nicht verändert werden sollen.
my @dns_domain_names = ( qw/meine-domain.com meine-firma.de/ );

# Sampling-Rate (versendet nur 1 von N Flows)
my $sampling_rate_N = 1;
#my $sampling_rate_N = 10;

# an welche Syslog Facility werden debug-Meldungen gesendet
my $syslog_facility = "local5";


##### unterhalb dieser Markierung sind meistens keine Änderungen notwendig #####




# benötigte Perl Module
use strict;
use LWP::UserAgent;
use URI::Escape;
use Time::HiRes qw( usleep );
use Sys::Syslog qw( openlog closelog setlogmask syslog );
use Digest::MD5;
use DateTime qw();
use Getopt::Long;


# Behandlung von Signalen
$SIG{INT}  = \&quitProgram;		# signal 15 SIGTERM   (kill <pid>)
$SIG{TERM} = \&quitProgram;		# signal  9 SIGKILL   (kill -9 <pid>) 
$SIG{USR1} = \&show_progress;	# signal 10 SIGUSR1   (kill -10 <pid>) 

# Variablen deklarieren
my $USER_AGENT	= "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20100101 Firefox/31.0";
my $GA_URL	= "http://www.google-analytics.com/collect";
my ( $number_of_flows_reported, $number_of_flows_processed, $script_id,
	$flow_info, $flow_file, %dns_cache, %args, %flow_tool );

# Kommandozeilenargumente
Getopt::Long::Configure( 'no_ignore_case' );
exit if !GetOptions( \%args,
	'debug|d',
	'dryrun',
	'format|f=s',
	'h|help' => sub { exit Help() },
);

# Default Dateiformat
$args{format} ||= 'flowtools';

$args{debug} && openlog( my $name, "ndelay", $syslog_facility );

# Jede Skriptinstanz erhält einen Identifier (um die Einträge in der Logdatei
# besser unterscheiden zu können)
$script_id = sprintf( "%04d", int( rand( 10**4 )) );
$args{debug} && syslog( "LOG_INFO|$syslog_facility",
	"[$script_id] gestartet" . ( $args{dryrun} ? ' im dryrun-Modus' : '' ) );

# Dieses Skript erwartet zwingend einen Dateinamen als Argument
if ( ! $ARGV[ 0 ] ) {
	print "Fehlendes Argument: Dateiname\n";
	$args{debug} && syslog( "LOG_ERR|$syslog_facility",
		"[$script_id] fehlendes Argument: Dateiname" );
	quitProgram( 1 );

} else {
	# NetFlow-Datei aus Parameterliste ermitteln
	$flow_file = $ARGV[ 0 ];

	# prÃ¼fen ob die Datei existiert
	if ( -e $flow_file && -f _ ) {
		$args{debug} && syslog( "LOG_INFO|$syslog_facility",
			"[$script_id] Flow-Datei $flow_file" );

		# wie viele Flows sind in dieser Datei vorhanden?
		$flow_info = get_flow_info( $flow_file );
		$args{debug} && syslog( "LOG_INFO|$syslog_facility",
			sprintf "[%04D] exporter IP ist %s", $script_id,
				$flow_info->{ 'exporter_ip' } || 'unbekannt'
		);

	} else {
		print "filename argument missing or file not found\n";
		$args{debug} && syslog( "LOG_ERR|$syslog_facility",
			"[$script_id] Datei $flow_file nicht gefunden" );
		quitProgram( 2 );
	}
}


# unterschiedliches Kommandos und Trennzeichen je nach gewähltem Format 
if ( $args{format} eq 'flowtools' ) {
	$flow_tool{ cmd_print } = "$FLOWTOOLS_BINDIR/flow-print -f5 < $flow_file";
	$flow_tool{ field_sep } = '\s+';
} elsif ( $args{format} eq 'nfdump' ) {
	$flow_tool{ cmd_print } = "$FLOWTOOLS_BINDIR/nfdump -o csv -r $flow_file";
	$flow_tool{ field_sep } = ',';
}

# Flowkommando ausführen und Ausgabe analysieren
my $t0 = time();
if ( open( my $fh_flows, "-|", $flow_tool{cmd_print} )) {

	# erste Zeile enthält die Spaltennamen
	my $first_line = <$fh_flows>;
	chomp( $first_line );
	my @column_names = split( /$flow_tool{field_sep}/, $first_line );


	# Feldernamen normalisieren (die Spaltennamen heissen je nach
	# Flow-Software unterschiedlich)
	if ( $args{format} eq 'nfdump' ) {
		@column_names = map{
			s/^da$/DstIPaddress/;
			s/^sa$/SrcIPaddress/;
			s/^ts$/Start/;		# Startzeit
			s/^te$/End/;		# Endzeit
			s/^sp$/SrcP/;		# Quellport
			s/^dp$/DstP/;		# Zielport
			s/^pr$/P/;		# Protokollnummer
			s/^ipkt$/Pkts/;		# Anzahl der Pakete
			s/^ibyt$/Octets/;	# Anzahl der Bytes
			s/^in$/Sif/;		# Eingehendes Interface (Nummer)
			s/^out$/Dif/;		# Ausgehendes Interface (Nummer)
			s/^stos$/Ts/;		# Type of Service
			$_;
		} @column_names;
	}
	#print "--$first_line--\n";
	#print "---@column_names---\n";
	#print Dumper( @column_names );


	# durch alle Floweinträge durchparsen
	while ( defined( my $line = <$fh_flows> ) ) {
		chomp( $line );

		# uninteressante Einträge überspringen
		if ( $args{format} eq 'nfdump' ) {
			last if $line eq 'Summary';
		}
		#print "--$line--\n";

		my %flow;
		@flow{ @column_names } = split( /$flow_tool{field_sep}/, $line );
		#print Dumper( %flow );

		if ( process_flow( \%flow ) ) {
			report_to_ga( \%flow );

			# kurze Pause um die Internetleitung nicht übermässig
			# zu belasten
			usleep( 1000 * ( 100 + int( rand( 400 )) ) );

			# Anzahl der berichteten Flows zÃ¤hlen
			$number_of_flows_reported++;
		}

		# Anzahl der verarbeiteten (aber eventuell nicht berichteten) Flows
		$number_of_flows_processed++;
	}

	close $fh_flows;
	quitProgram( 0 );

} else {
	print "flow command returned $?\n";
	quitProgram( 3 );
}



=head1 METHODS

=over 4

=item I<report_to_ga> I<(flow information)>

Sende den angegebenen Floweintrag an Google Analytics.
Diese Methode erwartet eine Referenz auf Floweintrag als Argument.
Rückgabewert ist undefiniert bei Erfolg und Exitcode bei Fehler.

=cut
sub report_to_ga( $ ) {
	my ( $flow_ref ) = @_;

	# ignorieren Broadcasts weil diese (hoffentlich) nicht geroutet werden
	return if $flow_ref->{ DstIPaddress } eq '255.255.255.255';

	# ignorieren Link-local IPv6 Verkehr
	return if substr( $flow_ref->{ DstIPaddress },0,6 ) eq 'fe80::';
	return if substr( $flow_ref->{ DstIPaddress },0,6 ) eq 'ff02::';

	# ignoriere Flows mit fehlender Zieladresse
	return if !$flow_ref->{ DstIPaddress } and !$flow_ref->{ DstIPaddress };

	# Subnetzmarke / Prefix von der IP-Adresse entfernen
	$flow_ref->{ SrcIPaddress } =~ s!/\d{1,2}$!!;
	$flow_ref->{ DstIPaddress } =~ s!/\d{1,2}$!!;

	# ermittle vollqualifizierten Hostname (FQDN) anhander der Zieladresse
	# bevor dieser anonymisiert wird.
	$flow_ref->{ DstHostname } = get_hostname( $flow_ref->{ DstIPaddress } );
	#printf "%s --> %s\n", $flow_ref->{ DstIPaddress }, $flow_ref->{ DstHostname };

	# echte IP-Adresse vor Google Analytics verschleiern (DATENSCHUTZ)
	$flow_ref->{ SrcIPaddress } = _anonymizeIp( $flow_ref->{ SrcIPaddress } );
	$flow_ref->{ DstIPaddress } = _anonymizeIp( $flow_ref->{ DstIPaddress } );


	# zeitliche Dauer des Flows berechnen
	my $flow_duration_sec;
	if ( $args{format} eq 'flowtools' ) {

		#  hier wirds etwas komplizierter, da flow-print den Zeitstempel in
		#  einem eigenartigen Format zurückgibt: 0803.21:45:05.124
		#  (bedeutet: 3. August um 21:45 Uhr ohne Jahresangabe)
		foreach my $timestamp ( qw/Start End/ ) {
			if ( $flow_ref->{ $timestamp } =~
				/(\d{2})(\d{2})\.(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/ ) {

				$flow_ref->{ $timestamp . '_dt' } = DateTime->new( 
					year	=> 2000,
					month	=> $1,
					day	=> $2,
					hour	=> $3,
					minute	=> $4,
					second	=> $5,
					nanosecond => 1000 * 1000 * $6
				);
			}
		}
		$flow_duration_sec =
			  $flow_ref->{ End_dt   }->hires_epoch()
			- $flow_ref->{ Start_dt }->hires_epoch();

	} elsif ( $args{format} eq 'nfdump' ) {
		$flow_duration_sec = $flow_ref->{ td };
	}

	
	# HTTP/POST-Anfrage zusammenbauen
	my $url = 'v=1&tid=' . $GA_ACCOUNT;

	# UUID für diesen NetFlow-Exporter generieren
	$url .= '&cid=' . hostname_to_uuid( $flow_ref->{ SrcIPaddress } );
	#$url .= '&cid=' . int(rand(0x7fffffff));	# sample from GA docs
	$url .= '&t=event';
	$url .= '&dp='  . uri_escape( '/' . $flow_ref->{ DstIPaddress }, '/.:' );
	$url .= '&uip=' . uri_escape( $flow_ref->{ SrcIPaddress }, '.:' );

	# diese Information findet sich später in GA > Echtzeit >  Besucherquellen
	$url .= '&dr='  . uri_escape(
		'http://' . $flow_ref->{ SrcIPaddress }, '.:/' );

	# Hostname as Seiten/Dokumententitel verwenden
	$url .= '&dt=' . uri_escape( $flow_ref->{ DstHostname }, '.-_' )
		if $flow_ref->{ DstHostname };
	
	# benutze Quell/Ziel-IP als Google Analytics "Ereignis"
	#  diese Information findet sich in GA > Echtzeit > Ereignisse
	#  Ereigniskategorie = Ziel-IP-Adresse
	#  Ereignislabel     = Quell-IP-Adresse
	#  Ereigniswert      = Übertragene Bytes (wird von GA nicht erkannt)
	$url .= "&el=" . uri_escape( $flow_ref->{ SrcIPaddress }, '.' );
	$url .= "&ec=" . uri_escape( $flow_ref->{ DstIPaddress }, '.' );
	$url .= "&ev=" . $flow_ref->{ Octets };
	$url .= '&ea=' . uri_escape( $flow_ref->{ DstHostname }, '.-_' )
		if $flow_ref->{ DstHostname };

	# benutze Ziel-IP-Adresse als Google Analytics "Inhalt"
	$url .= "&dl=" . uri_escape( $flow_ref->{ DstIPaddress }, '.' );

	## alle anderen Flowvariablen werden zur "Benutzerdefinierte Dimension"
	#    führende Nullen hinzufügen, damit GA korrekt sortiert
	$url .= "&cd1=" . uri_escape( $flow_ref->{ SrcIPaddress }, '.' );
	$url .= "&cd2=" . uri_escape( $flow_ref->{ DstIPaddress }, '.' );

	## Ganzzahl-basierte Flowwerte als "Benutzerdefinierte Messwerte"
	$url .= "&cd3=" . $flow_ref->{ SrcP };	# Quellport
	$url .= "&cd4=" . $flow_ref->{ DstP };	# Zielport
	$url .= "&cm1=" . $flow_ref->{ Pkts };	# Anzahl der Pakete
	$url .= "&cm2=" . $flow_ref->{ Octets };# Anzahl der Bytes
	if ( $args{format} eq 'flowtools' ) {
		$url .= "&cd5=" . uc( getprotobynumber( $flow_ref->{ P } ) );
	} elsif ( $args{format} eq 'nfdump' ) {
		$url .= "&cd5=" . $flow_ref->{ P };
	}
	$url .= "&cd6=" . uri_escape( $flow_info->{ exporter_ip }, '.' );
	$url .= "&cd7=" . $flow_info->{ Sif };	# eingehendes Interface
	$url .= "&cd8=" . $flow_info->{ DIf };	# ausgehendes Interface
	$url .= sprintf "&cd9=%d", $flow_info->{ Ts };	# Type of Service (QoS)

	## speicher die Dauer der Verbindung als "Benutzerdefinierte Messwerte"
	# GA Type "Zeit" verwendet Sekunden als Einheit
	$url .= sprintf "&cm3=%.0f", $flow_duration_sec;

	# cm4 is vom Typ "Ganzzahl" und wird hier als Millisekunden verwendet
	$url .= "&cm4=" . int( $flow_duration_sec * 1000 );

	if ( $args{debug} ) {
		#$args{debug} && syslog( "LOG_DEBUG|$syslog_facility",
		#	sprintf "[%04d] GA-URL %s?%s", $script_id, $GA_URL, $url );
		#printf "GA-URL %s?%s\n", $GA_URL, $url;
	}

	# Google Analytics POST-Zugriff an syslog melden
	if ( ! defined $args{dryrun} ) {
		# sende die Anfrage an GA
		return ga_http_request( $url );
	}
}



=item I<ga_http_request> I<(post string)>

Führt eine HTTP-Anfrage an den Google Analytics Server durch und sendet den
angegebenen String als POST-Request.
Diese Methode erwartet den POST-String als Argument.
Rückgabewert ist undefiniert bei Erfolg und der HTTP-Errorcode bei Fehler.

=cut
sub ga_http_request( $ ) {
	my ( $url ) = @_;

	# UserAgent-Object erstellen
	my $ua = LWP::UserAgent->new;
	$ua->timeout( 3 );
	$ua->agent( $USER_AGENT );

	# sende Anfrage an Google Analytics Server
	my $req = HTTP::Request->new( POST => $GA_URL );
	$req->content( $url );
	my $res	= $ua->request( $req );

	# Fehlerbehandlung
	if ( $res->is_error() ) { 
		printf "Error %d: %s\n", $res->code(), $res->message();
		$flow_info->{ 'ga_error' } ++;
		return $res->code();
	} else {
		return undef;
	}
}



=item I<process_flow> I<(Flow)>

Entscheidet ob ein Flow an GA gesendet werden soll oder nicht.
Diese Methode erwartet eine Referenz auf den Floweintrag als Argument.
Rückgabewert ist WAHR wenn der Flow an GA berichtet werden soll und FALSCH
wenn der Flow ignoriert werden soll.

=cut
sub process_flow( $ ) {
	my ( $flow_ref ) = @_;

	# prüfe ob die Sampling-Rate genutzt wird
	if ( $number_of_flows_processed % $sampling_rate_N ) {
		#printf "flow #%d übersprungen aufgrund der Samplingrate von %s\n",
		#	$number_of_flows_processed, $sampling_rate_N;
		return 0;		# skip this flow

	# diesen Flow überspringen wenn weniger als 100 Bytes beinhaltet
	# (hierbei handelt es sich wahrscheinlich um DNS-Anfragen oder pings)
	} elsif ( $flow_ref->{ Octets } < 100 ) {
		return 0;		# Flow überspringen

	} elsif ( grep{ in_subnet( $_, $flow_ref->{ SrcIPaddress } )
				or  in_subnet( $_, $flow_ref->{ DstIPaddress } )
			} @hide_ip_from_reporting ) {
		return 0;
	}

	return 1;			# Flow berücksichtigen
}



=item I<quitProgram> I<(exit code, error description)>

Beendet das Programm und liefert eine Zusammenfassung.
Diese Methode erwartet einen Fehlercode und eine optionale Fehlerbeschreibung.

=cut
sub quitProgram( $ ) {
	my ( $exit_code ) = @_;

	# Fehlercode ist leer wenn der Aufruf vom Signalhandler stammt
	$exit_code = 0 if ! defined $exit_code;

	if ( $args{'debug'} ) { 
		show_progress();
		my $runtime = time() - $t0;

		# bringe eine Warnung wenn die Laufzeit die Dauer der
		# Flows überschreitet
		if ( $flow_info->{ 'duration' } > 0 and
		     $runtime > $flow_info->{ 'duration' } ) {
			$args{debug} && syslog( "LOG_WARNING|$syslog_facility",
				sprintf "[%04d] Laufzeit übersteigt Dauer aller Flows".
						" um %d Sekunden (%d%%)",
					$script_id, $runtime - $flow_info->{ 'duration' },
					$runtime / $flow_info->{ 'duration' } * 100
			);
		}

		# prüfe auf unbearbeitete Flows: dies ist ein Indikator dass mehr
		# Flows vom Collector gesammelt wurden als an Google Analytics
		# berichtet wurde.
		if ( $number_of_flows_processed < $flow_info->{ 'total_flows' } ) {
			$args{debug} && syslog( "LOG_WARNING|$syslog_facility",
				"[$script_id] Skript beendet mit unbearbeiteten Flows" );
		}

		syslog( "LOG_INFO|$syslog_facility",
			sprintf "[%04d] beendet nach %d sec (Exitcode %d)%s",
				$script_id, $runtime, $exit_code,
				$! ? " with '$!'" : ''		# $! ist der Signaltyp
		);
		closelog();
	}
	exit( $exit_code );
}



=item I<_anonymizeIp> I<(IPv4/IPv6-Adresse)>

Anonymisiert die IP-Adresse anhang eines einfachen Algorithmus.
Diese Methode erwartet eine IPv4 oder IPv6-Adresse und liefert die
anonymisierte Adresse als Rückgabewert.

=cut
sub _anonymizeIp( $ ) {
	my ( $addr ) = @_;

	# Anonymisierung (vorübergehend) deaktivieren?
	#return $addr;	

	# IP Version 4
	if ( $addr =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ ) {
		my @ip = split( /\./, $addr, 4 );

		# Adresse nicht verändern im Fall von Multicast
		if ( in_subnet( '224.0.0.0/4', $addr ) ) {
			return $addr;
		} else {
			# Die "Google Analytics" Methode: letztes Oktett entfernen
			#return join( '.', ( $ip[0], $ip[1], $ip[2], 0 ) );

			# einfache Transformation: zweites und viertes Oktett invertieren
			#return join( '.', ( $ip[0], 255-$ip[1], $ip[2], 255-$ip[3] ) );

			# komplizierte Transformation
			#$ip[1] = 255 - $ip[1];
			#$ip[1] += 109; $ip[1] -= 256 if $ip[1] > 255;
			#$ip[2] += 34;  $ip[2] -= 256 if $ip[2] > 255;
			#return join( '.', @ip );

			# noch einfachere Transformation: zweites Oktett invertieren
			return join( '.', ( $ip[0], 255-$ip[1], $ip[2], $ip[3] ) );
		}

	# IP Version 6
	} else {
		# IPv6 Adresse in Kleinbuchstaben umwandeln, so wie in RFC 5952 angegeben
		# https://tools.ietf.org/html/rfc5952#section-4.3
		$addr = lc( $addr );

		# alle Zeichen nach :: löschen
		$addr =~ s/::.*/::/;

		# alles nach dem dritten Segment löschen
		$addr =~ s/^(([\da-f]{1,4}:){3}).*/$1:/;

		my @ip = split( /\:/, $addr );

		# einfache Transformation: zweites und viertes Segment invertieren
		return lc join( ':', (  $ip[0], sprintf("%x", 0xffff-$ip[1]),
					sprintf("%x", 0xffff-$ip[2]), $ip[3..-1] ) );
	}
}



=item I<show_progress> I<()>

Berichte Fortschritt und Laufzeitinformationen an Syslog.
Diese Methode erwartet kein Argument und liefert keinen Rückgabewert.

=cut
sub show_progress( ) {
	my $runtime = time() - $t0;

	# prüfe ob alle Werte grösser als Null sind
	# (verhintert den Fehler "Illegal division by zero")
	if ( $flow_info->{ 'total_flows' } and $flow_info->{ 'duration' } ) {
		syslog( "LOG_INFO|$syslog_facility", sprintf(
			"[%04d] %d/%d/%d Flows berichtet/verarbeitet/gesamt ".
			"in %d/%d (%d%%) Sekunden\n",
				$script_id,
				$number_of_flows_reported,
				$number_of_flows_processed,
				$flow_info->{ 'total_flows' },

				#$number_of_flows_processed / $flow_info->{ 'total_flows' } *100,
				$runtime, $flow_info->{ 'duration' },
				$runtime / $flow_info->{ 'duration' } * 100
			)
		);

	# keine Informationen über Flowdatei gefunden? Einfache Statistiken anzeigen
	} else {
		syslog( "LOG_INFO|$syslog_facility",
			sprintf( "[%04d] %d/%d Flows berichtet/verarbeitet in %d Sekunden\n",
				$script_id,
				$number_of_flows_reported,
				$number_of_flows_processed, $runtime )
		);
	}
	if ( $flow_info->{ 'ga_error' } > 0 ) {
		syslog( "LOG_INFO|$syslog_facility", sprintf(
			"[%04d] Error während upload an GA: %d\n",
				$script_id, $flow_info->{ 'ga_error' } ));
	}

	return undef;
}



=item I<get_flow_info> I<(filename)>

Ermittle statistische Informationen über eine Flowdatei.
Diese Methode erwartet einen Dateienamen als Argument und liefert eine
Referenz auf ein Hasharray zurück, welches die Infos über die Datei enthält.

=cut
sub get_flow_info( $ ) {
	my ( $file ) = @_;
	return undef if !$file;
	my %flow_stat;

	# sammle Informationen über die Flowdatei
	#	$flow_stat{ total_flows }
	#	$flow_stat{ duration }
	#	$flow_stat{ exporter_ip }

	if ( $args{format} eq 'flowtools' ) {
		my @flow_stat_output = `$FLOWTOOLS_BINDIR/flow-stat -f0 < $file`;

		foreach my $line ( @flow_stat_output ) {
			chomp( $line );
			if ( $line =~ /^Total Flows\s+:\s+(\d+)$/ ) {
				$flow_stat{ total_flows } = $1;
			} elsif ( $line =~ /^Duration of data\s+\(realtime\)\s+:\s+(\d+)$/ ) {
				$flow_stat{ duration } = $1;
			}
		}

		# NetFlow-Exporter IP-Adresse ermitteln
		$flow_stat{ exporter_ip } =
			`$FLOWTOOLS_BINDIR/flow-stat -f27 < $file |tail -1 |awk '{print \$1}'`;
		chomp( $flow_stat{ exporter_ip } );

	} elsif ( $args{format} eq 'nfdump' ) {
		my @nfdump_output = `$FLOWTOOLS_BINDIR/nfdump -I -r $file`;

		for my $line ( @nfdump_output ) {
			chomp( $line );
			if ( $line =~ /^Flows:\s+(\d+)$/ ) {
				$flow_stat{ total_flows } = $1;
			} elsif ( $line =~ /^First:\s+(\d+)$/ ) {
				$flow_stat{ first_flow } = $1;
			} elsif ( $line =~ /^Last:\s+(\d+)$/ ) {
				$flow_stat{ duration } = $1 - $flow_stat{ first_flow };
				undef $flow_stat{ first_flow };
			}
		}

		# NetFlow-Exporter IP-Adresse ermitteln
		$flow_stat{ exporter_ip } =
			`$FLOWTOOLS_BINDIR/nfdump -o csv -q -c 1 -r $file | awk 'BEGIN { FS = "," } ; { print \$45 }'`;
		chomp( $flow_stat{ exporter_ip } );
	}

	# GA Account je nach NetFlow-Exporter wechseln
	if ( $NF_GA_ACCOUNT{ $flow_stat{ exporter_ip } } ) {
		$GA_ACCOUNT = $NF_GA_ACCOUNT{ $flow_stat{ exporter_ip } };
	}

	return \%flow_stat;
}



=item I<hostname_to_uuid> I<(Hostname)>

Generiere UUID basierend auf einem Hostnamen.
Diese Methode erwartet einen Hostname (Zeichenkette) als Argument und liefert
die UUID zurück.

=cut
sub hostname_to_uuid( $ ) {
	my ( $hostname ) = @_;
	my ( $uuid, $md5 );

	# Benutze MD5 um die UUID zu erstellen. MD5 ist eine akzeptable Wahl da
	# seine Länge von 128 Bits zur Länge der UUID passt.
	# Diese Funktion ist nicht sicherheitkritisch, so dass der robustere
	# Algorithmus SHA1 erstmal vernachlässigt werden kann.
	my $md5 = Digest::MD5->new; 
	$md5->add( $hostname );
	$uuid = $md5->hexdigest;

	return sprintf( "%8s-%4s-%4s-%4s-%12s",
		substr( $uuid,0,8 ), substr( $uuid,8,4 ), substr( $uuid,12,4 ),
		substr( $uuid,16,4 ), substr( $uuid,20,12 )
	);
}



=item $boolean = in_subnet( Subnetz, IP-Adresse );

PrÃ¼fe ob die angegebene Adresse ins Subnetz passt. Subnetz wird definiert als
einzelne IP-Adresse oder CIDR-Block. Die CIDR-Kurzschreibweise (192.168/16)
wird nicht unterstützt.
 in_subnet( Subnetz,IP ), e.g. in_subnet('192.168.0.0/24', '192.168.0.3') = 1
   Hinweis: diese Methode wurde von der excellenten Software "netdisco"
            version 0.94 übernommen (http://www.netdisco.org)

=cut
sub in_subnet ($$) {
	my ( $subnet,$ip ) = @_;

	unless ( defined $subnet and defined $ip ) {
		return undef;
	}

	# No / just see if they're equal
	if ($subnet !~ m!/!) {
		return ($subnet eq $ip) ? 1 : 0;
	}

	# Parse Subnet
	my ($root,$bits);
	if ($subnet =~ /^([\d\.]+)\/(\d+)$/){
		($root,$bits) = ($1,$2);
	} else {
		return undef;
	}

	my $root_bin = unpack("N",pack("C4",split(/\./,$root)));
	my $mask = 2**32 - (2 ** (32 - $bits));

	# Parse IP
	my $ip_bin = unpack("N",pack("C4",split(/\./,$ip)));

	return (($ip_bin & $mask) == ($root_bin & $mask)) ? 1 : 0;
}



=item I<get_hostname> I<(address)>

Ermittle den Hostnamen anhand der IP-Adresse.
Diese Methode erwartet eine IP-Adresse als Argument und liefert den Hostnamen.

=cut
sub get_hostname( $ ) {
	my ( $addr ) = @_;

	my $hostname = $dns_cache{ $addr }
		||= gethostbyaddr( pack( "C4", split( /\./, $addr ) ), 2 );
	$hostname = lc( $hostname );	# convert to lower-case

	if ( grep{ $hostname =~ s/\.?$_$// } @dns_domain_names ) {
		# kleine Transformationstabelle um den Hostnamen unkenntlich zu machen
		$hostname =~ tr/abcdefghijklmnopqrstuvwxyz/wqretziuodpasfghjlykxvcbnm/;
		$hostname =~ tr/0123456789/1357924680/;
	}

	return $hostname;
}


__END__

=back

=head1 NAME

flow-ga.pl - Berichte NetFlow-Informationen an Google Analytics.

=head1 SYNOPSIS

flow-ga.pl Flowdatei

=head1 BESCHREIBUNG

Das flow-ga Tool berichtet jeden Eintrag in einer Flowdatei an Google
Analytics unter Verwendung des Measurement Protokolls.

Dieses Utility ist optimiert für die Verwendung von flow-capture (Option -R)
oder nfcapd (Option -x). Diese Optionen sorgen dafür, dass die Flow-Collector
Software dazu flow-ga.pl aufruft, sobald eine Flowdatei geschlossen/beendet wurde.

Mit dem Signal SIGUSR1 berichtet flow-ga.pl den aktuellen Status an Syslog.

=head1 OPTIONEN

B<-h>, B<--debug> I<level>, B<--dryrun>, B<--format> I<style>

=head1 BEISPIELE

Starte das Tool zum Berichten aller Flows aus der Datei /flows/krc4

flow-ga.pl /flows/krc4

=head1 BUGS

Keine bekannt.

=head1 AUTOR

Markus Stubbig

=head1 SEE ALSO

flow-tools(1), nfdump(1), nfcapd(1)

=cut
