diff options
author | Bob Beck <beck@cvs.openbsd.org> | 2003-03-02 19:22:01 +0000 |
---|---|---|
committer | Bob Beck <beck@cvs.openbsd.org> | 2003-03-02 19:22:01 +0000 |
commit | 866a15ac7401268d7e7ba88ce2463acc2a9f0794 (patch) | |
tree | 88b514fe30b236cde34a4bb432766687e29de0ed | |
parent | 641d06a264defa4e03e7fb9c5a2ac06d231e6eed (diff) |
Spamd changes to add blacklist awareness to spamd, new spamd-setup.pl
which configures individual blacklists sources and deals with whitelists.
Perl still needs some stylistic changes as suggested by bmc which will go
in shortly.
ok deraadt@
-rw-r--r-- | libexec/spamd/Makefile | 8 | ||||
-rw-r--r-- | libexec/spamd/sdl.c | 233 | ||||
-rw-r--r-- | libexec/spamd/sdl.h | 71 | ||||
-rw-r--r-- | libexec/spamd/spamd-setup.8 | 98 | ||||
-rw-r--r-- | libexec/spamd/spamd-setup.pl | 310 | ||||
-rw-r--r-- | libexec/spamd/spamd.8 | 68 | ||||
-rw-r--r-- | libexec/spamd/spamd.c | 437 |
7 files changed, 1175 insertions, 50 deletions
diff --git a/libexec/spamd/Makefile b/libexec/spamd/Makefile index ac9da0c2c60..b65d71caf91 100644 --- a/libexec/spamd/Makefile +++ b/libexec/spamd/Makefile @@ -1,13 +1,13 @@ -# $OpenBSD: Makefile,v 1.2 2003/02/14 05:32:02 jason Exp $ +# $OpenBSD: Makefile,v 1.3 2003/03/02 19:21:59 beck Exp $ PROG= spamd -SRCS= spamd.c +SRCS= spamd.c sdl.c MAN= spamd.8 spamd-setup.8 -CFLAGS+= -Wall -Werror +CFLAGS+= -Wall -Werror afterinstall: ${INSTALL} ${INSTALL_COPY} -o ${BINOWN} -g ${BINGRP} -m ${BINMODE} \ - ${.CURDIR}/spamd-setup.sh ${DESTDIR}${BINDIR}/spamd-setup + ${.CURDIR}/spamd-setup.pl ${DESTDIR}${BINDIR}/spamd-setup .include <bsd.prog.mk> diff --git a/libexec/spamd/sdl.c b/libexec/spamd/sdl.c new file mode 100644 index 00000000000..811aa320be7 --- /dev/null +++ b/libexec/spamd/sdl.c @@ -0,0 +1,233 @@ +/* $OpenBSD: sdl.c,v 1.1 2003/03/02 19:22:00 beck Exp $ */ +/* + * Copyright (c) 2003 Bob Beck. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * sdl.c - Implement spamd source lists + * + * This consists of everything we need to do to determine which lists + * someone is on. Spamd gets the connecting address, and looks it up + * against all lists to determine what deferral messages to feed back + * to the connecting machine. - The redirection to spamd will happen + * from pf in the kernel, first macth will rdr to us. Spamd (along with + * setup) must keep track of *all* matches, so as to tell someone all the + * lists that they are on. + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "sdl.h" + +extern int debug; +struct sdlist *blacklists = NULL; +int blc = 0, blu = 0; + +int +sdl_add (char *sdname, char *sdstring, char ** addrs, int addrc) +{ + int i, index = -1; + char astring[40]; + unsigned int maskbits; + struct sdaddr *m, *n; + + /* + * if a blacklist of same tag name is already there, replace it, + * otherwise append. + */ + + for (i = 0; i < blu; i++) + if (strcmp(blacklists[i].tag, sdname) == 0) + index = i; + if (index != -1) { + if (debug > 0) + printf("replacing list %s\n", blacklists[index].tag); + free(blacklists[index].tag); + free(blacklists[index].string); + free(blacklists[index].addrs); + } else { + if (debug > 0) + printf("adding list %s\n", sdname); + index = blu; + } + if (index == blu && blu == blc) { + struct sdlist *tmp; + tmp = realloc (blacklists, (blc + 128) * + sizeof(struct sdlist)); + if (tmp == NULL) + return (-1); + blacklists = tmp; + blc += 128; + } + blacklists[index].tag = strdup(sdname); + blacklists[index].string = strdup(sdstring); + blacklists[index].naddrs = addrc; + + /* cycle through addrs, converting. We assume they are correctly + * formatted v4 and v6 addrs, if they don't all convert correcly, the + * add fails. Each address should be address/maskbits + */ + + blacklists[index].addrs = malloc(addrc * sizeof(struct sdentry)); + if (blacklists[index].addrs == NULL) + return(-1); + + for(i = 0; i < addrc; i++) { + int j, k, af; + n = &blacklists[index].addrs[i].sda; + m = &blacklists[index].addrs[i].sdm; + + j = sscanf(addrs[i], "%39[^/]/%u", astring, &maskbits); + if (j != 2) + goto parse_error; + if (maskbits > 128) + goto parse_error; + /* sanity check! we don't allow a 0 mask - + * don't blacklist the entire net. + */ + if (maskbits == 0) + goto parse_error; + if (strchr(astring, ':') != NULL) + af = AF_INET6; + else + af = AF_INET; + if (af == AF_INET && maskbits > 32) + goto parse_error; + j = inet_pton(af, astring, n); + if (j != 1) + goto parse_error; + if (debug > 0) + printf("added %s/%u\n", astring, maskbits); + + /* set mask, borrowed from pf */ + k = 0; + for (j = 0; j < 4; j++) + m->addr32[j] = 0; + while (maskbits >= 32) { + m->addr32[k++] = 0xffffffff; + maskbits -= 32; + } + for (j = 31; j > 31 - maskbits; --j) + m->addr32[k] |= (1 << j); + if (maskbits) + m->addr32[k] = htonl(m->addr32[k]); + + /* mask off address bits that won't ever be used */ + for (j = 0; j < 4; j++) + n->addr32[j] = n->addr32[j] & m->addr32[j]; + } + if (index == blu) { + blu++; + blacklists[blu].tag = NULL; + } + return (0); + parse_error: + if (debug > 0) + printf("sdl_add: parse error, \"%s\"\n", astring); + return(-1); +} + + +/* + * Return 1 if the addresses a (with mask m) matches address b + * otherwise return 0. It is assumed that address a has been + * pre-masked out, we only need to mask b. + */ +int +match_addr(struct sdaddr *a, struct sdaddr *m, struct sdaddr *b, + sa_family_t af) +{ + int match = 0; + + switch (af) { + case AF_INET: + if ((a->addr32[0]) == + (b->addr32[0] & m->addr32[0])) + match++; + break; + case AF_INET6: + if (((a->addr32[0]) == + (b->addr32[0] & m->addr32[0])) && + ((a->addr32[1]) == + (b->addr32[1] & m->addr32[1])) && + ((a->addr32[2]) == + (b->addr32[2] & m->addr32[2])) && + ((a->addr32[3]) == + (b->addr32[3] & m->addr32[3]))) + match++; + break; + } + return (match); +} + + +/* + * Given an address and address family + * return list of pointers to matching nodes. or NULL if none. + */ +struct sdlist ** +sdl_lookup(struct sdlist *head, int af, void * src) +{ + int i, matches = 0; + struct sdlist *sdl; + struct sdentry *sda; + struct sdaddr *source = (struct sdaddr *) src ; + static int sdnewlen = 0; + static struct sdlist **sdnew = NULL; + + if (head == NULL) + return (NULL); + else + sdl = head; + while (sdl->tag != NULL) { + for (i = 0; i < sdl->naddrs; i++) { + sda = sdl->addrs + i; + if (match_addr(&sda->sda, &sda->sdm, source, af)) { + if (matches == sdnewlen) { + struct sdlist **tmp; + tmp = realloc(sdnew, + (sdnewlen + 128) * + sizeof(struct sdlist *) ); + if (tmp == NULL) + /* XXX out of memory - return + what we have */ + return(sdnew); + sdnew = tmp; + sdnewlen += 128; + } + sdnew[matches]= sdl; + matches++; + sdnew[matches]=NULL; + break; + } + } + sdl++; + } + return(sdnew); +} diff --git a/libexec/spamd/sdl.h b/libexec/spamd/sdl.h new file mode 100644 index 00000000000..795f1cc6795 --- /dev/null +++ b/libexec/spamd/sdl.h @@ -0,0 +1,71 @@ +/* $OpenBSD: sdl.h,v 1.1 2003/03/02 19:22:00 beck Exp $ */ +/* + * Copyright (c) 2003 Bob Beck, Kjell Wooding. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef _SDL_H_ +#define _SDL_H_ + +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/ip_ipsp.h> + +/* structs */ + +struct sdlist { /* spamd source list */ + char *tag; /* sdlist source name */ + char *string; /* Format (451) string with no smtp code or \r\n */ + struct sdentry *addrs; + size_t naddrs; +}; + +/* yeah. Stolen from pf */ +struct sdaddr { + union { + struct in_addr v4; + struct in6_addr v6; + u_int8_t addr8[16]; + u_int16_t addr16[8]; + u_int32_t addr32[4]; + } _sda; /* 128-bit address */ +#define v4 _sda.v4 +#define v6 _sda.v6 +#define addr8 _sda.addr8 +#define addr16 _sda.addr16 +#define addr32 _sda.addr32 +}; + +struct sdentry { /* spamd netblock (black) list */ + struct sdaddr sda; + struct sdaddr sdm; +}; + + +/* prototypes */ + +extern int sdl_add(char *, char *, char **, int); +extern struct sdlist ** +sdl_lookup(struct sdlist *head, int af, void * src); + + +#endif /* _SDL_H_ */ diff --git a/libexec/spamd/spamd-setup.8 b/libexec/spamd/spamd-setup.8 index fc3f0936e70..9265961d02c 100644 --- a/libexec/spamd/spamd-setup.8 +++ b/libexec/spamd/spamd-setup.8 @@ -1,4 +1,4 @@ -.\" $OpenBSD: spamd-setup.8,v 1.3 2003/02/19 19:18:54 jason Exp $ +.\" $OpenBSD: spamd-setup.8,v 1.4 2003/03/02 19:22:00 beck Exp $ .\" .\" Copyright (c) 2003 Jason L. Wright (jason@thought.net) .\" All rights reserved. @@ -36,39 +36,60 @@ .Nd parse and load file of spammer addresses .Sh SYNOPSIS .Nm spamd-setup +.Op Fl c +.Op Fl s .Op Fl s .Op Fl 1 .Op Fl 2 .Op Fl f Ar file .Op Fl w Ar file +.Op Ar file ... .Sh DESCRIPTION The .Nm -utility adds or deletes address from the +utility adds blacklists by adding addresses to the .Xr pf 4 table -.Aq spamd . -This table combined with -.Xr spamd 8 -and a +.Aq spamd , +as well as configuring mail rejection messages for +the added list of addresses in +.Xr spamd 8 . +The +.Aq spamd +table is used in conjuction with a .Xr pf 4 -redirection rule can be used to selectively send spammers +redirection rule can be used to selectively redirect mail connections +to the .Xr spamd 8 . +daemon . Sources and actions are as follows: .Bl -tag -width XXXXXXXXXX .It Fl s The SPEWS level 1 database is fetched via .Xr ftp 1 -and used in the black-list. +and used in a blacklist named +.Li spews-1 .It Fl 1 Synonym for .Fl s . .It Fl 2 The SPEWS level 2 database is fetched via .Xr ftp 1 -and used in the black-list. +and used in a blacklist named +.Li spews-2 +.It Fl c +The chinese netblock datbase is fetched via +.Xr ftp 1 +and used in a blacklist named +.Li china +.It Fl k +The korean netblock database is fetched via +.Xr ftp 1 +and used in a blacklist named +.Li korea .It Fl f Ar file -The local file specified is added to the black-list. +The local file specified is used in a black-list named +.Li local .It Fl w Ar file The local file specified is added to the white-list. .El @@ -78,11 +99,64 @@ output is concatenated to build up a table for .Xr pf 4 . First, all of the addresses from the black-list are added. Then, all of the addresses from the white-list are removed. -The input file is expected to consist of one IP address per line (optionally -followed by a space and text that is ignored). +Then, the blacklist address, are sent to a running +.Xr spamd 8 +along with the message spamd will give on mail rejection when +a matching client connects. +The input file is expected to consist of one network block or address + per line (optionally followed by a space and text that is ignored). Comment lines beginning with .Li # are ignored. +Network blocks may be specified in any of the formats as in +the following example: +.Bd -literal -offset indent +.Ic # CIDR format +.Ic 192.168.20.0/24 +.Ic # A start - end range +.Ic 192.168.21.0 - 192.168.21.255 +.Ic # A masked address +.Ic 192.168.22.0:255.255.255.0 +.Ic # As a single IP address +.Ic 192.168.23.1 +.Ed +.Sh BLACKLIST FILES +additional files given as parameters to +.Nm +will be read to configure blacklists. The blacklist file format is +as follows. +.Bd -literal -offset indent +.Ic SPAMD_SOURCE;mylist;"Sorry %A, You are a probably spammer\enBye\en" +.Ic file:/usr/local/share/spammers.txt +.Ic http://www.somewhere.org/bigspamblocks.txt +.Ic SPAMD_SOURCE_REMOVE +.Ic file:/usr/local/share/notspammers.txt +.Ic file://www.somewhereelse.org/notspamblocks.txt +.Ic SPAMD_SOURCE;mykorealist;"Your address %A, appears to be from korea" +.Ic http://www.okean.com/koreacidr.txt +.Ic SPAMD_SOURCE_REMOVE +.Ic file:/usr/local/share/taekwondopals +.Ed +.Pp +The +.Li SPAMD_SOURCE +line includes a tag to name the blacklist, and the message to be +given to any connections that match this list. the message must +be enclosed in double quotes +and may include \en to produce a newline in the output. \e\" will produce +a double quote in the output, and %% will produce a single % in the output. +%A will be expanded by +.Xr spamd 8 +to display the connecting IP address in the output. +.Pp +Following the +.Li SPAMD_SOURCE +should be URL's, one per line, from which to fetch the +network blocks to blacklist. +Following the +.Li SPAMD_SOURCE_REMOVE +line may be further URL's, one per line, from which to +fetch network blocks that will be removed from this blacklist. .Sh SEE ALSO .Xr ftp 1 , .Xr pf 4 , diff --git a/libexec/spamd/spamd-setup.pl b/libexec/spamd/spamd-setup.pl new file mode 100644 index 00000000000..cbf0696667c --- /dev/null +++ b/libexec/spamd/spamd-setup.pl @@ -0,0 +1,310 @@ +#!/usr/bin/perl + +# $OpenBSD: spamd-setup.pl,v 1.1 2003/03/02 19:22:00 beck Exp $ +# +# Copyright (c) 2003 Bob Beck <beck@openbsd.org>. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +use strict; +use Net::Netmask; +use Socket; + +my $i; +my @blacklist; +my $globalwhite; +my %Sources; +my $state=0; +my $tag; +my $file; + +sub quad2int +{ + my @bytes = split(/\./,$_[0]); + + return undef unless @bytes == 4 && ! grep {!(/\d+$/ && $_<256)} @bytes; + + return unpack("N",pack("C4",@bytes)); +} + +sub int2quad +{ + return join('.',unpack('C4', pack("N", $_[0]))); +} + +sub valid_addr() { + my @bytes = split(/\./,$_[0]); + return undef unless @bytes == 4 && ! grep {!(/\d+$/ && $_<256)} @bytes; + return($_[0]); +} + +sub prev_addr() { + my @bytes = split(/\./,$_[0]); + return undef unless @bytes == 4 && ! grep {!(/\d+$/ && $_<256)} @bytes; + return int2quad (unpack("N",pack("C4",@bytes)) - 1); +} + +sub next_addr() { + my @bytes = split(/\./,$_[0]); + return undef unless @bytes == 4 && ! grep {!(/\d+$/ && $_<256)} @bytes; + return int2quad (unpack("N",pack("C4",@bytes)) + 1); +} + +# retrieve lists of netblocks or ip's from a list or urls as first +# arg. Add valid addresses and blocks seen to seen hash (second arg), +# as well as incrementing and decrementing start/end values in list hash +# (third arg) for later use in computing where the list actually +# starts and ends. +sub retrieve_lists() { my ($urls, $seen, $list) = @_; + my $file = shift(@{$urls}); + for (; $file && open (LIST, "ftp -V -o - $file |"); + $file=shift(@{$urls})) { + while (<LIST>) { + my ($block, $start, $end); + # vanna vanna, find me a netblock we assume one per line + chomp; + $start = undef; + $end = undef; + next if (/^\s*\#/); + if ($_ =~ m/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2})/) { + #We're in CIDR format....... + $block = new Net::Netmask($1); + if (!$block->{'ERROR'}) { + $start = $block->base(); + $end = $block->next(); + } + } elsif ($_ =~ m/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})[\s-]+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,2})/){ + #We're in somthing like startaddress - endaddress (Whois) + $start = &valid_addr($1); + $end = &next_addr($2); + } elsif ($_ =~ m/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/) { + #We're in somthing like address:mask + $block = new Net::Netmask("$1:$2"); + if (!$block->{'ERROR'}) { + $start = $block->base(); + $end = $block->next(); + } + } + elsif ($_ =~ m/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/) { + #We're just a single solitary IP. + $start = &valid_addr($1); + $end = &next_addr($1); + } else { + # skip anything that doesn't look like an IP or netblock. + next; + } + if ($start && $end) { + ${$seen}{"$end"}++; + ${$seen}{"$start"}++; + ${$list}{"$start"}++; + ${$list}{"$end"}--; + } + } + close(LIST); + } +} + +# make a spamd blacklist - retrieve addressen/netblocks from sources +# in first arg, remove errors or execptions from sources in second +# arg, return a list of strings, consisting of the actual netblocks to +# blacklist in CIDR format. +sub blacklist () { my @args = @_; + + my $i; + my (@blackurls, @errurls); + my (%Seen, %Black, %White, @BlackBlocks); + my ($blackval, $whiteval, $blackstart, $laststate); + my $j = 0; + + # first, snarf the blacklist and extract the addresses. + for ($i = 0; $i<=$#args; $i++) { + if ($args[$i] =~ /^-w$/) { + $j = 1; + } else { + if ($j == 0) { + push (@blackurls, $args[$i]); + } else { + push (@errurls, $args[$i]); + } + } + } + + &retrieve_lists(\@blackurls, \%Seen, \%Black); + &retrieve_lists(\@errurls, \%Seen, \%White); + + foreach $a (sort_by_ip_address (keys %Seen)) { + my $newblack; + my $newwhite; + my $state; + $newblack = $Black{$a}?$Black{$a}:$blackval; + $newwhite = $White{$a}?$White{$a}:$whiteval; + if ($state == 0 && $newblack > 0) { + $state = 1; + } + elsif ($state == 1 && $newblack == 0) { + $state = 0; + } + if ($newwhite > 0) { + $state = 0; + } + if ($laststate == 0 && $state == 1) { + # start a blacklist + $blackstart = $a; + } + if ($laststate == 1 && $state == 0) { + # end a blacklist + push (@BlackBlocks, ( map {$_->desc()} + (range2cidrlist ($blackstart, &prev_addr($a))))); + } + $laststate = $state; + $blackval = $newblack; + $whiteval = $newwhite; + } + return @BlackBlocks; +} + + +# Tell spamd about a blacklist. +# returns list of blacklisted CIDR's, suitable for adding to pf rdr. +sub addblack () { + my ($name, $message, @urls) = @_; + my @blacknets = &blacklist(@urls); + + if ($#blacknets >= 0) { + my ($i, $remote, $port, $iaddr, $paddr, $proto, $line); + + # tell spamd about it + $remote = '127.0.0.1'; + $port = 8025; + $iaddr = inet_aton($remote); + $paddr = sockaddr_in($port, $iaddr); + $proto = getprotobyname('tcp'); + socket(SOCK, PF_INET, SOCK_STREAM, $proto) || die "socket $!"; + connect(SOCK, $paddr) || die "connect: $!"; + $line = "$name;$message;".join(";", @blacknets)."\n"; + print SOCK $line; + close (SOCK); + } + return(@blacknets); # caller must add to pf table. +} + + + +while ($_ = shift(@ARGV)) { + if ($_ !~ /^-/) { + unshift(@ARGV, $_); + last; + } else { + if ( /-s/ || /-1/ ) { + # spews level 1; + $Sources{"spews-1"}= [ "spews-1", + "\"SPAM. Your address %A is in spews level 1\\n". + "See http://www.spews.org/ask.cgi?x=%A for more details\\n\"", + "http://www.spews.org/spews_list_level1.txt", "-w" ]; + } elsif (/-2/) { + # spews level 2 + $Sources{"spews-2"}= [ "spews-2", + "\"SPAM. Your address %A is in spews level 2\\n". + "See http://www.spews.org/ask.cgi?x=%A for more details\\n\"", + "http://www.spews.org/spews_list_level2.txt", "-w" ]; + } elsif (/-k/) { + # korea + $Sources{"korea-okean"}= [ "korea-okean", + "\"SPAM. Your address %A appears to be from korea\\n". + "See http://www.okean.com/asianspamblocks.html for more". + " details\\n\"", + "http://www.okean.com/koreacidr.txt", "-w" ]; + } elsif (/-c/) { + # china + $Sources{"china-okean"}= [ "china-okean", + "\"SPAM. Your address %A appears to be from china\\n". + "See http://www.okean.com/asianspamblocks.html for more". + " details\\n\"", + "http://www.okean.com/chinacidr.txt", "-w" ]; + } elsif (/-w/) { + # global whitelist file + $globalwhite = shift(@ARGV); + } elsif (/-f/) { + # local blacklist file + my $blackfile = shift(@ARGV); + if ($blackfile) { + $Sources{"localblock"}= [ "localblock", + "\"SPAM. Your address %A has been locally blocked\\n" . + "as a SPAM source\\n\"", + "file:$blackfile", "-w" ]; + } + } + } +} + +## read remaining input as files, setting up other lists. +## +## format must be +#SPAMD_SOURCE:tag:"message to send to luser" +#blacklist url +#blacklist url +#SPAMD_SOURCE_REMOVE +#removelist url +#removelist url +while ($file = shift(@ARGV)) { + open(SETUP, "<$file") || die ("can't open $file"); + while (<SETUP>) { + chomp; + next if (/^\s*\#/); + if (/^SPAMD_SOURCE;([^;]*);(\".*\")$/) { + if ($state == 1) { + # finish off last one. + push(@{$Sources{$tag}}, "-w"); + } + $state = 1; + $tag = $1; + $Sources{$tag}=[ $tag, $2 ]; + } elsif ($state > 0) { + + if (/^SPAMD_SOURCE_REMOVE$/) { + push(@{$Sources{$tag}}, "-w"); + $state = 2; + } + else { + push(@{$Sources{$tag}}, $_); + } + } + } +} + + +foreach $i (keys %Sources) { + my @args = @{$Sources{$i}}; + if ($globalwhite) { + push(@args, "file:$globalwhite"); + } + push (@blacklist, &addblack(@args)); +} + + +# replace spamd table with new blacklist. +open (PFCTL, "|pfctl -q -t spamd -T replace -f -") || + die ("can't exec pfctl"); +for ($i=0; $i<=$#blacklist; $i++) { + print PFCTL $blacklist[$i], "\n"; +} +close(PFCTL); diff --git a/libexec/spamd/spamd.8 b/libexec/spamd/spamd.8 index 99fe382d0ce..9eea8d0e559 100644 --- a/libexec/spamd/spamd.8 +++ b/libexec/spamd/spamd.8 @@ -1,4 +1,4 @@ -.\" $OpenBSD: spamd.8,v 1.11 2003/02/26 15:05:07 david Exp $ +.\" $OpenBSD: spamd.8,v 1.12 2003/03/02 19:22:00 beck Exp $ .\" .\" Copyright (c) 2002 Theo de Raadt. All rights reserved. .\" @@ -103,31 +103,65 @@ The rules used for this purpose are described in .Xr pf.conf 5 . The rules can be loaded into an -.Em anchor +.Em table to simplify handling. -If the main ruleset contains the following -.Em rdr-anchor rule , -all -.Em rdr -rules inside the specified -.Em anchor -are evaluated for SMTP connections: .Bd -literal - rdr-anchor spews proto tcp from any to any port smtp + table <spamd> + rdr proto tcp from { <spamd> } to any port smtp -> 127.0.0.1 port 8025 .Ed .Pp -And all -.Em rdr -rules related to +Any addresses in table +.Aq spamd +are then redirected to .Nm -can be loaded into one or more rulesets inside that -.Em anchor , +running on port 25. +Addresses can then be can be loaded into the +.Em table , like: .Bd -literal - echo "rdr inet proto tcp from { 10.1.2.3, 10.2.3.4/30, 10.3.4.5/24 } - to any port smtp -> 127.0.0.1 port 8025" | pfctl -a spews:first -f - + pfctl -q -t spamd -T replace -f /usr/local/share/spammers .Ed .Pp +.Xr spamd-setup 8 +can also be used to load addresses into the +.Aq spamd +table. +.Xr spamd-setup 8 +also has the added benefit of being able to remove addresses from +blacklists, and will connect to +.Nm +over a localhost socket, giving +.Nm +information about each source of blacklist addresses, as well as custom +rejection messages for each blacklist source +that can be used to let any real person whose mail +is deferred by spamd know why their address has been listed +from sending mail. This is important as it allows legitimate mail +senders to pressure spam sources into behaving properly so that they +may be removed from the relevant blacklists. +.Pp +.Sh CONFIGURATION CONNECTIONS +.Nm +receives configuration information through a localhost connection +Configuration information consists of blacklists, each with a name, +a message to reject mail with, and addresses in CIDR format. When +multiple lists are configured, Each configuration line specifies +a blacklist, and must have the format: +.Bd -literal + tag;"rejection message";aaa.bbb.ccc.ddd/mm;aaa.bbb.ccc.ddd/mm +.Ed + +The rejection message must be inside double quotes. a \e" will +produce a double quote in the output. \en will produce a newline. %A +will expand to the connecting ip address in dotted quad format, %% may +be used to produce a single % in the output, \e\e will produce a +single \e. +.Nm +will reject mail by displaying all the messages from all blacklists in which +a connecting address is matched. +.Xr spamd-setup 8 +is normally used to configure this informaiton. +.Pp .Sh SEE ALSO .Xr pf.conf 5 , .Xr pfctl 8 , diff --git a/libexec/spamd/spamd.c b/libexec/spamd/spamd.c index fed6346ff8f..245ec334438 100644 --- a/libexec/spamd/spamd.c +++ b/libexec/spamd/spamd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spamd.c,v 1.10 2003/02/11 01:41:10 deraadt Exp $ */ +/* $OpenBSD: spamd.c,v 1.11 2003/03/02 19:22:00 beck Exp $ */ /* * Copyright (c) 2002 Theo de Raadt. All rights reserved. @@ -41,6 +41,7 @@ #include <stdlib.h> #include <getopt.h> #include <err.h> +#include "sdl.h" char hostname[MAXHOSTNAMELEN]; struct syslog_data sdata = SYSLOG_DATA_INIT; @@ -48,6 +49,12 @@ char *reply = NULL; char *nreply = "450"; char *spamd = "spamd IP-based SPAM blocker"; +extern struct sdlist *blacklists; + +int conffd = -1; +char *cb; +size_t cbs, cbu; + time_t t; #define MAXCON 200 @@ -59,6 +66,9 @@ int debug; struct con { int fd; int state; + int af; + struct sockaddr_in sin; + void *ia; char addr[32]; char mail[64], rcpt[64]; @@ -75,7 +85,9 @@ struct con { int il; char rend[5]; /* any chars in here causes input termination */ - char obuf[8192]; + int obufalloc; + char *obuf; + size_t osize; char *op; int ol; } *con; @@ -88,21 +100,334 @@ usage(void) exit(1); } +char * +grow_obuf(struct con *cp, int off) +{ + char * tmp; + if (!cp->obufalloc) + cp->obuf = NULL; + tmp = realloc(cp->obuf, cp->osize + 8192); + if (tmp != NULL) { + cp->osize += 8192; + cp->obuf = tmp; + cp->obufalloc = 1; + return(cp->obuf + off); + } + return(NULL); +} + + +int +parse_configline(char *line) +{ + char *cp, prev, *name, *msg; + static char **av = NULL; + static size_t ac = 0; + size_t au = 0; + int mdone = 0; + + if (debug) + printf("read config line %40s ...\n", line); + + name = line; + + for (cp = name; *cp != ';'; cp++); + *cp++ = '\0'; + msg = cp; + if (*cp++ != '"') + goto parse_error; + prev = '\0'; + for (;!mdone;cp++) { + switch (*cp) { + case '\\': + if (!prev) + prev = *cp; + else + prev = '\0'; + break; + case '"': + if (prev != '\\') { + cp++; + if (*cp == ';') { + mdone = 1; + *cp = '\0'; + } else + goto parse_error; + } + break; + case '\0': + goto parse_error; + default: + prev = '\0'; + } + + } + + do { + if (ac == au) { + char **tmp; + tmp = realloc(av, (ac + 2048) * sizeof(char *)); + if (tmp == NULL) { + return (-1); + } + av = tmp; + ac += 2048; + } + } while ((av[au++] = strsep(&cp, ";")) != NULL); + if (au < 2) + goto parse_error; + else + sdl_add(name, msg, av, au - 1); + return(0); + + parse_error: + if (debug > 0) + printf("bogus config line - need 'tag;message;a/m;a/m;a/m...'\n"); + return (-1); + +} + + +void +parse_configs(void) { + int i; + char *start, *end; + + if (cbu == cbs) { + char * tmp; + tmp = realloc(cb, cbs + 8192); + if (tmp == NULL) { + if (debug > 0) + perror("malloc()"); + free(cb); + cbs = cbu = 0; + return; + } + cbs += 8192; + cb = tmp; + } + cb[cbu++]='\0'; + + start = cb; + end = start; + for (i = 0; i < cbu; i++) { + if (*end == '\n') { + *end = '\0'; + if (end > start + 1) + parse_configline(start); + start = ++end; + } + else + ++end; + } + if (end > start + 1) + parse_configline(start); +} + + +void +do_config(void) +{ + int n; + + if (debug > 0) + printf("got configuration connection\n"); + + if (cbu == cbs) { + char * tmp; + tmp = realloc(cb, cbs + 8192); + if (tmp == NULL) { + if (debug > 0) + perror("malloc()"); + free(cb); + cbs = 0; + goto configdone; + } + cbs += 8192; + cb = tmp; + } + + n = read(conffd, cb+cbu, cbs-cbu); + if (debug > 0) + printf("read %d config bytes\n", n); + if (n == 0) { + parse_configs(); + goto configdone; + } else if (n == -1) { + if (debug > 0) + perror("read()"); + goto configdone; + } else { + cbu += n; + } + return; + configdone: + cbu = 0; + close(conffd); + conffd = -1; +} + + +int +append_error_string (struct con *cp, size_t off, char *fmt, int af, void *ia) +{ + char sav = '\0'; + static int lastcont = 0; + char *c = cp->obuf + off; + char *s = fmt; + size_t len = cp->osize - off; + int i = 0; + + if (off == 0) { + lastcont = 0; + } + if (lastcont != 0) + cp->obuf[lastcont] = '-'; + i += snprintf(c, len, "%s ", nreply); + lastcont = off + i - 1; + if (*s == '"') + s++; + while (*s) { + /* make sure we at minimum, have room to add a + * format code (4 bytes), and a v6 address(39 bytes) + * and a byte saved in sav. + */ + if (i >= len - 46) { + c = grow_obuf(cp, off); + if (c == NULL) + goto no_mem; + len = cp->osize - (off + i); + } + + if (c[i-1] == '\n') { + if (lastcont != 0) + cp->obuf[lastcont] = '-'; + i += snprintf(c + i, len, "%s ", nreply); + lastcont = off + i - 1; + } + + switch (*s) { + case '\\': + case '%': + if (!sav) + sav = *s; + else { + c[i++] = sav; + sav = '\0'; + c[i] = '\0'; + } + break; + case '"': + case 'A': + case 'n': + if (*(s+1) == '\0') { + break; + } + if (sav == '\\' && *s == 'n') { + c[i++] = '\n'; + sav = '\0'; + c[i] = '\0'; + break; + } else if (sav == '\\' && *s == '"') { + c[i++] = '"'; + sav = '\0'; + c[i] = '\0'; + break; + } else if (sav == '%' && *s == 'A') { + inet_ntop(af, ia, c + i, (len - i)); + i += strlen(c + i); + sav = '\0'; + break; + } + /* fallthrough */ + default: + if (sav) + c[i++] = sav; + c[i++] = *s; + sav='\0'; + c[i] = '\0'; + break; + } + s++; + } + return(i); + no_mem: + /* Out of memory, free obuf and bail, caller must deal */ + if (cp->osize) + free(cp->obuf); + cp->osize = 0; + cp->obuf = NULL; + return(-1); +} + + +void +build_reply(struct con * cp) +{ + struct sdlist **matches; + int off = 0; + + matches = sdl_lookup(blacklists, cp->af, cp->ia); + if (matches == NULL) { + if (cp->osize) + free(cp->obuf); + cp->osize = 0; + cp->obuf = NULL; + goto bad; + } + for (; *matches; matches++) { + int used = 0; + char *c = cp->obuf + off; + int left = cp->osize - off; + used = append_error_string(cp, off, matches[0]->string, + cp->af, cp->ia); + if (used == -1) + goto bad; + off += used; + left -= used; + if (cp->obuf[off - 1] != '\n') { + if ( left < 1) { + c = grow_obuf(cp, off); + if (c == NULL) { + if (cp->osize) + free(cp->obuf); + cp->osize = 0; + cp->obuf = NULL; + goto bad; + } + } + cp->obuf[off++]='\n'; + cp->obuf[off]='\0'; + } + } + return; +bad: + /* Out of memory, or no match. give generic reply */ + asprintf(&cp->obuf, + "%s-Sorry %s\n" + "%s-You are trying to send mail from an address listed by one\n" + "%s or more IP-based registries as being a SPAM source.\n", + nreply, cp->addr, nreply, nreply); + if (cp->obuf == NULL) { + /* we're having a really bad day.. */ + cp->obufalloc = 0; /* know not to free or mangle */ + cp->obuf="450 Try again\r\n"; + } else { + cp->osize = strlen(cp->obuf) + 1; + } +} + void doreply(struct con *cp) { if (reply) { - snprintf(cp->obuf, sizeof cp->obuf, + if (!cp->obufalloc) + err(1, "shouldn't happen"); + snprintf(cp->obuf, cp->osize, "%s %s\n", nreply, reply); return; } - - snprintf(cp->obuf, sizeof cp->obuf, - "%s-SPAM. www.spews.org/ask.cgi?x=%s\n" - "%s-You are trying to send mail from an address listed by one or\n" - "%s-more IP-based registries as being in a SPAM-generating netblock.\n" - "%s SPAM. www.spews.org/ask.cgi?x=%s\n", - nreply, cp->addr, nreply, nreply, nreply, cp->addr); + build_reply(cp); } void @@ -131,10 +456,18 @@ initcon(struct con *cp, int fd, struct sockaddr_in *sin) time_t t; time(&t); + if (cp->obufalloc) { + free(cp->obuf); + } bzero(cp, sizeof(struct con)); + if (grow_obuf(cp, 0) == NULL) + err(1, "malloc"); cp->fd = fd; + memcpy(&cp->sin, sin, sizeof(struct sockaddr_in)); + cp->af = sin->sin_family; + cp->ia = (void *) &cp->sin.sin_addr; strlcpy(cp->addr, inet_ntoa(sin->sin_addr), sizeof(cp->addr)); - snprintf(cp->obuf, sizeof(cp->obuf), + snprintf(cp->obuf, cp->osize, "220 %s ESMTP %s; %s", hostname, spamd, ctime(&t)); cp->op = cp->obuf; @@ -154,6 +487,11 @@ closecon(struct con *cp) time(&t); printf("%s connected for %d seconds.\n", cp->addr, t - cp->s); } + if (cp->osize > 0 && cp->obufalloc) { + free(cp->obuf); + cp->obuf = NULL; + cp->osize = 0; + } close(cp->fd); clients--; cp->fd = -1; @@ -180,7 +518,7 @@ nextstate(struct con *cp) /* received input: parse, and select next state */ if (match(cp->ibuf, "HELO") || match(cp->ibuf, "EHLO")) { - snprintf(cp->obuf, sizeof cp->obuf, + snprintf(cp->obuf, cp->osize, "250 Hello, spam sender. " "Pleased to be wasting your time.\n"); cp->op = cp->obuf; @@ -201,9 +539,9 @@ nextstate(struct con *cp) case 3: if (match(cp->ibuf, "MAIL")) { setlog(cp->mail, sizeof cp->mail, cp->ibuf); - snprintf(cp->obuf, sizeof cp->obuf, + snprintf(cp->obuf, cp->osize, "250 You are about to try to deliver spam. " - "Your time will be spent, amounting to nothing.\n"); + "Your time will be spent, for nothing.\n"); cp->op = cp->obuf; cp->ol = strlen(cp->op); cp->state = 4; @@ -222,7 +560,7 @@ nextstate(struct con *cp) case 5: if (match(cp->ibuf, "RCPT")) { setlog(cp->rcpt, sizeof(cp->rcpt), cp->ibuf); - snprintf(cp->obuf, sizeof cp->obuf, + snprintf(cp->obuf, cp->osize, "250 This is hurting you more than it is " "hurting me.\n"); cp->op = cp->obuf; @@ -295,6 +633,19 @@ handlew(struct con *cp, int one) int n; if (cp->w) { + if (*cp->op == '\n') { + /* insert \r before \n */ + n = write(cp->fd, "\r", 1); + if (n == 0) { + closecon(cp); + goto handled; + } else if (n == -1) { + if (debug > 0 && errno != EPIPE) + perror("write()"); + closecon(cp); + goto handled; + } + } n = write(cp->fd, cp->op, one ? 1 : cp->ol); if (n == 0) { closecon(cp); @@ -307,6 +658,7 @@ handlew(struct con *cp, int one) cp->ol -= n; } } + handled: cp->w = t + 1; if (cp->ol == 0) { cp->w = 0; @@ -319,8 +671,9 @@ main(int argc, char *argv[]) { fd_set *fdsr = NULL, *fdsw = NULL; struct sockaddr_in sin; + struct sockaddr_in lin; struct passwd *pw; - int ch, s, s2, i, omax = 0; + int ch, s, s2, conflisten = 0, i, omax = 0; int sinlen, one = 1; u_short port = 8025; @@ -367,6 +720,12 @@ main(int argc, char *argv[]) if (con == NULL) err(1, "calloc"); + con->obuf = malloc(8192); + + if (con->obuf == NULL) + err(1, "malloc"); + con->osize = 8192; + for (i = 0; i < maxcon; i++) con[i].fd = -1; @@ -380,14 +739,32 @@ main(int argc, char *argv[]) sizeof(one)) == -1) return(-1); + conflisten = socket(AF_INET, SOCK_STREAM, 0); + if (conflisten == -1) + err(1, "socket"); + + if (setsockopt(conflisten, SOL_SOCKET, SO_REUSEADDR, &one, + sizeof(one)) == -1) + return(-1); + memset(&sin, 0, sizeof sin); sin.sin_len = sizeof(sin); + sin.sin_addr.s_addr = htonl(INADDR_ANY); sin.sin_family = AF_INET; sin.sin_port = htons(port); if (bind(s, (struct sockaddr *)&sin, sizeof sin) == -1) err(1, "bind"); + memset(&lin, 0, sizeof sin); + lin.sin_len = sizeof(sin); + lin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + lin.sin_family = AF_INET; + lin.sin_port = htons(port); + + if (bind(conflisten, (struct sockaddr *)&lin, sizeof lin) == -1) + err(1, "bind local"); + pw = getpwnam("_spamd"); if (!pw) pw = getpwnam("nobody"); @@ -408,6 +785,9 @@ main(int argc, char *argv[]) if (listen(s, 10) == -1) err(1, "listen"); + if (listen(conflisten, 10) == -1) + err(1, "listen"); + if (debug == 0) { if (daemon(1, 1) == -1) err(1, "fork"); @@ -416,8 +796,10 @@ main(int argc, char *argv[]) while (1) { struct timeval tv, *tvp; - int max = s, i, n; + int max, i, n; int writers; + max = MAX(s, conflisten); + max = MAX(max, conffd); time(&t); for (i = 0; i < maxcon; i++) @@ -466,6 +848,13 @@ main(int argc, char *argv[]) } FD_SET(s, fdsr); + /* only one active config conn at a time */ + if (conffd == -1) + FD_SET(conflisten, fdsr); + else + FD_SET(conffd, fdsr); + + if (writers == 0) { tvp = NULL; } else { @@ -502,6 +891,20 @@ main(int argc, char *argv[]) else initcon(&con[i], s2, &sin); } + if (FD_ISSET(conflisten, fdsr)) { + sinlen = sizeof(lin); + conffd = accept(conflisten, (struct sockaddr *)&lin, + &sinlen); + if (conffd == -1) { + if (errno == EINTR) + continue; + err(1, "accept"); + } + } + if (conffd != -1 && FD_ISSET(conffd, fdsr)) { + do_config(); + } + } exit(1); } |