#!/usr/bin/perl -w
#
# This program is under GPL [http://www.gnu.org/copyleft/gpl.htm]
#
# midentd v2.0.2 - identd server with ipmasq support
#
# (c) shurdeek@panorama.sth.ac.at (current maintainer)
# (c) peter@adataloss.nl (original author and old maintainer)
#
# Fix for PPC by Turbo Fredriksson <turbo@debian.org>
#
use strict;
use Socket;
use Fcntl ':flock';

my ($uid, $in, $out);
my($rip,$lip,$request,$rport,$lport);

if ( ! -S STDIN ) {
	if (@ARGV < 2 || $ARGV[0] ne "-u") {
		&usage();
		exit;
	} else {
		$uid = $ARGV[1];
		if ($uid =~/^\d+$/) {
			my $tmp = getpwuid ($uid);
			if (!$tmp) {
				print "Invalid uid: $uid\n";
				exit;
			}
		} else {
			my $tmp = getpwnam ($uid);
			if (!$tmp) {
				print "Invalid username: $uid\n";
				exit;
			}
			$uid = $tmp;
		}
	}
	$SIG{CHLD} = \&REAPER;
	$SIG{INT} = \&INT;
	if ($> != 0) {
		die "Dude, only root can bind to ports < 1024";
	}
	my $port = 113;
	socket (SERVER, PF_INET, SOCK_STREAM, getprotobyname('tcp')) || die "socket: $!";
	bind (SERVER, sockaddr_in ($port, INADDR_ANY)) || die "Can't bind (1). Check for other running ident services";
	setsockopt (SERVER, SOL_SOCKET, SO_REUSEADDR, pack ("l", 1)) || die "setsockopt: $!";
	#bind (SERVER, sockaddr_in ($port, INADDR_ANY)) || die "Can't bind (2). Check for other running ident services";
	listen (SERVER, SOMAXCONN) || die "listen: $!";
	&daemon;
} else {
	$in = *STDIN;
	$out = *STDOUT;
	&process_request;
}

sub daemon
{
	chdir "/";
	close STDIN; open STDIN, "/dev/null";
	close STDOUT; open STDOUT, ">/dev/null";
	close STDERR; open STDERR, ">/dev/null";
	umask 0466;
	&logger ("Daemon started, listening.");
	# FIXME, maximum number of connections should be limited
	if (fork) {
		# exit parent
		# close SERVER;
		exit;
	}
	$> = $uid;
	$< = $uid;
	if ($< != $uid || $> != $uid) {
		die "Can't change UID\n";
	}
	while (accept (SOCKET, SERVER)) {
		$in = *SOCKET;
		$out = *SOCKET;
		if (fork) {
		} else {
			&process_request;
			exit; # unnecessary but just to be sure :-)
		}
	}
}

sub REAPER
{
  my $waitedpid = waitpid(-1, 0);
  $SIG{CHLD} = \&REAPER;
}

sub INT
{
	&logger ("Daemon exited.");
	close SOCKET; close SERVER;
  $SIG{INT} = \&INT;
}

