#!/usr/bin/perl
#
# PrivateOn-VPN -- Because privacy matters.
#
# Author: Mikko Rautiainen <info@tietosuojakone.fi>
#
# Copyright (C) 2014-2015  PrivateOn / Tietosuojakone Oy, Helsinki, Finland
# All rights reserved. Use is subject to license terms.
#

#
#		/opt/PrivateOn-VPN/vpn-monitor/vpn-monitor
#
#   This daemon verifies that a VPN connection is active and functioning.
#   If the vpn is inactive, the last used VPN connection is retried. 
#   If this fails, the network is crippled until the vpn connection is 
#   refreshed or turned off from the vpn-gui front-end.
#
#  Note: The vpn-gui requires that this daemon is running.
#


use strict;
use warnings;
use sigtrap qw(die normal-signals); 
use feature 'state';
use lib '/opt/PrivateOn-VPN/vpn-monitor/';

use Fcntl qw(:flock);
use File::Basename qw(dirname);
use File::Path qw(make_path);
use File::stat;
use IO::Interface::Simple;
use JSON qw(decode_json);
use JSON::backportPP;
use No::Worries::PidFile qw(pf_check pf_set pf_unset);
use POSIX qw(geteuid);
use Try::Tiny;
use UI::Dialog::Backend::KDialog;

#use AnyEvent::Impl::POE;
#use POE;
use AnyEvent;
use AnyEvent::Handle;
use AnyEvent::Socket;
use AnyEvent::Log;
use AnyEvent::Fork;
use AnyEvent::Fork::RPC;
use AnyEvent::HTTP;
use AnyEvent::CacheDNS ':register';

use constant {
	PATH          => '/opt/PrivateOn-VPN/',
	STATUS_FILE   => '/var/run/PrivateOn/.status',
	LOCK_FILE     => '/var/run/PrivateOn/.lock',
	PID_FILE      => '/var/run/PrivateOn/vpn-monitor.pid',
	LOG_FILE      => '/var/log/PrivateOn.log',
	DISPATCH_FILE => '/etc/NetworkManager/dispatcher.d/vpn-up',
	INI_FILE      => '/etc/PrivateOn/vpn-default.ini',
	SERVICE_NAME  => 'vpnmonitor',
	VERSION       => '1.1',
	DEBUG         => 1
};

use constant {
	NET_UNPROTECTED	=> 1,
	NET_PROTECTED	=> 2,
	NET_NEGATIVE	=> 3,
	NET_CONFIRMING	=> 4,
	NET_UNCONFIRMED	=> 5,
	NET_CRIPPLED	=> 6,
	NET_OFFLINE     => 7,
	NET_BROKEN	=> 8,
	NET_ERROR	=> 9,
	NET_UNKNOWN	=> 10
};

use constant {
	IPC_HOST	=> '127.0.0.1',
	IPC_PORT	=> 44244
};

use constant {
	API_CHECK_TIMEOUT         => 10,
	DETECT_CHANGE_INTERVAL    => 60,
	RETRY_WHEN_UNPROTECTED    => 'TRUE',
	RETRY_WHEN_NEGATIVE       => 'TRUE',
	RETRY_WHEN_UNCONFIRMED    => 'FALSE',
	CRIPPLE_WHEN_UNPROTECTED  => 'TRUE',
	CRIPPLE_WHEN_NEGATIVE     => 'TRUE',
	CRIPPLE_WHEN_UNCONFIRMED  => 'FALSE'
};

################	  Package-Wide Globals		################

my $Monitor_Enabled;            # monitor state (set in run_once())
my $Temporary_Disable = 0;      # used to temporarily disable crippling
my $Current_Task = "idle";      # stores the current forked task, idle if no task
my $Current_Status = 999;       # used to cache network status for get_monitor_state responses
my $Previous_Status = 999;      # used to store status result of previous iteration for detecting change 
my $Current_Update_Time = 0;    # used to store epoch time of last network status update for cache aging
my $HTTP_Request_Time = 0;      # used to store start time of last asynchronous HTTP request
my $Skip_Cleanup = 0;           # used to prevent cleanup when process aborted due to other instance running
my $Url_For_Api_Check;          # URL for checking VPN-provider's VPN status API (set in run_once())

my $CV = AnyEvent->condvar;     # Event loop object
my $ctx;                        # global AE logging context object
my $Detect_Change_Timer;        # Timer for periodic network status check
my $Api_Check_Timer;            # Timer for updating API status check before periodic network status check
my $Temporary_Disable_Timer;    # Timer for re-enabling monitor after GUI tasks
my $Lockfile_Handle;            # Keep exclusive lock alive until process exits
my $HTTP_Request_Handle;        # HTTP request handle for canceling previous request
my $TCP_Server_Handle;          # TCP server handle
my %TCP_Server_Connections;     # Keep TCP server alive after initialization


################	Network State subroutines	################

sub http_req_async
{
	my $url = shift;
	$Current_Status = NET_CONFIRMING;

	my $counter;
	if (DEBUG > 0) {
		state $persistent_counter = 0;
		$counter = ++$persistent_counter;
		$ctx->log(debug => "http_req_async sending request # " . $counter) if DEBUG > 2;
	}

	# cancel previous request
	undef $HTTP_Request_Handle;
	
	$HTTP_Request_Time = time();
	$HTTP_Request_Handle = http_request
		GET => $url,
		timeout => API_CHECK_TIMEOUT,
		sub {
			my ($data, $headers) = @_;

			if ( !defined($data) ) {
				$Current_Status = NET_UNCONFIRMED;
				$ctx->log(error => "http_req_async request error, " . (defined($counter) ? "request # $counter, " : "") . "Current_Status = NET_UNCONFIRMED");
				$ctx->log(debug => "\tPrevious_Status = " . get_status_text($Previous_Status) . "\tCurrent_Task = " . $Current_Task) if DEBUG > 2;
				$ctx->log(debug => "\tHTTP Status: " . $headers->{Status} || 'undef') if DEBUG > 0;
				$ctx->log(debug => "\tReason: " . $headers->{Reason} || 'undef') if DEBUG > 0;
				return;
			} elsif ( (length $data == 0) || ($headers->{"content-length"} == 0) ) {
				$Current_Status = NET_UNCONFIRMED;
				$ctx->log(error => "http_req_async returned empty content, " . (defined($counter) ? "request # $counter, " : "") . "Current_Status = NET_UNCONFIRMED");
				$ctx->log(debug => "\tPrevious_Status = " . get_status_text($Previous_Status) . "\tCurrent_Task = " . $Current_Task) if DEBUG > 2;
				$ctx->log(debug => "\tHTTP Status: " . ($headers->{Status} || 'undef') . "\tReason: " . ($headers->{Reason} || 'undef')) if DEBUG > 2;
				$ctx->log(debug => "\tContent-length: " . (defined($headers->{'content-length'}) ? $headers->{'content-length'} : 'undef') . "\tType: " . ($headers->{'content-type'} || 'undef')) if DEBUG > 0;
				return;
			}

			if ($data =~ /<meta name="flag" content="1"\/>/g) {
				$Current_Status = NET_CRIPPLED;
				return;
			}

			my $reply = decode_json($data);
			my $status = $reply->{'status'};
			if ($status eq 'Unprotected') { 
				$Current_Status = NET_NEGATIVE; 
			} elsif ($status eq 'Protected') { 
				$Current_Status = NET_PROTECTED; 
			} else {
				$Current_Status = NET_UNCONFIRMED;
				$ctx->log(error => "http_req_async returned unparseable data, " . (defined($counter) ? "request # $counter, " : "") . "Current_Status = NET_UNCONFIRMED");
				$ctx->log(debug => "\tPrevious_Status = " . get_status_text($Previous_Status) . "\tCurrent_Task = " . $Current_Task) if DEBUG > 2;
				$data =~ s/\R//g if DEBUG > 0;
				$ctx->log(debug => "http_req_async data=$data") if DEBUG > 0;
			}
		};

	return $Current_Status;
}


