#!/usr/bin/perl
#
# Module: dmvpn-config.pl
#

use strict;
use lib "/opt/vyatta/share/perl5";

use constant IKELIFETIME_DEFAULT => 28800;    # 8 hours
use constant ESPLIFETIME_DEFAULT => 3600;     # 1 hour
use constant REKEYMARGIN_DEFAULT => 540;      # 9 minutes
use constant REKEYFUZZ_DEFAULT   => 100;
use constant INVALID_LOCAL_IP    => 254;
use constant VPN_MAX_PROPOSALS   => 10;

use Vyatta::TypeChecker;
use Vyatta::VPN::Util;
use Getopt::Long;
use Vyatta::Misc;
use NetAddr::IP;
use Vyatta::VPN::vtiIntf;

my $config_file;
my $init_script;
my $tunnel_context;
my $tun_id;
GetOptions(
    "config_file=s"  => \$config_file,
    "init_script=s"  => \$init_script,
    "tunnel_context" => \$tunnel_context,
    "tun_id=s"       => \$tun_id
);

my $LOGFILE          = '/var/log/vyatta/ipsec.log';

my $vpn_cfg_err   = "VPN configuration error:";
my $genout;
my $dh_disable;

$genout .= "# generated by $0\n\n";

#
# Prepare Vyatta::Config object
#
use Vyatta::Config;
my $vc    = new Vyatta::Config();
my $vcVPN = new Vyatta::Config();
$vcVPN->setLevel('vpn');

