# dnsspam.pl # By Joost "Garion" Vunderink # # Tool to deal with hostnames that are considered dns pollution. # Features calculation of spam value, kicks and kickbans with the spam value # in the kick reason, and automated kicks/kickbans # if the spam value exceeds a certain threshold. # # IMPORTANT NOTE: # # This is just the irssi interface to the spamcalc script. You MUST get # the spamcalc script itself, or this script WILL NOT WORK. # # Get the latest spamcalc version from http://www.garion.org/spamcalc/test/ # and put this in ~/.irssi/scripts/spamcalc/ # You MUST have version 0.6.0 or higher. # # Read http://www.dnsspam.nl/ to see what dns pollution is. # TODO # - create a delayed whois command for /dnsspam calc # - show message in statuswindow whenever *any* spam host is encountered # - /dnsspam calc #chan while in window without #chan bugs # - add a minimum score for /dnsspam calc # - output to different window (see hideauth.pl says James) # - auto-ignore dnsspamhostppl # - group kicks/bans when dnsspam cleaning # - add timed unbans # - add multi-server support use Irssi; my $irssidir = Irssi::get_irssi_dir(); my $module_file = $irssidir . "/scripts/spamcalc/Dnsspam.pm"; Irssi::print("dnsspam - Testing if spamcalc module is present..."); if (!(-e $module_file)) { Irssi::print("Error loading dnsspam.pl! You MUST have the spamcalc module ". "in " . $irssidir . "/scripts/spamcalc or the script will NOT work.\n". "Go to http://spamcalc.net/ and download the latest ". "spamcalc version from there. You MUST have 0.6.0 or higher. This script ". "will NOT WORK with 0.5 or 0.5.1.\n". "Dnsspam script was NOT loaded successfully."); return 0; } use strict; use vars qw($VERSION %IRSSI); use spamcalc::Dnsspam; $VERSION = '1.0.0'; %IRSSI = ( authors => 'Garion', contact => 'joost@carnique.nl', name => 'dnsspam', description => 'Tools for (automatic) calculation, kicking, and kickbanning of dnsspam hostnames.', license => 'Public Domain', url => 'http://spamcalc.net/', changed => '14 Aug 2002 00:34:38', ); ########################################################################### # Globals my $calc; my $logfile; my %whitelist; ########################################################################### # Prints help sub cmd_dnsspam_help { Irssi::print( "DNSSPAM - Tools to (automatically) deal with dnsspam.\n\n". "Commands:\n\n". "/DNSSPAM CALC \n". " - Calculates the spam value of a hostname, the hostname of a nick, ". "or all nicks in a channel, and prints the result.\n". "/DNSSPAM CLEAN \n". " - Calculates the spam value of this nickname and kickbans it if the ". "hostname is spam. Or, calculates the spam value of all hosts in this ". "channel and kickbans all clients with a dnsspam hostname.\n". "/DNSSPAM KICK \n". " - Calculates the spam value of nick's hostname, and kicks nick with a ". "kick message containing the spam value. Works only if the spam value ". "exceeds the certain threshold.\n". "/DNSSPAM KICKBAN \n". " - Calculates the spam value of nick's hostname, sets a ban on nick's ". "host, and kicks nick with a kick message containing the spam value. ". "Works only if the spam value exceeds the certain threshold.\n". "/DNSSPAM RELOAD\n". " - Reloads the data of the spamcalc part. Use this after you have ". "modified datafiles in the spamcalc dir.\n". "/DNSSPAM STATUS\n". " - Shows some status info about this script.\n\n". "Settings:\n\n". "The script has a lot of settings to influence what's happening with it. ". "Here is the list:\n\n". "dnsspam_enable\n". " - If this is 0, the script will not take any automatic actions.\n". "dnsspam_enable_channel\n". " - Set this to 1 if you want automatic action when somebody joins a ". "channel you are opped in.\n". "dnsspam_enable_query_private\n". " - If enabled, the script will respond to private messages starting with ". "!sc.\n". "dnsspam_enable_query_public\n". " - If enabled, the script will respond to channel messages starting with ". "!sc.\n". "dnsspam_enable_log_hosts\n". " - If enabled, all calculations will be logged to the file ". "in dnsspam_logfile_hosts.\n". "dnsspam_enable_log_action\n". " - If enabled, all automatic actions taken will be logged to the file ". "in dnsspam_logfile_actions.\n". "dnsspam_config\n". " - The location of the spamcalc configuration file.\n". "dnsspam_logfile_hosts\n". " - The file where all calculations will be logged to.\n". "dnsspam_logfile_actions\n". " - The file where all automatic actions will be logged to.\n". "dnsspam_threshold_probable\n". " - The value above which a hostname is probably dnsspam. Used in automatic ". "channel actions.". "dnsspam_threshold_certain\n". " - The value above which a hostname is certainly dnsspam. Used in ". "automatic channel actions.". "dnsspam_channels_kickban\n". " - The list of space separated channels on which clients that join should ". "be kickbanned if their spam value is higher than dnsspam_threshold_kickban. ". "If their spam value is lower than that, but higher ". "than dnsspam_threshold_probable, a warning message is sent into the ". "channel.\n". "dnsspam_channels_public\n". " - The list of space separated channels on which a warning message should ". "be sent into the channel if a client joins with a hostname whose spam value ". "exceeds dnsspam_threshold_probable.\n". "dnsspam_channels_private\n". " - The list of space separated channels on which irssi will display a ". "private message if a client joins with a hostname whose spam value ". "exceeds dnsspam_threshold_probable.\n". "dnsspam_kickreason\n". " - The kickreason. If there is \"%spamvalue\" in the kickreason, this will ". "be replaced by the spam value of the kickee's hostname.\n". "dnsspam_ban_time\n". " - The time after which a ban set by this script should be removed. If set ". "to 0, the ban will not be removed. NOT FUNCTIONAL YET.\n". "\n". "", MSGLEVEL_CRAP); } ########################################################################### # Checks for public !spamcalc queries sub event_public { if (Irssi::settings_get_bool('dnsspam_enable') == 0 || Irssi::settings_get_bool('dnsspam_enable_query_public') == 0) { return; } #if ($data !~ /^!.*/) { return; } my ($server, $data, $nick, $mask, $target) = @_; if ($data =~ /^!(spamcalc|spam|sc) ([0-9a-zA-Z_\.-]+)$/) { if (is_host_whitelisted($2) == 1) { $server->command("msg $target [whitelist] " . cmd_dnsspam_calc($2) . " - " . $2); } else { $server->command("msg $target " . cmd_dnsspam_calc($2) . " - " . $2); } return; } if ($data =~ /^!(spamcalc|spam|sc) (.+)$/) { $server->command("notice $nick Please provide a valid hostname."); return; } if ($data =~ /^!(spamcalc|spam|sc) *$/) { $server->command("notice $nick Please provide a hostname."); return; } # Check spam value of a word if ($data =~ /^!(word|value|penalty) ([0-9a-zA-Z]+)$/ ) { my $pen = $calc->get_word_penalty($2); $server->command("msg $target $pen - $2."); #Irssi::print("$pen - $2."); } } ########################################################################### # Checks for private !spamcalc queries sub event_private { if (Irssi::settings_get_bool('dnsspam_enable') == 0 || Irssi::settings_get_bool('dnsspam_enable_query_private') == 0) { return; } my ($server, $data, $nick, $mask, $target) = @_; if ($data =~ /^!(spamcalc|spam|sc) ([0-9a-zA-Z_\.-]+)$/) { $server->command("notice $nick " . cmd_dnsspam_calc($2) . " - " . $2); return; } if ($data =~ /^!(spamcalc|spam|sc) (.+)$/) { $server->command("notice $nick Please provide a valid hostname."); return; } if ($data =~ /^!(spamcalc|spam|sc) *$/) { $server->command("notice $nick Please provide a hostname."); return; } } ########################################################################### # Check if any dnsspam hostnames have just joined one of the channels you # have enabled. If so, deal with them. sub event_massjoin { if (Irssi::settings_get_bool('dnsspam_enable') == 0 || Irssi::settings_get_bool('dnsspam_enable_channel') == 0) { return; } my ($channel, $nicksList) = @_; my @nicks = @{$nicksList}; my $server = $channel->{'server'}; my $channelName = $channel->{name}; my $actchannel; my @channels_kickban = split(/ /, Irssi::settings_get_str('dnsspam_channels_kickban')); foreach $actchannel (@channels_kickban) { if ($channelName eq $actchannel) { cmd_process_dnsspam($server, $channel, "kickban", @nicks); return; } } my @channels_public = split(/ /, Irssi::settings_get_str('dnsspam_channels_public')); foreach $actchannel (@channels_public) { if ($channelName eq $actchannel) { cmd_process_dnsspam($server, $channel, "public", @nicks); return; } } my @channels_private = split(/ /, Irssi::settings_get_str('dnsspam_channels_private')); foreach $actchannel (@channels_private) { if ($channelName eq $actchannel) { cmd_process_dnsspam($server, $channel, "private", @nicks); return; } } } ########################################################################### # Calculates the spam value of a nick, a hostname or a channel, and sub cmd_dnsspam_calc { my ($host) = @_; # If ipv6-ip, return 0 if ($host =~ /:/) { return 0; } my $val = $calc->calc($host); cmd_log_host($val . " - " . $host); return $val; } ########################################################################### # Calculates the spam value of a nick, a hostname or a channel, and # prints it. # If the argument contains at least one dot, it is considered a hostname; # otherwise, the spam value of the hostname of that nick is calculated. sub cmd_dnsspam_calcandshow { my ($data, $server, $item) = @_; #Irssi::print("Calc of $data"); $data =~ s/[\ ]+//g; return if ($data eq ''); # Calculating the spam value of all nicks in a channel # there are !channels, &channels and +channels too :-) if ($data =~ /^[#\!\&\+]/) { # let choose #channel other than the current window item my $channel = $server->channel_find($data); if (!$channel) { Irssi::print("Not on such channel: $data"); return 1; } my @nicks = $channel->nicks(); my $nick; my $certain = 0; my $probable = 0; my $nonspam = 0; foreach $nick (@nicks) { my $hostmask = $nick->{host}; my $hostname = $hostmask; $hostname =~ s/[^@]+@//; my $val = cmd_dnsspam_calc($hostname); if ($val > Irssi::settings_get_int('dnsspam_threshold_certain')) { Irssi::print("$val - $hostname (" . $nick->{nick} . ")"); $certain++; } elsif ($val > Irssi::settings_get_int('dnsspam_threshold_probable')) { Irssi::print("$val - $hostname (" . $nick->{nick} . ")"); $probable++; } else { $nonspam++; } } my $total = $certain + $probable + $nonspam; Irssi::print("Checked $channel->{name} ($total nicks). Spam: $certain; possible: $probable; no spam: $nonspam."); return 0; # Calculating the spam value of a hostname } elsif ($data =~ /\./) { my $val = cmd_dnsspam_calc($data); Irssi::print($val . " - " . $data); return $val; # Calculating the spam value of the hostname of a nickname } else { $data =~ s/[\ ]+//g; $server->redirect_event("userhost", 1, $data, 0, undef, { "event 302" => "redir dnsspam_userhost"}); $server->send_raw("USERHOST :$data"); } } # Calculating the spam value of the nickname's hostname # taken from friends-shasta.pl =) sub event_dnsspam_userhost { my ($mynick, $reply) = split(/ +/, $_[1]); my ($nick, $user, $host) = $reply =~ /^:?([^\*=]*)\*?=.(.*)@(.*)/; if (defined $nick && defined $user && defined $host) { Irssi::print(cmd_dnsspam_calc($host) . " - " . $host . " [" . $nick . "]"); } else { Irssi::print("No such nick"); } } ########################################################################### # Adds a string to the hosts logfile, if logging has been enabled. sub cmd_log_host { if (Irssi::settings_get_bool('dnsspam_enable_log_hosts') == 0) { return; } my ($data) = @_; my $time = sprintf("%02d:%02d", (localtime(time))[2], (localtime(time))[1]); my $msg = $time . " " . $data . "\n"; my $file = ">>" . Irssi::settings_get_str('dnsspam_logfile_hosts'); open(LOGFILE, $file) or return 1; print LOGFILE ($msg); close(LOGFILE); } ########################################################################### # Adds a string to the action logfile, if logging has been enabled. sub cmd_log_action { if (Irssi::settings_get_bool('dnsspam_enable_log_actions') == 0) { return; } my ($data) = @_; my ($data) = @_; my $time = sprintf("%02d:%02d", (localtime(time))[2], (localtime(time))[1]); my $msg = $time . " " . $data . "\n"; my $file = ">>" . Irssi::settings_get_str('dnsspam_logfile_actions'); open(LOGFILE, $file) or return 1; print LOGFILE ($msg); close(LOGFILE); } ########################################################################### # Kick $nick from $channel with kickreason including the spam value # $nick should be a string, and $server and $channel hash refs. sub cmd_dnsspam_kick { my ($nick, $server, $channel) = @_; my $channelname = $channel->{name}; #my $hostmask = $nick->{host}; my $hostmask = $channel->nick_find($nick)->{host}; my $hostname = $hostmask; $hostname =~ s/[^@]+@//; my $val = $calc->calc($hostname); my $reason = Irssi::settings_get_str('dnsspam_kickreason'); $reason =~ s/%spamvalue/$val/g; if ($val >= Irssi::settings_get_int('dnsspam_threshold_certain')) { $server->send_raw("KICK $channelname $nick :$reason"); } else { Irssi::print("Score of $hostname is $val, which is not above dnsspam threshold. Not kicking $nick."); } } ########################################################################### # Ban the host of $nick, and then kick $nick from $channel with kickreason # including the spam value # $nick must be a string, $server and $channel must be a hash ref. sub cmd_dnsspam_kickban { my ($nick, $server, $channel) = @_; #Irssi::print("kb:" . $data . $server. $channel); my $channelname = $channel->{name}; #my $hostmask = $nick->{host}; my $hostmask = $channel->nick_find($nick)->{host}; my $hostname = $hostmask; $hostname =~ s/[^@]+@//; my $val = $calc->calc($hostname); if ($val >= Irssi::settings_get_int('dnsspam_threshold_certain')) { $server->command("mode $channelname +b *!*\@$hostname"); cmd_dnsspam_kick($nick, $server, $channel); } else { Irssi::print("Score of $hostname is $val, which is not above dnsspam threshold. Not kickbanning $nick."); } } ########################################################################### # Cleans a channel from dnsspam by kickbans, or checks the nickname provided # and kickbans if they have a spammy hostname. sub cmd_dnsspam_clean { my ($data, $server, $item) = @_; #Irssi::print("cl" . $data. $server. $channel); $data =~ s/[\ ]+//g; return if ($data eq ''); # Process a whole channel # there are !channels, &channels and +channels too :-) if ($data =~ /^[#\!\&\+]/) { # let choose #channel other than the current window item my $channel = $server->channel_find($data); if (!$channel) { Irssi::print("Not on such channel: $data"); return 1; } my $channelname = $channel->{name}; Irssi::print("Cleaning channel $channelname."); my @nicks = $channel->nicks(); my $nick; my $total = 0; foreach $nick (@nicks) { my $hostmask = $nick->{host}; my $hostname = $hostmask; $hostname =~ s/[^@]+@//; my $val = cmd_dnsspam_calc($hostname); #Irssi::print($data . "'s hostname is valued $val."); if ($val > Irssi::settings_get_int('dnsspam_threshold_certain')) { cmd_dnsspam_kickban($nick->{nick}, $server, $channel); $total++; } } Irssi::print("Cleaned up $total dnsspam hosts."); } else { my $hostmask = $item->nick_find($data)->{host}; my $hostname = $hostmask; $hostname =~ s/[^@]+@//; my $val = cmd_dnsspam_calc($hostname); #Irssi::print($data . "'s hostname is valued $val."); if ($val > Irssi::settings_get_int('dnsspam_threshold_certain')) { cmd_dnsspam_kickban($data, $server, $item); } } } ########################################################################### # Checks the list of nicks that just joined this channel and takes action # if any of them have dnsspam hostnames. sub cmd_process_dnsspam { my ($server, $channel, $mode, @nicklist) = @_; return unless ($mode eq "kickban" || $mode eq "public" || $mode eq "private"); my $nick; foreach $nick (@nicklist) { my $hostname = $nick->{host}; my $nickname = $nick->{nick}; $hostname =~ s/[^@]+@//; my $spamvalue = cmd_dnsspam_calc($hostname); # certainly spam if ($spamvalue > Irssi::settings_get_int('dnsspam_threshold_certain')) { if ($mode eq "kickban") { cmd_dnsspam_kickban($nick->{nick}, $server, $channel); } if ($mode eq "public") { $server->command("msg $channel->{name} $nickname has a dnsspam hostname ($hostname) with a score of $spamvalue."); } if ($mode eq "private") { Irssi::print("Certain dnsspam in $channel->{name}: $spamvalue - $hostname($nickname)."); } } # possibly spam if ($spamvalue > Irssi::settings_get_int('dnsspam_threshold_probable') && $spamvalue < Irssi::settings_get_int('dnsspam_threshold_certain')) { if ($mode eq "kickban" || $mode eq "public") { $server->command("msg $channel->{name} $nickname has a probable dnsspam host name ($hostname) with a score of $spamvalue."); } if ($mode eq "private") { Irssi::print("Possible dnsspam in $channel->{name}: $spamvalue - $hostname($nickname)."); } } } } ########################################################################### # Reloads spamcalc data sub cmd_dnsspam_reload { Irssi::print("dnsspam - Reloading spamcalc data..."); $calc->loadconfig(Irssi::settings_get_str('dnsspam_config')); $calc->loaddatafiles(); load_whitelist(); Irssi::print("dnsspam - Done."); } ########################################################################### # Shows the status of the script. sub cmd_dnsspam_status { if (Irssi::settings_get_bool('dnsspam_enable')) { Irssi::print("dnsspam - Status: Master switch enabled."); } else { Irssi::print("dnsspam - Status: Master switch disabled."); } if (Irssi::settings_get_bool('dnsspam_enable_channel')) { my $warn = Irssi::settings_get_int('dnsspam_threshold_probable'); my $certain = Irssi::settings_get_int('dnsspam_threshold_certain'); Irssi::print("dnsspam - Channels: probable level is " . $warn . "; certain level is " . $certain . "."); my $chans = Irssi::settings_get_str('dnsspam_channels_kickban'); Irssi::print("dnsspam - Automatic kickbans enabled on: " . $chans); my $duration = Irssi::settings_get_int('dnsspam_ban_time'); Irssi::print("dnsspam - Bans will expire in $duration minutes."); $chans = Irssi::settings_get_str('dnsspam_channels_public'); Irssi::print("dnsspam - Automatic public warnings enabled on: " . $chans); $chans = Irssi::settings_get_str('dnsspam_channels_private'); Irssi::print("dnsspam - Automatic private info enabled on: " . $chans); } else { Irssi::print("dnsspam - Automatic channel action disabled."); } if (Irssi::settings_get_bool('dnsspam_enable_query_public')) { Irssi::print("dnsspam - Public !sc queries enabled."); } else { Irssi::print("dnsspam - Public !sc queries disabled."); } if (Irssi::settings_get_bool('dnsspam_enable_query_private')) { Irssi::print("dnsspam - Private !sc queries enabled."); } else { Irssi::print("dnsspam - Private !sc queries disabled."); } } ########################################################################### # Main dnsspam command sub cmd_dnsspam { my ($data, $server, $item) = @_; if ($data =~ m/^[(calc)|(clean)|(help)|(kick)|(kickban)|(reload)|(status)]/i ) { Irssi::command_runsub ('dnsspam', $data, $server, $item); } else { Irssi::print("Use /dnsspam help for help.") } } ########################################################################### # Tests if a domain is on the whitelist sub is_host_whitelisted { my($hostname) = @_; # Set the @array delimiter to '|' instead of ' ' local $" = '|'; # This will be useful in the whitelist expansion :) my @whitekeys = keys(%whitelist); if ($hostname =~ /\.(?:@whitekeys)$/) { return 1; } #my ($key, $testregexp); #foreach $key (keys(%whitelist)) { # $testregexp = "$key\$"; # if ($hostname =~ /$testregexp/) { # return 1; # } #} #if (defined($whitelist{$hostname})) { # return 1; #} return 0; } ########################################################################### # Loads the whitelist from file sub load_whitelist { undef(%whitelist); my $whitefile = $irssidir . "/scripts/dnsspam_whitelist"; open(F, $whitefile); my @whitelines = ; chop(@whitelines); my $whiteline; foreach $whiteline(@whitelines) { $whiteline =~ s/#.*//; $whiteline =~ s/^\s+|\s+$//; if ($whiteline =~ /[a-zA-Z0-9\.-_]+/) { $whitelist{$whiteline} = 1; } } close(F); } ########################################################################### # Add command bindings Irssi::command_bind('dnsspam', 'cmd_dnsspam'); Irssi::command_bind('dnsspam calc', 'cmd_dnsspam_calcandshow'); Irssi::command_bind('dnsspam clean', 'cmd_dnsspam_clean'); Irssi::command_bind('dnsspam help', 'cmd_dnsspam_help'); Irssi::command_bind('dnsspam kick', 'cmd_dnsspam_kick'); Irssi::command_bind('dnsspam kickban', 'cmd_dnsspam_kickban'); Irssi::command_bind('dnsspam kline', 'cmd_dnsspam_kline'); Irssi::command_bind('dnsspam reload', 'cmd_dnsspam_reload'); Irssi::command_bind('dnsspam status', 'cmd_dnsspam_status'); ########################################################################### # Add signal hooks Irssi::signal_add_last('massjoin', 'event_massjoin'); Irssi::signal_add_last('message public', 'event_public'); Irssi::signal_add_last('message private', 'event_private'); Irssi::signal_add('redir dnsspam_userhost', 'event_dnsspam_userhost'); ########################################################################### # Add settings Irssi::settings_add_str('dnsspam', 'dnsspam_config', "$irssidir/scripts/spamcalc/sc.conf"); Irssi::settings_add_str('dnsspam', 'dnsspam_logfile_hosts', "$irssidir/scripts/dnsspam_hosts.log"); Irssi::settings_add_str('dnsspam', 'dnsspam_logfile_actions', "$irssidir/scripts/dnsspam_actions.log"); Irssi::settings_add_bool('dnsspam', 'dnsspam_enable_log_hosts', 0); Irssi::settings_add_bool('dnsspam', 'dnsspam_enable_log_actions', 1); Irssi::settings_add_bool('dnsspam', 'dnsspam_enable', 1); Irssi::settings_add_bool('dnsspam', 'dnsspam_enable_channel', 0); Irssi::settings_add_bool('dnsspam', 'dnsspam_enable_query_public', 0); Irssi::settings_add_bool('dnsspam', 'dnsspam_enable_query_private', 0); Irssi::settings_add_int('dnsspam', 'dnsspam_threshold_probable', 50); Irssi::settings_add_int('dnsspam', 'dnsspam_threshold_certain', 100); Irssi::settings_add_str('dnsspam', 'dnsspam_channels_private', ""); Irssi::settings_add_str('dnsspam', 'dnsspam_channels_public', ""); Irssi::settings_add_str('dnsspam', 'dnsspam_channels_kickban', ""); Irssi::settings_add_str('dnsspam', 'dnsspam_kickreason', "Your host is dns pollution (score: %spamvalue). Read www.dnsspam.nl and use another host."); Irssi::settings_add_int('dnsspam', 'dnsspam_ban_time', 10); ########################################################################### # Initialization # Init the calculator $calc = Dnsspam->new(); Irssi::print("dnsspam - Loading spamcalc config..."); $calc->loadconfig($irssidir . "/scripts/spamcalc/sc.conf"); Irssi::print("dnsspam - Loading spamcalc datafiles..."); $calc->loaddatafiles(); Irssi::print("dnsspam - Loading whitelist..."); load_whitelist(); Irssi::print("dnsspam - Use /dnsspam help for help."); #open(LOGFILE, ">>" . Irssi::settings_get_str('dnsspam_logfile'); ########################################################################### # Function that will be called when the script is unloaded; this is # necessary to unload the module Dnsspam.pm. sub UNLOAD { Symbol::delete_package("spamcalc::Dnsspam"); }