sub get_api_status 
{
	$Current_Update_Time = time();

	my $default_route_status = get_route_status();
	if ($default_route_status) {
		$Current_Status = $default_route_status;
		return $Current_Status;
	}

	if (tun0_exists() && $Url_For_Api_Check ne 'none') {
		# http_req_async subroutine has already changed Current_Status
		return http_req_async($Url_For_Api_Check);
	}

	$Current_Status = get_interface_operstate();
	return $Current_Status;
}


sub quick_net_status
{
	$Current_Update_Time = time();

	my $default_route_status = get_route_status();
	if ($default_route_status) {
		return $default_route_status;
	}

	my $tun0_exists = tun0_exists();
	unless (defined $tun0_exists) {
		return NET_BROKEN;
	}
	if ($tun0_exists) {
		if ($Current_Status == NET_CONFIRMING) {
			return NET_CONFIRMING;
		} elsif ($Current_Status == NET_UNCONFIRMED) {
			return NET_UNCONFIRMED;
		} elsif ($Current_Status == NET_NEGATIVE) {
			return NET_NEGATIVE;
		} else {
			return NET_PROTECTED;
		}
	}
	
	return get_interface_operstate();
}


sub get_route_status
{
	my $default_route_exists = 0;

	unless (open ROUTE, '<', '/proc/net/route') {
		$ctx->log(error => "Could not open /proc/net/route for reading.  Reason: " . $!);
		return NET_BROKEN;
	}

	while (<ROUTE>) {
		# return NET_CRIPPLED if default route is interface lo or 127.0.0.1
		if ( (/^lo\s+00000000\s+/) || (/^\S+\s+00000000\s+0100007F\s+/i) ) {
			close ROUTE;
			return NET_CRIPPLED;
		}
		# check that a default route exists
		if (/^\S+\s+00000000\s+/) {
			$default_route_exists = 1;
		}
	}
	close ROUTE;

	unless ($default_route_exists) {
		return NET_OFFLINE;
	}

	return 0;
}


sub tun0_exists
{
	my $result = 0;

	open(my $iph, "ip link list |") or do {
		return $result;
	};
	while (<$iph>) {
		if (/^\d+: (tun\d+):/) {
			$result = $1;
		}
	}
	close $iph;

	return $result;
}


sub get_interface_operstate
{
	my $net_status = NET_UNKNOWN;

	my $sys_net_path = "/sys/class/net/";
	my $net;
	my @interface_array;

	unless (opendir $net, $sys_net_path) {
		$ctx->log(error => "Could not open directory: " . $sys_net_path . " Reason: " . $!);
		return NET_BROKEN;
	}
	while (my $file = readdir($net)) {
		next unless (-d $sys_net_path.$file);
		# skip loopback interface
		next if ($file eq "lo");
		# directory is read in reverse order, so push to beginning of array
		unshift(@interface_array, $file);
	}
	closedir $net;

	foreach my $interface (@interface_array) {
		# skip interfaces that do not have a hardware address
		next unless (-e $sys_net_path.$interface."/address");
		open my $address, $sys_net_path.$interface."/address";
		my @lines = <$address>;
		close $address;
		next if ($lines[0] =~ /00\:00\:00\:00\:00\:00/);
		next unless ($lines[0]);

		next unless (-e $sys_net_path.$interface."/operstate");
		open my $operstate, $sys_net_path.$interface."/operstate";
		my @line = <$operstate>;
		close $operstate;
		next if ($line[0] =~ /^unknown/);
		if ($line[0] =~ /^down/) {
			$net_status = NET_OFFLINE;
			next;
		} elsif ($line[0] =~ /^up/) {
			# check that the interface has an IP address
			my $if = IO::Interface::Simple->new($interface);
			if ( defined($if) && defined($if->address) ) {
				if ( $if->address =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ ) {
					return NET_UNPROTECTED;
				}
			}
			# otherwise this interface is offline
			$net_status = NET_OFFLINE;
		}
	}
	return $net_status;
}


sub reversed_hex_to_octet
{
	my $hex = shift;
	my $octet = join('.', reverse map { hex($_); } ($hex =~ /([0-9a-f]{2})/gi));
	return $octet;
}


sub octet_to_reversed_hex
{
	my $octet = shift;
	my @octet = reverse split /\./, $octet;
	return sprintf '%02X%02X%02X%02X', @octet;
}


sub get_local_gateway_and_nic
{
	unless (open ROUTE, '<', '/proc/net/route') {
		$ctx->log(error => "Could not open /proc/net/route for reading.  Reason: " . $!);
		return (undef, undef);
	}
	chomp(my $firstline = <ROUTE>);
	my @headers = split /\s+/, $firstline;

	my $found_nic;
	my $found_hex;
	while (<ROUTE>) {
		chomp;
		next if /^\s*$/;
		my @values = split /\s+/, $_;
		my %line;
		@line{@headers} = @values;

		# if interface is not tun*/lo and entry has Gateway route flag 
		if ( ($line{Iface} !~ /^(tun\d|lo)$/) && (hex($line{Flags}) & 2) ) {
			$found_nic = $line{Iface};
			$found_hex = $line{Gateway};
			# end search if entry has Host route flag or entry is default route
			last if (hex($line{Flags}) & 4);
			last if ($line{Destination} eq '00000000');
			
		}
	}
	close ROUTE;

	if (!defined $found_nic || !defined $found_hex) {
		$ctx->log(error => "Could not find local gateway IP");
		return (undef, undef);
	}
	
	my $ip = reversed_hex_to_octet($found_hex);
	$ctx->log(debug => "Found local gateway IP: $ip on interface $found_nic") if DEBUG > 1;
	return ($ip, $found_nic);
}


sub get_monitor_state
{
	my $output;

	# monitor part values = Enabled/Disabled
	if ($Monitor_Enabled) {
		$output = "Enabled-";
	} else {
		$output = "Disabled-";
	}

	# task part values = crippled/uncrippling/retrying/temporary/idle
	if ($Current_Task eq 'temporary') {
		$output = "Disabled-temporary";
	} else {
		$output .= $Current_Task;
	}

	# refresh network status if cached data is over 20 seconds old
	if ( time() - $Current_Update_Time > 20 ) {
		$Current_Status = quick_net_status();
	}

	# network part values = UNPROTECTED/PROTECTED/NEGATIVE/CONFIRMING/UNCONFIRMED/CRIPPLED/OFFLINE/BROKEN/ERROR/UNKNOWN
	$output .= "-" . get_status_text($Current_Status);

	return $output
}


