#!/usr/bin/perl use strict; use warnings; use Net::Traceroute; use Net::DNS::Resolver; use Net::Whois::IP; use Dancer2; use Socket; use File::Fetch; use HTTP::Tiny; use HTTP::Request; use Data::Validate::Domain qw(is_domain); use Data::Validate::IP qw(is_public_ipv4 is_public_ipv6); use MaxMind::DB::Reader; use Archive::Tar; use Term::ANSIColor; use Data::Dumper; ### README ######################################################################################################################################### # This is the client for the XTR (XiCoN Trace Route). It opens a port and listening to HTTP requests. It answers with a traceroute # in a json hash. The hash contains for each hop the IP, the PTR, the request time and the AS (Autonomous System) including the AS name. # # This client also tries to connect to a "master server" which helps keeping track of all the traceroute clients and their availability. # # This software uses the MaxMind GeoLite2 database (https://www.maxmind.com) to determine the AS for each hop. To lookup the AS information the script # uses the MaxMind perl module which uses the MaxMind DB library. # # # This product includes GeoLite2 ASN data created by MaxMind, available from http://www.maxmind.com. ###################################################################################################################################################### ### CHANGELOG ###################################################################################################################################### # v0.8 (2020-04-14) # * FIX: Maxmind now allows redistributing GeoLite2-ASN database if requirements are fulfilled (https://git.xicon.eu/xicon/xtr/issues/3) # * ADD: Maxmind's TOS added as start-up message # * ADD: Added some useful log output (and colors \o/) # * ADD: INSTALL notes now giving hint about "setuid-bit" for traceroute command # # v0.7 (2019-10-18) # * ADD: IPv6 requests are now possible # * ADD: Domain request are now possible (will be resolved to ip address) # * DEL: removed ip2asn database as source for AS lookup (no more SQLite for client) # * ADD: added MaxMind's GeoLite2-ASN database for AS lookup # * FIX: unknown domains or invalid IPs will be reported as a valid result including error message as object # * FIX: cleaned up source code by moving more code into funtions # # v0.6 (2019-08-22) # * FIX: replaced bug-driven LWP::* with HTTP::Tiny # * FIX: client_protocol_version moved from "3" to "4" # # v0.5 (2019-08-18) # * FIX: dropped "timeout" option of Net::Traceroute (details -> https://rt.cpan.org/Ticket/Display.html?id=107066) # * FIX: altered dependency on which an output will be send ("$tr->found" -> "$tr->hops > 1") # # v0.4 (2019-08-13) # * ADD: added better documentation # * ADD: updated API to v3 # * FIX: limied inputs to ipv4 addresses only (no more hostnames for now in requests) ###################################################################################################################################################### ### INSTALL ######################################################################################################################################## # This is a standalone software which usually runs in the "foreground". Starting it in a screen session or via init.d/systemd/rc.d is # highly recommended. For testing, starting this script in a screen session is also fine. # # To run properly as a non-root user you need to set the setuid-bit on the traceroute binary of your system: # > chmod 4755 /usr/bin/traceroute # Depending on your system /usr/bin/traceroute could just be a soft link, but you have to set the setuid to the real binary! # # # Either install all listed modules with "cpan -i " or use your system's package manager (apt, yum, yast). # # On Debian just install these packages: # wget # screen # libnet-whois-ip-perl # libdancer2-perl # libnet-dns-perl # libnet-traceroute-perl # libdata-validate-ip-perl # libdata-validate-domain-perl # build-essential # cpanminus # libmaxminddb0 # libmaxminddb-dev # # After that, execute this command to install maxmind database reader module: # cpanm -q -n MaxMind::DB::Reader::XS # ###################################################################################################################################################### ### VARS ########################################################################################################################################### my $VERSION = "0.8"; my $dbfile = 'GeoLite2-ASN.mmdb'; my $db_source = 'http://xtr.xicon.eu/GeoLite2-ASN.tar.gz'; my $master_server = 'xtr-master.xicon.eu'; my $get_my_ip_service = 'http://ipv4.xicon.eu/'; # some service, which returns just the ip of the requesting host (in this case, us.) my $client_protocol_version = 4; my $my_ip = get_my_ipv4($get_my_ip_service); my $my_ext_ip = ""; # set different ip, if access differs from default public ip my $my_port = 12111; my $my_ext_port = 0; # set different port, if public access port differs from app port ###################################################################################################################################################### ###################################################################################################################################################### ### NOTHING TO CHANGE BELOW HERE ################################################################################################################### ###################################################################################################################################################### ### Required mentioning of Maxmind's TOS (https://git.xicon.eu/xicon/xtr/issues/3) print_maxmind_tos(); ### set vars set port => $my_port; if($my_ext_ip eq "") { $my_ext_ip = $my_ip; } if($my_ext_ip eq "0.0.0.0") { print colored(['red'], "[ERR] Couldn't determine my own IP. Exiting...\n"); exit 1; } if($my_ext_port eq 0 || $my_ext_port eq "") { $my_ext_port = $my_port; } print colored(['green'], "[INFO] ") . color('reset'); print "External IP: ".$my_ext_ip."\n"; print colored(['green'], "[INFO] ") . color('reset'); print "External Port: ".$my_ext_port."\n"; get_maxmind_db($db_source,$dbfile); ### connect to database my $maxmind_reader = MaxMind::DB::Reader->new( file => $dbfile ); ### hook for HTTP "security" hook 'before' => sub { header 'Access-Control-Allow-Origin' => '*'; }; ### main get route get '/v3/client/request/:host' => sub { my $host = route_parameters->get('host') || 8.8.8.8; if(!is_public_ipv4($host) and !is_public_ipv6($host) and !is_domain($host)) { #status('bad_request'); my @array; push(@array, ["NO","ANSWER","FOUND",":(",""]); return encode_json \@array } my $trace = traceit($maxmind_reader,$host); return encode_json $trace; }; ### return client protocol verison get '/v3/client/info/version' => sub { return $client_protocol_version; }; ### give the master server the info, that we are available if(send_server_status($master_server,$my_ext_ip,$my_ext_port,"1")) { print colored(['green'], "[INFO] ") . color('reset'); print "Sending the master server ".$master_server." the info, that we are online.\n"; } else { print colored(['red'], "[ERR] ") . color('reset'); print "Failed to send our status to the master server ".$master_server." - nobody knows we are online :(\n"; } ### catch "INT" signal to send the status to the master server $SIG{'INT'} = sub { send_server_status($master_server,$my_ext_ip,$my_ext_port,"0"); exit; }; ### start the main loop start; ### if you reach this, you're done exit; sub send_server_status { my ($master_server,$hostname,$port,$status) = @_; my $url = "http://".$master_server."/v3/server/add/".$hostname."/".$port."/".$status; my $httptiny = HTTP::Tiny->new("timeout" => 10); my $response = $httptiny->request("PUT",$url); return $response->{'success'}; } sub get_my_ipv4 { my ($ip_service) = @_; my $httptiny = HTTP::Tiny->new("timeout" => 10); my $response = $httptiny->request("GET",$ip_service); if(is_public_ipv4($response->{'content'})) { return $response->{'content'}; } else { # no valid ip returned from ip service return "0.0.0.0"; } } sub traceit { my ($maxmind_reader,$host) = @_; my @array = (); if(is_domain($host)) { if(resolve_dns($host,"4") eq "" and resolve_dns($host,"6") eq "") { push(@array, ["NO","ANSWER","FOUND",":(",""]); return \@array; } } my $tr = Net::Traceroute->new(host => $host, use_icmp => 1); my $hops = $tr->hops; if($hops > 1) { foreach my $hop (@{$tr->{'hops'}}) { my $sum = sprintf("%.1f",($hop->[0]->[2] + $hop->[1]->[2] + $hop->[2]->[2])/3); my $answer = resolve_dns($hop->[0]->[1],"PTR"); ### get AS to IP my $as = 0; my $as_name = " "; if($hop->[0]->[1] ne "255.255.255.255") { my $as_data = find_as($maxmind_reader, $hop->[0]->[1]); if(defined($as_data->{'autonomous_system_number'})) { $as = $as_data->{'autonomous_system_number'}; } if(defined($as_data->{'autonomous_system_organization'})) { $as_name = $as_data->{'autonomous_system_organization'}; } } else { $hop->[0]->[1] = "???"; } push(@array, [$hop->[0]->[1],$answer,$sum,$as,$as_name]); } return \@array; } else { push(@array, ["NO","ANSWER","FOUND",":(",""]); return \@array; } } sub resolve_dns { my ($host,$type) = @_; if($type eq 4) { $type = "A"; } elsif($type eq 6) { $type = "AAAA"; } elsif($type eq "PTR") { $type = "PTR"; } else { return ""; } my $res = Net::DNS::Resolver->new; my $query = $res->query($host, $type); my $answer = ""; if($query) { foreach my $rr ($query->answer) { if($rr->type eq "A") { $answer = $rr->address; } if($rr->type eq "AAAA") { $answer = $rr->address_short; } if($rr->type eq "PTR") { $answer = $rr->ptrdname; } } } return $answer; } sub resolve_dns_reverse { my ($host) = @_; my $res = Net::DNS::Resolver->new; my $query = $res->query($host, 'PTR'); my $answer = " "; if($query) { foreach my $rr ($query->answer) { if($rr->type eq "PTR") { $answer = $rr->ptrdname; } } } return $answer; } sub find_as { my ($maxmind_reader, $ip) = @_; my $record = $maxmind_reader->record_for_address($ip); return $record; } sub get_maxmind_db { my ($db_source,$dbfile) = @_; print colored(['green'], "[INFO] ") . color('reset'); print "Fetching latest IP2ASN database: ".$db_source."\n"; my $ff = File::Fetch->new(uri => $db_source); my $temp_file = $ff->fetch( to => '/tmp' ); my $tar = Archive::Tar->new; $tar->read($temp_file); my @files = $tar->get_files(); foreach my $file (@files) { if($file->{'name'}=~/${dbfile}$/) { open(FILE,">",$dbfile) or die colored(['red'], "[ERR] Can't open file for writing: $!"); print FILE $file->{'data'}; close(FILE); } } $tar->clear; unlink($temp_file); } sub print_maxmind_tos { print qq{ --------------------------------------------------------------------------------------------------------------------------------------- }; print "\n"; print qq{ This product includes GeoLite2 ASN data created by MaxMind, available from http://www.maxmind.com. }; print "\n"; print qq{ --------------------------------------------------------------------------------------------------------------------------------------- }; print "\n"; print "\n"; }