# check to see if the config has changed.
# if it has not then exit
my $ipsecstatus = $vcVPN->isChanged('ipsec');
if ( $ipsecstatus && $tunnel_context ) {
	# no sence to do same update twice, will be done via vpn context
	exit 0;
}
if ( !$ipsecstatus ) {
	my $tun_ip_changed = 0;
	my @tuns           = $vc->listNodes('interfaces tunnel');
	my @profs          = $vcVPN->listNodes('ipsec profile');
	foreach my $prof (@profs) {
		my @tuns = $vcVPN->listNodes("ipsec profile $prof bind tunnel");
		foreach my $tun (@tuns) {
			my $lip_old = $vc->returnOrigValue("interfaces tunnel $tun local-ip");
			my $lip_new = $vc->returnValue("interfaces tunnel $tun local-ip");
			if ( !( "$lip_old" eq "$lip_new" ) ) {
				if ($tun_ip_changed) {
					# tunnel $tun_id is not the last tunnel with updated local-ip, so skip
                    exit 0;
				}
				if ( "$tun" eq "$tun_id" ) {
					$tun_ip_changed = 1;
				}
			}
		}
	}
	if ( !$tun_ip_changed ) {
		exit 0;
	}
}
if ( $vcVPN->exists('ipsec') ) {
	#
	# Connection configurations
	#
	my @profiles     = $vcVPN->listNodes('ipsec profile');
	my $prev_profile = "";
	foreach my $profile (@profiles) {
		my $conf_header = 0;
		my $profile_ike_group = $vcVPN->returnValue("ipsec profile $profile ike-group");
		if ( !defined($profile_ike_group) || $profile_ike_group eq '' ) {
			vpn_die([ "vpn", "ipsec", "profile", $profile, "ike-group" ],
			"$vpn_cfg_err No IKE group specified for profile \"$profile\".\n");
		}
		elsif ( !$vcVPN->exists("ipsec ike-group $profile_ike_group") ) {
			vpn_die([ "vpn", "ipsec", "profile", $profile, "ike-group" ],
			"$vpn_cfg_err The IKE group \"$profile_ike_group\" specified for profile "
			. "\"$profile\" has not been configured.\n");
		}

		#
		# ESP group
		#
		my $profile_esp_group = $vcVPN->returnValue("ipsec profile $profile esp-group");
		if ( !defined($profile_esp_group) || $profile_esp_group eq '' ) {
			vpn_die([ "vpn", "ipsec", "profile", $profile, "esp-group" ],
			"$vpn_cfg_err No ESP group specified for profile \"$profile\".\n");
		}
		elsif ( !$vcVPN->exists("ipsec esp-group $profile_esp_group") ) {
			vpn_die([ "vpn", "ipsec", "profile", $profile, "esp-group" ],
			"$vpn_cfg_err The ESP group \"$profile_esp_group\" specified "
			. "for profile \"$profile\" has not been configured.\n");
		}

		#
		# Authentication mode
		#
		#
		# Write shared secrets to ipsec.secrets
		#
		my $auth_mode = $vcVPN->returnValue("ipsec profile $profile authentication mode");
		my $psk = '';
		if ( !defined($auth_mode) || $auth_mode eq '' ) {
			vpn_die(
				[ "vpn", "ipsec", "profile", $profile, "authentication" ],
				"$vpn_cfg_err No authentication mode for profile \"$profile\" specified.\n"
			);
		}
		elsif ( defined($auth_mode) && ( $auth_mode eq 'pre-shared-secret' ) ) { 
			$psk = $vcVPN->returnValue("ipsec profile $profile authentication pre-shared-secret");
			my $orig_psk = $vcVPN->returnOrigValue("ipsec profile $profile authentication pre-shared-secret");
			$orig_psk = "" if ( !defined($orig_psk) );
			if ( $psk ne $orig_psk && $orig_psk ne "" ) {
				print "WARNING: The pre-shared-secret will not be updated until the next re-keying interval\n";
				print "To force the key change use: 'reset vpn ipsec-peer'\n";
			}
			if ( !defined($psk) || $psk eq '' ) {
				vpn_die(
					[ "vpn", "ipsec", "profile", $profile, "authentication" ],
					"$vpn_cfg_err No 'pre-shared-secret' specified for profile \"$profile\""
					. " while 'pre-shared-secret' authentication mode is specified.\n"
				);
			}
		}
		else {
			vpn_die(
				[ "vpn", "ipsec", "profile", $profile, "authentication" ],
				"$vpn_cfg_err Unknown/unsupported authentication mode \"$auth_mode\" for profile "
				. "\"$profile\" specified.\n"
			);
		}

		my @tunnels = $vcVPN->listNodes("ipsec profile $profile bind tunnel");
		foreach my $tunnel (@tunnels) {
			#
			# Check whether this tunnel is already in some profile
			#
			foreach my $prof (@profiles) {
				if ( $prof != $profile ) {
					if ( $vcVPN->exists("ipsec profile $prof bind tunnel $tunnel") )
                    {
						vpn_die(["vpn",  "ipsec",  "profile", $profile,"bind", "tunnel", $tunnel],
						"$vpn_cfg_err Tunnel \"$tunnel\" is already configured in profile \"$prof\".");
					}
				}
			}

			my $needs_passthrough = 'false';
			my $tunKeyword        = 'tunnel ' . "$tunnel";

			if ( $conf_header eq 0 ) {
				$genout .= "connections {\n";
				$conf_header = 1;
			}

			my $conn_head = "\tdmvpn-$profile-$tunnel {\n";
			$genout .= $conn_head;

			my $lip = $vc->returnValue("interfaces tunnel $tunnel local-ip");
			my $leftsourceip = undef;

			#
			# Write IKE configuration from group
			#
			my $ikelifetime = IKELIFETIME_DEFAULT;
			$genout .= "\t\tproposals = ";
			my $ike_group = $vcVPN->returnValue("ipsec  profile $profile ike-group");
			if ( defined($ike_group) && $ike_group ne '' ) {
				my @ike_proposals = $vcVPN->listNodes("ipsec ike-group $ike_group proposal");
				my $first_ike_proposal = 1;
				foreach my $ike_proposal (@ike_proposals) {
					#
					# Get encryption, hash & Diffie-Hellman  key size
					#
					my $encryption = $vcVPN->returnValue("ipsec ike-group $ike_group proposal $ike_proposal encryption");
					my $hash = $vcVPN->returnValue("ipsec ike-group $ike_group proposal $ike_proposal hash");
					my $dh_group = $vcVPN->returnValue("ipsec ike-group $ike_group proposal $ike_proposal dh-group");

					if ( defined($dh_group) ) {
						$dh_disable = 1;
					}

					#
					# Write separator if not first proposal
					#
					if ($first_ike_proposal) {
						if ( !defined($dh_group) ) {
							vpn_die(["vpn","ipsec","profile", $profile,"bind","tunnel", $tunnel],"$vpn_cfg_err 'dh-group' must be specified in ".
									"ike-group \"$ike_group\" proposal \"$ike_proposal\"  dh-group. \n");
						}
						$first_ike_proposal = 0;
					}
					else {
						$genout .= ",";
					}

					#
					# Write values
					#
					if ( defined($encryption) && defined($hash) ) {
						$genout .= "$encryption-$hash";
						if ( defined($dh_group) ) {
							my $cipher_out = get_dh_cipher_result($dh_group);
							if ($cipher_out eq 'unknown') {
								vpn_die(["vpn","ipsec","profile", $profile,"bind","tunnel", $tunnel],"$vpn_cfg_err Invalid 'dh-group' $dh_group specified in ".
								"profile \"$profile\" for $tunKeyword.  Only 2, 5, or 14 through 26 accepted.\n");
							} else {
								$genout .= "-$cipher_out";
							}
						}
					}
				}
				$genout .= "\n";
				
				#
				# Get IKE version setting
				#
				my $key_exchange = $vcVPN->returnValue("ipsec ike-group $ike_group key-exchange");
				if ( defined($key_exchange) ) {
					if ( $key_exchange eq 'ikev1' ) {
						$genout .= "\t\tversion = 1\n";
					}
					if ( $key_exchange eq 'ikev2' ) {
						$genout .= "\t\tversion = 2\n";
					}
				}else {
					$genout .= "\t\tversion = 0\n";
				}

				#
				# Get ikev2-reauth configuration
				# Check IKE Lifetime
				#
				my $ikev2_group_reauth = $vcVPN->returnValue("ipsec ike-group $ike_group ikev2-reauth");
				my $t_ikelifetime = $vcVPN->returnValue("ipsec ike-group $ike_group lifetime");
				if ( defined($t_ikelifetime) && $t_ikelifetime ne '' ) {
					$ikelifetime = $t_ikelifetime;
				}
				if ( defined($ikev2_group_reauth) ) {
					if ( $ikev2_group_reauth eq 'yes' && defined($ikelifetime) ) {
						$genout .= "\t\treauth_time = $ikelifetime" . "s\n";
						}else {
							$genout .= "\t\trekey_time = $ikelifetime" . "s\n";
						}
				} else {
					$genout .= "\t\trekey_time = $ikelifetime" . "s\n";
				}
                
				#
				# Allow the user to disable MOBIKE for IKEv2 connections
				#
				my $mob_ike = $vcVPN->returnValue("ipsec ike-group $ike_group mobike");
 
				if (defined($mob_ike)) {
					if (defined($key_exchange) && $key_exchange eq 'ikev2') {
						if ($mob_ike eq 'enable') {
							$genout .= "\t\tmobike = yes";
						}
						if ($mob_ike eq 'disable') {
							$genout .= "\t\tmobike = no";
						}
					}else {
						$genout .= "\t\tmobike = no";
					}
				}

				#
				# Check for Dead Peer Detection DPD
				#
				my $dpd_interval = $vcVPN->returnValue("ipsec ike-group $ike_group dead-peer-detection interval");
				my $dpd_timeout = $vcVPN->returnValue("ipsec ike-group $ike_group dead-peer-detection timeout");
				my $dpd_action = $vcVPN->returnValue("ipsec ike-group $ike_group dead-peer-detection action");
				if ( defined($dpd_interval) && defined($dpd_timeout) && defined($dpd_action) ) {
					$genout .= "\t\tdpd_delay = $dpd_interval" . "s\n";
					$genout .= "\t\tdpd_timeout = $dpd_timeout" . "s\n";
				}
			}

			$genout .= "\t\tkeyingtries = 0\n";

			#
			# Authentication
			#
			$genout .="\t\tlocal {\n";
			if ( defined($auth_mode) && ( $auth_mode eq 'pre-shared-secret' ) ) {
				$genout .= "\t\t\tauth = psk\n";
			}
			$genout .="\t\t}\n";
			$genout .="\t\tremote {\n";
			if ( defined($auth_mode) && ( $auth_mode eq 'pre-shared-secret' ) ) {
				$genout .= "\t\t\tauth = psk\n";
			}
			$genout .="\t\t}\n";

			#
			# Write ESP configuration from group
			#
			$genout .="\t\tchildren {\n";
			$genout .="\t\t\tdmvpn {\n";
			my $esplifetime = ESPLIFETIME_DEFAULT;
			$genout .= "\t\t\t\tesp_proposals = ";
			my $esp_group = $vcVPN->returnValue("ipsec profile $profile esp-group");
			if ( defined($esp_group) && $esp_group ne '' ) {
				my @esp_proposals =	$vcVPN->listNodes("ipsec esp-group $esp_group proposal");
				my $first_esp_proposal = 1;
				foreach my $esp_proposal (@esp_proposals) {
					#
					# Get encryption, hash
					#
					my $encryption = $vcVPN->returnValue("ipsec esp-group $esp_group proposal $esp_proposal encryption");
					my $hash = $vcVPN->returnValue("ipsec esp-group $esp_group proposal $esp_proposal hash");
					my $pfs = $vcVPN->returnValue("ipsec esp-group $esp_group pfs");
                    
					#
					# Write separator if not first proposal
					#
					if ($first_esp_proposal) {
						$first_esp_proposal = 0;
					}
					else {
						$genout .= ",";
					}
					if (defined($pfs)) {
						if ($pfs eq 'enable') {
							# Get the first IKE group's dh-group and use that as our PFS setting
							my $default_pfs = $vcVPN->returnValue("ipsec ike-group $ike_group proposal 1 dh-group");
							$pfs = get_dh_cipher_result($default_pfs);
							if ( !defined($default_pfs) && $pfs eq 'unknown' ) {
								vpn_die(["vpn","ipsec","profile", $profile,"bind","tunnel", $tunnel],"$vpn_cfg_err 'pfs enabled' needs 'dh-group' specified in ".
								"ike-group \"$ike_group\" proposal 1 dh-group. \n");
							}
						} elsif ($pfs eq 'disable') {
							undef $pfs;
						} else {
							$pfs = get_dh_cipher_result($pfs);
						}
					}

					#
					# Write values
					#
					if ( defined($encryption) && defined($hash) ) {
						$genout .= "$encryption-$hash";
						if (defined($pfs)) {
							$genout .= "-$pfs";
						}
					}
				}
				$genout .= "\n";

				my $t_esplifetime =	$vcVPN->returnValue("ipsec esp-group $esp_group lifetime");
				if ( defined($t_esplifetime) && $t_esplifetime ne '' ) {
					$esplifetime = $t_esplifetime;
				}
				$genout .= "\t\t\t\trekey_time = $esplifetime" . "s\n";

				my $lower_lifetime = $ikelifetime;
				if ( $esplifetime < $ikelifetime ) {
					$lower_lifetime = $esplifetime;
				}
				
				#
				# The lifetime values need to be greater than:
				#   rekeymargin*(100+rekeyfuzz)/100
				#
				my $rekeymargin = REKEYMARGIN_DEFAULT;
				if ($lower_lifetime <= (2 * $rekeymargin)) {
					$rekeymargin = int($lower_lifetime / 2) - 1;
				}
				$genout .= "\t\t\t\trand_time = $rekeymargin" . "s\n";
                
				#
				# Protocol/port
				#
				my $protocol   = "gre";
				my $lprotoport = '';
				if ( defined($protocol) ) {
					$lprotoport .= $protocol;
				}
				if ( not( $lprotoport eq '' ) ) {
					$genout .= "\t\t\t\tlocal_ts = dynamic[$lprotoport]\n";
				}

				my $rprotoport = '';
				if ( defined($protocol) ) {
					$rprotoport .= $protocol;
				}
				if ( not( $rprotoport eq '' ) ) {
					$genout .= "\t\t\t\tremote_ts = dynamic[$rprotoport]\n";
				}
            
				#
				# Mode (tunnel or transport)
				#
				my $espmode = $vcVPN->returnValue("ipsec esp-group $esp_group mode");
				if ( !defined($espmode) || $espmode eq '' ) {
					$espmode = "transport";
				}
				$genout .= "\t\t\t\tmode = $espmode\n";
				
				#
				# Check for Dead Peer Detection DPD
				#
				my $dpd_interval = $vcVPN->returnValue("ipsec ike-group $ike_group dead-peer-detection interval");
				my $dpd_timeout = $vcVPN->returnValue("ipsec ike-group $ike_group dead-peer-detection timeout");
				my $dpd_action = $vcVPN->returnValue("ipsec ike-group $ike_group dead-peer-detection action");	
				if (   defined($dpd_interval) && defined($dpd_timeout) && defined($dpd_action) ) {

					$genout .= "\t\t\t\tdpd_action = $dpd_action\n";
				}
				

				#
				# Compression
				#
				my $compression = $vcVPN->returnValue("ipsec esp-group $esp_group compression");
				if ( defined($compression) ) {
					if ( $compression eq 'enable' ) {
						$genout .= "\t\t\t\tipcomp=yes\n";
					}
				}
			}

			$genout .= "\t\t\t}\n";
			$genout .= "\t\t}\n";
			$genout .= "\t}\n"; # to identify end of connection definition
								# used by clear vpn op-mode command
		}
		$genout .= "}\n";
		$genout .= "secrets {\n";
		my @tunnels = $vcVPN->listNodes("ipsec profile $profile bind tunnel");
		foreach my $tunnel (@tunnels) {
			#
			# Check whether this tunnel is already in some profile
			#
			foreach my $prof (@profiles) {
				if ( $prof != $profile ) {
					if ($vcVPN->exists("ipsec profile $prof bind tunnel $tunnel")){
						vpn_die(["vpn",  "ipsec",  "profile", $profile,"bind", "tunnel", $tunnel],
								"$vpn_cfg_err Tunnel \"$tunnel\" is already configured in profile \"$prof\".");
					}
				}
			}
			my $ike_id = "\tike-dmvpn-$tunnel {\n";
			$genout .= $ike_id;
			$genout .= "\t\tsecret = $psk\n";
			$genout .= "\t}\n";
		}
		$genout .= "}\n";
	}
}
else {
	#
	# remove any previous config lines, so that when "clear vpn ipsec-process"
	# is called it won't find the vyatta keyword and therefore will not try
	# to start the ipsec process.
	#
	$genout = '';
	$genout         .= "# No VPN configuration exists.\n";
}