sub get_status_text
{
	my $status = shift;

	if ($status == NET_UNPROTECTED) { return "UNPROTECTED"; }
	elsif ($status == NET_PROTECTED) { return "PROTECTED"; }
	elsif ($status == NET_NEGATIVE) { return "NEGATIVE"; }
	elsif ($status == NET_CONFIRMING) { return "CONFIRMING"; }
	elsif ($status == NET_UNCONFIRMED) { return "UNCONFIRMED"; }
	elsif ($status == NET_CRIPPLED) { return "CRIPPLED"; }
	elsif ($status == NET_OFFLINE) { return "OFFLINE"; }
	elsif ($status == NET_BROKEN) { return "BROKEN"; }
	elsif ($status == NET_ERROR) { return "ERROR"; }

	return "UNKNOWN";
}


################	    Helper subroutines		################

sub print_usage_message
{
	print STDERR "Usage: vpn-monitor.pl [OPTION]\n" .
	   "  -c, --check      Start if VPN Monitor is not running\n" .
	   "  -d, --disable    Disable active monitoring\n" .
	   "  -e, --enable     Enable active monitoring\n" .
	   "  -f, --foreground Run as a foreground process\n" .
	   "  -h, --help       Print this message and exit\n" .
	   "  -s, --start      Same as --check\n" .
	   "  -k, --stop       Stop VPN Monitor service\n" .
	   "  -u, --uncripple  Remove network crippling and start daemon\n" .
	   "  -w, --watch      Periodically displays monitor information\n";
}


sub parse_command_line_arguments
{
	foreach (@ARGV) {
		if ( /^(-\?|-h|--help)$/ ) {
			$Skip_Cleanup = 1;
			print_usage_message();
			exit 0;
		} elsif ( /^(-c|--check|-s|--start)$/ ) {
			exit_if_not_root();
			$Skip_Cleanup = 1;
			exec('/usr/bin/perl ' . PATH . 'vpn-monitor/check_monitor.pl');
		} elsif ( /^(-d|--disable)$/ ) {
			$Skip_Cleanup = 1;
			send_command_using_netcat('disable-monitor');
			exit $?; 
		} elsif ( /^(-e|--enable)$/ ) {
			$Skip_Cleanup = 1;
			send_command_using_netcat('enable-monitor');
			exit $?; 
		} elsif ( /^(-f|--foreground)$/ ) {
			return 0;
		} elsif ( /^(-k|--stop)$/ ) {
			exit_if_not_root();
			$Skip_Cleanup = 1;
			if (system('/sbin/service monit status > /dev/null 2>&1') == 0) {
				print "\nInstructing monit to stop monitoring " . SERVICE_NAME . "\n";
				system('/usr/bin/monit unmonitor ' . SERVICE_NAME );
				print "\n";
			}
			print "Stopping service " . SERVICE_NAME . "\n";
			exec('/sbin/service ' . SERVICE_NAME . ' stop');
		} elsif ( /^(-u|--uncripple)$/ ) {
			# Process uncrippling in the main routine if user is root
			if (geteuid() == 0) {
				return 1;
			} else {
				$Skip_Cleanup = 1;
				send_command_using_netcat('undo-crippling');
				exit $?; 
			}
		} elsif ( /^(-w|--watch)$/ ) {
			$Skip_Cleanup = 1;
			exec('/bin/bash ' . PATH . 'vpn-monitor/watch_monitor.sh');
		}
	}

	# show usage and if no arguments selected
	$Skip_Cleanup = 1;
	print STDERR "Error: Unknown option\n";
	print_usage_message();
	exit 2;
}


sub send_command_using_netcat
{
	my $command = shift;
	
	unless ( -e PID_FILE ) {
		print "vpn-monitor is not running\nCommand ignored\n";
		exit 100;
	}

	print "Attempting " . $command . " by sending command to existing instance\n";
	my $uncripple_command = "/usr/bin/echo '" . $command . "' | /usr/bin/nc " . IPC_HOST . " " . IPC_PORT;
	if ( system($uncripple_command) eq 0 ) {
		print "Successfully sent " . $command . " command\n";
		return 0;
	} else {
		print "Command " . $command . " failed\n";
	}
	return 1;
}


sub send_uncripple_using_netcat
{
	$ctx->log(info => "Attempting uncrippling by sending command");
	$ctx->log(info => "\tfrom process " . $$ . " to running instance");
	print "\nAttempting uncrippling by sending command to existing instance\n";
	my $uncripple_command = "/usr/bin/echo 'undo-crippling' | /usr/bin/nc " . IPC_HOST . " " . IPC_PORT;
	if ( system($uncripple_command) eq 0 ) {
		$ctx->log(info => "Successfully sent uncrippling command");
		print "Successfully sent uncrippling command\n";
		return 0;
	} else {
		$ctx->log(info => "Uncrippling command failed");
		print "Uncrippling command failed\n";
	}
	return 1;
}


sub exit_if_not_root
{
	my $euid = geteuid();
	if ($euid != 0) {
		$Skip_Cleanup = 1;
		print STDERR "This program must be started and stopped as root.\n";
		exit(128);
	}
}


sub get_lock
{
	my $uncripple_option = shift;

	my $tmpfs_path = dirname(LOCK_FILE);
	unless ( -d $tmpfs_path ) {
		eval { make_path($tmpfs_path); };
	}
	# make sure directory is world readable, otherwise 'send_command_using_netcat'-subroutine 
	# fails to detect the running instance for non-root users
	eval { chmod(0755, $tmpfs_path); };

	unless (open $Lockfile_Handle, ">>", LOCK_FILE) {
		$Skip_Cleanup = 1;
		if ($uncripple_option) {
			my $return_code = send_uncripple_using_netcat();
			$ctx->log(info => "Exiting uncrippling process " . $$ . ".");
			exit $return_code;
		}
		$ctx->log(error => "Process " . $$ . " could not open lockfile " . LOCK_FILE . ": " . $!);
		$ctx->log(error => "$0 is already running. Exiting process " . $$ . ".");
		die "$0 is already running. Exiting.\n";
	}
	unless ( flock($Lockfile_Handle, LOCK_EX|LOCK_NB) ) {
		$Skip_Cleanup = 1;
		if ($uncripple_option) {
			my $return_code = send_uncripple_using_netcat();
			$ctx->log(info => "Exiting uncrippling process " . $$ . ".");
			exit $return_code;
		}
		$ctx->log(error => "Process " . $$ . " could not open exclusive lock: " . $!);
		$ctx->log(error => "$0 is already running. Exiting process " . $$ . ".");
		die "$0 is already running. Exiting.\n";
	}
}


sub update_status_file
{
	my $status = shift;

	open my $sf, ">", STATUS_FILE or return;
	print $sf $status;
	close $sf;
}


sub get_previous_status_from_file
{
	open my $sf, "<", STATUS_FILE or return NET_UNKNOWN;
	my @lines = <$sf>;
	close $sf;
	return $lines[0];
}


sub take_a_break
{
	$Temporary_Disable = 1; # disable crippling
	$Current_Task = "temporary";

	# kill vpn_retry instance if running
	system("/bin/pkill -9 vpn_retry");

	# destroy timer / re-enable crippling after 1 minute
	undef $Temporary_Disable_Timer; 
	$Temporary_Disable_Timer = AnyEvent->timer(
		after => 60, 
		cb => sub {
			$Temporary_Disable = 0;
			$Current_Task = "idle";
			$ctx->log(debug => "Temporary disable crippling ended") if DEBUG > 0;
		},
	);
	$ctx->log(debug => "Take-a-break requested, Temporary disable crippling") if DEBUG > 0;
}


