diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 16:18:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 16:18:56 +0000 |
commit | b7c15c31519dc44c1f691e0466badd556ffe9423 (patch) | |
tree | f944572f288bab482a615e09af627d9a2b6727d8 /src/postscreen | |
parent | Initial commit. (diff) | |
download | postfix-b7c15c31519dc44c1f691e0466badd556ffe9423.tar.xz postfix-b7c15c31519dc44c1f691e0466badd556ffe9423.zip |
Adding upstream version 3.7.10.upstream/3.7.10
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/postscreen')
l--------- | src/postscreen/.indent.pro | 1 | ||||
-rw-r--r-- | src/postscreen/Makefile.in | 428 | ||||
-rw-r--r-- | src/postscreen/postscreen.c | 1252 | ||||
-rw-r--r-- | src/postscreen/postscreen.h | 599 | ||||
-rw-r--r-- | src/postscreen/postscreen_dict.c | 182 | ||||
-rw-r--r-- | src/postscreen/postscreen_dnsbl.c | 624 | ||||
-rw-r--r-- | src/postscreen/postscreen_early.c | 377 | ||||
-rw-r--r-- | src/postscreen/postscreen_endpt.c | 232 | ||||
-rw-r--r-- | src/postscreen/postscreen_expand.c | 141 | ||||
-rw-r--r-- | src/postscreen/postscreen_haproxy.c | 137 | ||||
-rw-r--r-- | src/postscreen/postscreen_haproxy.h | 30 | ||||
-rw-r--r-- | src/postscreen/postscreen_misc.c | 164 | ||||
-rw-r--r-- | src/postscreen/postscreen_send.c | 293 | ||||
-rw-r--r-- | src/postscreen/postscreen_smtpd.c | 1339 | ||||
-rw-r--r-- | src/postscreen/postscreen_starttls.c | 317 | ||||
-rw-r--r-- | src/postscreen/postscreen_state.c | 317 | ||||
-rw-r--r-- | src/postscreen/postscreen_tests.c | 341 |
17 files changed, 6774 insertions, 0 deletions
diff --git a/src/postscreen/.indent.pro b/src/postscreen/.indent.pro new file mode 120000 index 0000000..5c837ec --- /dev/null +++ b/src/postscreen/.indent.pro @@ -0,0 +1 @@ +../../.indent.pro
\ No newline at end of file diff --git a/src/postscreen/Makefile.in b/src/postscreen/Makefile.in new file mode 100644 index 0000000..8ed8692 --- /dev/null +++ b/src/postscreen/Makefile.in @@ -0,0 +1,428 @@ +SHELL = /bin/sh +SRCS = postscreen.c postscreen_dict.c postscreen_dnsbl.c \ + postscreen_early.c postscreen_smtpd.c postscreen_misc.c \ + postscreen_state.c postscreen_tests.c postscreen_send.c \ + postscreen_starttls.c postscreen_expand.c postscreen_endpt.c \ + postscreen_haproxy.c +OBJS = postscreen.o postscreen_dict.o postscreen_dnsbl.o \ + postscreen_early.o postscreen_smtpd.o postscreen_misc.o \ + postscreen_state.o postscreen_tests.o postscreen_send.o \ + postscreen_starttls.o postscreen_expand.o postscreen_endpt.o \ + postscreen_haproxy.o +HDRS = +TESTSRC = +DEFS = -I. -I$(INC_DIR) -D$(SYSTYPE) +CFLAGS = $(DEBUG) $(OPT) $(DEFS) +TESTPROG= +PROG = postscreen +INC_DIR = ../../include +LIBS = ../../lib/lib$(LIB_PREFIX)master$(LIB_SUFFIX) \ + ../../lib/lib$(LIB_PREFIX)tls$(LIB_SUFFIX) \ + ../../lib/lib$(LIB_PREFIX)dns$(LIB_SUFFIX) \ + ../../lib/lib$(LIB_PREFIX)global$(LIB_SUFFIX) \ + ../../lib/lib$(LIB_PREFIX)util$(LIB_SUFFIX) + +.c.o:; $(CC) $(CFLAGS) -c $*.c + +$(PROG): $(OBJS) $(LIBS) + $(CC) $(CFLAGS) $(SHLIB_RPATH) -o $@ $(OBJS) $(LIBS) $(SYSLIBS) + +$(OBJS): ../../conf/makedefs.out + +Makefile: Makefile.in + cat ../../conf/makedefs.out $? >$@ + +test: $(TESTPROG) + +tests: test + +root_tests: + +update: ../../libexec/$(PROG) + +../../libexec/$(PROG): $(PROG) + cp $(PROG) ../../libexec + +printfck: $(OBJS) $(PROG) + rm -rf printfck + mkdir printfck + sed '1,/^# do not edit/!d' Makefile >printfck/Makefile + set -e; for i in *.c; do printfck -f .printfck $$i >printfck/$$i; done + cd printfck; make "INC_DIR=../../../include" `cd ..; ls *.o` + +lint: + lint $(DEFS) $(SRCS) $(LINTFIX) + +clean: + rm -f *.o *core $(PROG) $(TESTPROG) junk + rm -rf printfck + +tidy: clean + +depend: $(MAKES) + (sed '1,/^# do not edit/!d' Makefile.in; \ + set -e; for i in [a-z][a-z0-9]*.c; do \ + $(CC) -E $(DEFS) $(INCL) $$i | grep -v '[<>]' | sed -n -e '/^# *1 *"\([^"]*\)".*/{' \ + -e 's//'`echo $$i|sed 's/c$$/o/'`': \1/' \ + -e 's/o: \.\//o: /' -e p -e '}' ; \ + done | LANG=C sort -u) | grep -v '[.][o][:][ ][/]' >$$$$ && mv $$$$ Makefile.in + @$(EXPORT) make -f Makefile.in Makefile 1>&2 + +# do not edit below this line - it is generated by 'make depend' +postscreen.o: ../../include/addr_match_list.h +postscreen.o: ../../include/argv.h +postscreen.o: ../../include/attr.h +postscreen.o: ../../include/check_arg.h +postscreen.o: ../../include/data_redirect.h +postscreen.o: ../../include/dict.h +postscreen.o: ../../include/dict_cache.h +postscreen.o: ../../include/events.h +postscreen.o: ../../include/htable.h +postscreen.o: ../../include/inet_proto.h +postscreen.o: ../../include/iostuff.h +postscreen.o: ../../include/mail_conf.h +postscreen.o: ../../include/mail_params.h +postscreen.o: ../../include/mail_proto.h +postscreen.o: ../../include/mail_server.h +postscreen.o: ../../include/mail_version.h +postscreen.o: ../../include/maps.h +postscreen.o: ../../include/match_list.h +postscreen.o: ../../include/msg.h +postscreen.o: ../../include/myaddrinfo.h +postscreen.o: ../../include/myflock.h +postscreen.o: ../../include/mymalloc.h +postscreen.o: ../../include/name_code.h +postscreen.o: ../../include/nvtable.h +postscreen.o: ../../include/server_acl.h +postscreen.o: ../../include/set_eugid.h +postscreen.o: ../../include/string_list.h +postscreen.o: ../../include/sys_defs.h +postscreen.o: ../../include/vbuf.h +postscreen.o: ../../include/vstream.h +postscreen.o: ../../include/vstring.h +postscreen.o: postscreen.c +postscreen.o: postscreen.h +postscreen_dict.o: ../../include/addr_match_list.h +postscreen_dict.o: ../../include/argv.h +postscreen_dict.o: ../../include/check_arg.h +postscreen_dict.o: ../../include/dict.h +postscreen_dict.o: ../../include/dict_cache.h +postscreen_dict.o: ../../include/events.h +postscreen_dict.o: ../../include/htable.h +postscreen_dict.o: ../../include/maps.h +postscreen_dict.o: ../../include/match_list.h +postscreen_dict.o: ../../include/msg.h +postscreen_dict.o: ../../include/myaddrinfo.h +postscreen_dict.o: ../../include/myflock.h +postscreen_dict.o: ../../include/server_acl.h +postscreen_dict.o: ../../include/string_list.h +postscreen_dict.o: ../../include/sys_defs.h +postscreen_dict.o: ../../include/vbuf.h +postscreen_dict.o: ../../include/vstream.h +postscreen_dict.o: ../../include/vstring.h +postscreen_dict.o: postscreen.h +postscreen_dict.o: postscreen_dict.c +postscreen_dnsbl.o: ../../include/addr_match_list.h +postscreen_dnsbl.o: ../../include/argv.h +postscreen_dnsbl.o: ../../include/attr.h +postscreen_dnsbl.o: ../../include/check_arg.h +postscreen_dnsbl.o: ../../include/connect.h +postscreen_dnsbl.o: ../../include/dict.h +postscreen_dnsbl.o: ../../include/dict_cache.h +postscreen_dnsbl.o: ../../include/events.h +postscreen_dnsbl.o: ../../include/htable.h +postscreen_dnsbl.o: ../../include/iostuff.h +postscreen_dnsbl.o: ../../include/ip_match.h +postscreen_dnsbl.o: ../../include/mail_params.h +postscreen_dnsbl.o: ../../include/mail_proto.h +postscreen_dnsbl.o: ../../include/maps.h +postscreen_dnsbl.o: ../../include/match_list.h +postscreen_dnsbl.o: ../../include/msg.h +postscreen_dnsbl.o: ../../include/myaddrinfo.h +postscreen_dnsbl.o: ../../include/myflock.h +postscreen_dnsbl.o: ../../include/mymalloc.h +postscreen_dnsbl.o: ../../include/nvtable.h +postscreen_dnsbl.o: ../../include/server_acl.h +postscreen_dnsbl.o: ../../include/split_at.h +postscreen_dnsbl.o: ../../include/string_list.h +postscreen_dnsbl.o: ../../include/stringops.h +postscreen_dnsbl.o: ../../include/sys_defs.h +postscreen_dnsbl.o: ../../include/valid_hostname.h +postscreen_dnsbl.o: ../../include/vbuf.h +postscreen_dnsbl.o: ../../include/vstream.h +postscreen_dnsbl.o: ../../include/vstring.h +postscreen_dnsbl.o: postscreen.h +postscreen_dnsbl.o: postscreen_dnsbl.c +postscreen_early.o: ../../include/addr_match_list.h +postscreen_early.o: ../../include/argv.h +postscreen_early.o: ../../include/check_arg.h +postscreen_early.o: ../../include/dict.h +postscreen_early.o: ../../include/dict_cache.h +postscreen_early.o: ../../include/events.h +postscreen_early.o: ../../include/htable.h +postscreen_early.o: ../../include/mail_params.h +postscreen_early.o: ../../include/maps.h +postscreen_early.o: ../../include/match_list.h +postscreen_early.o: ../../include/msg.h +postscreen_early.o: ../../include/myaddrinfo.h +postscreen_early.o: ../../include/myflock.h +postscreen_early.o: ../../include/mymalloc.h +postscreen_early.o: ../../include/server_acl.h +postscreen_early.o: ../../include/string_list.h +postscreen_early.o: ../../include/stringops.h +postscreen_early.o: ../../include/sys_defs.h +postscreen_early.o: ../../include/vbuf.h +postscreen_early.o: ../../include/vstream.h +postscreen_early.o: ../../include/vstring.h +postscreen_early.o: postscreen.h +postscreen_early.o: postscreen_early.c +postscreen_endpt.o: ../../include/addr_match_list.h +postscreen_endpt.o: ../../include/argv.h +postscreen_endpt.o: ../../include/check_arg.h +postscreen_endpt.o: ../../include/dict.h +postscreen_endpt.o: ../../include/dict_cache.h +postscreen_endpt.o: ../../include/events.h +postscreen_endpt.o: ../../include/haproxy_srvr.h +postscreen_endpt.o: ../../include/htable.h +postscreen_endpt.o: ../../include/inet_proto.h +postscreen_endpt.o: ../../include/mail_params.h +postscreen_endpt.o: ../../include/maps.h +postscreen_endpt.o: ../../include/match_list.h +postscreen_endpt.o: ../../include/msg.h +postscreen_endpt.o: ../../include/myaddrinfo.h +postscreen_endpt.o: ../../include/myflock.h +postscreen_endpt.o: ../../include/server_acl.h +postscreen_endpt.o: ../../include/string_list.h +postscreen_endpt.o: ../../include/sys_defs.h +postscreen_endpt.o: ../../include/vbuf.h +postscreen_endpt.o: ../../include/vstream.h +postscreen_endpt.o: ../../include/vstring.h +postscreen_endpt.o: postscreen.h +postscreen_endpt.o: postscreen_endpt.c +postscreen_endpt.o: postscreen_haproxy.h +postscreen_expand.o: ../../include/addr_match_list.h +postscreen_expand.o: ../../include/argv.h +postscreen_expand.o: ../../include/attr.h +postscreen_expand.o: ../../include/check_arg.h +postscreen_expand.o: ../../include/dict.h +postscreen_expand.o: ../../include/dict_cache.h +postscreen_expand.o: ../../include/events.h +postscreen_expand.o: ../../include/htable.h +postscreen_expand.o: ../../include/iostuff.h +postscreen_expand.o: ../../include/mail_params.h +postscreen_expand.o: ../../include/mail_proto.h +postscreen_expand.o: ../../include/maps.h +postscreen_expand.o: ../../include/match_list.h +postscreen_expand.o: ../../include/msg.h +postscreen_expand.o: ../../include/myaddrinfo.h +postscreen_expand.o: ../../include/myflock.h +postscreen_expand.o: ../../include/mymalloc.h +postscreen_expand.o: ../../include/nvtable.h +postscreen_expand.o: ../../include/server_acl.h +postscreen_expand.o: ../../include/string_list.h +postscreen_expand.o: ../../include/stringops.h +postscreen_expand.o: ../../include/sys_defs.h +postscreen_expand.o: ../../include/vbuf.h +postscreen_expand.o: ../../include/vstream.h +postscreen_expand.o: ../../include/vstring.h +postscreen_expand.o: postscreen.h +postscreen_expand.o: postscreen_expand.c +postscreen_haproxy.o: ../../include/addr_match_list.h +postscreen_haproxy.o: ../../include/argv.h +postscreen_haproxy.o: ../../include/check_arg.h +postscreen_haproxy.o: ../../include/dict.h +postscreen_haproxy.o: ../../include/dict_cache.h +postscreen_haproxy.o: ../../include/events.h +postscreen_haproxy.o: ../../include/haproxy_srvr.h +postscreen_haproxy.o: ../../include/htable.h +postscreen_haproxy.o: ../../include/mail_params.h +postscreen_haproxy.o: ../../include/maps.h +postscreen_haproxy.o: ../../include/match_list.h +postscreen_haproxy.o: ../../include/msg.h +postscreen_haproxy.o: ../../include/myaddrinfo.h +postscreen_haproxy.o: ../../include/myflock.h +postscreen_haproxy.o: ../../include/mymalloc.h +postscreen_haproxy.o: ../../include/server_acl.h +postscreen_haproxy.o: ../../include/string_list.h +postscreen_haproxy.o: ../../include/stringops.h +postscreen_haproxy.o: ../../include/sys_defs.h +postscreen_haproxy.o: ../../include/vbuf.h +postscreen_haproxy.o: ../../include/vstream.h +postscreen_haproxy.o: ../../include/vstring.h +postscreen_haproxy.o: postscreen.h +postscreen_haproxy.o: postscreen_haproxy.c +postscreen_haproxy.o: postscreen_haproxy.h +postscreen_misc.o: ../../include/addr_match_list.h +postscreen_misc.o: ../../include/argv.h +postscreen_misc.o: ../../include/check_arg.h +postscreen_misc.o: ../../include/dict.h +postscreen_misc.o: ../../include/dict_cache.h +postscreen_misc.o: ../../include/events.h +postscreen_misc.o: ../../include/format_tv.h +postscreen_misc.o: ../../include/htable.h +postscreen_misc.o: ../../include/iostuff.h +postscreen_misc.o: ../../include/mail_params.h +postscreen_misc.o: ../../include/maps.h +postscreen_misc.o: ../../include/match_list.h +postscreen_misc.o: ../../include/msg.h +postscreen_misc.o: ../../include/myaddrinfo.h +postscreen_misc.o: ../../include/myflock.h +postscreen_misc.o: ../../include/server_acl.h +postscreen_misc.o: ../../include/string_list.h +postscreen_misc.o: ../../include/sys_defs.h +postscreen_misc.o: ../../include/vbuf.h +postscreen_misc.o: ../../include/vstream.h +postscreen_misc.o: ../../include/vstring.h +postscreen_misc.o: postscreen.h +postscreen_misc.o: postscreen_misc.c +postscreen_send.o: ../../include/addr_match_list.h +postscreen_send.o: ../../include/argv.h +postscreen_send.o: ../../include/attr.h +postscreen_send.o: ../../include/check_arg.h +postscreen_send.o: ../../include/connect.h +postscreen_send.o: ../../include/dict.h +postscreen_send.o: ../../include/dict_cache.h +postscreen_send.o: ../../include/events.h +postscreen_send.o: ../../include/htable.h +postscreen_send.o: ../../include/iostuff.h +postscreen_send.o: ../../include/mac_expand.h +postscreen_send.o: ../../include/mac_parse.h +postscreen_send.o: ../../include/mail_params.h +postscreen_send.o: ../../include/mail_proto.h +postscreen_send.o: ../../include/maps.h +postscreen_send.o: ../../include/match_list.h +postscreen_send.o: ../../include/msg.h +postscreen_send.o: ../../include/myaddrinfo.h +postscreen_send.o: ../../include/myflock.h +postscreen_send.o: ../../include/mymalloc.h +postscreen_send.o: ../../include/nvtable.h +postscreen_send.o: ../../include/server_acl.h +postscreen_send.o: ../../include/smtp_reply_footer.h +postscreen_send.o: ../../include/string_list.h +postscreen_send.o: ../../include/sys_defs.h +postscreen_send.o: ../../include/vbuf.h +postscreen_send.o: ../../include/vstream.h +postscreen_send.o: ../../include/vstring.h +postscreen_send.o: postscreen.h +postscreen_send.o: postscreen_send.c +postscreen_smtpd.o: ../../include/addr_match_list.h +postscreen_smtpd.o: ../../include/argv.h +postscreen_smtpd.o: ../../include/attr.h +postscreen_smtpd.o: ../../include/check_arg.h +postscreen_smtpd.o: ../../include/dict.h +postscreen_smtpd.o: ../../include/dict_cache.h +postscreen_smtpd.o: ../../include/dns.h +postscreen_smtpd.o: ../../include/ehlo_mask.h +postscreen_smtpd.o: ../../include/events.h +postscreen_smtpd.o: ../../include/htable.h +postscreen_smtpd.o: ../../include/info_log_addr_form.h +postscreen_smtpd.o: ../../include/iostuff.h +postscreen_smtpd.o: ../../include/is_header.h +postscreen_smtpd.o: ../../include/lex_822.h +postscreen_smtpd.o: ../../include/mail_params.h +postscreen_smtpd.o: ../../include/mail_proto.h +postscreen_smtpd.o: ../../include/maps.h +postscreen_smtpd.o: ../../include/match_list.h +postscreen_smtpd.o: ../../include/msg.h +postscreen_smtpd.o: ../../include/myaddrinfo.h +postscreen_smtpd.o: ../../include/myflock.h +postscreen_smtpd.o: ../../include/mymalloc.h +postscreen_smtpd.o: ../../include/name_code.h +postscreen_smtpd.o: ../../include/name_mask.h +postscreen_smtpd.o: ../../include/nvtable.h +postscreen_smtpd.o: ../../include/server_acl.h +postscreen_smtpd.o: ../../include/sock_addr.h +postscreen_smtpd.o: ../../include/string_list.h +postscreen_smtpd.o: ../../include/stringops.h +postscreen_smtpd.o: ../../include/sys_defs.h +postscreen_smtpd.o: ../../include/tls.h +postscreen_smtpd.o: ../../include/vbuf.h +postscreen_smtpd.o: ../../include/vstream.h +postscreen_smtpd.o: ../../include/vstring.h +postscreen_smtpd.o: postscreen.h +postscreen_smtpd.o: postscreen_smtpd.c +postscreen_starttls.o: ../../include/addr_match_list.h +postscreen_starttls.o: ../../include/argv.h +postscreen_starttls.o: ../../include/attr.h +postscreen_starttls.o: ../../include/check_arg.h +postscreen_starttls.o: ../../include/connect.h +postscreen_starttls.o: ../../include/dict.h +postscreen_starttls.o: ../../include/dict_cache.h +postscreen_starttls.o: ../../include/dns.h +postscreen_starttls.o: ../../include/events.h +postscreen_starttls.o: ../../include/htable.h +postscreen_starttls.o: ../../include/iostuff.h +postscreen_starttls.o: ../../include/mail_params.h +postscreen_starttls.o: ../../include/mail_proto.h +postscreen_starttls.o: ../../include/maps.h +postscreen_starttls.o: ../../include/match_list.h +postscreen_starttls.o: ../../include/msg.h +postscreen_starttls.o: ../../include/myaddrinfo.h +postscreen_starttls.o: ../../include/myflock.h +postscreen_starttls.o: ../../include/mymalloc.h +postscreen_starttls.o: ../../include/name_code.h +postscreen_starttls.o: ../../include/name_mask.h +postscreen_starttls.o: ../../include/nvtable.h +postscreen_starttls.o: ../../include/server_acl.h +postscreen_starttls.o: ../../include/sock_addr.h +postscreen_starttls.o: ../../include/string_list.h +postscreen_starttls.o: ../../include/stringops.h +postscreen_starttls.o: ../../include/sys_defs.h +postscreen_starttls.o: ../../include/tls.h +postscreen_starttls.o: ../../include/tls_proxy.h +postscreen_starttls.o: ../../include/vbuf.h +postscreen_starttls.o: ../../include/vstream.h +postscreen_starttls.o: ../../include/vstring.h +postscreen_starttls.o: postscreen.h +postscreen_starttls.o: postscreen_starttls.c +postscreen_state.o: ../../include/addr_match_list.h +postscreen_state.o: ../../include/argv.h +postscreen_state.o: ../../include/attr.h +postscreen_state.o: ../../include/check_arg.h +postscreen_state.o: ../../include/dict.h +postscreen_state.o: ../../include/dict_cache.h +postscreen_state.o: ../../include/events.h +postscreen_state.o: ../../include/htable.h +postscreen_state.o: ../../include/iostuff.h +postscreen_state.o: ../../include/mail_conf.h +postscreen_state.o: ../../include/mail_proto.h +postscreen_state.o: ../../include/mail_server.h +postscreen_state.o: ../../include/maps.h +postscreen_state.o: ../../include/match_list.h +postscreen_state.o: ../../include/msg.h +postscreen_state.o: ../../include/myaddrinfo.h +postscreen_state.o: ../../include/myflock.h +postscreen_state.o: ../../include/mymalloc.h +postscreen_state.o: ../../include/name_mask.h +postscreen_state.o: ../../include/nvtable.h +postscreen_state.o: ../../include/server_acl.h +postscreen_state.o: ../../include/string_list.h +postscreen_state.o: ../../include/sys_defs.h +postscreen_state.o: ../../include/vbuf.h +postscreen_state.o: ../../include/vstream.h +postscreen_state.o: ../../include/vstring.h +postscreen_state.o: postscreen.h +postscreen_state.o: postscreen_state.c +postscreen_tests.o: ../../include/addr_match_list.h +postscreen_tests.o: ../../include/argv.h +postscreen_tests.o: ../../include/check_arg.h +postscreen_tests.o: ../../include/dict.h +postscreen_tests.o: ../../include/dict_cache.h +postscreen_tests.o: ../../include/events.h +postscreen_tests.o: ../../include/htable.h +postscreen_tests.o: ../../include/mail_params.h +postscreen_tests.o: ../../include/maps.h +postscreen_tests.o: ../../include/match_list.h +postscreen_tests.o: ../../include/msg.h +postscreen_tests.o: ../../include/myaddrinfo.h +postscreen_tests.o: ../../include/myflock.h +postscreen_tests.o: ../../include/name_code.h +postscreen_tests.o: ../../include/sane_strtol.h +postscreen_tests.o: ../../include/server_acl.h +postscreen_tests.o: ../../include/string_list.h +postscreen_tests.o: ../../include/sys_defs.h +postscreen_tests.o: ../../include/vbuf.h +postscreen_tests.o: ../../include/vstream.h +postscreen_tests.o: ../../include/vstring.h +postscreen_tests.o: postscreen.h +postscreen_tests.o: postscreen_tests.c diff --git a/src/postscreen/postscreen.c b/src/postscreen/postscreen.c new file mode 100644 index 0000000..67716cb --- /dev/null +++ b/src/postscreen/postscreen.c @@ -0,0 +1,1252 @@ +/*++ +/* NAME +/* postscreen 8 +/* SUMMARY +/* Postfix zombie blocker +/* SYNOPSIS +/* \fBpostscreen\fR [generic Postfix daemon options] +/* DESCRIPTION +/* The Postfix \fBpostscreen\fR(8) server provides additional +/* protection against mail server overload. One \fBpostscreen\fR(8) +/* process handles multiple inbound SMTP connections, and decides +/* which clients may talk to a Postfix SMTP server process. +/* By keeping spambots away, \fBpostscreen\fR(8) leaves more +/* SMTP server processes available for legitimate clients, and +/* delays the onset of server overload conditions. +/* +/* This program should not be used on SMTP ports that receive +/* mail from end-user clients (MUAs). In a typical deployment, +/* \fBpostscreen\fR(8) handles the MX service on TCP port 25, and +/* \fBsmtpd\fR(8) receives mail from MUAs on the \fBsubmission\fR +/* service (TCP port 587) which requires client authentication. +/* Alternatively, a site could set up a dedicated, non-postscreen, +/* "port 25" server that provides \fBsubmission\fR service and +/* client authentication, but no MX service. +/* +/* \fBpostscreen\fR(8) maintains a temporary allowlist for +/* clients that have passed a number of tests. When an SMTP +/* client IP address is allowlisted, \fBpostscreen\fR(8) hands +/* off the connection immediately to a Postfix SMTP server +/* process. This minimizes the overhead for legitimate mail. +/* +/* By default, \fBpostscreen\fR(8) logs statistics and hands +/* off each connection to a Postfix SMTP server process, while +/* excluding clients in mynetworks from all tests (primarily, +/* to avoid problems with non-standard SMTP implementations +/* in network appliances). This default mode blocks no clients, +/* and is useful for non-destructive testing. +/* +/* In a typical production setting, \fBpostscreen\fR(8) is +/* configured to reject mail from clients that fail one or +/* more tests. \fBpostscreen\fR(8) logs rejected mail with the +/* client address, helo, sender and recipient information. +/* +/* \fBpostscreen\fR(8) is not an SMTP proxy; this is intentional. +/* The purpose is to keep spambots away from Postfix SMTP +/* server processes, while minimizing overhead for legitimate +/* traffic. +/* SECURITY +/* .ad +/* .fi +/* The \fBpostscreen\fR(8) server is moderately security-sensitive. +/* It talks to untrusted clients on the network. The process +/* can be run chrooted at fixed low privilege. +/* STANDARDS +/* RFC 821 (SMTP protocol) +/* RFC 1123 (Host requirements) +/* RFC 1652 (8bit-MIME transport) +/* RFC 1869 (SMTP service extensions) +/* RFC 1870 (Message Size Declaration) +/* RFC 1985 (ETRN command) +/* RFC 2034 (SMTP Enhanced Status Codes) +/* RFC 2821 (SMTP protocol) +/* Not: RFC 2920 (SMTP Pipelining) +/* RFC 3030 (CHUNKING without BINARYMIME) +/* RFC 3207 (STARTTLS command) +/* RFC 3461 (SMTP DSN Extension) +/* RFC 3463 (Enhanced Status Codes) +/* RFC 5321 (SMTP protocol, including multi-line 220 banners) +/* DIAGNOSTICS +/* Problems and transactions are logged to \fBsyslogd\fR(8) +/* or \fBpostlogd\fR(8). +/* BUGS +/* The \fBpostscreen\fR(8) built-in SMTP protocol engine +/* currently does not announce support for AUTH, XCLIENT or +/* XFORWARD. +/* If you need to make these services available +/* on port 25, then do not enable the optional "after 220 +/* server greeting" tests. +/* +/* The optional "after 220 server greeting" tests may result in +/* unexpected delivery delays from senders that retry email delivery +/* from a different IP address. Reason: after passing these tests a +/* new client must disconnect, and reconnect from the same IP +/* address before it can deliver mail. See POSTSCREEN_README, section +/* "Tests after the 220 SMTP server greeting", for a discussion. +/* CONFIGURATION PARAMETERS +/* .ad +/* .fi +/* Changes to main.cf are not picked up automatically, as +/* \fBpostscreen\fR(8) processes may run for several hours. +/* Use the command "postfix reload" after a configuration +/* change. +/* +/* The text below provides only a parameter summary. See +/* \fBpostconf\fR(5) for more details including examples. +/* +/* NOTE: Some \fBpostscreen\fR(8) parameters implement +/* stress-dependent behavior. This is supported only when the +/* default parameter value is stress-dependent (that is, it +/* looks like ${stress?{X}:{Y}}, or it is the $\fIname\fR +/* of an smtpd parameter with a stress-dependent default). +/* Other parameters always evaluate as if the \fBstress\fR +/* parameter value is the empty string. +/* COMPATIBILITY CONTROLS +/* .ad +/* .fi +/* .IP "\fBpostscreen_command_filter ($smtpd_command_filter)\fR" +/* A mechanism to transform commands from remote SMTP clients. +/* .IP "\fBpostscreen_discard_ehlo_keyword_address_maps ($smtpd_discard_ehlo_keyword_address_maps)\fR" +/* Lookup tables, indexed by the remote SMTP client address, with +/* case insensitive lists of EHLO keywords (pipelining, starttls, auth, +/* etc.) that the \fBpostscreen\fR(8) server will not send in the EHLO response +/* to a remote SMTP client. +/* .IP "\fBpostscreen_discard_ehlo_keywords ($smtpd_discard_ehlo_keywords)\fR" +/* A case insensitive list of EHLO keywords (pipelining, starttls, +/* auth, etc.) that the \fBpostscreen\fR(8) server will not send in the EHLO +/* response to a remote SMTP client. +/* .PP +/* Available in Postfix version 3.1 and later: +/* .IP "\fBdns_ncache_ttl_fix_enable (no)\fR" +/* Enable a workaround for future libc incompatibility. +/* .PP +/* Available in Postfix version 3.4 and later: +/* .IP "\fBpostscreen_reject_footer_maps ($smtpd_reject_footer_maps)\fR" +/* Optional lookup table for information that is appended after a 4XX +/* or 5XX \fBpostscreen\fR(8) server response. +/* .PP +/* Available in Postfix 3.6 and later: +/* .IP "\fBrespectful_logging (see 'postconf -d' output)\fR" +/* Avoid logging that implies white is better than black. +/* TROUBLE SHOOTING CONTROLS +/* .ad +/* .fi +/* .IP "\fBpostscreen_expansion_filter (see 'postconf -d' output)\fR" +/* List of characters that are permitted in postscreen_reject_footer +/* attribute expansions. +/* .IP "\fBpostscreen_reject_footer ($smtpd_reject_footer)\fR" +/* Optional information that is appended after a 4XX or 5XX +/* \fBpostscreen\fR(8) server +/* response. +/* .IP "\fBsoft_bounce (no)\fR" +/* Safety net to keep mail queued that would otherwise be returned to +/* the sender. +/* BEFORE-POSTSCREEN PROXY AGENT +/* .ad +/* .fi +/* Available in Postfix version 2.10 and later: +/* .IP "\fBpostscreen_upstream_proxy_protocol (empty)\fR" +/* The name of the proxy protocol used by an optional before-postscreen +/* proxy agent. +/* .IP "\fBpostscreen_upstream_proxy_timeout (5s)\fR" +/* The time limit for the proxy protocol specified with the +/* postscreen_upstream_proxy_protocol parameter. +/* PERMANENT ALLOW/DENYLIST TEST +/* .ad +/* .fi +/* This test is executed immediately after a remote SMTP client +/* connects. If a client is permanently allowlisted, the client +/* will be handed off immediately to a Postfix SMTP server +/* process. +/* .IP "\fBpostscreen_access_list (permit_mynetworks)\fR" +/* Permanent allow/denylist for remote SMTP client IP addresses. +/* .IP "\fBpostscreen_blacklist_action (ignore)\fR" +/* Renamed to postscreen_denylist_action in Postfix 3.6. +/* MAIL EXCHANGER POLICY TESTS +/* .ad +/* .fi +/* When \fBpostscreen\fR(8) is configured to monitor all primary +/* and backup MX addresses, it can refuse to allowlist clients +/* that connect to a backup MX address only. For small sites, +/* this requires configuring primary and backup MX addresses +/* on the same MTA. Larger sites would have to share the +/* \fBpostscreen\fR(8) cache between primary and backup MTAs, +/* which would introduce a common point of failure. +/* .IP "\fBpostscreen_whitelist_interfaces (static:all)\fR" +/* Renamed to postscreen_allowlist_interfaces in Postfix 3.6. +/* BEFORE 220 GREETING TESTS +/* .ad +/* .fi +/* These tests are executed before the remote SMTP client +/* receives the "220 servername" greeting. If no tests remain +/* after the successful completion of this phase, the client +/* will be handed off immediately to a Postfix SMTP server +/* process. +/* .IP "\fBdnsblog_service_name (dnsblog)\fR" +/* The name of the \fBdnsblog\fR(8) service entry in master.cf. +/* .IP "\fBpostscreen_dnsbl_action (ignore)\fR" +/* The action that \fBpostscreen\fR(8) takes when a remote SMTP client's combined +/* DNSBL score is equal to or greater than a threshold (as defined +/* with the postscreen_dnsbl_sites and postscreen_dnsbl_threshold +/* parameters). +/* .IP "\fBpostscreen_dnsbl_reply_map (empty)\fR" +/* A mapping from actual DNSBL domain name which includes a secret +/* password, to the DNSBL domain name that postscreen will reply with +/* when it rejects mail. +/* .IP "\fBpostscreen_dnsbl_sites (empty)\fR" +/* Optional list of DNS allow/denylist domains, filters and weight +/* factors. +/* .IP "\fBpostscreen_dnsbl_threshold (1)\fR" +/* The inclusive lower bound for blocking a remote SMTP client, based on +/* its combined DNSBL score as defined with the postscreen_dnsbl_sites +/* parameter. +/* .IP "\fBpostscreen_greet_action (ignore)\fR" +/* The action that \fBpostscreen\fR(8) takes when a remote SMTP client speaks +/* before its turn within the time specified with the postscreen_greet_wait +/* parameter. +/* .IP "\fBpostscreen_greet_banner ($smtpd_banner)\fR" +/* The \fItext\fR in the optional "220-\fItext\fR..." server +/* response that +/* \fBpostscreen\fR(8) sends ahead of the real Postfix SMTP server's "220 +/* text..." response, in an attempt to confuse bad SMTP clients so +/* that they speak before their turn (pre-greet). +/* .IP "\fBpostscreen_greet_wait (normal: 6s, overload: 2s)\fR" +/* The amount of time that \fBpostscreen\fR(8) will wait for an SMTP +/* client to send a command before its turn, and for DNS blocklist +/* lookup results to arrive (default: up to 2 seconds under stress, +/* up to 6 seconds otherwise). +/* .IP "\fBsmtpd_service_name (smtpd)\fR" +/* The internal service that \fBpostscreen\fR(8) hands off allowed +/* connections to. +/* .PP +/* Available in Postfix version 2.11 and later: +/* .IP "\fBpostscreen_dnsbl_whitelist_threshold (0)\fR" +/* Renamed to postscreen_dnsbl_allowlist_threshold in Postfix 3.6. +/* .PP +/* Available in Postfix version 3.0 and later: +/* .IP "\fBpostscreen_dnsbl_timeout (10s)\fR" +/* The time limit for DNSBL or DNSWL lookups. +/* .PP +/* Available in Postfix version 3.6 and later: +/* .IP "\fBpostscreen_denylist_action (ignore)\fR" +/* The action that \fBpostscreen\fR(8) takes when a remote SMTP client is +/* permanently denylisted with the postscreen_access_list parameter. +/* .IP "\fBpostscreen_allowlist_interfaces (static:all)\fR" +/* A list of local \fBpostscreen\fR(8) server IP addresses where a +/* non-allowlisted remote SMTP client can obtain \fBpostscreen\fR(8)'s temporary +/* allowlist status. +/* .IP "\fBpostscreen_dnsbl_allowlist_threshold (0)\fR" +/* Allow a remote SMTP client to skip "before" and "after 220 +/* greeting" protocol tests, based on its combined DNSBL score as +/* defined with the postscreen_dnsbl_sites parameter. +/* AFTER 220 GREETING TESTS +/* .ad +/* .fi +/* These tests are executed after the remote SMTP client +/* receives the "220 servername" greeting. If a client passes +/* all tests during this phase, it will receive a 4XX response +/* to all RCPT TO commands. After the client reconnects, it +/* will be allowed to talk directly to a Postfix SMTP server +/* process. +/* .IP "\fBpostscreen_bare_newline_action (ignore)\fR" +/* The action that \fBpostscreen\fR(8) takes when a remote SMTP client sends +/* a bare newline character, that is, a newline not preceded by carriage +/* return. +/* .IP "\fBpostscreen_bare_newline_enable (no)\fR" +/* Enable "bare newline" SMTP protocol tests in the \fBpostscreen\fR(8) +/* server. +/* .IP "\fBpostscreen_disable_vrfy_command ($disable_vrfy_command)\fR" +/* Disable the SMTP VRFY command in the \fBpostscreen\fR(8) daemon. +/* .IP "\fBpostscreen_forbidden_commands ($smtpd_forbidden_commands)\fR" +/* List of commands that the \fBpostscreen\fR(8) server considers in +/* violation of the SMTP protocol. +/* .IP "\fBpostscreen_helo_required ($smtpd_helo_required)\fR" +/* Require that a remote SMTP client sends HELO or EHLO before +/* commencing a MAIL transaction. +/* .IP "\fBpostscreen_non_smtp_command_action (drop)\fR" +/* The action that \fBpostscreen\fR(8) takes when a remote SMTP client sends +/* non-SMTP commands as specified with the postscreen_forbidden_commands +/* parameter. +/* .IP "\fBpostscreen_non_smtp_command_enable (no)\fR" +/* Enable "non-SMTP command" tests in the \fBpostscreen\fR(8) server. +/* .IP "\fBpostscreen_pipelining_action (enforce)\fR" +/* The action that \fBpostscreen\fR(8) takes when a remote SMTP client +/* sends +/* multiple commands instead of sending one command and waiting for +/* the server to respond. +/* .IP "\fBpostscreen_pipelining_enable (no)\fR" +/* Enable "pipelining" SMTP protocol tests in the \fBpostscreen\fR(8) +/* server. +/* CACHE CONTROLS +/* .ad +/* .fi +/* .IP "\fBpostscreen_cache_cleanup_interval (12h)\fR" +/* The amount of time between \fBpostscreen\fR(8) cache cleanup runs. +/* .IP "\fBpostscreen_cache_map (btree:$data_directory/postscreen_cache)\fR" +/* Persistent storage for the \fBpostscreen\fR(8) server decisions. +/* .IP "\fBpostscreen_cache_retention_time (7d)\fR" +/* The amount of time that \fBpostscreen\fR(8) will cache an expired +/* temporary allowlist entry before it is removed. +/* .IP "\fBpostscreen_bare_newline_ttl (30d)\fR" +/* The amount of time that \fBpostscreen\fR(8) will use the result from +/* a successful "bare newline" SMTP protocol test. +/* .IP "\fBpostscreen_dnsbl_max_ttl (${postscreen_dnsbl_ttl?{$postscreen_dnsbl_ttl}:{1}}h)\fR" +/* The maximum amount of time that \fBpostscreen\fR(8) will use the +/* result from a successful DNS-based reputation test before a +/* client IP address is required to pass that test again. +/* .IP "\fBpostscreen_dnsbl_min_ttl (60s)\fR" +/* The minimum amount of time that \fBpostscreen\fR(8) will use the +/* result from a successful DNS-based reputation test before a +/* client IP address is required to pass that test again. +/* .IP "\fBpostscreen_greet_ttl (1d)\fR" +/* The amount of time that \fBpostscreen\fR(8) will use the result from +/* a successful PREGREET test. +/* .IP "\fBpostscreen_non_smtp_command_ttl (30d)\fR" +/* The amount of time that \fBpostscreen\fR(8) will use the result from +/* a successful "non_smtp_command" SMTP protocol test. +/* .IP "\fBpostscreen_pipelining_ttl (30d)\fR" +/* The amount of time that \fBpostscreen\fR(8) will use the result from +/* a successful "pipelining" SMTP protocol test. +/* RESOURCE CONTROLS +/* .ad +/* .fi +/* .IP "\fBline_length_limit (2048)\fR" +/* Upon input, long lines are chopped up into pieces of at most +/* this length; upon delivery, long lines are reconstructed. +/* .IP "\fBpostscreen_client_connection_count_limit ($smtpd_client_connection_count_limit)\fR" +/* How many simultaneous connections any remote SMTP client is +/* allowed to have +/* with the \fBpostscreen\fR(8) daemon. +/* .IP "\fBpostscreen_command_count_limit (20)\fR" +/* The limit on the total number of commands per SMTP session for +/* \fBpostscreen\fR(8)'s built-in SMTP protocol engine. +/* .IP "\fBpostscreen_command_time_limit (normal: 300s, overload: 10s)\fR" +/* The time limit to read an entire command line with \fBpostscreen\fR(8)'s +/* built-in SMTP protocol engine. +/* .IP "\fBpostscreen_post_queue_limit ($default_process_limit)\fR" +/* The number of clients that can be waiting for service from a +/* real Postfix SMTP server process. +/* .IP "\fBpostscreen_pre_queue_limit ($default_process_limit)\fR" +/* The number of non-allowlisted clients that can be waiting for +/* a decision whether they will receive service from a real Postfix +/* SMTP server +/* process. +/* .IP "\fBpostscreen_watchdog_timeout (10s)\fR" +/* How much time a \fBpostscreen\fR(8) process may take to respond to +/* a remote SMTP client command or to perform a cache operation before it +/* is terminated by a built-in watchdog timer. +/* STARTTLS CONTROLS +/* .ad +/* .fi +/* .IP "\fBpostscreen_tls_security_level ($smtpd_tls_security_level)\fR" +/* The SMTP TLS security level for the \fBpostscreen\fR(8) server; when +/* a non-empty value is specified, this overrides the obsolete parameters +/* postscreen_use_tls and postscreen_enforce_tls. +/* .IP "\fBtlsproxy_service_name (tlsproxy)\fR" +/* The name of the \fBtlsproxy\fR(8) service entry in master.cf. +/* OBSOLETE STARTTLS SUPPORT CONTROLS +/* .ad +/* .fi +/* These parameters are supported for compatibility with +/* \fBsmtpd\fR(8) legacy parameters. +/* .IP "\fBpostscreen_use_tls ($smtpd_use_tls)\fR" +/* Opportunistic TLS: announce STARTTLS support to remote SMTP clients, +/* but do not require that clients use TLS encryption. +/* .IP "\fBpostscreen_enforce_tls ($smtpd_enforce_tls)\fR" +/* Mandatory TLS: announce STARTTLS support to remote SMTP clients, and +/* require that clients use TLS encryption. +/* MISCELLANEOUS CONTROLS +/* .ad +/* .fi +/* .IP "\fBconfig_directory (see 'postconf -d' output)\fR" +/* The default location of the Postfix main.cf and master.cf +/* configuration files. +/* .IP "\fBdelay_logging_resolution_limit (2)\fR" +/* The maximal number of digits after the decimal point when logging +/* sub-second delay values. +/* .IP "\fBcommand_directory (see 'postconf -d' output)\fR" +/* The location of all postfix administrative commands. +/* .IP "\fBmax_idle (100s)\fR" +/* The maximum amount of time that an idle Postfix daemon process waits +/* for an incoming connection before terminating voluntarily. +/* .IP "\fBprocess_id (read-only)\fR" +/* The process ID of a Postfix command or daemon process. +/* .IP "\fBprocess_name (read-only)\fR" +/* The process name of a Postfix command or daemon process. +/* .IP "\fBsyslog_facility (mail)\fR" +/* The syslog facility of Postfix logging. +/* .IP "\fBsyslog_name (see 'postconf -d' output)\fR" +/* A prefix that is prepended to the process name in syslog +/* records, so that, for example, "smtpd" becomes "prefix/smtpd". +/* .PP +/* Available in Postfix 3.3 and later: +/* .IP "\fBservice_name (read-only)\fR" +/* The master.cf service name of a Postfix daemon process. +/* .PP +/* Available in Postfix 3.5 and later: +/* .IP "\fBinfo_log_address_format (external)\fR" +/* The email address form that will be used in non-debug logging +/* (info, warning, etc.). +/* SEE ALSO +/* smtpd(8), Postfix SMTP server +/* tlsproxy(8), Postfix TLS proxy server +/* dnsblog(8), DNS allow/denylist logger +/* postlogd(8), Postfix logging +/* syslogd(8), system logging +/* README FILES +/* .ad +/* .fi +/* Use "\fBpostconf readme_directory\fR" or "\fBpostconf +/* html_directory\fR" to locate this information. +/* .nf +/* .na +/* POSTSCREEN_README, Postfix Postscreen Howto +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* HISTORY +/* .ad +/* .fi +/* This service was introduced with Postfix version 2.8. +/* +/* Many ideas in \fBpostscreen\fR(8) were explored in earlier +/* work by Michael Tokarev, in OpenBSD spamd, and in MailChannels +/* Traffic Control. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/stat.h> +#include <stdlib.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <events.h> +#include <myaddrinfo.h> +#include <dict_cache.h> +#include <set_eugid.h> +#include <vstream.h> +#include <name_code.h> +#include <inet_proto.h> + +/* Global library. */ + +#include <mail_conf.h> +#include <mail_params.h> +#include <mail_version.h> +#include <mail_proto.h> +#include <data_redirect.h> +#include <string_list.h> + +/* Master server protocols. */ + +#include <mail_server.h> + +/* Application-specific. */ + +#include <postscreen.h> + + /* + * Configuration parameters. + */ +char *var_smtpd_service; +char *var_smtpd_banner; +bool var_disable_vrfy_cmd; +bool var_helo_required; + +char *var_smtpd_cmd_filter; +char *var_psc_cmd_filter; + +char *var_smtpd_forbid_cmds; +char *var_psc_forbid_cmds; + +char *var_smtpd_ehlo_dis_words; +char *var_smtpd_ehlo_dis_maps; +char *var_psc_ehlo_dis_words; +char *var_psc_ehlo_dis_maps; + +char *var_smtpd_tls_level; +bool var_smtpd_use_tls; +bool var_smtpd_enforce_tls; +char *var_psc_tls_level; +bool var_psc_use_tls; +bool var_psc_enforce_tls; + +bool var_psc_disable_vrfy; +bool var_psc_helo_required; + +char *var_psc_cache_map; +int var_psc_cache_scan; +int var_psc_cache_ret; +int var_psc_post_queue_limit; +int var_psc_pre_queue_limit; +int var_psc_watchdog; + +char *var_psc_acl; +char *var_psc_dnlist_action; + +char *var_psc_greet_ttl; +int var_psc_greet_wait; + +char *var_psc_pregr_banner; +char *var_psc_pregr_action; +int var_psc_pregr_ttl; + +char *var_psc_dnsbl_sites; +char *var_psc_dnsbl_reply; +int var_psc_dnsbl_thresh; +int var_psc_dnsbl_althresh; +char *var_psc_dnsbl_action; +int var_psc_dnsbl_min_ttl; +int var_psc_dnsbl_max_ttl; +int var_psc_dnsbl_tmout; + +bool var_psc_pipel_enable; +char *var_psc_pipel_action; +int var_psc_pipel_ttl; + +bool var_psc_nsmtp_enable; +char *var_psc_nsmtp_action; +int var_psc_nsmtp_ttl; + +bool var_psc_barlf_enable; +char *var_psc_barlf_action; +int var_psc_barlf_ttl; + +int var_psc_cmd_count; +int var_psc_cmd_time; + +char *var_dnsblog_service; +char *var_tlsproxy_service; + +char *var_smtpd_rej_footer; +char *var_psc_rej_footer; +char *var_psc_rej_ftr_maps; + +int var_smtpd_cconn_limit; +int var_psc_cconn_limit; + +char *var_smtpd_exp_filter; +char *var_psc_exp_filter; + +char *var_psc_allist_if; +char *var_psc_uproxy_proto; +int var_psc_uproxy_tmout; + + /* + * Global variables. + */ +int psc_check_queue_length; /* connections being checked */ +int psc_post_queue_length; /* being sent to real SMTPD */ +DICT_CACHE *psc_cache_map; /* cache table handle */ +VSTRING *psc_temp; /* scratchpad */ +char *psc_smtpd_service_name; /* path to real SMTPD */ +int psc_pregr_action; /* PSC_ACT_DROP/ENFORCE/etc */ +int psc_dnsbl_action; /* PSC_ACT_DROP/ENFORCE/etc */ +int psc_pipel_action; /* PSC_ACT_DROP/ENFORCE/etc */ +int psc_nsmtp_action; /* PSC_ACT_DROP/ENFORCE/etc */ +int psc_barlf_action; /* PSC_ACT_DROP/ENFORCE/etc */ +int psc_min_ttl; /* Update with new tests! */ +STRING_LIST *psc_forbid_cmds; /* CONNECT GET POST */ +int psc_stress_greet_wait; /* stressed greet wait */ +int psc_normal_greet_wait; /* stressed greet wait */ +int psc_stress_cmd_time_limit; /* stressed command limit */ +int psc_normal_cmd_time_limit; /* normal command time limit */ +int psc_stress; /* stress level */ +int psc_lowat_check_queue_length; /* stress low-water mark */ +int psc_hiwat_check_queue_length; /* stress high-water mark */ +DICT *psc_dnsbl_reply; /* DNSBL name mapper */ +HTABLE *psc_client_concurrency; /* per-client concurrency */ + + /* + * Local variables and functions. + */ +static ARGV *psc_acl; /* permanent allow/denylist */ +static int psc_dnlist_action; /* PSC_ACT_DROP/ENFORCE/etc */ +static ADDR_MATCH_LIST *psc_allist_if; /* allowlist interfaces */ + +static void psc_endpt_lookup_done(int, VSTREAM *, + MAI_HOSTADDR_STR *, MAI_SERVPORT_STR *, + MAI_HOSTADDR_STR *, MAI_SERVPORT_STR *); + +/* psc_dump - dump some statistics before exit */ + +static void psc_dump(char *unused_service, char **unused_argv) +{ + + /* + * Dump preliminary cache cleanup statistics when the process commits + * suicide while a cache cleanup run is in progress. We can't currently + * distinguish between "postfix reload" (we should restart) or "maximal + * idle time reached" (we could finish the cache cleanup first). + */ + if (psc_cache_map) { + dict_cache_close(psc_cache_map); + psc_cache_map = 0; + } +} + +/* psc_drain - delayed exit after "postfix reload" */ + +static void psc_drain(char *unused_service, char **unused_argv) +{ + int count; + + /* + * After "postfix reload", complete work-in-progress in the background, + * instead of dropping already-accepted connections on the floor. + * + * Unfortunately we must close all writable tables, so we can't store or + * look up reputation information. The reason is that we don't have any + * multi-writer safety guarantees. We also can't use the single-writer + * proxywrite service, because its latency guarantees are too weak. + * + * All error retry counts shall be limited. Instead of blocking here, we + * could retry failed fork() operations in the event call-back routines, + * but we don't need perfection. The host system is severely overloaded + * and service levels are already way down. + * + * XXX Some Berkeley DB versions break with close-after-fork. Every new + * version is an improvement over its predecessor. + * + * XXX Don't assume that it is OK to share the same LMDB lockfile descriptor + * between different processes. + */ + if (psc_cache_map != 0 /* XXX && psc_cache_map + requires locking */ ) { + dict_cache_close(psc_cache_map); + psc_cache_map = 0; + } + for (count = 0; /* see below */ ; count++) { + if (count >= 5) { + msg_fatal("fork: %m"); + } else if (event_server_drain() != 0) { + msg_warn("fork: %m"); + sleep(1); + continue; + } else { + return; + } + } +} + +/* psc_service - handle new client connection */ + +static void psc_service(VSTREAM *smtp_client_stream, + char *unused_service, + char **unused_argv) +{ + + /* + * For sanity, require that at least one of INET or INET6 is enabled. + * Otherwise, we can't look up interface information, and we can't + * convert names or addresses. + */ + if (inet_proto_info()->ai_family_list[0] == 0) + msg_fatal("all network protocols are disabled (%s = %s)", + VAR_INET_PROTOCOLS, var_inet_protocols); + + /* + * This program handles all incoming connections, so it must not block. + * We use event-driven code for all operations that introduce latency. + * + * Note: instead of using VSTREAM-level timeouts, we enforce limits on the + * total amount of time to receive a complete SMTP command line. + */ + non_blocking(vstream_fileno(smtp_client_stream), NON_BLOCKING); + + /* + * Look up the remote SMTP client address and port. + */ + psc_endpt_lookup(smtp_client_stream, psc_endpt_lookup_done); +} + +/* psc_warn_compat_respectful_logging - compatibility warning */ + +static void psc_warn_compat_respectful_logging(PSC_STATE *state) +{ + msg_info("using backwards-compatible default setting " + VAR_RESPECTFUL_LOGGING "=no for client [%s]:%s", + PSC_CLIENT_ADDR_PORT(state)); + warn_compat_respectful_logging = 0; +} + +/* psc_endpt_lookup_done - endpoint lookup completed */ + +static void psc_endpt_lookup_done(int endpt_status, + VSTREAM *smtp_client_stream, + MAI_HOSTADDR_STR *smtp_client_addr, + MAI_SERVPORT_STR *smtp_client_port, + MAI_HOSTADDR_STR *smtp_server_addr, + MAI_SERVPORT_STR *smtp_server_port) +{ + const char *myname = "psc_endpt_lookup_done"; + PSC_STATE *state; + const char *stamp_str; + int saved_flags; + + /* + * Best effort - if this non-blocking write(2) fails, so be it. + */ + if (endpt_status < 0) { + (void) write(vstream_fileno(smtp_client_stream), + "421 4.3.2 No system resources\r\n", + sizeof("421 4.3.2 No system resources\r\n") - 1); + event_server_disconnect(smtp_client_stream); + return; + } + if (msg_verbose > 1) + msg_info("%s: sq=%d cq=%d connect from [%s]:%s", + myname, psc_post_queue_length, psc_check_queue_length, + smtp_client_addr->buf, smtp_client_port->buf); + + msg_info("CONNECT from [%s]:%s to [%s]:%s", + smtp_client_addr->buf, smtp_client_port->buf, + smtp_server_addr->buf, smtp_server_port->buf); + + /* + * Bundle up all the loose session pieces. This zeroes all flags and time + * stamps. + */ + state = psc_new_session_state(smtp_client_stream, smtp_client_addr->buf, + smtp_client_port->buf, + smtp_server_addr->buf, + smtp_server_port->buf); + + /* + * Reply with 421 when the client has too many open connections. + */ + if (var_psc_cconn_limit > 0 + && state->client_info->concurrency > var_psc_cconn_limit) { + msg_info("NOQUEUE: reject: CONNECT from [%s]:%s: too many connections", + state->smtp_client_addr, state->smtp_client_port); + PSC_DROP_SESSION_STATE(state, + "421 4.7.0 Error: too many connections\r\n"); + return; + } + + /* + * Reply with 421 when we can't forward more connections. + */ + if (var_psc_post_queue_limit > 0 + && psc_post_queue_length >= var_psc_post_queue_limit) { + msg_info("NOQUEUE: reject: CONNECT from [%s]:%s: all server ports busy", + state->smtp_client_addr, state->smtp_client_port); + PSC_DROP_SESSION_STATE(state, + "421 4.3.2 All server ports are busy\r\n"); + return; + } + + /* + * The permanent allow/denylist has highest precedence. + */ + if (psc_acl != 0) { + switch (psc_acl_eval(state, psc_acl, VAR_PSC_ACL)) { + + /* + * Permanently denylisted. + */ + case PSC_ACL_ACT_DENYLIST: + msg_info("%sLISTED [%s]:%s", + var_respectful_logging ? "DENY" : "BLACK", + PSC_CLIENT_ADDR_PORT(state)); + if (warn_compat_respectful_logging) + psc_warn_compat_respectful_logging(state); + PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_DNLIST_FAIL); + switch (psc_dnlist_action) { + case PSC_ACT_DROP: + PSC_DROP_SESSION_STATE(state, + "521 5.3.2 Service currently unavailable\r\n"); + return; + case PSC_ACT_ENFORCE: + PSC_ENFORCE_SESSION_STATE(state, + "550 5.3.2 Service currently unavailable\r\n"); + break; + case PSC_ACT_IGNORE: + PSC_UNFAIL_SESSION_STATE(state, PSC_STATE_FLAG_DNLIST_FAIL); + + /* + * Not: PSC_PASS_SESSION_STATE. Repeat this test the next + * time. + */ + break; + default: + msg_panic("%s: unknown denylist action value %d", + myname, psc_dnlist_action); + } + break; + + /* + * Permanently allowlisted. + */ + case PSC_ACL_ACT_ALLOWLIST: + msg_info("%sLISTED [%s]:%s", + var_respectful_logging ? "ALLOW" : "WHITE", + PSC_CLIENT_ADDR_PORT(state)); + if (warn_compat_respectful_logging) + psc_warn_compat_respectful_logging(state); + psc_conclude(state); + return; + + /* + * Other: dunno (don't know) or error. + */ + default: + break; + } + } + + /* + * The temporary allowlist (i.e. the postscreen cache) has the lowest + * precedence. This cache contains information about the results of prior + * tests. Allowlist the client when all enabled test results are still + * valid. + */ + if ((state->flags & PSC_STATE_MASK_ANY_FAIL) == 0 + && state->client_info->concurrency == 1 + && psc_cache_map != 0 + && (stamp_str = psc_cache_lookup(psc_cache_map, state->smtp_client_addr)) != 0) { + saved_flags = state->flags; + psc_parse_tests(state, stamp_str, event_time()); + state->flags |= saved_flags; + if (msg_verbose) + msg_info("%s: cached + recent flags: %s", + myname, psc_print_state_flags(state->flags, myname)); + if ((state->flags & PSC_STATE_MASK_ANY_TODO_FAIL) == 0) { + msg_info("PASS OLD [%s]:%s", PSC_CLIENT_ADDR_PORT(state)); + psc_conclude(state); + return; + } + } else if (state->client_info->concurrency > 1) { + saved_flags = state->flags; + psc_todo_tests(state, event_time()); + state->flags |= saved_flags; + if (msg_verbose) + msg_info("%s: new + recent flags: %s", + myname, psc_print_state_flags(state->flags, myname)); + } else { + saved_flags = state->flags; + psc_new_tests(state); + state->flags |= saved_flags; + if (msg_verbose) + msg_info("%s: new + recent flags: %s", + myname, psc_print_state_flags(state->flags, myname)); + } + + /* + * Don't allowlist clients that connect to backup MX addresses. Fail + * "closed" on error. + */ + if (addr_match_list_match(psc_allist_if, smtp_server_addr->buf) == 0) { + state->flags |= (PSC_STATE_FLAG_ALLIST_FAIL | PSC_STATE_FLAG_NOFORWARD); + msg_info("%sLIST VETO [%s]:%s", var_respectful_logging ? + "ALLOW" : "WHITE", PSC_CLIENT_ADDR_PORT(state)); + if (warn_compat_respectful_logging) + psc_warn_compat_respectful_logging(state); + } + + /* + * Reply with 421 when we can't analyze more connections. That also means + * no deep protocol tests when the noforward flag is raised. + */ + if (var_psc_pre_queue_limit > 0 + && psc_check_queue_length - psc_post_queue_length + >= var_psc_pre_queue_limit) { + msg_info("reject: connect from [%s]:%s: all screening ports busy", + state->smtp_client_addr, state->smtp_client_port); + PSC_DROP_SESSION_STATE(state, + "421 4.3.2 All screening ports are busy\r\n"); + return; + } + + /* + * If the client has no up-to-date results for some tests, do those tests + * first. Otherwise, skip the tests and hand off the connection. + */ + if (state->flags & PSC_STATE_MASK_EARLY_TODO) + psc_early_tests(state); + else if (state->flags & (PSC_STATE_MASK_SMTPD_TODO | PSC_STATE_FLAG_NOFORWARD)) + psc_smtpd_tests(state); + else + psc_conclude(state); +} + +/* psc_cache_validator - validate one cache entry */ + +static int psc_cache_validator(const char *client_addr, + const char *stamp_str, + void *unused_context) +{ + PSC_STATE dummy_state; + PSC_CLIENT_INFO dummy_client_info; + + /* + * This function is called by the cache cleanup pseudo thread. + * + * When an entry is removed from the cache, the client will be reported as + * "NEW" in the next session where it passes all tests again. To avoid + * silly logging we remove the cache entry only after all tests have + * expired longer ago than the cache retention time. + */ + dummy_state.client_info = &dummy_client_info; + psc_parse_tests(&dummy_state, stamp_str, event_time() - var_psc_cache_ret); + return ((dummy_state.flags & PSC_STATE_MASK_ANY_TODO) == 0); +} + +/* pre_jail_init - pre-jail initialization */ + +static void pre_jail_init(char *unused_name, char **unused_argv) +{ + VSTRING *redirect; + + /* + * Open read-only maps before dropping privilege, for consistency with + * other Postfix daemons. + */ + psc_acl_pre_jail_init(var_mynetworks, VAR_PSC_ACL); + if (*var_psc_acl) + psc_acl = psc_acl_parse(var_psc_acl, VAR_PSC_ACL); + /* Ignore smtpd_forbid_cmds lookup errors. Non-critical feature. */ + if (*var_psc_forbid_cmds) + psc_forbid_cmds = string_list_init(VAR_PSC_FORBID_CMDS, + MATCH_FLAG_RETURN, + var_psc_forbid_cmds); + if (*var_psc_dnsbl_reply) + psc_dnsbl_reply = dict_open(var_psc_dnsbl_reply, O_RDONLY, + DICT_FLAG_DUP_WARN); + + /* + * Never, ever, get killed by a master signal, as that would corrupt the + * database when we're in the middle of an update. + */ + if (setsid() < 0) + msg_warn("setsid: %m"); + + /* + * Security: don't create root-owned files that contain untrusted data. + * And don't create Postfix-owned files in root-owned directories, + * either. We want a correct relationship between (file or directory) + * ownership and (file or directory) content. To open files before going + * to jail, temporarily drop root privileges. + */ + SAVE_AND_SET_EUGID(var_owner_uid, var_owner_gid); + redirect = vstring_alloc(100); + + /* + * Keep state in persistent external map. As a safety measure we sync the + * database on each update. This hurts on LINUX file systems that sync + * all dirty disk blocks whenever any application invokes fsync(). + * + * Start the cache maintenance pseudo thread after dropping privileges. + */ +#define PSC_DICT_OPEN_FLAGS (DICT_FLAG_DUP_REPLACE | DICT_FLAG_SYNC_UPDATE | \ + DICT_FLAG_OPEN_LOCK) + + if (*var_psc_cache_map) + psc_cache_map = + dict_cache_open(data_redirect_map(redirect, var_psc_cache_map), + O_CREAT | O_RDWR, PSC_DICT_OPEN_FLAGS); + + /* + * Clean up and restore privilege. + */ + vstring_free(redirect); + RESTORE_SAVED_EUGID(); + + /* + * Initialize the dummy SMTP engine. + */ + psc_smtpd_pre_jail_init(); +} + +/* pre_accept - see if tables have changed */ + +static void pre_accept(char *unused_name, char **unused_argv) +{ + static time_t last_event_time; + time_t new_event_time; + const char *name; + + /* + * If some table has changed then stop accepting new connections. Don't + * check the tables more than once a second. + */ + new_event_time = event_time(); + if (new_event_time >= last_event_time + 1 + && (name = dict_changed_name()) != 0) { + msg_info("table %s has changed - finishing in the background", name); + event_server_drain(); + } else { + last_event_time = new_event_time; + } +} + +/* post_jail_init - post-jail initialization */ + +static void post_jail_init(char *unused_name, char **unused_argv) +{ + const NAME_CODE actions[] = { + PSC_NAME_ACT_DROP, PSC_ACT_DROP, + PSC_NAME_ACT_ENFORCE, PSC_ACT_ENFORCE, + PSC_NAME_ACT_IGNORE, PSC_ACT_IGNORE, + PSC_NAME_ACT_CONT, PSC_ACT_IGNORE, /* compatibility */ + 0, -1, + }; + int cache_flags; + const char *tmp; + + /* + * This routine runs after the skeleton code has entered the chroot jail. + * Prevent automatic process suicide after a limited number of client + * requests. It is OK to terminate after a limited amount of idle time. + */ + var_use_limit = 0; + + /* + * Workaround for parameters whose values may contain "$", and that have + * a default of "$parametername". Not sure if it would be a good idea to + * always to this in the mail_conf_raw(3) module. + */ + if (*var_psc_rej_footer == '$' + && mail_conf_lookup(var_psc_rej_footer + 1)) { + tmp = mail_conf_eval_once(var_psc_rej_footer); + myfree(var_psc_rej_footer); + var_psc_rej_footer = mystrdup(tmp); + } + if (*var_psc_exp_filter == '$' + && mail_conf_lookup(var_psc_exp_filter + 1)) { + tmp = mail_conf_eval_once(var_psc_exp_filter); + myfree(var_psc_exp_filter); + var_psc_exp_filter = mystrdup(tmp); + } + + /* + * Other one-time initialization. + */ + psc_temp = vstring_alloc(10); + vstring_sprintf(psc_temp, "%s/%s", MAIL_CLASS_PRIVATE, var_smtpd_service); + psc_smtpd_service_name = mystrdup(STR(psc_temp)); + psc_dnsbl_init(); + psc_early_init(); + psc_smtpd_init(); + + if ((psc_dnlist_action = name_code(actions, NAME_CODE_FLAG_NONE, + var_psc_dnlist_action)) < 0) + msg_fatal("bad %s value: %s", VAR_PSC_DNLIST_ACTION, + var_psc_dnlist_action); + if ((psc_dnsbl_action = name_code(actions, NAME_CODE_FLAG_NONE, + var_psc_dnsbl_action)) < 0) + msg_fatal("bad %s value: %s", VAR_PSC_DNSBL_ACTION, + var_psc_dnsbl_action); + if ((psc_pregr_action = name_code(actions, NAME_CODE_FLAG_NONE, + var_psc_pregr_action)) < 0) + msg_fatal("bad %s value: %s", VAR_PSC_PREGR_ACTION, + var_psc_pregr_action); + if ((psc_pipel_action = name_code(actions, NAME_CODE_FLAG_NONE, + var_psc_pipel_action)) < 0) + msg_fatal("bad %s value: %s", VAR_PSC_PIPEL_ACTION, + var_psc_pipel_action); + if ((psc_nsmtp_action = name_code(actions, NAME_CODE_FLAG_NONE, + var_psc_nsmtp_action)) < 0) + msg_fatal("bad %s value: %s", VAR_PSC_NSMTP_ACTION, + var_psc_nsmtp_action); + if ((psc_barlf_action = name_code(actions, NAME_CODE_FLAG_NONE, + var_psc_barlf_action)) < 0) + msg_fatal("bad %s value: %s", VAR_PSC_BARLF_ACTION, + var_psc_barlf_action); + /* Fail "closed" on error. */ + psc_allist_if = addr_match_list_init(VAR_PSC_ALLIST_IF, MATCH_FLAG_RETURN, + var_psc_allist_if); + + /* + * Start the cache maintenance pseudo thread last. Early cleanup makes + * verbose logging more informative (we get positive confirmation that + * the cleanup thread runs). + */ + cache_flags = DICT_CACHE_FLAG_STATISTICS; + if (msg_verbose > 1) + cache_flags |= DICT_CACHE_FLAG_VERBOSE; + if (psc_cache_map != 0 && var_psc_cache_scan > 0) + dict_cache_control(psc_cache_map, + CA_DICT_CACHE_CTL_FLAGS(cache_flags), + CA_DICT_CACHE_CTL_INTERVAL(var_psc_cache_scan), + CA_DICT_CACHE_CTL_VALIDATOR(psc_cache_validator), + CA_DICT_CACHE_CTL_CONTEXT((void *) 0), + CA_DICT_CACHE_CTL_END); + + /* + * Pre-compute the minimal and maximal TTL. + */ + psc_min_ttl = + PSC_MIN(PSC_MIN(var_psc_pregr_ttl, var_psc_dnsbl_min_ttl), + PSC_MIN(PSC_MIN(var_psc_pipel_ttl, var_psc_nsmtp_ttl), + var_psc_barlf_ttl)); + + /* + * Pre-compute the stress and normal command time limits. + */ + mail_conf_update(VAR_STRESS, "yes"); + psc_stress_cmd_time_limit = + get_mail_conf_time(VAR_PSC_CMD_TIME, DEF_PSC_CMD_TIME, 1, 0); + psc_stress_greet_wait = + get_mail_conf_time(VAR_PSC_GREET_WAIT, DEF_PSC_GREET_WAIT, 1, 0); + + mail_conf_update(VAR_STRESS, ""); + psc_normal_cmd_time_limit = + get_mail_conf_time(VAR_PSC_CMD_TIME, DEF_PSC_CMD_TIME, 1, 0); + psc_normal_greet_wait = + get_mail_conf_time(VAR_PSC_GREET_WAIT, DEF_PSC_GREET_WAIT, 1, 0); + + psc_lowat_check_queue_length = .7 * var_psc_pre_queue_limit; + psc_hiwat_check_queue_length = .9 * var_psc_pre_queue_limit; + if (msg_verbose) + msg_info(VAR_PSC_CMD_TIME ": stress=%d normal=%d lowat=%d hiwat=%d", + psc_stress_cmd_time_limit, psc_normal_cmd_time_limit, + psc_lowat_check_queue_length, psc_hiwat_check_queue_length); + + if (psc_lowat_check_queue_length == 0) + msg_panic("compiler error: 0.7 * %d = %d", var_psc_pre_queue_limit, + psc_lowat_check_queue_length); + if (psc_hiwat_check_queue_length == 0) + msg_panic("compiler error: 0.9 * %d = %d", var_psc_pre_queue_limit, + psc_hiwat_check_queue_length); + + /* + * Per-client concurrency. + */ + psc_client_concurrency = htable_create(var_psc_pre_queue_limit); +} + +MAIL_VERSION_STAMP_DECLARE; + +/* main - pass control to the multi-threaded skeleton */ + +int main(int argc, char **argv) +{ + + /* + * List smtpd(8) parameters before any postscreen(8) parameters that have + * defaults dependencies on them. + */ + static const CONFIG_STR_TABLE str_table[] = { + VAR_SMTPD_SERVICE, DEF_SMTPD_SERVICE, &var_smtpd_service, 1, 0, + VAR_SMTPD_BANNER, DEF_SMTPD_BANNER, &var_smtpd_banner, 1, 0, + VAR_SMTPD_FORBID_CMDS, DEF_SMTPD_FORBID_CMDS, &var_smtpd_forbid_cmds, 0, 0, + VAR_SMTPD_EHLO_DIS_WORDS, DEF_SMTPD_EHLO_DIS_WORDS, &var_smtpd_ehlo_dis_words, 0, 0, + VAR_SMTPD_EHLO_DIS_MAPS, DEF_SMTPD_EHLO_DIS_MAPS, &var_smtpd_ehlo_dis_maps, 0, 0, + VAR_SMTPD_TLS_LEVEL, DEF_SMTPD_TLS_LEVEL, &var_smtpd_tls_level, 0, 0, + VAR_SMTPD_CMD_FILTER, DEF_SMTPD_CMD_FILTER, &var_smtpd_cmd_filter, 0, 0, + VAR_PSC_CACHE_MAP, DEF_PSC_CACHE_MAP, &var_psc_cache_map, 0, 0, + VAR_PSC_PREGR_BANNER, DEF_PSC_PREGR_BANNER, &var_psc_pregr_banner, 0, 0, + VAR_PSC_PREGR_ACTION, DEF_PSC_PREGR_ACTION, &var_psc_pregr_action, 1, 0, + VAR_PSC_DNSBL_SITES, DEF_PSC_DNSBL_SITES, &var_psc_dnsbl_sites, 0, 0, + VAR_PSC_DNSBL_ACTION, DEF_PSC_DNSBL_ACTION, &var_psc_dnsbl_action, 1, 0, + VAR_PSC_PIPEL_ACTION, DEF_PSC_PIPEL_ACTION, &var_psc_pipel_action, 1, 0, + VAR_PSC_NSMTP_ACTION, DEF_PSC_NSMTP_ACTION, &var_psc_nsmtp_action, 1, 0, + VAR_PSC_BARLF_ACTION, DEF_PSC_BARLF_ACTION, &var_psc_barlf_action, 1, 0, + VAR_PSC_ACL, DEF_PSC_ACL, &var_psc_acl, 0, 0, + VAR_PSC_DNLIST_ACTION, DEF_PSC_DNLIST_ACTION, &var_psc_dnlist_action, 1, 0, + VAR_PSC_FORBID_CMDS, DEF_PSC_FORBID_CMDS, &var_psc_forbid_cmds, 0, 0, + VAR_PSC_EHLO_DIS_WORDS, DEF_PSC_EHLO_DIS_WORDS, &var_psc_ehlo_dis_words, 0, 0, + VAR_PSC_EHLO_DIS_MAPS, DEF_PSC_EHLO_DIS_MAPS, &var_psc_ehlo_dis_maps, 0, 0, + VAR_PSC_DNSBL_REPLY, DEF_PSC_DNSBL_REPLY, &var_psc_dnsbl_reply, 0, 0, + VAR_PSC_TLS_LEVEL, DEF_PSC_TLS_LEVEL, &var_psc_tls_level, 0, 0, + VAR_PSC_CMD_FILTER, DEF_PSC_CMD_FILTER, &var_psc_cmd_filter, 0, 0, + VAR_DNSBLOG_SERVICE, DEF_DNSBLOG_SERVICE, &var_dnsblog_service, 1, 0, + VAR_TLSPROXY_SERVICE, DEF_TLSPROXY_SERVICE, &var_tlsproxy_service, 1, 0, + VAR_PSC_ALLIST_IF, DEF_PSC_ALLIST_IF, &var_psc_allist_if, 0, 0, + VAR_PSC_UPROXY_PROTO, DEF_PSC_UPROXY_PROTO, &var_psc_uproxy_proto, 0, 0, + VAR_PSC_REJ_FTR_MAPS, DEF_PSC_REJ_FTR_MAPS, &var_psc_rej_ftr_maps, 0, 0, + 0, + }; + static const CONFIG_INT_TABLE int_table[] = { + VAR_PSC_DNSBL_THRESH, DEF_PSC_DNSBL_THRESH, &var_psc_dnsbl_thresh, 1, 0, + VAR_PSC_CMD_COUNT, DEF_PSC_CMD_COUNT, &var_psc_cmd_count, 1, 0, + VAR_SMTPD_CCONN_LIMIT, DEF_SMTPD_CCONN_LIMIT, &var_smtpd_cconn_limit, 0, 0, + 0, + }; + static const CONFIG_NINT_TABLE nint_table[] = { + VAR_PSC_POST_QLIMIT, DEF_PSC_POST_QLIMIT, &var_psc_post_queue_limit, 5, 0, + VAR_PSC_PRE_QLIMIT, DEF_PSC_PRE_QLIMIT, &var_psc_pre_queue_limit, 10, 0, + VAR_PSC_CCONN_LIMIT, DEF_PSC_CCONN_LIMIT, &var_psc_cconn_limit, 0, 0, + VAR_PSC_DNSBL_ALTHRESH, DEF_PSC_DNSBL_ALTHRESH, &var_psc_dnsbl_althresh, 0, 0, + 0, + }; + static const CONFIG_TIME_TABLE time_table[] = { + VAR_PSC_CMD_TIME, DEF_PSC_CMD_TIME, &var_psc_cmd_time, 1, 0, + VAR_PSC_GREET_WAIT, DEF_PSC_GREET_WAIT, &var_psc_greet_wait, 1, 0, + VAR_PSC_PREGR_TTL, DEF_PSC_PREGR_TTL, &var_psc_pregr_ttl, 1, 0, + VAR_PSC_DNSBL_MIN_TTL, DEF_PSC_DNSBL_MIN_TTL, &var_psc_dnsbl_min_ttl, 1, 0, + VAR_PSC_DNSBL_MAX_TTL, DEF_PSC_DNSBL_MAX_TTL, &var_psc_dnsbl_max_ttl, 1, 0, + VAR_PSC_PIPEL_TTL, DEF_PSC_PIPEL_TTL, &var_psc_pipel_ttl, 1, 0, + VAR_PSC_NSMTP_TTL, DEF_PSC_NSMTP_TTL, &var_psc_nsmtp_ttl, 1, 0, + VAR_PSC_BARLF_TTL, DEF_PSC_BARLF_TTL, &var_psc_barlf_ttl, 1, 0, + VAR_PSC_CACHE_RET, DEF_PSC_CACHE_RET, &var_psc_cache_ret, 1, 0, + VAR_PSC_CACHE_SCAN, DEF_PSC_CACHE_SCAN, &var_psc_cache_scan, 0, 0, + VAR_PSC_WATCHDOG, DEF_PSC_WATCHDOG, &var_psc_watchdog, 10, 0, + VAR_PSC_UPROXY_TMOUT, DEF_PSC_UPROXY_TMOUT, &var_psc_uproxy_tmout, 1, 0, + VAR_PSC_DNSBL_TMOUT, DEF_PSC_DNSBL_TMOUT, &var_psc_dnsbl_tmout, 1, 0, + + 0, + }; + static const CONFIG_BOOL_TABLE bool_table[] = { + VAR_HELO_REQUIRED, DEF_HELO_REQUIRED, &var_helo_required, + VAR_DISABLE_VRFY_CMD, DEF_DISABLE_VRFY_CMD, &var_disable_vrfy_cmd, + VAR_SMTPD_USE_TLS, DEF_SMTPD_USE_TLS, &var_smtpd_use_tls, + VAR_SMTPD_ENFORCE_TLS, DEF_SMTPD_ENFORCE_TLS, &var_smtpd_enforce_tls, + VAR_PSC_PIPEL_ENABLE, DEF_PSC_PIPEL_ENABLE, &var_psc_pipel_enable, + VAR_PSC_NSMTP_ENABLE, DEF_PSC_NSMTP_ENABLE, &var_psc_nsmtp_enable, + VAR_PSC_BARLF_ENABLE, DEF_PSC_BARLF_ENABLE, &var_psc_barlf_enable, + 0, + }; + static const CONFIG_RAW_TABLE raw_table[] = { + VAR_SMTPD_REJ_FOOTER, DEF_SMTPD_REJ_FOOTER, &var_smtpd_rej_footer, 0, 0, + VAR_PSC_REJ_FOOTER, DEF_PSC_REJ_FOOTER, &var_psc_rej_footer, 0, 0, + VAR_SMTPD_EXP_FILTER, DEF_SMTPD_EXP_FILTER, &var_smtpd_exp_filter, 1, 0, + VAR_PSC_EXP_FILTER, DEF_PSC_EXP_FILTER, &var_psc_exp_filter, 1, 0, + 0, + }; + static const CONFIG_NBOOL_TABLE nbool_table[] = { + VAR_PSC_HELO_REQUIRED, DEF_PSC_HELO_REQUIRED, &var_psc_helo_required, + VAR_PSC_DISABLE_VRFY, DEF_PSC_DISABLE_VRFY, &var_psc_disable_vrfy, + VAR_PSC_USE_TLS, DEF_PSC_USE_TLS, &var_psc_use_tls, + VAR_PSC_ENFORCE_TLS, DEF_PSC_ENFORCE_TLS, &var_psc_enforce_tls, + 0, + }; + + /* + * Fingerprint executables and core dumps. + */ + MAIL_VERSION_STAMP_ALLOCATE; + + event_server_main(argc, argv, psc_service, + CA_MAIL_SERVER_STR_TABLE(str_table), + CA_MAIL_SERVER_INT_TABLE(int_table), + CA_MAIL_SERVER_NINT_TABLE(nint_table), + CA_MAIL_SERVER_TIME_TABLE(time_table), + CA_MAIL_SERVER_BOOL_TABLE(bool_table), + CA_MAIL_SERVER_RAW_TABLE(raw_table), + CA_MAIL_SERVER_NBOOL_TABLE(nbool_table), + CA_MAIL_SERVER_PRE_INIT(pre_jail_init), + CA_MAIL_SERVER_POST_INIT(post_jail_init), + CA_MAIL_SERVER_PRE_ACCEPT(pre_accept), + CA_MAIL_SERVER_SOLITARY, + CA_MAIL_SERVER_SLOW_EXIT(psc_drain), + CA_MAIL_SERVER_EXIT(psc_dump), + CA_MAIL_SERVER_WATCHDOG(&var_psc_watchdog), + 0); +} diff --git a/src/postscreen/postscreen.h b/src/postscreen/postscreen.h new file mode 100644 index 0000000..69a5e17 --- /dev/null +++ b/src/postscreen/postscreen.h @@ -0,0 +1,599 @@ +/*++ +/* NAME +/* postscreen 3h +/* SUMMARY +/* postscreen internal interfaces +/* SYNOPSIS +/* #include <postscreen.h> +/* DESCRIPTION +/* .nf + + /* + * System library. + */ + + /* + * Utility library. + */ +#include <dict_cache.h> +#include <vstream.h> +#include <vstring.h> +#include <events.h> +#include <htable.h> +#include <myaddrinfo.h> + + /* + * Global library. + */ +#include <addr_match_list.h> +#include <string_list.h> +#include <maps.h> +#include <server_acl.h> + + /* + * Preliminary stuff, to be fixed. + */ +#define PSC_READ_BUF_SIZE 1024 + + /* + * Numeric indices and symbolic names for tests whose time stamps and status + * flags can be accessed by numeric index. + */ +#define PSC_TINDX_PREGR 0 /* pregreet */ +#define PSC_TINDX_DNSBL 1 /* dnsbl */ +#define PSC_TINDX_PIPEL 2 /* pipelining */ +#define PSC_TINDX_NSMTP 3 /* non-smtp command */ +#define PSC_TINDX_BARLF 4 /* bare newline */ +#define PSC_TINDX_COUNT 5 /* number of tests */ + +#define PSC_TNAME_PREGR "pregreet" +#define PSC_TNAME_DNSBL "dnsbl" +#define PSC_TNAME_PIPEL "pipelining" +#define PSC_TNAME_NSMTP "non-smtp command" +#define PSC_TNAME_BARLF "bare newline" + +#define PSC_TINDX_BYTNAME(tname) (PSC_TINDX_ ## tname) + + /* + * Per-client shared state. + */ +typedef struct { + int concurrency; /* per-client */ + int pass_new_count; /* per-client */ + time_t expire_time[PSC_TINDX_COUNT]; /* per-test expiration */ +} PSC_CLIENT_INFO; + + /* + * Per-session state. + */ +typedef struct { + int flags; /* see below */ + /* Socket state. */ + VSTREAM *smtp_client_stream; /* remote SMTP client */ + int smtp_server_fd; /* real SMTP server */ + char *smtp_client_addr; /* client address */ + char *smtp_client_port; /* client port */ + char *smtp_server_addr; /* server address */ + char *smtp_server_port; /* server port */ + const char *final_reply; /* cause for hanging up */ + VSTRING *send_buf; /* pending output */ + /* Test context. */ + struct timeval start_time; /* start of current test */ + const char *test_name; /* name of current test */ + PSC_CLIENT_INFO *client_info; /* shared client state */ + VSTRING *dnsbl_reply; /* dnsbl reject text */ + int dnsbl_score; /* saved DNSBL score */ + int dnsbl_ttl; /* saved DNSBL TTL */ + const char *dnsbl_name; /* DNSBL name with largest weight */ + int dnsbl_index; /* dnsbl request index */ + const char *rcpt_reply; /* how to reject recipients */ + int command_count; /* error + junk command count */ + const char *protocol; /* SMTP or ESMTP */ + char *helo_name; /* SMTP helo/ehlo */ + char *sender; /* MAIL FROM */ + VSTRING *cmd_buffer; /* command read buffer */ + int read_state; /* command read state machine */ + /* smtpd(8) compatibility */ + int ehlo_discard_mask; /* EHLO filter */ + VSTRING *expand_buf; /* macro expansion */ + const char *where; /* SMTP protocol state */ +} PSC_STATE; + + /* + * Special expiration time values. + */ +#define PSC_TIME_STAMP_NEW (0) /* test was never passed */ +#define PSC_TIME_STAMP_DISABLED (1) /* never passed but disabled */ +#define PSC_TIME_STAMP_INVALID (-1) /* must not be cached */ + + /* + * Status flags. + */ +#define PSC_STATE_FLAG_NOFORWARD (1<<0) /* don't forward this session */ +#define PSC_STATE_FLAG_USING_TLS (1<<1) /* using the TLS proxy */ +#define PSC_STATE_FLAG_UNUSED2 (1<<2) /* use me! */ +#define PSC_STATE_FLAG_NEW (1<<3) /* some test was never passed */ +#define PSC_STATE_FLAG_DNLIST_FAIL (1<<4) /* denylisted */ +#define PSC_STATE_FLAG_HANGUP (1<<5) /* NOT a test failure */ +#define PSC_STATE_FLAG_SMTPD_X21 (1<<6) /* hang up after command */ +#define PSC_STATE_FLAG_ALLIST_FAIL (1<<7) /* do not allowlist */ +#define PSC_STATE_FLAG_TEST_BASE (8) /* start of indexable flags */ + + /* + * Tests whose flags and expiration time can be accessed by numerical index. + * + * Important: every MUMBLE_TODO flag must have a MUMBLE_PASS flag, such that + * MUMBLE_PASS == PSC_STATE_FLAGS_TODO_TO_PASS(MUMBLE_TODO). + * + * MUMBLE_TODO flags must not be cleared once raised. The _TODO_TO_PASS and + * _TODO_TO_DONE macros depend on this to decide that a group of tests is + * passed or completed. + * + * MUMBLE_DONE flags are used for "early" tests that have final results. + * + * MUMBLE_SKIP flags are used for "deep" tests where the client messed up. + * These flags look like MUMBLE_DONE but they are different. Deep tests can + * tentatively pass, but can still fail later in a session. The "ignore" + * action introduces an additional complication. MUMBLE_PASS indicates + * either that a deep test passed tentatively, or that the test failed but + * the result was ignored. MUMBLE_FAIL, on the other hand, is always final. + * We use MUMBLE_SKIP to indicate that a decision was either "fail" or + * forced "pass". + * + * The difference between DONE and SKIP is in the beholder's eye. These flags + * share the same bit. + */ +#define PSC_STATE_FLAGS_TODO_TO_PASS(todo_flags) ((todo_flags) >> 1) +#define PSC_STATE_FLAGS_TODO_TO_DONE(todo_flags) ((todo_flags) << 1) + +#define PSC_STATE_FLAG_SHIFT_FAIL (0) /* failed test */ +#define PSC_STATE_FLAG_SHIFT_PASS (1) /* passed test */ +#define PSC_STATE_FLAG_SHIFT_TODO (2) /* expired test */ +#define PSC_STATE_FLAG_SHIFT_DONE (3) /* decision is final */ +#define PSC_STATE_FLAG_SHIFT_SKIP (3) /* action is already logged */ +#define PSC_STATE_FLAG_SHIFT_STRIDE (4) /* nr of flags per test */ + +#define PSC_STATE_FLAG_SHIFT_BYFNAME(fname) (PSC_STATE_FLAG_SHIFT_ ## fname) + + /* + * Indexable per-test flags. These are used for DNS allowlisting multiple + * tests, without needing per-test ad-hoc code. + */ +#define PSC_STATE_FLAG_BYTINDX_FNAME(tindx, fname) \ + (1U << (PSC_STATE_FLAG_TEST_BASE \ + + PSC_STATE_FLAG_SHIFT_STRIDE * (tindx) \ + + PSC_STATE_FLAG_SHIFT_BYFNAME(fname))) + +#define PSC_STATE_FLAG_BYTINDX_FAIL(tindx) \ + PSC_STATE_FLAG_BYTINDX_FNAME((tindx), FAIL) +#define PSC_STATE_FLAG_BYTINDX_PASS(tindx) \ + PSC_STATE_FLAG_BYTINDX_FNAME((tindx), PASS) +#define PSC_STATE_FLAG_BYTINDX_TODO(tindx) \ + PSC_STATE_FLAG_BYTINDX_FNAME((tindx), TODO) +#define PSC_STATE_FLAG_BYTINDX_DONE(tindx) \ + PSC_STATE_FLAG_BYTINDX_FNAME((tindx), DONE) +#define PSC_STATE_FLAG_BYTINDX_SKIP(tindx) \ + PSC_STATE_FLAG_BYTINDX_FNAME((tindx), SKIP) + + /* + * Flags with distinct names. These are used in the per-test ad-hoc code. + */ +#define PSC_STATE_FLAG_BYTNAME_FNAME(tname, fname) \ + (1U << (PSC_STATE_FLAG_TEST_BASE \ + + PSC_STATE_FLAG_SHIFT_STRIDE * PSC_TINDX_BYTNAME(tname) \ + + PSC_STATE_FLAG_SHIFT_BYFNAME(fname))) + +#define PSC_STATE_FLAG_PREGR_FAIL PSC_STATE_FLAG_BYTNAME_FNAME(PREGR, FAIL) +#define PSC_STATE_FLAG_PREGR_PASS PSC_STATE_FLAG_BYTNAME_FNAME(PREGR, PASS) +#define PSC_STATE_FLAG_PREGR_TODO PSC_STATE_FLAG_BYTNAME_FNAME(PREGR, TODO) +#define PSC_STATE_FLAG_PREGR_DONE PSC_STATE_FLAG_BYTNAME_FNAME(PREGR, DONE) + +#define PSC_STATE_FLAG_DNSBL_FAIL PSC_STATE_FLAG_BYTNAME_FNAME(DNSBL, FAIL) +#define PSC_STATE_FLAG_DNSBL_PASS PSC_STATE_FLAG_BYTNAME_FNAME(DNSBL, PASS) +#define PSC_STATE_FLAG_DNSBL_TODO PSC_STATE_FLAG_BYTNAME_FNAME(DNSBL, TODO) +#define PSC_STATE_FLAG_DNSBL_DONE PSC_STATE_FLAG_BYTNAME_FNAME(DNSBL, DONE) + +#define PSC_STATE_FLAG_PIPEL_FAIL PSC_STATE_FLAG_BYTNAME_FNAME(PIPEL, FAIL) +#define PSC_STATE_FLAG_PIPEL_PASS PSC_STATE_FLAG_BYTNAME_FNAME(PIPEL, PASS) +#define PSC_STATE_FLAG_PIPEL_TODO PSC_STATE_FLAG_BYTNAME_FNAME(PIPEL, TODO) +#define PSC_STATE_FLAG_PIPEL_SKIP PSC_STATE_FLAG_BYTNAME_FNAME(PIPEL, SKIP) + +#define PSC_STATE_FLAG_NSMTP_FAIL PSC_STATE_FLAG_BYTNAME_FNAME(NSMTP, FAIL) +#define PSC_STATE_FLAG_NSMTP_PASS PSC_STATE_FLAG_BYTNAME_FNAME(NSMTP, PASS) +#define PSC_STATE_FLAG_NSMTP_TODO PSC_STATE_FLAG_BYTNAME_FNAME(NSMTP, TODO) +#define PSC_STATE_FLAG_NSMTP_SKIP PSC_STATE_FLAG_BYTNAME_FNAME(NSMTP, SKIP) + +#define PSC_STATE_FLAG_BARLF_FAIL PSC_STATE_FLAG_BYTNAME_FNAME(BARLF, FAIL) +#define PSC_STATE_FLAG_BARLF_PASS PSC_STATE_FLAG_BYTNAME_FNAME(BARLF, PASS) +#define PSC_STATE_FLAG_BARLF_TODO PSC_STATE_FLAG_BYTNAME_FNAME(BARLF, TODO) +#define PSC_STATE_FLAG_BARLF_SKIP PSC_STATE_FLAG_BYTNAME_FNAME(BARLF, SKIP) + + /* + * Aggregates for individual tests. + */ +#define PSC_STATE_MASK_PREGR_TODO_FAIL \ + (PSC_STATE_FLAG_PREGR_TODO | PSC_STATE_FLAG_PREGR_FAIL) +#define PSC_STATE_MASK_DNSBL_TODO_FAIL \ + (PSC_STATE_FLAG_DNSBL_TODO | PSC_STATE_FLAG_DNSBL_FAIL) +#define PSC_STATE_MASK_PIPEL_TODO_FAIL \ + (PSC_STATE_FLAG_PIPEL_TODO | PSC_STATE_FLAG_PIPEL_FAIL) +#define PSC_STATE_MASK_NSMTP_TODO_FAIL \ + (PSC_STATE_FLAG_NSMTP_TODO | PSC_STATE_FLAG_NSMTP_FAIL) +#define PSC_STATE_MASK_BARLF_TODO_FAIL \ + (PSC_STATE_FLAG_BARLF_TODO | PSC_STATE_FLAG_BARLF_FAIL) + +#define PSC_STATE_MASK_PREGR_TODO_DONE \ + (PSC_STATE_FLAG_PREGR_TODO | PSC_STATE_FLAG_PREGR_DONE) +#define PSC_STATE_MASK_PIPEL_TODO_SKIP \ + (PSC_STATE_FLAG_PIPEL_TODO | PSC_STATE_FLAG_PIPEL_SKIP) +#define PSC_STATE_MASK_NSMTP_TODO_SKIP \ + (PSC_STATE_FLAG_NSMTP_TODO | PSC_STATE_FLAG_NSMTP_SKIP) +#define PSC_STATE_MASK_BARLF_TODO_SKIP \ + (PSC_STATE_FLAG_BARLF_TODO | PSC_STATE_FLAG_BARLF_SKIP) + +#define PSC_STATE_MASK_PREGR_FAIL_DONE \ + (PSC_STATE_FLAG_PREGR_FAIL | PSC_STATE_FLAG_PREGR_DONE) + +#define PSC_STATE_MASK_PIPEL_TODO_PASS_FAIL \ + (PSC_STATE_MASK_PIPEL_TODO_FAIL | PSC_STATE_FLAG_PIPEL_PASS) +#define PSC_STATE_MASK_NSMTP_TODO_PASS_FAIL \ + (PSC_STATE_MASK_NSMTP_TODO_FAIL | PSC_STATE_FLAG_NSMTP_PASS) +#define PSC_STATE_MASK_BARLF_TODO_PASS_FAIL \ + (PSC_STATE_MASK_BARLF_TODO_FAIL | PSC_STATE_FLAG_BARLF_PASS) + + /* + * Separate aggregates for early tests and deep tests. + */ +#define PSC_STATE_MASK_EARLY_DONE \ + (PSC_STATE_FLAG_PREGR_DONE | PSC_STATE_FLAG_DNSBL_DONE) +#define PSC_STATE_MASK_EARLY_TODO \ + (PSC_STATE_FLAG_PREGR_TODO | PSC_STATE_FLAG_DNSBL_TODO) +#define PSC_STATE_MASK_EARLY_PASS \ + (PSC_STATE_FLAG_PREGR_PASS | PSC_STATE_FLAG_DNSBL_PASS) +#define PSC_STATE_MASK_EARLY_FAIL \ + (PSC_STATE_FLAG_PREGR_FAIL | PSC_STATE_FLAG_DNSBL_FAIL) + +#define PSC_STATE_MASK_SMTPD_TODO \ + (PSC_STATE_FLAG_PIPEL_TODO | PSC_STATE_FLAG_NSMTP_TODO | \ + PSC_STATE_FLAG_BARLF_TODO) +#define PSC_STATE_MASK_SMTPD_PASS \ + (PSC_STATE_FLAG_PIPEL_PASS | PSC_STATE_FLAG_NSMTP_PASS | \ + PSC_STATE_FLAG_BARLF_PASS) +#define PSC_STATE_MASK_SMTPD_FAIL \ + (PSC_STATE_FLAG_PIPEL_FAIL | PSC_STATE_FLAG_NSMTP_FAIL | \ + PSC_STATE_FLAG_BARLF_FAIL) + + /* + * Super-aggregates for all tests combined. + */ +#define PSC_STATE_MASK_ANY_FAIL \ + (PSC_STATE_FLAG_DNLIST_FAIL | \ + PSC_STATE_MASK_EARLY_FAIL | PSC_STATE_MASK_SMTPD_FAIL | \ + PSC_STATE_FLAG_ALLIST_FAIL) + +#define PSC_STATE_MASK_ANY_PASS \ + (PSC_STATE_MASK_EARLY_PASS | PSC_STATE_MASK_SMTPD_PASS) + +#define PSC_STATE_MASK_ANY_TODO \ + (PSC_STATE_MASK_EARLY_TODO | PSC_STATE_MASK_SMTPD_TODO) + +#define PSC_STATE_MASK_ANY_TODO_FAIL \ + (PSC_STATE_MASK_ANY_TODO | PSC_STATE_MASK_ANY_FAIL) + +#define PSC_STATE_MASK_ANY_UPDATE \ + (PSC_STATE_MASK_ANY_PASS) + + /* + * Meta-commands for state->where that reflect the initial command processor + * state and commands that aren't implemented. + */ +#define PSC_SMTPD_CMD_CONNECT "CONNECT" +#define PSC_SMTPD_CMD_UNIMPL "UNIMPLEMENTED" + + /* + * See log_adhoc.c for discussion. + */ +typedef struct { + int dt_sec; /* make sure it's signed */ + int dt_usec; /* make sure it's signed */ +} DELTA_TIME; + +#define PSC_CALC_DELTA(x, y, z) \ + do { \ + (x).dt_sec = (y).tv_sec - (z).tv_sec; \ + (x).dt_usec = (y).tv_usec - (z).tv_usec; \ + while ((x).dt_usec < 0) { \ + (x).dt_usec += 1000000; \ + (x).dt_sec -= 1; \ + } \ + while ((x).dt_usec >= 1000000) { \ + (x).dt_usec -= 1000000; \ + (x).dt_sec += 1; \ + } \ + if ((x).dt_sec < 0) \ + (x).dt_sec = (x).dt_usec = 0; \ + } while (0) + +#define SIG_DIGS 2 + + /* + * Event management. + */ + +/* PSC_READ_EVENT_REQUEST - prepare for transition to next state */ + +#define PSC_READ_EVENT_REQUEST(fd, action, context, timeout) do { \ + if (msg_verbose > 1) \ + msg_info("%s: read-request fd=%d", myname, (fd)); \ + event_enable_read((fd), (action), (context)); \ + event_request_timer((action), (context), (timeout)); \ + } while (0) + +#define PSC_READ_EVENT_REQUEST2(fd, read_act, time_act, context, timeout) do { \ + if (msg_verbose > 1) \ + msg_info("%s: read-request fd=%d", myname, (fd)); \ + event_enable_read((fd), (read_act), (context)); \ + event_request_timer((time_act), (context), (timeout)); \ + } while (0) + +/* PSC_CLEAR_EVENT_REQUEST - complete state transition */ + +#define PSC_CLEAR_EVENT_REQUEST(fd, time_act, context) do { \ + if (msg_verbose > 1) \ + msg_info("%s: clear-request fd=%d", myname, (fd)); \ + event_disable_readwrite(fd); \ + event_cancel_timer((time_act), (context)); \ + } while (0) + + /* + * Failure enforcement policies. + */ +#define PSC_NAME_ACT_DROP "drop" +#define PSC_NAME_ACT_ENFORCE "enforce" +#define PSC_NAME_ACT_IGNORE "ignore" +#define PSC_NAME_ACT_CONT "continue" + +#define PSC_ACT_DROP 1 +#define PSC_ACT_ENFORCE 2 +#define PSC_ACT_IGNORE 3 + + /* + * Global variables. + */ +extern int psc_check_queue_length; /* connections being checked */ +extern int psc_post_queue_length; /* being sent to real SMTPD */ +extern DICT_CACHE *psc_cache_map; /* cache table handle */ +extern VSTRING *psc_temp; /* scratchpad */ +extern char *psc_smtpd_service_name; /* path to real SMTPD */ +extern int psc_pregr_action; /* PSC_ACT_DROP etc. */ +extern int psc_dnsbl_action; /* PSC_ACT_DROP etc. */ +extern int psc_pipel_action; /* PSC_ACT_DROP etc. */ +extern int psc_nsmtp_action; /* PSC_ACT_DROP etc. */ +extern int psc_barlf_action; /* PSC_ACT_DROP etc. */ +extern int psc_min_ttl; /* Update with new tests! */ +extern STRING_LIST *psc_forbid_cmds; /* CONNECT GET POST */ +extern int psc_stress_greet_wait; /* stressed greet wait */ +extern int psc_normal_greet_wait; /* stressed greet wait */ +extern int psc_stress_cmd_time_limit; /* stressed command limit */ +extern int psc_normal_cmd_time_limit; /* normal command time limit */ +extern int psc_stress; /* stress level */ +extern int psc_lowat_check_queue_length;/* stress low-water mark */ +extern int psc_hiwat_check_queue_length;/* stress high-water mark */ +extern DICT *psc_dnsbl_reply; /* DNSBL name mapper */ +extern HTABLE *psc_client_concurrency; /* per-client concurrency */ + +#define PSC_EFF_GREET_WAIT \ + (psc_stress ? psc_stress_greet_wait : psc_normal_greet_wait) +#define PSC_EFF_CMD_TIME_LIMIT \ + (psc_stress ? psc_stress_cmd_time_limit : psc_normal_cmd_time_limit) + + /* + * String plumbing macros. + */ +#define PSC_STRING_UPDATE(str, text) do { \ + if (str) myfree(str); \ + (str) = ((text) ? mystrdup(text) : 0); \ + } while (0) + +#define PSC_STRING_RESET(str) do { \ + if (str) { \ + myfree(str); \ + (str) = 0; \ + } \ + } while (0) + + /* + * SLMs. + */ +#define STR(x) vstring_str(x) +#define LEN(x) VSTRING_LEN(x) + + /* + * postscreen_state.c + */ +#define PSC_CLIENT_ADDR_PORT(state) \ + (state)->smtp_client_addr, (state)->smtp_client_port + +#define PSC_PASS_SESSION_STATE(state, what, bits) do { \ + if (msg_verbose) \ + msg_info("PASS %s [%s]:%s", (what), PSC_CLIENT_ADDR_PORT(state)); \ + (state)->flags |= (bits); \ + } while (0) +#define PSC_FAIL_SESSION_STATE(state, bits) do { \ + if (msg_verbose) \ + msg_info("FAIL [%s]:%s", PSC_CLIENT_ADDR_PORT(state)); \ + (state)->flags |= (bits); \ + } while (0) +#define PSC_SKIP_SESSION_STATE(state, what, bits) do { \ + if (msg_verbose) \ + msg_info("SKIP %s [%s]:%s", (what), PSC_CLIENT_ADDR_PORT(state)); \ + (state)->flags |= (bits); \ + } while (0) +#define PSC_DROP_SESSION_STATE(state, reply) do { \ + if (msg_verbose) \ + msg_info("DROP [%s]:%s", PSC_CLIENT_ADDR_PORT(state)); \ + (state)->flags |= PSC_STATE_FLAG_NOFORWARD; \ + (state)->final_reply = (reply); \ + psc_conclude(state); \ + } while (0) +#define PSC_ENFORCE_SESSION_STATE(state, reply) do { \ + if (msg_verbose) \ + msg_info("ENFORCE [%s]:%s", PSC_CLIENT_ADDR_PORT(state)); \ + (state)->rcpt_reply = (reply); \ + (state)->flags |= PSC_STATE_FLAG_NOFORWARD; \ + } while (0) +#define PSC_UNPASS_SESSION_STATE(state, bits) do { \ + if (msg_verbose) \ + msg_info("UNPASS [%s]:%s", PSC_CLIENT_ADDR_PORT(state)); \ + (state)->flags &= ~(bits); \ + } while (0) +#define PSC_UNFAIL_SESSION_STATE(state, bits) do { \ + if (msg_verbose) \ + msg_info("UNFAIL [%s]:%s", PSC_CLIENT_ADDR_PORT(state)); \ + (state)->flags &= ~(bits); \ + } while (0) +#define PSC_ADD_SERVER_STATE(state, fd) do { \ + (state)->smtp_server_fd = (fd); \ + psc_post_queue_length++; \ + } while (0) +#define PSC_DEL_SERVER_STATE(state) do { \ + close((state)->smtp_server_fd); \ + (state)->smtp_server_fd = (-1); \ + psc_post_queue_length--; \ + } while (0) +#define PSC_DEL_CLIENT_STATE(state) do { \ + event_server_disconnect((state)->smtp_client_stream); \ + (state)->smtp_client_stream = 0; \ + psc_check_queue_length--; \ + } while (0) +extern PSC_STATE *psc_new_session_state(VSTREAM *, const char *, const char *, const char *, const char *); +extern void psc_free_session_state(PSC_STATE *); +extern const char *psc_print_state_flags(int, const char *); + + /* + * postscreen_dict.c + */ +extern int psc_addr_match_list_match(ADDR_MATCH_LIST *, const char *); +extern const char *psc_cache_lookup(DICT_CACHE *, const char *); +extern void psc_cache_update(DICT_CACHE *, const char *, const char *); +const char *psc_dict_get(DICT *, const char *); +const char *psc_maps_find(MAPS *, const char *, int); + + /* + * postscreen_dnsbl.c + */ +extern void psc_dnsbl_init(void); +extern int psc_dnsbl_retrieve(const char *, const char **, int, int *); +extern int psc_dnsbl_request(const char *, void (*) (int, void *), void *); + + /* + * postscreen_tests.c + */ +#define PSC_INIT_TESTS(dst) do { \ + time_t *_it_stamp_p; \ + (dst)->flags = 0; \ + for (_it_stamp_p = (dst)->client_info->expire_time; \ + _it_stamp_p < (dst)->client_info->expire_time + PSC_TINDX_COUNT; \ + _it_stamp_p++) \ + *_it_stamp_p = PSC_TIME_STAMP_INVALID; \ + } while (0) +#define PSC_INIT_TEST_FLAGS_ONLY(dst) do { \ + (dst)->flags = 0; \ + } while (0) +#define PSC_BEGIN_TESTS(state, name) do { \ + (state)->test_name = (name); \ + GETTIMEOFDAY(&(state)->start_time); \ + } while (0) +extern void psc_new_tests(PSC_STATE *); +extern void psc_parse_tests(PSC_STATE *, const char *, time_t); +extern void psc_todo_tests(PSC_STATE *, time_t); +extern char *psc_print_tests(VSTRING *, PSC_STATE *); +extern char *psc_print_grey_key(VSTRING *, const char *, const char *, + const char *, const char *); +extern const char *psc_test_name(int); + +#define PSC_MIN(x, y) ((x) < (y) ? (x) : (y)) +#define PSC_MAX(x, y) ((x) > (y) ? (x) : (y)) + + /* + * postscreen_early.c + */ +extern void psc_early_tests(PSC_STATE *); +extern void psc_early_init(void); + + /* + * postscreen_smtpd.c + */ +extern void psc_smtpd_tests(PSC_STATE *); +extern void psc_smtpd_init(void); +extern void psc_smtpd_pre_jail_init(void); + +#define PSC_SMTPD_X21(state, reply) do { \ + (state)->flags |= PSC_STATE_FLAG_SMTPD_X21; \ + (state)->final_reply = (reply); \ + psc_smtpd_tests(state); \ + } while (0) + + /* + * postscreen_misc.c + */ +extern char *psc_format_delta_time(VSTRING *, struct timeval, DELTA_TIME *); +extern void psc_conclude(PSC_STATE *); +extern void psc_hangup_event(PSC_STATE *); + + /* + * postscreen_send.c + */ +#define PSC_SEND_REPLY psc_send_reply /* legacy macro */ +extern void pcs_send_pre_jail_init(void); +extern int psc_send_reply(PSC_STATE *, const char *); +extern void psc_send_socket(PSC_STATE *); + + /* + * postscreen_starttls.c + */ +extern void psc_starttls_open(PSC_STATE *, EVENT_NOTIFY_FN); + + /* + * postscreen_expand.c + */ +extern VSTRING *psc_expand_filter; +extern void psc_expand_init(void); +extern const char *psc_expand_lookup(const char *, int, void *); + + /* + * postscreen_endpt.c + */ +typedef void (*PSC_ENDPT_LOOKUP_FN) (int, VSTREAM *, + MAI_HOSTADDR_STR *, MAI_SERVPORT_STR *, + MAI_HOSTADDR_STR *, MAI_SERVPORT_STR *); +extern void psc_endpt_lookup(VSTREAM *, PSC_ENDPT_LOOKUP_FN); +extern void psc_endpt_local_lookup(VSTREAM *, PSC_ENDPT_LOOKUP_FN); + + /* + * postscreen_access emulation. + */ +#define PSC_ACL_ACT_ALLOWLIST SERVER_ACL_ACT_PERMIT +#define PSC_ACL_ACT_DUNNO SERVER_ACL_ACT_DUNNO +#define PSC_ACL_ACT_DENYLIST SERVER_ACL_ACT_REJECT +#define PSC_ACL_ACT_ERROR SERVER_ACL_ACT_ERROR + +#define psc_acl_pre_jail_init server_acl_pre_jail_init +#define psc_acl_parse server_acl_parse +#define psc_acl_eval(s,a,p) server_acl_eval((s)->smtp_client_addr, (a), (p)) + +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ diff --git a/src/postscreen/postscreen_dict.c b/src/postscreen/postscreen_dict.c new file mode 100644 index 0000000..131ff81 --- /dev/null +++ b/src/postscreen/postscreen_dict.c @@ -0,0 +1,182 @@ +/*++ +/* NAME +/* postscreen_dict 3 +/* SUMMARY +/* postscreen table access wrappers +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* int psc_addr_match_list_match(match_list, client_addr) +/* ADDR_MATCH_LIST *match_list; +/* const char *client_addr; +/* +/* const char *psc_cache_lookup(DICT_CACHE *cache, const char *key) +/* DICT_CACHE *cache; +/* const char *key; +/* +/* void psc_cache_update(cache, key, value) +/* DICT_CACHE *cache; +/* const char *key; +/* const char *value; +/* +/* void psc_dict_get(dict, key) +/* DICT *dict; +/* const char *key; +/* +/* void psc_maps_find(maps, key, flags) +/* MAPS *maps; +/* const char *key; +/* int flags; +/* DESCRIPTION +/* This module implements wrappers around time-critical table +/* access functions. The functions log a warning when table +/* access takes a non-trivial amount of time. +/* +/* psc_addr_match_list_match() is a wrapper around +/* addr_match_list_match(). +/* +/* psc_cache_lookup() and psc_cache_update() are wrappers around +/* the corresponding dict_cache() methods. +/* +/* psc_dict_get() and psc_maps_find() are wrappers around +/* dict_get() and maps_find(), respectively. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> + +/* Utility library. */ + +#include <msg.h> +#include <dict.h> + +/* Global library. */ + +#include <maps.h> + +/* Application-specific. */ + +#include <postscreen.h> + + /* + * Monitor time-critical operations. + * + * XXX Averaging support was added during a stable release candidate, so it + * provides only the absolute minimum necessary. A complete implementation + * should maintain separate statistics for each table, and it should not + * complain when the access latency is less than the time between accesses. + */ +#define PSC_GET_TIME_BEFORE_LOOKUP { \ + struct timeval _before, _after; \ + DELTA_TIME _delta; \ + double _new_delta_ms; \ + GETTIMEOFDAY(&_before); + +#define PSC_DELTA_MS(d) ((d).dt_sec * 1000.0 + (d).dt_usec / 1000.0) + +#define PSC_AVERAGE(new, old) (0.1 * (new) + 0.9 * (old)) + +#ifndef PSC_THRESHOLD_MS +#define PSC_THRESHOLD_MS 100 /* nag if latency > 100ms */ +#endif + +#ifndef PSC_WARN_LOCKOUT_S +#define PSC_WARN_LOCKOUT_S 60 /* don't nag for 60s */ +#endif + + /* + * Shared warning lock, so that we don't spam the logfile when the system + * becomes slow. + */ +static time_t psc_last_warn = 0; + +#define PSC_CHECK_TIME_AFTER_LOOKUP(table, action, average) \ + GETTIMEOFDAY(&_after); \ + PSC_CALC_DELTA(_delta, _after, _before); \ + _new_delta_ms = PSC_DELTA_MS(_delta); \ + if ((average = PSC_AVERAGE(_new_delta_ms, average)) > PSC_THRESHOLD_MS \ + && psc_last_warn < _after.tv_sec - PSC_WARN_LOCKOUT_S) { \ + msg_warn("%s: %s %s average delay is %.0f ms", \ + myname, (table), (action), average); \ + psc_last_warn = _after.tv_sec; \ + } \ +} + +/* psc_addr_match_list_match - time-critical address list lookup */ + +int psc_addr_match_list_match(ADDR_MATCH_LIST *addr_list, + const char *addr_str) +{ + const char *myname = "psc_addr_match_list_match"; + int result; + static double latency_ms; + + PSC_GET_TIME_BEFORE_LOOKUP; + result = addr_match_list_match(addr_list, addr_str); + PSC_CHECK_TIME_AFTER_LOOKUP("address list", "lookup", latency_ms); + return (result); +} + +/* psc_cache_lookup - time-critical cache lookup */ + +const char *psc_cache_lookup(DICT_CACHE *cache, const char *key) +{ + const char *myname = "psc_cache_lookup"; + const char *result; + static double latency_ms; + + PSC_GET_TIME_BEFORE_LOOKUP; + result = dict_cache_lookup(cache, key); + PSC_CHECK_TIME_AFTER_LOOKUP(dict_cache_name(cache), "lookup", latency_ms); + return (result); +} + +/* psc_cache_update - time-critical cache update */ + +void psc_cache_update(DICT_CACHE *cache, const char *key, const char *value) +{ + const char *myname = "psc_cache_update"; + static double latency_ms; + + PSC_GET_TIME_BEFORE_LOOKUP; + dict_cache_update(cache, key, value); + PSC_CHECK_TIME_AFTER_LOOKUP(dict_cache_name(cache), "update", latency_ms); +} + +/* psc_dict_get - time-critical table lookup */ + +const char *psc_dict_get(DICT *dict, const char *key) +{ + const char *myname = "psc_dict_get"; + const char *result; + static double latency_ms; + + PSC_GET_TIME_BEFORE_LOOKUP; + result = dict_get(dict, key); + PSC_CHECK_TIME_AFTER_LOOKUP(dict->name, "lookup", latency_ms); + return (result); +} + +/* psc_maps_find - time-critical table lookup */ + +const char *psc_maps_find(MAPS *maps, const char *key, int flags) +{ + const char *myname = "psc_maps_find"; + const char *result; + static double latency_ms; + + PSC_GET_TIME_BEFORE_LOOKUP; + result = maps_find(maps, key, flags); + PSC_CHECK_TIME_AFTER_LOOKUP(maps->title, "lookup", latency_ms); + return (result); +} diff --git a/src/postscreen/postscreen_dnsbl.c b/src/postscreen/postscreen_dnsbl.c new file mode 100644 index 0000000..7d9a5e9 --- /dev/null +++ b/src/postscreen/postscreen_dnsbl.c @@ -0,0 +1,624 @@ +/*++ +/* NAME +/* postscreen_dnsbl 3 +/* SUMMARY +/* postscreen DNSBL support +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* void psc_dnsbl_init(void) +/* +/* int psc_dnsbl_request(client_addr, callback, context) +/* char *client_addr; +/* void (*callback)(int, char *); +/* char *context; +/* +/* int psc_dnsbl_retrieve(client_addr, dnsbl_name, dnsbl_index, +/* dnsbl_ttl) +/* char *client_addr; +/* const char **dnsbl_name; +/* int dnsbl_index; +/* int *dnsbl_ttl; +/* DESCRIPTION +/* This module implements preliminary support for DNSBL lookups. +/* Multiple requests for the same information are handled with +/* reference counts. +/* +/* psc_dnsbl_init() initializes this module, and must be called +/* once before any of the other functions in this module. +/* +/* psc_dnsbl_request() requests a blocklist score for the +/* specified client IP address and increments the reference +/* count. The request completes in the background. The client +/* IP address must be in inet_ntop(3) output format. The +/* callback argument specifies a function that is called when +/* the requested result is available. The context is passed +/* on to the callback function. The callback should ignore its +/* first argument (it exists for compatibility with Postfix +/* generic event infrastructure). +/* The result value is the index for the psc_dnsbl_retrieve() +/* call. +/* +/* psc_dnsbl_retrieve() retrieves the result score and reply +/* TTL requested with psc_dnsbl_request(), and decrements the +/* reference count. The reply TTL value is clamped to +/* postscreen_dnsbl_min_ttl and postscreen_dnsbl_max_ttl. It +/* is an error to retrieve a score without requesting it first. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/socket.h> /* AF_INET */ +#include <netinet/in.h> /* inet_pton() */ +#include <arpa/inet.h> /* inet_pton() */ +#include <stdio.h> /* sscanf */ +#include <limits.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <argv.h> +#include <htable.h> +#include <events.h> +#include <vstream.h> +#include <connect.h> +#include <split_at.h> +#include <valid_hostname.h> +#include <ip_match.h> +#include <myaddrinfo.h> +#include <stringops.h> + +/* Global library. */ + +#include <mail_params.h> +#include <mail_proto.h> + +/* Application-specific. */ + +#include <postscreen.h> + + /* + * Talking to the DNSBLOG service. + */ +static char *psc_dnsbl_service; + + /* + * Per-DNSBL filters and weights. + * + * The postscreen_dnsbl_sites parameter specifies zero or more DNSBL domains. + * We provide multiple access methods, one for quick iteration when sending + * queries to all DNSBL servers, and one for quick location when receiving a + * reply from one DNSBL server. + * + * Each DNSBL domain can be specified more than once, each time with a + * different (filter, weight) pair. We group (filter, weight) pairs in a + * linked list under their DNSBL domain name. The list head has a reference + * to a "safe name" for the DNSBL, in case the name includes a password. + */ +static HTABLE *dnsbl_site_cache; /* indexed by DNSBNL domain */ +static HTABLE_INFO **dnsbl_site_list; /* flattened cache */ + +typedef struct { + const char *safe_dnsbl; /* from postscreen_dnsbl_reply_map */ + struct PSC_DNSBL_SITE *first; /* list of (filter, weight) tuples */ +} PSC_DNSBL_HEAD; + +typedef struct PSC_DNSBL_SITE { + char *filter; /* printable filter (default: null) */ + char *byte_codes; /* encoded filter (default: null) */ + int weight; /* reply weight (default: 1) */ + struct PSC_DNSBL_SITE *next; /* linked list */ +} PSC_DNSBL_SITE; + + /* + * Per-client DNSBL scores. + * + * Some SMTP clients make parallel connections. This can trigger parallel + * blocklist score requests when the pre-handshake delays of the connections + * overlap. + * + * We combine requests for the same score under the client IP address in a + * single reference-counted entry. The reference count goes up with each + * request for a score, and it goes down with each score retrieval. Each + * score has one or more requestors that need to be notified when the result + * is ready, so that postscreen can terminate a pre-handshake delay when all + * pre-handshake tests are completed. + */ +static HTABLE *dnsbl_score_cache; /* indexed by client address */ + +typedef struct { + void (*callback) (int, void *); /* generic call-back routine */ + void *context; /* generic call-back argument */ +} PSC_CALL_BACK_ENTRY; + +typedef struct { + const char *dnsbl_name; /* DNSBL with largest contribution */ + int dnsbl_weight; /* weight of largest contribution */ + int total; /* combined allow+denylist score */ + int fail_ttl; /* combined reply TTL */ + int pass_ttl; /* combined reply TTL */ + int refcount; /* score reference count */ + int pending_lookups; /* nr of DNS requests in flight */ + int request_id; /* duplicate suppression */ + /* Call-back table support. */ + int index; /* next table index */ + int limit; /* last valid index */ + PSC_CALL_BACK_ENTRY table[1]; /* actually a bunch */ +} PSC_DNSBL_SCORE; + +#define PSC_CALL_BACK_INIT(sp) do { \ + (sp)->limit = 0; \ + (sp)->index = 0; \ + } while (0) + +#define PSC_CALL_BACK_INDEX_OF_LAST(sp) ((sp)->index - 1) + +#define PSC_CALL_BACK_CANCEL(sp, idx) do { \ + PSC_CALL_BACK_ENTRY *_cb_; \ + if ((idx) < 0 || (idx) >= (sp)->index) \ + msg_panic("%s: index %d must be >= 0 and < %d", \ + myname, (idx), (sp)->index); \ + _cb_ = (sp)->table + (idx); \ + event_cancel_timer(_cb_->callback, _cb_->context); \ + _cb_->callback = 0; \ + _cb_->context = 0; \ + } while (0) + +#define PSC_CALL_BACK_EXTEND(hp, sp) do { \ + if ((sp)->index >= (sp)->limit) { \ + int _count_ = ((sp)->limit ? (sp)->limit * 2 : 5); \ + (hp)->value = myrealloc((void *) (sp), sizeof(*(sp)) + \ + _count_ * sizeof((sp)->table)); \ + (sp) = (PSC_DNSBL_SCORE *) (hp)->value; \ + (sp)->limit = _count_; \ + } \ + } while (0) + +#define PSC_CALL_BACK_ENTER(sp, fn, ctx) do { \ + PSC_CALL_BACK_ENTRY *_cb_ = (sp)->table + (sp)->index++; \ + _cb_->callback = (fn); \ + _cb_->context = (ctx); \ + } while (0) + +#define PSC_CALL_BACK_NOTIFY(sp, ev) do { \ + PSC_CALL_BACK_ENTRY *_cb_; \ + for (_cb_ = (sp)->table; _cb_ < (sp)->table + (sp)->index; _cb_++) \ + if (_cb_->callback != 0) \ + _cb_->callback((ev), _cb_->context); \ + } while (0) + +#define PSC_NULL_EVENT (0) + + /* + * Per-request state. + * + * This implementation stores the client IP address and DNSBL domain in the + * DNSBLOG query/reply stream. This simplifies code, and allows the DNSBLOG + * server to produce more informative logging. + */ +static VSTRING *reply_client; /* client address in DNSBLOG reply */ +static VSTRING *reply_dnsbl; /* domain in DNSBLOG reply */ +static VSTRING *reply_addr; /* address list in DNSBLOG reply */ + +/* psc_dnsbl_add_site - add DNSBL site information */ + +static void psc_dnsbl_add_site(const char *site) +{ + const char *myname = "psc_dnsbl_add_site"; + char *saved_site = mystrdup(site); + VSTRING *byte_codes = 0; + PSC_DNSBL_HEAD *head; + PSC_DNSBL_SITE *new_site; + char junk; + const char *weight_text; + char *pattern_text; + int weight; + HTABLE_INFO *ht; + char *parse_err; + const char *safe_dnsbl; + + /* + * Parse the required DNSBL domain name, the optional reply filter and + * the optional reply weight factor. + */ +#define DO_GRIPE 1 + + /* Negative weight means allowlist. */ + if ((weight_text = split_at(saved_site, '*')) != 0) { + if (sscanf(weight_text, "%d%c", &weight, &junk) != 1) + msg_fatal("bad DNSBL weight factor \"%s\" in \"%s\"", + weight_text, site); + } else { + weight = 1; + } + /* Reply filter. */ + if ((pattern_text = split_at(saved_site, '=')) != 0) { + byte_codes = vstring_alloc(100); + if ((parse_err = ip_match_parse(byte_codes, pattern_text)) != 0) + msg_fatal("bad DNSBL filter syntax: %s", parse_err); + } + if (valid_hostname(saved_site, DO_GRIPE) == 0) + msg_fatal("bad DNSBL domain name \"%s\" in \"%s\"", + saved_site, site); + + if (msg_verbose > 1) + msg_info("%s: \"%s\" -> domain=\"%s\" pattern=\"%s\" weight=%d", + myname, site, saved_site, pattern_text ? pattern_text : + "null", weight); + + /* + * Look up or create the (filter, weight) list head for this DNSBL domain + * name. + */ + if ((head = (PSC_DNSBL_HEAD *) + htable_find(dnsbl_site_cache, saved_site)) == 0) { + head = (PSC_DNSBL_HEAD *) mymalloc(sizeof(*head)); + ht = htable_enter(dnsbl_site_cache, saved_site, (void *) head); + /* Translate the DNSBL name into a safe name if available. */ + if (psc_dnsbl_reply == 0 + || (safe_dnsbl = dict_get(psc_dnsbl_reply, saved_site)) == 0) + safe_dnsbl = ht->key; + head->safe_dnsbl = mystrdup(safe_dnsbl); + if (psc_dnsbl_reply && psc_dnsbl_reply->error) + msg_fatal("%s:%s lookup error", psc_dnsbl_reply->type, + psc_dnsbl_reply->name); + head->first = 0; + } + + /* + * Append the new (filter, weight) node to the list for this DNSBL domain + * name. + */ + new_site = (PSC_DNSBL_SITE *) mymalloc(sizeof(*new_site)); + new_site->filter = (pattern_text ? mystrdup(pattern_text) : 0); + new_site->byte_codes = (byte_codes ? ip_match_save(byte_codes) : 0); + new_site->weight = weight; + new_site->next = head->first; + head->first = new_site; + + myfree(saved_site); + if (byte_codes) + vstring_free(byte_codes); +} + +/* psc_dnsbl_match - match DNSBL reply filter */ + +static int psc_dnsbl_match(const char *filter, ARGV *reply) +{ + char addr_buf[MAI_HOSTADDR_STRSIZE]; + char **cpp; + + /* + * Run the replies through the pattern-matching engine. + */ + for (cpp = reply->argv; *cpp != 0; cpp++) { + if (inet_pton(AF_INET, *cpp, addr_buf) != 1) + msg_warn("address conversion error for %s -- ignoring this reply", + *cpp); + if (ip_match_execute(filter, addr_buf)) + return (1); + } + return (0); +} + +/* psc_dnsbl_retrieve - retrieve blocklist score, decrement reference count */ + +int psc_dnsbl_retrieve(const char *client_addr, const char **dnsbl_name, + int dnsbl_index, int *dnsbl_ttl) +{ + const char *myname = "psc_dnsbl_retrieve"; + PSC_DNSBL_SCORE *score; + int result_score; + int result_ttl; + + /* + * Sanity check. + */ + if ((score = (PSC_DNSBL_SCORE *) + htable_find(dnsbl_score_cache, client_addr)) == 0) + msg_panic("%s: no blocklist score for %s", myname, client_addr); + + /* + * Disable callbacks. + */ + PSC_CALL_BACK_CANCEL(score, dnsbl_index); + + /* + * Reads are destructive. + */ + result_score = score->total; + *dnsbl_name = score->dnsbl_name; + result_ttl = (result_score > 0) ? score->fail_ttl : score->pass_ttl; + /* As with dnsblog(8), a value < 0 means no reply TTL. */ + if (result_ttl < var_psc_dnsbl_min_ttl) + result_ttl = var_psc_dnsbl_min_ttl; + if (result_ttl > var_psc_dnsbl_max_ttl) + result_ttl = var_psc_dnsbl_max_ttl; + *dnsbl_ttl = result_ttl; + if (msg_verbose) + msg_info("%s: addr=%s score=%d ttl=%d", + myname, client_addr, result_score, result_ttl); + score->refcount -= 1; + if (score->refcount < 1) { + if (msg_verbose > 1) + msg_info("%s: delete blocklist score for %s", myname, client_addr); + htable_delete(dnsbl_score_cache, client_addr, myfree); + } + return (result_score); +} + +/* psc_dnsbl_receive - receive DNSBL reply, update blocklist score */ + +static void psc_dnsbl_receive(int event, void *context) +{ + const char *myname = "psc_dnsbl_receive"; + VSTREAM *stream = (VSTREAM *) context; + PSC_DNSBL_SCORE *score; + PSC_DNSBL_HEAD *head; + PSC_DNSBL_SITE *site; + ARGV *reply_argv; + int request_id; + int dnsbl_ttl; + + PSC_CLEAR_EVENT_REQUEST(vstream_fileno(stream), psc_dnsbl_receive, context); + + /* + * Receive the DNSBL lookup result. + * + * This is preliminary code to explore the field. Later, DNSBL lookup will + * be handled by an UDP-based DNS client that is built directly into some + * Postfix daemon. + * + * Don't bother looking up the blocklist score when the client IP address is + * not listed at the DNSBL. + * + * Don't panic when the blocklist score no longer exists. It may be deleted + * when the client triggers a "drop" action after pregreet, when the + * client does not pregreet and the DNSBL reply arrives late, or when the + * client triggers a "drop" action after hanging up. + */ + if (event == EVENT_READ + && attr_scan(stream, + ATTR_FLAG_STRICT, + RECV_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, reply_dnsbl), + RECV_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, reply_client), + RECV_ATTR_INT(MAIL_ATTR_LABEL, &request_id), + RECV_ATTR_STR(MAIL_ATTR_RBL_ADDR, reply_addr), + RECV_ATTR_INT(MAIL_ATTR_TTL, &dnsbl_ttl), + ATTR_TYPE_END) == 5 + && (score = (PSC_DNSBL_SCORE *) + htable_find(dnsbl_score_cache, STR(reply_client))) != 0 + && score->request_id == request_id) { + + /* + * Run this response past all applicable DNSBL filters and update the + * blocklist score for this client IP address. + * + * Don't panic when the DNSBL domain name is not found. The DNSBLOG + * server may be messed up. + */ + if (msg_verbose > 1) + msg_info("%s: client=\"%s\" score=%d domain=\"%s\" reply=\"%d %s\"", + myname, STR(reply_client), score->total, + STR(reply_dnsbl), dnsbl_ttl, STR(reply_addr)); + head = (PSC_DNSBL_HEAD *) + htable_find(dnsbl_site_cache, STR(reply_dnsbl)); + if (head == 0) { + /* Bogus domain. Do nothing. */ + } else if (*STR(reply_addr) != 0) { + /* DNS reputation record(s) found. */ + reply_argv = 0; + for (site = head->first; site != 0; site = site->next) { + if (site->byte_codes == 0 + || psc_dnsbl_match(site->byte_codes, reply_argv ? reply_argv : + (reply_argv = argv_split(STR(reply_addr), " ")))) { + if (score->dnsbl_name == 0 + || score->dnsbl_weight < site->weight) { + score->dnsbl_name = head->safe_dnsbl; + score->dnsbl_weight = site->weight; + } + score->total += site->weight; + if (msg_verbose > 1) + msg_info("%s: filter=\"%s\" weight=%d score=%d", + myname, site->filter ? site->filter : "null", + site->weight, score->total); + } + /* As with dnsblog(8), a value < 0 means no reply TTL. */ + if (site->weight > 0) { + if (score->fail_ttl < 0 || score->fail_ttl > dnsbl_ttl) + score->fail_ttl = dnsbl_ttl; + } else { + if (score->pass_ttl < 0 || score->pass_ttl > dnsbl_ttl) + score->pass_ttl = dnsbl_ttl; + } + } + if (reply_argv != 0) + argv_free(reply_argv); + } else { + /* No DNS reputation record found. */ + for (site = head->first; site != 0; site = site->next) { + /* As with dnsblog(8), a value < 0 means no reply TTL. */ + if (site->weight > 0) { + if (score->pass_ttl < 0 || score->pass_ttl > dnsbl_ttl) + score->pass_ttl = dnsbl_ttl; + } else { + if (score->fail_ttl < 0 || score->fail_ttl > dnsbl_ttl) + score->fail_ttl = dnsbl_ttl; + } + } + } + + /* + * Notify the requestor(s) that the result is ready to be picked up. + * If this call isn't made, clients have to sit out the entire + * pre-handshake delay. + */ + score->pending_lookups -= 1; + if (score->pending_lookups == 0) + PSC_CALL_BACK_NOTIFY(score, PSC_NULL_EVENT); + } else if (event == EVENT_TIME) { + msg_warn("dnsblog reply timeout %ds for %s", + var_psc_dnsbl_tmout, (char *) vstream_context(stream)); + } + /* Here, score may be a null pointer. */ + vstream_fclose(stream); +} + +/* psc_dnsbl_request - send dnsbl query, increment reference count */ + +int psc_dnsbl_request(const char *client_addr, + void (*callback) (int, void *), + void *context) +{ + const char *myname = "psc_dnsbl_request"; + int fd; + VSTREAM *stream; + HTABLE_INFO **ht; + PSC_DNSBL_SCORE *score; + HTABLE_INFO *hash_node; + static int request_count; + + /* + * Some spambots make several connections at nearly the same time, + * causing their pregreet delays to overlap. Such connections can share + * the efforts of DNSBL lookup. + * + * We store a reference-counted DNSBL score under its client IP address. We + * increment the reference count with each score request, and decrement + * the reference count with each score retrieval. + * + * Do not notify the requestor NOW when the DNS replies are already in. + * Reason: we must not make a backwards call while we are still in the + * middle of executing the corresponding forward call. Instead we create + * a zero-delay timer request and call the notification function from + * there. + * + * psc_dnsbl_request() could instead return a result value to indicate that + * the DNSBL score is already available, but that would complicate the + * caller with two different notification code paths: one asynchronous + * code path via the callback invocation, and one synchronous code path + * via the psc_dnsbl_request() result value. That would be a source of + * future bugs. + */ + if ((hash_node = htable_locate(dnsbl_score_cache, client_addr)) != 0) { + score = (PSC_DNSBL_SCORE *) hash_node->value; + score->refcount += 1; + PSC_CALL_BACK_EXTEND(hash_node, score); + PSC_CALL_BACK_ENTER(score, callback, context); + if (msg_verbose > 1) + msg_info("%s: reuse blocklist score for %s refcount=%d pending=%d", + myname, client_addr, score->refcount, + score->pending_lookups); + if (score->pending_lookups == 0) + event_request_timer(callback, context, EVENT_NULL_DELAY); + return (PSC_CALL_BACK_INDEX_OF_LAST(score)); + } + if (msg_verbose > 1) + msg_info("%s: create blocklist score for %s", myname, client_addr); + score = (PSC_DNSBL_SCORE *) mymalloc(sizeof(*score)); + score->request_id = request_count++; + score->dnsbl_name = 0; + score->dnsbl_weight = 0; + /* As with dnsblog(8), a value < 0 means no reply TTL. */ + score->pass_ttl = -1; + score->fail_ttl = -1; + score->total = 0; + score->refcount = 1; + score->pending_lookups = 0; + PSC_CALL_BACK_INIT(score); + PSC_CALL_BACK_ENTER(score, callback, context); + (void) htable_enter(dnsbl_score_cache, client_addr, (void *) score); + + /* + * Send a query to all DNSBL servers. Later, DNSBL lookup will be done + * with an UDP-based DNS client that is built directly into Postfix code. + * We therefore do not optimize the maximum out of this temporary + * implementation. + */ + for (ht = dnsbl_site_list; *ht; ht++) { + if ((fd = LOCAL_CONNECT(psc_dnsbl_service, NON_BLOCKING, 1)) < 0) { + msg_warn("%s: connect to %s service: %m", + myname, psc_dnsbl_service); + continue; + } + stream = vstream_fdopen(fd, O_RDWR); + vstream_control(stream, + CA_VSTREAM_CTL_CONTEXT(ht[0]->key), + CA_VSTREAM_CTL_END); + attr_print(stream, ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_RBL_DOMAIN, ht[0]->key), + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, client_addr), + SEND_ATTR_INT(MAIL_ATTR_LABEL, score->request_id), + ATTR_TYPE_END); + if (vstream_fflush(stream) != 0) { + msg_warn("%s: error sending to %s service: %m", + myname, psc_dnsbl_service); + vstream_fclose(stream); + continue; + } + PSC_READ_EVENT_REQUEST(vstream_fileno(stream), psc_dnsbl_receive, + (void *) stream, var_psc_dnsbl_tmout); + score->pending_lookups += 1; + } + return (PSC_CALL_BACK_INDEX_OF_LAST(score)); +} + +/* psc_dnsbl_init - initialize */ + +void psc_dnsbl_init(void) +{ + const char *myname = "psc_dnsbl_init"; + ARGV *dnsbl_site = argv_split(var_psc_dnsbl_sites, CHARS_COMMA_SP); + char **cpp; + + /* + * Sanity check. + */ + if (dnsbl_site_cache != 0) + msg_panic("%s: called more than once", myname); + + /* + * pre-compute the DNSBLOG socket name. + */ + psc_dnsbl_service = concatenate(MAIL_CLASS_PRIVATE, "/", + var_dnsblog_service, (char *) 0); + + /* + * Prepare for quick iteration when sending out queries to all DNSBL + * servers, and for quick lookup when a reply arrives from a specific + * DNSBL server. + */ + dnsbl_site_cache = htable_create(13); + for (cpp = dnsbl_site->argv; *cpp; cpp++) + psc_dnsbl_add_site(*cpp); + argv_free(dnsbl_site); + dnsbl_site_list = htable_list(dnsbl_site_cache); + + /* + * The per-client blocklist score. + */ + dnsbl_score_cache = htable_create(13); + + /* + * Space for ad-hoc DNSBLOG server request/reply parameters. + */ + reply_client = vstring_alloc(100); + reply_dnsbl = vstring_alloc(100); + reply_addr = vstring_alloc(100); +} diff --git a/src/postscreen/postscreen_early.c b/src/postscreen/postscreen_early.c new file mode 100644 index 0000000..c9d8faf --- /dev/null +++ b/src/postscreen/postscreen_early.c @@ -0,0 +1,377 @@ +/*++ +/* NAME +/* postscreen_early 3 +/* SUMMARY +/* postscreen pre-handshake tests +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* void psc_early_init(void) +/* +/* void psc_early_tests(state) +/* PSC_STATE *state; +/* DESCRIPTION +/* psc_early_tests() performs protocol tests before the SMTP +/* handshake: the pregreet test and the DNSBL test. Control +/* is passed to the psc_smtpd_tests() routine as appropriate. +/* +/* psc_early_init() performs one-time initialization. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/socket.h> +#include <limits.h> + +/* Utility library. */ + +#include <msg.h> +#include <stringops.h> +#include <mymalloc.h> +#include <vstring.h> + +/* Global library. */ + +#include <mail_params.h> + +/* Application-specific. */ + +#include <postscreen.h> + +static char *psc_teaser_greeting; +static VSTRING *psc_escape_buf; + +/* psc_allowlist_non_dnsbl - allowlist pending non-dnsbl tests */ + +static void psc_allowlist_non_dnsbl(PSC_STATE *state) +{ + time_t now; + int tindx; + + /* + * If no tests failed (we can't undo those), and if the allowlist + * threshold is met, flag non-dnsbl tests that are pending or disabled as + * successfully completed, and set their expiration times equal to the + * DNSBL expiration time, except for tests that would expire later. + * + * Why flag disabled tests as passed? When a disabled test is turned on, + * postscreen should not apply that test to clients that are already + * allowlisted based on their combined DNSBL score. + */ + if ((state->flags & PSC_STATE_MASK_ANY_FAIL) == 0 + && state->dnsbl_score < var_psc_dnsbl_thresh + && var_psc_dnsbl_althresh < 0 + && state->dnsbl_score <= var_psc_dnsbl_althresh) { + now = event_time(); + for (tindx = 0; tindx < PSC_TINDX_COUNT; tindx++) { + if (tindx == PSC_TINDX_DNSBL) + continue; + if ((state->flags & PSC_STATE_FLAG_BYTINDX_TODO(tindx)) + && !(state->flags & PSC_STATE_FLAG_BYTINDX_PASS(tindx))) { + if (msg_verbose) + msg_info("skip %s test for [%s]:%s", + psc_test_name(tindx), PSC_CLIENT_ADDR_PORT(state)); + /* Wrong for deep protocol tests, but we disable those. */ + state->flags |= PSC_STATE_FLAG_BYTINDX_DONE(tindx); + /* This also disables pending deep protocol tests. */ + state->flags |= PSC_STATE_FLAG_BYTINDX_PASS(tindx); + } + /* Update expiration even if the test was completed or disabled. */ + if (state->client_info->expire_time[tindx] < now + state->dnsbl_ttl) + state->client_info->expire_time[tindx] = now + state->dnsbl_ttl; + } + } +} + +/* psc_early_event - handle pre-greet, EOF, and DNSBL results. */ + +static void psc_early_event(int event, void *context) +{ + const char *myname = "psc_early_event"; + PSC_STATE *state = (PSC_STATE *) context; + time_t *expire_time = state->client_info->expire_time; + char read_buf[PSC_READ_BUF_SIZE]; + int read_count; + DELTA_TIME elapsed; + + if (msg_verbose > 1) + msg_info("%s: sq=%d cq=%d event %d on smtp socket %d from [%s]:%s flags=%s", + myname, psc_post_queue_length, psc_check_queue_length, + event, vstream_fileno(state->smtp_client_stream), + state->smtp_client_addr, state->smtp_client_port, + psc_print_state_flags(state->flags, myname)); + + PSC_CLEAR_EVENT_REQUEST(vstream_fileno(state->smtp_client_stream), + psc_early_event, context); + + /* + * XXX Be sure to empty the DNSBL lookup buffer otherwise we have a + * memory leak. + * + * XXX We can avoid "forgetting" to do this by keeping a pointer to the + * DNSBL lookup buffer in the PSC_STATE structure. This also allows us to + * shave off a hash table lookup when retrieving the DNSBL result. + * + * A direct pointer increases the odds of dangling pointers. Hash-table + * lookup is safer, and that is why it's done that way. + */ + switch (event) { + + /* + * We either reached the end of the early tests time limit, or all + * early tests completed before the pregreet timer would go off. + */ + case EVENT_TIME: + + /* + * Check if the SMTP client spoke before its turn. + */ + if ((state->flags & PSC_STATE_FLAG_PREGR_TODO) != 0 + && (state->flags & PSC_STATE_MASK_PREGR_FAIL_DONE) == 0) { + expire_time[PSC_TINDX_PREGR] = event_time() + var_psc_pregr_ttl; + PSC_PASS_SESSION_STATE(state, "pregreet test", + PSC_STATE_FLAG_PREGR_PASS); + } + if ((state->flags & PSC_STATE_FLAG_PREGR_FAIL) + && psc_pregr_action == PSC_ACT_IGNORE) { + PSC_UNFAIL_SESSION_STATE(state, PSC_STATE_FLAG_PREGR_FAIL); + /* Not: PSC_PASS_SESSION_STATE. Repeat this test the next time. */ + } + + /* + * Collect the DNSBL score, and allowlist other tests if applicable. + * Note: this score will be partial when some DNS lookup did not + * complete before the pregreet timer expired. + * + * If the client is DNS blocklisted, drop the connection, send the + * client to a dummy protocol engine, or continue to the next test. + */ +#define PSC_DNSBL_FORMAT \ + "%s 5.7.1 Service unavailable; client [%s] blocked using %s\r\n" +#define NO_DNSBL_SCORE INT_MAX + + if (state->flags & PSC_STATE_FLAG_DNSBL_TODO) { + if (state->dnsbl_score == NO_DNSBL_SCORE) { + state->dnsbl_score = + psc_dnsbl_retrieve(state->smtp_client_addr, + &state->dnsbl_name, + state->dnsbl_index, + &state->dnsbl_ttl); + if (var_psc_dnsbl_althresh < 0) + psc_allowlist_non_dnsbl(state); + } + if (state->dnsbl_score < var_psc_dnsbl_thresh) { + expire_time[PSC_TINDX_DNSBL] = event_time() + state->dnsbl_ttl; + PSC_PASS_SESSION_STATE(state, "dnsbl test", + PSC_STATE_FLAG_DNSBL_PASS); + } else { + msg_info("DNSBL rank %d for [%s]:%s", + state->dnsbl_score, PSC_CLIENT_ADDR_PORT(state)); + PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_DNSBL_FAIL); + switch (psc_dnsbl_action) { + case PSC_ACT_DROP: + state->dnsbl_reply = vstring_sprintf(vstring_alloc(100), + PSC_DNSBL_FORMAT, "521", + state->smtp_client_addr, + state->dnsbl_name); + PSC_DROP_SESSION_STATE(state, STR(state->dnsbl_reply)); + return; + case PSC_ACT_ENFORCE: + state->dnsbl_reply = vstring_sprintf(vstring_alloc(100), + PSC_DNSBL_FORMAT, "550", + state->smtp_client_addr, + state->dnsbl_name); + PSC_ENFORCE_SESSION_STATE(state, STR(state->dnsbl_reply)); + break; + case PSC_ACT_IGNORE: + PSC_UNFAIL_SESSION_STATE(state, PSC_STATE_FLAG_DNSBL_FAIL); + /* Not: PSC_PASS_SESSION_STATE. Repeat this test. */ + break; + default: + msg_panic("%s: unknown dnsbl action value %d", + myname, psc_dnsbl_action); + + } + } + } + + /* + * Pass the connection to a real SMTP server, or enter the dummy + * engine for deep tests. + */ + if ((state->flags & PSC_STATE_FLAG_NOFORWARD) != 0 + || ((state->flags & PSC_STATE_MASK_SMTPD_PASS) + != PSC_STATE_FLAGS_TODO_TO_PASS(state->flags & PSC_STATE_MASK_SMTPD_TODO))) + psc_smtpd_tests(state); + else + psc_conclude(state); + return; + + /* + * EOF, or the client spoke before its turn. We simply drop the + * connection, or we continue waiting and allow DNS replies to + * trickle in. + */ + default: + if ((read_count = recv(vstream_fileno(state->smtp_client_stream), + read_buf, sizeof(read_buf) - 1, MSG_PEEK)) <= 0) { + /* Avoid memory leak. */ + if (state->dnsbl_score == NO_DNSBL_SCORE + && (state->flags & PSC_STATE_FLAG_DNSBL_TODO)) + (void) psc_dnsbl_retrieve(state->smtp_client_addr, + &state->dnsbl_name, + state->dnsbl_index, + &state->dnsbl_ttl); + /* XXX Wait for DNS replies to come in. */ + psc_hangup_event(state); + return; + } + read_buf[read_count] = 0; + escape(psc_escape_buf, read_buf, read_count); + msg_info("PREGREET %d after %s from [%s]:%s: %.100s", read_count, + psc_format_delta_time(psc_temp, state->start_time, &elapsed), + PSC_CLIENT_ADDR_PORT(state), STR(psc_escape_buf)); + PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_PREGR_FAIL); + switch (psc_pregr_action) { + case PSC_ACT_DROP: + /* Avoid memory leak. */ + if (state->dnsbl_score == NO_DNSBL_SCORE + && (state->flags & PSC_STATE_FLAG_DNSBL_TODO)) + (void) psc_dnsbl_retrieve(state->smtp_client_addr, + &state->dnsbl_name, + state->dnsbl_index, + &state->dnsbl_ttl); + PSC_DROP_SESSION_STATE(state, "521 5.5.1 Protocol error\r\n"); + return; + case PSC_ACT_ENFORCE: + /* We call psc_dnsbl_retrieve() when the timer expires. */ + PSC_ENFORCE_SESSION_STATE(state, "550 5.5.1 Protocol error\r\n"); + break; + case PSC_ACT_IGNORE: + /* We call psc_dnsbl_retrieve() when the timer expires. */ + /* We must handle this case after the timer expires. */ + break; + default: + msg_panic("%s: unknown pregreet action value %d", + myname, psc_pregr_action); + } + + /* + * Terminate the greet delay if we're just waiting for the pregreet + * test to complete. It is safe to call psc_early_event directly, + * since we are already in that function. + * + * XXX After this code passes all tests, swap around the two blocks in + * this switch statement and fall through from EVENT_READ into + * EVENT_TIME, instead of calling psc_early_event recursively. + */ + state->flags |= PSC_STATE_FLAG_PREGR_DONE; + if (elapsed.dt_sec >= PSC_EFF_GREET_WAIT + || ((state->flags & PSC_STATE_MASK_EARLY_DONE) + == PSC_STATE_FLAGS_TODO_TO_DONE(state->flags & PSC_STATE_MASK_EARLY_TODO))) + psc_early_event(EVENT_TIME, context); + else + event_request_timer(psc_early_event, context, + PSC_EFF_GREET_WAIT - elapsed.dt_sec); + return; + } +} + +/* psc_early_dnsbl_event - cancel pregreet timer if waiting for DNS only */ + +static void psc_early_dnsbl_event(int unused_event, void *context) +{ + const char *myname = "psc_early_dnsbl_event"; + PSC_STATE *state = (PSC_STATE *) context; + + if (msg_verbose) + msg_info("%s: notify [%s]:%s", myname, PSC_CLIENT_ADDR_PORT(state)); + + /* + * Collect the DNSBL score, and allowlist other tests if applicable. + */ + state->dnsbl_score = + psc_dnsbl_retrieve(state->smtp_client_addr, &state->dnsbl_name, + state->dnsbl_index, &state->dnsbl_ttl); + if (var_psc_dnsbl_althresh < 0) + psc_allowlist_non_dnsbl(state); + + /* + * Terminate the greet delay if we're just waiting for DNSBL lookup to + * complete. Don't call psc_early_event directly, that would result in a + * dangling pointer. + */ + state->flags |= PSC_STATE_FLAG_DNSBL_DONE; + if ((state->flags & PSC_STATE_MASK_EARLY_DONE) + == PSC_STATE_FLAGS_TODO_TO_DONE(state->flags & PSC_STATE_MASK_EARLY_TODO)) + event_request_timer(psc_early_event, context, EVENT_NULL_DELAY); +} + +/* psc_early_tests - start the early (before protocol) tests */ + +void psc_early_tests(PSC_STATE *state) +{ + const char *myname = "psc_early_tests"; + + /* + * Report errors and progress in the context of this test. + */ + PSC_BEGIN_TESTS(state, "tests before SMTP handshake"); + + /* + * Run a PREGREET test. Send half the greeting banner, by way of teaser, + * then wait briefly to see if the client speaks before its turn. + */ + if ((state->flags & PSC_STATE_FLAG_PREGR_TODO) != 0 + && psc_teaser_greeting != 0 + && PSC_SEND_REPLY(state, psc_teaser_greeting) != 0) { + psc_hangup_event(state); + return; + } + + /* + * Run a DNS blocklist query. + */ + if ((state->flags & PSC_STATE_FLAG_DNSBL_TODO) != 0) + state->dnsbl_index = + psc_dnsbl_request(state->smtp_client_addr, psc_early_dnsbl_event, + (void *) state); + else + state->dnsbl_index = -1; + state->dnsbl_score = NO_DNSBL_SCORE; + + /* + * Wait for the client to respond or for DNS lookup to complete. + */ + if ((state->flags & PSC_STATE_FLAG_PREGR_TODO) != 0) + PSC_READ_EVENT_REQUEST(vstream_fileno(state->smtp_client_stream), + psc_early_event, (void *) state, PSC_EFF_GREET_WAIT); + else + event_request_timer(psc_early_event, (void *) state, PSC_EFF_GREET_WAIT); +} + +/* psc_early_init - initialize early tests */ + +void psc_early_init(void) +{ + if (*var_psc_pregr_banner) { + vstring_sprintf(psc_temp, "220-%s\r\n", var_psc_pregr_banner); + psc_teaser_greeting = mystrdup(STR(psc_temp)); + psc_escape_buf = vstring_alloc(100); + } +} diff --git a/src/postscreen/postscreen_endpt.c b/src/postscreen/postscreen_endpt.c new file mode 100644 index 0000000..36949e3 --- /dev/null +++ b/src/postscreen/postscreen_endpt.c @@ -0,0 +1,232 @@ +/*++ +/* NAME +/* postscreen_endpt 3 +/* SUMMARY +/* look up connection endpoint information +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* void psc_endpt_lookup(smtp_client_stream, lookup_done) +/* VSTREAM *smtp_client_stream; +/* void (*lookup_done)(status, smtp_client_stream, +/* smtp_client_addr, smtp_client_port, +/* smtp_server_addr, smtp_server_port) +/* int status; +/* MAI_HOSTADDR_STR *smtp_client_addr; +/* MAI_SERVPORT_STR *smtp_client_port; +/* MAI_HOSTADDR_STR *smtp_server_addr; +/* MAI_SERVPORT_STR *smtp_server_port; +/* AUXILIARY METHODS +/* void psc_endpt_local_lookup(smtp_client_stream, lookup_done) +/* VSTREAM *smtp_client_stream; +/* void (*lookup_done)(status, smtp_client_stream, +/* smtp_client_addr, smtp_client_port, +/* smtp_server_addr, smtp_server_port) +/* int status; +/* MAI_HOSTADDR_STR *smtp_client_addr; +/* MAI_SERVPORT_STR *smtp_client_port; +/* MAI_HOSTADDR_STR *smtp_server_addr; +/* MAI_SERVPORT_STR *smtp_server_port; +/* DESCRIPTION +/* psc_endpt_lookup() looks up remote and local connection +/* endpoint information, either through local system calls, +/* or through an adapter for an up-stream proxy protocol. +/* +/* The following summarizes what the postscreen(8) server +/* expects from a proxy protocol adapter routine. +/* .IP \(bu +/* Accept the same arguments as psc_endpt_lookup(). +/* .IP \(bu +/* Call psc_endpt_local_lookup() to look up connection info +/* when the upstream proxy indicates that the connection is +/* not proxied (e.g., health check probe). +/* .IP \(bu +/* Validate protocol, address and port syntax. Permit only +/* protocols that are configured with the main.cf:inet_protocols +/* setting. +/* .IP \(bu +/* Convert IPv4-in-IPv6 address syntax to IPv4 syntax when +/* both IPv6 and IPv4 support are enabled with main.cf:inet_protocols. +/* .IP \(bu +/* Log a clear warning message that explains why a request +/* fails. +/* .IP \(bu +/* Never talk to the remote SMTP client. +/* .PP +/* Arguments: +/* .IP client_stream +/* A brand-new stream that is connected to the remote client. +/* This argument MUST be passed to psc_endpt_local_lookup() +/* if the up-stream proxy indicates that a connection is not +/* proxied. +/* .IP lookup +/* Call-back routine that reports the result status, address +/* and port information. The result status is -1 in case of +/* error, 0 in case of success. This MUST NOT be called directly +/* if the up-stream proxy indicates that a connection is not +/* proxied; instead this MUST be called indirectly by +/* psc_endpt_local_lookup(). +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <string.h> + +#ifdef STRCASECMP_IN_STRINGS_H +#include <strings.h> +#endif + +/* Utility library. */ + +#include <msg.h> +#include <myaddrinfo.h> +#include <vstream.h> +#include <inet_proto.h> + +/* Global library. */ + +#include <mail_params.h> +#include <haproxy_srvr.h> + +/* Application-specific. */ + +#include <postscreen.h> +#include <postscreen_haproxy.h> + +static const INET_PROTO_INFO *proto_info; + +/* psc_sockaddr_to_hostaddr - transform endpoint address and port to string */ + +static int psc_sockaddr_to_hostaddr(struct sockaddr *addr_storage, + SOCKADDR_SIZE addr_storage_len, + MAI_HOSTADDR_STR *addr_buf, + MAI_SERVPORT_STR *port_buf, + int socktype) +{ + int aierr; + + if ((aierr = sockaddr_to_hostaddr(addr_storage, addr_storage_len, + addr_buf, port_buf, socktype)) == 0 + && strncasecmp("::ffff:", addr_buf->buf, 7) == 0 + && strchr((char *) proto_info->sa_family_list, AF_INET) != 0) + memmove(addr_buf->buf, addr_buf->buf + 7, + sizeof(addr_buf->buf) - 7); + return (aierr); +} + +/* psc_endpt_local_lookup - look up local system connection information */ + +void psc_endpt_local_lookup(VSTREAM *smtp_client_stream, + PSC_ENDPT_LOOKUP_FN lookup_done) +{ + struct sockaddr_storage addr_storage; + SOCKADDR_SIZE addr_storage_len = sizeof(addr_storage); + int status; + MAI_HOSTADDR_STR smtp_client_addr; + MAI_SERVPORT_STR smtp_client_port; + MAI_HOSTADDR_STR smtp_server_addr; + MAI_SERVPORT_STR smtp_server_port; + int aierr; + + /* + * Look up the remote SMTP client address and port. + */ + if (getpeername(vstream_fileno(smtp_client_stream), (struct sockaddr *) + &addr_storage, &addr_storage_len) < 0) { + msg_warn("getpeername: %m -- dropping this connection"); + status = -1; + } + + /* + * Convert the remote SMTP client address and port to printable form for + * logging and access control. + */ + else if ((aierr = psc_sockaddr_to_hostaddr( + (struct sockaddr *) &addr_storage, + addr_storage_len, &smtp_client_addr, + &smtp_client_port, SOCK_STREAM)) != 0) { + msg_warn("cannot convert client address/port to string: %s" + " -- dropping this connection", + MAI_STRERROR(aierr)); + status = -1; + } + + /* + * Look up the local SMTP server address and port. + */ + else if (getsockname(vstream_fileno(smtp_client_stream), + (struct sockaddr *) &addr_storage, + &addr_storage_len) < 0) { + msg_warn("getsockname: %m -- dropping this connection"); + status = -1; + } + + /* + * Convert the local SMTP server address and port to printable form for + * logging. + */ + else if ((aierr = psc_sockaddr_to_hostaddr( + (struct sockaddr *) &addr_storage, + addr_storage_len, &smtp_server_addr, + &smtp_server_port, SOCK_STREAM)) != 0) { + msg_warn("cannot convert server address/port to string: %s" + " -- dropping this connection", + MAI_STRERROR(aierr)); + status = -1; + } else { + status = 0; + } + lookup_done(status, smtp_client_stream, + &smtp_client_addr, &smtp_client_port, + &smtp_server_addr, &smtp_server_port); +} + + /* + * Lookup table for available proxy protocols. + */ +typedef struct { + const char *name; + void (*endpt_lookup) (VSTREAM *, PSC_ENDPT_LOOKUP_FN); +} PSC_ENDPT_LOOKUP_INFO; + +static const PSC_ENDPT_LOOKUP_INFO psc_endpt_lookup_info[] = { + NOPROXY_PROTO_NAME, psc_endpt_local_lookup, + HAPROXY_PROTO_NAME, psc_endpt_haproxy_lookup, + 0, +}; + +/* psc_endpt_lookup - look up connection endpoint information */ + +void psc_endpt_lookup(VSTREAM *smtp_client_stream, + PSC_ENDPT_LOOKUP_FN notify) +{ + const PSC_ENDPT_LOOKUP_INFO *pp; + + if (proto_info == 0) + proto_info = inet_proto_info(); + + for (pp = psc_endpt_lookup_info; /* see below */ ; pp++) { + if (pp->name == 0) + msg_fatal("unsupported %s value: %s", + VAR_PSC_UPROXY_PROTO, var_psc_uproxy_proto); + if (strcmp(var_psc_uproxy_proto, pp->name) == 0) { + pp->endpt_lookup(smtp_client_stream, notify); + return; + } + } +} diff --git a/src/postscreen/postscreen_expand.c b/src/postscreen/postscreen_expand.c new file mode 100644 index 0000000..ecda543 --- /dev/null +++ b/src/postscreen/postscreen_expand.c @@ -0,0 +1,141 @@ +/*++ +/* NAME +/* postscreen_expand 3 +/* SUMMARY +/* SMTP server macro expansion +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* void psc_expand_init() +/* +/* VSTRING *psc_expand_filter; +/* +/* const char *psc_expand_lookup(name, unused_mode, context) +/* const char *name; +/* int unused_mode; +/* char *context; +/* DESCRIPTION +/* This module expands session-related macros. +/* +/* psc_expand_init() performs one-time initialization +/* of the psc_expand_filter buffer. +/* +/* The psc_expand_filter buffer contains the characters +/* that are allowed in macro expansion, as specified with the +/* psc_expand_filter configuration parameter. +/* +/* psc_expand_lookup() returns the value of the named +/* macro or a null pointer. +/* +/* Arguments: +/* .IP name +/* Macro name. +/* .IP context +/* Call-back context (a PSC_STATE pointer). +/* DIAGNOSTICS +/* Panic: interface violations. Fatal errors: out of memory. +/* internal protocol errors. postscreen_expand() returns the +/* binary OR of MAC_PARSE_ERROR (syntax error) and MAC_PARSE_UNDEF +/* (undefined macro name). +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <time.h> + +/* Utility library. */ + +#include <msg.h> +#include <vstring.h> +#include <stringops.h> + +/* Global library. */ + +#include <mail_params.h> +#include <mail_proto.h> + +/* Application-specific. */ + +#include <postscreen.h> + + /* + * Pre-parsed expansion filter. + */ +VSTRING *psc_expand_filter; + +/* psc_expand_init - initialize once during process lifetime */ + +void psc_expand_init(void) +{ + + /* + * Expand the expansion filter :-) + */ + psc_expand_filter = vstring_alloc(10); + unescape(psc_expand_filter, var_psc_exp_filter); +} + +/* psc_expand_lookup - generic SMTP attribute $name expansion */ + +const char *psc_expand_lookup(const char *name, int unused_mode, + void *context) +{ + PSC_STATE *state = (PSC_STATE *) context; + time_t now; + struct tm *lt; + + if (state->expand_buf == 0) + state->expand_buf = vstring_alloc(10); + + if (msg_verbose > 1) + msg_info("psc_expand_lookup: ${%s}", name); + +#define STREQ(x,y) (*(x) == *(y) && strcmp((x), (y)) == 0) +#define STREQN(x,y,n) (*(x) == *(y) && strncmp((x), (y), (n)) == 0) +#define CONST_LEN(x) (sizeof(x) - 1) + + /* + * Don't query main.cf parameters, as the result of expansion could + * reveal system-internal information in server replies. + * + * XXX: This said, multiple servers may be behind a single client-visible + * name or IP address, and each may generate its own logs. Therefore, it + * may be useful to expose the replying MTA id (myhostname) in the + * contact footer, to identify the right logs. So while we don't expose + * the raw configuration dictionary, we do expose "$myhostname" as + * expanded in var_myhostname. + * + * Return NULL only for non-existent names. + */ + if (STREQ(name, MAIL_ATTR_SERVER_NAME)) { + return (var_myhostname); + } else if (STREQ(name, MAIL_ATTR_ACT_CLIENT_ADDR)) { + return (state->smtp_client_addr); + } else if (STREQ(name, MAIL_ATTR_ACT_CLIENT_PORT)) { + return (state->smtp_client_port); + } if (STREQ(name, MAIL_ATTR_LOCALTIME)) { + if (time(&now) == (time_t) -1) + msg_fatal("time lookup failed: %m"); + lt = localtime(&now); + VSTRING_RESET(state->expand_buf); + do { + VSTRING_SPACE(state->expand_buf, 100); + } while (strftime(STR(state->expand_buf), + vstring_avail(state->expand_buf), + "%b %d %H:%M:%S", lt) == 0); + return (STR(state->expand_buf)); + } else { + msg_warn("unknown macro name \"%s\" in expansion request", name); + return (0); + } +} diff --git a/src/postscreen/postscreen_haproxy.c b/src/postscreen/postscreen_haproxy.c new file mode 100644 index 0000000..45c6b9a --- /dev/null +++ b/src/postscreen/postscreen_haproxy.c @@ -0,0 +1,137 @@ +/*++ +/* NAME +/* postscreen_haproxy 3 +/* SUMMARY +/* haproxy protocol adapter +/* SYNOPSIS +/* #include <postscreen_haproxy.h> +/* +/* void psc_endpt_haproxy_lookup(smtp_client_stream, lookup_done) +/* VSTRING *smtp_client_stream; +/* void (*lookup_done)(status, smtp_client_stream, +/* smtp_client_addr, smtp_client_port, +/* smtp_server_addr, smtp_server_port) +/* int status; +/* MAI_HOSTADDR_STR *smtp_client_addr; +/* MAI_SERVPORT_STR *smtp_client_port; +/* MAI_HOSTADDR_STR *smtp_server_addr; +/* MAI_SERVPORT_STR *smtp_server_port; +/* DESCRIPTION +/* psc_endpt_haproxy_lookup() looks up connection endpoint +/* information via the haproxy protocol, or looks up local +/* information if the haproxy handshake indicates that a +/* connection is not proxied. Arguments and results conform +/* to the postscreen_endpt(3) API. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <stdio.h> +#include <stdarg.h> +#include <stdlib.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <events.h> +#include <myaddrinfo.h> +#include <vstream.h> +#include <vstring.h> +#include <stringops.h> + +/* Global library. */ + +#include <haproxy_srvr.h> +#include <mail_params.h> + +/* Application-specific. */ + +#include <postscreen.h> +#include <postscreen_haproxy.h> + + /* + * Per-session state. + */ +typedef struct { + VSTREAM *stream; + PSC_ENDPT_LOOKUP_FN notify; +} PSC_HAPROXY_STATE; + +/* psc_endpt_haproxy_event - read or time event */ + +static void psc_endpt_haproxy_event(int event, void *context) +{ + const char *myname = "psc_endpt_haproxy_event"; + PSC_HAPROXY_STATE *state = (PSC_HAPROXY_STATE *) context; + int status = 0; + MAI_HOSTADDR_STR smtp_client_addr; + MAI_SERVPORT_STR smtp_client_port; + MAI_HOSTADDR_STR smtp_server_addr; + MAI_SERVPORT_STR smtp_server_port; + int non_proxy = 0; + + switch (event) { + case EVENT_TIME: + msg_warn("haproxy read: time limit exceeded"); + status = -1; + break; + case EVENT_READ: + status = haproxy_srvr_receive(vstream_fileno(state->stream), &non_proxy, + &smtp_client_addr, &smtp_client_port, + &smtp_server_addr, &smtp_server_port); + } + + /* + * Terminate this pseudo thread, and notify the caller. + */ + PSC_CLEAR_EVENT_REQUEST(vstream_fileno(state->stream), + psc_endpt_haproxy_event, context); + if (status == 0 && non_proxy) + psc_endpt_local_lookup(state->stream, state->notify); + else + state->notify(status, state->stream, + &smtp_client_addr, &smtp_client_port, + &smtp_server_addr, &smtp_server_port); + /* Note: the stream may be closed at this point. */ + myfree((void *) state); +} + +/* psc_endpt_haproxy_lookup - event-driven haproxy client */ + +void psc_endpt_haproxy_lookup(VSTREAM *stream, + PSC_ENDPT_LOOKUP_FN notify) +{ + const char *myname = "psc_endpt_haproxy_lookup"; + PSC_HAPROXY_STATE *state; + + /* + * Prepare the per-session state. XXX To improve overload behavior, + * maintain a pool of these so that we can reduce memory allocator + * activity. + */ + state = (PSC_HAPROXY_STATE *) mymalloc(sizeof(*state)); + state->stream = stream; + state->notify = notify; + + /* + * Read the haproxy line. + */ + PSC_READ_EVENT_REQUEST(vstream_fileno(stream), psc_endpt_haproxy_event, + (void *) state, var_psc_uproxy_tmout); +} diff --git a/src/postscreen/postscreen_haproxy.h b/src/postscreen/postscreen_haproxy.h new file mode 100644 index 0000000..7557fb3 --- /dev/null +++ b/src/postscreen/postscreen_haproxy.h @@ -0,0 +1,30 @@ +/*++ +/* NAME +/* postscreen_haproxy 3h +/* SUMMARY +/* postscreen haproxy protocol support +/* SYNOPSIS +/* #include <postscreen_haproxy.h> +/* DESCRIPTION +/* .nf + + /* + * haproxy protocol interface. + */ +extern void psc_endpt_haproxy_lookup(VSTREAM *, PSC_ENDPT_LOOKUP_FN); + +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ diff --git a/src/postscreen/postscreen_misc.c b/src/postscreen/postscreen_misc.c new file mode 100644 index 0000000..f07c9ac --- /dev/null +++ b/src/postscreen/postscreen_misc.c @@ -0,0 +1,164 @@ +/*++ +/* NAME +/* postscreen_misc 3 +/* SUMMARY +/* postscreen misc routines +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* char *psc_format_delta_time(buf, tv, delta) +/* VSTRING *buf; +/* struct timeval tv; +/* DELTA_TIME *delta; +/* +/* void psc_conclude(state) +/* PSC_STATE *state; +/* +/* void psc_hangup_event(state) +/* PSC_STATE *state; +/* DESCRIPTION +/* psc_format_delta_time() computes the time difference between +/* tv (past) and the present, formats the time difference with +/* sub-second resolution in a human-readable way, and returns +/* the integer time difference in seconds through the delta +/* argument. +/* +/* psc_conclude() logs when a client passes all necessary tests, +/* updates the postscreen cache for any testes that were passed, +/* and either forwards the connection to a real SMTP server or +/* replies with the text in state->error_reply and hangs up the +/* connection (by default, state->error_reply is set to a +/* default 421 reply). +/* +/* psc_hangup_event() cleans up after a client connection breaks +/* unexpectedly. If logs the test where the break happened, +/* and how much time as spent in that test before the connection +/* broke. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> + +/* Utility library. */ + +#include <msg.h> +#include <vstring.h> +#include <iostuff.h> +#include <format_tv.h> + +/* Global library. */ + +#include <mail_params.h> + +/* Application-specific. */ + +#include <postscreen.h> + +/* psc_format_delta_time - pretty-formatted delta time */ + +char *psc_format_delta_time(VSTRING *buf, struct timeval tv, + DELTA_TIME *delta) +{ + DELTA_TIME pdelay; + struct timeval now; + + GETTIMEOFDAY(&now); + PSC_CALC_DELTA(pdelay, now, tv); + VSTRING_RESET(buf); + format_tv(buf, pdelay.dt_sec, pdelay.dt_usec, SIG_DIGS, var_delay_max_res); + *delta = pdelay; + return (STR(buf)); +} + +/* psc_conclude - bring this session to a conclusion */ + +void psc_conclude(PSC_STATE *state) +{ + const char *myname = "psc_conclude"; + + if (msg_verbose) + msg_info("flags for %s: %s", + myname, psc_print_state_flags(state->flags, myname)); + + /* + * Handle clients that passed at least one test other than permanent + * allowlisting, and that didn't fail any test including permanent + * denylisting. There may still be unfinished tests; those tests will + * need to be completed when the client returns in a later session. + */ + if (state->flags & PSC_STATE_MASK_ANY_FAIL) + state->flags &= ~PSC_STATE_MASK_ANY_PASS; + + /* + * Log our final blessing when all unfinished tests were completed. + */ + if ((state->flags & PSC_STATE_MASK_ANY_PASS) != 0 + && (state->flags & PSC_STATE_MASK_ANY_PASS) == + PSC_STATE_FLAGS_TODO_TO_PASS(state->flags & PSC_STATE_MASK_ANY_TODO)) + msg_info("PASS %s [%s]:%s", (state->flags & PSC_STATE_FLAG_NEW) == 0 + || state->client_info->pass_new_count++ > 0 ? + "OLD" : "NEW", PSC_CLIENT_ADDR_PORT(state)); + + /* + * Update the postscreen cache. This still supports a scenario where a + * client gets allowlisted in the course of multiple sessions, as long as + * that client does not "fail" any test. Don't try to optimize away cache + * updates; we want cached information to be up-to-date even if a test + * result is renewed during overlapping SMTP sessions, and even if + * 'postfix reload' happens in the middle of that. + */ + if ((state->flags & PSC_STATE_MASK_ANY_UPDATE) != 0 + && psc_cache_map != 0) { + psc_print_tests(psc_temp, state); + psc_cache_update(psc_cache_map, state->smtp_client_addr, STR(psc_temp)); + } + + /* + * Either hand off the socket to a real SMTP engine, or say bye-bye. + */ + if ((state->flags & PSC_STATE_FLAG_NOFORWARD) == 0) { + psc_send_socket(state); + } else { + if ((state->flags & PSC_STATE_FLAG_HANGUP) == 0) + (void) PSC_SEND_REPLY(state, state->final_reply); + msg_info("DISCONNECT [%s]:%s", PSC_CLIENT_ADDR_PORT(state)); + psc_free_session_state(state); + } +} + +/* psc_hangup_event - handle unexpected disconnect */ + +void psc_hangup_event(PSC_STATE *state) +{ + DELTA_TIME elapsed; + + /* + * Sessions can break at any time, even after the client passes all tests + * (some MTAs including Postfix don't send QUIT when connection reuse is + * enabled). This must not be treated as a protocol test failure. + * + * Log the current test phase, and the elapsed time after the start of that + * phase. + */ + state->flags |= PSC_STATE_FLAG_HANGUP; + msg_info("HANGUP after %s from [%s]:%s in %s", + psc_format_delta_time(psc_temp, state->start_time, &elapsed), + PSC_CLIENT_ADDR_PORT(state), state->test_name); + state->flags |= PSC_STATE_FLAG_NOFORWARD; + psc_conclude(state); +} diff --git a/src/postscreen/postscreen_send.c b/src/postscreen/postscreen_send.c new file mode 100644 index 0000000..53714b1 --- /dev/null +++ b/src/postscreen/postscreen_send.c @@ -0,0 +1,293 @@ +/*++ +/* NAME +/* postscreen_send 3 +/* SUMMARY +/* postscreen low-level output +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* void pcs_send_pre_jail_init(void) +/* +/* int psc_send_reply(state, text) +/* PSC_STATE *state; +/* const char *text; +/* +/* int PSC_SEND_REPLY(state, text) +/* PSC_STATE *state; +/* const char *text; +/* +/* void psc_send_socket(state) +/* PSC_STATE *state; +/* DESCRIPTION +/* pcs_send_pre_jail_init() performs one-time initialization. +/* +/* psc_send_reply() sends the specified text to the specified +/* remote SMTP client. In case of an immediate error, it logs +/* a warning (except EPIPE) with the client address and port, +/* and returns a non-zero result (all errors including EPIPE). +/* +/* psc_send_reply() does a best effort to send the reply, but +/* it won't block when the output is throttled by a hostile +/* peer. +/* +/* PSC_SEND_REPLY() is a legacy wrapper for psc_send_reply(). +/* It will eventually be replaced by its expansion. +/* +/* psc_send_socket() sends the specified socket to the real +/* Postfix SMTP server. The socket is delivered in the background. +/* This function must be called after all other session-related +/* work is finished including postscreen cache updates. +/* +/* In case of an immediate error, psc_send_socket() sends a 421 +/* reply to the remote SMTP client and closes the connection. +/* If the 220- greeting was sent, sending 421 would be invalid; +/* instead, the client is redirected to the dummy SMTP engine +/* which sends the 421 reply at the first legitimate opportunity. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <string.h> +#include <errno.h> + +/* Utility library. */ + +#include <msg.h> +#include <iostuff.h> +#include <connect.h> +#include <attr.h> +#include <vstream.h> + +/* Global library. */ + +#include <mail_params.h> +#include <smtp_reply_footer.h> +#include <mail_proto.h> +#include <maps.h> + +/* Application-specific. */ + +#include <postscreen.h> + +static MAPS *psc_rej_ftr_maps; + + /* + * This program screens all inbound SMTP connections, so it better not waste + * time. + */ +#define PSC_SEND_SOCK_CONNECT_TIMEOUT 1 +#define PSC_SEND_SOCK_NOTIFY_TIMEOUT 100 + +/* pcs_send_pre_jail_init - initialize */ + +void pcs_send_pre_jail_init(void) +{ + static int init_count = 0; + + if (init_count++ != 0) + msg_panic("pcs_send_pre_jail_init: multiple calls"); + + /* + * SMTP server reject footer. + */ + if (*var_psc_rej_ftr_maps) + psc_rej_ftr_maps = maps_create(VAR_SMTPD_REJ_FTR_MAPS, + var_psc_rej_ftr_maps, + DICT_FLAG_LOCK); +} + +/* psc_get_footer - find that footer */ + +static const char *psc_get_footer(const char *text, ssize_t text_len) +{ + static VSTRING *footer_buf = 0; + + if (footer_buf == 0) + footer_buf = vstring_alloc(100); + /* Strip the \r\n for consistency with smtpd. */ + vstring_strncpy(footer_buf, text, text_len); + return (psc_maps_find(psc_rej_ftr_maps, STR(footer_buf), 0)); +} + +/* psc_send_reply - send reply to remote SMTP client */ + +int psc_send_reply(PSC_STATE *state, const char *text) +{ + ssize_t start; + int ret; + const char *footer; + ssize_t text_len = strlen(text) - 2; + + if (msg_verbose) + msg_info("> [%s]:%s: %.*s", state->smtp_client_addr, + state->smtp_client_port, (int) text_len, text); + + /* + * Append the new text to earlier text that could not be sent because the + * output was throttled. + */ + start = VSTRING_LEN(state->send_buf); + vstring_strcat(state->send_buf, text); + + /* + * For soft_bounce support, we also fix the REJECT logging before the + * dummy SMTP engine calls the psc_send_reply() output routine. We do + * some double work, but it is for debugging only. + */ + if (var_soft_bounce) { + if (text[0] == '5') + STR(state->send_buf)[start + 0] = '4'; + if (text[4] == '5') + STR(state->send_buf)[start + 4] = '4'; + } + + /* + * Append the optional reply footer. + */ + if ((*text == '4' || *text == '5') + && ((psc_rej_ftr_maps != 0 + && (footer = psc_get_footer(text, text_len)) != 0) + || *(footer = var_psc_rej_footer) != 0)) + smtp_reply_footer(state->send_buf, start, footer, + STR(psc_expand_filter), psc_expand_lookup, + (void *) state); + + /* + * Do a best effort sending text, but don't block when the output is + * throttled by a hostile peer. + */ + ret = write(vstream_fileno(state->smtp_client_stream), + STR(state->send_buf), LEN(state->send_buf)); + if (ret > 0) + vstring_truncate(state->send_buf, ret - LEN(state->send_buf)); + if (ret < 0 && errno != EAGAIN && errno != EPIPE && errno != ECONNRESET) + msg_warn("write [%s]:%s: %m", state->smtp_client_addr, + state->smtp_client_port); + return (ret < 0 && errno != EAGAIN); +} + +/* psc_send_socket_close_event - file descriptor has arrived or timeout */ + +static void psc_send_socket_close_event(int event, void *context) +{ + const char *myname = "psc_send_socket_close_event"; + PSC_STATE *state = (PSC_STATE *) context; + + if (msg_verbose > 1) + msg_info("%s: sq=%d cq=%d event %d on send socket %d from [%s]:%s", + myname, psc_post_queue_length, psc_check_queue_length, + event, state->smtp_server_fd, state->smtp_client_addr, + state->smtp_client_port); + + /* + * The real SMTP server has closed the local IPC channel, or we have + * reached the limit of our patience. In the latter case it is still + * possible that the real SMTP server will receive the socket so we + * should not interfere. + */ + PSC_CLEAR_EVENT_REQUEST(state->smtp_server_fd, psc_send_socket_close_event, + context); + if (event == EVENT_TIME) + msg_warn("timeout sending connection to service %s", + psc_smtpd_service_name); + psc_free_session_state(state); +} + +/* psc_send_socket - send socket to real SMTP server process */ + +void psc_send_socket(PSC_STATE *state) +{ + const char *myname = "psc_send_socket"; + int server_fd; + int pass_err; + VSTREAM *fp; + + if (msg_verbose > 1) + msg_info("%s: sq=%d cq=%d send socket %d from [%s]:%s", + myname, psc_post_queue_length, psc_check_queue_length, + vstream_fileno(state->smtp_client_stream), + state->smtp_client_addr, state->smtp_client_port); + + /* + * Connect to the real SMTP service over a local IPC channel, send the + * file descriptor, and close the file descriptor to save resources. + * Experience has shown that some systems will discard information when + * we close a channel immediately after writing. Thus, we waste resources + * waiting for the remote side to close the local IPC channel first. The + * good side of waiting is that we learn when the real SMTP server is + * falling behind. + * + * This is where we would forward the connection to an SMTP server that + * provides an appropriate level of service for this client class. For + * example, a server that is more forgiving, or one that is more + * suspicious. Alternatively, we could send attributes along with the + * socket with client reputation information, making everything even more + * Postfix-specific. + */ + if ((server_fd = + LOCAL_CONNECT(psc_smtpd_service_name, NON_BLOCKING, + PSC_SEND_SOCK_CONNECT_TIMEOUT)) < 0) { + msg_warn("cannot connect to service %s: %m", psc_smtpd_service_name); + if (state->flags & PSC_STATE_FLAG_PREGR_TODO) { + PSC_SMTPD_X21(state, "421 4.3.2 No system resources\r\n"); + } else { + PSC_SEND_REPLY(state, "421 4.3.2 All server ports are busy\r\n"); + psc_free_session_state(state); + } + return; + } + /* XXX Note: no dummy read between LOCAL_SEND_FD() and attr_print(). */ + fp = vstream_fdopen(server_fd, O_RDWR); + pass_err = + (LOCAL_SEND_FD(server_fd, + vstream_fileno(state->smtp_client_stream)) < 0 + || (attr_print(fp, ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, state->smtp_client_addr), + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_PORT, state->smtp_client_port), + SEND_ATTR_STR(MAIL_ATTR_ACT_SERVER_ADDR, state->smtp_server_addr), + SEND_ATTR_STR(MAIL_ATTR_ACT_SERVER_PORT, state->smtp_server_port), + ATTR_TYPE_END) || vstream_fflush(fp))); + /* XXX Note: no read between attr_print() and vstream_fdclose(). */ + (void) vstream_fdclose(fp); + if (pass_err != 0) { + msg_warn("cannot pass connection to service %s: %m", + psc_smtpd_service_name); + (void) close(server_fd); + if (state->flags & PSC_STATE_FLAG_PREGR_TODO) { + PSC_SMTPD_X21(state, "421 4.3.2 No system resources\r\n"); + } else { + PSC_SEND_REPLY(state, "421 4.3.2 No system resources\r\n"); + psc_free_session_state(state); + } + return; + } else { + + /* + * Closing the smtp_client_fd here triggers a FreeBSD 7.1 kernel bug + * where smtp-source sometimes sees the connection being closed after + * it has already received the real SMTP server's 220 greeting! + */ +#if 0 + PSC_DEL_CLIENT_STATE(state); +#endif + PSC_ADD_SERVER_STATE(state, server_fd); + PSC_READ_EVENT_REQUEST(state->smtp_server_fd, psc_send_socket_close_event, + (void *) state, PSC_SEND_SOCK_NOTIFY_TIMEOUT); + return; + } +} diff --git a/src/postscreen/postscreen_smtpd.c b/src/postscreen/postscreen_smtpd.c new file mode 100644 index 0000000..edd5d71 --- /dev/null +++ b/src/postscreen/postscreen_smtpd.c @@ -0,0 +1,1339 @@ +/*++ +/* NAME +/* postscreen_smtpd 3 +/* SUMMARY +/* postscreen built-in SMTP server engine +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* void psc_smtpd_pre_jail_init(void) +/* +/* void psc_smtpd_init(void) +/* +/* void psc_smtpd_tests(state) +/* PSC_STATE *state; +/* +/* void PSC_SMTPD_X21(state, final_reply) +/* PSC_STATE *state; +/* const char *final_reply; +/* DESCRIPTION +/* psc_smtpd_pre_jail_init() performs one-time per-process +/* initialization during the "before chroot" execution phase. +/* +/* psc_smtpd_init() performs one-time per-process initialization. +/* +/* psc_smtpd_tests() starts up an SMTP server engine for deep +/* protocol tests and for collecting helo/sender/recipient +/* information. +/* +/* PSC_SMTPD_X21() redirects the SMTP client to an SMTP server +/* engine, which sends the specified final reply at the first +/* legitimate opportunity without doing any protocol tests. +/* +/* Unlike the Postfix SMTP server, this engine does not announce +/* PIPELINING support. This exposes spambots that pipeline +/* their commands anyway. Like the Postfix SMTP server, this +/* engine will accept input with bare newline characters. To +/* pass the "pipelining" and "bare newline" test, the client +/* has to properly speak SMTP all the way to the RCPT TO +/* command. These tests fail if the client violates the protocol +/* at any stage. +/* +/* No support is announced for AUTH, XCLIENT or XFORWARD. +/* Clients that need this should be allowlisted or should talk +/* directly to the submission service. +/* +/* The engine rejects RCPT TO and VRFY commands with the +/* state->rcpt_reply response which depends on program history, +/* rejects ETRN with a generic response, and closes the +/* connection after QUIT. +/* +/* Since this engine defers or rejects all non-junk commands, +/* there is no point maintaining separate counters for "error" +/* commands and "junk" commands. Instead, the engine maintains +/* a per-session command counter, and terminates the session +/* with a 421 reply when the command count exceeds the limit. +/* +/* We limit the command count, as well as the total time to +/* receive a command. This limits the time per client more +/* effectively than would be possible with read() timeouts. +/* +/* There is no concern about getting blocked on output. The +/* psc_send() routine uses non-blocking output, and discards +/* output that the client is not willing to receive. +/* PROTOCOL INSPECTION VERSUS CONTENT INSPECTION +/* The goal of postscreen is to keep spambots away from Postfix. +/* To recognize spambots, postscreen measures properties of +/* the client IP address and of the client SMTP protocol +/* implementation. These client properties don't change with +/* each delivery attempt. Therefore it is possible to make a +/* long-term decision after a single measurement. For example, +/* allow a good client to skip the DNSBL test for 24 hours, +/* or to skip the pipelining test for one week. +/* +/* If postscreen were to measure properties of message content +/* (MIME compliance, etc.) then it would measure properties +/* that may change with each delivery attempt. Here, it would +/* be wrong to make a long-term decision after a single +/* measurement. Instead, postscreen would need to develop a +/* ranking based on the content of multiple messages from the +/* same client. +/* +/* Many spambots avoid spamming the same site repeatedly. +/* Thus, postscreen must make decisions after a single +/* measurement. Message content is not a good indicator for +/* making long-term decisions after single measurements, and +/* that is why postscreen does not inspect message content. +/* REJECTING RCPT TO VERSUS SENDING LIVE SOCKETS TO SMTPD(8) +/* When post-handshake protocol tests are enabled, postscreen +/* rejects the RCPT TO command from a good client, and forces +/* it to deliver mail in a later session. This is why +/* post-handshake protocol tests have a longer expiration time +/* than pre-handshake tests. +/* +/* Instead, postscreen could send the network socket to smtpd(8) +/* and ship the session history (including TLS and other SMTP +/* or non-SMTP attributes) as auxiliary data. The Postfix SMTP +/* server would then use new code to replay the session history, +/* and would use existing code to validate the client, helo, +/* sender and recipient address. +/* +/* Such an approach would increase the implementation and +/* maintenance effort, because: +/* +/* 1) New replay code would be needed in smtpd(8), such that +/* the HELO, EHLO, and MAIL command handlers can delay their +/* error responses until the RCPT TO reply. +/* +/* 2) postscreen(8) would have to implement more of smtpd(8)'s +/* syntax checks, to avoid confusing delayed "syntax error" +/* and other error responses syntax error responses while +/* replaying history. +/* +/* 3) New code would be needed in postscreen(8) and smtpd(8) +/* to send and receive the session history (including TLS and +/* other SMTP or non-SMTP attributes) as auxiliary data while +/* sending the network socket from postscreen(8) to smtpd(8). +/* REJECTING RCPT TO VERSUS PROXYING LIVE SESSIONS TO SMTPD(8) +/* An alternative would be to proxy the session history to a +/* real Postfix SMTP process, presumably passing TLS and other +/* attributes via an extended XCLIENT implementation. That +/* would require all the work described in 2) above, plus +/* duplication of all the features of the smtpd(8) TLS engine, +/* plus additional XCLIENT support for a lot more attributes. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <string.h> +#include <ctype.h> + +#ifdef STRCASECMP_IN_STRINGS_H +#include <strings.h> +#endif + +/* Utility library. */ + +#include <msg.h> +#include <stringops.h> +#include <mymalloc.h> +#include <iostuff.h> +#include <vstring.h> + +/* Global library. */ + +#include <mail_params.h> +#include <mail_proto.h> +#include <is_header.h> +#include <string_list.h> +#include <maps.h> +#include <ehlo_mask.h> +#include <lex_822.h> +#include <info_log_addr_form.h> + +/* TLS library. */ + +#include <tls.h> + +/* Application-specific. */ + +#include <postscreen.h> + + /* + * Plan for future body processing. See smtp-sink.c. For now, we have no + * per-session push-back except for the single-character push-back that + * VSTREAM guarantees after we read one character. + */ +#define PSC_SMTPD_HAVE_PUSH_BACK(state) (0) +#define PSC_SMTPD_PUSH_BACK_CHAR(state, ch) \ + vstream_ungetc((state)->smtp_client_stream, (ch)) +#define PSC_SMTPD_NEXT_CHAR(state) \ + VSTREAM_GETC((state)->smtp_client_stream) + +#define PSC_SMTPD_BUFFER_EMPTY(state) \ + (!PSC_SMTPD_HAVE_PUSH_BACK(state) \ + && vstream_peek((state)->smtp_client_stream) <= 0) + +#define PSC_SMTPD_PEEK_DATA(state) \ + vstream_peek_data((state)->smtp_client_stream) +#define PSC_SMTPD_PEEK_LEN(state) \ + vstream_peek((state)->smtp_client_stream) + + /* + * Dynamic reply strings. To minimize overhead we format these once. + */ +static char *psc_smtpd_greeting; /* smtp banner */ +static char *psc_smtpd_helo_reply; /* helo reply */ +static char *psc_smtpd_ehlo_reply_plain;/* multi-line ehlo reply, non-TLS */ +static char *psc_smtpd_ehlo_reply_tls; /* multi-line ehlo reply, with TLS */ +static char *psc_smtpd_timeout_reply; /* timeout reply */ +static char *psc_smtpd_421_reply; /* generic final_reply value */ + + /* + * Forward declaration, needed by PSC_CLEAR_EVENT_REQUEST. + */ +static void psc_smtpd_time_event(int, void *); +static void psc_smtpd_read_event(int, void *); + + /* + * Encapsulation. The STARTTLS, EHLO and AUTH command handlers temporarily + * suspend SMTP command events, send an asynchronous proxy request, and + * resume SMTP command events after receiving the asynchronous proxy + * response (the EHLO handler must asynchronously talk to the auth server + * before it can announce the SASL mechanism list; the list can depend on + * the client IP address and on the presence on TLS encryption). + */ +#define PSC_RESUME_SMTP_CMD_EVENTS(state) do { \ + PSC_READ_EVENT_REQUEST2(vstream_fileno((state)->smtp_client_stream), \ + psc_smtpd_read_event, psc_smtpd_time_event, \ + (void *) (state), PSC_EFF_CMD_TIME_LIMIT); \ + if (!PSC_SMTPD_BUFFER_EMPTY(state)) \ + psc_smtpd_read_event(EVENT_READ, (void *) state); \ + } while (0) + +#define PSC_SUSPEND_SMTP_CMD_EVENTS(state) \ + PSC_CLEAR_EVENT_REQUEST(vstream_fileno((state)->smtp_client_stream), \ + psc_smtpd_time_event, (void *) (state)); + + /* + * Make control characters and other non-text visible. + */ +#define PSC_SMTPD_ESCAPE_TEXT(dest, src, src_len, max_len) do { \ + ssize_t _s_len = (src_len); \ + ssize_t _m_len = (max_len); \ + (void) escape((dest), (src), _s_len < _m_len ? _s_len : _m_len); \ + } while (0) + + /* + * Command parser support. + */ +#define PSC_SMTPD_NEXT_TOKEN(ptr) mystrtok(&(ptr), " ") + + /* + * EHLO keyword filter + */ +static MAPS *psc_ehlo_discard_maps; +static int psc_ehlo_discard_mask; + + /* + * Command editing filter. + */ +static DICT *psc_cmd_filter; + + /* + * Encapsulation. We must not forget turn off input/timer events when we + * terminate the SMTP protocol engine. + * + * It would be safer to turn off input/timer events after each event, and to + * turn on input/timer events again when we want more input. But experience + * with the Postfix smtp-source and smtp-sink tools shows that this would + * noticeably increase the run-time cost. + */ +#define PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, event, reply) do { \ + PSC_CLEAR_EVENT_REQUEST(vstream_fileno((state)->smtp_client_stream), \ + (event), (void *) (state)); \ + PSC_DROP_SESSION_STATE((state), (reply)); \ + } while (0) + +#define PSC_CLEAR_EVENT_HANGUP(state, event) do { \ + PSC_CLEAR_EVENT_REQUEST(vstream_fileno((state)->smtp_client_stream), \ + (event), (void *) (state)); \ + psc_hangup_event(state); \ + } while (0) + +/* psc_helo_cmd - record HELO and respond */ + +static int psc_helo_cmd(PSC_STATE *state, char *args) +{ + char *helo_name = PSC_SMTPD_NEXT_TOKEN(args); + + /* + * smtpd(8) incompatibility: we ignore extra words; smtpd(8) saves them. + */ + if (helo_name == 0) + return (PSC_SEND_REPLY(state, "501 Syntax: HELO hostname\r\n")); + + PSC_STRING_UPDATE(state->helo_name, helo_name); + PSC_STRING_RESET(state->sender); + /* Don't downgrade state->protocol, in case some test depends on this. */ + return (PSC_SEND_REPLY(state, psc_smtpd_helo_reply)); +} + +/* psc_smtpd_format_ehlo_reply - format EHLO response */ + +static void psc_smtpd_format_ehlo_reply(VSTRING *buf, int discard_mask + /* , const char *sasl_mechanism_list */ ) +{ + const char *myname = "psc_smtpd_format_ehlo_reply"; + int saved_len = 0; + + if (msg_verbose) + msg_info("%s: discard_mask %s", myname, str_ehlo_mask(discard_mask)); + +#define PSC_EHLO_APPEND(save, buf, fmt) do { \ + (save) = LEN(buf); \ + vstring_sprintf_append((buf), (fmt)); \ + } while (0) + +#define PSC_EHLO_APPEND1(save, buf, fmt, arg1) do { \ + (save) = LEN(buf); \ + vstring_sprintf_append((buf), (fmt), (arg1)); \ + } while (0) + + vstring_sprintf(psc_temp, "250-%s\r\n", var_myhostname); + if ((discard_mask & EHLO_MASK_SIZE) == 0) { + if (ENFORCING_SIZE_LIMIT(var_message_limit)) + PSC_EHLO_APPEND1(saved_len, psc_temp, "250-SIZE %lu\r\n", + (unsigned long) var_message_limit); + else + PSC_EHLO_APPEND(saved_len, psc_temp, "250-SIZE\r\n"); + } + if ((discard_mask & EHLO_MASK_VRFY) == 0 && var_disable_vrfy_cmd == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-VRFY\r\n"); + if ((discard_mask & EHLO_MASK_ETRN) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-ETRN\r\n"); + if ((discard_mask & EHLO_MASK_STARTTLS) == 0 && var_psc_use_tls) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-STARTTLS\r\n"); +#ifdef TODO_SASL_AUTH + if ((discard_mask & EHLO_MASK_AUTH) == 0 && sasl_mechanism_list + && (!var_psc_tls_auth_only || (discard_mask & EHLO_MASK_STARTTLS))) { + PSC_EHLO_APPEND1(saved_len, psc_temp, "AUTH %s", sasl_mechanism_list); + if (var_broken_auth_clients) + PSC_EHLO_APPEND1(saved_len, psc_temp, "AUTH=%s", sasl_mechanism_list); + } +#endif + if ((discard_mask & EHLO_MASK_ENHANCEDSTATUSCODES) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-ENHANCEDSTATUSCODES\r\n"); + if ((discard_mask & EHLO_MASK_8BITMIME) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-8BITMIME\r\n"); + if ((discard_mask & EHLO_MASK_DSN) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-DSN\r\n"); + /* Fix 20140708: announce SMTPUTF8. */ + if (var_smtputf8_enable && (discard_mask & EHLO_MASK_SMTPUTF8) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-SMTPUTF8\r\n"); + if ((discard_mask & EHLO_MASK_CHUNKING) == 0) + PSC_EHLO_APPEND(saved_len, psc_temp, "250-CHUNKING\r\n"); + STR(psc_temp)[saved_len + 3] = ' '; +} + +/* psc_ehlo_cmd - record EHLO and respond */ + +static int psc_ehlo_cmd(PSC_STATE *state, char *args) +{ + char *helo_name = PSC_SMTPD_NEXT_TOKEN(args); + const char *ehlo_words; + int discard_mask; + char *reply; + + /* + * smtpd(8) incompatibility: we ignore extra words; smtpd(8) saves them. + */ + if (helo_name == 0) + return (PSC_SEND_REPLY(state, "501 Syntax: EHLO hostname\r\n")); + + PSC_STRING_UPDATE(state->helo_name, helo_name); + PSC_STRING_RESET(state->sender); + state->protocol = MAIL_PROTO_ESMTP; + + /* + * smtpd(8) compatibility: dynamic reply filtering. + */ + if (psc_ehlo_discard_maps != 0 + && (ehlo_words = psc_maps_find(psc_ehlo_discard_maps, + state->smtp_client_addr, 0)) != 0 + && (discard_mask = ehlo_mask(ehlo_words)) != psc_ehlo_discard_mask) { + if (discard_mask && !(discard_mask & EHLO_MASK_SILENT)) + msg_info("[%s]%s: discarding EHLO keywords: %s", + PSC_CLIENT_ADDR_PORT(state), str_ehlo_mask(discard_mask)); + if (state->flags & PSC_STATE_FLAG_USING_TLS) + discard_mask |= EHLO_MASK_STARTTLS; + psc_smtpd_format_ehlo_reply(psc_temp, discard_mask); + reply = STR(psc_temp); + state->ehlo_discard_mask = discard_mask; + } else if (psc_ehlo_discard_maps && psc_ehlo_discard_maps->error) { + msg_fatal("%s lookup error for %s", + psc_ehlo_discard_maps->title, state->smtp_client_addr); + } else if (state->flags & PSC_STATE_FLAG_USING_TLS) { + reply = psc_smtpd_ehlo_reply_tls; + state->ehlo_discard_mask = psc_ehlo_discard_mask | EHLO_MASK_STARTTLS; + } else { + reply = psc_smtpd_ehlo_reply_plain; + state->ehlo_discard_mask = psc_ehlo_discard_mask; + } + return (PSC_SEND_REPLY(state, reply)); +} + +/* psc_starttls_resume - resume the SMTP protocol after tlsproxy activation */ + +static void psc_starttls_resume(int unused_event, void *context) +{ + const char *myname = "psc_starttls_resume"; + PSC_STATE *state = (PSC_STATE *) context; + + /* + * Reset SMTP server state if STARTTLS was successful. + */ + if (state->flags & PSC_STATE_FLAG_USING_TLS) { + /* Purge the push-back buffer, when implemented. */ + PSC_STRING_RESET(state->helo_name); + PSC_STRING_RESET(state->sender); +#ifdef TODO_SASL_AUTH + /* Reset SASL AUTH state. Dovecot responses may change. */ +#endif + } + + /* + * Resume read/timeout events. If we still have unread input, resume the + * command processor immediately. + */ + PSC_RESUME_SMTP_CMD_EVENTS(state); +} + +/* psc_starttls_cmd - activate the tlsproxy server */ + +static int psc_starttls_cmd(PSC_STATE *state, char *args) +{ + const char *myname = "psc_starttls_cmd"; + + /* + * smtpd(8) incompatibility: we can't send a 4XX reply that TLS is + * unavailable when tlsproxy(8) detects the problem too late. + */ + if (PSC_SMTPD_NEXT_TOKEN(args) != 0) + return (PSC_SEND_REPLY(state, "501 Syntax: EHLO hostname\r\n")); + if (state->flags & PSC_STATE_FLAG_USING_TLS) + return (PSC_SEND_REPLY(state, + "554 5.5.1 Error: TLS already active\r\n")); + if (var_psc_use_tls == 0 || (state->ehlo_discard_mask & EHLO_MASK_STARTTLS)) + return (PSC_SEND_REPLY(state, + "502 5.5.1 Error: command not implemented\r\n")); + + /* + * Suspend the SMTP protocol until psc_starttls_resume() is called. + */ + PSC_SUSPEND_SMTP_CMD_EVENTS(state); + psc_starttls_open(state, psc_starttls_resume); + return (0); +} + +/* psc_extract_addr - extract MAIL/RCPT address, unquoted form */ + +static char *psc_extract_addr(VSTRING *result, const char *string) +{ + const unsigned char *cp = (const unsigned char *) string; + char *addr; + char *colon; + int stop_at; + int inquote = 0; + + /* + * smtpd(8) incompatibility: we allow more invalid address forms, and we + * don't validate recipients. We are not going to deliver them so we + * won't have to worry about deliverability. This may have to change when + * we pass the socket to a real SMTP server and replay message envelope + * commands. + */ + + /* Skip SP characters. */ + while (*cp && *cp == ' ') + cp++; + + /* Choose the terminator for <addr> or bare addr. */ + if (*cp == '<') { + cp++; + stop_at = '>'; + } else { + stop_at = ' '; + } + + /* Skip to terminator or end. */ + VSTRING_RESET(result); + for ( /* void */ ; *cp; cp++) { + if (!inquote && *cp == stop_at) + break; + if (*cp == '"') { + inquote = !inquote; + } else { + if (*cp == '\\' && *++cp == 0) + break; + VSTRING_ADDCH(result, *cp); + } + } + VSTRING_TERMINATE(result); + + /* + * smtpd(8) compatibility: truncate deprecated route address form. This + * is primarily to simplify logfile analysis. + */ + addr = STR(result); + if (*addr == '@' && (colon = strchr(addr, ':')) != 0) + addr = colon + 1; + return (addr); +} + +/* psc_mail_cmd - record MAIL and respond */ + +static int psc_mail_cmd(PSC_STATE *state, char *args) +{ + char *colon; + char *addr; + + /* + * smtpd(8) incompatibility: we never reject the sender, and we ignore + * additional arguments. + */ + if (var_psc_helo_required && state->helo_name == 0) + return (PSC_SEND_REPLY(state, + "503 5.5.1 Error: send HELO/EHLO first\r\n")); + if (state->sender != 0) + return (PSC_SEND_REPLY(state, + "503 5.5.1 Error: nested MAIL command\r\n")); + if (args == 0 || (colon = strchr(args, ':')) == 0) + return (PSC_SEND_REPLY(state, + "501 5.5.4 Syntax: MAIL FROM:<address>\r\n")); + if ((addr = psc_extract_addr(psc_temp, colon + 1)) == 0) + return (PSC_SEND_REPLY(state, + "501 5.1.7 Bad sender address syntax\r\n")); + PSC_STRING_UPDATE(state->sender, addr); + return (PSC_SEND_REPLY(state, "250 2.1.0 Ok\r\n")); +} + +/* psc_soften_reply - copy and soft-bounce a reply */ + +static char *psc_soften_reply(const char *reply) +{ + static VSTRING *buf = 0; + + if (buf == 0) + buf = vstring_alloc(100); + vstring_strcpy(buf, reply); + if (reply[0] == '5') + STR(buf)[0] = '4'; + if (reply[4] == '5') + STR(buf)[4] = '4'; + return (STR(buf)); +} + +/* psc_rcpt_cmd record RCPT and respond */ + +static int psc_rcpt_cmd(PSC_STATE *state, char *args) +{ + char *colon; + char *addr; + + /* + * smtpd(8) incompatibility: we reject all recipients, and ignore + * additional arguments. + */ + if (state->sender == 0) + return (PSC_SEND_REPLY(state, + "503 5.5.1 Error: need MAIL command\r\n")); + if (args == 0 || (colon = strchr(args, ':')) == 0) + return (PSC_SEND_REPLY(state, + "501 5.5.4 Syntax: RCPT TO:<address>\r\n")); + if ((addr = psc_extract_addr(psc_temp, colon + 1)) == 0) + return (PSC_SEND_REPLY(state, + "501 5.1.3 Bad recipient address syntax\r\n")); + msg_info("NOQUEUE: reject: RCPT from [%s]:%s: %.*s; " + "from=<%s>, to=<%s>, proto=%s, helo=<%s>", + PSC_CLIENT_ADDR_PORT(state), + (int) strlen(state->rcpt_reply) - 2, + var_soft_bounce == 0 ? state->rcpt_reply : + psc_soften_reply(state->rcpt_reply), + info_log_addr_form_sender(state->sender), + info_log_addr_form_recipient(addr), state->protocol, + state->helo_name ? state->helo_name : ""); + return (PSC_SEND_REPLY(state, state->rcpt_reply)); +} + +/* psc_data_cmd - respond to DATA and disconnect */ + +static int psc_data_cmd(PSC_STATE *state, char *args) +{ + const char myname[] = "psc_data_cmd"; + + /* + * smtpd(8) incompatibility: postscreen(8) drops the connection, instead + * of waiting for the next command. Justification: postscreen(8) should + * never see DATA from a legitimate client, because 1) the server rejects + * every recipient, and 2) the server does not announce PIPELINING. + */ + msg_info("DATA without valid RCPT from [%s]:%s", + PSC_CLIENT_ADDR_PORT(state)); + if (PSC_SMTPD_NEXT_TOKEN(args) != 0) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "501 5.5.4 Syntax: DATA\r\n"); + else if (state->sender == 0) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "503 5.5.1 Error: need RCPT command\r\n"); + else + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "554 5.5.1 Error: no valid recipients\r\n"); + /* Caution: state is now a dangling pointer. */ + return (0); +} + +/* psc_bdat_cmd - respond to BDAT and disconnect */ + +static int psc_bdat_cmd(PSC_STATE *state, char *args) +{ + const char *myname = "psc_bdat_cmd"; + + /* + * smtpd(8) incompatibility: postscreen(8) drops the connection, instead + * of reading the entire BDAT chunk and staying in sync with the client. + * Justification: postscreen(8) should never see BDAT from a legitimate + * client, because 1) the server rejects every recipient, and 2) the + * server does not announce PIPELINING. + */ + msg_info("BDAT without valid RCPT from [%s]:%s", + PSC_CLIENT_ADDR_PORT(state)); + if (state->ehlo_discard_mask & EHLO_MASK_CHUNKING) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "502 5.5.1 Error: command not implemented\r\n"); + else if (PSC_SMTPD_NEXT_TOKEN(args) == 0) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "501 5.5.4 Syntax: BDAT count [LAST]\r\n"); + else if (state->sender == 0) + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "554 5.5.1 Error: need MAIL command\r\n"); + else + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "554 5.5.1 Error: no valid recipients\r\n"); + /* Caution: state is now a dangling pointer. */ + return (0); +} + +/* psc_rset_cmd - reset, send 250 OK */ + +static int psc_rset_cmd(PSC_STATE *state, char *unused_args) +{ + PSC_STRING_RESET(state->sender); + return (PSC_SEND_REPLY(state, "250 2.0.0 Ok\r\n")); +} + +/* psc_noop_cmd - respond to something */ + +static int psc_noop_cmd(PSC_STATE *state, char *unused_args) +{ + return (PSC_SEND_REPLY(state, "250 2.0.0 Ok\r\n")); +} + +/* psc_vrfy_cmd - respond to VRFY */ + +static int psc_vrfy_cmd(PSC_STATE *state, char *args) +{ + + /* + * smtpd(8) incompatibility: we reject all requests, and ignore + * additional arguments. + */ + if (PSC_SMTPD_NEXT_TOKEN(args) == 0) + return (PSC_SEND_REPLY(state, + "501 5.5.4 Syntax: VRFY address\r\n")); + if (var_psc_disable_vrfy) + return (PSC_SEND_REPLY(state, + "502 5.5.1 VRFY command is disabled\r\n")); + return (PSC_SEND_REPLY(state, state->rcpt_reply)); +} + +/* psc_etrn_cmd - reset, send 250 OK */ + +static int psc_etrn_cmd(PSC_STATE *state, char *args) +{ + + /* + * smtpd(8) incompatibility: we reject all requests, and ignore + * additional arguments. + */ + if (var_psc_helo_required && state->helo_name == 0) + return (PSC_SEND_REPLY(state, + "503 5.5.1 Error: send HELO/EHLO first\r\n")); + if (PSC_SMTPD_NEXT_TOKEN(args) == 0) + return (PSC_SEND_REPLY(state, + "500 Syntax: ETRN domain\r\n")); + return (PSC_SEND_REPLY(state, "458 Unable to queue messages\r\n")); +} + +/* psc_quit_cmd - respond to QUIT and disconnect */ + +static int psc_quit_cmd(PSC_STATE *state, char *unused_args) +{ + const char *myname = "psc_quit_cmd"; + + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + "221 2.0.0 Bye\r\n"); + /* Caution: state is now a dangling pointer. */ + return (0); +} + +/* psc_smtpd_time_event - handle per-session time limit */ + +static void psc_smtpd_time_event(int event, void *context) +{ + const char *myname = "psc_smtpd_time_event"; + PSC_STATE *state = (PSC_STATE *) context; + + if (msg_verbose > 1) + msg_info("%s: sq=%d cq=%d event %d on smtp socket %d from [%s]:%s flags=%s", + myname, psc_post_queue_length, psc_check_queue_length, + event, vstream_fileno(state->smtp_client_stream), + state->smtp_client_addr, state->smtp_client_port, + psc_print_state_flags(state->flags, myname)); + + msg_info("COMMAND TIME LIMIT from [%s]:%s after %s", + PSC_CLIENT_ADDR_PORT(state), state->where); + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + psc_smtpd_timeout_reply); +} + + /* + * The table of all SMTP commands that we know. + */ +typedef struct { + const char *name; + int (*action) (PSC_STATE *, char *); + int flags; /* see below */ +} PSC_SMTPD_COMMAND; + +#define PSC_SMTPD_CMD_FLAG_NONE (0) /* no flags (i.e. disabled) */ +#define PSC_SMTPD_CMD_FLAG_ENABLE (1<<0) /* command is enabled */ +#define PSC_SMTPD_CMD_FLAG_DESTROY (1<<1) /* dangling pointer alert */ +#define PSC_SMTPD_CMD_FLAG_PRE_TLS (1<<2) /* allowed with mandatory TLS */ +#define PSC_SMTPD_CMD_FLAG_SUSPEND (1<<3) /* suspend command engine */ +#define PSC_SMTPD_CMD_FLAG_HAS_PAYLOAD (1<<4) /* command has payload */ + +static const PSC_SMTPD_COMMAND command_table[] = { + "HELO", psc_helo_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_PRE_TLS, + "EHLO", psc_ehlo_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_PRE_TLS, + "STARTTLS", psc_starttls_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_PRE_TLS | PSC_SMTPD_CMD_FLAG_SUSPEND, + "XCLIENT", psc_noop_cmd, PSC_SMTPD_CMD_FLAG_NONE, + "XFORWARD", psc_noop_cmd, PSC_SMTPD_CMD_FLAG_NONE, + "AUTH", psc_noop_cmd, PSC_SMTPD_CMD_FLAG_NONE, + "MAIL", psc_mail_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "RCPT", psc_rcpt_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "DATA", psc_data_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_DESTROY, + /* ".", psc_dot_cmd, PSC_SMTPD_CMD_FLAG_NONE, */ + "BDAT", psc_bdat_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_DESTROY | PSC_SMTPD_CMD_FLAG_HAS_PAYLOAD, + "RSET", psc_rset_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "NOOP", psc_noop_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_PRE_TLS, + "VRFY", psc_vrfy_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "ETRN", psc_etrn_cmd, PSC_SMTPD_CMD_FLAG_ENABLE, + "QUIT", psc_quit_cmd, PSC_SMTPD_CMD_FLAG_ENABLE | PSC_SMTPD_CMD_FLAG_DESTROY | PSC_SMTPD_CMD_FLAG_PRE_TLS, + 0, +}; + +/* psc_smtpd_read_event - pseudo responder */ + +static void psc_smtpd_read_event(int event, void *context) +{ + const char *myname = "psc_smtpd_read_event"; + PSC_STATE *state = (PSC_STATE *) context; + time_t *expire_time = state->client_info->expire_time; + int ch; + struct cmd_trans { + int state; + int want; + int next_state; + }; + const char *saved_where; + +#define PSC_SMTPD_CMD_ST_ANY 0 +#define PSC_SMTPD_CMD_ST_CR 1 +#define PSC_SMTPD_CMD_ST_CR_LF 2 + + static const struct cmd_trans cmd_trans[] = { + PSC_SMTPD_CMD_ST_ANY, '\r', PSC_SMTPD_CMD_ST_CR, + PSC_SMTPD_CMD_ST_CR, '\n', PSC_SMTPD_CMD_ST_CR_LF, + 0, 0, 0, + }; + const struct cmd_trans *transp; + char *cmd_buffer_ptr; + char *command; + const PSC_SMTPD_COMMAND *cmdp; + int write_stat; + + if (msg_verbose > 1) + msg_info("%s: sq=%d cq=%d event %d on smtp socket %d from [%s]:%s flags=%s", + myname, psc_post_queue_length, psc_check_queue_length, + event, vstream_fileno(state->smtp_client_stream), + state->smtp_client_addr, state->smtp_client_port, + psc_print_state_flags(state->flags, myname)); + + /* + * Basic liveness requirements. + * + * Drain all input in the VSTREAM buffer, otherwise this socket will not + * receive further read event notification until the client disconnects! + * + * To suspend this loop temporarily before the buffer is drained, use the + * PSC_SUSPEND_SMTP_CMD_EVENTS() and PSC_RESUME_SMTP_CMD_EVENTS() macros, + * and set the PSC_SMTPD_CMD_FLAG_SUSPEND flag in the command table. + * + * Don't try to read input before it has arrived, otherwise we would starve + * the pseudo threads of other sessions. Get out of here as soon as the + * VSTREAM read buffer dries up. Do not look for more input in kernel + * buffers. That input wasn't likely there when psc_smtpd_read_event() + * was called. Also, yielding the pseudo thread will improve fairness for + * other pseudo threads. + */ + + /* + * Note: on entry into this function the VSTREAM buffer may or may not be + * empty, so we test the "no more input" condition at the bottom of the + * loops. + */ + for (;;) { + + /* + * Read one command line, possibly one fragment at a time. + */ + for (;;) { + + if ((ch = PSC_SMTPD_NEXT_CHAR(state)) == VSTREAM_EOF) { + PSC_CLEAR_EVENT_HANGUP(state, psc_smtpd_time_event); + return; + } + + /* + * Sanity check. We don't want to store infinitely long commands. + */ + if (state->read_state == PSC_SMTPD_CMD_ST_ANY + && VSTRING_LEN(state->cmd_buffer) >= var_line_limit) { + msg_info("COMMAND LENGTH LIMIT from [%s]:%s after %s", + PSC_CLIENT_ADDR_PORT(state), state->where); + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + psc_smtpd_421_reply); + return; + } + VSTRING_ADDCH(state->cmd_buffer, ch); + + /* + * Try to match the current character desired by the state + * machine. If that fails, try to restart the machine with a + * match for its first state. Like smtpd(8), we understand lines + * ending in <CR><LF> and bare <LF>. Unlike smtpd(8), we may + * treat lines ending in bare <LF> as an offense. + */ + for (transp = cmd_trans; transp->state != state->read_state; transp++) + if (transp->want == 0) + msg_panic("%s: command_read: unknown state: %d", + myname, state->read_state); + if (ch == transp->want) + state->read_state = transp->next_state; + else if (ch == cmd_trans[0].want) + state->read_state = cmd_trans[0].next_state; + else + state->read_state = PSC_SMTPD_CMD_ST_ANY; + if (state->read_state == PSC_SMTPD_CMD_ST_CR_LF) { + vstring_truncate(state->cmd_buffer, + VSTRING_LEN(state->cmd_buffer) - 2); + break; + } + + /* + * Bare newline test. + */ + if (ch == '\n') { + if ((state->flags & PSC_STATE_MASK_BARLF_TODO_SKIP) + == PSC_STATE_FLAG_BARLF_TODO) { + PSC_SMTPD_ESCAPE_TEXT(psc_temp, STR(state->cmd_buffer), + VSTRING_LEN(state->cmd_buffer) - 1, 100); + msg_info("BARE NEWLINE from [%s]:%s after %s", + PSC_CLIENT_ADDR_PORT(state), STR(psc_temp)); + PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_BARLF_FAIL); + PSC_UNPASS_SESSION_STATE(state, PSC_STATE_FLAG_BARLF_PASS); + expire_time[PSC_TINDX_BARLF] = PSC_TIME_STAMP_DISABLED; /* XXX */ + /* Skip this test for the remainder of this session. */ + PSC_SKIP_SESSION_STATE(state, "bare newline test", + PSC_STATE_FLAG_BARLF_SKIP); + switch (psc_barlf_action) { + case PSC_ACT_DROP: + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "521 5.5.1 Protocol error\r\n"); + return; + case PSC_ACT_ENFORCE: + PSC_ENFORCE_SESSION_STATE(state, + "550 5.5.1 Protocol error\r\n"); + break; + case PSC_ACT_IGNORE: + PSC_UNFAIL_SESSION_STATE(state, + PSC_STATE_FLAG_BARLF_FAIL); + /* Temporarily allowlist until something expires. */ + PSC_PASS_SESSION_STATE(state, "bare newline test", + PSC_STATE_FLAG_BARLF_PASS); + expire_time[PSC_TINDX_BARLF] = event_time() + psc_min_ttl; + break; + default: + msg_panic("%s: unknown bare_newline action value %d", + myname, psc_barlf_action); + } + } + vstring_truncate(state->cmd_buffer, + VSTRING_LEN(state->cmd_buffer) - 1); + break; + } + + /* + * Yield this pseudo thread when the VSTREAM buffer is empty in + * the middle of a command. + * + * XXX Do not reset the read timeout. The entire command must be + * received within the time limit. + */ + if (PSC_SMTPD_BUFFER_EMPTY(state)) + return; + } + + /* + * Avoid complaints from Postfix maps about malformed content. + */ +#define PSC_BAD_UTF8(str, len) \ + (var_smtputf8_enable && !valid_utf8_string((str), (len))) + + /* + * Terminate the command buffer, and apply the last-resort command + * editing workaround. + */ + VSTRING_TERMINATE(state->cmd_buffer); + if (psc_cmd_filter != 0 && !PSC_BAD_UTF8(STR(state->cmd_buffer), + LEN(state->cmd_buffer))) { + const char *cp; + + for (cp = STR(state->cmd_buffer); *cp && IS_SPACE_TAB(*cp); cp++) + /* void */ ; + if ((cp = psc_dict_get(psc_cmd_filter, cp)) != 0) { + msg_info("[%s]:%s: replacing command \"%.100s\" with \"%.100s\"", + state->smtp_client_addr, state->smtp_client_port, + STR(state->cmd_buffer), cp); + vstring_strcpy(state->cmd_buffer, cp); + } else if (psc_cmd_filter->error != 0) { + msg_fatal("%s:%s lookup error for \"%.100s\"", + psc_cmd_filter->type, psc_cmd_filter->name, + STR(state->cmd_buffer)); + } + } + + /* + * Reset the command buffer write pointer and state machine in + * preparation for the next command. For this to work as expected, + * VSTRING_RESET() must be non-destructive. We just can't ask for the + * VSTRING_LEN() and vstring_end() results. + */ + state->read_state = PSC_SMTPD_CMD_ST_ANY; + VSTRING_RESET(state->cmd_buffer); + + /* + * Process the command line. + * + * Caution: some command handlers terminate the session and destroy the + * session state structure. When this happens we must leave the SMTP + * engine to avoid a dangling pointer problem. + */ + cmd_buffer_ptr = STR(state->cmd_buffer); + if (msg_verbose) + msg_info("< [%s]:%s: %s", state->smtp_client_addr, + state->smtp_client_port, cmd_buffer_ptr); + + /* Parse the command name. */ + if ((command = PSC_SMTPD_NEXT_TOKEN(cmd_buffer_ptr)) == 0) + command = ""; + + /* + * The non-SMTP, PIPELINING and command COUNT tests depend on the + * client command handler. + * + * Caution: cmdp->name and cmdp->action may be null on loop exit. + */ + saved_where = state->where; + state->where = PSC_SMTPD_CMD_UNIMPL; + for (cmdp = command_table; cmdp->name != 0; cmdp++) { + if (strcasecmp(command, cmdp->name) == 0) { + state->where = cmdp->name; + break; + } + } + + if ((state->flags & PSC_STATE_FLAG_SMTPD_X21) + && cmdp->action != psc_quit_cmd) { + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + state->final_reply); + return; + } + /* Non-SMTP command test. */ + if ((state->flags & PSC_STATE_MASK_NSMTP_TODO_SKIP) + == PSC_STATE_FLAG_NSMTP_TODO && cmdp->name == 0 + && (is_header(command) + || PSC_BAD_UTF8(command, strlen(command)) + /* Ignore forbid_cmds lookup errors. Non-critical feature. */ + || (*var_psc_forbid_cmds + && string_list_match(psc_forbid_cmds, command)))) { + printable(command, '?'); + PSC_SMTPD_ESCAPE_TEXT(psc_temp, cmd_buffer_ptr, + strlen(cmd_buffer_ptr), 100); + msg_info("NON-SMTP COMMAND from [%s]:%s after %s: %.100s %s", + PSC_CLIENT_ADDR_PORT(state), saved_where, + command, STR(psc_temp)); + PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_NSMTP_FAIL); + PSC_UNPASS_SESSION_STATE(state, PSC_STATE_FLAG_NSMTP_PASS); + expire_time[PSC_TINDX_NSMTP] = PSC_TIME_STAMP_DISABLED; /* XXX */ + /* Skip this test for the remainder of this SMTP session. */ + PSC_SKIP_SESSION_STATE(state, "non-smtp test", + PSC_STATE_FLAG_NSMTP_SKIP); + switch (psc_nsmtp_action) { + case PSC_ACT_DROP: + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "521 5.7.0 Error: I can break rules, too. Goodbye.\r\n"); + return; + case PSC_ACT_ENFORCE: + PSC_ENFORCE_SESSION_STATE(state, + "550 5.5.1 Protocol error\r\n"); + break; + case PSC_ACT_IGNORE: + PSC_UNFAIL_SESSION_STATE(state, + PSC_STATE_FLAG_NSMTP_FAIL); + /* Temporarily allowlist until something else expires. */ + PSC_PASS_SESSION_STATE(state, "non-smtp test", + PSC_STATE_FLAG_NSMTP_PASS); + expire_time[PSC_TINDX_NSMTP] = event_time() + psc_min_ttl; + break; + default: + msg_panic("%s: unknown non_smtp_command action value %d", + myname, psc_nsmtp_action); + } + } + /* Command PIPELINING test. */ + if ((cmdp->flags & PSC_SMTPD_CMD_FLAG_HAS_PAYLOAD) == 0 + && (state->flags & PSC_STATE_MASK_PIPEL_TODO_SKIP) + == PSC_STATE_FLAG_PIPEL_TODO && !PSC_SMTPD_BUFFER_EMPTY(state)) { + printable(command, '?'); + PSC_SMTPD_ESCAPE_TEXT(psc_temp, PSC_SMTPD_PEEK_DATA(state), + PSC_SMTPD_PEEK_LEN(state), 100); + msg_info("COMMAND PIPELINING from [%s]:%s after %.100s: %s", + PSC_CLIENT_ADDR_PORT(state), command, STR(psc_temp)); + PSC_FAIL_SESSION_STATE(state, PSC_STATE_FLAG_PIPEL_FAIL); + PSC_UNPASS_SESSION_STATE(state, PSC_STATE_FLAG_PIPEL_PASS); + expire_time[PSC_TINDX_PIPEL] = PSC_TIME_STAMP_DISABLED; /* XXX */ + /* Skip this test for the remainder of this SMTP session. */ + PSC_SKIP_SESSION_STATE(state, "pipelining test", + PSC_STATE_FLAG_PIPEL_SKIP); + switch (psc_pipel_action) { + case PSC_ACT_DROP: + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, + psc_smtpd_time_event, + "521 5.5.1 Protocol error\r\n"); + return; + case PSC_ACT_ENFORCE: + PSC_ENFORCE_SESSION_STATE(state, + "550 5.5.1 Protocol error\r\n"); + break; + case PSC_ACT_IGNORE: + PSC_UNFAIL_SESSION_STATE(state, + PSC_STATE_FLAG_PIPEL_FAIL); + /* Temporarily allowlist until something else expires. */ + PSC_PASS_SESSION_STATE(state, "pipelining test", + PSC_STATE_FLAG_PIPEL_PASS); + expire_time[PSC_TINDX_PIPEL] = event_time() + psc_min_ttl; + break; + default: + msg_panic("%s: unknown pipelining action value %d", + myname, psc_pipel_action); + } + } + + /* + * The following tests don't pass until the client gets all the way + * to the RCPT TO command. However, the client can still fail these + * tests with some later command. + */ + if (cmdp->action == psc_rcpt_cmd) { + if ((state->flags & PSC_STATE_MASK_BARLF_TODO_PASS_FAIL) + == PSC_STATE_FLAG_BARLF_TODO) { + PSC_PASS_SESSION_STATE(state, "bare newline test", + PSC_STATE_FLAG_BARLF_PASS); + /* XXX Reset to PSC_TIME_STAMP_DISABLED on failure. */ + expire_time[PSC_TINDX_BARLF] = event_time() + var_psc_barlf_ttl; + } + if ((state->flags & PSC_STATE_MASK_NSMTP_TODO_PASS_FAIL) + == PSC_STATE_FLAG_NSMTP_TODO) { + PSC_PASS_SESSION_STATE(state, "non-smtp test", + PSC_STATE_FLAG_NSMTP_PASS); + /* XXX Reset to PSC_TIME_STAMP_DISABLED on failure. */ + expire_time[PSC_TINDX_NSMTP] = event_time() + var_psc_nsmtp_ttl; + } + if ((state->flags & PSC_STATE_MASK_PIPEL_TODO_PASS_FAIL) + == PSC_STATE_FLAG_PIPEL_TODO) { + PSC_PASS_SESSION_STATE(state, "pipelining test", + PSC_STATE_FLAG_PIPEL_PASS); + /* XXX Reset to PSC_TIME_STAMP_DISABLED on failure. */ + expire_time[PSC_TINDX_PIPEL] = event_time() + var_psc_pipel_ttl; + } + } + /* Command COUNT limit test. */ + if (++state->command_count > var_psc_cmd_count + && cmdp->action != psc_quit_cmd) { + msg_info("COMMAND COUNT LIMIT from [%s]:%s after %s", + PSC_CLIENT_ADDR_PORT(state), saved_where); + PSC_CLEAR_EVENT_DROP_SESSION_STATE(state, psc_smtpd_time_event, + psc_smtpd_421_reply); + return; + } + /* Finally, execute the command. */ + if (cmdp->name == 0 || (cmdp->flags & PSC_SMTPD_CMD_FLAG_ENABLE) == 0) { + write_stat = PSC_SEND_REPLY(state, + "502 5.5.2 Error: command not recognized\r\n"); + } else if (var_psc_enforce_tls + && (state->flags & PSC_STATE_FLAG_USING_TLS) == 0 + && (cmdp->flags & PSC_SMTPD_CMD_FLAG_PRE_TLS) == 0) { + write_stat = PSC_SEND_REPLY(state, + "530 5.7.0 Must issue a STARTTLS command first\r\n"); + } else { + write_stat = cmdp->action(state, cmd_buffer_ptr); + if (cmdp->flags & PSC_SMTPD_CMD_FLAG_DESTROY) + return; + } + + /* + * Terminate the session after a write error. + */ + if (write_stat < 0) { + PSC_CLEAR_EVENT_HANGUP(state, psc_smtpd_time_event); + return; + } + + /* + * We're suspended, waiting for some external event to happen. + * Hopefully, someone will call us back to process the remainder of + * the pending input, otherwise we could hang. + */ + if (cmdp->flags & PSC_SMTPD_CMD_FLAG_SUSPEND) + return; + + /* + * Reset the command read timeout before reading the next command. + */ + event_request_timer(psc_smtpd_time_event, (void *) state, + PSC_EFF_CMD_TIME_LIMIT); + + /* + * Yield this pseudo thread when the VSTREAM buffer is empty. + */ + if (PSC_SMTPD_BUFFER_EMPTY(state)) + return; + } +} + +/* psc_smtpd_tests - per-session deep protocol test initialization */ + +void psc_smtpd_tests(PSC_STATE *state) +{ + static char *myname = "psc_smtpd_tests"; + + /* + * Report errors and progress in the context of this test. + */ + PSC_BEGIN_TESTS(state, "tests after SMTP handshake"); + + /* + * Initialize per-session state that is used only by the dummy engine: + * the command read buffer and the command read state machine. + */ + state->cmd_buffer = vstring_alloc(100); + state->read_state = PSC_SMTPD_CMD_ST_ANY; + + /* + * Disable all after-220 tests when we need to reply with 421 and hang up + * after reading the next SMTP client command. + * + * Opportunistically make postscreen more useful, by turning on all + * after-220 tests when a bad client failed a before-220 test. + * + * Otherwise, only apply the explicitly-configured after-220 tests. + */ + if (state->flags & PSC_STATE_FLAG_SMTPD_X21) { + state->flags &= ~PSC_STATE_MASK_SMTPD_TODO; + } else if (state->flags & PSC_STATE_MASK_ANY_FAIL) { + state->flags |= PSC_STATE_MASK_SMTPD_TODO; + } + + /* + * Send no SMTP banner to pregreeting clients. This eliminates a lot of + * "NON-SMTP COMMAND" events, and improves sender/recipient logging. + */ + if ((state->flags & PSC_STATE_FLAG_PREGR_FAIL) == 0 + && PSC_SEND_REPLY(state, psc_smtpd_greeting) != 0) { + psc_hangup_event(state); + return; + } + + /* + * Wait for the client to respond. + */ + PSC_READ_EVENT_REQUEST2(vstream_fileno(state->smtp_client_stream), + psc_smtpd_read_event, psc_smtpd_time_event, + (void *) state, PSC_EFF_CMD_TIME_LIMIT); +} + +/* psc_smtpd_init - per-process deep protocol test initialization */ + +void psc_smtpd_init(void) +{ + + /* + * Initialize the server banner. + */ + vstring_sprintf(psc_temp, "220 %s\r\n", var_smtpd_banner); + psc_smtpd_greeting = mystrdup(STR(psc_temp)); + + /* + * Initialize the HELO reply. + */ + vstring_sprintf(psc_temp, "250 %s\r\n", var_myhostname); + psc_smtpd_helo_reply = mystrdup(STR(psc_temp)); + + /* + * STARTTLS support. Note the complete absence of #ifdef USE_TLS + * throughout the postscreen(8) source code. If Postfix is built without + * TLS support, then the TLS proxy will simply report that TLS is not + * available, and conventional error handling will take care of the + * issue. + * + * Legacy code copied from smtpd(8). The pre-fabricated EHLO reply depends + * on this. + */ + if (*var_psc_tls_level) { + switch (tls_level_lookup(var_psc_tls_level)) { + default: + msg_fatal("Invalid TLS level \"%s\"", var_psc_tls_level); + /* NOTREACHED */ + break; + case TLS_LEV_SECURE: + case TLS_LEV_VERIFY: + case TLS_LEV_FPRINT: + msg_warn("%s: unsupported TLS level \"%s\", using \"encrypt\"", + VAR_PSC_TLS_LEVEL, var_psc_tls_level); + /* FALLTHROUGH */ + case TLS_LEV_ENCRYPT: + var_psc_enforce_tls = var_psc_use_tls = 1; + break; + case TLS_LEV_MAY: + var_psc_enforce_tls = 0; + var_psc_use_tls = 1; + break; + case TLS_LEV_NONE: + var_psc_enforce_tls = var_psc_use_tls = 0; + break; + } + } + var_psc_use_tls = var_psc_use_tls || var_psc_enforce_tls; +#ifdef TODO_SASL_AUTH + var_psc_tls_auth_only = var_psc_tls_auth_only || var_psc_enforce_tls; +#endif + + /* + * Initialize the EHLO reply. Once for plaintext sessions, and once for + * TLS sessions. + */ + psc_smtpd_format_ehlo_reply(psc_temp, psc_ehlo_discard_mask); + psc_smtpd_ehlo_reply_plain = mystrdup(STR(psc_temp)); + + psc_smtpd_format_ehlo_reply(psc_temp, + psc_ehlo_discard_mask | EHLO_MASK_STARTTLS); + psc_smtpd_ehlo_reply_tls = mystrdup(STR(psc_temp)); + + /* + * Initialize the 421 timeout reply. + */ + vstring_sprintf(psc_temp, "421 4.4.2 %s Error: timeout exceeded\r\n", + var_myhostname); + psc_smtpd_timeout_reply = mystrdup(STR(psc_temp)); + + /* + * Initialize the generic 421 reply. + */ + vstring_sprintf(psc_temp, "421 %s Service unavailable - try again later\r\n", + var_myhostname); + psc_smtpd_421_reply = mystrdup(STR(psc_temp)); + + /* + * Initialize the reply footer. + */ + if (*var_psc_rej_footer || *var_psc_rej_ftr_maps) + psc_expand_init(); +} + +/* psc_smtpd_pre_jail_init - per-process deep protocol test initialization */ + +void psc_smtpd_pre_jail_init(void) +{ + + /* + * Determine what server ESMTP features to suppress, typically to avoid + * inter-operability problems. We do the default filter here, and + * determine client-dependent filtering on the fly. + * + * XXX Bugger. This means we have to restart when the table changes! + */ + if (*var_psc_ehlo_dis_maps) + psc_ehlo_discard_maps = maps_create(VAR_PSC_EHLO_DIS_MAPS, + var_psc_ehlo_dis_maps, + DICT_FLAG_LOCK); + psc_ehlo_discard_mask = ehlo_mask(var_psc_ehlo_dis_words); + + /* + * Last-resort command editing support. + */ + if (*var_psc_cmd_filter) + psc_cmd_filter = dict_open(var_psc_cmd_filter, O_RDONLY, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX); + + /* + * SMTP server reply footer. + */ + if (*var_psc_rej_ftr_maps) + pcs_send_pre_jail_init(); +} diff --git a/src/postscreen/postscreen_starttls.c b/src/postscreen/postscreen_starttls.c new file mode 100644 index 0000000..4036a3d --- /dev/null +++ b/src/postscreen/postscreen_starttls.c @@ -0,0 +1,317 @@ +/*++ +/* NAME +/* postscreen_starttls 3 +/* SUMMARY +/* postscreen TLS proxy support +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* int psc_starttls_open(state, resume_event) +/* PSC_STATE *state; +/* void (*resume_event)(int unused_event, char *context); +/* DESCRIPTION +/* This module inserts the tlsproxy(8) proxy between the +/* postscreen(8) server and the remote SMTP client. The entire +/* process happens in the background, including notification +/* of completion to the remote SMTP client and to the calling +/* application. +/* +/* Before calling psc_starttls_open() the caller must turn off +/* all pending timer and I/O event requests on the SMTP client +/* stream. +/* +/* psc_starttls_open() starts the first transaction in the +/* tlsproxy(8) hand-off protocol, and sets up event handlers +/* for the successive protocol stages. +/* +/* Upon completion, the event handlers call resume_event() +/* which must reset the SMTP helo/sender/etc. state when the +/* PSC_STATE_FLAG_USING_TLS is set, and set up timer and read +/* event requests to receive the next SMTP command. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <connect.h> +#include <stringops.h> /* concatenate() */ +#include <vstring.h> + +/* Global library. */ + +#include <mail_params.h> +#include <mail_proto.h> + +/* TLS library. */ + +#include <tls_proxy.h> + +/* Application-specific. */ + +#include <postscreen.h> + + /* + * For now, this code is built into the postscreen(8) daemon. In the future + * it may be abstracted into a reusable library module for use by other + * event-driven programs (perhaps smtp-source and smtp-sink). + */ + + /* + * Transient state for the postscreen(8)-to-tlsproxy(8) hand-off protocol. + */ +typedef struct { + VSTREAM *tlsproxy_stream; /* hand-off negotiation */ + EVENT_NOTIFY_FN resume_event; /* call-back handler */ + PSC_STATE *smtp_state; /* SMTP session state */ +} PSC_STARTTLS; + +#define TLSPROXY_INIT_TIMEOUT 10 + +static char *psc_tlsp_service = 0; + +/* Resume the dummy SMTP engine after an event handling error */ + +#define PSC_STARTTLS_EVENT_ERR_RESUME_RETURN() do { \ + event_disable_readwrite(vstream_fileno(tlsproxy_stream)); \ + PSC_STARTTLS_EVENT_RESUME_RETURN(starttls_state); \ + } while (0); + +/* Resume the dummy SMTP engine, possibly after swapping streams */ + +#define PSC_STARTTLS_EVENT_RESUME_RETURN(starttls_state) do { \ + vstream_fclose(tlsproxy_stream); \ + starttls_state->resume_event(event, (void *) smtp_state); \ + myfree((void *) starttls_state); \ + return; \ + } while (0) + +/* psc_starttls_finish - complete negotiation with TLS proxy */ + +static void psc_starttls_finish(int event, void *context) +{ + const char *myname = "psc_starttls_finish"; + PSC_STARTTLS *starttls_state = (PSC_STARTTLS *) context; + PSC_STATE *smtp_state = starttls_state->smtp_state; + VSTREAM *tlsproxy_stream = starttls_state->tlsproxy_stream; + int status; + + if (msg_verbose) + msg_info("%s: send client handle on proxy socket %d" + " for smtp socket %d from [%s]:%s flags=%s", + myname, vstream_fileno(tlsproxy_stream), + vstream_fileno(smtp_state->smtp_client_stream), + smtp_state->smtp_client_addr, smtp_state->smtp_client_port, + psc_print_state_flags(smtp_state->flags, myname)); + + /* + * We leave read-event notification enabled on the postscreen to TLS + * proxy stream, to avoid two kqueue/epoll/etc. system calls: one here, + * and one when resuming the dummy SMTP engine. + */ + if (event != EVENT_TIME) + event_cancel_timer(psc_starttls_finish, (void *) starttls_state); + + /* + * Receive the "TLS is available" indication. + * + * This may seem out of order, but we must have a read transaction between + * sending the request attributes and sending the SMTP client file + * descriptor. We can't assume UNIX-domain socket semantics here. + */ + if (event != EVENT_READ + || attr_scan(tlsproxy_stream, ATTR_FLAG_STRICT, + RECV_ATTR_INT(MAIL_ATTR_STATUS, &status), + ATTR_TYPE_END) != 1 || status == 0) { + + /* + * The TLS proxy reports that the TLS engine is not available (due to + * configuration error, or other causes). + */ + msg_warn("%s receiving status from %s service", + event == EVENT_TIME ? "timeout" : "problem", psc_tlsp_service); + PSC_SEND_REPLY(smtp_state, + "454 4.7.0 TLS not available due to local problem\r\n"); + PSC_STARTTLS_EVENT_ERR_RESUME_RETURN(); + } + + /* + * Send the remote SMTP client file descriptor. + */ + else if (LOCAL_SEND_FD(vstream_fileno(tlsproxy_stream), + vstream_fileno(smtp_state->smtp_client_stream)) < 0) { + + /* + * Some error: drop the TLS proxy stream. + */ + msg_warn("problem sending file handle to %s service", psc_tlsp_service); + PSC_SEND_REPLY(smtp_state, + "454 4.7.0 TLS not available due to local problem\r\n"); + PSC_STARTTLS_EVENT_ERR_RESUME_RETURN(); + } + + /* + * After we send the plaintext 220 greeting, the client-side TLS engine + * is supposed to talk first, then the server-side TLS engine. However, + * postscreen(8) will not participate in that conversation. + */ + else { + PSC_SEND_REPLY(smtp_state, "220 2.0.0 Ready to start TLS\r\n"); + + /* + * Swap the SMTP client stream and the TLS proxy stream, and close + * the direct connection to the SMTP client. The TLS proxy will talk + * directly to the SMTP client, and once the TLS handshake is + * completed, the TLS proxy will talk plaintext to postscreen(8). + * + * Swap the file descriptors from under the VSTREAM so that we don't + * have to worry about loss of user-configurable VSTREAM attributes. + */ + vstream_fpurge(smtp_state->smtp_client_stream, VSTREAM_PURGE_BOTH); + vstream_control(smtp_state->smtp_client_stream, + CA_VSTREAM_CTL_SWAP_FD(tlsproxy_stream), + CA_VSTREAM_CTL_END); + smtp_state->flags |= PSC_STATE_FLAG_USING_TLS; + PSC_STARTTLS_EVENT_RESUME_RETURN(starttls_state); + } +} + +/* psc_starttls_first - start negotiation with TLS proxy */ + +static void psc_starttls_first(int event, void *context) +{ + const char *myname = "psc_starttls_first"; + PSC_STARTTLS *starttls_state = (PSC_STARTTLS *) context; + PSC_STATE *smtp_state = starttls_state->smtp_state; + VSTREAM *tlsproxy_stream = starttls_state->tlsproxy_stream; + static VSTRING *remote_endpt = 0; + + if (msg_verbose) + msg_info("%s: receive server protocol on proxy socket %d" + " for smtp socket %d from [%s]:%s flags=%s", + myname, vstream_fileno(tlsproxy_stream), + vstream_fileno(smtp_state->smtp_client_stream), + smtp_state->smtp_client_addr, smtp_state->smtp_client_port, + psc_print_state_flags(smtp_state->flags, myname)); + + /* + * We leave read-event notification enabled on the postscreen to TLS + * proxy stream, to avoid two kqueue/epoll/etc. system calls: one here, + * and one when resuming the dummy SMTP engine. + */ + if (event != EVENT_TIME) + event_cancel_timer(psc_starttls_first, (void *) starttls_state); + + /* + * Receive and verify the server protocol. + */ + if (event != EVENT_READ + || attr_scan(tlsproxy_stream, ATTR_FLAG_STRICT, + RECV_ATTR_STREQ(MAIL_ATTR_PROTO, MAIL_ATTR_PROTO_TLSPROXY), + ATTR_TYPE_END) != 0) { + msg_warn("%s receiving %s attribute from %s service: %m", + event == EVENT_TIME ? "timeout" : "problem", + MAIL_ATTR_PROTO, psc_tlsp_service); + PSC_SEND_REPLY(smtp_state, + "454 4.7.0 TLS not available due to local problem\r\n"); + PSC_STARTTLS_EVENT_ERR_RESUME_RETURN(); + } + + /* + * Send the data attributes now, and send the client file descriptor in a + * later transaction. We report all errors asynchronously, to avoid + * having to maintain multiple error delivery paths. + * + * XXX The formatted endpoint should be a state member. Then, we can + * simplify all the format strings throughout the program. + */ + if (remote_endpt == 0) + remote_endpt = vstring_alloc(20); + vstring_sprintf(remote_endpt, "[%s]:%s", smtp_state->smtp_client_addr, + smtp_state->smtp_client_port); + attr_print(tlsproxy_stream, ATTR_FLAG_NONE, + SEND_ATTR_STR(TLS_ATTR_REMOTE_ENDPT, STR(remote_endpt)), + SEND_ATTR_INT(TLS_ATTR_FLAGS, TLS_PROXY_FLAG_ROLE_SERVER), + SEND_ATTR_INT(TLS_ATTR_TIMEOUT, psc_normal_cmd_time_limit), + SEND_ATTR_INT(TLS_ATTR_TIMEOUT, psc_normal_cmd_time_limit), + SEND_ATTR_STR(TLS_ATTR_SERVERID, MAIL_SERVICE_SMTPD), /* XXX */ + ATTR_TYPE_END); + if (vstream_fflush(tlsproxy_stream) != 0) { + msg_warn("error sending request to %s service: %m", psc_tlsp_service); + PSC_SEND_REPLY(smtp_state, + "454 4.7.0 TLS not available due to local problem\r\n"); + PSC_STARTTLS_EVENT_ERR_RESUME_RETURN(); + } + + /* + * Set up a read event for the next phase of the TLS proxy handshake. + */ + PSC_READ_EVENT_REQUEST(vstream_fileno(tlsproxy_stream), psc_starttls_finish, + (void *) starttls_state, TLSPROXY_INIT_TIMEOUT); +} + +/* psc_starttls_open - open negotiations with TLS proxy */ + +void psc_starttls_open(PSC_STATE *smtp_state, EVENT_NOTIFY_FN resume_event) +{ + const char *myname = "psc_starttls_open"; + PSC_STARTTLS *starttls_state; + VSTREAM *tlsproxy_stream; + int fd; + + if (psc_tlsp_service == 0) { + psc_tlsp_service = concatenate(MAIL_CLASS_PRIVATE "/", + var_tlsproxy_service, (char *) 0); + } + + /* + * Connect to the tlsproxy(8) daemon. We report all errors + * asynchronously, to avoid having to maintain multiple delivery paths. + */ + if ((fd = LOCAL_CONNECT(psc_tlsp_service, NON_BLOCKING, 1)) < 0) { + msg_warn("connect to %s service: %m", psc_tlsp_service); + PSC_SEND_REPLY(smtp_state, + "454 4.7.0 TLS not available due to local problem\r\n"); + event_request_timer(resume_event, (void *) smtp_state, 0); + return; + } + if (msg_verbose) + msg_info("%s: connecting to proxy socket %d" + " for smtp socket %d from [%s]:%s flags=%s", + myname, fd, vstream_fileno(smtp_state->smtp_client_stream), + smtp_state->smtp_client_addr, smtp_state->smtp_client_port, + psc_print_state_flags(smtp_state->flags, myname)); + + tlsproxy_stream = vstream_fdopen(fd, O_RDWR); + vstream_control(tlsproxy_stream, + VSTREAM_CTL_PATH, psc_tlsp_service, + VSTREAM_CTL_END); + + /* + * Set up a read event for the next phase of the TLS proxy handshake. + */ + starttls_state = (PSC_STARTTLS *) mymalloc(sizeof(*starttls_state)); + starttls_state->tlsproxy_stream = tlsproxy_stream; + starttls_state->resume_event = resume_event; + starttls_state->smtp_state = smtp_state; + PSC_READ_EVENT_REQUEST(vstream_fileno(tlsproxy_stream), psc_starttls_first, + (void *) starttls_state, TLSPROXY_INIT_TIMEOUT); +} diff --git a/src/postscreen/postscreen_state.c b/src/postscreen/postscreen_state.c new file mode 100644 index 0000000..2b5db3c --- /dev/null +++ b/src/postscreen/postscreen_state.c @@ -0,0 +1,317 @@ +/*++ +/* NAME +/* postscreen_state 3 +/* SUMMARY +/* postscreen session state and queue length management +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* PSC_STATE *psc_new_session_state(stream, client_addr, client_port, +/* server_addr, server_port) +/* VSTREAM *stream; +/* const char *client_addr; +/* const char *client_port; +/* const char *server_addr; +/* const char *server_port; +/* +/* void psc_free_session_state(state) +/* PSC_STATE *state; +/* +/* char *psc_print_state_flags(flags, context) +/* int flags; +/* const char *context; +/* +/* void PSC_ADD_SERVER_STATE(state, server_fd) +/* PSC_STATE *state; +/* int server_fd; +/* +/* void PSC_DEL_SERVER_STATE(state) +/* PSC_STATE *state; +/* +/* void PSC_DEL_CLIENT_STATE(state) +/* PSC_STATE *state; +/* +/* void PSC_DROP_SESSION_STATE(state, final_reply) +/* PSC_STATE *state; +/* const char *final_reply; +/* +/* void PSC_ENFORCE_SESSION_STATE(state, rcpt_reply) +/* PSC_STATE *state; +/* const char *rcpt_reply; +/* +/* void PSC_PASS_SESSION_STATE(state, testname, pass_flag) +/* PSC_STATE *state; +/* const char *testname; +/* int pass_flag; +/* +/* void PSC_FAIL_SESSION_STATE(state, fail_flag) +/* PSC_STATE *state; +/* int fail_flag; +/* +/* void PSC_UNFAIL_SESSION_STATE(state, fail_flag) +/* PSC_STATE *state; +/* int fail_flag; +/* DESCRIPTION +/* This module maintains per-client session state, and two +/* global file descriptor counters: +/* .IP psc_check_queue_length +/* The total number of remote SMTP client sockets. +/* .IP psc_post_queue_length +/* The total number of server file descriptors that are currently +/* in use for client file descriptor passing. This number +/* equals the number of client file descriptors in transit. +/* .PP +/* psc_new_session_state() creates a new session state object +/* for the specified client stream, and increments the +/* psc_check_queue_length counter. The flags and per-test time +/* stamps are initialized with PSC_INIT_TESTS(), or for concurrent +/* sessions, with PSC_INIT_TEST_FLAGS_ONLY(). The addr and +/* port arguments are null-terminated strings with the remote +/* SMTP client endpoint. The _reply members are set to +/* polite "try again" SMTP replies. The protocol member is set +/* to "SMTP". +/* +/* The psc_stress variable is set to non-zero when +/* psc_check_queue_length passes over a high-water mark. +/* +/* psc_free_session_state() destroys the specified session state +/* object, closes the applicable I/O channels, and decrements +/* the applicable file descriptor counters: psc_check_queue_length +/* and psc_post_queue_length. +/* +/* The psc_stress variable is reset to zero when psc_check_queue_length +/* passes under a low-water mark. +/* +/* psc_print_state_flags() converts per-session flags into +/* human-readable form. The context is for error reporting. +/* The result is overwritten upon each call. +/* +/* PSC_ADD_SERVER_STATE() updates the specified session state +/* object with the specified server file descriptor, and +/* increments the global psc_post_queue_length file descriptor +/* counter. +/* +/* PSC_DEL_SERVER_STATE() closes the specified session state +/* object's server file descriptor, and decrements the global +/* psc_post_queue_length file descriptor counter. +/* +/* PSC_DEL_CLIENT_STATE() updates the specified session state +/* object, closes the client stream, and decrements the global +/* psc_check_queue_length file descriptor counter. +/* +/* PSC_DROP_SESSION_STATE() updates the specified session state +/* object and closes the client stream after sending the +/* specified SMTP reply. +/* +/* PSC_ENFORCE_SESSION_STATE() updates the specified session +/* state object. It arranges that the built-in SMTP engine +/* logs sender/recipient information and rejects all RCPT TO +/* commands with the specified SMTP reply. +/* +/* PSC_PASS_SESSION_STATE() sets the specified "pass" flag. +/* The testname is used for debug logging. +/* +/* PSC_FAIL_SESSION_STATE() sets the specified "fail" flag. +/* +/* PSC_UNFAIL_SESSION_STATE() unsets the specified "fail" flag. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <name_mask.h> +#include <htable.h> + +/* Global library. */ + +#include <mail_proto.h> + +/* Master server protocols. */ + +#include <mail_server.h> + +/* Application-specific. */ + +#include <postscreen.h> + +/* psc_new_session_state - fill in connection state for event processing */ + +PSC_STATE *psc_new_session_state(VSTREAM *stream, + const char *client_addr, + const char *client_port, + const char *server_addr, + const char *server_port) +{ + PSC_STATE *state; + + state = (PSC_STATE *) mymalloc(sizeof(*state)); + if ((state->smtp_client_stream = stream) != 0) + psc_check_queue_length++; + state->smtp_server_fd = (-1); + state->smtp_client_addr = mystrdup(client_addr); + state->smtp_client_port = mystrdup(client_port); + state->smtp_server_addr = mystrdup(server_addr); + state->smtp_server_port = mystrdup(server_port); + state->send_buf = vstring_alloc(100); + state->test_name = "TEST NAME HERE"; + state->dnsbl_reply = 0; + state->final_reply = "421 4.3.2 Service currently unavailable\r\n"; + state->rcpt_reply = "450 4.3.2 Service currently unavailable\r\n"; + state->command_count = 0; + state->protocol = MAIL_PROTO_SMTP; + state->helo_name = 0; + state->sender = 0; + state->cmd_buffer = 0; + state->read_state = 0; + state->ehlo_discard_mask = 0; /* XXX Should be ~0 */ + state->expand_buf = 0; + state->where = PSC_SMTPD_CMD_CONNECT; + + /* + * Update the stress level. + */ + if (psc_stress == 0 + && psc_check_queue_length >= psc_hiwat_check_queue_length) { + psc_stress = 1; + msg_info("entering STRESS mode with %d connections", + psc_check_queue_length); + } + + /* + * Update the per-client session count. + */ + if ((state->client_info = (PSC_CLIENT_INFO *) + htable_find(psc_client_concurrency, client_addr)) == 0) { + state->client_info = (PSC_CLIENT_INFO *) + mymalloc(sizeof(state->client_info[0])); + (void) htable_enter(psc_client_concurrency, client_addr, + (void *) state->client_info); + PSC_INIT_TESTS(state); + state->client_info->concurrency = 1; + state->client_info->pass_new_count = 0; + } else { + PSC_INIT_TEST_FLAGS_ONLY(state); + state->client_info->concurrency += 1; + } + + return (state); +} + +/* psc_free_session_state - destroy connection state including connections */ + +void psc_free_session_state(PSC_STATE *state) +{ + const char *myname = "psc_free_session_state"; + HTABLE_INFO *ht; + + /* + * Update the per-client session count. + */ + if ((ht = htable_locate(psc_client_concurrency, + state->smtp_client_addr)) == 0) + msg_panic("%s: unknown client address: %s", + myname, state->smtp_client_addr); + if (--(state->client_info->concurrency) == 0) + htable_delete(psc_client_concurrency, state->smtp_client_addr, myfree); + + if (state->smtp_client_stream != 0) { + PSC_DEL_CLIENT_STATE(state); + } + if (state->smtp_server_fd >= 0) { + PSC_DEL_SERVER_STATE(state); + } + if (state->send_buf != 0) + state->send_buf = vstring_free(state->send_buf); + myfree(state->smtp_client_addr); + myfree(state->smtp_client_port); + myfree(state->smtp_server_addr); + myfree(state->smtp_server_port); + if (state->dnsbl_reply) + vstring_free(state->dnsbl_reply); + if (state->helo_name) + myfree(state->helo_name); + if (state->sender) + myfree(state->sender); + if (state->cmd_buffer) + vstring_free(state->cmd_buffer); + if (state->expand_buf) + vstring_free(state->expand_buf); + myfree((void *) state); + + if (psc_check_queue_length < 0 || psc_post_queue_length < 0) + msg_panic("bad queue length: check_queue=%d, post_queue=%d", + psc_check_queue_length, psc_post_queue_length); + + /* + * Update the stress level. + */ + if (psc_stress != 0 + && psc_check_queue_length <= psc_lowat_check_queue_length) { + psc_stress = 0; + msg_info("leaving STRESS mode with %d connections", + psc_check_queue_length); + } +} + +/* psc_print_state_flags - format state flags */ + +const char *psc_print_state_flags(int flags, const char *context) +{ + static const NAME_MASK flags_mask[] = { + "NOFORWARD", PSC_STATE_FLAG_NOFORWARD, + "USING_TLS", PSC_STATE_FLAG_USING_TLS, + "NEW", PSC_STATE_FLAG_NEW, + "DNLIST_FAIL", PSC_STATE_FLAG_DNLIST_FAIL, + "HANGUP", PSC_STATE_FLAG_HANGUP, + /* unused */ + "ALLIST_FAIL", PSC_STATE_FLAG_ALLIST_FAIL, + + "PREGR_FAIL", PSC_STATE_FLAG_PREGR_FAIL, + "PREGR_PASS", PSC_STATE_FLAG_PREGR_PASS, + "PREGR_TODO", PSC_STATE_FLAG_PREGR_TODO, + "PREGR_DONE", PSC_STATE_FLAG_PREGR_DONE, + + "DNSBL_FAIL", PSC_STATE_FLAG_DNSBL_FAIL, + "DNSBL_PASS", PSC_STATE_FLAG_DNSBL_PASS, + "DNSBL_TODO", PSC_STATE_FLAG_DNSBL_TODO, + "DNSBL_DONE", PSC_STATE_FLAG_DNSBL_DONE, + + "PIPEL_FAIL", PSC_STATE_FLAG_PIPEL_FAIL, + "PIPEL_PASS", PSC_STATE_FLAG_PIPEL_PASS, + "PIPEL_TODO", PSC_STATE_FLAG_PIPEL_TODO, + "PIPEL_SKIP", PSC_STATE_FLAG_PIPEL_SKIP, + + "NSMTP_FAIL", PSC_STATE_FLAG_NSMTP_FAIL, + "NSMTP_PASS", PSC_STATE_FLAG_NSMTP_PASS, + "NSMTP_TODO", PSC_STATE_FLAG_NSMTP_TODO, + "NSMTP_SKIP", PSC_STATE_FLAG_NSMTP_SKIP, + + "BARLF_FAIL", PSC_STATE_FLAG_BARLF_FAIL, + "BARLF_PASS", PSC_STATE_FLAG_BARLF_PASS, + "BARLF_TODO", PSC_STATE_FLAG_BARLF_TODO, + "BARLF_SKIP", PSC_STATE_FLAG_BARLF_SKIP, + 0, + }; + + return (str_name_mask_opt((VSTRING *) 0, context, flags_mask, flags, + NAME_MASK_PIPE | NAME_MASK_NUMBER)); +} diff --git a/src/postscreen/postscreen_tests.c b/src/postscreen/postscreen_tests.c new file mode 100644 index 0000000..5e18622 --- /dev/null +++ b/src/postscreen/postscreen_tests.c @@ -0,0 +1,341 @@ +/*++ +/* NAME +/* postscreen_tests 3 +/* SUMMARY +/* postscreen tests timestamp/flag bulk support +/* SYNOPSIS +/* #include <postscreen.h> +/* +/* void PSC_INIT_TESTS(state) +/* PSC_STATE *state; +/* +/* void psc_new_tests(state) +/* PSC_STATE *state; +/* +/* void psc_parse_tests(state, stamp_text, time_value) +/* PSC_STATE *state; +/* const char *stamp_text; +/* time_t time_value; +/* +/* void psc_todo_tests(state, time_value) +/* PSC_STATE *state; +/* const char *stamp_text; +/* time_t time_value; +/* +/* char *psc_print_tests(buffer, state) +/* VSTRING *buffer; +/* PSC_STATE *state; +/* +/* char *psc_print_grey_key(buffer, client, helo, sender, rcpt) +/* VSTRING *buffer; +/* const char *client; +/* const char *helo; +/* const char *sender; +/* const char *rcpt; +/* +/* const char *psc_test_name(tindx) +/* int tindx; +/* DESCRIPTION +/* The functions in this module overwrite the per-test expiration +/* time stamps and all flags bits. Some functions are implemented +/* as unsafe macros, meaning they evaluate one or more arguments +/* multiple times. +/* +/* PSC_INIT_TESTS() is an unsafe macro that sets the per-test +/* expiration time stamps to PSC_TIME_STAMP_INVALID, and that +/* zeroes all the flags bits. These values are not meant to +/* be stored into the postscreen(8) cache. +/* +/* PSC_INIT_TEST_FLAGS_ONLY() zeroes all the flag bits. It +/* should be used when the time stamps are already initialized. +/* +/* psc_new_tests() sets all test expiration time stamps to +/* PSC_TIME_STAMP_NEW, and invokes psc_todo_tests(). +/* +/* psc_parse_tests() parses a cache file record and invokes +/* psc_todo_tests(). +/* +/* psc_todo_tests() overwrites all per-session flag bits, and +/* populates the flags based on test expiration time stamp +/* information. Tests are considered "expired" when they +/* would be expired at the specified time value. Only enabled +/* tests are flagged as "expired"; the object is flagged as +/* "new" if some enabled tests have "new" time stamps. +/* +/* psc_print_tests() creates a cache file record for the +/* specified flags and per-test expiration time stamps. +/* This may modify the time stamps for disabled tests. +/* +/* psc_print_grey_key() prints a greylist lookup key. +/* +/* psc_test_name() returns the name for the specified text +/* index. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <stdio.h> /* sscanf */ + +/* Utility library. */ + +#include <msg.h> +#include <name_code.h> +#include <sane_strtol.h> + +/* Global library. */ + +#include <mail_params.h> + +/* Application-specific. */ + +#include <postscreen.h> + + /* + * Kludge to detect if some test is enabled. + */ +#define PSC_PREGR_TEST_ENABLE() (*var_psc_pregr_banner != 0) +#define PSC_DNSBL_TEST_ENABLE() (*var_psc_dnsbl_sites != 0) + + /* + * Format of a persistent cache entry (which is almost but not quite the + * same as the in-memory representation). + * + * Each cache entry has one time stamp for each test. + * + * - A time stamp of PSC_TIME_STAMP_INVALID must never appear in the cache. It + * is reserved for in-memory objects that are still being initialized. + * + * - A time stamp of PSC_TIME_STAMP_NEW indicates that the test never passed. + * Postscreen will log the client with "pass new" when it passes the final + * test. + * + * - A time stamp of PSC_TIME_STAMP_DISABLED indicates that the test never + * passed, and that the test was disabled when the cache entry was written. + * + * - Otherwise, the test was passed, and the time stamp indicates when that + * test result expires. + * + * A cache entry is expired when the time stamps of all passed tests are + * expired. + */ + +/* psc_new_tests - initialize new test results from scratch */ + +void psc_new_tests(PSC_STATE *state) +{ + time_t *expire_time = state->client_info->expire_time; + + /* + * Give all tests a PSC_TIME_STAMP_NEW time stamp, so that we can later + * recognize cache entries that haven't passed all enabled tests. When we + * write a cache entry to the database, any new-but-disabled tests will + * get a PSC_TIME_STAMP_DISABLED time stamp. + */ + expire_time[PSC_TINDX_PREGR] = PSC_TIME_STAMP_NEW; + expire_time[PSC_TINDX_DNSBL] = PSC_TIME_STAMP_NEW; + expire_time[PSC_TINDX_PIPEL] = PSC_TIME_STAMP_NEW; + expire_time[PSC_TINDX_NSMTP] = PSC_TIME_STAMP_NEW; + expire_time[PSC_TINDX_BARLF] = PSC_TIME_STAMP_NEW; + + /* + * Determine what tests need to be completed. + */ + psc_todo_tests(state, PSC_TIME_STAMP_NEW + 1); +} + +/* psc_parse_tests - parse test results from cache */ + +void psc_parse_tests(PSC_STATE *state, + const char *stamp_str, + time_t time_value) +{ + const char *start = stamp_str; + char *cp; + time_t *time_stamps = state->client_info->expire_time; + time_t *sp; + + /* + * Parse the cache entry, and allow for older postscreen versions that + * implemented fewer tests. We pretend that the newer tests were disabled + * at the time that the cache entry was written. + */ + for (sp = time_stamps; sp < time_stamps + PSC_TINDX_COUNT; sp++) { + *sp = sane_strtoul(start, &cp, 10); + if (*start == 0 || (*cp != '\0' && *cp != ';') || errno == ERANGE) + *sp = PSC_TIME_STAMP_DISABLED; + if (msg_verbose) + msg_info("%s -> %lu", start, (unsigned long) *sp); + if (*cp == ';') + start = cp + 1; + else + start = cp; + } + + /* + * Determine what tests need to be completed. + */ + psc_todo_tests(state, time_value); +} + +/* psc_todo_tests - determine what tests to perform */ + +void psc_todo_tests(PSC_STATE *state, time_t time_value) +{ + time_t *expire_time = state->client_info->expire_time; + time_t *sp; + + /* + * Reset all per-session flags. + */ + state->flags = 0; + + /* + * Flag the tests as "new" when the cache entry has fields for all + * enabled tests, but the remote SMTP client has not yet passed all those + * tests. + */ + for (sp = expire_time; sp < expire_time + PSC_TINDX_COUNT; sp++) { + if (*sp == PSC_TIME_STAMP_NEW) + state->flags |= PSC_STATE_FLAG_NEW; + } + + /* + * Don't flag disabled tests as "todo", because there would be no way to + * make those bits go away. + */ + if (PSC_PREGR_TEST_ENABLE() && time_value > expire_time[PSC_TINDX_PREGR]) + state->flags |= PSC_STATE_FLAG_PREGR_TODO; + if (PSC_DNSBL_TEST_ENABLE() && time_value > expire_time[PSC_TINDX_DNSBL]) + state->flags |= PSC_STATE_FLAG_DNSBL_TODO; + if (var_psc_pipel_enable && time_value > expire_time[PSC_TINDX_PIPEL]) + state->flags |= PSC_STATE_FLAG_PIPEL_TODO; + if (var_psc_nsmtp_enable && time_value > expire_time[PSC_TINDX_NSMTP]) + state->flags |= PSC_STATE_FLAG_NSMTP_TODO; + if (var_psc_barlf_enable && time_value > expire_time[PSC_TINDX_BARLF]) + state->flags |= PSC_STATE_FLAG_BARLF_TODO; + + /* + * If any test has expired, proactively refresh tests that will expire + * soon. This can increase the occurrence of client-visible delays, but + * avoids questions about why a client can pass some test and then fail + * within seconds. The proactive refresh time is really a surrogate for + * the user's curiosity level, and therefore hard to choose optimally. + */ +#ifdef VAR_PSC_REFRESH_TIME + if ((state->flags & PSC_STATE_MASK_ANY_TODO) != 0 + && var_psc_refresh_time > 0) { + time_t refresh_time = time_value + var_psc_refresh_time; + + if (PSC_PREGR_TEST_ENABLE() && refresh_time > expire_time[PSC_TINDX_PREGR]) + state->flags |= PSC_STATE_FLAG_PREGR_TODO; + if (PSC_DNSBL_TEST_ENABLE() && refresh_time > expire_time[PSC_TINDX_DNSBL]) + state->flags |= PSC_STATE_FLAG_DNSBL_TODO; + if (var_psc_pipel_enable && refresh_time > expire_time[PSC_TINDX_PIPEL]) + state->flags |= PSC_STATE_FLAG_PIPEL_TODO; + if (var_psc_nsmtp_enable && refresh_time > expire_time[PSC_TINDX_NSMTP]) + state->flags |= PSC_STATE_FLAG_NSMTP_TODO; + if (var_psc_barlf_enable && refresh_time > expire_time[PSC_TINDX_BARLF]) + state->flags |= PSC_STATE_FLAG_BARLF_TODO; + } +#endif + + /* + * Gratuitously make postscreen logging more useful by turning on all + * enabled pre-handshake tests when any pre-handshake test is turned on. + * + * XXX Don't enable PREGREET gratuitously before the test expires. With a + * short TTL for DNSBL allowlisting, turning on PREGREET would force a + * full postscreen_greet_wait too frequently. + */ +#if 0 + if (state->flags & PSC_STATE_MASK_EARLY_TODO) { + if (PSC_PREGR_TEST_ENABLE()) + state->flags |= PSC_STATE_FLAG_PREGR_TODO; + if (PSC_DNSBL_TEST_ENABLE()) + state->flags |= PSC_STATE_FLAG_DNSBL_TODO; + } +#endif +} + +/* psc_print_tests - print postscreen cache record */ + +char *psc_print_tests(VSTRING *buf, PSC_STATE *state) +{ + const char *myname = "psc_print_tests"; + time_t *expire_time = state->client_info->expire_time; + + /* + * Sanity check. + */ + if ((state->flags & PSC_STATE_MASK_ANY_UPDATE) == 0) + msg_panic("%s: attempt to save a no-update record", myname); + + /* + * Give disabled tests a dummy time stamp so that we don't log a client + * with "pass new" when some disabled test becomes enabled at some later + * time. + */ + if (PSC_PREGR_TEST_ENABLE() == 0 && expire_time[PSC_TINDX_PREGR] == PSC_TIME_STAMP_NEW) + expire_time[PSC_TINDX_PREGR] = PSC_TIME_STAMP_DISABLED; + if (PSC_DNSBL_TEST_ENABLE() == 0 && expire_time[PSC_TINDX_DNSBL] == PSC_TIME_STAMP_NEW) + expire_time[PSC_TINDX_DNSBL] = PSC_TIME_STAMP_DISABLED; + if (var_psc_pipel_enable == 0 && expire_time[PSC_TINDX_PIPEL] == PSC_TIME_STAMP_NEW) + expire_time[PSC_TINDX_PIPEL] = PSC_TIME_STAMP_DISABLED; + if (var_psc_nsmtp_enable == 0 && expire_time[PSC_TINDX_NSMTP] == PSC_TIME_STAMP_NEW) + expire_time[PSC_TINDX_NSMTP] = PSC_TIME_STAMP_DISABLED; + if (var_psc_barlf_enable == 0 && expire_time[PSC_TINDX_BARLF] == PSC_TIME_STAMP_NEW) + expire_time[PSC_TINDX_BARLF] = PSC_TIME_STAMP_DISABLED; + + vstring_sprintf(buf, "%lu;%lu;%lu;%lu;%lu", + (unsigned long) expire_time[PSC_TINDX_PREGR], + (unsigned long) expire_time[PSC_TINDX_DNSBL], + (unsigned long) expire_time[PSC_TINDX_PIPEL], + (unsigned long) expire_time[PSC_TINDX_NSMTP], + (unsigned long) expire_time[PSC_TINDX_BARLF]); + return (STR(buf)); +} + +/* psc_print_grey_key - print postscreen cache record */ + +char *psc_print_grey_key(VSTRING *buf, const char *client, + const char *helo, const char *sender, + const char *rcpt) +{ + return (STR(vstring_sprintf(buf, "%s/%s/%s/%s", + client, helo, sender, rcpt))); +} + +/* psc_test_name - map test index to symbolic name */ + +const char *psc_test_name(int tindx) +{ + const char *myname = "psc_test_name"; + const NAME_CODE test_name_map[] = { + PSC_TNAME_PREGR, PSC_TINDX_PREGR, + PSC_TNAME_DNSBL, PSC_TINDX_DNSBL, + PSC_TNAME_PIPEL, PSC_TINDX_PIPEL, + PSC_TNAME_NSMTP, PSC_TINDX_NSMTP, + PSC_TNAME_BARLF, PSC_TINDX_BARLF, + 0, -1, + }; + const char *result; + + if ((result = str_name_code(test_name_map, tindx)) == 0) + msg_panic("%s: bad index %d", myname, tindx); + return (result); +} |