if (!(defined($config_file) && ( $config_file ne '' ))) {
	print "Regular config file output would be:\n\n$genout\n\n";
	exit(0);
}

write_config( $genout, $config_file);

my $update_interval      = $vcVPN->returnValue("ipsec auto-update");
my $update_interval_orig = $vcVPN->returnOrigValue("ipsec auto-update");
$update_interval_orig = 0 if !defined($update_interval_orig);
if ( is_vpn_running() ) {
	vpn_exec( 'ipsec rereadall >&/dev/null', 're-read secrets and certs' );
	vpn_exec( 'ipsec reload >&/dev/null',    'reload changes to ipsec.conf' );
	vpn_exec( 'swanctl -q >&/dev/null',    'reload changes to swanctl.conf' );
}
else {
	if ( !defined($update_interval) ) {
		vpn_exec( 'ipsec start >&/dev/null', 'start ipsec' );
        my $counter = 10;
		while($counter > 0){
			if (-e "/var/run/charon.pid") {
				vpn_exec( 'swanctl -q >&/dev/null',    'reload changes to swanctl.conf' );
				last;
			}
			$counter--;
			sleep(1);
			if($counter == 0){
				vpn_die(["vpn", "ipsec"], "$vpn_cfg_err IPsec is not running.\n");
			}
		}
	}
	else {
		vpn_exec(
			'ipsec start --auto-update ' . $update_interval . ' >&/dev/null',
			'start ipsec with auto-update $update_interval' );
			        my $counter = 10;
		while($counter > 0){
			if (-e "/var/run/charon.pid") {
				vpn_exec( 'swanctl -q >&/dev/null',    'reload changes to swanctl.conf' );
				last;
			}
			$counter--;
			sleep(1);
			if($counter == 0){
				vpn_die(["vpn", "ipsec"], "$vpn_cfg_err IPsec is not running.\n");
			}
		}
	}
}