sub set_current_task_to_idle
{
	my $override_temporary_disable = shift;

	# don't change task if monitor is retrying the VPN
	if ($Current_Task eq "retrying") {
		return;
	}

	# set task to "temporary" if Temporary_Disable_Timer still running and override is not requested
	if ($override_temporary_disable) {
		$Current_Task = "idle";
	} elsif ($Temporary_Disable) {
		$Current_Task = "temporary";
	} else {
		$Current_Task = "idle";
	}
	return;
}


sub change_active_monitoring
{
	my $enable_monitor = shift;

	# disable temporary disable
	$Temporary_Disable = 0;

	# kill vpn_retry instance if running
	system("/bin/pkill -9 vpn_retry");

	if ($enable_monitor) {
		# restart timers
		undef $Detect_Change_Timer;
		$Detect_Change_Timer = AnyEvent->timer(
			after => 40,
			interval => DETECT_CHANGE_INTERVAL,
			cb => \&detect_change,
		);
		undef $Api_Check_Timer;
		$Api_Check_Timer = AnyEvent->timer(
			after => 40 - API_CHECK_TIMEOUT,
			cb => \&get_api_status,
		);

		$ctx->log(info => "Active monitoring enabled, first check after 40 seconds");
	} else {
		undef $Detect_Change_Timer;
		undef $Api_Check_Timer;
		
		$ctx->log(info => "Active monitoring disabled");
	}

	# read and update ini
	my $vpn_ini;
	unless (open $vpn_ini, "<" . INI_FILE) {
		my $error = $!;
		if ( -e INI_FILE ) {
			$ctx->log(error => "Could not open " . INI_FILE . " for reading.  Reason: " . $error);
		} else {
			$ctx->log(info => "Ini file " . INI_FILE . " missing.");
			$ctx->log(info => "   Fire up the GUI and press Server to download server list and create ini file.");
		}
		$ctx->log(info => "Active monitoring state change is not persistent because ini file parsing failed.");
		return 1;
	}
	my @vpn_ini_lines = <$vpn_ini>;
	close $vpn_ini;

	unless (open VPN_INI, ">" . INI_FILE) {
		$ctx->log(error => "Could not open " . INI_FILE . " for writing.  Reason: " . $!);
		$ctx->log(info => "Active monitoring state change is not persistent because ini file writing failed.");
		return 1;
	}

	my $has_been_written = 0;
	foreach my $line (@vpn_ini_lines) {
		if ($line =~ /monitor/) {
			if ($enable_monitor) {
				print VPN_INI "monitor=enabled\n";
			} else {
				print VPN_INI "monitor=disabled\n";
			}
			$has_been_written = 1;
		} else {
			print VPN_INI $line;
		}
	}
	if ($has_been_written == 0) {
		if ($enable_monitor) {
			print VPN_INI "monitor=enabled\n";
		} else {
			print VPN_INI "monitor=disabled\n";
		}
	}
	close VPN_INI;

	return 0;
}


sub remove_route
# returns undef in case of error,
# negative number if bad routes still exist,
# positive number if bad routes were removed,
# zero if there were no bad routes.
{
	open(my $fh, "/proc/net/route") or do {
		$ctx->log(error => "Cannot open /proc/net/route: $!");
		return -1;
	};
	chomp(my $firstline = <$fh>);
	my @headers = split /\s+/, $firstline;

	my @badroutes;
	while (<$fh>) {
		chomp;
		next if /^\s*$/;
		my @values = split /\s+/, $_;
		my %line;
		@line{@headers} = @values;

		if ($line{Iface} !~ /^tun\d+/ && $line{Gateway} eq '00000000' && $line{Mask} eq 'FFFFFFFF') {
			push @badroutes, \%line;
		}
	}
	close $fh;

	if (!scalar @badroutes) {
		return 0;
	}

	my $error = 0;
	foreach my $badroute (@badroutes) {
		my $host = join('.', reverse map { hex($_); } ($badroute->{Destination} =~ /([0-9A-F][0-9A-F])/gi));
		my $gw = join('.', reverse map { hex($_); } ($badroute->{Gateway} =~ /([0-9A-F][0-9A-F])/gi));
		my $metric = $badroute->{Metric};
		my $iface = $badroute->{Iface};
		my $cmd = "route del -host '$host' gw '$gw' metric $metric dev $iface";
		my $rc = system($cmd);
		if ($rc != 0) {
			$ctx->log(error => "Error running command: $cmd. Return code: $rc");
			$error = 1;
		}
	}

	if ($error) {
		return undef;
	}

	# there should not be any bad routes here now - let's check
	open(my $gh, "/proc/net/route") or do {
		$ctx->log(error => "Cannot open /proc/net/route: $!");
		return -1;
	};
	<$gh>; # skip first line

	my @badroutes_left;
	while (<$gh>) {
		chomp;
		next if /^\s*$/;
		my @values = split /\s+/, $_;
		my %line;
		@line{@headers} = @values;

		if ($line{Iface} !~ /^tun\d+/ && $line{Gateway} eq '00000000' && $line{Mask} eq 'FFFFFFFF') {
			push @badroutes_left, \%line;
		}
	}
	close $gh;

	if (scalar @badroutes_left) {
		return -(scalar @badroutes_left);
	}

	return scalar(@badroutes);
}


sub write_dispatcher 
{
	my ($uuid, $error) = read_ini_file('uuid');
	if ( ($error) || ($uuid eq '') ) {
		$ctx->log(info => "Dispatch file was not written because ini file parsing failed.");
		return 1;
	}

	# write dispatcher file with "up|vpn-down" case
	my $dfh;
	unless (open $dfh, ">", DISPATCH_FILE) {
		$ctx->log(error => "Could not open " . DISPATCH_FILE . " for writing.  Reason: " . $!);
		return 2;
	}
	print $dfh "#!/bin/sh\n";
	print $dfh "ESSID=\"$uuid\"\n\n";
	print $dfh "interface=\$1 status=\$2\n";
	print $dfh "case \$status in\n";
	print $dfh "  up|vpn-down)\n";
	print $dfh "	sleep 3 && /usr/bin/nmcli con up uuid \"\$ESSID\" &\n";
	print $dfh "	;;\n";
	print $dfh "esac\n";
	close $dfh;
	$ctx->log(debug => "Dispatch file written") if DEBUG > 0;

	return 0;
}