sub process_request {
	(undef,$rip)=sockaddr_in(getpeername($in)); 

	(undef,$lip)=sockaddr_in(getsockname($out));
	logger("Connection from ".inet_ntoa($rip)." on ".inet_ntoa($lip));

	$|=1;
	#$request=<>;
	sysread($in,$request,128);
	#logger("Request: ".substr($request,0,-1));
	if($request =~ /^MASQ:/) {
	#	print "$'";
		my(@masqreq);
		@masqreq=split(/:/,$');
		if(@masqreq != 4) {
			ret("MQ:ERROR : INVALID-MASQ");
		}
		# THIS IS IMHO OBSOLETE, inetd/xined should handle this with tcp wrappers.
		#if(!allowmasq(inet_ntoa($rip))) {
		#	ret("MQ:ERROR : NOT-ALLOWED");
		#}
	
		$rport=$masqreq[0];
		$rip=pack("H8",$masqreq[1]);
		$lport=$masqreq[2];
		$lip=pack("H8",$masqreq[3]);
		$_=getident(procstr());
		ret("MQ:$_");
	} else {
		($lport,$rport)=split(/[\,\r\n{*}]/,$request);
		if(!($rport =~ /^([ \d])+$/)) { $rport=0; }
		if(!($lport =~ /^([ \d])+$/)) { $lport=0; }
		$lport =~ s/ //g;
		$rport =~ s/ //g;
	
		if($rport<1 || $rport>65535 || $lport<1 || $lport>65535) {
			ret("$lport,$rport : ERROR : INVALID-PORT");
		}
	
		$_=getident(procstr());
		ret("$lport,$rport : $_");
	}
}

sub procstr {
	my $big_endian = check_big_endian();
	logger("Running on big-endian system") if ($big_endian);

	return (sprintf("%8s:%04x %8s:%04x",
		unpack("H8",($big_endian ? $lip : reverse($lip))),
		$lport,
		unpack("H8",($big_endian ? $lip : reverse($rip))),
		$rport),
		sprintf("%8s:%04x %04x",
			unpack("H8",$rip),
			$rport,
			$lport));
}

sub getident {
	my($procstr,$uid);

	$procstr=shift;
	logger("Wants ".inet_ntoa($rip).":$rport-".inet_ntoa($lip).":$lport");

	# TRY LOCAL
	open(PNT,"</proc/net/tcp");
	$_=<PNT>; 
	while(<PNT>) {
		if(/^....:.$procstr/i) {
			$uid=substr($_,76,5);
			if($_=(getpwuid($uid))[0]) {
				return("USERID : UNIX : $_");
			} else {
				return("USERID : UNIX : $uid");
			}
		}
	}
	close(PNT);

	# TRY FORWARD
	$procstr=shift;
	if(defined($procstr)) {    
		my($q, $kernel, $mip, $mport);
		if ( -e "/proc/net/ip_masquerade") {
			# 2.0 and 2.2 kernel
			$kernel = 2;
			open(PNT,"</proc/net/ip_masquerade");
		} elsif ( -e "/proc/net/ip_conntrack") {
			# 2.4 kernel
			$kernel = 4;
			open(PNT,"</proc/net/ip_conntrack");
			$procstr = "src=".inet_ntoa($rip)." dst=".inet_ntoa($lip)." sport=$rport dport=$lport";
		}
		$_=<PNT>;
		while(<PNT>) {
			$q=$_;
				# 2.0 and 2.2
			if(($kernel == 2 && $q =~ /^TCP(.){15}$procstr/i) ||
				# 2.4
				($kernel == 4 && $q =~ /^tcp.*src=([^ ]+).*sport=([^ ]+).*$procstr/)
				) {
				socket(MQC,PF_INET,SOCK_STREAM,getprotobyname('tcp'));
				my $client;
				if ($kernel == 2) {	
					$client=pack("H8",substr($q,4,8));
				} else {
					$mip = $1;
					$mport = $2;
					$client=pack("H8",sprintf("%08X", &iptobit($mip)));
					$mip = sprintf("%08X", &iptobit($mip));
				}
				logger("Masqueraded connection, checking with ".inet_ntoa($client));
				if(!connect(MQC,sockaddr_in(113,$client))) {
					return("ERROR : NO-USER");
				}
				select(MQC);
				$|=1;
				select($out);
				logger("Doing MASQ request");
				if ($kernel == 2) {
					print MQC "MASQ:$rport:".unpack("H8",$rip).":".hex(substr($q,13,4)).":".substr($q,4,8)."\n";
				} else {
					print MQC "MASQ:$rport:".unpack("H8",$rip).":$mport:$mip\n";
				}
				#$_=<MQC>;
				sysread(MQC,$_,128);
				chop();
				close MQC;
				if(/^MQ:/) {
					return("$'");
				} else {
					socket(MQC,PF_INET,SOCK_STREAM,getprotobyname('tcp'));
					logger("Failed, doing standard ident request");
					if(!connect(MQC,sockaddr_in(113,$client))) {
						return("ERROR : NO-USER");
					}
					select(MQC);
					$|=1;
					select($out);
					print MQC "$lport, $rport\r\n";
					#$_=<MQC>;
					sysread(MQC,$_,128);
					chop();
					if(/USERID/) {
						ret($_);
					} else {
						return("ERROR : NO-USER");
					}
				}
			}
		}
	}
	
	# NOTHING WORKED
	return("ERROR : NO-USER");
}

sub logger {
	my($logtext);
	$logtext=shift;
	open(LOG,">>/var/log/midentd.log");
	flock (LOG, 2); # LOCK_EX
	print LOG localtime()." [$$] $logtext\n";
	flock (LOG, 8); # LOCK_UN
	close(LOG);
}

sub ret {
	$_=shift;
	print $out "$_\r\n";
	logger("Returning $_");
	close SOCKET;
	exit;
}

# Fix for 'http://www.debian.org/Bugs/db/34/34299.html'
sub check_big_endian {
	my($native, $big, $result);

 	$native = 1234567890;
 	$big    = pack("N", $native);
 	$result = unpack("L", $big);

 	if($result == $native) {
		# This is a big endian system.    (PPC's etc)
		return(1);
 	} else {
		# This is a little endian system. (PC's etc)
		return(0);
	}
}

sub iptobit {
	my $ip = shift;
	my $result = 0;
	my @quads;
	if ($ip =~ /\./) {
		@quads = split /\./, $ip;
		my $i;
		for ($i = 0; $i < scalar @quads; ++$i) {
			if (defined $quads[$i]) {
				$result += $quads[$i] * 256 ** (3 - $i);
			}
		}
	} else {
		$result = ~ ( 0 << $ip );
		$result <<= ( 32 - $ip );
	}
	return $result;
}

sub usage {
	print "Usage: midentd -u username/uid (when running from command line)\n";
	print "       midentd (when running from inetd/xinetd)\n";
	print "However, using midentd as a standalone daemon doesn't take the advantage of\n";
	print "tcp_wrappers.\n";
	print "If you need to limit who can connect to this service, use midentd from\n";
	print "inetd/xinetd or similar.\n";
}

# vim: tabstop=2