#
# Return success
#
exit 0;

sub vpn_die {
	my ( @path, $msg ) = @_;
	Vyatta::Config::outputError( @path, $msg );
	exit 1;
}

sub write_config {
	my ( $genout, $config_file) = @_;

	open my $output_config, '>', $config_file
		or die "Can't open $config_file: $!";
	print ${output_config} $genout;
	close $output_config;
}

sub vpn_exec {
	my ( $command, $desc ) = @_;

	open my $logf, '>>', $LOGFILE
		or die "Can't open $LOGFILE: $!";

	use POSIX;
	my $timestamp = strftime( "%Y-%m-%d %H:%M.%S", localtime );

	print ${logf} "$timestamp\nExecuting: $command\nDescription: $desc\n";

	my $cmd_out = qx($command);
	my $rval    = ( $? >> 8 );
	print ${logf} "Output:\n$cmd_out\n---\n";
	print ${logf} "Return code: $rval\n";
	if ($rval) {
		if ( $command =~ /^ipsec.*--asynchronous$/ && ( $rval == 104 || $rval == 29 ) ) {
			print ${logf} "OK when bringing up VPN connection\n";
		}
		else {
			#
			# We use to consider the commit failed if we got a error
			# from the call to ipsec, but this causes the configuration
			# to not get included in the running config.  Now that
			# we support dynamic interface/address (e.g. dhcp, pppoe)
			# we want a valid config to get committed even if the
			# interface doesn't exist yet.  That way we can use
			# "clear vpn ipsec-process" to bring up the tunnel once
			# the interface is instantiated.  For pppoe we will add
			# a script to /etc/ppp/ip-up.d to bring up the vpn
			# tunnel.
			#
			print ${logf} "VPN commit error.  Unable to $desc, received error code $?\n";

			#
			# code 768 is for a syntax error in the secrets file
			# this happens when a dhcp interface is configured
			# but no address is assigned yet.
			# only the line that has the syntax error is not loaded
			# So we can safely ignore this error since our code generates
			# secrets file.
			#
			if ( $? ne '768' ) {
				print "Warning: unable to [$desc], received error code $?\n";
				print "$cmd_out\n";
			}
		}
	}
	print ${logf} "---\n\n";
	close $logf;
}

