Subversion Repositories sysadmin_scripts

Rev

Blame | Last modification | View Log | Download | RSS feed

#! /usr/bin/env perl

=head1 NAME

backdoor - Network Backdoor Management Script

=head1 SYNOPSIS

    backdoor [options]            # Check remote sites and bring up/down accordingly
    backdoor [options] up         # Manually bring up the backdoor interface
    backdoor [options] down       # Manually bring down the backdoor interface
    
    Options:
      --test              Test mode - show commands without executing them
      --verbose           Display interface status after changes
      --help              Show this help message

=head1 DESCRIPTION

This script manages a network backdoor interface by checking remote sites for
specific MD5 checksums. If a matching checksum is found, the backdoor interface
is brought up with configured routes. If no match is found, the interface is
brought down.

The script can also be run with manual 'up' or 'down' commands to directly
control the backdoor interface without checking remote sites.

Configuration is managed via a YAML file, which will be created with default
values if it doesn't exist on first run.

=head1 CONFIGURATION

The configuration file is located at: ./backdoor.conf.yaml

If the configuration file does not exist, it will be created with default values
on first run. You must edit this file with your specific network settings before
using the script.

Configuration options:
  - TEST:            Set to 1 to enable test mode (show commands without executing)
                     Also changes configuration file to cwd/backdoor.conf.yaml
  - verbose:         Set to 1 to display interface status after changes
  - ip:              IP address to assign to the backdoor interface
  - netmask:         Network mask for the interface
  - nic:             Network interface card name (e.g., eth0, eth1)
  - gateway:         Gateway/router IP for routing
  - allowedSubnets:  Hash of allowed subnets/IPs with netmask and ports
                     Key is the subnet/IP (can be DNS name), value contains:
                       - netmask: CIDR notation (e.g., 24, 32)
                       - ports: Port numbers to allow
  - sitesToCheck:    Hash of URLs to check with their expected MD5 checksums
                     Key is the URL, value is the expected checksum

Note: Command-line options (--test, --verbose) override configuration file settings.

=head1 REQUIREMENTS

  - Perl 5.x
  - YAML::Tiny module
  - Getopt::Long module (core Perl)
  - Pod::Usage module (core Perl)
  - wget command-line tool
  - ifconfig and ip route commands (iproute2 package)
  - Root/sudo privileges for network configuration

=head1 EXAMPLES

  # Run in test mode to see what commands would be executed
  backdoor --test

  # Check remote sites and manage interface accordingly with verbose output
  backdoor --verbose

  # Manually bring up the interface without checking remote sites
  backdoor up

  # Manually bring down the interface in test mode
  backdoor --test down

  # Show help documentation
  backdoor --help

=head1 OPERATION

The script operates in two modes:

1. Automatic Mode (default): Checks configured remote sites for MD5 checksums.
   If a matching checksum is found, the backdoor interface is brought up.
   Otherwise, it is brought down.

2. Manual Mode: When 'up' or 'down' is specified as an argument, the script
   directly controls the interface without checking remote sites.

The script is idempotent - it will not attempt to bring up an interface that
is already up, or bring down an interface that is already down.

=head1 LICENSE

Copyright (c) 2025, R. W. Rodolico
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

=head1 AUTHOR

R. W. Rodolico <rodo@dailydata.net>

=head1 VERSION

Version 1.0.0 - December 2025

=cut

use strict;
use warnings;
use YAML::Tiny;
use Getopt::Long;
use Pod::Usage;

my $configFile = './backdoor.conf.yaml';  # Path to YAML configuration file

# Default configuration
# $config is a hashref used globally by routines; no parameters are passed in many cases
my $config = {
   TEST        => 0,         # Set to 1 to test without executing commands
   verbose     => 0,         # Set to 1 to display interface status after changes
   ip          => '0.0.0.0', # the IP to assign when bringing up
   netmask     => '255.255.255.0', # the netmask to use
   nic         => 'eth1', # the network interface card to use
   gateway     => '0.0.0.0', # the upstream router/gateway
   # Allowed Subnet/IPs for backdoor access. The value is what ports to open.
   # may also be a DNS resolvable URL (uses realIP subroutine to resolve)
   allowedSubnets  => {
      '0.0.0.0' => { netmask => 24, ports => '22' },
   },
   # key is site to check via wget, value is the expected md5 checksum
   sitesToCheck       => {
      'https://example.com/key1' => 'f9620d8865581a9402a49e25b3504ff7',
      'https://example.org/key2' => 'a196c22275cd5e54400bd63d5954cf01',
   },
};

# Subroutines for network management

# determines the real IP address of a domain name or returns the IP if given an IP
# Uses the 'host' command to resolve domain names to IP addresses
#
# Parameters:
#   $ip - an IP address or domain name
#
# Returns: the resolved IP address or an empty string on failure
sub realIP {
   my $ip = shift;
   # if it is a real IP address, just return it
   return $ip if $ip =~ m/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
   # Otherwise, use host to grab the IP address
   $ip = `host -t A $ip | rev | cut -d' ' -f1 | rev`;
   chomp $ip;
   return $ip;
}

