#!/usr/bin/python
#
# Copyright (C) 2012 Paul Wouters <pwouters@redhat.com>
#
# Based on old perl and shell code:
# Copyright (C) 2003 Sam Sgro <sam@freeswan.org> 
# Copyright (C) 2005-2008 Michael Richardson <mcr@xelerance.com>
# Copyright (C) 2005-2009 Paul Wouters <paul@xelerance.com>
#
# Based on "verify" from the FreeS/WAN distribution, (C) 2001 Michael 
# Richardson <mcr@freeswan.org>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
#
# 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.

import os, sys, subprocess, glob


retcode = 0

confdir = os.getenv("IPSEC_CONFS")
if not confdir:
	# should get substituted by 'make programs'
	confdir = "/usr/libexec/ipsec"

ipsecbin = "/usr/sbin/ipsec"

if not os.path.isfile(ipsecbin):
	# hopefully somewhere in our path then
	ipsecbin = "ipsec"

if not os.path.isfile("%s/ipsec.conf"%confdir):
	try:
		p = subprocess.Popen("%s --config"%ipsecbin, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		output = output.strip()
		if os.path.isfile("%s/ipsec.conf"%output):
			confdir = output
	except:
		sys.exit("Failed to find ipsec.conf")

# Should we print in colour by default?
colour = 0
try:
	p = subprocess.Popen("consoletype", stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
	output, err = p.communicate()
	output = output.strip()
	if output in ("vc","tty","pty"):
		colour = 1
except:
	try:
		p = subprocess.Popen("tput colors", stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if int(output) > 0:
			colour = 1
	except:
		pass


def printfun(text):
	# suppress newline
	sys.stdout.write("%-50s"%text)

def print_result(rcode, rtext):
	global colour
	global retcode
	OK = '\033[92m'
	WARN = '\033[93m'
	FAIL = '\033[91m'
	ENDC = '\033[0m'

	if rcode == "FAIL":
		retcode = 1
		if not rtext:
			rtext = "FAILED"
		if colour:
			print "\t[%s%s%s]"%(FAIL,rtext,ENDC)
		else:
			print "\t[%s]"%rtext
	elif rcode == "WARN":
		# don't log warnings until we implemented IKE over TCP
		# retcode = 1
		if not rtext:
			rtext = "WARNING"
		if colour:
			print "\t[%s%s%s]"%(WARN,rtext,ENDC)
		else:
			print "\t[%s]"%rtext
	elif rcode == "OK":
		if not rtext:
			rtext = "OK"
		if colour:
			print "\t[%s%s%s]"%(OK,rtext,ENDC)
		else:
			print "\t[%s]"%rtext
	else:
		print "INTERNAL ERROR - unknown rcode:%s"%rcode




# Verification routines begin here...
#
# Check DNS Configuration based on a hostname
# $1 = Hostname (string)
# eg: checkdnshost oetest.freeswan.org
#sub checkdnshost {
#    run "host -t key $_[0]";
#    ($keypresent)=grep /(0x4200|16896)/, @out;
#    if($keypresent) 
#    { 
#	printfun "   Looking for KEY in forward dns zone: $_[0]";
#	deprecated; 
#    }
#
#
#    printfun "   Looking for TXT in forward dns zone: $_[0]";
#    run "host -t txt $_[0]";
#    ($txtpresent)=grep /X-IPsec-Server/,@out;
#    errchk "$txtpresent", "MISSING";
#}

# Check DNS Configuration based on IP address
# $1 = IP Address (string)
# eg: checkdnsip 127.0.0.2
#sub checkdnsip {
#    $fortxt=$_[0];
#    $revtxt=join('.',reverse(split(/\./, $fortxt))).".in-addr.arpa.";
#    printfun "   Looking for TXT in reverse dns zone: $revtxt";
#    run "host -t txt $revtxt";
#    ($txtpresent)=grep /X-IPsec-Server/,@out;
#    errchk "$txtpresent", "MISSING";
#
#    if($txtpresent) {
#	$txtpresent=~ s/.*X-IPsec-Server\([0-9].*\)=//; $txtpresent=~ s/[\"\ ].*//;
#	$gwip=$txtpresent;
#	chomp($gwip);
#	$gwrev=join('.',reverse(split(/\./, $gwip))).".in-addr.arpa.";
#	# Check for a KEY record for the indicated IPSec GW.
#	run "host -t key $gwrev";
#	($keypresent)=grep /(0x4200|16896)/, @out;
#	if($keypresent) 
#	{ 
#	    printfun "   Looking for KEY in reverse dns zone: $gwrev";
#	    deprecated; 
#	    $print_deprecated = 1; 
#
#	}
#	# If the host is its own gateway, then we know we've got a TXT record.
#	if($gwip ne $fortxt) {	
#	    printfun "Looking for TXT in reverse dns zone: $gwrev";
#	    run "host -t txt $gwrev";
#	    ($txtpresent)=grep /X-IPsec-Server/,@out;
#	    errchk "$txtpresent", "MISSING";
#	}
#
#    }
#}

def plutocheck():
        printfun("Checking that pluto is running")
	p = subprocess.Popen(["pidof", "pluto"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
	output, err = p.communicate()
	if not output:
		print_result("FAIL","FAILED")
		return
	else:
		print_result("OK","OK")

	# only if pluto is running, do the listen tests
	udp500check()
	tcp500check()
	udp4500check()
	tcp4500check()
	tcp10000check()

# This is pretty broken, as you can enable forwarding specificaly via iptables as well
# It also won't find/exclude all kinds of interfaces. Also, lots of people have one
# ethernet and one wifi interface, but don't want to bridge these.
# So, mostly left here for historic reasons - candidate to be changed/removed
def forwardcheck():
	try:
		output = open("/proc/net/dev","r").read().strip()
	except:
		printfun("Checking for multiple interfaces")
		print_result("FAIL","UNEXPECTED KERNEL DEV LIST")
	count = 0
	for line in output.split("\n"):
		if ":" in line:
			if not "lo:" in line and not "ipsec" in line and not "mast" in line and not "virbr:" in line:
				# let's count this as a real physical interface
				count += 1
	if count > 1:
		printfun("Two or more interfaces found, checking IP forwarding")
		try:
			output = open("/proc/sys/net/ipv4/ip_forward","r").read().strip()
		except:
			print_result("FAIL","MISSING ip_forward proc file")
			return
		if output == "1":
			print_result("OK","OK")
		else:
			print_result("FAIL","FAILED")

def rpfiltercheck():
	fail = 0
	printfun("Checking rp_filter")
	for dirname in glob.glob("/proc/sys/net/ipv4/conf/*"):
		val = open("%s/rp_filter"%dirname,"r").read().strip()
		if val == "1":
			if fail == 0:
				print_result("FAIL","ENABLED")
			fail = 1
			printfun(" %s/rp_filter"%dirname)
			print_result("FAIL","ENABLED")
	if fail == 0:
			print_result("OK","OK")
		
# Check if any NAT or MASQUERADE would accidentally mess up our packets
def natcheck():
	printfun("Checking NAT and MASQUERADEing")
	print_result("WARN","TEST INCOMPLETE")

	# run iptables -t nat -L -n
	# grep for NAT or MASQ
	# for KLIPS
	#   grep over /proc/net/ipsec_eroute check for 'tun0x' then find overlap
	# for NETKEY
	# grep over ip xfrm state/policy and find overlap

def cmdchecks():
	printfun("Checking 'ip' command")
	if not os.path.isfile("/sbin/ip") and not os.path.isfile("/usr/sbin/ip"):
			print_result("FAIL","FAILED")
	p = subprocess.Popen(["ip", "xfrm"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
	output, err = p.communicate()
	if not ( ("XFRM-OBJECT" in err) or ("XFRM_OBJECT" in err) ):
			print_result("FAIL","IP XFRM BROKEN")
	else:
			print_result("OK","OK")

	printfun("Checking 'iptables' command")
	if not os.path.isfile("/sbin/iptables") and not os.path.isfile("/usr/sbin/iptables"):
			print_result("WARN","MISSING")
	else:
			print_result("OK","OK")


def udp500check():
	printfun(" Pluto listening for IKE on udp 500")
	try:
		p = subprocess.Popen(["ss", "-n", "-l", "-u", "sport = :500"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		output, err = p.communicate()
		if ":500" in output:
			print_result("OK","OK")
		else:
			print_result("FAIL","FAILED")
	except:
			print_result("FAIL","FAILED")

# not yet implemented in *swan
def tcp500check():
	printfun(" Pluto listening for IKE on tcp 500")
	try:
		p = subprocess.Popen(["ss", "-n", "-l", "-t", "sport = :500"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		output, err = p.communicate()
		if ":500" in output:
			print_result("OK","OK")
		else:
			print_result("WARN","NOT IMPLEMENTED")
	except:
			print_result("WARN","NOT IMPLEMENTED")

def udp4500check():
	printfun(" Pluto listening for IKE/NAT-T on udp 4500")
	try:
		p = subprocess.Popen(["ss", "-n", "-l", "-u", "sport = :4500"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		output, err = p.communicate()
		if ":4500" in output:
			print_result("OK","OK")
		else:
			print_result("FAIL","DISABLED")
	except:
			print_result("FAIL","DISABLED")

# not yet implemented in *swan
def tcp4500check():
	printfun(" Pluto listening for IKE/NAT-T on tcp 4500")
	try:
		p = subprocess.Popen(["ss", "-n", "-l", "-t", "sport = :4500"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		output, err = p.communicate()
		if ":4500" in output:
			print_result("OK","OK")
		else:
			print_result("WARN","NOT IMPLEMENTED")
	except:
			print_result("WARN","NOT IMPLEMENTED")

# not yet implemented in *swan
def tcp10000check():
	printfun(" Pluto listening for IKE on tcp 10000 (cisco)")
	try:
		p = subprocess.Popen(["ss", "-n", "-l", "-t", "sport = :10000"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		output, err = p.communicate()
		if ":10000" in output:
			print_result("OK","OK")
		else:
			print_result("WARN","NOT IMPLEMENTED")
	except:
			print_result("WARN","NOT IMPLEMENTED")

def installstartcheck():
	print "Checking if IPsec got installed and started correctly:\n"
	printfun("Version check and ipsec on-path")
	try:
		p = subprocess.Popen(["ipsec", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if "swan" in output:
			print_result("OK","OK")
			print output.replace("Linux","").strip()
		else:
			print_result("FAIL","FAILED")
	except:
			print_result("FAIL","FAILED")

	printfun("Checking for IPsec support in kernel")
	if not os.path.isfile("/proc/net/ipsec_eroute") and not os.path.isfile("/proc/net/pfkey"):
		print_result("FAIL","FAILED")
		if "no kernel code presently loaded" in output:
			print("\n The ipsec service should be started before running 'ipsec verify'\n")
		return
	else:
		print_result("OK","OK")

	# we know we have some stack, so continue
	if os.path.isfile("/proc/net/ipsec_eroute"):
		installcheckklips()
	else:
		installchecknetkey()

def installcheckklips():
	# NAT-T patch check
	printfun(" KLIPS: checking for NAT Traversal support")
	try:
		natt = open("/sys/module/ipsec/parameters/natt_available","r").read().strip()
		if natt == "1":
			print_result("WARN","OLD STYLE")
		elif natt == "2":
			print_result("OK","OK")
		else:
			print_result("FAIL","UNKNOWN CODE")
	except:
		print_result("WARN","OLD/MISSING")

	# OCF patch check
	printfun(" KLIPS: checking for OCF crypto offload support ")
	try:
		ocf = open("/sys/module/ipsec/parameters/ocf_available","r").read().strip()
		if ocf == "1":
			print_result("OK","OK")
		else:
			print_result("FAIL","UNKNOWN CODE")
	except:
		print_result("WARN","N/A")

	# SAref patch check
	saref = ""
	printfun(" KLIPS: IPsec SAref kernel support")
	try:
		saref = open("/proc/net/ipsec/saref","r").read().strip()
		if "refinfo patch applied" in saref:
			print_result("OK","OK")
		else:
			print_result("WARN","N/A")
			
	except:
		print_result("WARN","N/A")

	# SAref bind patch check
	printfun(" KLIPS: IPsec SAref Bind kernel support")
	if "bindref patch applied" in saref:
		print_result("OK","OK")
	else:
		print_result("WARN","N/A")
			

def installchecknetkey():
	print   (" NETKEY: Testing XFRM related proc values")
	for option in ( "send_redirects", "accept_redirects"):
		printfun("         ICMP default/%s"%option) 
		try:
			redir = open("/proc/sys/net/ipv4/conf/default/%s"%option,"r").read().strip()
		except:
			print_result("FAIL","VERY BROKEN KERNEL")
			return
		if redir == "0":
			print_result("OK","OK")
		else:
			print_result("FAIL","NOT DISABLED")
			print("\n  Disable /proc/sys/net/ipv4/conf/*/%s or NETKEY will cause act on or cause sending of bogus ICMP redirects!\n"%option)

	printfun("         XFRM larval drop")
	try:
		larval = open("/proc/sys/net/core/xfrm_larval_drop","r").read().strip()
	except:
		print_result("FAIL","OLD OR BROKEN KERNEL")
		return
	if larval == "1":
		print_result("OK","OK")
	else:
			print_result("FAIL","NOT ENABLED")

	
def randomdevcheck():
	printfun("Hardware random device check")
	if os.path.isfile("/dev/hw_random") or os.path.isfile("/dev/hwrng"):
		p = subprocess.Popen(["pidof", "rngd"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
		output, err = p.communicate()
		if not output:
			p = subprocess.Popen(["pidof", "clrngd"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
			output, err = p.communicate()
			if not output:
				print_result("FAIL","NO RUNNING (cl)rngd DAEMON FOR HW")
			else:
				print_result("OK","CLRNGD")
		else:
			print_result("OK","RNGD")
	else:
			print_result("OK","N/A")
		








def main():
	installstartcheck()
	randomdevcheck()
	forwardcheck()
	rpfiltercheck()
	plutocheck()
	natcheck()
	cmdchecks()
	if retcode:
		sys.exit("\nipsec verify: encountered errors")



if __name__ == "__main__":
	main()

##  old perl code not yet ported
## 
## sub checktunnel {
##     $csource=$_[0]; $cdest=$_[1]; $ctun=$_[2]; $all="0.0.0.0/0";
## 
##     printfun "Checking $ctun from $csource to $cdest";
##     run "iptables -t nat -L POSTROUTING -n";
##     @out=grep !/(Chain POSTROUTING|target)/, @out;
##     foreach (@out) {
## 	( $target, $prot, $opt, $source, $dest ) = split(' ',$_);
## 	if(((($source eq $csource) || ($source eq $all)) && (($dest eq $cdest) || ($dest = $all))) && $target eq "ACCEPT")
## 	{ 
## 	    errchk "@out";
## 	    $reterr = 1;
## 	}
## 	else
## 	{
## 	    @err="$target from $source to $dest kills tunnel $source -> $cdest\n";
## 	    errchk "","FAILED";
## 	    $reterr = 1;
## 	}
##     }
## }
## 
## 	printfun "Checking NAT and MASQUERADEing";
## 	# This assumes KLIPS eroute information, we should add support
## 	# for NETKEY, but ip xfrm is very annoying to parse
## 	if(( -e "/proc/net/nf_conntrack" || -e "/proc/net/ip_conntrack")
## && -e "/proc/net/ipsec_eroute" )
## 	{
## 		run "iptables -t nat -L -n";
## 		if(grep /(NAT|MASQ)/, @out)
## 		{
## 		    printf "\n";
## 		    open("cat", "/proc/net/ipsec_eroute");
## 		    foreach(grep /tun0x/, <cat>)
## 		    {      
## 			@eroute=split(' ',$_);
## 			checktunnel $eroute[1], $eroute[3], $eroute[5];
## 		    }
## 		}
## 		else
## 		{
## 		    errchk "1";
## 		}
## 	}
## 	    else
## 	{ 
## 		errchk "OK";
## 	}
##     }
## }
## 
##     
## sub dnschecks {
##     # Check the running hostname.
##     printf "\nOpportunistic Encryption DNS checks:\n";
##     run "hostname"; 
##     ($hostname)=@out; chomp $hostname;
##     checkdnshost $hostname;
##     
##     # Check all the public IP addresses...
##     run "/sbin/ifconfig -a";
##     foreach (grep /inet addr/,@out)
##     {
## 	$_=~ s/^\s*//;
##         @temp=split(/[:\ ]+/, $_);
## 	push(@address,$temp[2]);
##     }
##     # Purge all non-routeable IPs...
##     @address=grep !/^(127.*|10.*|172.1[6789]+.*.*|172.2+.*.*|172.3[01]+.*.*|192.168.*.*|169.254.*.*)/,@address;
##     printfun "   Does the machine have at least one non-private address?";
##     errchk @address;
##     foreach(@address=grep !$check{$_}++,@address)
##     {
## 	checkdnsip $_;
##     }
## }
## 
## 
## # If you've passed --host or --ip, do only those checks.
## if($hostname || $ip)
## {
## # Check this --host for OE.
##     if($hostname)
##     {
## 	printf "Checking $hostname for Opportunistic Encryption:\n";
## 	checkdnshost $hostname;
## 	run "host -t A $hostname";
## 	if(($ipaddr) = grep (/address/i, @out))
## 	{
## 	    $ipaddr=~ s/.*address\ //;
## 	    chomp $ipaddr;
## 	    checkdnsip $ipaddr;
## 	}
## 	else
## 	{
## 	    printf "$hostname does not resolve to an IP, no reverse lookup tests possible.\n";
## 	}
##     }
## # Check this IP for OE.
##     if($ip)
##     {
## 	printf "Checking IP $ip for Opportunistic Encryption:\n";
## 	checkdnsip $ip;
##     }
## }
## else
## {
##     # Call the default routines...
##     # Root check...
##     if($> != "0") 
##     {
## 	print "To check this machine, you need to run \"$me\" as root.\n"; exit;
##     }
##     else
##     {
## 	installstartcheck;
## 	tunnelchecks;
## 	cmdchecks;
## 	run "ipsec addconn --configsetup";
##         ($oe)=grep(/oe=\'yes\'/, @out);
## 	if( $oe) {
## 		dnschecks;
##         	if($print_deprecated)
##         	{
##         	print "
## 
##    RFC 3445 restricts the use of the KEY RR to DNSSEC applications. The use of 
##    a KEY record sub-type for Opporunistic Encryption (OE) has been deprecated.
##    TXT records are used to provide all OE functionality.\n";
##         	}
## 	} else {
## 		printfun "Opportunistic Encryption Support";
## 		warnchk "", "DISABLED";
##         }
##     }
##     # finally, run the config file through the parser checks
##     run "ipsec auto addconn --checkconfig";
## }
## exit $reterr
