plan9port

fork of plan9port with libvec, libstr and libsdb
Log | Files | Refs | README | LICENSE

greylist.c (6351B)


      1 #include "common.h"
      2 #include "smtpd.h"
      3 #include "smtp.h"
      4 #include <ctype.h>
      5 #include <ip.h>
      6 #include <ndb.h>
      7 
      8 typedef struct {
      9 	int	existed;	/* these two are distinct to cope with errors */
     10 	int	created;
     11 	int	noperm;
     12 	long	mtime;		/* mod time, iff it already existed */
     13 } Greysts;
     14 
     15 /*
     16  * There's a bit of a problem with yahoo; they apparently have a vast
     17  * pool of machines that all run the same queue(s), so a 451 retry can
     18  * come from a different IP address for many, many retries, and it can
     19  * take ~5 hours for the same IP to call us back.  Various other goofballs,
     20  * notably the IEEE, try to send mail just before 9 AM, then refuse to try
     21  * again until after 5 PM.  Doh!
     22  */
     23 enum {
     24 	Nonspammax = 14*60*60,  /* must call back within this time if real */
     25 };
     26 static char *whitelist = "#9/mail/lib/whitelist";
     27 
     28 /*
     29  * matches ip addresses or subnets in whitelist against nci->rsys.
     30  * ignores comments and blank lines in /mail/lib/whitelist.
     31  */
     32 static int
     33 onwhitelist(void)
     34 {
     35 	int lnlen;
     36 	char *line, *parse;
     37 	char input[128];
     38 	uchar ip[IPaddrlen], ipmasked[IPaddrlen];
     39 	uchar mask4[IPaddrlen], addr4[IPaddrlen];
     40 	uchar mask[IPaddrlen], addr[IPaddrlen], addrmasked[IPaddrlen];
     41 	Biobuf *wl;
     42 	static int beenhere;
     43 
     44 	if (!beenhere) {
     45 		beenhere = 1;
     46 		fmtinstall('I', eipfmt);
     47 		whitelist = unsharp(whitelist);
     48 	}
     49 
     50 	parseip(ip, nci->rsys);
     51 	wl = Bopen(whitelist, OREAD);
     52 	if (wl == nil)
     53 		return 1;
     54 	while ((line = Brdline(wl, '\n')) != nil) {
     55 		if (line[0] == '#' || line[0] == '\n')
     56 			continue;
     57 		lnlen = Blinelen(wl);
     58 		line[lnlen-1] = '\0';		/* clobber newline */
     59 
     60 		/* default mask is /32 (v4) or /128 (v6) for bare IP */
     61 		parse = line;
     62 		if (strchr(line, '/') == nil) {
     63 			strncpy(input, line, sizeof input - 5);
     64 			if (strchr(line, '.') != nil)
     65 				strcat(input, "/32");
     66 			else
     67 				strcat(input, "/128");
     68 			parse = input;
     69 		}
     70 		/* sorry, dave; where's parsecidr for v4 or v6? */
     71 		v4parsecidr(addr4, mask4, parse);
     72 		v4tov6(addr, addr4);
     73 		v4tov6(mask, mask4);
     74 
     75 		maskip(addr, mask, addrmasked);
     76 		maskip(ip, mask, ipmasked);
     77 		if (memcmp(ipmasked, addrmasked, IPaddrlen) == 0)
     78 			break;
     79 	}
     80 	Bterm(wl);
     81 	return line != nil;
     82 }
     83 
     84 static int mkdirs(char *);
     85 
     86 /*
     87  * if any directories leading up to path don't exist, create them.
     88  * modifies but restores path.
     89  */
     90 static int
     91 mkpdirs(char *path)
     92 {
     93 	int rv = 0;
     94 	char *sl = strrchr(path, '/');
     95 
     96 	if (sl != nil) {
     97 		*sl = '\0';
     98 		rv = mkdirs(path);
     99 		*sl = '/';
    100 	}
    101 	return rv;
    102 }
    103 
    104 /*
    105  * if path or any directories leading up to it don't exist, create them.
    106  * modifies but restores path.
    107  */
    108 static int
    109 mkdirs(char *path)
    110 {
    111 	int fd;
    112 
    113 	if (access(path, AEXIST) >= 0)
    114 		return 0;
    115 
    116 	/* make presumed-missing intermediate directories */
    117 	if (mkpdirs(path) < 0)
    118 		return -1;
    119 
    120 	/* make final directory */
    121 	fd = create(path, OREAD, 0777|DMDIR);
    122 	if (fd < 0)
    123 		/*
    124 		 * we may have lost a race; if the directory now exists,
    125 		 * it's okay.
    126 		 */
    127 		return access(path, AEXIST) < 0? -1: 0;
    128 	close(fd);
    129 	return 0;
    130 }
    131 
    132 static long
    133 getmtime(char *file)
    134 {
    135 	long mtime = -1;
    136 	Dir *ds = dirstat(file);
    137 
    138 	if (ds != nil) {
    139 		mtime = ds->mtime;
    140 		free(ds);
    141 	}
    142 	return mtime;
    143 }
    144 
    145 static void
    146 tryaddgrey(char *file, Greysts *gsp)
    147 {
    148 	int fd = create(file, OWRITE|OEXCL, 0444|DMEXCL);
    149 
    150 	gsp->created = (fd >= 0);
    151 	if (fd >= 0) {
    152 		close(fd);
    153 		gsp->existed = 0;  /* just created; couldn't have existed */
    154 	} else {
    155 		/*
    156 		 * why couldn't we create file? it must have existed
    157 		 * (or we were denied perm on parent dir.).
    158 		 * if it existed, fill in gsp->mtime; otherwise
    159 		 * make presumed-missing intermediate directories.
    160 		 */
    161 		gsp->existed = access(file, AEXIST) >= 0;
    162 		if (gsp->existed)
    163 			gsp->mtime = getmtime(file);
    164 		else if (mkpdirs(file) < 0)
    165 			gsp->noperm = 1;
    166 	}
    167 }
    168 
    169 static void
    170 addgreylist(char *file, Greysts *gsp)
    171 {
    172 	tryaddgrey(file, gsp);
    173 	if (!gsp->created && !gsp->existed && !gsp->noperm)
    174 		/* retry the greylist entry with parent dirs created */
    175 		tryaddgrey(file, gsp);
    176 }
    177 
    178 static int
    179 recentcall(Greysts *gsp)
    180 {
    181 	long delay = time(0) - gsp->mtime;
    182 
    183 	if (!gsp->existed)
    184 		return 0;
    185 	/* reject immediate call-back; spammers are doing that now */
    186 	return delay >= 30 && delay <= Nonspammax;
    187 }
    188 
    189 /*
    190  * policy: if (caller-IP, my-IP, rcpt) is not on the greylist,
    191  * reject this message as "451 temporary failure".  if the caller is real,
    192  * he'll retry soon, otherwise he's a spammer.
    193  * at the first rejection, create a greylist entry for (my-ip, caller-ip,
    194  * rcpt, time), where time is the file's mtime.  if they call back and there's
    195  * already a greylist entry, and it's within the allowed interval,
    196  * add their IP to the append-only whitelist.
    197  *
    198  * greylist files can be removed at will; at worst they'll cause a few
    199  * extra retries.
    200  */
    201 
    202 static int
    203 isrcptrecent(char *rcpt)
    204 {
    205 	char *user;
    206 	char file[256];
    207 	Greysts gs;
    208 	Greysts *gsp = &gs;
    209 
    210 	if (rcpt[0] == '\0' || strchr(rcpt, '/') != nil ||
    211 	    strcmp(rcpt, ".") == 0 || strcmp(rcpt, "..") == 0)
    212 		return 0;
    213 
    214 	/* shorten names to fit pre-fossil or pre-9p2000 file servers */
    215 	user = strrchr(rcpt, '!');
    216 	if (user == nil)
    217 		user = rcpt;
    218 	else
    219 		user++;
    220 
    221 	/* check & try to update the grey list entry */
    222 	snprint(file, sizeof file, "%s/mail/grey/%s/%s/%s",
    223 		get9root(), nci->lsys, nci->rsys, user);
    224 	memset(gsp, 0, sizeof *gsp);
    225 	addgreylist(file, gsp);
    226 
    227 	/* if on greylist already and prior call was recent, add to whitelist */
    228 	if (gsp->existed && recentcall(gsp)) {
    229 		syslog(0, "smtpd",
    230 			"%s/%s was grey; adding IP to white", nci->rsys, rcpt);
    231 		return 1;
    232 	} else if (gsp->existed)
    233 		syslog(0, "smtpd", "call for %s/%s was seconds ago or long ago",
    234 			nci->rsys, rcpt);
    235 	else
    236 		syslog(0, "smtpd", "no call registered for %s/%s; registering",
    237 			nci->rsys, rcpt);
    238 	return 0;
    239 }
    240 
    241 void
    242 vfysenderhostok(void)
    243 {
    244 	int recent = 0;
    245 	Link *l;
    246 
    247 	if (onwhitelist())
    248 		return;
    249 
    250 	for (l = rcvers.first; l; l = l->next)
    251 		if (isrcptrecent(s_to_c(l->p)))
    252 			recent = 1;
    253 
    254 	/* if on greylist already and prior call was recent, add to whitelist */
    255 	if (recent) {
    256 		int fd = create(whitelist, OWRITE, 0666|DMAPPEND);
    257 
    258 		if (fd >= 0) {
    259 			seek(fd, 0, 2);			/* paranoia */
    260 			fprint(fd, "# unknown\n%s\n\n", nci->rsys);
    261 			close(fd);
    262 		}
    263 	} else {
    264 		syslog(0, "smtpd",
    265 	"no recent call from %s for a rcpt; rejecting with temporary failure",
    266 			nci->rsys);
    267 		reply("451 please try again soon from the same IP.\r\n");
    268 		exits("no recent call for a rcpt");
    269 	}
    270 }