#!/usr/bin/perl -w
#
############################################################################
#
# File: fwknop_serv
#
# Purpose: To provide a minimal TCP server over which the fwknop client can
#          connect to send the SPA packet.  This breaks the traditional SPA
#          model of only using a single packet to transmit desired access
#          modifications, but if you want to send SPA packets over the Tor
#          network then this server is necessary.  A circuit through the
#          Tor network is built up over successive TCP connections, and
#          there is no way to send packets through Tor without an
#          established circuit.
#
# Author: Michael Rash (mbr@cipherdyne.org)
#
# Version: 1.9.11
#
# Copyright (C) 2004-2008 Michael Rash (mbr@cipherdyne.org)
#
# License (GNU Public License):
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
#    USA
#
############################################################################
#
# $Id: fwknop_serv 1433 2009-05-12 01:58:26Z mbr $
#

use IO::Socket;
use POSIX;
use Getopt::Long;
use strict;

my $config_file = '/etc/fwknop/fwknop.conf';
my %config = ();
my $print_help = 0;
my $no_locale  = 0;
my $print_version  = 0;
my $cmdline_locale = '';
my $override_config_str = '';

my $version = '1.9.11';
my $revision_svn = '$Revision: 1359 $';
my $rev_num = '1';
($rev_num) = $revision_svn =~ m|\$Rev.*:\s+(\S+)|;

### run GetOpt() to get comand line args
&handle_command_line();

&usage(0) if $print_help;

&setup();

my $server = IO::Socket::INET->new(
    LocalPort => $config{'TCPSERV_PORT'},
    Type   => SOCK_STREAM,
    Reuse  => 1,
    Listen => 5
) or die $!;

&drop_privs();

### trivial loop; we just want the local TCP stack to accept connections;
### fwknopd gets data from pcap anyway
while (my $client = $server->accept()) {

    $client->recv(my $request, 1500, 0);
}

close $server;

exit 0;

#================================= end main ===============================

sub drop_privs() {

    my ($login, $pass, $uid, $gid) = getpwnam('nobody');
    unless ($uid and $gid) {
        warn "[-] Could not get UID and GID of user nobody";
    }

    ### drop privileges
    if ($uid and $gid) {
        POSIX::setuid($uid);
        POSIX::setgid($gid);
    }

    return;
}

sub setup() {

    ### import any override config files first
    &import_override_configs() if $override_config_str;

    ### import config
    &import_config();

    ### expand any embedded vars within config values
    &expand_vars();

    ### make sure all the vars we need are actually in the config file.
    &required_vars();

    ### validate config
    &validate_config();

    my $pid = fork();
    exit 0 if $pid;
    die "[*] $0: Couldn't fork: $!" unless defined $pid;
    POSIX::setsid() or die "[*] $0: Can't start a new session: $!";

    ### make sure there isn't another fwknop_serv process already running
    &uniquepid();

    ### write our pid out to disk
    &writepid();

    &handle_locale();

    return;
}

sub handle_locale() {
    if ($config{'LOCALE'} ne 'NONE') {
        ### set LC_ALL env variable
        $ENV{'LC_ALL'} = $config{'LOCALE'};
    }
    return;
}

sub validate_config() {
    unless ($config{'TCPSERV_PORT'} > 0
            and $config{'TCPSERV_PORT'} < 65535) {
        die "[*] TCPSERV_PORT must be between 1 and 65535";
    }
    return;
}

sub uniquepid() {
    if (-e $config{'TCPSERV_PID_FILE'}) {
        my $caller = $0;
        open PIDFILE, "< $config{'TCPSERV_PID_FILE'}";
        my $pid = <PIDFILE>;
        close PIDFILE;
        chomp $pid;
        if (kill 0, $pid) {  # fwknop_serv is already running
            die "[*] fwknop_serv (pid: $pid) is already running!  Exiting.\n";
        }
    }
    return;
}

sub writepid() {
    open P, "> $config{'TCPSERV_PID_FILE'}" or die "[*] Could not open ",
        "$config{'TCPSERV_PID_FILE'}: $!";
    print P $$, "\n";
    close P;
    chmod 0600, $config{'TCPSERV_PID_FILE'};
    return;
}

sub import_override_configs() {
    my @override_configs = split /,/, $override_config_str;
    for my $file (@override_configs) {
        die "[*] Override config file $file does not exist"
            unless -e $file;
        &import_config($file);
    }
    return;
}

sub import_config() {
    open C, "< $config_file" or die "[*] Could not open ",
        "config file $config_file: $!";
    while (<C>) {
        next if /^\s*#/;
        if (/^\s*(\S+)\s+(\S+);/) {
            $config{$1} = $2;
        }
    }
    close C;
    return;
}

sub expand_vars() {

    my $has_sub_var = 1;
    my $resolve_ctr = 0;

    while ($has_sub_var) {
        $resolve_ctr++;
        $has_sub_var = 0;
        if ($resolve_ctr >= 20) {
            die "[*] Exceeded maximum variable resolution counter.";
        }
        for my $var (keys %config) {
            my $val = $config{$var};
            if ($val =~ m|\$(\w+)|) {
                my $sub_var = $1;
                die "[*] sub-ver $sub_var not allowed within same ",
                    "variable $var" if $sub_var eq $var;
                if (defined $config{$sub_var}) {
                    $val =~ s|\$$sub_var|$config{$sub_var}|;
                    $config{$var} = $val;
                } else {
                    die "[*] sub-var \"$sub_var\" not defined in ",
                        "config for var: $var."
                }
                $has_sub_var = 1;
            }
        }
    }
    return;
}

sub handle_command_line() {

    ### make Getopts case sensitive
    Getopt::Long::Configure('no_ignore_case');
    die "[*] Use --help for usage information.\n"  unless (GetOptions(
        'config=s'  => \$config_file,
        'Override-config=s' => \$override_config_str,
        'LC_ALL=s'  => \$cmdline_locale,
        'locale=s'  => \$cmdline_locale,
        'no-LC_ALL' => \$no_locale,
        'no-locale' => \$no_locale,
        'Version'   => \$print_version,
        'help'      => \$print_help
    ));
    return;
}

sub required_vars() {
    for my $var qw(TCPSERV_PORT TCPSERV_PID_FILE LOCALE) {
        die "[*] Required variable $var is not defined in $config_file"
            unless defined $config{$var};
    }
    return;
}

sub usage() {
    my $exit_status = shift;
    print <<_HELP_;

fwknop_serv - Lightweight TCP socket service for fwknop SPA communications

[+] Version: $version (file revision: $rev_num)
    By Michael Rash (mbr\@cipherdyne.org)
    URL: http://www.cipherdyne.org/fwknop/

Usage: fwknop_serv [options]

Options:
    -c, --config <file>         - Specify path to config file instead of
                                  using the default path:
                                  $config_file
    -O, --Override-config <str> - Allow config variables from the normal
                                  $config_file to be superseded with values
                                  from the specified file(s).
_HELP_

    exit $exit_status;
}
