Re: [exim] Rate Limit Message Relaying

Top Page
Delete this message
Reply to this message
Author: Kirill Miazine
Date:  
To: exim-users
Subject: Re: [exim] Rate Limit Message Relaying
* Marc Perkel [2004-12-18 13:53]:
> >>I'd like Exim to be able to rate limit outgoing messages from certian IP
> >>addresses who I would otherwise relay for. Initially I'd like to
> >>restrice the rate to one message per 30 seconds and after 5 messages
> >>climb to 1 message every 2 minutes - or something like that. Is this
> >>doable? Sort of like the bounce retry stuff but applied to relating
> >>instead of retry.
> >>
> >
> >Write a daemon (in a language you feel comfortable with) that implements
> >the logic you are describing. Exim can communicate with that daemon
> >using ${readsocket}.
> >
> I have no idea how to do that - but it would be nice to have.


I don't want to someone that answers with bare suggestions, so I provide
a simple solution as well. Perl is my prefered language and POE is a
great framework, so my solution is using POE.

1 message per 30 seconds is just too low. What about refreshing the
counters? I'd suggest limiting relay service to e.g. 100 messages per
hour and 1000 messages per day.

Below you'll find a quick-hack-daemon that will allow a host to send 100
messages per hour. Statistics will be stored in the daemons memory. The
action happens in "handle_input". You *do* want to change $socket_path
as well (it's set to /tmp/relay.sock now) and you *do* want to place it
in a directory in which only Exim user can access it.

=== start ===
#!/usr/bin/perl

use strict;
use vars qw(%stats);

use POE;
use POE::Session;
use POE::Wheel::SocketFactory;
use POE::Wheel::ReadWrite;
use POE::Filter::Line;

use Socket;
umask 0002;

%stats = ();
POE::Session->create(
    inline_states => {
        _start => sub {
            my ($kernel, $heap) = @_[KERNEL, HEAP];


            my $socket_path = '/tmp/relay.sock';
            unlink $socket_path if -e $socket_path;


            $heap->{'server'} = POE::Wheel::SocketFactory->new(
                BindAddress => $socket_path,
                SocketDomain => AF_UNIX,
                SocketType => SOCK_STREAM,
                SuccessEvent => 'handle_accept',
                FailureEvent => 'handle_error',
            );
        },
        _stop => sub { },
        handle_accept => sub {
            my ($kernel, $heap) = @_[KERNEL, HEAP];
            my $handle = $_[ARG0];


            POE::Session->create(
                inline_states => {
                    _start => sub {
                        my ($kernel, $heap) = @_[KERNEL, HEAP];
                        my $handle = $_[ARG0];
                        $heap->{'client'} = POE::Wheel::ReadWrite->new(
                            Handle => $handle,
                            Filter => POE::Filter::Line->new(Literal => "\n"),
                            InputEvent => 'handle_input',
                            FlushedEvent => 'handle_flush',
                            ErrorEvent => 'handle_error',
                        );


                        $heap->{'finished'} = 0;
                    },
                    _stop => sub {
                        my ($kernel, $heap) = @_[KERNEL, HEAP];
                    },
                    handle_input => sub {
                        my ($kernel, $heap) = @_[KERNEL, HEAP];
                        my $input = $_[ARG0];


                        my $client = $heap->{'client'};
                        my $now = time;
                        my $host = $input;


                        # new or expired, allow relay
                        if (!exists $stats{$host} or
                            $stats{$host}[0] + 3600 < $now)
                        {
                            $stats{$host} = [$now, 1];
                            $client->put('no');
                        # limit reached, deny relay
                        } elsif ($stats{$host}[1]++ > 100) {
                            $client->put('yes');
                        # allow relay
                        } else {
                            $client->put('no');
                        }


                        $heap->{'finished'} = 1;
                    },
                    handle_flush => sub {
                        my ($kernel, $heap) = @_[KERNEL, HEAP];
                        delete $heap->{'client'} if $heap->{'finished'};
                    },
                    handle_error => sub {
                        my ($kernel, $heap) = @_[KERNEL, HEAP];
                        delete $heap->{'client'};
                    },
                },
                args => [$handle],
            );
        },
        handle_error => sub {
            my ($kernel, $heap) = @_[KERNEL, HEAP];
            delete $heap->{'server'};
        },
    },
);


POE::Kernel->run();
=== end ===

Fire up this daemon and you can use following in your ACL:

    condition = ${readsocket{/tmp/relay.sock}{$sender_host_address\n}{5s}{}{no}}


${readsocket} in the above line will give "yes" if the relay access
should be denied and "no" if it should be allowed. Think that the daemon
answers the question: Should I deny access to client $sender_host_address?

Now some homework for you: which ACL you'll put this in? (Hint: you're
certainly wanting to avoid RCPT ACL, unless you set some $acl_mN
variable once you have a ${readsocket} result.)

Cheers
Kirill

--
Drive defensively. Buy a tank.