sub printTree {
	my ( $vc, $path, $depth ) = @_;

	my @children = $vc->listNodes($path);
	foreach my $child (@children) {
		print '    ' x $depth;
		print $child . "\n";
		printTree( $vc, "$path $child", $depth + 1 );
	}
}

sub printTreeOrig {
	my ( $vc, $path, $depth ) = @_;

	my @children = $vc->listOrigNodes($path);
	foreach my $child (@children) {
		print '    ' x $depth;
		print $child . "\n";
		printTreeOrig( $vc, "$path $child", $depth + 1 );
	}
}

sub get_dh_cipher_result { 
	my ($cipher) = @_;
	my $ciph_out;
	if ($cipher eq '2' || $cipher eq 'dh-group2') {
		$ciph_out = 'modp1024';
	} elsif ($cipher eq '5' || $cipher eq 'dh-group5') {
		$ciph_out = 'modp1536';
	} elsif ($cipher eq '14' || $cipher eq 'dh-group14') {
		$ciph_out = 'modp2048';
	} elsif ($cipher eq '15' || $cipher eq 'dh-group15') {
		$ciph_out = 'modp3072';
	} elsif ($cipher eq '16' || $cipher eq 'dh-group16') {
		$ciph_out = 'modp4096';
	} elsif ($cipher eq '17' || $cipher eq 'dh-group17') {
		$ciph_out = 'modp6144';
	} elsif ($cipher eq '18' || $cipher eq 'dh-group18') {
		$ciph_out = 'modp8192';
	} elsif ($cipher eq '19' || $cipher eq 'dh-group19') {
		$ciph_out = 'ecp256';
	} elsif ($cipher eq '20' || $cipher eq 'dh-group20') {
		$ciph_out = 'ecp384';	
	} elsif ($cipher eq '21' || $cipher eq 'dh-group21') {
		$ciph_out = 'ecp521';
	} elsif ($cipher eq '22' || $cipher eq 'dh-group22') {
		$ciph_out = 'modp1024s160';
	} elsif ($cipher eq '23' || $cipher eq 'dh-group23') {
		$ciph_out = 'modp2048s224';
	} elsif ($cipher eq '24' || $cipher eq 'dh-group24') {
		$ciph_out = 'modp2048s256';
	} elsif ($cipher eq '25' || $cipher eq 'dh-group25') {
		$ciph_out = 'ecp192';
	} elsif ($cipher eq '26' || $cipher eq 'dh-group26') {
		$ciph_out = 'ecp224';
	} else {
		$ciph_out = 'unknown';
	}
	return $ciph_out;
}
# end of file
