| =head1 NAME |
| |
| Mail::SpamAssassin::Plugin::SIQ version: 20060305 |
| |
| =head1 SYNOPSIS |
| |
| loadplugin Mail::SpamAssassin::Plugin::SIQ [/path/to/SIQ.pm] |
| |
| siq_server db.outboundindex.net:6264 |
| |
| siq_server_ttl db.outboundindex.net:6264 300 |
| |
| siq_oi_workaround 0 |
| |
| siq_query_timeout 5 |
| |
| siq_skip_domain example.com |
| |
| siq_skip_ip 1.2.3.4 |
| |
| header SIQ_OI_00 eval:siq_score('db.outboundindex.net',0,0) |
| score SIQ_OI_00 1.5 |
| describe SIQ_OI_00 Outbound Index Reputation: http://outboundindex.org/ |
| tflags SIQ_OI_00 net |
| priority SIQ_OI_00 900 |
| |
| header SIQ_OI_IP_01 eval:siq_ip_score('db.outboundindex.net',1,1) |
| score SIQ_OI_IP_01 1.0 |
| describe SIQ_OI_IP_01 Outbound Index IP Reputation: http://outboundindex.org/ |
| tflags SIQ_OI_IP_01 net |
| priority SIQ_OI_IP_01 900 |
| |
| header SIQ_OI_DOM_50 eval:siq_domain_score('db.outboundindex.net',50,59) |
| score SIQ_OI_DOM_50 0.1 |
| describe SIQ_OI_DOM_50 Outbound Index Domain Reputation: http://outboundindex.org/ |
| tflags SIQ_OI_DOM_50 net |
| priority SIQ_OI_DOM_50 900 |
| |
| header SIQ_OI_REL_01 eval:siq_relative_score('db.outboundindex.net',1,1) |
| score SIQ_OI_REL_01 1.0 |
| describe SIQ_OI_REL_01 Outbound Index Relative Reputation: http://outboundindex.org/ |
| tflags SIQ_OI_REL_01 net |
| priority SIQ_OI_REL_01 900 |
| |
| header SIQ_OI_CONF_01 eval:siq_confidence('db.outboundindex.net',1,1) |
| score SIQ_OI_CONF_01 1.0 |
| describe SIQ_OI_CONF_01 Outbound Index Confidence: http://outboundindex.org/ |
| tflags SIQ_OI_CONF_01 net |
| priority SIQ_OI_CONF_01 900 |
| |
| header SIQ_OI_STAB_1 db.outboundindex.net:6264 =~ /stability=1\./ |
| score SIQ_OI_STAB_1 0.5 |
| describe SIQ_OI_STAB_1 Outbound Index stability value of 1 |
| tflags SIQ_OI_STAB_1 net |
| priority SIQ_OI_STAB_1 901 |
| |
| =head1 DESCRIPTION |
| |
| This plugin queries for reputation data, based on domain & IP pairs, from a |
| reputation service provider using the IETF ASRG draft SIQ protocol: |
| |
| http://www.ietf.org/internet-drafts/draft-irtf-asrg-iar-howe-siq-02.txt |
| |
| A number of eval functions are provided for writing eval-type rules against |
| the reputation data returned by the reputation service queried. |
| |
| A pseudo-header is also provided for testing of the optional text area in an |
| SIQ response. |
| |
| =head1 AUTHOR |
| |
| Daryl C. W. O'Shea, DOS Technologies <spamassassin@dostech.ca> |
| |
| =head1 COPYRIGHT |
| |
| Copyright (c) 2006 Daryl C. W. O'Shea, DOS Technologies. All rights reserved. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| =head1 NOTICE |
| |
| Built-in caching is used, so queries against the same domain and IP pair will |
| not incur the expense (both time and reputation service provider charges) of |
| an additional query. Note that each SpamAssassin child process maintains its |
| own idependent cache which is not shared with other children and lasts only |
| for the lifetime of the current child. The cache life time is configurable. |
| |
| =head1 PRIVACY CONCERNS |
| |
| As with any third-party data service used to classify email, use of services |
| utilizing the SIQ protocol has inherent privacy implications. Many/most |
| reputation services use aggregated data from their query logs as a part of |
| their reputation calculations. With the data provided (domain and IP pairs) |
| by a query client, such as this plugin, a reputation service provider could |
| estimate your email volume, a breakdown of email domains sending mail to your |
| systems, and etc. |
| |
| Depending on your DNS setup, use of services using the SIQ protocol might not |
| impose privacy concerns greater than those already imposed by the use of DNS |
| based IP and/or URI blacklists (or whitelists). |
| |
| =cut |
| |
| package Mail::SpamAssassin::Plugin::SIQ; |
| |
| use Mail::SpamAssassin::Plugin; |
| use Mail::SpamAssassin::Logger; |
| use strict; |
| use warnings; |
| use bytes; |
| |
| use Socket; |
| use IO::Socket; |
| use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK); |
| |
| our @ISA = qw(Mail::SpamAssassin::Plugin); |
| |
| |
| sub new { |
| my $class = shift; |
| my $mailsaobject = shift; |
| |
| $class = ref($class) || $class; |
| my $self = $class->SUPER::new($mailsaobject); |
| bless ($self, $class); |
| |
| if ($mailsaobject->{local_tests_only}) { |
| $self->{disabled} = 1; |
| } else { |
| $self->{disabled} = 0; |
| } |
| |
| $self->register_eval_rule("siq_score"); |
| $self->register_eval_rule("siq_ip_score"); |
| $self->register_eval_rule("siq_domain_score"); |
| $self->register_eval_rule("siq_relative_score"); |
| $self->register_eval_rule("siq_confidence"); |
| |
| $self->set_config($mailsaobject->{conf}); |
| |
| return $self; |
| } |
| |
| |
| sub set_config { |
| my($self, $conf) = @_; |
| my @cmds = (); |
| |
| =head1 USER PREFERENCES |
| |
| =over 4 |
| |
| =item siq_skip_domain example.com (default: none) |
| |
| A list of domain name patterns to exclude from SIQ queries. Normal shell |
| wild cards may be used, similar to those used in <C>whilelist_from entries. |
| |
| Multiple domain name patterns per line are permitted, as are multiple lines. |
| |
| Example: |
| siq_skip_domain example.com *.example.com |
| siq_skip_domain *.apache.org |
| |
| =cut |
| |
| push (@cmds, { |
| setting => 'siq_skip_domain', |
| default => {}, |
| code => sub { |
| my ($self, $key, $value, $line) = @_; |
| if ($value =~ /^$/) { |
| return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; |
| } |
| if ($value !~ /^[-.*?\w\s]+$/) { |
| return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
| } |
| foreach my $domain (split(/\s+/, $value)) { |
| my $pattern = $domain; |
| $domain =~ s/\./\\\./g; |
| $domain =~ s/\?/\./g; |
| $domain =~ s/\*/\.\*/g; |
| $self->{siq_skip_domain}->{lc $domain} = $pattern; |
| } |
| } |
| }); |
| |
| =item siq_skip_ip 192.168.123.* (default: none) |
| |
| A list of ip patterns to exclude from SIQ queries. Normal shell wild cards |
| may be used, similar to those used in <C>whilelist_from entries. |
| |
| Multiple ip patterns per line are permitted, as are multiple lines. |
| |
| Example: |
| siq_skip_ip 192.168.123.* 127.* |
| siq_skip_ip 10.1.* |
| |
| <b>Note: Currently only file-glob style wildcards are supported. CIDR |
| notation, nor any other format, is <b> NOT supported. |
| |
| =cut |
| |
| push (@cmds, { |
| setting => 'siq_skip_ip', |
| default => {}, |
| code => sub { |
| my ($self, $key, $value, $line) = @_; |
| if ($value =~ /^$/) { |
| return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; |
| } |
| if ($value !~ /^[\.\*\?0-9\s]+$/) { |
| return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
| } |
| foreach my $ip (split(/\s+/, $value)) { |
| my $pattern = $ip; |
| $ip =~ s/\./\\\./g; |
| $ip =~ s/\?/\./g; |
| $ip =~ s/\*/\.\*/g; |
| $self->{siq_skip_ip}->{$ip} = $pattern; |
| } |
| } |
| }); |
| |
| =back |
| |
| =head1 RULE DEFINITIONS AND PRIVILEGED SETTINGS |
| |
| There are no privileged settings provided. |
| |
| =head1 ADMINISTRATOR SETTINGS |
| |
| These settings differ from the ones above, in that they are considered 'more |
| privileged' -- even more than the ones in the B<PRIVILEGED SETTINGS> section. |
| No matter what C<allow_user_rules> is set to, these can never be set from a |
| user's C<user_prefs> file when spamc/spamd is being used. However, all |
| settings can be used by local programs run directly by the user. |
| |
| =over 4 |
| |
| =item siq_server db.example.net:6264 |
| |
| An SIQ server hostname to query. An optional :port number may be included. If |
| no port is specified, port 6264 will be used by default. Multiple servers per |
| line are permitted, as are multiple lines. |
| |
| Examples: |
| |
| siq_server db.example.net:6264 |
| siq_server db.example.org |
| siq_server db.example.com db.example.org:1234 |
| siq_server db.example.net:6264 db.example.org:1234 |
| |
| =cut |
| |
| push (@cmds, { |
| setting => 'siq_server', |
| default => {}, |
| is_admin => 1, |
| code => sub { |
| my ($self, $key, $value, $line) = @_; |
| if ($value =~ /^$/) { |
| return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; |
| } |
| if ($value !~ /^[-.\w\d]+(?::\d{1,5})?(?:\s+[-.\w\d]+(?::\d{1,5})?)*$/) { |
| return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
| } |
| foreach my $server (split(/\s+/, $value)) { |
| $server =~ /^(.*?)(?::(.*))?$/; |
| my $host = lc $1; |
| my $port = (defined $2 ? $2 : "6264"); |
| $self->{siq_servers}->{lc $1}->{$port} = 1; |
| dbg("config: added SIQ server host: $1 port: $port"); |
| } |
| } |
| }); |
| |
| =item siq_server_ttl db.example.net:6264 300 |
| |
| The amount of time in seconds to keep cached SIQ query responses from a |
| particular server. Note that domain and IP pairs may be cached more often |
| than this value as caches are not shared between children and expire when a |
| child expires (after 200 messages by default). |
| |
| This option overrides the TTL returned in an SIQ response by the specified SIQ |
| server hostname. An optional :port number may be included. If no port is |
| specified, port 6264 will be used by default. Only one server, with optional |
| port, and TTL value per line is permitted. Multiple lines are permitted. |
| |
| Examples: |
| |
| siq_server_ttl db.example.net:6264 300 |
| siq_server_ttl db.example.org 500 |
| |
| Note: To prevent abuse of services, the longer of the TTL provided with this |
| option and the TTL provided in the SIQ response will be used. |
| |
| =cut |
| |
| push (@cmds, { |
| setting => 'siq_server_ttl', |
| default => {}, |
| is_admin => 1, |
| code => sub { |
| my ($self, $key, $value, $line) = @_; |
| if ($value =~ /^$/) { |
| return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; |
| } |
| if ($value !~ /^([-.\w\d]+)(?::(\d{1,5}))?\s+(\d+)$/) { |
| return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
| } |
| my $host = lc $1; |
| my $port = (defined $2 ? $2 : "6264"); |
| my $ttl = $3; |
| $self->{siq_server_ttls}->{lc $1}->{$port} = $3; |
| dbg("config: added SIQ response TTL: $3 for server host: $1 port: $port"); |
| } |
| }); |
| |
| =item siq_oi_workaround (0|1) (default: 0) |
| |
| As of March 5, 2006, Oubtbound Index does not yet include octets 8-11 (TTL, |
| Confidence and Extra-Length values) as specified by the draft in their |
| responses. |
| |
| Outbound Index plans on updating their software to include these octets in the |
| near future. Set this option to 1 to enable correct parsing of Outbound Index |
| responses in the interim. |
| |
| <B>Note: Enabling this option will affect parsing of ALL SIQ servers' |
| responses. Therefore you cannot use Outbound Index and another service |
| together until Outbound Index updates their service so that this option |
| is not required. This shouldn't be a problem since there aren't any other |
| public services using SIQ yet. |
| |
| =cut |
| |
| push(@cmds, { |
| setting => 'siq_oi_workaround', |
| default => 0, |
| is_admin => 1, |
| type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL |
| }); |
| |
| =item siq_query_timeout n (default: 5) |
| |
| The amount of time in seconds to wait for an SIQ query to complete. |
| |
| =cut |
| |
| push(@cmds, { |
| setting => 'siq_query_timeout', |
| default => 5, |
| is_admin => 1, |
| type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC |
| }); |
| |
| $conf->{parser}->register_commands(\@cmds); |
| } |
| |
| |
| =item eval:siq_score('host:port',min,max) |
| |
| This eval function is provided for writing eval-type rules against the |
| reputation score returned by the reputation service queried. |
| |
| <i>min and <i>max define a range of scores to match against. |
| |
| Example: |
| |
| header SIQ_OI_00 eval:siq_score('db.outboundindex.net',0,0) |
| score SIQ_OI_00 1.5 |
| describe SIQ_OI_00 Outbound Index Reputation: http://outboundindex.org/ |
| tflags SIQ_OI_00 net |
| priority SIQ_OI_00 900 |
| |
| Note: The priority value gives SIQ responses more time to arrive before |
| SpamAssassin pauses to wait for responses for the amount of time specified by |
| <I>siq_query_timeout. Changing the priority value is not recommended. Adjust |
| the <I>siq_query_timeout value to shorten or lengthen the time SpamAssassin |
| will wait for SIQ responses. Set <I>siq_query_timeout to <I>0 if you do not |
| want SpamAssassin to wait at all for SIQ responses. |
| |
| =cut |
| |
| sub siq_score { |
| my ($self, $pms, $server, $min, $max) = @_; |
| return 0 if $self->{disabled}; |
| return 0 unless $pms->{siq_checking}; |
| |
| my $rule_name = $pms->get_current_eval_rule_name(); |
| |
| my ($config_ok, $host, $port) |
| = $self->_parse_eval_call($pms, "siq_score", $rule_name, $server, $min, $max); |
| |
| return 0 if (!$config_ok); |
| |
| # this comes after $self->_parse_eval_call to avoid being called earlier |
| # than necessary by an incorrect eval call |
| $self->_get_results($pms) unless $pms->{siq_got_results}; |
| |
| # log the hit (if any) and return 0, otherwise hits will appear twice |
| my @results = $self->_get_results_from_cache($pms->{siq_time}, $host, |
| $port, $pms->{siq_domain}, $pms->{siq_ip}); |
| |
| if (defined $results[1] && |
| $min <= $results[1] && $results[1] <= $max) { |
| $self->_log_hit($pms, $rule_name, "SIQ: score: $results[1] queried: ". |
| "$pms->{siq_domain}/$pms->{siq_ip}"); |
| } |
| return 0; |
| } |
| |
| |
| =item eval:siq_ip_score('host:port',min,max) |
| |
| This eval function is provided for writing eval-type rules against the |
| IP reputation score returned by the reputation service queried. |
| |
| <i>min and <i>max define a range of scores to match against. |
| |
| Example: |
| |
| header SIQ_OI_IP_01 eval:siq_ip_score('db.outboundindex.net',1,1) |
| score SIQ_OI_IP_01 1.0 |
| describe SIQ_OI_IP_01 Outbound Index IP Reputation: http://outboundindex.org/ |
| tflags SIQ_OI_IP_01 net |
| priority SIQ_OI_IP_01 900 |
| |
| Note: See the note above for <I>eval:siq_score regarding the priority value. |
| |
| =cut |
| |
| sub siq_ip_score { |
| my ($self, $pms, $server, $min, $max) = @_; |
| return 0 if $self->{disabled}; |
| return 0 unless $pms->{siq_checking}; |
| |
| my $rule_name = $pms->get_current_eval_rule_name(); |
| |
| my ($config_ok, $host, $port) |
| = $self->_parse_eval_call($pms, "siq_ip_score", $rule_name, $server, $min, $max); |
| |
| return 0 if (!$config_ok); |
| |
| # this comes after $self->_parse_eval_call to avoid being called earlier |
| # than necessary by an incorrect eval call |
| $self->_get_results($pms) unless $pms->{siq_got_results}; |
| |
| # log the hit (if any) and return 0, otherwise hits will appear twice |
| my @results = $self->_get_results_from_cache($pms->{siq_time}, $host, |
| $port, $pms->{siq_domain}, $pms->{siq_ip}); |
| |
| if (defined $results[3] && |
| $min <= $results[3] && $results[3] <= $max) { |
| $self->_log_hit($pms, $rule_name, "SIQ: score: $results[3] queried: ". |
| "$pms->{siq_domain}/$pms->{siq_ip}"); |
| } |
| return 0; |
| } |
| |
| |
| =item eval:siq_domain_score('host:port',min,max) |
| |
| This eval function is provided for writing eval-type rules against the |
| domain reputation score returned by the reputation service queried. |
| |
| <i>min and <i>max define a range of scores to match against. |
| |
| Example: |
| |
| header SIQ_OI_DOM_50 eval:siq_domain_score('db.outboundindex.net',50,59) |
| score SIQ_OI_DOM_50 0.1 |
| describe SIQ_OI_DOM_50 Outbound Index Domain Reputation: http://outboundindex.org/ |
| tflags SIQ_OI_DOM_50 net |
| priority SIQ_OI_DOM_50 900 |
| |
| Note: See the note above for <I>eval:siq_score regarding the priority value. |
| |
| =cut |
| |
| sub siq_domain_score { |
| my ($self, $pms, $server, $min, $max) = @_; |
| return 0 if $self->{disabled}; |
| return 0 unless $pms->{siq_checking}; |
| |
| my $rule_name = $pms->get_current_eval_rule_name(); |
| |
| my ($config_ok, $host, $port) |
| = $self->_parse_eval_call($pms, "siq_domain_score", $rule_name, $server, $min, $max); |
| |
| return 0 if (!$config_ok); |
| |
| # this comes after $self->_parse_eval_call to avoid being called earlier |
| # than necessary by an incorrect eval call |
| $self->_get_results($pms) unless $pms->{siq_got_results}; |
| |
| # log the hit (if any) and return 0, otherwise hits will appear twice |
| my @results = $self->_get_results_from_cache($pms->{siq_time}, $host, |
| $port, $pms->{siq_domain}, $pms->{siq_ip}); |
| |
| if (defined $results[4] && |
| $min <= $results[4] && $results[4] <= $max) { |
| $self->_log_hit($pms, $rule_name, "SIQ: score: $results[4] queried: ". |
| "$pms->{siq_domain}/$pms->{siq_ip}"); |
| } |
| return 0; |
| } |
| |
| |
| =item eval:siq_relative_score('host:port',min,max) |
| |
| This eval function is provided for writing eval-type rules against the |
| relative reputation score returned by the reputation service queried. |
| |
| <i>min and <i>max define a range of scores to match against. |
| |
| Example: |
| |
| header SIQ_OI_REL_01 eval:siq_relative_score('db.outboundindex.net',1,1) |
| score SIQ_OI_REL_01 1.0 |
| describe SIQ_OI_REL_01 Outbound Index Relative Reputation: http://outboundindex.org/ |
| tflags SIQ_OI_REL_01 net |
| priority SIQ_OI_REL_01 900 |
| |
| Note: See the note above for <I>eval:siq_score regarding the priority value. |
| |
| =cut |
| |
| sub siq_relative_score { |
| my ($self, $pms, $server, $min, $max) = @_; |
| return 0 if $self->{disabled}; |
| return 0 unless $pms->{siq_checking}; |
| |
| my $rule_name = $pms->get_current_eval_rule_name(); |
| |
| my ($config_ok, $host, $port) |
| = $self->_parse_eval_call($pms, "siq_relative_score", $rule_name, $server, $min, $max); |
| |
| return 0 if (!$config_ok); |
| |
| # this comes after $self->_parse_eval_call to avoid being called earlier |
| # than necessary by an incorrect eval call |
| $self->_get_results($pms) unless $pms->{siq_got_results}; |
| |
| # log the hit (if any) and return 0, otherwise hits will appear twice |
| my @results = $self->_get_results_from_cache($pms->{siq_time}, $host, |
| $port, $pms->{siq_domain}, $pms->{siq_ip}); |
| |
| if (defined $results[5] && |
| $min <= $results[5] && $results[5] <= $max) { |
| $self->_log_hit($pms, $rule_name, "SIQ: score: $results[5] queried: ". |
| "$pms->{siq_domain}/$pms->{siq_ip}"); |
| } |
| return 0; |
| } |
| |
| |
| =item eval:siq_confidence('host:port',min,max) |
| |
| This eval function is provided for writing eval-type rules against the |
| confidence value returned by the reputation service queried. |
| |
| <i>min and <i>max define a range of values to match against. |
| |
| Example: |
| |
| header SIQ_OI_CONF_01 eval:siq_confidence('db.outboundindex.net',1,1) |
| score SIQ_OI_CONF_01 1.0 |
| describe SIQ_OI_CONF_01 Outbound Index Confidence: http://outboundindex.org/ |
| tflags SIQ_OI_CONF_01 net |
| priority SIQ_OI_CONF_01 900 |
| |
| Note: See the note above for <I>eval:siq_score regarding the priority value. |
| |
| =cut |
| |
| sub siq_confidence { |
| my ($self, $pms, $server, $min, $max) = @_; |
| return 0 if $self->{disabled}; |
| return 0 unless $pms->{siq_checking}; |
| |
| my $rule_name = $pms->get_current_eval_rule_name(); |
| |
| my ($config_ok, $host, $port) |
| = $self->_parse_eval_call($pms, "siq_confidence", $rule_name, $server, $min, $max); |
| |
| return 0 if (!$config_ok); |
| |
| # this comes after $self->_parse_eval_call to avoid being called earlier |
| # than necessary by an incorrect eval call |
| $self->_get_results($pms) unless $pms->{siq_got_results}; |
| |
| # log the hit (if any) and return 0, otherwise hits will appear twice |
| my @results = $self->_get_results_from_cache($pms->{siq_time}, $host, |
| $port, $pms->{siq_domain}, $pms->{siq_ip}); |
| |
| if (defined $results[8] && |
| $min <= $results[8] && $results[8] <= $max) { |
| $self->_log_hit($pms, $rule_name, "SIQ: value: $results[8] queried: ". |
| "$pms->{siq_domain}/$pms->{siq_ip}"); |
| } |
| return 0; |
| } |
| |
| |
| =item header siqhost[:port] =~ /pattern/modifiers |
| |
| A pseudo-header containing the text portion of the SIQ result is |
| provided for each SIQ server that you have called at least one of |
| the above eval tests on one of more times. |
| |
| Example: |
| |
| header SIQ_OI_STAB_1 db.outboundindex.net =~ /stability=1\./ |
| score SIQ_OI_STAB_1 0.5 |
| describe SIQ_OI_STAB_1 Outbound Index stability value of 1 |
| tflags SIQ_OI_STAB_1 net |
| priority SIQ_OI_STAB_1 901 |
| |
| header SIQ_EX_STAB_20 db.example.org:1234 =~ /stability=2[0-9]\./ |
| score SIQ_EX_STAB_20 0.1 |
| describe SIQ_EX_STAB_20 Example Service stability value of 20 to 29 |
| tflags SIQ_EX_STAB_20 net |
| priority SIQ_EX_STAB_20 9 |
| |
| Notes: |
| |
| You <b>MUST call at least one of the above eval tests on each of the servers |
| that you want to test the text portion of the response, otherwise the |
| pseudo-header will not be present. |
| |
| You <B>MUST include the port number in the psuedo-header if the default port |
| 6264 is not used, otherwise it is optional. |
| |
| You <B>MUST include a priority for the rule that is greater in value than the |
| priority of the required pre-requisite eval test. The pseudo-header will not |
| yet be present if this rule's priority is less than (higher) than the above |
| eval tests. |
| |
| =back |
| |
| =cut |
| |
| sub _parse_eval_call { |
| my ($self, $pms, $eval_name, $rule_name, $server, $min, $max) = @_; |
| |
| my ($host, $port); |
| |
| # validate the eval call and complain if it was done wrong |
| unless (defined $server && $server =~ /^([-.\w\d]+)(?::(\d{1,5}))?$/) { |
| warn("siq: eval rule: $rule_name ". |
| "requires an SIQ server parameter (host with optional :port) ". |
| "as the first parameter"); |
| |
| dbg("config: eval rule: $rule_name ". |
| "requires an SIQ server parameter such as: header $rule_name ". |
| "eval:$eval_name\('db.example.com:6264',20,30\)"); |
| |
| $pms->{rule_errors}++; # flag to --lint that there was an error ... |
| return 0; |
| } else { |
| $host = $1; |
| $port = (defined $2 ? $2 : "6264"); |
| } |
| |
| unless (exists $pms->{conf}->{siq_servers}->{$host}->{$port}) { |
| warn("siq: the SIQ server specified in eval rule: $rule_name ". |
| "has not been added to the list of SIQ servers to query"); |
| |
| dbg("config: you must add \'siq_server $host:$port\' to your configuration ". |
| "if you want to be able to test SIQ results from this server"); |
| |
| $pms->{rule_errors}++; # flag to --lint that there was an error ... |
| return 0; |
| } |
| |
| unless (defined $min && $min =~ /^-?\d+(?:\.\d+)?$/ && |
| defined $max && $max =~ /^-?\d+(?:\.\d+)?$/) { |
| warn("siq: eval rule: $rule_name requires a minimum and maximum value"); |
| |
| dbg("config: eval rule: $rule_name ". |
| "requires minimum and maximum parameters such as: header ". |
| "$rule_name eval:$eval_name\('db.example.com',20,30\)"); |
| |
| $pms->{rule_errors}++; # flag to --lint that there was an error ... |
| return 0; |
| } |
| |
| return (1, $host, $port); |
| } |
| |
| |
| sub _log_hit { |
| my ($self, $pms, $rulename, $text) = @_; |
| |
| $pms->test_log ($text); |
| $pms->got_hit ($rulename, ""); |
| } |
| |
| |
| sub parsed_metadata { |
| my ($self, $opts) = @_; |
| my $pms = $opts->{permsgstatus}; |
| |
| return if $self->{disabled}; |
| |
| $pms->{siq_queries_remaining} = 0; |
| $pms->{siq_queries_sent} = 0; |
| $pms->{siq_time} = time; |
| $pms->{siq_got_results} = 0; |
| $pms->{siq_checking} = 0; |
| |
| # get an appropriate relay to test against |
| my $lasthop = $self->_get_relay($pms); |
| if (!defined $lasthop) { |
| dbg("siq: no suitable relay for siq use found, skipping SIQ query"); |
| return; |
| } |
| |
| $pms->{siq_ip} = $lasthop->{ip}; |
| $pms->{siq_domain} = $self->_get_sender($pms); |
| |
| # we already dbg'd if we couldn't get a sender, just return |
| return unless (defined $pms->{siq_domain}); |
| $pms->{siq_domain} =~ s/^.*\@//; |
| |
| # check to see if the domain is in the list of domains to skip |
| my $skip_it = 0; |
| while (my ($regexp, $simple) = each (%{$pms->{conf}->{siq_skip_domain}})) { |
| if ($pms->{siq_domain} =~ /^$regexp$/) { # both already lc |
| dbg("siq: domain: $pms->{siq_domain} matches skip pattern: $simple"); |
| $skip_it = 1; |
| } |
| } |
| return if $skip_it; |
| |
| # check to see if the domain is in the list of domains to skip |
| while (my ($regexp, $simple) = each (%{$pms->{conf}->{siq_skip_ip}})) { |
| if ($pms->{siq_ip} =~ /^$regexp$/) { # both already lc |
| dbg("siq: ip: $pms->{siq_ip} matches skip pattern: $simple"); |
| $skip_it = 1; |
| } |
| } |
| return if $skip_it; |
| |
| # signal to the evals that we're doing checks this time around |
| $pms->{siq_checking} = 1; |
| |
| # do queries |
| foreach my $host (keys %{$pms->{conf}->{siq_servers}}) { |
| foreach my $port (keys %{$pms->{conf}->{siq_servers}->{$host}}) { |
| next if $self->_check_for_cached_results($pms, $pms->{siq_time}, $host, |
| $port, $pms->{siq_domain}, $pms->{siq_ip}); |
| dbg("siq: querying $host:$port"); |
| $self->_send_siq_query($pms, $pms->{siq_domain}, $pms->{siq_ip}, |
| $host, $port); |
| } |
| } |
| |
| return; |
| } |
| |
| |
| sub _cache_results { |
| my ($self, $time, $host, $port, $domain, $ip, @results) = @_; |
| |
| # set cache item expiry time |
| # don't allow TTLs shorter than the TTL specified in the response |
| if (exists $self->{main}->{conf}->{siq_server_ttls}->{$host}->{$port} && |
| $self->{main}->{conf}->{siq_server_ttls}->{$host}->{$port} > $results[7]) { |
| $time += $self->{main}->{conf}->{siq_server_ttls}->{$host}->{$port}; |
| } else { |
| $time += $results[7] if ($results[7] > 0); # unknown OI TTL is set to -999 |
| } |
| |
| $self->{siq_cache} = {} unless (exists $self->{siq_cache}); |
| |
| $self->{siq_cache}->{$host} = {} |
| unless (exists $self->{siq_cache}->{$host}); |
| |
| $self->{siq_cache}->{$host}->{$port} = {} |
| unless (exists $self->{siq_cache}->{$host}->{$port}); |
| |
| $self->{siq_cache}->{$host}->{$port}->{$domain} = {} |
| unless (exists $self->{siq_cache}->{$host}->{$port}->{$domain}); |
| |
| $self->{siq_cache}->{$host}->{$port}->{$domain}->{$ip} = [$time, @results]; |
| |
| dbg("siq: saved results to cache: $host:$port/$domain/$ip"); |
| |
| return; |
| } |
| |
| |
| sub _check_for_cached_results { |
| my ($self, $pms, $time, $host, $port, $domain, $ip) = @_; |
| |
| if (exists $self->{siq_cache}->{$host}->{$port}->{$domain}->{$ip}) { |
| if ($self->{siq_cache}->{$host}->{$port}->{$domain}->{$ip}->[0] > $time) { |
| dbg("siq: found results in cache: $host:$port/$domain/$ip"); |
| |
| # make the cached text portion available for testing |
| # the port number is optional if the default 6264 is used |
| $pms->{msg}->put_metadata("$host:$port", |
| $self->{siq_cache}->{$host}->{$port}->{$domain}->{$ip}->[10]); |
| if ($port == 6264) { |
| $pms->{msg}->put_metadata($host, |
| $self->{siq_cache}->{$host}->{$port}->{$domain}->{$ip}->[10]); |
| } |
| return 1; |
| } else { |
| dbg("siq: found expired result in cache, doing new query"); |
| } |
| } else { |
| dbg("siq: no results found in cache for $host:$port"); |
| } |
| return 0; |
| } |
| |
| |
| sub _get_results_from_cache { |
| my ($self, $time, $host, $port, $domain, $ip) = @_; |
| |
| if (exists $self->{siq_cache}->{$host}->{$port}->{$domain}->{$ip}) { |
| if ($self->{siq_cache}->{$host}->{$port}->{$domain}->{$ip}->[0] > $time) { |
| my @results = @{$self->{siq_cache}->{$host}->{$port}->{$domain}->{$ip}}; |
| shift @results; |
| return @results; |
| } |
| } |
| return undef; |
| } |
| |
| |
| sub _generate_query_id { |
| return int(rand(65535)); |
| } |
| |
| |
| sub _send_siq_query { |
| my ($self, $pms, $domain, $ip, $host, $port) = @_; |
| |
| unless (defined $ip && |
| $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) { |
| warn("siq: invalid (non-IPv4) IP passed to _send_siq_query\n"); |
| return 0; |
| } |
| |
| unless (defined $domain) { |
| warn("siq: missing domain in call to _send_siq_query\n"); |
| return 0; |
| } |
| |
| # the query ID identifies the query and is used in the response packet |
| # since we send multiple queries out on the same socket, we have to |
| # create a new packet with a random query ID for each query |
| my $query_id = $self->_generate_query_id(); |
| $pms->{siq_query_ids}->{$host}->{$port} = $query_id; |
| |
| # build request packet |
| my $payload = pack("B8", "00000001"); # version |
| $payload .= pack("B8"); # QT |
| $payload .= substr(pack("N", $query_id), 2); # ID |
| $payload .= pack("B96"); # IPv6 zero-padding |
| $payload .= inet_aton($ip); # IPv4 in IPv6 |
| $payload .= substr(pack("N", length($domain)), 3); # QD-length in octets |
| $payload .= pack("B8"); # extra length (octets) |
| $payload .= $domain; # domain |
| |
| # save socket handle on $pms, we'll check for a response later |
| unless (exists $pms->{siq_handle} && $pms->{siq_handle}) { |
| dbg("siq: opening socket for SIQ queries"); |
| unless ($pms->{siq_handle} = IO::Socket::INET->new(Proto => 'udp')) { |
| dbg("siq: socket creation failed: $@"); |
| return 0; |
| } else { |
| # try to prevent unwanted blocking |
| my $flags = fcntl($pms->{siq_handle}, F_GETFL, 0) |
| or warn "siq: Can't get flags for the socket: $!\n"; |
| if ($flags) { |
| fcntl($pms->{siq_handle}, F_SETFL, $flags | O_NONBLOCK) |
| or warn "siq: Can't set flags for the socket: $!\n"; |
| } |
| } |
| } else { |
| dbg("siq: using existing socket for SIQ queries"); |
| } |
| |
| my $ipaddr = inet_aton($host); |
| my $portaddr = sockaddr_in($port, $ipaddr); |
| |
| unless (send($pms->{siq_handle}, $payload, 0, $portaddr) |
| == length($payload)) { |
| dbg("siq: cannot send query: $!"); |
| return 0; |
| } else { |
| dbg("siq: sent query ID $query_id to $host:$port"); |
| $pms->{siq_queries_remaining}++; |
| $pms->{siq_queries_sent}++; |
| } |
| return 1; |
| } |
| |
| |
| sub _get_results { |
| my ($self, $pms) = @_; |
| |
| $pms->{siq_got_results} = 1; |
| $self->_harvest_siq_responses($pms); |
| |
| dbg("siq: sent ". $pms->{siq_queries_sent} ." queries, received ". |
| ($pms->{siq_queries_sent} - $pms->{siq_queries_remaining}) |
| ." responses"); |
| return; |
| } |
| |
| |
| sub _harvest_siq_responses { |
| my ($self, $pms) = @_; |
| |
| return unless $pms->{siq_queries_sent}; |
| |
| my $rout; |
| my $rin = ''; |
| |
| vec($rin,fileno($pms->{siq_handle}),1) = 1; |
| |
| my $timeout = $pms->{conf}->{siq_query_timeout}; |
| |
| my $nfound = 0; |
| my $wait_time = 0.05; |
| |
| while ($timeout > 0) { |
| $nfound = select($rout=$rin, undef, undef, $wait_time); |
| |
| if (!defined $nfound || $nfound < 1) { |
| $timeout -= $wait_time; |
| } else { |
| # read results |
| READRESULT: for (my $i = 0; $i < $nfound; $i++) { |
| my ($response, $portaddr); |
| unless ($portaddr = recv($pms->{siq_handle}, $response, 512, 0)) { |
| dbg ("siq: recv failed: $!"); |
| return 0; |
| } |
| |
| # parse response |
| # returns: ($version, $score, $id, $ipscore, $dscore, $rscore, $textlen, $ttl, |
| # $confidence, $text) |
| my (@results) = $self->_parse_response($response, $pms->{conf}->{siq_oi_workaround}); |
| next READRESULT unless @results; |
| |
| foreach my $host (keys %{$pms->{siq_query_ids}}) { |
| foreach my $port (keys %{$pms->{siq_query_ids}->{$host}}) { |
| my $query_id = $pms->{siq_query_ids}->{$host}->{$port}; |
| |
| if ($results[2] == $query_id) { |
| dbg("siq: response ID $query_id matches query to $host:$port"); |
| $pms->{siq_queries_remaining}--; |
| |
| dbg("siq: response: ". join("/", @results)); |
| |
| $self->_cache_results($pms->{siq_time}, $host, $port, |
| $pms->{siq_domain}, $pms->{siq_ip}, @results); |
| |
| # we store the text section as metadata so people can write |
| # rules against it |
| # the port number is optional if the default 6264 is used |
| $pms->{msg}->put_metadata("$host:$port", $results[9]); |
| if ($port == 6264) { |
| $pms->{msg}->put_metadata($host, $results[9]); |
| } |
| |
| unless ($pms->{siq_queries_remaining}) { |
| dbg("siq: received responses to all queries after waiting ". |
| (sprintf "%.2f", ($pms->{conf}->{siq_query_timeout} - $timeout)) |
| ." seconds, closing socket"); |
| close $pms->{siq_handle}; |
| return 1; |
| } |
| next READRESULT; |
| } |
| } |
| } |
| dbg("siq: response ID $results[2] does not match any queries sent ". |
| "for this message, discarding"); |
| } |
| dbg("siq: waiting up to $timeout seconds for more responses"); |
| } |
| } |
| |
| dbg("siq: query response timeout, closing socket"); |
| close $pms->{siq_handle}; |
| return 0; |
| } |
| |
| |
| sub _parse_response { |
| my ($self, $response, $enable_oi_workaround) = @_; |
| |
| my $min_response_length = 12; |
| if ($enable_oi_workaround) { |
| dbg("siq: using Outbound Index response missing octets workaround"); |
| $min_response_length = 8; |
| } |
| |
| if (length($response) < $min_response_length) { |
| dbg("siq: packet shorter than minimum response length, ignoring packet"); |
| return; |
| } |
| |
| my $version = unpack("c8", substr($response, 0, 1)); |
| my $score = unpack("c8", substr($response, 1, 1)); |
| my $id = unpack("N", pack("x2B16", unpack("B16", substr($response, 2, 2)))); |
| my $ipscore = unpack("c8", substr($response, 4, 1)); |
| my $dscore = unpack("c8", substr($response, 5, 1)); |
| my $rscore = unpack("c8", substr($response, 6, 1)); |
| my $textlen = unpack("c8", substr($response, 7, 1)); |
| |
| # ensure we've got the entire packet |
| if (length($response) < ($min_response_length + $textlen)) { |
| dbg("siq: packet length shorter than minimum length plus reported TEXT ". |
| "section length, ignoring packet"); |
| return; |
| } |
| |
| # workaround Outbound Index not using the current draft yet |
| # (they don't include draft response octets 8-11) |
| my ($ttl, $confidence, $text); |
| if ($enable_oi_workaround) { |
| $ttl = -999; |
| $confidence = -999; |
| $text = unpack("A*", substr($response, 8, $textlen)); |
| } else { |
| $ttl = unpack("N", pack("x2B16", unpack("B16", substr($response, 8, 2)))); |
| $confidence = unpack("c8", substr($response, 10, 1)); |
| my $xtralen = unpack("c8", substr($response, 11, 1)); |
| $text = unpack("A*", substr($response, 12, $textlen)); |
| |
| # the 'EXTRA' data is server/client dependent, we can't reasonably parse |
| # ever implementations EXTRA section, so don't parse any -- they can |
| # always use the TEXT section which we do support |
| dbg("siq: plugin does not support parsing of the $xtralen octets of ". |
| "'EXTRA' data provided in the SIQ response, not using 'EXTRA' data") |
| if $xtralen; |
| } |
| |
| return ($version, $score, $id, $ipscore, $dscore, $rscore, $textlen, $ttl, |
| $confidence, $text); |
| } |
| |
| |
| # dos: copied (with s/SPF/SIQ/) from my patch for SA bug 4661 -- the current |
| # SPF code does it wrong for non-trivial cases |
| # http://issues.apache.org/SpamAssassin/attachment.cgi?id=3241&action=view |
| # this really needs to get into Received.pm itself |
| sub _get_relay { |
| my ($self, $scanner) = @_; |
| |
| # return relay if already determined |
| return $scanner->{siq_relay} if exists $scanner->{siq_relay}; |
| |
| # DOS: For SIQ checks we want to use the relay that passed the message to |
| # the internal network. This relay can be any of the trusted relays or the |
| # first untrusted relay. No matter which it is, the next (newer) relay has |
| # to be an internal relay. If there are no trusted relays, the first |
| # untrusted relay is the one we want. If internal_networks aren't set we |
| # have to assume all trusted relays are internal. |
| |
| my $relay = undef; |
| my $relays_trusted = $scanner->{relays_trusted}; |
| |
| # no trusted relays, use first untrusted |
| if (scalar @{$relays_trusted} == 0) { |
| $relay = $scanner->{relays_untrusted}->[0]; |
| dbg("siq: no trusted relays found, using first (untrusted) relay (if present) for SIQ checks"); |
| } |
| |
| # last trusted relay is internal (or internal_networks not set), use first untrusted |
| elsif ($relays_trusted->[-1]->{internal} || !($scanner->{conf}->{internal_networks}->get_num_nets() > 0)) { |
| $relay = $scanner->{relays_untrusted}->[0]; |
| dbg("siq: last trusted relay is internal, using first untrusted relay (parsed relay #". (scalar @{$relays_trusted}+1) ." if present) for SIQ checks"); |
| } |
| |
| # find external relay that passed the message to the last internal relay |
| else { |
| |
| # found an internal relay? |
| my $found = 0; |
| |
| # start at the end; don't check for an internal relay before the first one |
| for (my $i = scalar @{$relays_trusted} - 1; $i > 0 && !$found; $i--) { |
| # if the next relay is internal, we can use the current external one |
| if ($relays_trusted->[$i-1]->{internal}) { |
| $relay = $relays_trusted->[$i]; |
| $found = 1; |
| dbg("siq: using first external trusted relay (parsed relay #". ($i+1) .") for SIQ checks"); |
| } |
| } |
| |
| # if none of the trusted relays were internal, internal_networks isn't set |
| # correctly -- dbg about it |
| if (!$found) { |
| dbg("siq: none of the trusted relays are internal, please check your internal_networks configuration"); |
| } |
| } |
| |
| $scanner->{siq_relay} = $relay; |
| return $relay; |
| } |
| |
| |
| # copied with modifications from patched (bug 4661) SPF.pm |
| # this also needs to get into Received.pm or elsewhere |
| sub _get_sender { |
| my ($self, $scanner) = @_; |
| my $sender; |
| |
| my $relay = $self->_get_relay($scanner); |
| if (defined $relay) { |
| $sender = $relay->{envfrom}; |
| } |
| |
| if ($sender) { |
| dbg("siq: found Envelope-From in first external Received header"); |
| } |
| else { |
| # We cannot use the env-from data, since it went through 1 or more relays |
| # since the untrusted sender and they may have rewritten it. |
| if ($scanner->{num_relays_trusted} > 0 && !$scanner->{conf}->{always_trust_envelope_sender}) { |
| dbg("siq: relayed through one or more trusted relays, cannot use header-based Envelope-From, skipping"); |
| return; |
| } |
| |
| # we can (apparently) use whatever the current Envelope-From was, |
| # from the Return-Path, X-Envelope-From, or whatever header. |
| # it's better to get it from Received though, as that is updated |
| # hop-by-hop. |
| $sender = $scanner->get ("EnvelopeFrom"); |
| } |
| |
| if (!$sender) { |
| dbg("siq: cannot get Envelope-From, cannot use SIQ"); |
| return; # avoid setting $scanner->{sender} to undef |
| } |
| |
| return lc $sender; |
| } |
| |
| |
| 1; |