summaryrefslogtreecommitdiff
path: root/bin/tls-check
diff options
context:
space:
mode:
Diffstat (limited to 'bin/tls-check')
-rwxr-xr-xbin/tls-check566
1 files changed, 566 insertions, 0 deletions
diff --git a/bin/tls-check b/bin/tls-check
new file mode 100755
index 0000000..db06c32
--- /dev/null
+++ b/bin/tls-check
@@ -0,0 +1,566 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Net::DNS;
+use Net::IPv4Addr qw( :all );
+use POSIX ":sys_wait_h";
+use IO::File;
+use IO::Socket::INET;
+use IO::Select;
+use Fcntl;
+use IPC::Open3;
+use Data::Dumper;
+use English;
+use POSIX qw(:errno_h);
+
+$| = 1;
+my $hostname = "opium.multi24.com";
+my $VERBOSE = 0;
+
+my $AFTER_SSL_SLEEP = 2;
+my $OPENSSL_TIMEOUT = 45;
+my $CHECK_LISTENS_TIMEOUT = 15; # 50 # done 3 times
+my $CHECK_TLS_TIMEOUT = 15; # 50 # done 3 times
+my $GET_MX_TIMEOUT = 5;
+my $WAIT_AFTER_QUIT = 2;
+
+$SIG{'CHLD'} = 'IGNORE';
+
+sub parse_reply($) {
+ my ($fh) = @_;
+
+ my $code = undef;
+ my $text = '';
+ while (1) {
+ my $line = <$fh>;
+ next unless defined $line;
+ my ($thiscode, $thistext);
+ if (($thiscode, $thistext) = $line =~ /^(\d\d\d)-(.*)$/s) {
+ $thistext =~ s/\r\n/\n/;
+ $text .= $thistext;
+ next;
+ } elsif (($thiscode, $thistext) = $line =~ /^(\d\d\d)\s+(.*)$/s) {
+ $thistext =~ s/\r\n/\n/;
+ $text .= $thistext;
+ $code = $thiscode;
+ last;
+ } else {
+ die ("Cannot parse $line\n");
+ };
+ };
+ return ($code, $text);
+}
+
+sub quit($) {
+ my ($peer) = @_;
+ print $peer "QUIT\r\n";
+ sleep ($WAIT_AFTER_QUIT);
+ close $peer;
+};
+
+sub do_ssl($) {
+ my ($peer) = @_;
+
+ my $listen = IO::Socket::INET->new(
+ Listen => 1,
+ Proto => 'tcp',
+ );
+ my $port = $listen->sockport();
+
+ my $p_pid = fork();
+ unless ($p_pid) {
+ my $socket = $listen->accept();
+ my $pid;
+ my @children;
+ $pid = fork();
+ push @children, $pid;
+ unless ($pid) {
+ close STDOUT;
+ my $d;
+ while (read($socket, $d, 1)) {
+ print $peer $d;
+ $peer->flush();
+ }
+ exit(0);
+ };
+ $pid = fork();
+ push @children, $pid;
+ unless ($pid) {
+ close STDOUT;
+ my $d;
+ while (read($peer, $d, 1)) {
+ print $socket $d;
+ $socket->flush();
+ };
+ exit(0);
+ };
+ #while (@children) {
+ # for (@children) {
+ # my $child = waitpid($_,WNOHANG);
+ # if ($child == $_) {
+ # @children = grep { $_ != $child } @children;
+ # };
+ # };
+ #};
+
+ exit(0);
+ };
+
+ my($wtr, $rdr, $err);
+ my $pid = open3($wtr, $rdr, $err, "timeout $OPENSSL_TIMEOUT openssl s_client -showcerts -connect localhost:$port");
+ #waitpid($p_pid,0);
+
+ return ($wtr, $rdr, $err);
+};
+
+sub check_listens($$$) {
+ my ($peer_host, $port, $do_ssl) = @_;
+
+ my ($result, $warning, $error) = (undef,undef,undef,undef);
+ my $tls_text = "";
+
+ eval {
+ while (1) {
+ local $SIG{ALRM} = sub { die "alarm\n" };
+ alarm($CHECK_LISTENS_TIMEOUT);
+
+ my $peer = IO::Socket::INET->new(
+ PeerAddr => $peer_host,
+ PeerPort => $port,
+ Protocol => 'tcp',
+ Timeout => 5,
+ ) or $result = 0, $error = ("Cannot connect: $!"), last;
+
+
+ alarm($CHECK_LISTENS_TIMEOUT);
+ my ($wtr, $rdr, $err);
+ if ($do_ssl) {
+ print STDERR "Checking SSL for $peer_host ... do ssl\n" if ($VERBOSE >= 3);
+ ($wtr, $rdr, $err) = do_ssl($peer);
+ print STDERR "Checking SSL for $peer_host ... ssl done\n" if ($VERBOSE >= 3);
+ sleep ($AFTER_SSL_SLEEP);
+ } else {
+ ($wtr, $rdr) = ($peer, $peer);
+ };
+
+ alarm($CHECK_LISTENS_TIMEOUT);
+
+ my ($code, $comment);
+ while (my $line = readline($rdr)) {
+ my ($thiscode, $thiscomment);
+ if (($thiscode, $thiscomment) = $line =~ /^(\d\d\d)-(.*)$/s) {
+ $thiscomment =~ s/\r\n/\n/;
+ $comment .= $thiscomment;
+ next;
+ } elsif (($thiscode, $thiscomment) = $line =~ /^(\d\d\d)\s+(.*)$/s) {
+ $thiscomment =~ s/\r\n/\n/;
+ $comment .= $thiscomment;
+ $code = $thiscode;
+ last;
+ } else {
+ $tls_text .= $line;
+ next;
+ };
+ };
+
+ if (defined $code && defined $comment) {
+ $error = "Code is $code and not 220 ($comment)" unless ($code == 220);
+ #$error .= "'$comment' does not include SMTP magic string" unless $comment =~ /SMTP/;
+ $result = 1 unless defined $error;
+ quit($wtr);
+ } else {
+ close($wtr);
+ };
+ close($rdr);
+ last;
+ };
+ alarm(0);
+ };
+ if ($@) {
+ die $@ unless $@ eq "alarm\n";
+ $error = "alarm - timeout";
+ };
+ if (defined $error) {
+ $error =~ s/Operation now in progress$/Timeout/;
+ };
+ return ($result, $warning, $error, $tls_text);
+};
+
+sub check_tls($) {
+ my ($peer_host) = @_;
+ my ($result, $warning, $error, $tls_text) = (undef,undef,undef,undef,undef);
+ print STDERR "Checking TLS for $peer_host\n" if ($VERBOSE >= 2);
+
+ eval {
+ while(1) {
+ local $SIG{ALRM} = sub { die "alarm\n" };
+ alarm($CHECK_TLS_TIMEOUT);
+
+ print STDERR "Checking TLS for $peer_host ... connet to port 25\n" if ($VERBOSE >= 3);
+ my $peer = IO::Socket::INET->new(
+ PeerAddr => $peer_host,
+ PeerPort => 'smtp',
+ Protocol => 'tcp',
+ Timeout => 5,
+ ) or $error = ("Cannot connect: $!"), last;
+
+ alarm($CHECK_TLS_TIMEOUT);
+
+ my ($code, $comment) = parse_reply($peer);
+ $error = "Code is $code and not 220 ($comment)", quit($peer), last unless ($code == 220);
+ #$error = "'$comment' does not include SMTP magic string", quit($peer), last unless $comment =~ /SMTP/;
+ $result = 0, quit($peer), last unless $comment =~ /ESMTP/;
+
+ print STDERR "Checking TLS for $peer_host ... send EHLO\n" if ($VERBOSE >= 3);
+ print $peer "EHLO $hostname\r\n";
+ ($code, $comment) = parse_reply($peer);
+ $error = "Code is $code and not 250 ($comment)", quit($peer), last unless ($code == 250);
+ my $tls = $comment =~ m/^STARTTLS$/im ? 1 : 0;
+ $result = 0, quit($peer), last unless $tls;
+
+ print STDERR "Checking TLS for $peer_host ... send STARTTLS\n" if ($VERBOSE >= 3);
+ $result = 1;
+ print $peer "STARTTLS\r\n";
+ ($code, $comment) = parse_reply($peer);
+ $warning = "STARTTLS return code is $code and not 220 ($comment)", quit($peer), last unless ($code == 220);
+
+ alarm($CHECK_TLS_TIMEOUT);
+ print STDERR "Checking TLS for $peer_host ... do ssl\n" if ($VERBOSE >= 3);
+ my ($wtr, $rdr, $err) = do_ssl($peer);
+ print STDERR "Checking TLS for $peer_host ... ssl done\n" if ($VERBOSE >= 3);
+
+ sleep $AFTER_SSL_SLEEP;
+ print STDERR "Checking TLS for $peer_host ... quit\n" if ($VERBOSE >= 3);
+ quit($wtr);
+ my $r = join '', <$rdr>;
+ close $rdr;
+ $tls_text = $r;
+
+ last;
+ };
+ alarm(0);
+ };
+ if ($@) {
+ die $@ unless $@ eq "alarm\n";
+ $error = "alarm - timeout";
+ };
+ if (defined $error) {
+ $error =~ s/Operation now in progress$/Timeout/;
+ };
+ print STDERR "Checking TLS for $peer_host ... done\n" if ($VERBOSE >= 3);
+ return ($result, $warning, $error, $tls_text);
+};
+
+
+sub get_mx($) {
+ my ($domain) = @_;
+
+ my @result;
+ my @mx = mx($domain);
+ if (scalar @mx) {
+ @result = map { { preference => $_->preference, exchange => $_->exchange } } @mx;
+ } else {
+ @result = ( { preference => 0, exchange => $domain } );
+ };
+ return @result;
+};
+
+sub check_mx($) {
+ my ($host) = @_;
+ my $error = undef;
+ my $result = undef;
+ my $warning = undef;
+ my $tls = undef;
+
+ my $query;
+ my $address;
+ my $res;
+ eval {
+ local $SIG{ALRM} = sub { die "alarm\n" };
+ alarm($GET_MX_TIMEOUT);
+ $res = Net::DNS::Resolver->new;
+ $query = $res->search($host);
+ alarm(0);
+ };
+ if ($@) {
+ die $@ unless $@ eq "alarm\n";
+ $error = "DNS query timed out.";
+ };
+
+ if ($query) {
+ foreach my $rr ($query->answer) {
+ next unless $rr->type eq "A";
+ if (defined $address) {
+ $warning = "$host has round robin A records";
+ next;
+ };
+ $address = $rr->address;
+ }
+ $error = "does not resolve to an ipv4 address" unless defined $address;
+ } else {
+ $error = "query failed: ".$res->errorstring
+ }
+
+ # 0.0.0.0 /8 Reserved
+ # 10.0.0.0 /8 RFC 1918 Private
+ # 14.0.0.0 /8 Public Data Network1
+ # 127.0.0.0 /8 Loopback
+ # 169.254.0.0 /16 Link-local
+ # 172.16.0.0 /12 RFC 1918 Private
+ # 192.0.2.0 /24 Example network
+ # 192.168.0.0 /16 RFC 1918 Private
+ # 224.0.0.0 /4 Multicast (Class D)
+ # 240.0.0.0 /4 Unspecified (Class >D)
+ if (defined $address &&
+ (ipv4_in_network("0.0.0.0/8", $address) ||
+ ipv4_in_network("10.0.0.0/8", $address) ||
+ ipv4_in_network("127.0.0.1/8", $address) ||
+ ipv4_in_network("169.254.0.0/16", $address) ||
+ ipv4_in_network("172.16.0.0/12", $address) ||
+ ipv4_in_network("192.0.2.0/24", $address) ||
+ ipv4_in_network("192.168.0.0/16", $address) ||
+ ipv4_in_network("224.0.0.0/4", $address) ||
+ ipv4_in_network("240.0.0.0/4", $address))) {
+ $error = "stupid admin sets A record to special address space ($address)"
+ };
+
+ my $return;
+ $return->{'smtp'} = {
+ error => $error,
+ warning => $warning,
+ };
+ unless (defined $error) {
+ my ($r, $w, $e, $t);
+ print STDERR $host." tls1\n" if ($VERBOSE >= 2);
+ ($r, $w, $e, $t) = check_tls($host);
+ $result .= $r if defined $r;
+ $warning .= $w if defined $w;
+ $error .= $e if defined $e;
+ $tls = $t if defined $t;
+
+ $return->{'smtp'} = {
+ error => $error,
+ warning => $warning,
+ result => $result,
+ tls => $t
+ };
+ print STDERR $host." sub\n" if ($VERBOSE >= 2);
+ ($r, $w, $e, $t) = check_listens($host, '587', 0);
+ $return->{'submission'} = {
+ error => $e,
+ warning => $w,
+ result => $r,
+ tls => $t
+ };
+ print STDERR $host." smtps\n" if ($VERBOSE >= 2);
+ ($r, $w, $e, $t) = check_listens($host, 'smtps', 1);
+ $return->{'smtps'} = {
+ error => $e,
+ warning => $w,
+ result => $r,
+ tls => $t
+ };
+ print STDERR $host." 2525\n" if ($VERBOSE >= 2);
+ ($r, $w, $e, $t) = check_listens($host, '2525', 0);
+ $return->{'2525'} = {
+ error => $e,
+ warning => $w,
+ result => $r,
+ tls => $t
+ };
+ #print STDERR $host." 25000\n" if ($VERBOSE >= 2);
+ #($r, $w, $e, $t) = check_listens($host, '25000', 0);
+ #$return->{'25000'} = {
+ # error => $e,
+ # warning => $w,
+ # result => $r,
+ # tls => $t
+ #};
+ #print STDERR $host." 22222\n" if ($VERBOSE >= 2);
+ #($r, $w, $e, $t) = check_listens($host, '22222', 0);
+ #$return->{'22222'} = {
+ # error => $e,
+ # warning => $w,
+ # result => $r,
+ # tls => $t
+ #};
+ print STDERR $host." checkmx DONE\n" if ($VERBOSE >= 2);
+ };
+
+ return $return;
+};
+
+sub do_address($) {
+ my ($address) = @_;
+
+ my ($localpart, $domain) = split (/@/, $address);
+
+ my @mx = get_mx($domain);
+ my @result;
+ for my $mx (@mx) {
+ my $res = check_mx($mx->{exchange});
+ push @result, {
+ mx => $mx->{exchange},
+ pri => $mx->{preference},
+ res => $res,
+ };
+ };
+ return \@result;
+};
+
+
+
+my @addresses;
+my %NICKS;
+while (<>) {
+ # $remailer{"aarg"} = "<remailer@aarg.net> cpunk
+ if (/remailer\{"(\S+)"\}.*<([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+)>/) {
+ push @addresses, $2;
+ $NICKS{$2} = $1;
+ } elsif (/<([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+)>/) {
+ push @addresses, $1;
+ };
+};
+
+my %unique;
+@addresses = sort { $a cmp $b } @addresses;
+@addresses = grep { !$unique{$_}++ } @addresses;
+
+my %FH;
+my @PIDs;
+for my $address (@addresses) {
+ $FH{$address} = new IO::File;
+ my $pid = $FH{$address}->open("-|");
+ unless (defined $pid) {
+ die ("Could not fork: $!");
+ };
+ unless ($pid) { # child
+ my $result = do_address($address);
+ my $dump = Data::Dumper->Dump( [$result] );
+ print STDERR "printing data for $address\n" if ($VERBOSE >= 2);
+ print $dump;
+ print STDERR "printed: $dump\n" if ($VERBOSE >= 6);
+ print STDERR "exiting child for $address\n" if ($VERBOSE >= 2);
+ exit(0);
+ };
+ push @PIDs, $pid;
+ fcntl($FH{$address}, F_SETFL, O_NONBLOCK) or die "can't set non blocking: $!";
+};
+
+
+
+
+my $result;
+my $END = time + 600;
+my $last_notice = 0;
+while ($END > time) {
+ my $s = IO::Select->new();
+ for my $address (@addresses) {
+ $s->add($FH{$address}) if exists $FH{$address};
+ };
+ last if $s->count() == 0;
+
+ if ($VERBOSE && $last_notice < time - 3) {
+ print STDERR ("Still waiting for ".join(", ", grep {exists $FH{$_}} @addresses)."\n");
+ $last_notice = time;
+ };
+ my $timeout = 3;
+ print STDERR "Calling can_read\n" if ($VERBOSE >= 3);
+ my @ready = $s->can_read($timeout);
+
+ my $forced = 0;
+ if (scalar @ready == 0) {
+ print STDERR "no ready handles, read them all\n" if $VERBOSE;
+ @ready = $s->handles();
+ $forced = 1;
+ };
+
+ for my $fh (@ready) {
+ print STDERR "$fh is ready".($forced ? " (not really)" : "")."\n" if ($VERBOSE >= 3);
+ my $addr;
+ for my $address (@addresses) {
+ next unless exists $FH{$address};
+ if ($fh->fileno() == $FH{$address}->fileno()) {
+ $addr = $address;
+ last;
+ };
+ };
+ die ("No address for $fh\n"), next unless defined $addr;
+
+ print STDERR "reading fh for $addr\n" if ($VERBOSE >= 2);
+
+ my $buf;
+
+ my $res = sysread($fh, $buf, 100000);
+ my $error = $ERRNO;
+ print STDERR "$fh read $res bytes\n" if (defined $res && $VERBOSE);
+ if (!defined $res) {
+ printf STDERR ("got error $ERRNO, again: %d, wouldb: %d\n", EAGAIN, EWOULDBLOCK) if ($VERBOSE > 5);
+ printf STDERR ("got error $ERRNO\n") if ($ERRNO != EWOULDBLOCK);
+ } elsif ($res > 0) {
+ $result->{$addr} .= $buf;
+ } else {
+ $fh->close;
+ delete $FH{$addr};
+ print STDERR "$addr reading DONE\n" if $VERBOSE;
+ };
+ };
+};
+print STDERR "DONE\n" if $VERBOSE;
+
+for my $address (@addresses) {
+ if (exists $FH{$address}) {
+ delete $result->{$address};
+ warn ("$address failed\n");
+ };
+};
+for my $key (keys %$result) {
+ my $VAR1;
+ my $fo = eval ($result->{$key});
+ $VAR1 = undef;
+ $result->{$key}= $fo;
+};
+
+=pod
+print STDERR "FOOOOO\n";
+my $result;
+ for my $address (@addresses) {
+eval {
+ alarm(200);
+ print STDERR "waiting for $address.\n" if $VERBOSE;
+ my $fh = $FH{$address};
+ undef $/;
+ my $res = <$fh>;
+ $FH{$address}->close;
+
+ my $VAR1;
+ my $fo = eval ($res);
+ $result->{$address} = $fo;
+ $VAR1 = undef;
+ print STDERR "$address READ.\n" if $VERBOSE;
+};
+ };
+ alarm(0);
+if ($@) {
+ die $@ unless $@ eq "alarm\n";
+};
+
+=cut
+
+
+#my $kid;
+#do {
+# $kid = waitpid(-1,WNOHANG);
+#} until $kid == -1;
+
+
+my $data;
+for my $remailer (keys %$result) {
+ my $d;
+ $d->{'address'} = $remailer;
+ $d->{'nick'} = $NICKS{$remailer} || 'N/A';
+ $d->{'mx'} = $result->{$remailer};
+ push @{$data}, $d;
+};
+
+print Data::Dumper->Dump( [$data] );