sub read_ini_file
{
	# Parse ini file for the following keys: uuid, remote, url or monitor
	my $key = shift;
	unless ( defined $key ) { return ('', 1); };

	my $value;
	my $error = 0;
	my $vpn_ini;
	if (open $vpn_ini, "<" . INI_FILE) {
		if ($key eq "uuid") {
			while (<$vpn_ini>) {
				if (/^\s*uuid\s*=\s*([-0-9a-fA-F]+)/) {
					$value = $1;
					last;
				}
			}
		} elsif ($key eq "remote") {
			while (<$vpn_ini>) {
				if (/^\s*remote\s*=\s*([1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*\.[1-9][0-9]*)/) {
					$value = $1;
					last;
				}
			}
		} elsif ($key eq 'url') {
			while (<$vpn_ini>) {
				if (/^\s*url\s*=\s*(.*)/) {
					$value = $1;
					last;
				}
			}
		} elsif ($key eq 'monitor') {
			while (<$vpn_ini>) {
				if (/^\s*monitor=\s*([a-zA-Z]+)/) {
					$value = $1;
					last;
				}
			}
		}
		close $vpn_ini;
	} else {
		$error = $!;
		if ( -e INI_FILE ) {
			$ctx->log(error => "Could not open " . INI_FILE . " for reading.  Reason: " . $error);
		} else {
			$ctx->log(info => "Ini file " . INI_FILE . " missing.");
			$ctx->log(info => "   Fire up the GUI and press Server to download server list and create ini file.");
		}
	}

	# always return error if key-value pair not found
	unless ( defined $value ) { 
		$value = '';
		unless ($error) { $error = 1; };
	}

	return ($value, $error);
}


sub read_api_url_from_ini_file
{
	my ($url, $error) = read_ini_file('url');

	if ($error) {
		$ctx->log(info => "   Disabling API check.");
		$Url_For_Api_Check = 'none';
		return 2;
	} elsif ($url eq 'none') {
		$ctx->log(info => "No URL entry found in " . INI_FILE);
		$ctx->log(info => "   Disabling API check.");
		$Url_For_Api_Check = 'none';
		return 1;
	} elsif ($url eq '') {
		$ctx->log(info => "URL entry empty in " . INI_FILE);
		$ctx->log(info => "   Disabling API check.");
		$Url_For_Api_Check = 'none';
		return 1;
	} elsif ($url =~ /http\:.*/) {
		$ctx->log(info => "Using API check URL $url");
		$Url_For_Api_Check = $url;
		return 0;
	} else {
		$ctx->log(error => "Error adding API check URL $url");
		$ctx->log(error => "   URL must start with \"http:\"   Disabling API check.");
		$Url_For_Api_Check = 'none';
		return 2;
	}
	return 2;
}


sub popup_dialog
{
	my $status_to_display = shift;
	my $msg;

	$ctx->log(debug => "Should display popup right about now ($status_to_display vs $Previous_Status)") if DEBUG > 1;
	if ($status_to_display == NET_UNPROTECTED) {
		$msg = 'VPN connection is DOWN';
	} elsif ($status_to_display == NET_PROTECTED) {
		$msg = 'VPN connection is UP!';
	} elsif ($status_to_display == NET_NEGATIVE) {
		$msg = 'VPN connection is up, but not in use';
	} elsif ($status_to_display == NET_CONFIRMING) {
		$msg = 'VPN connection status is being confirmed';
	} elsif ($status_to_display == NET_UNCONFIRMED) {
		$msg = 'VPN connection status is UNCONFIRMED';
	} elsif ($status_to_display == NET_CRIPPLED) {
		$msg = 'Unable to start VPN. Network put into safe mode';
	} elsif ($status_to_display == NET_OFFLINE) {
		$msg = 'Network is OFFLINE';
	} elsif ($status_to_display == NET_BROKEN) {
		$msg = 'Network is BROKEN';
	} else {
		$msg = 'Network is in an unknown status (' . $status_to_display . ')';
	}
	$ctx->log(debug => "Popup: " . $msg ) if DEBUG > 0;

	try {
		# find user with terminal :0
		my ($line, $username);
		open(WHO, "who -s |");
		while ($line = <WHO>) {
			if ($line =~ /^(\S+)\s+:0\s+.*/) {
				$username = $1;
				last;
			}
		}
		close(WHO);
		# if who parse fails, use username with ID 1000
		if ( !defined($username) ) {
			$username = getpwuid(1000);
			$ctx->log(debug => "Who parse failed. using user ($username) ID 1000." ) if DEBUG > 0;
		}

		# check is xhost already allows non-network local connections 
		my $remove_access = 0;
		if (system("su -l " . $username . " -c \"DISPLAY=:0 xhost \" | grep -i LOCAL >/dev/null")) {
			# add non-network local connections to X display access control list
			system("su -l " . $username . " -c \"DISPLAY=:0 xhost +local:\" >/dev/null");
			$remove_access = 1;
			$ctx->log(debug => "Non-network local connections added to X display access control list" ) if DEBUG > 0;
		}

		my $cmd=("kdialog --display :0 --title \"PrivateOn-VPN\" --passivepopup \"" . $msg . "\" 120 &");
		$ctx->log(debug => '<' . $cmd . '>') if DEBUG > 1;
		system($cmd);

		# undo xhost exception
		if ($remove_access) {
			system("su -l " . $username . " -c \"DISPLAY=:0 xhost -local:\" >/dev/null");
			$ctx->log(debug => "Non-network local connections removed from X display access control list" ) if DEBUG > 0;
		}
	} catch {
		$ctx->log(error => "Popup routine failed. Cause = $_" );
	};

	return;
}


sub fake_systemv_logger
{
	# do nothing if logger program already running
	if (`ps -ef | grep journalctl | grep -v grep | grep NetworkManager | wc -l` > 0) {
		return 0;
	}

	# check if system has systemd journal logging
	if ( system('pidof systemd-journald >/dev/null 2>&1') eq 0 ) {
		$ctx->log(debug => "Starting vpn_logger.sh background process" ) if DEBUG > 0;
		system( PATH . "vpn-monitor/vpn_logger.sh &");
		return 0;
	}

	return 1;
}


sub stop_systemv_logger
{
	# do nothing if system doesn't have systemd journal logging
	if ( system('pidof systemd-journald >/dev/null 2>&1') eq 0 ) {
		return 0;
	}

	$ctx->log(debug => "Stopping vpn_logger.sh background process" ) if DEBUG > 0;

	if (`ps -ef | grep vpn_logger.sh | grep -v grep | wc -l` > 0) {
		system("/bin/pkill -9 vpn_logger.sh &");
	}

	my @pid;
	@pid = `ps -ef | grep journalctl | grep -v grep | grep NetworkManager | awk '{print \$2}'`;
	kill 'KILL', @pid;

	# remove file since it is not updated anymore
	system( "/usr/bin/rm -f /var/log/NetworkManager");
}


################	   Cripple subroutines		################

sub retry_and_redirect
{
	# retry and redirect depending on case specific user settings 
	if ( $Current_Status == NET_UNPROTECTED && 
	   (RETRY_WHEN_UNPROTECTED eq 'TRUE' || RETRY_WHEN_UNPROTECTED eq '1') ) {
		spawn_retry_vpn();
		return;
	} elsif ( $Current_Status == NET_NEGATIVE && 
	   (RETRY_WHEN_NEGATIVE eq 'TRUE' || RETRY_WHEN_NEGATIVE eq '1') ) {
		spawn_retry_vpn();
		return;
	} elsif ( ($Current_Status == NET_CONFIRMING || $Current_Status == NET_UNCONFIRMED) && 
	   (RETRY_WHEN_UNCONFIRMED eq 'TRUE' || RETRY_WHEN_UNCONFIRMED eq '1') ) {
		spawn_retry_vpn();
		return;
	}

	# if retry flag is set, spawn_retry_vpn calls redirect_page when it finishes
	# otherwise crippling is processed here
	if ( $Current_Status == NET_UNPROTECTED && 
	   (CRIPPLE_WHEN_UNPROTECTED eq 'TRUE' || CRIPPLE_WHEN_UNPROTECTED eq '1') ) {
		redirect_page();
	} elsif ( $Current_Status == NET_NEGATIVE && 
	   (CRIPPLE_WHEN_NEGATIVE eq 'TRUE' || CRIPPLE_WHEN_NEGATIVE eq '1') ) {
		redirect_page();
	} elsif ( ($Current_Status == NET_CONFIRMING || $Current_Status == NET_UNCONFIRMED) && 
	   (CRIPPLE_WHEN_UNCONFIRMED eq 'TRUE' || CRIPPLE_WHEN_UNCONFIRMED eq '1') ) {
		redirect_page();
	}
}


sub add_route_to_vpn_server
{
	# get local gateway and active NIC before removing routes 
	my ($local_gateway_ip, $active_nic) = get_local_gateway_and_nic();
	unless ( defined $local_gateway_ip && defined $active_nic ) { return 1; };

	# read vpn server ip
	my ($vpn_server_ip, $error) = read_ini_file('remote');
	if ( ($error) || ($vpn_server_ip eq '') ) {
		$ctx->log(error => "Failed to add route because ini file parsing failed.");
		return 1;
	}

	# remove old routes to vpn server
	my $status = 0;
	my $iteration = 0; # prevent deadlock in case of route error
	while ( !$status && $iteration < 5 ) {
		system("/sbin/route del " . $vpn_server_ip . " 2>/dev/null");
		$status = $? >> 8; # $? >> 8 is the exit status, see perldoc -f system
		$iteration++;
	}

	# add route to vpn server so we can retry the vpn without leaving crippled state 
	system("/sbin/route add " . $vpn_server_ip . " gw " . $local_gateway_ip . " dev " . $active_nic . " 2>/dev/null");
	$status = $? >> 8; # $? >> 8 is the exit status, see perldoc -f system
	if ($status) {
		$ctx->log(error => "Failed to add route, error($status): dest=$vpn_server_ip gw=$local_gateway_ip dev=$active_nic");
	}

	return $status;
}


sub redirect_page
{
	# redirect web traffic to a static page
	$ctx->log(warn => "Redirecting all web traffic to warning page" );

	add_route_to_vpn_server();
	
	# delete all default routes
	my $status = 0;
	my $iteration = 0; # prevent deadlock in case of route error
	while ( !$status && $iteration < 5 ) {
		system("/sbin/route del default 2>/dev/null");
		$status = $? >> 8; # $? >> 8 is the exit status, see perldoc -f system
		$iteration++;
	}

	# set default route to localhost
	system("/sbin/route add default gw 127.0.0.1 lo");
	$status = $? >> 8;

	# if the command above succeeded then...
	if (!$status) {
		# start dnsmasq
		system("dnsmasq --address=/#/127.0.0.1 --listen-address=127.0.0.1 --bind-interfaces");

		system("/usr/bin/cp /etc/resolv.conf /etc/resolv.conf.bak");
		# overwrite resolv.conf
		if (open my $fh, ">", "/etc/resolv.conf") {
			print $fh "nameserver 127.0.0.1";
			close $fh;
		} else {
			$ctx->log(error => "Could not open /etc/resolv.conf for writing.  Reason: " . $!);
		}

		# start web server that listens to localhost
		system("thttpd -r -h localhost -d " . PATH . "vpn-monitor/htdocs");
	} else {
		return $status;
	}

	$Current_Task = "crippled";
	$ctx->log(debug => "redirect_page successfull" ) if DEBUG > 0;
	popup_dialog(NET_CRIPPLED);
}


################	    Undo Crippling fork		################

sub spawn_undo_crippling
{
	$Current_Task = "uncrippling";
	$ctx->log(info => "Undoing all network crippling");
	$ctx->log(debug => "Spawning undo_crippling") if DEBUG > 0;

	# kill previous instance or vpn_retry if still running
	system("/bin/pkill -9 vpn_uncripple");
	system("/bin/pkill -9 vpn_retry");

	# vpn_uncripple requires a NetworkManager log file
	fake_systemv_logger();

	my $rpc = AnyEvent::Fork
		->new     
		->require ("AnyEvent::Fork::RPC::Async","vpn_uncripple")
		->AnyEvent::Fork::RPC::run ("vpn_uncripple::run",
			async      => 1,
			on_error   =>  \&undo_crippling_on_error,
			on_event   => sub { 
					$ctx->log(debug => "undo_crippling sent event $_[0]") if DEBUG > 0; 
				},
			on_destroy => sub { 
					$ctx->log(debug => "undo_crippling child process destoyed") if DEBUG > 0; 
				},
		);

	$rpc->( \&undo_crippling_callback);
}


sub undo_crippling_callback
{
	set_current_task_to_idle(0);
	$Current_Status = quick_net_status();
	update_status_file($Current_Status);
	popup_dialog($Current_Status);
	return 0;
}


sub undo_crippling_on_error
{
	set_current_task_to_idle(0);
	my $msg = shift;
	$ctx->log(error => "undo_crippling child process died unexpectedly: " . $msg);
	$Current_Status = quick_net_status();
	if ($Current_Status == NET_PROTECTED || $Current_Status == NET_UNPROTECTED ||
	   $Current_Status == NET_UNCONFIRMED || $Current_Status == NET_CONFIRMING) {
		return 0;
	} else {
		system("/usr/bin/rm -f /etc/resolv.conf");
		system("/sbin/rcnetwork restart");
	}
	return 0;
}


sub check_crippled
{
	# Returns true if crippling is on

	# Process check
	my $pslist = qx!/usr/bin/ps -aef!;
	my @pslist = split("\n", $pslist);
	my $line;
	my $thttpd_pid  = undef;
	my $dnsmasq_pid = undef;
	while ( defined($line = shift(@pslist))) {
		if ($line =~ /^[^\d]+\s*(\d+).*thttpd/) {
			$thttpd_pid = $1;
		} elsif ($line =~ /^[^\d]+\s*(\d+).*dnsmasq/) {
			$dnsmasq_pid = $1;
		}
	}
	return $thttpd_pid  if (defined($thttpd_pid ));
	return $dnsmasq_pid if (defined($dnsmasq_pid));

	# Route check
	unless (open ROUTE, '<', '/proc/net/route') {
		$ctx->log(error => "Could not open /proc/net/route for reading.  Reason: " . $!);
	}
	while (<ROUTE>) {
		if ( (/^lo\s+00000000\s+/) || (/^\S+\s+00000000\s+0100007F\s+/i) ) {
			close ROUTE;
			return "Default route";
		}
	}
	close ROUTE;

	# Nameserver check
	my $nameservers = qx!/usr/bin/grep nameserver /etc/resolv.conf!;
	my @nameservers = split("\n", $nameservers);
	my @resolvers = ();
	my $resolver;
	while (defined($line = shift(@nameservers))) {
		if ($line =~ /^nameserver\s*(\d+\.\d+\.\d+\.\d+)/) {
			$resolver = $1;
			push @resolvers, $resolver;
			return "Localhost as DNS" if $resolver eq '127.0.0.1';
		}
	}
	
	# if not crippled, change task to idle or temporary
	if ( $Current_Task eq "crippled" || $Current_Task eq "uncrippling" ) { 
		set_current_task_to_idle(0);
	}

	return 0;
}


################	     VPN Retry fork		################

sub spawn_retry_vpn
{
	$Current_Task = "retrying";
	$ctx->log(debug => "Spawning retry_vpn") if DEBUG > 0;

	# kill previous instance if still running
	system("/bin/pkill -9 vpn_retry");

	# vpn_retry requires a NetworkManager log file
	fake_systemv_logger();

	my $rpc = AnyEvent::Fork
		->new     
		->require ("AnyEvent::Fork::RPC::Async","vpn_retry")
		->AnyEvent::Fork::RPC::run ("vpn_retry::run",
			async      => 1,
			on_error   =>  \&retry_vpn_on_error,
			on_event   => sub { 
					$ctx->log(debug => "Retry_vpn sent event $_[0]") if DEBUG > 0; 
				},
			on_destroy => sub { 
					$ctx->log(debug => "Retry_vpn child process destoyed") if DEBUG > 0; 
				},
		);

	$rpc->( \&retry_vpn_callback);
}


sub retry_vpn_callback
{
	my $result = shift;
	if ($result eq 0) {
		$ctx->log(info => "VPN activation successful");
	} else {
		$ctx->log(info => "VPN activation failed");
		$ctx->log(debug => "Retry_vpn child process returned error code: " .  $result) if DEBUG > 0;
	}

	set_current_task_to_idle(0);

	$Current_Status = quick_net_status();
	$ctx->log(debug => "\tcurrent_status = " . get_status_text($Current_Status) ) if DEBUG > 1;
	if ( $Current_Status == NET_UNPROTECTED && 
	   (CRIPPLE_WHEN_UNPROTECTED eq 'TRUE' || CRIPPLE_WHEN_UNPROTECTED eq '1') ) {
		redirect_page();
	} elsif ( $Current_Status == NET_NEGATIVE && 
	   (CRIPPLE_WHEN_NEGATIVE eq 'TRUE' || CRIPPLE_WHEN_NEGATIVE eq '1') ) {
		redirect_page();
	} elsif ( ($Current_Status == NET_CONFIRMING || $Current_Status == NET_UNCONFIRMED) && 
	   (CRIPPLE_WHEN_UNCONFIRMED eq 'TRUE' || CRIPPLE_WHEN_UNCONFIRMED eq '1') ) {
		redirect_page();
	}
	return 0;
}


sub retry_vpn_on_error
{
	set_current_task_to_idle(0);
	my $msg = shift;
	$ctx->log(error => "Retry_vpn child process died unexpectedly: " . $msg);

	$Current_Status = quick_net_status();
	if ($Current_Status == NET_PROTECTED) {
		return 0;
	} elsif ( $Current_Status == NET_OFFLINE || $Current_Status == NET_BROKEN ||
	   $Current_Status == NET_ERROR || $Current_Status == NET_UNKNOWN ) {
		system("/usr/bin/rm -f /etc/resolv.conf");
		system("/sbin/rcnetwork restart");
		return 0;
	}

	retry_vpn_callback();

	return 0;
}


################     Detect Change in Network State	################

sub detect_change
{
	if ( !defined($Monitor_Enabled) || $Monitor_Enabled == 0 || $Temporary_Disable == 1) {
		return;
	}

	$ctx->log(debug => "Refreshing network status") if DEBUG > 0;

	$Current_Status = quick_net_status();
	$ctx->log(debug => "\tprevious_status = " . get_status_text($Previous_Status) . " current_status = " . get_status_text($Current_Status) ) if DEBUG > 1;

	my $tmp_previous = $Previous_Status;
	$Previous_Status = $Current_Status;

	# Start timer for http-request so that we receive a reply before the next detect_change 
	undef $Api_Check_Timer;
	$Api_Check_Timer = AnyEvent->timer(
		after => DETECT_CHANGE_INTERVAL - API_CHECK_TIMEOUT,
		cb => \&get_api_status,
	);

	# do not retry/redirect if previous state was CRIPPLED, redirect on next iteration
	if ($tmp_previous != NET_CRIPPLED) {
		if ($Current_Status == NET_UNPROTECTED || $Current_Status == NET_NEGATIVE ||
		   $Current_Status == NET_CONFIRMING || $Current_Status == NET_UNCONFIRMED) {
			retry_and_redirect();
			return(0);
		}
	}

	# update Current_Task in case callbacks failed to be called
	if ($Current_Task ne "idle") {
		if ($Current_Task eq "uncrippling" && $Current_Status != NET_CRIPPLED) { 
			unless ( check_crippled() ) { $Current_Task = "idle"; };
		} elsif ($Current_Task eq "retrying") {
			if (`ps -ef | grep vpn_retry | grep -v grep | grep root | wc -l` == 0) { $Current_Task = "idle"; };
		}
	}

	if ($Current_Status eq $tmp_previous) {
		return(0);
	} else {
		$ctx->log(info => "\tState changed from " . get_status_text($tmp_previous) . " to " . get_status_text($Current_Status) );
		update_status_file($Current_Status);
		popup_dialog($Current_Status) if DEBUG > 2;
	}
}


################	  Initialize subroutine		################

sub run_once
{
	my $uncripple_option = parse_command_line_arguments();
	exit_if_not_root();

	$ctx = new AnyEvent::Log::Ctx;
	AnyEvent::Log::exact_time 0;
	$ctx->log_to_file(LOG_FILE);
	$ctx->log(info => "PrivateOn VPN-monitor daemon ".VERSION." starting up.");

	# make sure there is only one instance running
	get_lock($uncripple_option);

	# write pid if stale or missing
	if ( -e PID_FILE ) {
		# pf_check returns empty string if pid is OK
		my $action = 0;
		eval { $action = pf_check( PID_FILE ); };
		$ctx->log(debug => "PID check: " . $@) if ($@ && DEBUG > 1);
		if (length $action) {
			$ctx->log( info => "Removing stale PID file " . PID_FILE );
			system( "/usr/bin/rm -f " . PID_FILE );
			pf_set( PID_FILE );
		}
	} else {
		pf_set( PID_FILE );
	}

	# read API check URL from inifile to global variable
	read_api_url_from_ini_file();

	# Enable monitor if this is the first time this software has been run since reboot
	# Since the status file is written to a tmpfs, if it is missing, this is the first run
	unless ( -e STATUS_FILE ) {
		unless ( -e INI_FILE ) {
			$Monitor_Enabled = 0;
			$ctx->log(info => "Ini file " . INI_FILE . " missing.");
			$ctx->log(info => "   Fire up the GUI and press Server to download server list and create ini file.");
			$ctx->log(info => "Active monitoring disabled because ini file parsing failed.");
			if ($uncripple_option) { spawn_undo_crippling(); };
			return 1;
		}

		my $vpn_ini;
		unless (open $vpn_ini, "<" . INI_FILE) {
			$Monitor_Enabled = 0;
			$ctx->log(error => "Could not open " . INI_FILE . " for reading.  Reason: " . $!);
			$ctx->log(info => "Active monitoring disabled because ini file parsing failed.");
			if ($uncripple_option) { spawn_undo_crippling(); };
			return 1;
		}
		my @vpn_ini_lines = <$vpn_ini>;
		close $vpn_ini;

		unless (open VPN_INI, ">" . INI_FILE) {
			$Monitor_Enabled = 0;
			$ctx->log(error => "Could not open " . INI_FILE . " for writing.  Reason: " . $!);
			$ctx->log(info => "Active monitoring disabled because ini file writing failed.");
			if ($uncripple_option) { spawn_undo_crippling(); };
			return 1;
		}
		my $has_been_written = 0;
		foreach my $line (@vpn_ini_lines) {
			if ($line =~ /monitor/) {
				print VPN_INI "monitor=enabled\n";
				$has_been_written = 1;
			} else {
				print VPN_INI $line;
			}
		}
		if ($has_been_written == 0) {
			print VPN_INI "monitor=enabled\n";
		}
		close VPN_INI;

	} else { # status file exists
		$Previous_Status = get_previous_status_from_file();

		my ($monitor, $error) = read_ini_file('monitor');
		if ( ($error) || ($monitor eq '') ) {
			$Monitor_Enabled = 0;
			$ctx->log(info => "Active monitoring disabled because ini file parsing failed.");
			if ($uncripple_option) { spawn_undo_crippling(); };
			return 1;
		}

		if ($monitor =~ /disabled/) {
			$Monitor_Enabled = 0;
			$ctx->log(info => "Restoring active monitoring to disabled state.");
			if ($uncripple_option) { spawn_undo_crippling(); };
			return 1;
		}
	}

	# write uuid from ini file to dispatcher file, if uuid is missing don't enable monitoring
	if ( write_dispatcher() == 1 ) {
		$Monitor_Enabled = 0;
		change_active_monitoring(0);
		$ctx->log(info => "\tbecause ini file missing 'uuid'-key.");
		if ($uncripple_option) { spawn_undo_crippling(); };
		return 1;
	}

	$Monitor_Enabled = 1;
	$ctx->log(info => "Active monitoring enabled.");

	# Start periodic network status checking
	$Detect_Change_Timer = AnyEvent->timer(
		after => DETECT_CHANGE_INTERVAL,
		interval => DETECT_CHANGE_INTERVAL,
		cb => \&detect_change,
	);

	# One-shot API check status
	$Api_Check_Timer = AnyEvent->timer(
		after => DETECT_CHANGE_INTERVAL - API_CHECK_TIMEOUT,
		cb => \&get_api_status,
	);

	if ($uncripple_option) { spawn_undo_crippling(); };
}


# run initialization code
run_once();


################		TCP server		################

tcp_server(
	IPC_HOST, IPC_PORT, sub {
	my ($fh) = @_;

	$TCP_Server_Handle = AnyEvent::Handle->new(
		fh => $fh,
		poll => 'r',
		on_read => sub {
			my ($self) = @_;
			my $buf = $self->{rbuf};
			chomp($buf);
			$self->rbuf = ""; # clear buffer

			print "Received: " . $buf . "\n" if DEBUG > 2;

			if ($buf eq "force-refresh") {
				# force get_monitor_state to update Current_Status by time traveling to the age of disco
				$Current_Update_Time = 0;
				# force get-api-status to send new http request
				$HTTP_Request_Time = 0;
				$self->push_write("ok - forcing refresh on next get_monitor_state\n");

			} elsif ($buf eq "get-api-status") {
				# don't make new http request before previous has had time to arrive
				if ($HTTP_Request_Time + API_CHECK_TIMEOUT < time()) {
					$self->push_write(get_api_status() . "\n");
				} else {
					$Current_Status = quick_net_status();
					$self->push_write($Current_Status . "\n");
				}

			} elsif ($buf eq "get-net-status") {
				$Current_Status = quick_net_status();
				$self->push_write($Current_Status . "\n");

			} elsif ($buf eq "reread-config") {
				my $result = read_api_url_from_ini_file();
				$self->push_write($result == 0 ? "ok\n" : "not ok - returned $result\n");
			} elsif ($buf eq "write-dispatcher") {
				if (write_dispatcher() == 0) {
					$self->push_write("ok - dispatch file written\n");
				} else {
					$self->push_write("not ok - see error log\n");
				}

			} elsif ($buf eq "remove-dispatcher") {
				unlink(DISPATCH_FILE);
				$self->push_write("ok - dispatch file unlinked\n");
				$ctx->log(debug => "Dispatch file unlinked") if DEBUG > 0;

			} elsif ($buf eq "check-crippling") {
				$self->push_write(check_crippled());

			} elsif ($buf eq "undo-crippling") {
				spawn_undo_crippling();
				$self->push_write("ok - called spawn_undo_crippling()\n");

			} elsif ($buf eq "take-a-break") {
				take_a_break();
				$self->push_write("ok - monitoring disabled for 1 minute\n");

			} elsif ($buf eq "resume-idling") {
				set_current_task_to_idle(1);
				$self->push_write("ok - task changed to idle\n");

			} elsif ($buf eq "enable-monitor") {
				$Monitor_Enabled = 1;
				if (change_active_monitoring($Monitor_Enabled) == 0) {
					$self->push_write("ok - monitor enabled\n");
				} else {
					$self->push_write("not ok - see error log\n");
				}

			} elsif ($buf eq "disable-monitor") {
				$Monitor_Enabled = 0;
				if (change_active_monitoring($Monitor_Enabled) == 0) {
					$self->push_write("ok - monitor disabled\n");
				} else {
					$self->push_write("not ok - see error log\n");
				}

			} elsif ($buf eq "monitor-state") {
				$self->push_write(get_monitor_state() . "\n");

			} elsif ($buf eq "remove-route") {
				my $retval = remove_route();
				if (!defined $retval) {
					$self->push_write("not ok - see error log\n");
				} elsif ($retval < 0) {
					$self->push_write("not ok - " . (-$retval) . " bad route(s) still exist(s)\n");
				} elsif ($retval > 0) {
					$self->push_write("ok - removed $retval bad route(s)\n");
				} else {
					$self->push_write("ok - no bad routes detected\n");
				}
			} else {
				$self->push_write("say what?\n");
				$ctx->log(debug => "Unrecognized command: " . $buf) if DEBUG > 0;
			}
			$self->push_shutdown;
		},
		on_eof => sub {
			my ($hdl) = @_;
			$hdl->destroy();
		},
		on_error => sub {
			my ($hdl, $fatal, $msg) = @_;
			$ctx->log(error => "tcp_server error :" . $msg);
			$hdl->destroy;
		}
	);
	$TCP_Server_Connections{$TCP_Server_Handle} = $TCP_Server_Handle; # keep it alive.
	return;
	}
);

$ctx->log(info => "Daemon is listening on " . IPC_HOST . ":" . IPC_PORT);


################		Main Loop		################

$CV->recv;


################		Clean up		################

END {
	unless ($Skip_Cleanup) {
		$ctx->log(debug => "PrivateOn VPN-monitor daemon shutting down.") if DEBUG > 0;
		stop_systemv_logger();
		# remove pid file
		pf_unset( PID_FILE );
		$ctx->log(info => "PrivateOn VPN-monitor daemon stopped.");
		# print daemon stopped to systemd journal or foreground
		print("\nPrivateOn vpn-monitor daemon stopped.\n");
	}
}
