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 = ¤tStatus();
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 = ¤tStatus();
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;