#!/usr/bin/perl -w
#
# ifc - Interface configuration script
#
# Author: Jochen Wiedmann
# Am Eisteich 9
# 72555 Metzingen
# Germany
#
# E-Mail: +49 7123 14887
#
my $VERSION = "ifc, Version 20-May-1999, by Jochen Wiedmann";
my $INTERFACE = "eth0";
my $IP = "192.168.1.2";
my $NETMASK = "255.255.255.0";
my $IFCONFIG = "/sbin/ifconfig";
my $ROUTE = "/sbin/route";
my $GATEWAY = "192.168.1.1";
my $NAMESERVER = "192.168.1.1";
use strict;
use Getopt::Long ();
use Data::Dumper ();
use Socket ();
=pod
=head1 NAME
ifc - Interface Configuration Script
=head1 SYNOPSIS
# Configure an interface in an interactive dialog, optionally
# creating a new configuration
ifc
# Configure an interface using the builtin <configuration>
ifc <configuration>
=head1 DESCRIPTION
This script is for you, laptop users, who are frequently attaching your
machine into a different network. No longer entering "ifconfig" and
"route" commands, simply entering IP addresses and related data in
an interacetive dialog. Even better, you may save the current configuration
and make it part of the script.
Enough words, let's look at an example session:
[root@gate joe]# /tmp/ifc
Enter interface configuration data:
Interface to configure: [eth0]
IP address: [192.168.1.2] 149.71.202.201
Netmask: [255.255.255.0]
Gateway ('none' for no gateway): [192.168.1.1] 149.202.71.254
Nameservers (blank separated list): [192.168.1.1] 149.202.71.109
Configuration name (empty if you don't want to save): sni
First of all, this will issue the commands
/sbin/ifconfig eth0 149.71.202.201 netmask 255.255.255.0 \
broadcast 149.71.202.255
/sbin/route add -net 149.71.202.0 netmask 255.255.255.0
/sbin/route add default gw 149.71.202.254
and create a file /etc/resolv.conf. But additionally the file will
modify itself to contain a configuration called I<sni>. If you invoke
the script with
ifc sni
later, then the same configuration will be invoked again.
=head1 CPAN
This script is available as a CPAN script. You can download it from
any CPAN mirror, in particular
ftp://ftp.funet.fi/pub/languages/perl/CPAN/authors/id/JWIED
The following sections are important for CPAN's automatisms only,
you can safely ignore them.
=head2 SCRIPT_CATEGORIES
=head2 PREREQUISITES
This script requires the C<Data::Dumper> module, which is part of
the core Perl installation since version 5.005. You need to install
it manually for previous versions.
Additionally required are the C<Getopt::Long> and C<Socket> modules,
which are availably with any Perl version I know, at least 5.002
and later.
=head2 OSNAMES
This script was developed on a C<linux> machine. I see no real
problems with porting it to other machines, but you need to
modify at least the sections parsing the ifconfig output and
the ifconfig and route commands.
=head1 COPYRIGHT AND AUTHOR
This program is
Copyright (C) 1998 Jochen Wiedmann
Am Eisteich 9
72555 Metzingen
Germany
Email:
[email protected]
All rights reserved.
You may distribute this script under the terms of either the GNU General
Public License or the Artistic License, as specified in the Perl README file.
=head1 SEE ALSO
L<ifconfig(8)>, L<route(8)>
=cut
############################################################################
#
# Global Variables
#
############################################################################
use vars qw($debug $verbose $configurations);
my $OLD_PATH = $ENV{'PATH'};
$ENV{'PATH'} = '/sbin:/usr/sbin:/bin:/usr/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
############################################################################
#
# Name: Usage
#
# Purpose: Print usage message and exit
#
############################################################################
sub Usage () {
print <<"EOF";
Usage: $0 [options] [configuration]
Possible options are:
--debug Turn on debugging mode, implies verbose mode. In debugging
mode, no changes happen, the program simply prints what
would be done.
--help Print this message.
--verbose By default operation is silent, unless you turn on
verbose mode.
--version Print version string and exit.
EOF
exit 0;
}
############################################################################
#
# Name: Ip2Integer
#
# Purpose: Convert an IP address into an integer value
#
# Inputs: $ip - IP address
#
# Returns: Integer value, dies in case of problems
#
############################################################################
sub Ip2Integer ($) {
my $ip = shift;
die "Invalid IP address: $ip"
unless defined($ip) and $ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/;
($1 << 24) | ($2 << 16) | ($3 << 8) | $4;
}
############################################################################
#
# Name: Integer2Ip
#
# Purpose: Convert an integer value into an dotted quad
#
# Inputs: $integer - Integer value
#
# Returns: Dotted quad string, dies in case of problems
#
############################################################################
sub Integer2Ip ($) {
my $integer = shift;
my $four = $integer & 0xff;
$integer >>= 8;
my $three = $integer & 0xff;
$integer >>= 8;
my $two = $integer & 0xff;
$integer >>= 8;
my $one = $integer;
"$one.$two.$three.$four";
}
############################################################################
#
# Name: FindIp
#
# Purpose: Convert a string into an IP address
#
# Input: $reply - String to convert
#
# Returns: IP address as dotted quad or undef for an invalid IP address
#
############################################################################
sub FindIp ($) {
my $reply = shift;
if ($reply =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) {
return undef unless $1 < 256 and $2 < 256 and $3 < 256 and $4 < 256;
return "$1.$2.$3.$4";
}
# Rats! Need a DNS lookup ...
print "Querying IP address of host name $reply ... ";
my $ip = Socket::inet_aton($reply);
if (!defined($ip)) {
print "Cannot resolv.\n";
return undef;
}
$ip = Socket::inet_aton($ip);
# Return an untainted string
$ip = $1 if $ip =~ /(.*)/;
$ip;
}
############################################################################
#
# Name: MakeConfig
#
# Purpose: Enter configuration data and save the configuration in
# in interactive dialog.
#
# Input: $o - Options hash ref
#
# Returns: Configuration; aborts in case of errors.
#
############################################################################
sub MakeConfig ($) {
my $o = shift;
my $config = {};
my $reply;
my $command = "$IFCONFIG -a";
print "Querying interface configuration: $command\n" if $verbose;
my $ifconfig = `$command`;
print "\nEnter interface configuration data:\n\n";
undef $reply;
while (!$config->{'interface'}) {
if (defined($o->{'interface'})) {
$reply = delete $o->{'interface'};
} else {
$reply = $INTERFACE unless defined($reply);
print "Interface to configure: [$reply] ";
my $r = <STDIN>;
chomp $r;
$reply = $r if length($r);
}
$reply =~ s/^\s+//; $reply =~ s/\s+$//;
if ($ifconfig =~ /^($reply)\s+Link encap:/) {
$config->{'interface'} = $1;
} else {
print "An interface $reply doesn't exist.\n";
}
}
undef $reply;
while (!$config->{'ip'}) {
if (defined($o->{'ip'})) {
$reply = delete $o->{'ip'};
} else {
$reply = $IP unless defined($reply);
print "IP address: [$reply] ";
my $r = <STDIN>;
chomp $r;
$reply = $r if length($r);
}
$config->{'ip'} = FindIp($reply)
or print "Invalid IP address: $reply\n";
}
undef $reply;
while (!$config->{'netmask'}) {
if (defined($o->{'netmask'})) {
$reply = delete $o->{'netmask'};
} else {
$reply = $NETMASK unless defined($reply);
print "Netmask: [$reply] ";
my $r = <STDIN>;
chomp $r;
$reply = $r if length($r);
}
if ($reply =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) {
if ($1 > 255 || $2 > 255 || $3 > 255 || $4 > 255) {
print "Invalid Netmask: $reply\n";
} else {
my $num = ($1 >> 24) + ($2 >> 16) + ($3 >> 8) + $4;
my $netmask = "$1.$2.$3.$4";
my $one = 0;
while ($num) {
my $bit = $num & 1;
$num >>= 1;
if ($bit) {
$one = 1;
} elsif ($one) {
print "Invalid Netmask: $reply\n";
undef $netmask;
last;
}
}
$config->{'netmask'} = $netmask if defined($netmask);
}
} elsif ($reply =~ /(\d+)/) {
my $bits = $1;
if ($bits > 32) {
print "Invalid Netmask: $reply\n";
} else {
my $num = 0;
for (my $i = 0; $i < 32; $i++) {
$num = ($num << 1) | ($bits ? 1 : 0);
--$bits if $bits;
}
$config->{'netmask'} = Integer2Ip($num);
}
}
}
undef $reply;
while (!$config->{'gateway'}) {
if (defined($o->{'gateway'})) {
$reply = delete $o->{'gateway'};
} else {
$reply = $GATEWAY unless defined($reply);
print "Gateway ('none' for no gateway): [$reply] ";
my $r = <STDIN>;
chomp $r;
$reply = $r if length($r);
}
if ($reply eq 'none') {
$config->{'gateway'} = $reply;
} else {
my $gw = $config->{'gateway'} = FindIp($reply)
or print "Invalid IP address: $reply\n";
if (defined($gw)) {
my $ip_val = Ip2Integer($config->{'ip'});
my $gateway_val = Ip2Integer($gw);
my $netmask_val = Ip2Integer($config->{'netmask'});
if (($ip_val & $netmask_val) !=
($gateway_val & $netmask_val)) {
print "Gateway $gw doesn't match network.\n";
undef $config->{'gateway'};
}
}
}
}
undef $reply;
while (!defined($config->{'nameserver'})) {
if (defined($o->{'nameserver'})) {
$reply = delete $o->{'nameserver'};
} else {
$reply = $NAMESERVER unless defined($reply);
print "Nameservers (blank separated list): [$reply] ";
my $r = <STDIN>;
chomp $r;
$reply = $r if length($r);
}
my $invalid;
my @nameservers =
map {
my $ip = FindIp($_);
if (!defined($ip)) {
$invalid = 1;
print "Invalid IP address: $_\n";
}
$ip;
} split(/ /, $reply);
$config->{'nameserver'} = join(" ", @nameservers) unless $invalid;
}
undef $reply;
while (!defined($config->{'name'})) {
if (defined($o->{'name'})) {
$reply = delete $o->{'name'};
} else {
print "Configuration name (empty if you don't want to save): ";
$reply = <STDIN>;
chomp $reply;
}
$reply =~ s/^\s+//; $reply =~ s/\s+$//;
$config->{'name'} = $reply;
if (length($reply) and exists($configurations->{$reply})) {
print "A configuration $reply already exists.\n";
undef $config->{'name'};
} elsif (length($reply)) {
$configurations->{$reply} = $config;
# Save this configuration
my $dump = Data::Dumper->new([$configurations],
['configurations']);
$dump->Indent(1);
my $cstr = $dump->Dump($dump);
my $file;
print "Saving data in file $0\n" if $verbose;
if ($0 =~ /\//) {
# Absolute path name
$file = $0 if $0;
} else {
foreach my $dir (split(/:/, $OLD_PATH)) {
if (-f "$dir/$0") {
$file = "$dir/$0";
}
}
}
if (defined($file)) {
open(FILE, $debug ? "<$file" : "+<$file")
or die "Failed to open $file: $!";
local $/ = undef;
my $contents = <FILE>;
die "Failed to read $file: $!" unless defined($contents);
$contents =~ s/(\n__END__\s*\n)(.*)/$1$cstr/s
or die "Cannot parse $file";
# Untaint the contents
$contents = $1 if $contents =~ /(.*)/s;
if ($debug) {
print "Writing $file:\n$contents\n";
} else {
seek(FILE, 0, 0) or die "Failed to seek in $file: $!";
(print FILE $contents) or die "Failed to write $file: $!";
truncate(FILE, length($contents))
or die "Failed to truncate $file: $!";
}
close(FILE) or die "Failed to close $file: $!";
} else {
print "Cannot save data: No such file: $0\n";
}
}
}
$config;
}
############################################################################
#
# Name: UseConfig
#
# Purpose: Read an existing configuration
#
# Input: $o - Options hash ref
# $name - Configuration name
#
# Returns: Configuratio hash ref; aborts in case of problems
#
############################################################################
sub UseConfig {
my $o = shift; my $name = shift;
unless (exists($configurations->{$name})) {
print "No such configuration: $name\n\n";
print "Available configurations are:\n";
foreach my $c (keys %$configurations) {
print " $c\n";
}
exit 1;
}
$configurations->{$name};
}
############################################################################
#
# Name: DoConfig
#
# Purpose: Perform the real configuration
#
# Inputs: $o - Options hash ref
# $config - Configuration hash ref
#
# Returns: Nothing, aborts in case of trouble
#
############################################################################
sub DoConfig {
my($o, $config) = @_;
my $interface = $config->{'interface'};
my $ip = $config->{'ip'};
my $netmask = $config->{'netmask'};
my $ip_val = Ip2Integer($ip);
my $netmask_val = Ip2Integer($netmask);
my $bcast = Integer2Ip($ip_val | ~$netmask_val);
my $command = "$IFCONFIG $interface $ip netmask $netmask broadcast $bcast";
print "Configuring interface: $command\n" if $verbose;
system $command unless $debug;
my $network = Integer2Ip($ip_val & $netmask_val);
$command = "$ROUTE add -net $network netmask $netmask $interface";
print "Setting interface route: $command\n" if $verbose;
system $command unless $debug;
my $gateway = $config->{'gateway'};
if ($gateway ne 'none') {
$command = "$ROUTE add default gw $gateway";
print "Setting default route: $command\n" if $verbose;
system $command unless $debug;
}
my $nameserver = $config->{'nameserver'};
if ($nameserver) {
my $r = "/etc/resolv.conf";
open(FILE, $debug ? "<$r" : "+<$r") or die "Failed to open $r: $!";
local $/ = undef;
my $contents = <FILE>;
die "Failed to read $r: $!" unless defined($contents);
$contents =~ s/^\s*nameserver\s+.*?\n//gm;
$contents .= join("",
map {"nameserver $_\n"} split(/ /, $nameserver));
# Untaint the contents
$contents = $1 if $contents =~ /(.*)/s;
print "Writing $r:\n$contents\n" if $verbose;
unless ($debug) {
seek(FILE, 0, 0) or die "Failed to seek $r: $!";
(print FILE $contents) or die "Failed to write $r: $!";
truncate(FILE, length($contents))
or die "Failed to truncate $r: $!";
}
close(FILE) or die "Failed to close $r: $!";
}
}
############################################################################
#
# This is main()
#
############################################################################
{
if ($>) {
print STDERR "Warning: The ifc script is not running as root.\n";
print STDERR "Interface configuration or saving may fail!\n\n";
}
# Read the list of configurations
{
local $/ = undef;
my $data = <DATA>;
$configurations = eval $data;
die $@ if $@;
}
my %o = ( 'debug' => \$debug, 'verbose' => \$verbose );
Getopt::Long::GetOptions(\%o, 'debug', 'verbose', 'version', 'help');
if ($o{'version'}) {
print STDERR "$VERSION\n";
exit 1;
}
$verbose = 1 if $debug and !$verbose;
my $cfname = shift @ARGV;
Usage() if @ARGV || $o{'help'};
my $config = defined($cfname) ? UseConfig(\%o, $cfname) : MakeConfig(\%o);
DoConfig(\%o, $config);
}
__END__