# $Id: Jail.pm 138 2004-11-19 21:04:24Z kirk $
# This software was written by Kirk Strauser <kirk@strauser.com>, and may be
# freely distributed under the terms of the BSD License.
#
# Please submit any changes to this program back to the author so that they
# can be easily distributed to other others who might be interested.
############################################################
#### Package definition ####
############################################################
package Jail;
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw();
@EXPORT_OK = qw();
# Use Subversion tags to dynamically create the module version
$VERSION = '$Rev: 138 $';
$VERSION =~ s/\$Rev:\s*(.*)\s*\$/$1/;
require 'dumpvar.pl';
use strict;
use Config;
use Getopt::Long;
############################################################
#### Configuration section ####
############################################################
my $conffile = '/usr/local/etc/jailadmin.conf';
Getopt::Long::Configure('pass_through');
Getopt::Long::GetOptions('conffile=s', \$conffile);
my %conf = (
'debug' => 0,
'default_fstab' => '/etc/fstab',
'default_shutdown' => 'naive',
'default_startcommand' => '/bin/sh /etc/rc',
'default_usejtools' => 0,
'maxparallel' => 1
);
my %jaildata;
open INFILE, $conffile or die "Unable to read the configuration file: $!";
my $server;
while (defined (my $inline = <INFILE>))
{
chomp $inline;
$inline =~ s/\#.*$//; # Remove comments
next unless $inline =~ /\S/; # Skip blank lines
my $sub = $inline =~ /^\s+/ || 0; # Is this line a subordinate setting?
$inline =~ s/^\s*(.*)\s*$/$1/;
# Set global config options
if ($inline =~ /=/)
{
my ($key, $value) = (split /\s*=\s*/, $inline)[0, 1];
# Handle group definitions
if ($key =~ /^group_/)
{
$conf{$key} = [canonicalizeList(split /\s*,\s*/, $value)];
}
else
{
$conf{$key} = $value;
}
next;
}
# Unset global config options
if ($inline =~ /^!\s*(.*)$/ and not $sub)
{
delete $conf{$1};
next;
}
# Start a new server
unless ($sub)
{
$server = $inline;
foreach my $key (keys %conf)
{
if ($key =~ /^default_(.*)$/)
{
$jaildata{$server}{$1} = $conf{$key};
}
}
$jaildata{$server}{'dir'} = "$conf{'jaildir'}/$server";
next;
}
# Unset server-specific options
if ($inline =~ /^\!\s*(.*)$/)
{
delete $jaildata{$server}{$1};
next;
}
# Set server-specific options
my ($key, $value) = (split /\s*:\s*/, $inline)[0, 1];
# Pre-process select key types
if ($key eq 'dir')
{
$value = "$conf{'jaildir'}/$jaildata{$server}{'dir'}" unless $value =~ /^\//;
}
# Make the assignment
$jaildata{$server}{$key} = $value;
}
# Defined the "all" group if it is not already set.
if (not defined $conf{'group_all'})
{
$conf{'group_all'} = [ canonicalizeList('ALL') ];
}
# Finally, sanity-check the configuration.
my $procismounted;
foreach my $server (keys %jaildata)
{
if ($jaildata{$server}{'shutdown'} eq 'emulated' and not
$jaildata{$server}{'usejtools'})
{
die "In server $server, you must set usejtools in order to use the emulated shutdown method, stopped";
}
if (not $jaildata{$server}{'usejtools'})
{
unless (defined $procismounted)
{
open INPIPE, "mount -t procfs |";
$procismounted = grep / \/proc/, <INPIPE>;
}
unless ($procismounted)
{
die "In server $server, you must set usejtools unless /proc is mounted, stopped";
}
}
}
# Display the configuration hashes
if ($conf{'debug'} > 2)
{
print "System options:\n";
main::dumpValue(\%conf);
print "\nServer options:\n";
main::dumpValue(\%jaildata);
print "\n\n";
}
# From the 'perlipc' Perldoc page:
my %signo;
my $i = 0;
defined $Config{sig_name} || die "No sigs?";
foreach my $name (split(' ', $Config{sig_name})) {
$signo{$name} = $i;
$i++;
}
my %processList;
my %uidcache;
1;
############################################################
#### Subroutines ####
############################################################
# Apply a set of rules to a jail's devfs
sub applyDevfsRuleset
{
my $server = shift;
my $ruleset = shift;
my $devdir = "$jaildata{$server}{'dir'}/dev";
my $shellscript = <<__EOSS__;
#!/bin/sh
. /etc/defaults/rc.conf
. /etc/rc.subr
devfs_init_rulesets
devfs_set_ruleset $ruleset $devdir
devfs -m $devdir rule applyset
__EOSS__
open OUTPIPE, "| /bin/sh";
print OUTPIPE $shellscript;
close OUTPIPE;
}
sub canonicalizeList
{
my $count = 0;
my %servers;
while (defined ($server = shift @_))
{
# A list of all servers
if ($server eq 'ALL')
{
foreach my $key (keys %jaildata)
{
$servers{$key} = $count++;
}
}
# Remove a specific server
elsif ($server =~ /^!\s*(.*)$/)
{
delete $servers{$1};
}
# Add a single server
elsif (defined ($jaildata{$server}))
{
$servers{$server} = $count++;
}
# Add a group of servers
elsif (defined ($conf{"group_$server"}))
{
foreach my $key (@{$conf{"group_$server"}})
{
$servers{$key} = $count++;
}
}
else
{
debug('No server or group named $server exists.');
}
}
return (sort { $servers{$a} <=> $servers{$b} } keys %servers);
}
sub debug
{
my $message = shift;
my $level = shift || 1;
print STDERR ">>> $message\n" if $conf{'debug'} >= $level;
}
# Get the names of all defined jails
sub getJailList
{
return sort (keys %jaildata);
}
sub getJailInfo
{
my $server = shift;
return \%{$jaildata{$server}};
}
sub getJid
{
my $server = shift;
die "You must set 'usejtools' to use getJid(), stopped"
if not $jaildata{$server}{'usejtools'};
open INPIPE, "jls |" or die "Unable to open a pipe from 'jls': $!";
# Toss the first line
$_ = <INPIPE>;
my $jid;
while (defined ($_ = <INPIPE>))
{
chomp;
s/^\s*(.*?)\s*$/$1/;
my @fields = split /\s+/, $_;
if ($fields[1] eq $jaildata{$server}{'ip'})
{
$jid = $fields[0];
last;
}
}
close INPIPE;
return $jid || 0;
}
sub getProcInfo
{
my $server = shift;
my %proclist = getProcList($server);
if ($jaildata{$server}{'usejtools'})
{
# Note that getProcList() already does all of the work if
# usejtools is set, since it's extremely cheap to parse the
# additional fields of the "ps" output when building the
# process list.
return %proclist;
}
my @psdata;
foreach my $pid (keys %proclist)
{
open INPIPE, "ps wup $pid |" or die "Couldn't open the ps pipe: $!";
$_ = <INPIPE>;
push @psdata, <INPIPE>;
close INPIPE;
}
return psToHash(@psdata);
}
# Get a list of all PIDs currently running in a particular jail
sub getProcList
{
my $server = shift;
my $hostname = $jaildata{$server}{'hostname'};
return () unless isRunning($server);
if ($jaildata{$server}{'usejtools'})
{
my $pscmd = 'ps wxua';
my @psout = jexecute($server, $pscmd);
shift @psout;
my %procs = psToHash(@psout);
# Delete the entry for the highest-numbered command that looks
# exactly like our ps command from above. If we don't do
# this, then ps itself appears in the list, which probably
# isn't what the use wants. Note that we can't be sure that
# the found ps is really *our* ps, but it's pretty likely, and
# probably not worth being pedantic.
my $removepid;
foreach my $pid (sort { $a <=> $b } keys %procs)
{
$removepid = $pid if $procs{$pid}{'user'} eq 'root' and $procs{$pid}{'command'} eq $pscmd;
}
delete $procs{$removepid} if defined $removepid;
return %procs;
}
else
{
updateProcessList();
return %{$processList{$hostname}};
}
}
# Caching version of getpwuid. The syscall may be expensive, and it may
# otherwise potentially be called thousands of times with the same arguments.
sub getpwuidcache
{
my $uid = shift;
unless (defined ($uidcache{$uid}))
{
$uidcache{$uid} = getpwuid $uid;
}
return $uidcache{$uid};
}
sub isRunning
{
my $server = shift;
my $hostname = $jaildata{$server}{'hostname'};
if ($jaildata{$server}{'usejtools'})
{
return getJid($server) ? 1 : 0;
}
else
{
updateProcessList();
return keys %{$processList{$hostname}} ? 1 : 0;
}
}
sub jexecute
{
my $server = shift;
my $command = shift;
die "You must set 'usejtools' to use jexecute(), stopped"
if not $jaildata{$server}{'usejtools'};
my $jid = getJid($server);
return if not $jid;
debug("Executing \"$command\"");
open INPIPE, "jexec $jid $command |" or die "Unable to open a pipe from 'jexec': $!";
my @retlist;
while (defined ($_ = <INPIPE>))
{
chomp;
push @retlist, $_;
}
close INPIPE;
return @retlist;
}
sub maxparallel
{
return $conf{'maxparallel'};
}
sub mount
{
my $server = shift;
my $mountlist = $jaildata{$server}{'mount'} or return;
my @entry = split ',', $mountlist;
foreach my $fs (@entry)
{
my $sleep = 0;
if ($fs =~ /^(.*)\((\d+)\)$/)
{
$fs = $1;
$sleep = $2;
}
system "mount -F $jaildata{$server}{'fstab'} $jaildata{$server}{'dir'}/$fs";
sleep $sleep if $sleep;
}
if (defined $jaildata{$server}{'devfsruleset'})
{
applyDevfsRuleset($server, $jaildata{$server}{'devfsruleset'});
}
}
sub signalProcs
{
my $server = shift;
my $signal = shift;
my %temp = getProcList($server);
my @pids = keys %temp;
kill $signo{$signal}, @pids;
}
sub launch
{
my $server = shift;
system "jail $jaildata{$server}{'dir'} $jaildata{$server}{'hostname'} $jaildata{$server}{'ip'} $jaildata{$server}{'startcommand'}";
}
sub psToHash
{
my %rethash;
foreach my $psline (@_)
{
chomp $psline;
my @fields = split /\s+/, $psline, 11;
my $pid = $fields[1];
my %procinfo =
(
'user' => $fields[0],
'pid' => $fields[1],
'cpu' => $fields[2],
'mem' => $fields[3],
'vsz' => $fields[4],
'rss' => $fields[5],
'tt' => $fields[6],
'stat' => $fields[7],
'started' => $fields[8],
'time' => $fields[9],
'command' => $fields[10]
);
$rethash{$pid} = \%procinfo;
}
return %rethash;
}
sub removeDupes
{
my @retlist;
foreach my $entry (@_)
{
push @retlist, $entry if not grep /^$entry$/, @retlist;
}
return @retlist;
}
sub stop
{
my $server = shift;
my $state = shift;
my $sleep = 0;
my $nextstate;
debug("--- Server: $server state $state");
# Old-style shutdown ('naive'): All processes get the TERM
# signal, then after a delay, all remaining processes get KILLed.
# This is similar to the final stages of FreeBSD's own shutdown
# process. While it isn't the cleanest shutdown possible, it has
# the advantage of working on every FreeBSD system, and there have
# not been any reported problems with it.
# New-style shutdown ('emulated'): This executes the jail's own
# rc.shutdown script to stop processes by their preferred method.
# Then, the process detailed above shuts down the remaining
# processes. It mimics the full FreeBSD shutdown very closely,
# but depends on certain system executables that may not be
# present on older systems.
$state == 1 and do {
if ($jaildata{$server}{'shutdown'} eq 'emulated')
{
debug('Executing /etc/rc.shutdown.');
jexecute($server, 'sh /etc/rc.shutdown');
debug('Done. Sleeping at least 5 seconds.');
$sleep = 5;
$nextstate = 2;
}
else
{
$state = 2;
}
};
# Send term signal
$state == 2 and do {
# TERM all of the processes
debug('Sending signal TERM to all processes. Sleeping at least 5 seconds.');
Jail::signalProcs($server, 'TERM');
$sleep = 5;
$nextstate = $state + 1;
};
# Sleep 10 seconds for slow processes
$state == 3 and do {
if (Jail::isRunning($server))
{
debug('Some processes are still running. Sleeping at least 10 seconds.');
$sleep = 10;
$nextstate = $state + 1;
}
else
{
debug('No processess left.');
# Skip the next process check - we've already passed.
$nextstate = $state + 2;
}
};
# Send kill signal
$state == 4 and do {
if (Jail::isRunning($server))
{
debug('Sending remaining processes the KILL signal. Sleeping at least 2 seconds.');
Jail::signalProcs($server, 'KILL');
}
else
{
debug('No processess left.');
}
$sleep = 2;
$nextstate = $state + 1;
};
# Unmount specified filesystems
$state == 5 and do {
debug('Unmounting filesystems.');
Jail::umount($server);
$nextstate = 0;
};
return ($nextstate, $sleep);
}
sub umount
{
my $server = shift;
my $mountlist = $jaildata{$server}{'mount'} or return;
my @entry = split ',', $mountlist;
foreach my $fs (reverse @entry)
{
$fs = $1 if $fs =~ /^(.*)\((\d+)\)$/;
system "umount $jaildata{$server}{'dir'}/$fs";
}
}
sub updateProcessList
{
%processList = ();
opendir INDIR, '/proc' or die "Unable to read the /proc filesystem: $!";
foreach my $pid (readdir INDIR)
{
next if $pid =~ /^\.\.?$/ or $pid eq 'CURPROC';
unless (open INPROC, "/proc/$pid/status")
{
print STDERR "Unable to read /proc/$pid: $!";
next;
}
my $status = <INPROC>;
close INPROC;
my ($procname,
$uid,
$gids,
$hostname) = (split /\s+/, $status)[0, 11, 13, 14];
# Get the process' command line
unless (open INPROC, "/proc/$pid/cmdline")
{
print STDERR "Unable to read /proc/$pid/cmdline: $!";
next;
}
my $cmdline = <INPROC> || '';
close INPROC;
# Clean up the results a little
# print "PID: $pid\n" unless defined $cmdline;
$cmdline =~ s/\000/ /g;
$cmdline =~ s/\s*$//;
my @uidinfo = getpwuidcache($uid);
my %procinfo =
(
'hostname' => $hostname,
'uid' => $uid,
'user' => $uidinfo[0],
'procname' => $procname,
'cmdline' => $cmdline
);
$processList{$hostname}{$pid} = \%procinfo;
}
closedir INDIR;
}
syntax highlighted by Code2HTML, v. 0.9.1