#!/usr/bin/perl use strict; use warnings; use IPC::Open3; use IO::Select; use Fcntl qw( LOCK_EX LOCK_NB ); use Data::Dumper; open( SELF, "<$0" ) || error( int( $! ), "open(): $0: $!" ); flock( SELF, LOCK_EX|LOCK_NB ) || error( -1, "Another instance is running" ); use constant CONFDIR => '/etc'; use constant CONFIGFILE => CONFDIR . '/network.conf'; use constant NOACT => eval { for ( my $i = 0; $i <= $#ARGV; $i++ ) { if ( $ARGV[$i] eq '-n' || $ARGV[$i] eq '--no-act' ) { splice( @ARGV, $i, 1 ); return( 1 ); } } return( 0 ); }; use constant VERBOSE => eval { for ( my $i = 0; $i <= $#ARGV; $i++ ) { if ( $ARGV[$i] eq '-v' || $ARGV[$i] eq '--verbose' ) { splice( @ARGV, $i, 1 ); return( 1 ); } } return( 0 ); }; use constant VERSION => '0.1'; sub aliasdown { my $alias = shift; my $runningconf = shift; my $aliases = $runningconf->{$alias->{'dev'}}{'aliases'}; my $result; if ( my $running = $aliases->{$alias->{'name'}} ) { if ( VERBOSE ) { print "Deleting $running->{'address'}" . " from $alias->{'dev'}" . " (label $alias->{'dev'}:" . "$alias->{'name'})\n"; } $result = runcmd( "ip address delete" . " $running->{'address'}" . " dev $running->{'dev'} label" . " $running->{'dev'}:$alias->{'name'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } return( $result->{'stat'} ); } } sub aliasup { my $alias = shift; my $runningconf = shift; my $dev = $alias->{'dev'}; my $name = $alias->{'name'}; my $result; if ( my $old = $runningconf->{$dev}{'aliases'}{$name} ) { if ( $old->{'address'} eq $alias->{'address'} && $old->{'dev'} eq $alias->{'dev'} && $old->{'name'} eq $alias->{'name'} ) { if ( VERBOSE ) { print STDERR "Interface $dev:$name" . " is already up\n"; } return( 1 ); } if ( my $retval = aliasdown( $old, $runningconf ) ) { return( $retval ); } } if ( VERBOSE ) { print "Adding $alias->{'address'} to $alias->{'dev'}" . " (label $alias->{'dev'}" . ":$alias->{'name'})\n"; } $result = runcmd( "ip address add $alias->{'address'}" . " dev $alias->{'dev'}" . " label $alias->{'dev'}:$alias->{'name'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } return( $result->{'stat'} ); } sub cidr2netmask { my $cidr = shift; my $bit = ( 2 ** ( 32 - $cidr ) ) - 1; my $full_mask = unpack( 'N', pack( 'C4', 255,255,255,255 ) ); my $netmask = join( '.', unpack( 'C4', pack( 'N', ( $full_mask ^ $bit ) ) ) ); return( $netmask ); } sub error { my $status = shift; my $errstr = shift; my $level = 1; my @stack = (); chomp( $errstr ); print "\n"; print STDERR ( caller() )[1] .":\n $errstr at line " . ( caller() )[2] . "\n"; if ( VERBOSE ) { if ( caller( $level ) ) { print "\n=== Stack Trace ===\n"; } while ( my @trace = caller( $level++ ) ) { print STDERR " $trace[1]:\n" . " $trace[3]() called at line $trace[2]\n"; } } print "\n"; if ( $status ) { exit( $status ); } } sub getroutes { my $routes = {}; my $result = runcmd( 'ip route show' ); if ( $result->{'stat'} || ! $result->{'stdout'} ) { return; } open( my $fh, '<', \$result->{'stdout'} ); while ( <$fh> ) { if ( $_ =~ /^(\S+)\s+via\s+(\S+)\s+dev\s+(\S+)/ ) { $routes->{$1}{'via'} = $2; $routes->{$1}{'dev'} = $3; } } close( $fh ); return( $routes ); } sub ifdown { my $iface = shift; my $result = {}; my $stat = 0; if ( VERBOSE ) { print "Flushing interface $iface->{'name'}\n"; } $result = runcmd( "ip addr flush dev $iface->{'name'}" ); if ( $result->{'stat'} ) { $stat = $result->{'stat'}; } if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } print "Bringing down interface $iface->{'name'}\n"; } $result = runcmd( "ip link set $iface->{'name'} down" ); if ( $result->{'stat'} ) { $stat = $result->{'stat'}; } if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } return( $stat ); } sub ifup { my $iface = shift; my $runningconf = shift; my $routes = getroutes(); my $result; my $stat = 0; if ( my $old = $runningconf->{$iface->{'name'}} ) { if ( ( $old->{'address'} && $iface->{'address'} && $old->{'address'} eq $iface->{'address'} ) || ( $old->{'state'} eq 'UP' && $iface->{'inet'} eq 'dhcp' ) ) { if ( VERBOSE ) { print STDERR "Interface $iface->{'name'}" . " is already up\n"; } return; } if ( VERBOSE ) { print "Flushing interface $iface->{'name'}\n"; } $result = runcmd( "ip addr flush dev $iface->{'name'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } if ( $result->{'stat'} ) { return( $result->{'stat'} ); } } if ( $iface->{'macaddr'} ) { if ( VERBOSE ) { print "Setting MAC address on interface" . " $iface->{'name'} to $iface->{'macaddr'}\n"; } $result = runcmd( "ip link set dev $iface->{'name'}" . " address $iface->{'macaddr'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } if ( $result->{'stat'} ) { return( $result->{'stat'} ); } } if ( VERBOSE ) { print "Bringing up interface $iface->{'name'}\n"; } if ( $iface->{'inet'} && $iface->{'inet'} eq 'static' ) { if ( VERBOSE ) { print "Setting address to $iface->{'address'}" . " on interface $iface->{'name'}\n"; } $result = runcmd( "ip address add $iface->{'address'}" . " dev $iface->{'name'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } if ( $result->{'stat'} ) { return( $result->{'stat'} ); } } elsif ( $iface->{'inet'} && $iface->{'inet'} eq 'loopback' && ! $runningconf->{$iface->{'name'}}{'name'} ) { if ( VERBOSE ) { print "Setting address to $iface->{'address'}" . " on interface $iface->{'name'}\n"; } $result = runcmd( "ip -d address add $iface->{'address'}" . " dev $iface->{'name'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } if ( $result->{'stat'} ) { return( $result->{'stat'} ); } } elsif ( $iface->{'inet'} && $iface->{'inet'} eq 'dhcp' ) { if ( -f '/var/run/dhclient.pid' ) { system( "dhclient -r $iface->{'name'}" ); } if ( VERBOSE ) { print "Starting dhclient for $iface->{'name'}\n"; } $result = runcmd( "dhclient $iface->{'name'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } if ( $result->{'stat'} ) { return( $result->{'stat'} ); } } $result = runcmd( "ip link set $iface->{'name'} up" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } if ( $result->{'stat'} ) { return( $result->{'stat'} ); } if ( ! $routes->{'default'} && $iface->{'gateway'} ) { if ( VERBOSE ) { print "Adding default gateway $iface->{'gateway'}\n"; } $result = runcmd( "ip route add default" . " via $iface->{'gateway'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } if ( $result->{'stat'} ) { return( $result->{'stat'} ); } } elsif ( $routes->{'default'}{'via'} && $iface->{'gateway'} && $routes->{'default'}{'via'} ne $iface->{'gateway'} && $routes->{'default'}{'dev'} eq $iface->{'name'} ) { if ( VERBOSE ) { print "Changing default gateway to" . " $iface->{'gateway'}\n"; } $result = runcmd( "ip route delete default via" . " $routes->{'default'}{'via'} dev $iface->{'name'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } if ( $result->{'stat'} ) { return( $result->{'stat'} ); } $result = runcmd( "ip route add default" . " via $iface->{'gateway'}" ); if ( VERBOSE ) { if ( $result->{'stdout'} ) { print $result->{'stdout'}; } if ( $result->{'stderr'} ) { print STDERR $result->{'stderr'}; } } if ( $result->{'stat'} ) { return( $result->{'stat'} ); } } if ( $iface->{'routes'} ) { foreach my $target ( keys( %{$iface->{'routes'}} ) ) { if ( VERBOSE ) { print "Adding route $target via" . " $iface->{'routes'}{$target}" . " dev $iface->{'name'}\n"; } system( "ip route add $target via" . " $iface->{'routes'}{$target}" . " dev $iface->{'name'}" ); } } } sub listfiles { my $dir = shift; my $files = []; my $sorted = []; if ( ! -d $dir ) { return; } opendir( DIR, $dir ) || error( int( $! ), "opendir(): $dir: $!" ); while ( readdir( DIR ) ) { if ( ! -f "$dir/$_" || $_ =~ /^\./ ) { next; } push ( @$files, $_ ); } @$sorted = sort( @$files ); return( $sorted ); } sub netmask2cidr { my $netmask = shift; my $bits = { 0 => 0, 128 => 1, 192 => 2, 224 => 3, 240 => 4, 248 => 5, 252 => 6, 254 => 7, 255 => 8 }; my $lastoct; my $cidr = 0; foreach ( split( /\./, $netmask ) ) { if ( ( $lastoct && $lastoct != 255 && $_ != 0 ) || ! defined( $bits->{$_} ) ) { error( int( $! ), "netmask2cidr(): $netmask:" . " Invalid netmask" ); } $cidr += $bits->{$_}; $lastoct = $_; } return( $cidr ); } sub readconf { my $file = shift; my $conf = shift || {}; my $iface; open( my $config, "<$file" ) || error( int( $! ), "open(): $file: $!" ); while ( <$config> ) { if ( $_ =~ /^\s*#/ ) { next; } if ( $_ =~ /source-directory\s+(\S+)/ ) { my $path = $1; my $files; if ( substr( $path, 0, 1 ) ne '/' ) { $path = CONFDIR . "/$path"; } $files = listfiles( $path ); foreach my $file ( @$files ) { readconf( "$path/$file", $conf ); } } elsif ( $_ =~ /auto\s+(\S+)/ && $1 =~ /^(\S+):(\S+)$/ ) { $conf->{$1}{'aliases'}{$2}{'auto'} = 1; } elsif ( $_ =~ /auto\s+(\S+)/ ) { $conf->{$1}{'auto'} = 1; } elsif ( $_ =~ /iface\s+(\S+):(\S+)/ ) { $iface = "$1:$2"; $conf->{$1}{'aliases'}{$2}{'dev'} = $1; $conf->{$1}{'aliases'}{$2}{'name'} = $2; $conf->{$1}{'aliases'}{$2}{'cnt'} = keys( %{$conf->{$1}{'aliases'}} ); } elsif ( $_ =~ /iface\s+(\S+)\s+(\S+)\s+(\S+)/ ) { $iface = $1; if ( $conf->{$iface}{'inet'} ) { error( -1, "Multiple instances of $iface" . " in $file" ); } $conf->{$iface}{$2} = $3; $conf->{$iface}{'name'} = $iface; if ( $3 eq 'loopback' ) { $conf->{$iface}{'address'} = '127.0.0.1/8'; } $conf->{$iface}{'cnt'} = scalar( keys( %$conf ) ); } elsif ( $_ =~ /route\s+(\S+)\s+via\s+(\S+)/ ) { $conf->{$iface}{'routes'}{$1} = $2; } elsif ( $iface && $_ =~ /^\s*(\S+)\s+(\S+)/ ) { my ( $attr, $value ) = ( $1, $2 ); if ( $iface =~ /^(\S+):(\S+)/ ) { $conf->{$1}{'aliases'}{$2}{$attr} = $value; } else { $conf->{$iface}{$attr} = $value; } } elsif ( $iface && $_ =~ /^\s*(\S+)\s+(\S+)/ ) { $conf->{$iface}{$1} = $2; } } close( $config ); foreach my $devname ( keys( %$conf ) ) { my $dev = $conf->{$devname}; if ( $dev->{'address'} && $dev->{'netmask'} ) { $dev->{'address'} .= '/' . netmask2cidr( $dev->{'netmask'} ); undef( $dev->{'netmask'} ); } if ( ! keys( %{$dev->{'aliases'}} ) ) { next; } foreach my $aliasname ( keys( %{$dev->{'aliases'}} ) ) { my $alias = $dev->{'aliases'}{$aliasname}; if ( $alias->{'address'} && $alias->{'netmask'} ) { $alias->{'address'} .= '/' . netmask2cidr( $alias->{'netmask'} ); undef( $alias->{'netmask'} ); } } } return( $conf ); } sub runcmd { if ( NOACT ) { return( { stat => 0 } );; } my $cmd = shift; my $result = { stdout => '', stderr => '', stat => '' }; my $sel = IO::Select->new(); my $pid = eval { return( open3( \*CHLDIN, \*CHLDOUT, \*CHLDERR, $cmd ) ); } || error( int( $! ), "open3(): $cmd: $!" ); close( CHLDIN ); $sel->add( *CHLDOUT, *CHLDERR ); while ( my @fhs = $sel->can_read ) { foreach my $fh ( @fhs ) { if ( eof( $fh ) ) { $sel->remove( $fh ); next; } if ( fileno( $fh ) == fileno( *CHLDOUT ) ) { $result->{'stdout'} .= <$fh>; } elsif ( fileno( $fh ) == fileno( *CHLDERR ) ) { $result->{'stderr'} .= <$fh>; } } } close( CHLDOUT ); close( CHLDERR ); waitpid( $pid, 0 ); $result->{'stat'} = $? >> 8; return( $result ); } sub runningconf { my $cmd = 'ip -d address show'; my $sel = IO::Select->new(); my $pid = eval { return( open3( \*CHLDIN, \*CHLDOUT, \*CHLDERR, $cmd ) ); } || error( int( $! ), "open3(): $cmd: $!" ); my $stat; my $stdout = ''; my $stderr = ''; my $runningconf = {}; my $macaddr; my $dev; close( CHLDIN ); $sel->add( *CHLDOUT, *CHLDERR ); while ( my @fhs = $sel->can_read ) { foreach my $fh ( @fhs ) { if ( eof( $fh ) ) { $sel->remove( $fh ); next; } if ( fileno( $fh ) == fileno( *CHLDOUT ) ) { $stdout .= <$fh>; } elsif ( fileno( $fh ) == fileno( *CHLDERR ) ) { $stderr .= <$fh>; } } } close( CHLDOUT ); close( CHLDERR ); waitpid( $pid, 0 ); $stat = $? >> 8; if ( $stat ) { error( int( $! ), "open3: $cmd: $stderr" ); } open( my $fh, '<', \$stdout ) || error( int( $! ), "open(): $!" ); while ( <$fh> ) { if ( $_ =~ /^\d+:\s*(\S+):/ ) { $dev = $1; } if ( $_ =~ /\s+mtu\s+(\d+)/ ) { $runningconf->{$dev}{'mtu'} = $1; } if ( $_ =~ /\s+qdisc\s+(\S+)/ ) { $runningconf->{$dev}{'qdisc'} = $1; } if ( $_ =~ /\s+state\s+(\S+)/ ) { $runningconf->{$dev}{'state'} = $1; } if ( $_ =~ /\s+group\s+(\S+)/ ) { $runningconf->{$dev}{'group'} = $1; } if ( $_ =~ /\s+qlen\s+(\S+)/ ) { $runningconf->{$dev}{'qlen'} = $1; } if ( $_ =~ /^\s+link\/(\S+)\s+(\S+)/ ) { $runningconf->{$dev}{'link'} = $1; $runningconf->{$dev}{'macaddr'} = $2; } if ( $_ =~ /\s+promiscuity\s+(\S+)/ ) { $runningconf->{$dev}{'promiscuity'} = $1; } if ( $_ =~ /^\s+inet\s+(\S+).*\s+(\S+):(\S+)/ ) { $runningconf->{$2}{'aliases'}{$3}{'address'} = $1; $runningconf->{$2}{'aliases'}{$3}{'dev'} = $dev; $runningconf->{$2}{'aliases'}{$3}{'name'} = $3; } elsif ( $_ =~ /^\s+inet\s+(\S+).*(\S+)$/ ) { if ( $runningconf->{$dev}{'address'} ) { next; } $runningconf->{$dev}{'address'} = $1; } } close( $fh ); return( $runningconf ); } my $conf = readconf( CONFIGFILE ); my $runningconf = runningconf(); my $self = ( split( '/', $0 ) )[-1]; my $ifaces = []; my $all = eval { for ( my $i = 0; $i <= $#ARGV; $i++ ) { if ( $ARGV[$i] eq '-a' || $ARGV[$i] eq '--all' ) { splice( @ARGV, $i, 1 ); return( 1 ); } } }; my $stat = 0; #print Dumper( $conf ); exit; if ( grep( $_ eq '-h' || $_ eq '--help', @ARGV ) ) { print "Usage: $self [OPTIONS] [INTERFACES]\n"; if ( $self eq 'ifup' ) { print "Bring a network interface up\n\n"; } elsif ( $self eq 'ifdown' ) { print "Bring a network interface down\n\n"; } elsif ( $self eq 'ifreload' ) { print "Reload interface configuration\n\n"; } elsif ( $self eq 'ifquery' ) { print "View current status of interfaces\n\n"; } print "Options:\n" . "\t-a, --all\t\tperform actions on all 'auto' interfaces\n" . "\t-h, --help\t\tprint usage information\n"; if ( $self ne 'ifquery' ) { print "\t-n, --no-act\t\tdisplay actions without taking them\n" . "\t-v, --verbose\t\tdisplay actions as they occur\n"; } print "\t-V, --version\t\tdisplay version information\n"; exit( 0 ); } elsif ( grep( $_ eq '-V' || $_ eq '--version', @ARGV ) ) { print "iftools version " . VERSION . "\n"; exit( 0 ); } if ( ! $all ) { foreach ( @ARGV ) { ( my $dev = $_ ) =~ s/:.*//; if ( $runningconf->{$dev} ) { push( @$ifaces, $_ ); } elsif ( VERBOSE ) { print STDERR "$_: No such device\n"; } } } else { foreach ( sort { $conf->{$a}{'cnt'} <=> $conf->{$b}{'cnt'} } ( keys( %$conf ) ) ) { ( my $dev = $_ ) =~ s/:.*//; if ( $self eq 'ifup' && ! $conf->{$_}{'auto'} ) { next; } if ( $runningconf->{$dev} ) { push( @$ifaces, $_ ); } elsif ( VERBOSE ) { print STDERR "$_: No such device\n"; next; } foreach my $alias ( sort { $conf->{$dev}{'aliases'}{$a}{'cnt'} <=> $conf->{$dev}{'aliases'}{$b}{'cnt'} } keys( %{$conf->{$dev}{'aliases'}} ) ) { push( @$ifaces, "$dev:$alias" ); } } } foreach my $iface ( @$ifaces ) { if ( $self eq 'ifup' ) { if ( $iface =~ /^(\S+):(\S+)/ ) { $stat = aliasup( $conf->{$1}{'aliases'}{$2}, $runningconf ) || $stat; } else { $stat = ifup( $conf->{$iface}, $runningconf ) || $stat; } } elsif ( $self eq 'ifdown' ) { if ( $iface =~ /^(\S+):(\S+)/ ) { $stat = aliasdown( $conf->{$1}{'aliases'}{$2}, $runningconf ) || $stat; } else { $stat = ifdown( $conf->{$iface}, $runningconf ) || $stat; } } elsif ( $self eq 'ifreload' ) { if ( $iface =~ /^(\S+):(\S+)/ ) { $stat = aliasdown( $conf->{$1}{'aliases'}{$2}, $runningconf ) || $stat; $runningconf = runningconf(); $stat = aliasup( $conf->{$1}{'aliases'}{$2}, $runningconf ) || $stat; } else { $stat = ifdown( $conf->{$iface}, $runningconf ) || $stat; $runningconf = runningconf(); $stat = ifup( $conf->{$iface}, $runningconf ) || $stat; } } elsif ( $self eq 'ifquery' ) { if ( $iface =~ /^\S+:\S+/ ) { next; } elsif ( ! $runningconf->{$iface} ) { $runningconf->{$iface} = { state => 'No such device', address => '', macaddr => '' }; } print "[$iface]\n" . " State: $runningconf->{$iface}{'state'}\n"; if ( $runningconf->{$iface}{'address'} ) { print " Address: $runningconf->{$iface}{'address'}\n"; } if ( $runningconf->{$iface}{'macaddr'} ) { print " MAC: $runningconf->{$iface}{'macaddr'}\n"; } foreach my $name ( sort( keys( %{$runningconf->{$iface}{'aliases'}} ) ) ) { my $alias = $runningconf->{$iface}{'aliases'}{$name}; print "[$iface:$name]\n" . " Address: $alias->{'address'}\n"; } } $runningconf = runningconf(); } close( SELF ); exit( $stat );