After reading the "Packet Logging Through Syslog" section of the pf FAQ
I decided to try a different approach. Now that it's working (for my
system and needs) I'm wondering 1) Is it (relatively) safe? 2) Is it
useful to others? and 3) Did I re-invent something already available I
missed?
Here's a quick description.
#!/usr/bin/perl -WT
use strict;
#-------------------------------------------------------------------------------
#
# pflogger logs to syslog(3), IN REAL TIME, packets logged by pf(4).
# This is done by by invoking tcpdump(8) on the pflog(4) interface
# and piping the output to logger(1).
#
# In effect pflogger is really just the shell command:
#
# (tcpdump -lent -i pflog0 2>&1 | \
# logger -p user.info -t pflogger) >/dev/null 2>&1 &
#
# but with both tcpdump and logger running as daemon processes, logger
running
# unprivileged, and no shell process providing glue.
# Once setup even the perl interpreter process running this script is gone.
#
#------------------------------------------------------------------------------
See attachment for full script.
Thanks,
--
_ _ _
__| | __ _ _ __ | |__ __ _ ___ ___| | ___ _ __
/ _` |/ _` | '_ \ | '_ \ / _` / __/ __| |/ _ \ '__|
| (_| | (_| | | | | | | | | (_| \__ \__ \ | __/ |
\__,_|\__,_|_| |_| |_| |_|\__,_|___/___/_|\___|_|
[EMAIL PROTECTED]
#!/usr/bin/perl -WT
use strict;
#-------------------------------------------------------------------------------
#
# pflogger logs to syslog(3), IN REAL TIME, packets logged by pf(4).
# This is done by by invoking tcpdump(8) on the pflog(4) interface
# and piping the output to logger(1).
#
# In effect pflogger is really just the shell command:
#
# (tcpdump -lent -i pflog0 2>&1 | \
# logger -p user.info -t pflogger) >/dev/null 2>&1 &
#
# but with both tcpdump and logger running as daemon processes, logger running
# unprivileged, and no shell process providing glue.
# Once setup even the perl interpreter process running this script is gone.
#
#-------------------------------------------------------------------------------
use constant LOGGRP => '_syslogd'; # unprivileged group
use constant LOGUSR => '_syslogd'; # unprivileged user
use constant TCPUSR => '_tcpdump'; # tcpdump unprivileged
use constant PIDFILE => '/var/run/pflogger.pid'; # daemon's pidfile
use constant SETSID => 147; # from <sys/syscall.h>
use constant MAXFD => 1023; # open files hard limit
use constant CDDIR => '/var/empty'; # daemon's directory
use constant DMASK => 0177; # daemon's umask
#use ABSOLUTELY_NOTHING_ELSE # a design goal
#require ABSOLUTELY_NOTHING # a design goal
$ENV{PATH} = '/usr/sbin:/usr/bin'; # taint safe path
if (scalar @ARGV) { die "USE: pflogger\nNo arguments!\n"; } # USE
if ( $< ) { die "pflogger: got root?\n"; } # need root!
my $lgid;
if ( ( $lgid = getgrnam(LOGGRP) ) < 0 ) { die "pflogger: getgrnam: $!\n"; }
my $luid;
if ( ( $luid = getpwnam(LOGUSR) ) < 0 ) { die "pflogger: getpwnam: $!\n"; }
if ( -e PIDFILE ) {
&check_pidfile() or die "pflogger: check_pidfile error\n";
}
&daemonize() or die "pflogger: daemonize error\n";
my $child = open( STDOUT, "|-" ); # open pipe: parent|child
if ( $child > 0 ) { # parent: exec tcpdump
open(STDERR, ">&STDOUT"); # STDERR to child - like shell's 2>&1
# CAUTION: tcpdump needs to run as root to read pflog0.
# Choose tcpdump(8) flags carefully please!
# These flags are not ARGV or config file options by design.
#
exec( 'tcpdump', '-lent', '-i', 'pflog0' ) or die "$!\n";
} elsif ( $child == 0 ) { # child: exec logger (unprivileged)
# drop privilege - uid last - no more changes after that
# set then check - perl silently ignores set error.
#
$) = $lgid; if ( $) != $lgid ) { exit($!); } # egid
$( = $lgid; if ( $( != $lgid ) { exit($!); } # gid
$> = $luid; if ( $> != $luid ) { exit($!); } # euid
$< = $luid; if ( $< != $luid ) { exit($!); } # uid
exec( 'logger', '-p', 'user.info', '-t', 'pflogger' ) or exit($!);
} elsif ( $child < 0 ) { die "open pipe failed: $!\n"; }
# /* NOTREACHED */
#-------------------------------------------------------------------------------
# SUBROUTINE DEFINITIONS BELOW
#-------------------------------------------------------------------------------
sub check_pidfile() # check for and kill existing process
{
my $rv = 0; # 0 for error, 1 for success
my $err = ""; # diagnostic messages
LCB: {
if ( open(IN, '<', PIDFILE) == 0 ) { # open
$err = "open PIDFILE";
last LCB;
}
my $opid;
if ( ! ($opid = <IN>) ) { # read
$err = "read PIDFILE";
last LCB;
}
if ( close(IN) == 0 ) { # close
$err = "close PIDFILE";
last LCB;
}
if ( $opid =~ /^(\d+)$/ ) { $opid = $1; } # untaint
else {
$err = "tainted PIDFILE: $opid";
last LCB;
}
my $tuid;
if ( ( $tuid = getpwnam( TCPUSR ) ) < 0 ) {
$err = "getpwnam: $!\n";
last LCB;
}
$> = $tuid; # euid _tcpdump
if ( $> != $tuid ) { # verify
$err = "priv drop:$!\n";
last LCB;
}
if ( kill 0 => $opid ) { # alive?
warn "restart - terminating $opid\n";
if ( ! kill TERM => $opid ) { # terminate
warn "TERM failed $!\n using KILL\n";
if ( ! kill KILL => $opid ) { # forcefully
$err = "restart failed: $!";
}
}
}
$> = $<; # euid restored
if ( $> != $< ) { $err = "priv restore:$!\n" } # verify
}
if ($err) { warn "check_pidfile(): $err\n"; }
else {
unlink( PIDFILE ) or warn "unlink PIDFILE failed\n";
$rv = 1;
}
return($rv);
}
#-------------------------------------------------------------------------------
sub daemonize() # caller becomes a daemon process
{
my $rv = 0; # 0 for failure, 1 for success
my $err; # diagnostic messages
LCB: {
local $SIG{HUP} = 'IGNORE'; # local = only within LCB
my $pid = fork;
if ( $pid > 0 ) { exit(0); }
elsif ( $pid < 0 ) {
$err = "fork1: $!";
last LCB;
}
if ( syscall( SETSID ) == 0 ) {
$err = "syscall SETSID: $!";
last LCB;
}
if ( ($pid = fork) > 0 ) { exit(0); }
elsif ( $pid < 0 ) {
$err = "fork2: $!";
last LCB;
}
if ( chdir( CDDIR ) == 0 ) {
$err = "chdir: $!";
last LCB;
}
umask DMASK;
#
# call pidfile() here before STDERR becomes /dev/null
# want to see diagnostic messages if anything goes wrong.
#
if ( ! &pidfile() ) {
$err = "pidfile failed.";
last LCB;
}
#
# NOTE: re-opening causes implicit close
# should be fd's 0,1, and 2 unless changed before here.
# STDERR last - then diagnostics from warn/die dissappear.
#
if ( open( STDIN, "</dev/null" ) == 0 ) {
$err="STDIN: $!";
last LCB;
}
if ( open( STDOUT, ">/dev/null" ) == 0 ) {
$err="STDOUT: $!";
last LCB;
}
if ( open( STDERR, ">/dev/null" ) == 0 ) {
$err="STDERR: $!";
last LCB;
}
for (MAXFD..3) { close($_); } # ok to ignore errors here
}
if ($err) { warn "daemonize(): $err\n"; }
else { $rv = 1; }
return($rv);
}
#-------------------------------------------------------------------------------
sub pidfile() # create PIDFILE
{
my $rv = 0; # 0 for failure, 1 for success
my $err = ""; # diagnostic messages
my $saved = umask 0133; # want -rw-r--r--
if ( open(OUT, '>', PIDFILE ) == 0 ) { $err = "open: $!"; }
elsif ( (print OUT "$$\n") == 0 ) { $err = "print $$: $!"; }
elsif ( close(OUT) == 0 ) { $err = "close: $!"; }
else { $rv = 1; }
umask $saved; # restore original mask
if ($err) {
warn "pidfile(): $err\n";
unlink( PIDFILE );
}
return($rv);
}
#-------------------------------------------------------------------------------
__END__