fwknop/test/test-fwknop.pl

725 lines
19 KiB
Perl
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/perl -w
use Data::Dumper;
use Getopt::Long 'GetOptions';
use strict;
#==================== config =====================
my $logfile = 'test.log';
my $output_dir = 'output';
my $conf_dir = 'conf';
my $default_conf = "$conf_dir/default_fwknopd.conf";
my $default_access_conf = "$conf_dir/default_access.conf";
my $fwknopCmd = '../client/.libs/fwknop';
my $fwknopdCmd = '../server/.libs/fwknopd';
my $libfko_bin = '../lib/.libs/libfko.so.0.0.3';
#================== end config ===================
my $passed = 0;
my $failed = 0;
my $executed = 0;
my $test_include = '';
my @tests_to_include = ();
my $test_exclude = '';
my @tests_to_exclude = ();
my $list_mode = 0;
my $firewall = '';
my $loopback_intf = 'lo'; ### default on linux
my $prepare_results = 0;
my $current_test_file = '';
my $help = 0;
my $YES = 1;
my $NO = 0;
my $APPEND = 1;
my $CREATE = 0;
my $PRINT_LEN = 68;
my $REQUIRED = 1;
my $OPTIONAL = 0;
exit 1 unless GetOptions(
'Prepare-results' => \$prepare_results,
'fwknop-path=s' => \$fwknopCmd,
'fwknopd-path=s' => \$fwknopdCmd,
'firewall=s' => \$firewall,
'libfko-path=s' => \$libfko_bin,
'loopback-intf=s' => \$loopback_intf,
'test-include=s' => \$test_include,
'include=s' => \$test_include, ### synonym
'test-exclude=s' => \$test_exclude,
'exclude=s' => \$test_exclude, ### synonym
'List-mode' => \$list_mode,
'help' => \$help
);
&usage() if $help;
### main array that defines the tests we will run
my @tests = (
{
'category' => 'build',
'detail' => 'recompile and look for compilation warnings',
'err_msg' => 'compile warnings exist',
'function' => \&compile_warnings,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'client',
'detail' => 'new binary exists',
'err_msg' => 'binary not found',
'function' => \&binary_exists,
'binary' => $fwknopCmd,
'fatal' => $YES
},
{
'category' => 'build',
'subcategory' => 'client',
'detail' => 'Position Independent Executable (PIE)',
'err_msg' => 'non PIE binary (fwknop client)',
'function' => \&pie_binary,
'binary' => $fwknopCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'client',
'detail' => 'stack protected binary',
'err_msg' => 'non stack protected binary (fwknop client)',
'function' => \&stack_protected_binary,
'binary' => $fwknopCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'client',
'detail' => 'fortify source functions',
'err_msg' => 'source functions not fortified (fwknop client)',
'function' => \&fortify_source_functions,
'binary' => $fwknopCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'client',
'detail' => 'read-only relocations',
'err_msg' => 'no read-only relocations (fwknop client)',
'function' => \&read_only_relocations,
'binary' => $fwknopCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'client',
'detail' => 'immediate binding',
'err_msg' => 'no immediate binding (fwknop client)',
'function' => \&immediate_binding,
'binary' => $fwknopCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'server',
'detail' => 'new binary exists',
'err_msg' => 'binary not found',
'function' => \&binary_exists,
'binary' => $fwknopdCmd,
'fatal' => $YES
},
{
'category' => 'build',
'subcategory' => 'server',
'detail' => 'Position Independent Executable (PIE)',
'err_msg' => 'non PIE binary (fwknopd server)',
'function' => \&pie_binary,
'binary' => $fwknopdCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'server',
'detail' => 'stack protected binary',
'err_msg' => 'non stack protected binary (fwknopd server)',
'function' => \&stack_protected_binary,
'binary' => $fwknopdCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'server',
'detail' => 'fortify source functions',
'err_msg' => 'source functions not fortified (fwknopd server)',
'function' => \&fortify_source_functions,
'binary' => $fwknopdCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'server',
'detail' => 'read-only relocations',
'err_msg' => 'no read-only relocations (fwknopd server)',
'function' => \&read_only_relocations,
'binary' => $fwknopdCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'server',
'detail' => 'immediate binding',
'err_msg' => 'no immediate binding (fwknopd server)',
'function' => \&immediate_binding,
'binary' => $fwknopdCmd,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'libfko',
'detail' => 'new binary exists',
'err_msg' => 'binary not found',
'function' => \&binary_exists,
'binary' => $libfko_bin,
'fatal' => $YES
},
{
'category' => 'build',
'subcategory' => 'libfko',
'detail' => 'stack protected binary',
'err_msg' => 'non stack protected binary (libfko)',
'function' => \&stack_protected_binary,
'binary' => $libfko_bin,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'libfko',
'detail' => 'fortify source functions',
'err_msg' => 'source functions not fortified (libfko)',
'function' => \&fortify_source_functions,
'binary' => $libfko_bin,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'libfko',
'detail' => 'read-only relocations',
'err_msg' => 'no read-only relocations (libfko)',
'function' => \&read_only_relocations,
'binary' => $libfko_bin,
'fatal' => $NO
},
{
'category' => 'build',
'subcategory' => 'libfko',
'detail' => 'immediate binding',
'err_msg' => 'no immediate binding (libfko)',
'function' => \&immediate_binding,
'binary' => $libfko_bin,
'fatal' => $NO
},
{
'category' => 'preliminaries',
'subcategory' => 'client',
'detail' => 'usage info',
'err_msg' => 'could not get usage info',
'function' => \&usage_info,
'binary' => $fwknopCmd,
'fatal' => $NO
},
{
'category' => 'preliminaries',
'subcategory' => 'client',
'detail' => 'getopt() no such argument',
'err_msg' => 'getopt() allowed non-existant argument',
'function' => \&no_such_arg,
'binary' => $fwknopCmd,
'fatal' => $NO
},
{
'category' => 'preliminaries',
'subcategory' => 'client',
'detail' => 'expected code version',
'err_msg' => 'code version mis-match',
'function' => \&expected_code_version,
'cmdline' => "$fwknopCmd --version",
'fatal' => $NO
},
{
'category' => 'preliminaries',
'subcategory' => 'server',
'detail' => 'usage info',
'err_msg' => 'could not get usage info',
'function' => \&usage_info,
'binary' => $fwknopdCmd,
'fatal' => $NO
},
{
'category' => 'preliminaries',
'subcategory' => 'server',
'detail' => 'getopt() no such argument',
'err_msg' => 'getopt() allowed non-existant argument',
'function' => \&no_such_arg,
'binary' => $fwknopdCmd,
'fatal' => $NO
},
{
'category' => 'preliminaries',
'subcategory' => 'server',
'detail' => 'expected code version',
'err_msg' => 'code version mis-match',
'function' => \&expected_code_version,
'cmdline' => "$fwknopdCmd -c $default_conf -a $default_access_conf --version",
'fatal' => $NO
},
{
'category' => 'preliminaries',
'detail' => 'collecting system specifics',
'err_msg' => 'could not get complete system specs',
'function' => \&specs,
'binary' => $fwknopdCmd,
'fatal' => $NO
},
{
'category' => 'basic operations',
'detail' => 'dump config',
'err_msg' => 'could not dump configuration',
'function' => \&dump_config,
'cmdline' => "$fwknopdCmd -c $default_conf -a $default_access_conf --dump-config",
'fatal' => $NO
},
{
'category' => 'basic operations',
'detail' => 'client SPA packet generation',
'err_msg' => 'could not generate SPA packet',
'function' => \&generate_basic_spa_packet,
'fatal' => $YES
}
);
my %test_keys = (
'category' => $REQUIRED,
'subcategory' => $OPTIONAL,
'detail' => $REQUIRED,
'function' => $REQUIRED,
'binary' => $OPTIONAL,
'cmdline' => $OPTIONAL,
'fatal' => $OPTIONAL,
);
### make sure everything looks as expected before continuing
&init();
&logr("\n[+] Starting the fwknop test suite (firewall: $firewall)...\n\n");
for my $test_hr (@tests) {
&run_test($test_hr);
}
&logr("\n[+] passed/failed/executed: $passed/$failed/$executed tests\n\n");
exit 0;
#===================== end main =======================
sub run_test() {
my $test_hr = shift;
return unless &process_include_exclude($test_hr);
my $msg = "[$test_hr->{'category'}]";
$msg .= " [$test_hr->{'subcategory'}]" if $test_hr->{'subcategory'};
$msg .= " $test_hr->{'detail'}";
if ($list_mode) {
print $msg, "\n";
return;
}
&dots_print($msg);
$executed++;
$current_test_file = "$output_dir/$executed.test";
if (&{$test_hr->{'function'}}($test_hr)) {
&logr("pass ($executed)\n");
$passed++;
} else {
&logr("fail ($executed)\n");
$failed++;
if ($test_hr->{'fatal'} eq $YES) {
die "[*] required test failed, exiting.";
}
}
return;
}
sub process_include_exclude() {
my $test_hr = shift;
### inclusions/exclusions
if (@tests_to_include) {
my $found = 0;
for my $test (@tests_to_include) {
if ($test_hr->{'category'} =~ /$test)/) {
$found = 1;
last;
}
}
return 1 unless $found;
}
if (@tests_to_exclude) {
my $found = 0;
for my $test (@tests_to_exclude) {
if ($test_hr->{'category'} =~ /$test/) {
$found = 1;
last;
}
}
return 0 if $found;
}
return 1;
}
sub generate_basic_spa_packet() {
return 1;
}
sub compile_warnings() {
return 1;
return 0 unless &run_cmd('make -C .. clean', $CREATE);
return 0 unless &run_cmd('make -C ..', $APPEND);
### look for compilation warnings - something like:
### warning: test is used uninitialized in this function
return 0 if &file_find_regex([qr/\swarning:\s/], $current_test_file);
return 1;
}
sub binary_exists() {
my $test_hr = shift;
return 0 unless $test_hr->{'binary'};
return 0 unless -e $test_hr->{'binary'} and -x $test_hr->{'binary'};
return 1;
}
sub expected_code_version() {
my $test_hr = shift;
unless (-e '../VERSION') {
&write_test_file("[-] ../VERSION file does not exist.\n", $CREATE);
return 0;
}
open F, "< ../VERSION" or die $!;
my $line = <F>;
close F;
if ($line =~ /(\d.*\d)/) {
my $version = $1;
return 0 unless &run_cmd($test_hr->{'cmdline'}, $APPEND);
return 1 if &file_find_regex([qr/$version/], $current_test_file);
}
return 0;
}
sub dump_config() {
my $test_hr = shift;
return 0 unless &run_cmd($test_hr->{'cmdline'}, $CREATE);
return 1;
}
sub usage_info() {
my $test_hr = shift;
return 0 unless $test_hr->{'binary'};
return 0 unless &run_cmd("$test_hr->{'binary'} -h", $CREATE);
return 1;
}
sub no_such_arg() {
my $test_hr = shift;
return 0 unless $test_hr->{'binary'};
return 0 if &run_cmd("$test_hr->{'binary'} --no-such-arg", $CREATE);
return 1;
}
### check for PIE
sub pie_binary() {
my $test_hr = shift;
return 0 unless $test_hr->{'binary'};
&run_cmd("./hardening-check $test_hr->{'binary'}", $CREATE);
return 0 if &file_find_regex([qr/Position\sIndependent.*:\sno/i],
$current_test_file);
return 1;
}
### check for stack protection
sub stack_protected_binary() {
my $test_hr = shift;
return 0 unless $test_hr->{'binary'};
&run_cmd("./hardening-check $test_hr->{'binary'}", $CREATE);
return 0 if &file_find_regex([qr/Stack\sprotected.*:\sno/i],
$current_test_file);
return 1;
}
### check for fortified source functions
sub fortify_source_functions() {
my $test_hr = shift;
return 0 unless $test_hr->{'binary'};
&run_cmd("./hardening-check $test_hr->{'binary'}", $CREATE);
return 0 if &file_find_regex([qr/Fortify\sSource\sfunctions:\sno/i],
$current_test_file);
return 1;
}
### check for read-only relocations
sub read_only_relocations() {
my $test_hr = shift;
return 0 unless $test_hr->{'binary'};
&run_cmd("./hardening-check $test_hr->{'binary'}", $CREATE);
return 0 if &file_find_regex([qr/Read.only\srelocations:\sno/i],
$current_test_file);
return 1;
}
### check for immediate binding
sub immediate_binding() {
my $test_hr = shift;
return 0 unless $test_hr->{'binary'};
&run_cmd("./hardening-check $test_hr->{'binary'}", $CREATE);
return 0 if &file_find_regex([qr/Immediate\sbinding:\sno/i],
$current_test_file);
return 1;
}
sub specs() {
my $cmd = '';
if ($firewall eq 'iptables') {
$cmd = "iptables -v -n -L";
} else {
$cmd = "ipfw list";
}
&run_cmd($cmd, $CREATE);
for my $cmd (
'uname -a',
'uptime',
'ifconfig -a',
'ls -l /etc', 'if [ -e /etc/issue ]; then cat /etc/issue; fi',
'if [ `which iptables` ]; then iptables -V; fi',
'if [ -e /proc/cpuinfo ]; then cat /proc/cpuinfo; fi',
'if [ -e /proc/config.gz ]; then zcat /proc/config.gz; fi',
'if [ `which gpg` ]; then gpg --version; fi',
'if [ `which tcpdump` ]; then ldd `which tcpdump`; fi',
"ldd $fwknopCmd",
"ldd $fwknopdCmd",
"ldd $libfko_bin",
'ls -l /usr/lib/*pcap*',
'ls -l /usr/local/lib/*pcap*',
'ls -l /usr/lib/*fko*',
'ls -l /usr/local/lib/*fko*',
) {
&run_cmd($cmd, $APPEND);
}
return 1;
}
sub run_cmd() {
my ($cmd, $file_mode) = @_;
if ($file_mode == $APPEND) {
open F, ">> $current_test_file"
or die "[*] Could not open $current_test_file: $!";
print F "CMD: $cmd\n";
close F;
} else {
open F, "> $current_test_file"
or die "[*] Could not open $current_test_file: $!";
print F "CMD: $cmd\n";
close F;
}
my $rv = ((system "$cmd >> $current_test_file 2>&1") >> 8);
if ($rv == 0) {
return 1;
}
return 0;
}
sub dots_print() {
my $msg = shift;
&logr($msg);
my $dots = '';
for (my $i=length($msg); $i < $PRINT_LEN; $i++) {
$dots .= '.';
}
&logr($dots);
return;
}
sub init() {
### validate test hashes
my $hash_num = 0;
for my $test_hr (@tests) {
for my $key (keys %test_keys) {
if ($test_keys{$key} == $REQUIRED) {
die "[*] Missing '$key' element in hash: $hash_num"
unless defined $test_hr->{$key};
} else {
$test_hr->{$key} = '' unless defined $test_hr->{$key};
}
}
$hash_num++;
}
$|++; ### turn off buffering
$< == 0 && $> == 0 or
die "[*] $0: You must be root (or equivalent ",
"UID 0 account) to effectively test fwknop";
die "[*] $conf_dir directory does not exist." unless -d $conf_dir;
die "[*] default config $default_conf does not exist" unless -e $default_conf;
die "[*] default access config $default_access_conf does not exist"
unless -e $default_access_conf;
unless (-d $output_dir) {
mkdir $output_dir or die "[*] Could not mkdir $output_dir: $!";
}
for my $file (glob("$output_dir/*.test")) {
unlink $file or die "[*] Could not unlink($file)";
}
if (-e $logfile) {
unlink $logfile or die $!;
}
if ($test_include) {
@tests_to_include = split /\s*,\s*/, $test_include;
}
if ($test_exclude) {
@tests_to_exclude = split /\s*,\s*/, $test_exclude;
}
### make sure no fwknopd instance is currently running
die "[*] Please stop the running fwknopd instance."
if &is_fwknopd_running();
unless ((&find_command('cc') or &find_command('gcc')) and &find_command('make')) {
### disable compilation checks
push @tests_to_exclude, 'build';
}
### detect the installed firewall
&detect_firewall();
return;
}
sub is_fwknopd_running() {
my $cmd = "$fwknopdCmd -c $default_conf -a $default_access_conf --status";
my $is_running = 1;
open C, "$cmd |" or die "[*] Could not execute $cmd: $!";
while (<C>) {
if (/no\s+running/i) {
$is_running = 0;
last;
}
}
close C;
return $is_running;
}
sub detect_firewall() {
unless ($firewall) {
for my $fw qw/iptables pf ipfw/ {
my $path = &find_command($fw);
if ($path) {
$firewall = $path;
last;
}
}
}
unless ($firewall) {
die "[*] Could not find firewall binary, use --firewall";
}
return;
}
sub file_find_regex() {
my ($re_ar, $file) = @_;
my $found = 0;
open F, "< $file" or die "[*] Could not open $file: $!";
LINE: while (<F>) {
my $line = $_;
for my $re (@$re_ar) {
if ($line =~ $re) {
$found = 1;
last LINE;
}
}
}
close F;
return $found;
}
sub find_command() {
my $cmd = shift;
my $path = '';
open C, "which $cmd |" or die "[*] Could not execute: which $cmd: $!";
while (<C>) {
if (m|^(/.*$cmd)$|) {
$path = $1;
last;
}
}
close C;
return $path;
}
sub write_test_file() {
my ($msg, $file_mode) = @_;
if ($file_mode == $APPEND) {
open F, ">> $current_test_file"
or die "[*] Could not open $current_test_file: $!";
print F $msg;
close F;
} else {
open F, "> $current_test_file"
or die "[*] Could not open $current_test_file: $!";
print F $msg;
close F;
}
return;
}
sub logr() {
my $msg = shift;
print STDOUT $msg;
open F, ">> $logfile" or die $!;
print F $msg;
close F;
return;
}