3 ######################################################################
4 # make-ssh-known-hosts.pl -- Make ssh-known-hosts file
5 # Copyright (c) 1995 Tero Kivinen
8 # Make-ssh-known-hosts is distributed in the hope that it will be
9 # useful, but WITHOUT ANY WARRANTY. No author or distributor accepts
10 # responsibility to anyone for the consequences of using it or for
11 # whether it serves any particular purpose or works at all, unless he
12 # says so in writing. Refer to the GNU General Public License for full
15 # Everyone is granted permission to copy, modify and redistribute
16 # make-ssh-known-hosts, but only under the conditions described in
17 # the GNU General Public License. A copy of this license is supposed to
18 # have been given to you along with make-ssh-known-hosts so you can
19 # know your rights and responsibilities. It should be in a file named
20 # gnu-COPYING-GPL. Among other things, the copyright notice and this notice
21 # must be preserved on all copies.
22 ######################################################################
23 # Program: make-ssh-known-hosts.pl
27 # (C) Tero Kivinen 1995 <Tero.Kivinen@hut.fi>
29 # Creation : 19:52 Jun 27 1995 kivinen
30 # Last Modification : 00:07 Jul 8 1998 kivinen
31 # Last check in : $Date$
32 # Revision number : $Revision$
37 # Description : Make ssh-known-host file from dns data.
40 # Revision 1.1 2000/03/15 01:13:03 damien
41 # - Created contrib/ subdirectory. Included helpers from Phil Hands'
42 # Debian package, README file and chroot patch from Ricardo Cerqueira
44 # - Moved gnome-ssh-askpass.c to contrib directory and reomved config
46 # - Slight cleanup to doc files
48 # Revision 1.6 1998/07/08 00:44:23 kivinen
49 # Fixed to understand bind 8 nslookup output.
51 # Revision 1.5 1998/04/30 01:53:33 kivinen
52 # Moved kill before close and added sending SIGINT first and
53 # then 1 second sleep before sending SIGKILL.
55 # Revision 1.4 1998/04/17 00:39:19 kivinen
56 # Changed to close ssh program filedescriptor before killing it.
57 # Removed ^ from the password matching prompt.
59 # Revision 1.3 1997/04/17 04:21:27 kivinen
60 # Changed to use 3des by default.
62 # Revision 1.2 1997/03/26 07:14:01 kivinen
65 # Revision 1.1.1.1 1996/02/18 21:38:10 ylo
66 # Imported ssh-1.2.13.
68 # Revision 1.4 1995/10/02 01:23:45 ylo
69 # Ping packet size fixes from Kivinen.
71 # Revision 1.3 1995/08/29 22:37:39 ylo
72 # Now uses GlobalKnownHostsFile and UserKnownHostsFile.
74 # Revision 1.2 1995/07/15 13:26:37 ylo
75 # Changes from kivinen.
77 # Revision 1.1.1.1 1995/07/12 22:41:05 ylo
82 # If you have any useful modifications or extensions please send them to
85 ######################################################################
97 $command_line = "$0 ";
99 $command_line .= $a . " ";
101 STDERR->autoflush(1);
103 ######################################################################
104 # default values for options
109 $public_key = '/etc/ssh_host_key.pub';
110 $private_ssh_known_hosts = "/tmp/ssh_known_hosts$$";
113 $passwordtimeout = undef;
115 $domainnamesplit = 0;
118 ######################################################################
119 # Programs and their options
121 $nslookup = "nslookup";
123 $ssh="ssh -a -c 3des -x -o 'ConnectionAttempts 1' -o 'FallBackToRsh no' -o 'GlobalKnownHostsFile /dev/null' -o 'KeepAlive yes' -o 'StrictHostKeyChecking no' -o 'UserKnownHostsFile $private_ssh_known_hosts'";
124 $sshdisablepasswordoption="-o 'BatchMode yes' -o 'PasswordAuthentication no'";
126 ######################################################################
127 # Cleanup and initialization
129 unlink($private_ssh_known_hosts);
130 $sockaddr = 'S n a4 x8';
131 ($junk, $junk, $sshport) = getservbyname("ssh", "tcp");
132 if (!defined($sshport)) {
135 ($tcpprotoname, $junk, $tcpproto) = getprotobyname('tcp');
136 defined($tcpprotoname) || die "getprotobyname : $!";
138 ######################################################################
141 GetOptions("initialdns=s", "server=s", "subdomains=s",
142 "debug=i", "timeout=i", "passwordtimeout=i",
143 "trustdaemon!", "domainnamesplit", "silent",
144 "nslookup=s", "pingtimeout=i", "recursive!",
147 || die "Getopt : $!";
149 if (defined($opt_initialdns)) { $defserver = $opt_initialdns; }
151 if (defined($opt_server)) { $server = $opt_server; }
153 if (defined($opt_subdomains)) { @subdomains = split(/,/, $opt_subdomains); }
155 if (defined($opt_debug)) { $debug = $opt_debug; }
157 if (defined($opt_timeout)) { $timeout = $opt_timeout; }
159 if (defined($opt_pingtimeout)) { $ping_timeout = $opt_pingtimeout; }
161 if (defined($opt_passwordtimeout)) {
162 $passwordtimeout = $opt_passwordtimeout;
163 $sshdisablepasswordoption = '';
166 if (defined($opt_trustdaemon)) { $trustdaemon = $opt_trustdaemon; }
168 if (defined($opt_recursive)) { $recursive = $opt_recursive; }
170 if (defined($opt_domainnamesplit)) { $domainnamesplit = $opt_domainnamesplit; }
172 if (defined($opt_silent)) { $bell = ''; }
174 if (defined($opt_nslookup)) { $nslookup = $opt_nslookup; }
176 if (defined($opt_ssh)) { $ssh = $opt_ssh; } else {
177 $ssh = "$ssh $sshdisablepasswordoption";
181 $domain = "\L$ARGV[0]\E";
184 } elsif ($#ARGV == 1) {
185 $domain = "\L$ARGV[0]\E";
186 $grep_yes = $ARGV[1];
188 } elsif ($#ARGV == 2) {
189 $domain = "\L$ARGV[0]\E";
190 $grep_yes = $ARGV[1];
193 print(STDERR "$0 [--initialdns initial_dns_server] [--server dns_server] [--subdomains sub.sub.domain,sub.sub,sub,] [--debug debug_level] [--timeout ssh_exec_timeout_in_secs] [--pingtimeout ping_timeout_in_secs] [--passwordtimeout timeout_for_password_in_secs] [--notrustdaemon] [--norecursive] [--domainnamesplit] [--silent] [--keyscan] [--nslookup path_to_nslookup] [--ssh path_to_ssh] full.domain [ host_info_take_regexp [ host_info_remove_regex ]]\n");
197 ######################################################################
198 # Check that ssh program exists
200 if (system("$ssh > /dev/null 2>&1 ") != 256) {
201 print(STDERR "Error: Could not run ssh program ($ssh): $!\nError: Try giving the path to it with --ssh option\n");
205 ######################################################################
206 # Generate subdomains list
208 if (!$domainnamesplit) {
209 debug(6, "Auto splitting host entries");
210 } elsif (!defined(@subdomains)) {
211 debug(6, "Generating subdomain list");
213 # split domain to pieces
214 @domain_pieces = split(/\./, $domain);
216 # add empty domain part
217 push(@subdomains, '');
219 # add rest parts, except the one before full domain name
221 for(; $#domain_pieces > 1; ) {
222 $entry .= "." . shift(@domain_pieces);
223 push(@subdomains, $entry);
226 # add full domain name
227 push(@subdomains, ".$domain");
228 debug(5, "Subdomain list: " . join(',', @subdomains));
230 debug(5, "Using given subdomain list:" . join(',', @subdomains));
233 ######################################################################
234 # finding SOA entry for domain
237 if (!defined($server)) {
238 debug(6, "Finding DNS database SOA entry");
240 ($server, @other_servers) = find_soa($domain, $defserver);
242 if (!defined($server)) {
243 print(STDERR "Error: Could not find DNS SOA entry from default dns server\nError: Try giving the initial nameserver with --initialdns option\n");
246 debug(5, "DNS server found : $server");
249 debug(5, "Using given DNS server : $server");
252 ######################################################################
255 ($name, $junk, $junk, $junk, $junk, $junk, $gecos) = getpwuid($<);
258 if (!defined($opt_keyscan)) {
259 print(STDOUT "# This file is generated with make-ssh-known-hosts.pl\n");
260 print(STDOUT "#$version\n");
261 print(STDOUT "# with command line :\n");
262 print(STDOUT "# $command_line\n");
264 print(STDOUT "# The script was run by $gecos ($name) at " . localtime() . "\n");
265 print(STDOUT "# using perl ($^X) version $].\n");
268 ######################################################################
269 # Get DNS database list from server
272 $domains_done{$domain} = 1;
273 delete $domains_waiting{$domain};
284 debug(1, "Getting DNS database for $domain from server $server");
285 open(DNS, "echo ls -d $domain | nslookup - $server 2>&1 |") ||
286 die "Error: Could not start nslookup to make dns list : $!\nError: Try giving --nslookup option and telling the path to nslookup program\n";
291 undef $hostname if/^\s*$/;
292 if (/^\s{0,1}([a-zA-Z0-9-]\S*)/) {
293 $hostname = "\L$1\E";
295 next unless defined $hostname;
296 if (/^.*\s(SOA)\s+(.*)\s*$/ || $hostname eq "SOA") {
297 undef $soa if(/^.*\s(SOA)\s+(.*)\s*$/);
298 $data = $_ if ($hostname eq "SOA");
299 $data = $2 unless $hostname eq "SOA";
300 $data =~ s/\s*;.*$//;
303 $soa .= " \L$data\E";
308 } elsif (/^.*\s(A|CNAME|NS)\s+(.*)\s*$/) {
312 debug(70, "Line = /$host/$field/$data/");
313 if ($host !~ /\.$/) {
319 if ($host =~ /$domain$/) {
320 if (defined($host{$host})) {
321 $host{$host} .= ",$data";
323 $host{$host} = "$data";
326 debug(30, "$host A == $host{$host}");
328 } elsif ($field eq "cname") {
329 if ($data !~ /\.$/ && ! /^\s/ ) {
334 if ($host =~ /$domain$/) {
335 if (defined($cname{$data})) {
336 $cname{$data} .= ",$host";
338 $cname{$data} = "$host";
341 debug(30, "$host CNAME $data");
346 } elsif ($field eq "ns") {
347 if (!defined($domains_done{$host})) {
348 if (!defined($domains_waiting{$host})) {
349 debug(10, "Adding subdomain $host to domains list, with NS $data");
350 $domains_waiting{$host} = $data;
351 push(@domains_waiting, $host);
353 debug(10, "Adding NS $data for domain $host");
354 $domains_waiting{$host} .= ",$data";
358 if (!defined($hostdata{$host})) {
359 $hostdata{$host} = "$host\n$field=$data\n";
361 $hostdata{$host} .= "$field=$data\n";
366 if ($hostcnt == 0 && $cnamecnt == 0) {
367 if ($#other_servers != -1) {
368 $server = shift(@other_servers);
372 debug(1, "Found $hostcnt hosts, $cnamecnt CNAMEs (total $lines lines)");
373 if (!defined($opt_keyscan)) {
375 print(STDOUT "# Domain = $domain, server = $server\n");
376 print(STDOUT "# Found $hostcnt hosts, $cnamecnt CNAMEs (total $lines lines)\n");
377 print(STDOUT "# SOA = $soa\n");
381 ######################################################################
382 # Loop through hosts and try to connect to hosts
384 foreach $i (sort (keys %host)) {
385 debug(50, "Host = $i, Hostdata = $hostdata{$i}");
386 if ($hostdata{$i} =~ /$grep_yes/im &&
387 $hostdata{$i} !~ /$grep_no/im &&
388 $i !~ /^localhost\./ &&
389 $host{$i} !~ /^127.0.0.1$|^127.0.0.1,|,127.0.0.1$|,127.0.0.1,/) {
390 debug(2, "Trying host $i");
393 if (defined($cname{$i})) {
394 expand($i, \@hostnames, \@subdomains);
395 foreach $j (split(/,/, $cname{$i})) {
396 expand($j, \@hostnames, \@subdomains);
399 expand($i, \@hostnames, \@subdomains);
401 foreach $j (split(/,/, $host{$i})) {
402 push(@hostnames, $j);
404 $hostnames = join(',', (@hostnames));
406 if (defined($opt_keyscan)) {
407 printf(STDOUT "$host{$i}\t$hostnames\n");
408 } elsif (try_ping($i, $host{$i})) {
410 $err = 'Timeout expired';
411 $ssh_key = try_ssh("$i");
412 if (!defined($ssh_key)) {
413 $ssh_key = find_host_from_known_hosts($i);
416 if (defined($ssh_key)) {
418 debug(2, "Ssh to $i succeded");
420 debug(2, "Ssh to $i failed, using local known_hosts entry");
422 debug(4, "adding entries : $hostnames");
423 $ssh_key =~ s/root@//i;
424 if (!$trusted && !$trustdaemon) {
425 print(STDOUT "# $hostnames $ssh_key\n");
427 print(STDOUT "$hostnames $ssh_key\n");
430 debug(2, "ssh failed : $err");
433 debug(2, "ping failed");
436 debug(10, "Skipped host $i");
440 $domain = shift(@domains_waiting);
441 if (defined($domain)) {
442 $server = $domains_waiting{$domain};
443 @other_servers = split(',', $server);
444 $server = shift(@other_servers);
445 ($server, @other_servers) = find_soa($domain, $server);
446 if(!defined($server)) {
447 debug(1, "Skipping domain $domain because no DNS SOA entry found");
448 $domains_done{$domain} = 1;
449 delete $domains_waiting{$domain};
453 } while ($recursive && defined($domain));
455 unlink($private_ssh_known_hosts);
458 ######################################################################
459 # try_ping -- try to ping to host and return 1 if success
460 # $success = try_ping($host, $list_ip_addrs);
463 my($host, $ipaddrs) = @_;
464 my(@ipaddrs, $ipaddr, $serv, $ip);
465 my($rin, $rout, $win, $wout, $nfound, $tmout, $buf, $len, $ret, $err);
468 debug(51,"Trying to ping host $host");
469 @ipaddrs = split(/,/, $ipaddrs);
471 while ($ipaddr = shift(@ipaddrs)) {
473 debug(55,"Trying ipaddr $ipaddr");
476 socket(PING, PF_INET, SOCK_STREAM, $tcpproto) ||
477 die "socket failed : $!";
478 setsockopt(PING, SOL_SOCKET, SO_REUSEADDR, 1) ||
479 die "setsockopt failed : $!";
481 fcntl(PING, F_SETFL, fcntl(PING, F_GETFL, 0) | POSIX::O_NONBLOCK) ||
482 die "fcntl failed : $!";
484 $ip = pack('C4', split(/\./, $ipaddr, 4));
485 $serv = pack($sockaddr, AF_INET, $sshport, $ip);
489 $ret = connect(PING, $serv);
492 debug(60, "Connect failed : $err");
496 # socket not yet connected, wait for result, it will
497 # wake up for writing when done
498 $tmout = $ping_timeout;
502 vec($rin, fileno(PING), 1) = 1;
503 vec($win, fileno(PING), 1) = 1;
504 debug(60, "Waiting in select, rin = " . unpack('H*', $rin) .
505 ", win = " . unpack('H*', $win));
506 ($nfound) = select($rout = $rin, $wout = $win, undef, $tmout);
508 debug(80, "Select returned $nfound, rout = " . unpack('H*', $rout) .
509 ", wout = " . unpack('H*', $wout));
511 # connect done, read the status with sysread
512 $ret = sysread(PING, $buf, 1);
514 if (defined($ret) || $err == EAGAIN || $err == EWOULDBLOCK) {
515 debug(60, "Select ok, read ok ($err), returning ok");
516 # connection done, return ok
521 # connection failed, try next ipaddr
522 debug(60, "Select ok, read failed : $err, trying next");
526 # timeout exceeded, try next ipaddr
527 debug(60, "Select failed : $err, trying next");
531 # connect succeeded, return ok.
532 debug(60, "Connect ok, returning ok");
538 debug(60, "Returning fail");
542 ######################################################################
543 # try_ssh -- try ssh connection to host and return ssh_key if success
544 # if failure return undef, and set $err string to contain error message.
545 # $ssh_key = try_ssh($host);
549 my($buf, $ret, $pos, $pid, $rin, $nfound, $tmout);
551 $pid = open(SSH, "$ssh $host cat $public_key 2>&1 |");
555 $err = "could not open ssh connection to host";
562 debug(10, "Starting ssh select loop");
567 vec($rin, fileno(SSH), 1) = 1;
568 ($nfound, $tmout) = select($rin, undef, undef, $tmout);
572 debug(20, "Ssh select timed out");
573 kill(2, $pid); sleep(1); kill(9, $pid);
575 $err = "Timeout expired";
579 $ret = sysread(SSH, $buf, 256, $pos);
582 # Yes, close the pipe and return
584 debug(20, "Ssh select closed status = $?");
585 $err = "No reply from ssh";
589 while ($buf =~ /^(.*)\n\r?([\000-\377]*)$/) {
593 debug(20, "Ssh select loop, line = \"$_\"");
594 if (/^connection.*refused/i) {
595 $err = "connection refused";
596 } elsif (/^permission/i) {
597 $err = "permission denied";
598 } elsif (/$public_key.*no\s+file/i) {
599 $err = "$public_key file not found";
600 } elsif (/$public_key.*permission\s+denied/i) {
601 $err = "$public_key file permission denied";
602 } elsif (/^\d+\s+\d+\s+\d/) {
603 kill(2, $pid); sleep(1); kill(9, $pid);
608 kill(2, $pid); sleep(1); kill(9, $pid);
613 if ($buf =~ /password: $/i) {
614 if (defined($passwordtimeout)) {
615 $tmout = $passwordtimeout;
616 print(STDERR "$bell\n\rPassword: ");
629 ######################################################################
630 # find_hosts_from_known_hosts -- find host key from private known_hosts file
631 # $ssh_key = find_host_from_known_hosts($host);
633 sub find_host_from_known_hosts {
635 open(KNOWNHOSTS, "<$private_ssh_known_hosts") || return undef;
636 while(<KNOWNHOSTS>) {
637 @_ = split(/\s+/, $_);
638 if ($_[0] =~ /^$host$|^$host,|,$host$/) {
641 return join(' ', @_);
648 ######################################################################
649 # expand -- insert expanded hostnames to hostnames table
650 # expand($hostname, \@hostnames, \@subdomains);
653 my($host, $hostnames, $subdomains) = @_;
654 my($newhost, $sub, $entry);
656 if (!$domainnamesplit) {
659 # split domain to pieces
660 @domain_pieces = split(/\./, $host);
662 # add rest parts, except the one before full domain name
663 $entry = shift(@domain_pieces);
665 debug(20, "Adding autosplit entry $entry");
666 push(@$hostnames, $entry);
668 for(; $#domain_pieces > 1; ) {
669 $entry .= "." . shift(@domain_pieces);
670 debug(20, "Adding autosplit entry $entry");
671 push(@$hostnames, $entry);
673 # add full domain name
674 debug(20, "Adding autosplit entry $host");
675 push(@$hostnames, $host);
677 if ($host =~ /^(.*)$domain$/i) {
679 $newhost =~ s/\.$//g;
680 foreach $sub (@$subdomains) {
681 $entry = $newhost . $sub;
684 debug(20, "Adding entry $entry");
685 push(@$hostnames, $entry);
692 ######################################################################
694 # debug(text_debug_level, string)
697 my($level, $str) = @_;
698 if ($debug > $level) {
699 print(STDERR "$0:debug[$level]: $str\n");
703 ######################################################################
704 # find_soa -- find soa entry for domain
705 # ($soa_origin, @other_servers) = find_soa($domain, $initial_server)
708 my($domain, $initial_server) = @_;
709 my($field, $data, $server, @other_servers);
711 open(DNS, "$nslookup -type=soa $domain $initial_server 2>&1 |") ||
712 die "Error: Could not start nslookup to find SOA entry for $domain : $!\nError: Try giving the path to it with --nslookup option\n";
715 if (/^[^=]*origin\s*=\s*(.*)/) {
717 debug(10, "Found origin : $1");
718 } elsif (/^[^=]*nameserver\s*=\s*(.*)\s*$/) {
719 push(@other_servers, $1);
720 debug(10, "Found nameserver : $1");
724 return($server, @other_servers);
727 ######################################################################
728 # make_perl_happy -- use some symbols, so perl doesn't complain so much
731 sub make_perl_happy {