# checks if the backdoor interface is currently up
# Searches for the configured IP in the interface output
#
# Returns: 1 if up, 0 if down
sub currentStatus {
   my $out = `ifconfig $config->{nic} | grep $config->{ip}`;
   return $out =~ m/$config->{ip}/;
}
   
# runs a list of commands
# Executes system commands, respecting the $config->{TEST} flag
#
# Parameters:
#   @commands - list of shell commands to execute
#
# Returns: nothing
sub runCommand {
   while ( my $command = shift ) {
      print "$command\n" if $config->{TEST};
      `$command` unless $config->{TEST};
   }
}

# brings up the backdoor interface if it is down
# Configures the network interface with the specified IP/netmask and
# adds routes for all allowed subnets through the configured gateway
#
# Returns: nothing (exits early if already up)
sub bringUp {
   my $currentStatus = &currentStatus();
   return if $currentStatus; # Already up
   
   print "Open Sesame\n";
   # configure the interface and bring it up
   &runCommand( "ifconfig $config->{nic} $config->{ip} netmask $config->{netmask}" );
   foreach my $allowedSubnet ( keys %{$config->{allowedSubnets}} ) {
      my $subnetIP = &realIP( $allowedSubnet );
      my $netmask = $config->{allowedSubnets}->{$allowedSubnet}->{netmask} || 32;
      $subnetIP .= "/$netmask";
      &runCommand( "ip route add $subnetIP via $config->{gateway} dev $config->{nic}" );
   }
   # display the status
   &runCommand( "ifconfig $config->{nic}" ) if $config->{verbose};
   &runCommand( "route -n" ) if $config->{verbose};

}

# brings down the backdoor interface if it is up
# Removes all configured routes and brings down the network interface
#
# Returns: nothing (exits early if already down)
sub bringDown {
   my $currentStatus = &currentStatus();
   return unless $currentStatus; # Already down
   
   print "You shall not pass!\n";
   # first, remove the routes
   foreach my $allowedSubnet ( keys %{$config->{allowedSubnets}} ) {
      my $subnetIP = &realIP( $allowedSubnet );
      my $netmask = $config->{allowedSubnets}->{$allowedSubnet}->{netmask} || 32;
      $subnetIP .= "/$netmask";
      &runCommand( "ip route del $subnetIP" );
   }
   # reset and bring down the interface
   &runCommand( "ifconfig $config->{nic} 192.168.1.1 netmask 255.255.255.255" );
   &runCommand( "ifconfig $config->{nic} down" );
   # display the status
   &runCommand( "ifconfig $config->{nic}" ) if $config->{verbose};
   &runCommand( "route -n" ) if $config->{verbose};
}

# Subroutine to check remote sites
# Checks each URL in the sitesToCheck hash for its expected MD5 checksum
# 
# Parameters:
#   $sitesToCheck - hashref of URL => expected_md5_checksum
#
# Returns:
#   1 if any site returns the expected checksum
#   0 if no matches are found
sub checkRemote {
   my ( $sitesToCheck ) = @_;
   
   foreach my $url ( keys %$sitesToCheck ) {
      my $expectedChecksum = $sitesToCheck->{$url};
      my $output = `wget -q -O - $url | md5sum`;
      $output =~ m/^([a-f0-9]+)/i;
      if ( $1 eq $expectedChecksum ) {
         return 1; # Match found
      }
   }
   
   return 0; # No matches
}

#==============================================================================
# Main Program Logic
#==============================================================================

# Read config file or create it with defaults if it doesn't exist
# Path to YAML configuration file. It can be in cwd or /etc/backdoor/
$configFile = -f './backdoor.conf.yaml' ? './backdoor.conf.yaml' : '/etc/backdoor/backdoor.conf.yaml';
if ( -e $configFile ) {
   # Config file exists, read it
   my $yaml = YAML::Tiny->read( $configFile );
   my $fileConfig = $yaml->[0];
   # Merge file config with current config
   foreach my $key ( keys %$fileConfig ) {
      $config->{$key} = $fileConfig->{$key};
   }
} else {
   # Config file doesn't exist, create it with defaults
   my $yaml = YAML::Tiny->new( $config );
   makedir( '/etc/backdoor', 0755 ) unless -d '/etc/backdoor';
   $yaml->write( $configFile );
   die "Created default config file at $configFile. Please edit it and rerun.\n";
}

# Parse command-line options (these override config file settings)
my $help = 0;
GetOptions(
   'test'     => \$config->{TEST},
   'verbose'  => \$config->{verbose},
   'help|?'   => \$help,
) or pod2usage(2);

pod2usage(1) if $help;

# Validate that all required configuration fields are present
die "Configuration file $configFile is missing required fields.\n"
   unless exists $config->{ip}
      && exists $config->{netmask}
      && exists $config->{nic}
      && exists $config->{gateway}
      && exists $config->{sitesToCheck};

# Process command-line argument if provided
my $command = $ARGV[0] ? lc shift : '';
if ( $command eq 'up' ) {
   &bringUp();
   exit 0;
} elsif ( $command eq 'down' ) {
   &bringDown();
   exit 0;
}

# Check remote sites and manage backdoor interface accordingly
if ( checkRemote( $config->{sitesToCheck} ) ) {
   &bringUp();
   exit 0;
} else {
   &bringDown();
   exit 0;
}

1;