#!/usr/bin/perl -w $rcsid = q$Id: ppp-connect 1.1 Sat, 19 Jun 1999 19:19:22 +0100 paul $; use strict; #no strict 'vars'; use vars qw($rcsid $opt_help $opt_version $opt_verbose), qw($opt_max_idle_time $opt_max_time $opt_min_time), qw($opt_chatdir $opt_chatfiles $opt_chat_timeout $opt_serial_rate), qw($opt_sleep_time $opt_max_dials $opt_max_time_per_attempt), qw($opt_sampling_interval $opt_logfile $opt_config_file), qw($opt_chat_verbose $opt_monitor_idle $opt_pjob_interval), qw(@user_options @root_options), qw($progname $is_setuid), qw($prev_event $prev_time), qw(@pids_launched $pppd_pid $pppd_start_time $ppp_device), qw(%pid_desc); use FileHandle; use Getopt::Long; use POSIX "sys_wait_h"; # -------------------------------------------------------------------- sub logmsg { # system(qw(/usr/bin/logger -s -t), $basename, @_); print @_, "\n"; print DIALOUT_LOG @_, "\n"; } sub verbose { if ($opt_verbose) {logmsg @_}; } sub timing_announce { my $new_event = shift; my ($time_elapsed, $mins, $secs, $new_time); $new_time = time; if (defined $prev_time) { $time_elapsed = $new_time - $prev_time; $mins = int($time_elapsed / 60); $secs = $time_elapsed % 60; $secs = sprintf("%02d", $secs); logmsg "$new_event ($mins:$secs after $prev_event)"; } else { logmsg $new_event; } $prev_time = $new_time; $prev_event = $new_event; } sub kill_pppd { if (defined $pppd_pid) { # Kill it! if (kill('TERM', $pppd_pid)) { timing_announce "PPPD killed with signal"; } else { timing_announce "PPPD kill failed with error $!"; } undef $pppd_pid; } } sub snuff_it { my $exitcode = shift; my $exit_message = shift; if (defined $pppd_pid) { kill_pppd(); logmsg $exit_message; } else { timing_announce $exit_message; } if(defined &main::before_exit) { eval '&before_exit' or die "before_exit command failed: $@, stopped"; } exit $exitcode; } sub signal_handler { my $signal = shift; snuff_it(-1, "Shutting down after signal $signal"); } sub com_launch { my $wait_time = shift; my $uid = shift; my $gid = shift; my $pid = fork; unless (defined $pid) { snuff_it(-1, "Failed to fork: $!, stopped"); } if ($pid == 0) { # We're the child $) = $( = $gid; $> = $< = $uid; sleep $wait_time; exec @_; # Exec must have failed die "Exec of " . join(' ', @_) . " failed: $!, stopped"; } # We're the parent logmsg "PID $pid: ", join(' ', @_); push @pids_launched, $pid; $pid_desc{$pid} = join(" ", @_); return $pid; } sub launch { my $wait_time = shift; return com_launch($wait_time, $>, $), @_); } sub su_launch { my $wait_time = shift; my $user = shift; # unless (($user, $group) = ($usergroup =~ /^(.+),(.+)$/)) { # $user = $group = $usergroup; # } my ($login, $pass, $uid, $gid); unless (($login, $pass, $uid, $gid) = getpwnam($user)) { snuff_it(-1, "Failed to get user information for $user: $!, stopped"); } return com_launch($wait_time, $uid, $gid, @_); } sub is_pppd_dead { unless (defined $pppd_pid) { return 1; } my $pid; while (($pid = waitpid(-1, WNOHANG)) != 0) { my $error_code = $? >> 8; my $error_desc; if ($error_code) { $error_desc = "died with error number $error_code"; } else { $error_desc = "completed successfully" } if ($pid == $pppd_pid) { undef $pppd_pid; if ($error_code) { timing_announce "PPPD $error_desc"; } return 1; } else { timing_announce "PID $pid (" . $pid_desc{$pid} . ") $error_desc"; } } return 0; } sub get_usage { my $ppp_device = shift; open DEV, "/proc/net/dev" or snuff_it(-1, "Couldn't open dev file: $!"); my $usage = 0; LINE: while () { chomp; my @dev = split(/\s+/); unless ($dev[0]) { shift @dev; } if ($dev[0] eq "$ppp_device\:") { $usage += $dev[1]; # Receive $usage += $dev[6]; # Transmit } } close DEV or snuff_it(-1, "Couldn't close dev file: $!"); return $usage; } # -------------------------------------------------------------------- # -------------------------------------------------------------------- # -------------------------------------------------------------------- # Switch off buffering. STDOUT->autoflush(1); # CD to somewhere everyone can reach chdir("/tmp") or die "Couldn't even cd to /tmp: $!, stopped"; # -------------------------------------------------------------------- $opt_help = 0; $opt_version = 0; $Getopt::Long::passthrough = 1; GetOptions(qw(help version)) or die "Failed to parse command line options, stopped"; if ($opt_help) { print "Sorry, I haven't written help yet.\n"; print "You don't really need any options, but if you do, use the source\n"; print "cheers, paul\@hedonism.demon.co.uk\n"; exit; } if ($opt_version) { print "RCS version: $rcsid\n"; exit; } # -------------------------------------------------------------------- $is_setuid = scalar( $< != $> or $( != $) ); if ($is_setuid) { $ENV{PATH} = q(/usr/bin:/bin); } $opt_max_idle_time = 180; $opt_logfile = '/var/local/log/dialout'; $opt_chatdir = '/etc/local/dialout/chatfiles'; $opt_sleep_time = 30; $opt_max_dials = 12; $opt_max_time_per_attempt = 180; # usually not needed, chat dies first. $opt_sampling_interval = 10; $opt_pjob_interval = 30; $opt_chat_timeout = 60; $opt_max_time = 3600; # An hour $opt_min_time = 300; # Five minutes $opt_serial_rate = 115200; $opt_monitor_idle = 0; $opt_verbose = 1; $opt_chat_verbose = 0; @user_options = (qw(verbose! monitor_idle!), qw(max-idle-time=i sleep-time=i max-time=i min-time=i), qw(max-dials=i max-time-per-attempt=i), qw(chatfiles=s chat-timeout=i sampling-interval=i), qw(pjob-interval=i chat-verbose! serial-rate=i)); @root_options = (qw(chatdir=s logfile=s)); $opt_config_file = "default"; if (!$is_setuid) { $Getopt::Long::passthrough = 1; GetOptions(qw(config-file=s)) or die "Failed to parse command line options, stopped"; } { my $config_file; if ($opt_config_file eq "default") { $config_file = '/etc/local/dialout/ppp-config'; } else { $config_file = $opt_config_file; } if (-e $config_file) { unless (do $config_file) { print "$@\n"; die "Failed reading config file $config_file, stopped"; } } elsif ($opt_config_file ne "default") { die "Config file $config_file doesn't exist, stopped"; } } $Getopt::Long::passthrough = 0; GetOptions(@user_options, $is_setuid ? () : @root_options) or die "Failed to parse command line options, stopped"; if (scalar(@ARGV) > 0) { die "Unparsed arguments on command line: \"" . join(' ', @ARGV) . "\", stopped"; } if ($opt_max_time < $opt_min_time) { die "Doesn't make sense to have max-time < min-time, stopped"; } if ($opt_chatfiles eq "") { die "Chatfile list empty, stopped"; } my @chatfiles = split(/,/, $opt_chatfiles); my $file; foreach $file (@chatfiles) { unless ($file =~ /^[-a-z]+$/) { die "Invalid chatfile name $file, stopped"; } unless (-f "$opt_chatdir/$file") { die "Chatfile $opt_chatdir/$file doesn't exist, stopped"; } } # -------------------------------------------------------------------- open DIALOUT_LOG, ">>$opt_logfile" or die "Couldn't open log file for appending: $!, stopped"; flock DIALOUT_LOG, (2 | 4) # Exclusive, nonblocking or die "Couldn't get lock on log file: $!, stopped"; seek DIALOUT_LOG, 0, 2 # Seek to the end now we've got the lock. or die "Couldn't seek to end of log file: $!, stopped"; DIALOUT_LOG->autoflush(1); $is_setuid = scalar( $< != $> or $( != $) ); if ($is_setuid) { logmsg "Started dialout for user: ", scalar(getpwuid($<)), ": ", scalar(gmtime); } else { logmsg "Started non-setuid dialout: ", scalar(gmtime); } # -------------------------------------------------------------------- if(defined &main::before_connect) { eval '&before_connect' or die "before_connect command failed: $@, stopped"; } foreach (qw(HUP INT QUIT TRAP ABRT IOT BUS FPE KILL USR1 USR2 PIPE ALRM), qw(TERM STKFLT STOP URG XCPU XFSZ IO)) { $SIG{$_} = \&signal_handler; } my $dial_attempts = 0; DIAL_ATTEMPT: while (1) { if ($dial_attempts >= $opt_max_dials) { snuff_it(-1, "Maximum dial attempts reached"); } if ($dial_attempts > 0) { sleep $opt_sleep_time; } # Pull the chatfile from one end my $chatfile = shift @chatfiles; # Push it on the other. push @chatfiles, $chatfile; my $connect_script = join(" ", "/usr/sbin/chat", "-t", $opt_chat_timeout, $opt_chat_verbose ? ('-v') : (), '-f', ($opt_chatdir . "/" . $chatfile)); $dial_attempts++; timing_announce "starting PPPD"; $pppd_pid = launch(0, qw(/usr/sbin/pppd /dev/modem), $opt_serial_rate, qw(defaultroute -detach), 'connect', $connect_script); $pppd_start_time = time; logmsg "PPP pid $pppd_pid (attempt $dial_attempts, \"$chatfile\")"; # Now wait either for PPPD to die or a route to be found while (1) { if (is_pppd_dead()) { # It's already been announced. next DIAL_ATTEMPT; } # verbose "Opening /proc/net/route"; # Really too verbose. open ROUTE, "/proc/net/route" or snuff_it(-1, "Couldn't open route file: $!, stopped"); my @route_names = split(/\t/, ); while () { chomp; my $i = 0; my %route; my $value; foreach $value (split(/\t/)) { $route{$route_names[$i]} = $value; $i++; } if ($route{Mask} eq '00000000') { timing_announce "default route found"; $ppp_device = $route{Iface}; last DIAL_ATTEMPT; } } close ROUTE or snuff_it(-1, "Couldn't close route file, error $!"); if (time - $pppd_start_time > $opt_max_time_per_attempt) { logmsg "PPPD seems to be wedged, shooting..."; kill_pppd(); next DIAL_ATTEMPT; } # verbose "Failed to find route, sleeping..."; sleep 2; } } # We're now connected. logmsg "Dialout started: ", scalar(gmtime($pppd_start_time)); if(defined &main::once_connected) { eval '&once_connected' or die "once_connected command failed: $@, stopped"; } # Now wait for some idle time. my $prev_usage = get_usage($ppp_device); my $initial_deadline = $pppd_start_time + $opt_min_time; my $time_now = time; my $use_deadline = $time_now + $opt_max_idle_time; my $pjob_deadline = $time_now + $opt_pjob_interval; my $final_deadline = $pppd_start_time + $opt_max_time; SAMPLE: while (1) { sleep($opt_sampling_interval); if (is_pppd_dead()) { snuff_it(-1, "PPPD is dead"); } $time_now = time; if ($time_now >= $final_deadline) { snuff_it(0, "PPPD maximum dialout time reached"); } my $usage = get_usage($ppp_device); if ($usage > $prev_usage) { if ($opt_monitor_idle) { logmsg "Still working with ", $use_deadline - $time_now, " seconds to go"; } $prev_usage = $usage; $use_deadline = $time_now + $opt_max_idle_time; } else { if ($opt_monitor_idle) { logmsg "all quiet with ", $use_deadline - $time_now, " seconds to go"; } if ($time_now >= $initial_deadline && $time_now >= $use_deadline) { snuff_it(0, "PPPD long idle time detected."); } } if ($time_now >= $pjob_deadline) { if (defined &main::do_pjob) { eval '&do_pjob' or snuff_it(0, "pjob command failed: $@."); } $pjob_deadline = $time_now + $opt_pjob_interval; } }