diff --git a/postfix/.indent.pro b/postfix/.indent.pro index ebb7b7270..f1671a6ec 100644 --- a/postfix/.indent.pro +++ b/postfix/.indent.pro @@ -260,6 +260,10 @@ -TPCF_SERVICE_DEF -TPCF_SERVICE_PATTERN -TPCF_STRING_NV +-TPEER_FROM_HAPROXY_CASE +-TPEER_FROM_NON_SOCKET_CASE +-TPEER_FROM_PASS_ATTR_CASE +-TPEER_FROM_UNCONN_SOCKET_CASE -TPEER_NAME -TPGSQL_NAME -TPICKUP_INFO @@ -358,6 +362,7 @@ -TSTRING_LIST -TSTRING_TABLE -TSYS_EXITS_DETAIL +-TTEST_BASE -TTEST_CASE -TTLSMGR_SCACHE -TTLSP_STATE diff --git a/postfix/HISTORY b/postfix/HISTORY index 449cbad12..489e0f9e9 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -29081,3 +29081,24 @@ Apologies for any names omitted. Bit rot: sane_sockaddr_to_hostaddr() may modify its inputs. smtp/smtp_tlsrpt.c, postscreen/postscreen_endpt.c + +20250411 + + Code health: simplified the Postfix SMTP server code to + find out the client and server IP addresses for an SMTP + connection. This takes advantage of the improved support + for address normalization and for haproxy load balancers. + Files: smtpd/smtpd_peer.c, smtpd/smtpd_haproxy.c. + + Documentation: XCLIENT attribute availability. File: + proto/XCLIENT. + +20250418 + + Code health: added unit tests for connection address and + port information received through haproxy or postscreen, + and improved error handling. Files: smtpd/smtpd_peer.c, + smtpd/smtpd_haproxy.c, smtpd/smtpd_peer_test.c. + + Unit tests for 'direct' connections are deferred pending + support to mock or intercept system library function calls. diff --git a/postfix/Makefile.in b/postfix/Makefile.in index a37f89f3b..12789cad7 100644 --- a/postfix/Makefile.in +++ b/postfix/Makefile.in @@ -115,7 +115,8 @@ manpages: done -

Note 4: Some Postfix implementations do not implement the PORT -or LOGIN attributes.

+

Note 4: The PORT attribute is implemented in Postfix 2.5 and +later; the LOGIN attribute in Postfix 2.9 and later.

XCLIENT Server response

diff --git a/postfix/proto/XCLIENT_README.html b/postfix/proto/XCLIENT_README.html index 36a42bc91..ed228f49f 100644 --- a/postfix/proto/XCLIENT_README.html +++ b/postfix/proto/XCLIENT_README.html @@ -145,8 +145,8 @@ xtext encode attribute values. Servers that wish to interoperate with these older implementations should be prepared to receive unencoded information.

-

Note 4: Some Postfix implementations do not implement the PORT -or LOGIN attributes.

+

Note 4: The PORT attribute is implemented in Postfix 2.5 and +later; the LOGIN attribute in Postfix 2.9 and later.

XCLIENT Server response

diff --git a/postfix/proto/stop b/postfix/proto/stop index 3ac8e4bb8..7c06d7ee4 100644 --- a/postfix/proto/stop +++ b/postfix/proto/stop @@ -1673,3 +1673,4 @@ bugfix MLKEM cleartext redacted +subclassed diff --git a/postfix/proto/stop.spell-history b/postfix/proto/stop.spell-history index 1a66bdb7f..da067ab97 100644 --- a/postfix/proto/stop.spell-history +++ b/postfix/proto/stop.spell-history @@ -105,3 +105,4 @@ Oemer Kozmenko Oleksandr Bataille +balancers diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h index 5e4813491..1d3ab0d81 100644 --- a/postfix/src/global/mail_version.h +++ b/postfix/src/global/mail_version.h @@ -20,7 +20,7 @@ * Patches change both the patchlevel and the release date. Snapshots have no * patchlevel; they change the release date only. */ -#define MAIL_RELEASE_DATE "20250409" +#define MAIL_RELEASE_DATE "20250418" #define MAIL_VERSION_NUMBER "3.11" #ifdef SNAPSHOT diff --git a/postfix/src/smtpd/Makefile.in b/postfix/src/smtpd/Makefile.in index 139918f41..5bd1a78f6 100644 --- a/postfix/src/smtpd/Makefile.in +++ b/postfix/src/smtpd/Makefile.in @@ -13,7 +13,7 @@ HDRS = smtpd_token.h smtpd_check.h smtpd_chat.h smtpd_sasl_proto.h \ TESTSRC = smtpd_token_test.c DEFS = -I. -I$(INC_DIR) -D$(SYSTYPE) CFLAGS = $(DEBUG) $(OPT) $(DEFS) -TESTPROG= smtpd_token smtpd_check +TESTPROG= smtpd_token smtpd_check smtpd_peer_test PROG = smtpd INC_DIR = ../../include LIBS = ../../lib/lib$(LIB_PREFIX)master$(LIB_SUFFIX) \ @@ -53,18 +53,24 @@ smtpd_check: smtpd_check.o smtpd_check.c $(SMTPD_CHECK_OBJ) $(LIBS) $(LIBS) $(SYSLIBS) mv junk $@.o +SMTPD_PEER_OBJ = smtpd_state.o smtpd_peer.o smtpd_haproxy.o + +smtpd_peer_test: smtpd_peer_test.c $(SMTPD_PEER_OBJ) $(LIBS) + $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(SMTPD_PEER_OBJ) \ + $(LIBS) $(SYSLIBS) + clean: rm -f *.o *core $(PROG) $(TESTPROG) junk *.db *.out *.tmp tidy: clean -broken-tests: smtpd_check_test smtpd_check_test2 +broken-tests: smtpd_check_test smtpd_check_test2 tests: smtpd_acl_test smtpd_addr_valid_test smtpd_exp_test \ smtpd_token_test smtpd_check_test4 smtpd_check_dsn_test \ smtpd_check_backup_test smtpd_dnswl_test smtpd_error_test \ smtpd_server_test smtpd_nullmx_test smtpd_dns_filter_test \ - smtpd_deprecated_test + smtpd_deprecated_test test_smtpd_peer root_tests: @@ -165,6 +171,9 @@ smtpd_deprecated_test: smtpd_check smtpd_deprecated.in smtpd_deprecated.ref diff smtpd_deprecated.ref smtpd_check.tmp rm -f smtpd_check.tmp +test_smtpd_peer: smtpd_peer_test + $(SHLIB_ENV) $(VALGRIND) ./smtpd_peer_test + depend: $(MAKES) (sed '1,/^# do not edit/!d' Makefile.in; \ set -e; for i in [a-z][a-z0-9]*.c; do \ @@ -503,6 +512,36 @@ smtpd_peer.o: ../../include/vstream.h smtpd_peer.o: ../../include/vstring.h smtpd_peer.o: smtpd.h smtpd_peer.o: smtpd_peer.c +smtpd_peer_test.o: ../../include/argv.h +smtpd_peer_test.o: ../../include/attr.h +smtpd_peer_test.o: ../../include/check_arg.h +smtpd_peer_test.o: ../../include/dns.h +smtpd_peer_test.o: ../../include/haproxy_srvr.h +smtpd_peer_test.o: ../../include/htable.h +smtpd_peer_test.o: ../../include/inet_proto.h +smtpd_peer_test.o: ../../include/iostuff.h +smtpd_peer_test.o: ../../include/mail_params.h +smtpd_peer_test.o: ../../include/mail_proto.h +smtpd_peer_test.o: ../../include/mail_stream.h +smtpd_peer_test.o: ../../include/milter.h +smtpd_peer_test.o: ../../include/msg.h +smtpd_peer_test.o: ../../include/msg_vstream.h +smtpd_peer_test.o: ../../include/myaddrinfo.h +smtpd_peer_test.o: ../../include/mymalloc.h +smtpd_peer_test.o: ../../include/name_code.h +smtpd_peer_test.o: ../../include/name_mask.h +smtpd_peer_test.o: ../../include/nvtable.h +smtpd_peer_test.o: ../../include/sock_addr.h +smtpd_peer_test.o: ../../include/stringops.h +smtpd_peer_test.o: ../../include/sys_defs.h +smtpd_peer_test.o: ../../include/tls.h +smtpd_peer_test.o: ../../include/vbuf.h +smtpd_peer_test.o: ../../include/vstream.h +smtpd_peer_test.o: ../../include/vstring.h +smtpd_peer_test.o: smtpd.h +smtpd_peer_test.o: smtpd_chat.h +smtpd_peer_test.o: smtpd_peer_test.c +smtpd_peer_test.o: smtpd_sasl_glue.h smtpd_proxy.o: ../../include/argv.h smtpd_proxy.o: ../../include/attr.h smtpd_proxy.o: ../../include/check_arg.h diff --git a/postfix/src/smtpd/smtpd_haproxy.c b/postfix/src/smtpd/smtpd_haproxy.c index 542c3fe3d..2b1d26915 100644 --- a/postfix/src/smtpd/smtpd_haproxy.c +++ b/postfix/src/smtpd/smtpd_haproxy.c @@ -14,11 +14,16 @@ /* /* The following summarizes what the Postfix SMTP server expects /* from an up-stream proxy adapter. +/* +/* .IP +/* Return -1 in case of error, zero otherwise. In case of error, +/* the caller will clean up any incomplete endpoint info. /* .IP \(bu /* Call smtpd_peer_from_default() if the up-stream proxy /* indicates that the connection is not proxied. In that case, -/* a proxy adapter MUST NOT update any STATE fields: the -/* smtpd_peer_from_default() function will do that instead. +/* a proxy adapter MUST NOT update dynamically-allocated STATE +/* text fields: the smtpd_peer_from_default() function will do +/* that instead. /* .IP \(bu /* Validate protocol, address and port syntax. Permit only /* protocols that are configured with the main.cf:inet_protocols @@ -27,13 +32,17 @@ /* Convert IPv4-in-IPv6 address syntax to IPv4 syntax when /* both IPv6 and IPv4 support are enabled with main.cf:inet_protocols. /* .IP \(bu -/* Update the following session context fields: addr, port, -/* rfc_addr, addr_family, dest_addr, dest_port. The addr_family -/* field applies to the client address. +/* Update the following STATE fields: addr, port, rfc_addr, +/* addr_family, dest_addr, dest_port. The addr_family field applies +/* to the client address. +/* .IP \(bu +/* Either update the STATE binary fields sockaddr and dest_sockaddr, +/* or call smtpd_peer_hostaddr_to_sockaddr() after updating the +/* STATE text fields addr, port, dest_addr, and dest_port. /* .IP \(bu /* Dynamically allocate storage for string information with /* mystrdup(). In case of error, leave unassigned string fields -/* at their initial zero value. +/* at their initial zero value. The caller will clean up. /* .IP \(bu /* Log a clear warning message that explains why a request /* fails. @@ -62,6 +71,9 @@ /* Google, Inc. /* 111 8th Avenue /* New York, NY 10011, USA +/* +/* Wietse Venema +/* porcupine.org /*--*/ /* System library. */ @@ -107,9 +119,15 @@ int smtpd_peer_from_haproxy(SMTPD_STATE *state) msg_warn("haproxy read: timeout error"); return (-1); } - if (haproxy_srvr_receive(vstream_fileno(state->client), &non_proxy, - &smtp_client_addr, &smtp_client_port, - &smtp_server_addr, &smtp_server_port) < 0) { + state->sockaddr_len = sizeof(state->sockaddr); + state->dest_sockaddr_len = sizeof(state->dest_sockaddr); + if (haproxy_srvr_receive_sa(vstream_fileno(state->client), &non_proxy, + &smtp_client_addr, &smtp_client_port, + &smtp_server_addr, &smtp_server_port, + (struct sockaddr *) &state->sockaddr, + &state->sockaddr_len, + (struct sockaddr *) &state->dest_sockaddr, + &state->dest_sockaddr_len) <0) { return (-1); } if (non_proxy) { diff --git a/postfix/src/smtpd/smtpd_peer.c b/postfix/src/smtpd/smtpd_peer.c index b77d5e2a1..3ca3ce595 100644 --- a/postfix/src/smtpd/smtpd_peer.c +++ b/postfix/src/smtpd/smtpd_peer.c @@ -21,12 +21,16 @@ /* are set to "unknown". /* /* Alternatively, the peer address and port may be obtained -/* from a proxy server. +/* from a proxy server or from attributes that postscreen(8) +/* passes to smtpd(8) over local IPC. /* /* This module uses the local name service via getaddrinfo() /* and getnameinfo(). It does not query the DNS directly. /* /* smtpd_peer_init() updates the following fields: +/* .IP flags.SMTPD_FLAG_HANGUP +/* This flag is raised when the program should hang up +/* without reading client input. /* .IP name /* The verified client hostname. This name is represented by /* the string "unknown" when 1) the address->name lookup failed, @@ -36,16 +40,12 @@ /* The unverified client hostname as found with address->name /* lookup; it is not verified for consistency with the client /* IP address result from name->address lookup. -/* .IP forward_name -/* The unverified client hostname as found with address->name -/* lookup followed by name->address lookup; it is not verified -/* for consistency with the result from address->name lookup. -/* For example, when the address->name lookup produces as -/* hostname an alias, the name->address lookup will produce -/* as hostname the expansion of that alias, so that the two -/* lookups produce different names. /* .IP addr /* Printable representation of the client address. +/* .IP addr_family +/* AF_INET or AF_INET6 in case of an open TCP connection. +/* AF_UNSPEC in all other cases, including an open non-socket +/* connection, or a closed connection. /* .IP namaddr /* String of the form: "name[addr]:port". /* .IP rfc_addr @@ -88,19 +88,6 @@ /* .IP 5 /* The address->name lookup failed with an unrecoverable error. /* .RE -/* .IP forward_name_status -/* The forward_name_status result field specifies how the -/* forward_name information should be interpreted: -/* .RS -/* .IP 2 -/* The address->name and name->address lookup succeeded. -/* .IP 4 -/* The address->name lookup or name->address failed with a -/* recoverable error. -/* .IP 5 -/* The address->name lookup or name->address failed with an -/* unrecoverable error. -/* .RE /* .PP /* smtpd_peer_reset() releases memory allocated by smtpd_peer_init(). /* @@ -159,8 +146,6 @@ #include "smtpd.h" -static const INET_PROTO_INFO *proto_info; - /* * XXX If we make local port information available via logging, then we must * also support these attributes with the XFORWARD command. @@ -177,7 +162,7 @@ static int smtpd_peer_sockaddr_to_hostaddr(SMTPD_STATE *state) { const char *myname = "smtpd_peer_sockaddr_to_hostaddr"; struct sockaddr *sa = (struct sockaddr *) &(state->sockaddr); - SOCKADDR_SIZE sa_length = state->sockaddr_len; + SOCKADDR_SIZE *sa_length = &state->sockaddr_len; /* * XXX If we're given an IPv6 (or IPv4) connection from, e.g., inetd, @@ -195,12 +180,11 @@ static int smtpd_peer_sockaddr_to_hostaddr(SMTPD_STATE *state) MAI_HOSTADDR_STR server_addr; MAI_SERVPORT_STR server_port; int aierr; - char *colonp; /* * Sanity check: we can't use sockets that we're not configured for. */ - if (strchr((char *) proto_info->sa_family_list, sa->sa_family) == 0) + if (strchr((char *) inet_proto_info()->sa_family_list, sa->sa_family) == 0) msg_fatal("cannot handle socket type %s with \"%s = %s\"", #ifdef AF_INET6 sa->sa_family == AF_INET6 ? "AF_INET6" : @@ -222,15 +206,15 @@ static int smtpd_peer_sockaddr_to_hostaddr(SMTPD_STATE *state) /* * Convert the client address to printable form. */ - if ((aierr = sockaddr_to_hostaddr(sa, sa_length, &client_addr, - &client_port, 0)) != 0) + if ((aierr = sane_sockaddr_to_hostaddr(sa, sa_length, &client_addr, + &client_port, 0)) != 0) msg_fatal("%s: cannot convert client sockaddr type %s length %ld " "to string: %s", myname, #ifdef AF_INET6 sa->sa_family == AF_INET6 ? "AF_INET6" : #endif sa->sa_family == AF_INET ? "AF_INET" : "other", - (long) sa_length, MAI_STRERROR(aierr)); + (long) *sa_length, MAI_STRERROR(aierr)); state->port = mystrdup(client_port.buf); /* @@ -241,55 +225,20 @@ static int smtpd_peer_sockaddr_to_hostaddr(SMTPD_STATE *state) if (strchr(client_addr.buf, '%') != 0) msg_panic("%s: address %s has datalink suffix", myname, client_addr.buf); -#endif /* - * We convert IPv4-in-IPv6 address to 'true' IPv4 address early on, - * but only if IPv4 support is enabled (why would anyone want to turn - * it off)? With IPv4 support enabled we have no need for the IPv6 - * form in logging, hostname verification and access checks. + * Following RFC 2821 section 4.1.3, an IPv6 address literal gets a + * prefix of 'IPv6:'. We do this consistently for all IPv6 addresses + * that appear in headers or envelopes. The fact that + * valid_mailhost_addr() enforces the form helps of course. We use + * the form without IPV6: prefix when doing access control, or when + * accessing the connection cache. */ -#ifdef HAS_IPV6 if (sa->sa_family == AF_INET6) { - if (strchr((char *) proto_info->sa_family_list, AF_INET) != 0 - && IN6_IS_ADDR_V4MAPPED(&SOCK_ADDR_IN6_ADDR(sa)) - && (colonp = strrchr(client_addr.buf, ':')) != 0) { - struct addrinfo *res0; - - if (msg_verbose > 1) - msg_info("%s: rewriting V4-mapped address \"%s\" to \"%s\"", - myname, client_addr.buf, colonp + 1); - - state->addr = mystrdup(colonp + 1); - state->rfc_addr = mystrdup(colonp + 1); - state->addr_family = AF_INET; - aierr = - hostaddr_to_sockaddr(state->addr, state->port, 0, &res0); - if (aierr) - msg_fatal("%s: cannot convert [%s]:%s to binary: %s", - myname, state->addr, state->port, - MAI_STRERROR(aierr)); - sa_length = res0->ai_addrlen; - if (sa_length > sizeof(state->sockaddr)) - sa_length = sizeof(state->sockaddr); - memcpy((void *) sa, res0->ai_addr, sa_length); - freeaddrinfo(res0); /* 200412 */ - } - - /* - * Following RFC 2821 section 4.1.3, an IPv6 address literal gets - * a prefix of 'IPv6:'. We do this consistently for all IPv6 - * addresses that appear in headers or envelopes. The fact that - * valid_mailhost_addr() enforces the form helps of course. We - * use the form without IPV6: prefix when doing access control, - * or when accessing the connection cache. - */ - else { - state->addr = mystrdup(client_addr.buf); - state->rfc_addr = - concatenate(IPV6_COL, client_addr.buf, (char *) 0); - state->addr_family = sa->sa_family; - } + state->addr = mystrdup(client_addr.buf); + state->rfc_addr = + concatenate(IPV6_COL, client_addr.buf, (char *) 0); + state->addr_family = sa->sa_family; } /* @@ -306,14 +255,13 @@ static int smtpd_peer_sockaddr_to_hostaddr(SMTPD_STATE *state) /* * Convert the server address/port to printable form. */ - if ((aierr = sockaddr_to_hostaddr((struct sockaddr *) - &state->dest_sockaddr, - state->dest_sockaddr_len, - &server_addr, - &server_port, 0)) != 0) - /* TODO: convert IPv4-in-IPv6 to IPv4 form. */ + if ((aierr = sane_sockaddr_to_hostaddr((struct sockaddr *) + &state->dest_sockaddr, + &state->dest_sockaddr_len, + &server_addr, + &server_port, 0)) != 0) msg_fatal("%s: cannot convert server sockaddr type %s length %ld " - "to string: %s", myname, + "to string: %s", myname, #ifdef AF_INET6 state->dest_sockaddr.ss_family == AF_INET6 ? "AF_INET6" : #endif @@ -404,7 +352,7 @@ static void smtpd_peer_sockaddr_to_hostname(SMTPD_STATE *state) REJECT_PEER_NAME(state, SMTPD_PEER_CODE_FORGED); break; } - if (strchr((char *) proto_info->sa_family_list, res->ai_family) == 0) { + if (strchr((char *) inet_proto_info()->sa_family_list, res->ai_family) == 0) { msg_info("skipping address family %d for host %s", res->ai_family, state->name); continue; @@ -425,6 +373,11 @@ static void smtpd_peer_hostaddr_to_sockaddr(SMTPD_STATE *state) struct addrinfo *res; int aierr; + /* + * The client binary address value is provided by non-error peer lookup + * methods. It is used to compute the anvil aggregation prefix for client + * IP addresses. + */ if ((aierr = hostaddr_to_sockaddr(state->addr, state->port, SOCK_STREAM, &res)) != 0) msg_fatal("%s: cannot convert client address '%s' port '%s' to binary: %s", @@ -434,11 +387,26 @@ static void smtpd_peer_hostaddr_to_sockaddr(SMTPD_STATE *state) memcpy((void *) &(state->sockaddr), res->ai_addr, res->ai_addrlen); state->sockaddr_len = res->ai_addrlen; freeaddrinfo(res); + + /* + * The server binary address is provided by non-error peer lookup + * methods. It is currently unused, but it is the result of a hermetic + * conversion, therefore low-risk. + */ + if ((aierr = hostaddr_to_sockaddr(state->dest_addr, state->dest_port, + SOCK_STREAM, &res)) != 0) + msg_fatal("%s: cannot convert server address '%s' port '%s' to binary: %s", + myname, state->dest_addr, state->dest_port, MAI_STRERROR(aierr)); + if (res->ai_addrlen > sizeof(state->dest_sockaddr)) + msg_panic("%s: address length > struct sockaddr_storage", myname); + memcpy((void *) &(state->dest_sockaddr), res->ai_addr, res->ai_addrlen); + state->dest_sockaddr_len = res->ai_addrlen; + freeaddrinfo(res); } -/* smtpd_peer_not_inet - non-socket or non-Internet endpoint */ +/* smtpd_peer_assume_local_client - non-Internet endpoint */ -static void smtpd_peer_not_inet(SMTPD_STATE *state) +static void smtpd_peer_assume_local_client(SMTPD_STATE *state) { /* @@ -448,7 +416,7 @@ static void smtpd_peer_not_inet(SMTPD_STATE *state) state->name = mystrdup("localhost"); state->reverse_name = mystrdup("localhost"); #ifdef AF_INET6 - if (proto_info->sa_family_list[0] == PF_INET6) { + if (inet_proto_info()->sa_family_list[0] == PF_INET6) { state->addr = mystrdup("::1"); /* XXX bogus. */ state->rfc_addr = mystrdup(IPV6_COL "::1"); /* XXX bogus. */ } else @@ -464,13 +432,15 @@ static void smtpd_peer_not_inet(SMTPD_STATE *state) state->dest_addr = mystrdup(state->addr); /* XXX bogus. */ state->dest_port = mystrdup(state->port); /* XXX bogus. */ + + state->sockaddr_len = 0; + state->dest_sockaddr_len = 0; } -/* smtpd_peer_no_client - peer went away, or peer info unavailable */ +/* smtpd_peer_assume_unknown_client - peer went away, or peer info unavailable */ -static void smtpd_peer_no_client(SMTPD_STATE *state) +static void smtpd_peer_assume_unknown_client(SMTPD_STATE *state) { - smtpd_peer_reset(state); state->name = mystrdup(CLIENT_NAME_UNKNOWN); state->reverse_name = mystrdup(CLIENT_NAME_UNKNOWN); state->addr = mystrdup(CLIENT_ADDR_UNKNOWN); @@ -482,58 +452,67 @@ static void smtpd_peer_no_client(SMTPD_STATE *state) state->dest_addr = mystrdup(SERVER_ADDR_UNKNOWN); state->dest_port = mystrdup(SERVER_PORT_UNKNOWN); + + state->sockaddr_len = 0; + state->dest_sockaddr_len = 0; } /* smtpd_peer_from_pass_attr - initialize from attribute hash */ -static void smtpd_peer_from_pass_attr(SMTPD_STATE *state) +static int smtpd_peer_from_pass_attr(SMTPD_STATE *state) { HTABLE *attr = (HTABLE *) vstream_context(state->client); const char *cp; +#define BAD_PASS_ATTR(...) do { \ + msg_warn(__VA_ARGS__); \ + return (-1); \ + } while (0) + /* * Extract the client endpoint information from the attribute hash. */ if ((cp = htable_find(attr, MAIL_ATTR_ACT_CLIENT_ADDR)) == 0) - msg_fatal("missing client address from proxy"); + BAD_PASS_ATTR("missing client address from proxy"); if (strrchr(cp, ':') != 0) { if (valid_ipv6_hostaddr(cp, DO_GRIPE) == 0) - msg_fatal("bad IPv6 client address syntax from proxy: %s", cp); + BAD_PASS_ATTR("bad IPv6 client address syntax from proxy: %s", cp); state->addr = mystrdup(cp); state->rfc_addr = concatenate(IPV6_COL, cp, (char *) 0); state->addr_family = AF_INET6; } else { if (valid_ipv4_hostaddr(cp, DO_GRIPE) == 0) - msg_fatal("bad IPv4 client address syntax from proxy: %s", cp); + BAD_PASS_ATTR("bad IPv4 client address syntax from proxy: %s", cp); state->addr = mystrdup(cp); state->rfc_addr = mystrdup(cp); state->addr_family = AF_INET; } if ((cp = htable_find(attr, MAIL_ATTR_ACT_CLIENT_PORT)) == 0) - msg_fatal("missing client port from proxy"); + BAD_PASS_ATTR("missing client port from proxy"); if (valid_hostport(cp, DO_GRIPE) == 0) - msg_fatal("bad TCP client port number syntax from proxy: %s", cp); + BAD_PASS_ATTR("bad TCP client port number syntax from proxy: %s", cp); state->port = mystrdup(cp); /* * The Dovecot authentication server needs the server IP address. */ if ((cp = htable_find(attr, MAIL_ATTR_ACT_SERVER_ADDR)) == 0) - msg_fatal("missing server address from proxy"); + BAD_PASS_ATTR("missing server address from proxy"); if (valid_hostaddr(cp, DO_GRIPE) == 0) - msg_fatal("bad IPv6 server address syntax from proxy: %s", cp); + BAD_PASS_ATTR("bad IPv6 server address syntax from proxy: %s", cp); state->dest_addr = mystrdup(cp); if ((cp = htable_find(attr, MAIL_ATTR_ACT_SERVER_PORT)) == 0) - msg_fatal("missing server port from proxy"); + BAD_PASS_ATTR("missing server port from proxy"); if (valid_hostport(cp, DO_GRIPE) == 0) - msg_fatal("bad TCP server port number syntax from proxy: %s", cp); + BAD_PASS_ATTR("bad TCP server port number syntax from proxy: %s", cp); state->dest_port = mystrdup(cp); /* * Convert the client address from string to binary form. */ smtpd_peer_hostaddr_to_sockaddr(state); + return (0); } /* smtpd_peer_from_default - try to initialize peer information from socket */ @@ -544,8 +523,8 @@ void smtpd_peer_from_default(SMTPD_STATE *state) /* * The "no client" routine provides surrogate information so that the * application can produce sensible logging when a client disconnects - * before the server wakes up. The "not inet" routine provides surrogate - * state for (presumably) local IPC channels. + * before the server wakes up. The "assume local" routine provides + * surrogate state for open, presumably, local, IPC channels. */ state->sockaddr_len = sizeof(state->sockaddr); state->dest_sockaddr_len = sizeof(state->dest_sockaddr); @@ -556,18 +535,34 @@ void smtpd_peer_from_default(SMTPD_STATE *state) (struct sockaddr *) &state->dest_sockaddr, &state->dest_sockaddr_len) < 0) { if (errno == ENOTSOCK) - smtpd_peer_not_inet(state); + smtpd_peer_assume_local_client(state); else - smtpd_peer_no_client(state); + smtpd_peer_assume_unknown_client(state); } else { if (smtpd_peer_sockaddr_to_hostaddr(state) < 0) - smtpd_peer_not_inet(state); + smtpd_peer_assume_local_client(state); } } +/* smtpd_peer_fall_back_and_hangup - recover after incomplete peer info */ + +static void smtpd_peer_fall_back_and_hangup(SMTPD_STATE *state) +{ + + /* + * Clear incomplete endpoint info. Populate the SMTPD_STATE with default + * endpoint info, so that the caller won't trip over a null pointer. Hang + * up before accepting input: we don't know what we're talking to and + * what rights they might have. + */ + smtpd_peer_reset(state); + smtpd_peer_assume_unknown_client(state); + state->flags |= SMTPD_FLAG_HANGUP; +} + /* smtpd_peer_from_proxy - get endpoint info from proxy agent */ -static void smtpd_peer_from_proxy(SMTPD_STATE *state) +static int smtpd_peer_from_proxy(SMTPD_STATE *state) { typedef struct { const char *name; @@ -575,6 +570,11 @@ static void smtpd_peer_from_proxy(SMTPD_STATE *state) } SMTPD_ENDPT_LOOKUP_INFO; static const SMTPD_ENDPT_LOOKUP_INFO smtpd_endpt_lookup_info[] = { HAPROXY_PROTO_NAME, smtpd_peer_from_haproxy, + + /* + * See smtpd_haproxy.c for the a summary of the information that a + * proxy endpoint lookup function is expected to provide. + */ 0, }; const SMTPD_ENDPT_LOOKUP_INFO *pp; @@ -588,13 +588,7 @@ static void smtpd_peer_from_proxy(SMTPD_STATE *state) msg_fatal("unsupported %s value: %s", VAR_SMTPD_UPROXY_PROTO, var_smtpd_uproxy_proto); if (strcmp(var_smtpd_uproxy_proto, pp->name) == 0) - break; - } - if (pp->endpt_lookup(state) < 0) { - smtpd_peer_from_default(state); - state->flags |= SMTPD_FLAG_HANGUP; - } else { - smtpd_peer_hostaddr_to_sockaddr(state); + return (pp->endpt_lookup(state)); } } @@ -602,13 +596,6 @@ static void smtpd_peer_from_proxy(SMTPD_STATE *state) void smtpd_peer_init(SMTPD_STATE *state) { - int af; - - /* - * Initialize. - */ - if (proto_info == 0) - proto_info = inet_proto_info(); /* * Prepare for partial initialization after error. @@ -629,19 +616,25 @@ void smtpd_peer_init(SMTPD_STATE *state) /* * Determine the remote SMTP client address and port. * + * If we can't process the connection hand-off info from postscreen or + * proxy, fall back to some default endpoint info for logging and force a + * hangup. We can't determine what rights the peer should have. + * * XXX In stand-alone mode, don't assume that the peer will be a local * process. That could introduce a gaping hole when the SMTP daemon is * hooked up to the network via inetd or some other super-server. */ if (vstream_context(state->client) != 0) { - smtpd_peer_from_pass_attr(state); + if (smtpd_peer_from_pass_attr(state) < 0) + smtpd_peer_fall_back_and_hangup(state); if (*var_smtpd_uproxy_proto != 0) msg_warn("ignoring non-empty %s setting behind postscreen", VAR_SMTPD_UPROXY_PROTO); } else if (SMTPD_STAND_ALONE(state) || *var_smtpd_uproxy_proto == 0) { smtpd_peer_from_default(state); } else { - smtpd_peer_from_proxy(state); + if (smtpd_peer_from_proxy(state) < 0) + smtpd_peer_fall_back_and_hangup(state); } /* @@ -659,14 +652,14 @@ void smtpd_peer_init(SMTPD_STATE *state) state->port); /* - * Generate 'address' or 'net/mask' index for anvil event aggregation. - * Don't do this for non-socket input. See smtpd_peer_not_inet(). + * Generate the 'address' or 'net/mask' index for anvil event + * aggregation. */ + if (state->addr_family != AF_UNSPEC) { - af = SOCK_ADDR_FAMILY(&(state->sockaddr)); - state->anvil_range = inet_prefix_top(af, + state->anvil_range = inet_prefix_top(state->addr_family, SOCK_ADDR_ADDRP(&(state->sockaddr)), - af == AF_INET ? + state->addr_family == AF_INET ? var_smtpd_cipv4_prefix : var_smtpd_cipv6_prefix); } @@ -676,22 +669,24 @@ void smtpd_peer_init(SMTPD_STATE *state) void smtpd_peer_reset(SMTPD_STATE *state) { +#define MYFREE_AND_ZERO(e) do { myfree(e); (e) = 0; } while (0); + if (state->name) - myfree(state->name); + MYFREE_AND_ZERO(state->name); if (state->reverse_name) - myfree(state->reverse_name); + MYFREE_AND_ZERO(state->reverse_name); if (state->addr) - myfree(state->addr); + MYFREE_AND_ZERO(state->addr); if (state->namaddr) - myfree(state->namaddr); + MYFREE_AND_ZERO(state->namaddr); if (state->rfc_addr) - myfree(state->rfc_addr); + MYFREE_AND_ZERO(state->rfc_addr); if (state->port) - myfree(state->port); + MYFREE_AND_ZERO(state->port); if (state->dest_addr) - myfree(state->dest_addr); + MYFREE_AND_ZERO(state->dest_addr); if (state->dest_port) - myfree(state->dest_port); + MYFREE_AND_ZERO(state->dest_port); if (state->anvil_range) - myfree(state->anvil_range); + MYFREE_AND_ZERO(state->anvil_range); } diff --git a/postfix/src/smtpd/smtpd_peer_test.c b/postfix/src/smtpd/smtpd_peer_test.c new file mode 100644 index 000000000..d1e42cc48 --- /dev/null +++ b/postfix/src/smtpd/smtpd_peer_test.c @@ -0,0 +1,715 @@ +/*++ +/* NAME +/* smtpd_peer_test 1t +/* SUMMARY +/* smtpd_peer_init() unit tests +/* SYNOPSIS +/* ./smtpd_peer_test +/* DESCRIPTION +/* Verifies that smtpd_peer_init() will update the SMTPD_STATE +/* structure with the expected error or endpoint information for +/* different input sources. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* porcupine.org +/*--*/ + + /* + * System library. + */ +#include + + /* + * Utility library. + */ +#include +#include +#include +#include +#include + + /* + * Global library. + */ +#include +#include +#include +#include + + /* + * TODO(wietse) make this a proper VSTREAM interface or test helper API. + */ + +/* vstream_swap - capture output for testing */ + +static void vstream_swap(VSTREAM *one, VSTREAM *two) +{ + VSTREAM save; + + save = *one; + *one = *two; + *two = save; +} + + /* + * Application-specific. + */ +#include + +int tests_failed = 0; +int tests_passed = 0; + + /* + * Fakes to satisfy some dependencies. + */ +#include +#include + +#define TEST_TIMEOUT 10 +char *var_smtpd_uproxy_proto = ""; +int var_smtpd_uproxy_tmout = TEST_TIMEOUT; +bool var_smtpd_peername_lookup; +bool var_smtpd_client_port_log; +bool var_smtpd_sasl_enable; +int var_smtpd_cipv4_prefix = DEF_SMTPD_CIPV4_PREFIX; +int var_smtpd_cipv6_prefix = DEF_SMTPD_CIPV6_PREFIX; +char *var_notify_classes = DEF_NOTIFY_CLASSES; +void smtpd_chat_reset(SMTPD_STATE *state) +{ +} +void smtpd_sasl_state_init(SMTPD_STATE *state) +{ +} +void smtpd_xforward_init(SMTPD_STATE *state) +{ +} + +/* Reset globals that may be tweaked by individual tests */ + +static void reset_global_variables(void) +{ + var_smtpd_uproxy_proto = ""; + inet_proto_init("reset_global_variables", "all"); +} + + /* + * Basic tests that smtpd_peer_init() will update the SMTPD_STATE structure + * with the expected error info or endpoint info. This needs to be subclassed + * to support different input sources (local client, no open connection, + * HaProxy, postscreen, etc.). + */ +typedef struct TEST_BASE { + const char *label; + int want_hangup; + const char *want_warning; + const char *want_client_name; + int want_client_name_status; + const char *want_client_reverse_name; + int want_client_reverse_name_status; + const char *want_client_addr; + const char *want_client_rfc_addr; + const char *want_client_port; + int want_client_addr_family; + const char *want_server_addr; + const char *want_server_port; + int want_sockaddr_len; + int want_dest_sockaddr_len; +} TEST_BASE; + +/* test_smtpd_peer_init - enforce smtpd_peer_init() expectations */ + +static int test_smtpd_peer_init(const TEST_BASE *tp, VSTREAM *fp, + SMTPD_STATE *state) +{ + VSTRING *msg_buf = vstring_alloc(100); + VSTREAM *memory_stream; + int test_passed = 1; + int aierr; + MAI_HOSTADDR_STR got_addr; + MAI_SERVPORT_STR got_port; + + /* + * Detonate smtpd_state_init() in a little sandbox. + */ + VSTRING_RESET(msg_buf); + VSTRING_TERMINATE(msg_buf); + if ((memory_stream = vstream_memopen(msg_buf, O_WRONLY)) == 0) + msg_fatal("open memory stream: %m"); + vstream_swap(VSTREAM_ERR, memory_stream); + smtpd_state_init(state, fp, "something"); + vstream_swap(memory_stream, VSTREAM_ERR); + (void) vstream_fclose(memory_stream); + + /* Verify the results. */ + if (tp->want_hangup != (state->flags & SMTPD_FLAG_HANGUP)) { + msg_warn("got hangup flag '0x%x', want '0x%x'", + state->flags & SMTPD_FLAG_HANGUP, tp->want_hangup); + test_passed = 0; + } else if (tp->want_warning == 0 && VSTRING_LEN(msg_buf) > 0) { + msg_warn("got warning ``%s'', want ``null''", vstring_str(msg_buf)); + test_passed = 0; + } else if (tp->want_warning != 0) { + if (strstr(vstring_str(msg_buf), tp->want_warning) == 0) { + msg_warn("got warning ``%s'', want ``%s''", + vstring_str(msg_buf), tp->want_warning); + test_passed = 0; + } + } + if (test_passed == 0) { + /* void */ ; + } else if (tp->want_client_name + && strcmp(state->name, tp->want_client_name) != 0) { + msg_warn("got client name '%s', want '%s'", + state->name, tp->want_client_name); + test_passed = 0; + } else if (tp->want_client_name_status != 0 + && state->name_status != tp->want_client_name_status) { + msg_warn("got client name status '%d', want '%d'", + state->name_status, tp->want_client_name_status); + test_passed = 0; + } else if (tp->want_client_reverse_name + && strcmp(state->reverse_name, tp->want_client_reverse_name) != 0) { + msg_warn("got client reverse name '%s', want '%s'", + state->reverse_name, tp->want_client_reverse_name); + test_passed = 0; + } else if (tp->want_client_reverse_name_status != 0 + && state->reverse_name_status != tp->want_client_reverse_name_status) { + msg_warn("got client reverse name status '%d', want '%d'", + state->reverse_name_status, tp->want_client_reverse_name_status); + test_passed = 0; + } else if (tp->want_client_addr + && strcmp(state->addr, tp->want_client_addr) != 0) { + msg_warn("got text client address '%s', want '%s'", + state->addr, tp->want_client_addr); + test_passed = 0; + } if (tp->want_client_rfc_addr + && strcmp(tp->want_client_rfc_addr, state->rfc_addr) != 0) { + msg_warn("got client rfc_addr '%s', want '%s'", + state->rfc_addr, tp->want_client_rfc_addr); + test_passed = 0; + } else if (tp->want_client_port + && strcmp(state->port, tp->want_client_port) != 0) { + msg_warn("got text client port '%s', want '%s'", + state->port, tp->want_client_port); + test_passed = 0; + } else if (!state->sockaddr_len != !tp->want_sockaddr_len) { + msg_warn("got sockaddr_len '%d', want '%d'", + (int) state->sockaddr_len, (int) tp->want_sockaddr_len); + test_passed = 0; + } else if (tp->want_server_addr + && strcmp(state->dest_addr, tp->want_server_addr) != 0) { + msg_warn("got text server address '%s', want '%s'", + state->dest_addr, tp->want_server_addr); + test_passed = 0; + } else if (tp->want_server_port + && strcmp(state->dest_port, tp->want_server_port) != 0) { + msg_warn("got text server port '%s', want '%s'", + state->dest_port, tp->want_server_port); + test_passed = 0; + } else if (!state->dest_sockaddr_len != !tp->want_dest_sockaddr_len) { + msg_warn("got dest_sockaddr_len '%d', want '%d'", + (int) state->dest_sockaddr_len, (int) tp->want_dest_sockaddr_len); + test_passed = 0; + } else { + if (state->sockaddr_len > 0) { + if ((aierr = + sockaddr_to_hostaddr((struct sockaddr *) &state->sockaddr, + state->sockaddr_len, &got_addr, &got_port, 0)) != 0) { + msg_warn("sockaddr_to_hostaddr: %s", MAI_STRERROR(aierr)); + test_passed = 0; + } else if (tp->want_client_addr + && strcmp(got_addr.buf, tp->want_client_addr) != 0) { + msg_warn("got binary client address '%s', want '%s'", + got_addr.buf, tp->want_client_addr); + test_passed = 0; + } else if (tp->want_client_port + && strcmp(got_port.buf, tp->want_client_port) != 0) { + msg_warn("got binary client port '%s', want '%s'", + got_port.buf, tp->want_client_port); + test_passed = 0; + } + } + if (state->dest_sockaddr_len > 0) { + if ((aierr = + sockaddr_to_hostaddr((struct sockaddr *) &state->dest_sockaddr, + state->dest_sockaddr_len, &got_addr, &got_port, 0)) != 0) { + msg_warn("sockaddr_to_hostaddr: %s", MAI_STRERROR(aierr)); + test_passed = 0; + } else if (tp->want_server_addr + && strcmp(got_addr.buf, tp->want_server_addr) != 0) { + msg_warn("got binary server address '%s', want '%s'", + got_addr.buf, tp->want_server_addr); + test_passed = 0; + } else if (tp->want_server_port + && strcmp(got_port.buf, tp->want_server_port) != 0) { + msg_warn("got binary server port '%s', want '%s'", + got_port.buf, tp->want_server_port); + test_passed = 0; + } + } + } + (void) vstring_free(msg_buf); + smtpd_state_reset(state); + return (test_passed); +} + + /* + * Tests that a non-socket client results in fake localhost info. + */ +typedef struct PEER_FROM_NON_SOCKET_CASE { + TEST_BASE base; + const char *inet_protocols; +} PEER_FROM_NON_SOCKET_CASE; + +static const PEER_FROM_NON_SOCKET_CASE peer_from_non_socket_cases[] = { + { + .base = { + .label = "prefer_ipv4", + .want_client_name = "localhost", + .want_client_name_status = SMTPD_PEER_CODE_OK, + .want_client_addr = "127.0.0.1", + .want_client_addr_family = AF_UNSPEC, + .want_client_rfc_addr = "127.0.0.1", + .want_client_reverse_name_status = SMTPD_PEER_CODE_OK, + .want_client_port = "0", + .want_server_addr = "127.0.0.1", + .want_server_port = "0", + }, + .inet_protocols = "ipv4", + }, + { + .base = { + .label = "prefer_ipv6", + .want_client_name = "localhost", + .want_client_name_status = SMTPD_PEER_CODE_OK, + .want_client_reverse_name = "localhost", + .want_client_reverse_name_status = SMTPD_PEER_CODE_OK, + .want_client_addr = "::1", + .want_client_addr_family = AF_UNSPEC, + .want_client_rfc_addr = "IPv6:::1", + .want_client_port = "0", + .want_server_addr = "::1", + .want_server_port = "0", + }, + .inet_protocols = "ipv6", + }, + {0}, +}; + +static void test_peer_from_non_socket(void) +{ + int test_passed; + const PEER_FROM_NON_SOCKET_CASE *tp; + + reset_global_variables(); + + for (tp = peer_from_non_socket_cases; tp->base.label != 0; tp++) { + msg_info("RUN test_peer_from_non_socket/%s", tp->base.label); + { + SMTPD_STATE state; + VSTREAM *fp; + int pair[2]; + + if (pipe(pair) < 0) + msg_fatal("pipe: %m"); + if ((fp = vstream_fdopen(pair[0], O_RDONLY)) == 0) + msg_fatal("vstream_fdopen: %m"); + (void) inet_proto_init("test_peer_from_non_socket", + tp->inet_protocols); + + test_passed = test_smtpd_peer_init((TEST_BASE *) tp, fp, &state); + + (void) vstream_fdclose(fp); + (void) close(pair[0]); + (void) close(pair[1]); + } + if (test_passed) { + msg_info("PASS test_peer_from_non_socket/%s", tp->base.label); + tests_passed += 1; + } else { + msg_info("FAIL test_peer_from_non_socket/%s", tp->base.label); + tests_failed += 1; + } + } +} + + /* + * Tests that a non-connected socket results in 'unknown' endpoint info. + */ +typedef struct PEER_FROM_UNCONN_SOCKET_CASE { + TEST_BASE base; + int proto_family; +} PEER_FROM_UNCONN_SOCKET_CASE; + +static const PEER_FROM_UNCONN_SOCKET_CASE peer_from_unconn_socket_cases[] = { + { + .base = { + .label = "tcp4", + .want_client_name = CLIENT_NAME_UNKNOWN, + .want_client_name_status = SMTPD_PEER_CODE_PERM, + .want_client_addr = CLIENT_ADDR_UNKNOWN, + .want_client_addr_family = AF_UNSPEC, + .want_client_rfc_addr = CLIENT_ADDR_UNKNOWN, + .want_client_reverse_name = CLIENT_NAME_UNKNOWN, + .want_client_reverse_name_status = SMTPD_PEER_CODE_PERM, + .want_client_port = CLIENT_PORT_UNKNOWN, + .want_server_addr = SERVER_ADDR_UNKNOWN, + .want_server_port = SERVER_PORT_UNKNOWN, + }, + .proto_family = PF_INET, + }, + { + .base = { + .label = "tcp6", + .want_client_name = CLIENT_NAME_UNKNOWN, + .want_client_name_status = SMTPD_PEER_CODE_PERM, + .want_client_addr = CLIENT_ADDR_UNKNOWN, + .want_client_addr_family = AF_UNSPEC, + .want_client_rfc_addr = CLIENT_ADDR_UNKNOWN, + .want_client_reverse_name = CLIENT_NAME_UNKNOWN, + .want_client_reverse_name_status = SMTPD_PEER_CODE_PERM, + .want_client_port = CLIENT_PORT_UNKNOWN, + .want_server_addr = SERVER_ADDR_UNKNOWN, + .want_server_port = SERVER_PORT_UNKNOWN, + }, + .proto_family = PF_INET6, + }, + { + .base = { + .label = "unix", + .want_client_name = CLIENT_NAME_UNKNOWN, + .want_client_name_status = SMTPD_PEER_CODE_PERM, + .want_client_addr = CLIENT_ADDR_UNKNOWN, + .want_client_addr_family = AF_UNSPEC, + .want_client_rfc_addr = CLIENT_ADDR_UNKNOWN, + .want_client_reverse_name = CLIENT_NAME_UNKNOWN, + .want_client_reverse_name_status = SMTPD_PEER_CODE_PERM, + .want_client_port = CLIENT_PORT_UNKNOWN, + .want_server_addr = SERVER_ADDR_UNKNOWN, + .want_server_port = SERVER_PORT_UNKNOWN, + }, + .proto_family = PF_UNIX, + }, + {0}, +}; + +static void test_peer_from_unconn_socket(void) +{ + int test_passed; + const PEER_FROM_UNCONN_SOCKET_CASE *tp; + + reset_global_variables(); + + for (tp = peer_from_unconn_socket_cases; tp->base.label != 0; tp++) { + msg_info("RUN test_peer_from_unconn_socket/%s", tp->base.label); + { + SMTPD_STATE state; + VSTREAM *fp; + int sock; + + if ((sock = socket(tp->proto_family, SOCK_STREAM, 0)) < 0) + msg_fatal("socketpair: %m"); + if ((fp = vstream_fdopen(sock, O_RDONLY)) == 0) + msg_fatal("vstream_fdopen: %m"); + + test_passed = test_smtpd_peer_init((TEST_BASE *) tp, fp, &state); + + (void) vstream_fclose(fp); + } + if (test_passed) { + msg_info("PASS test_peer_from_unconn_socket/%s", tp->base.label); + tests_passed += 1; + } else { + msg_info("FAIL test_peer_from_unconn_socket/%s", tp->base.label); + tests_failed += 1; + } + } +} + + /* + * Tests that smtpd_peer_from_pass_attr() updates the SMTPD_STATE structure + * with the expected error or endpoint information. + */ +#define PASS_ATTR_COUNT 5 +struct PASS_ATTR { + const char *key; + const char *value; +}; +typedef struct PEER_FROM_PASS_ATTR_CASE { + const TEST_BASE base; /* parent class */ + struct PASS_ATTR attrs[PASS_ATTR_COUNT]; +} PEER_FROM_PASS_ATTR_CASE; + + /* + * We need one test for every constraint in smtpd_peer_from_pass(), to + * demonstrate that smtpd_pass_attr.c propagates errors and endpoint info. + */ +static const PEER_FROM_PASS_ATTR_CASE peer_from_pass_attr_cases[] = { + { + .base = { + .label = "propagates_endpoint_info_from_good_pass_attr", + .want_client_addr = "1.2.3.4", + .want_client_port = "123", + .want_client_addr_family = AF_INET, + .want_server_addr = "4.3.2.1", + .want_server_port = "321", + .want_sockaddr_len = 1, + .want_dest_sockaddr_len = 1, + }, + .attrs = { + MAIL_ATTR_ACT_CLIENT_ADDR, "1.2.3.4", + MAIL_ATTR_ACT_CLIENT_PORT, "123", + MAIL_ATTR_ACT_SERVER_ADDR, "4.3.2.1", + MAIL_ATTR_ACT_SERVER_PORT, "321", + }, + }, + { + .base = { + .label = "propagates_error_from_bad_IPv4_client_addr", + .want_hangup = 1, + .want_warning = "bad IPv4 client address", + /* TODO(wietse) Should we verify the surrogate endpoint info? */ + }, + .attrs = { + MAIL_ATTR_ACT_CLIENT_ADDR, "1.1.2.3.4", + MAIL_ATTR_ACT_CLIENT_PORT, "123", + MAIL_ATTR_ACT_SERVER_ADDR, "4.3.2.1", + MAIL_ATTR_ACT_SERVER_PORT, "321", + }, + }, + { + .base = { + .label = "propagates_error_from_missing_client_addr", + .want_hangup = 1, + .want_warning = "missing client address", + }, + .attrs = { + MAIL_ATTR_ACT_CLIENT_PORT, "123", + MAIL_ATTR_ACT_SERVER_ADDR, "4.3.2.1", + MAIL_ATTR_ACT_SERVER_PORT, "321", + }, + }, + { + .base = { + .label = "propagates_error_from_bad_TCP_client_port", + .want_hangup = 1, + .want_warning = "bad TCP client port", + }, + .attrs = { + MAIL_ATTR_ACT_CLIENT_ADDR, "1.2.3.4", + MAIL_ATTR_ACT_CLIENT_PORT, "A23", + MAIL_ATTR_ACT_SERVER_ADDR, "4.3.2.1", + MAIL_ATTR_ACT_SERVER_PORT, "321", + }, + }, + { + .base = { + .label = "propagates_error_from_missing_client_port", + .want_hangup = 1, + .want_warning = "missing client port", + }, + .attrs = { + MAIL_ATTR_ACT_CLIENT_ADDR, "1.2.3.4", + MAIL_ATTR_ACT_SERVER_ADDR, "4.3.2.1", + MAIL_ATTR_ACT_SERVER_PORT, "321", + }, + }, + { + .base = { + .label = "propagates_error_from_bad_IPv6_server_addr", + .want_hangup = 1, + .want_warning = "bad IPv6 server address", + }, + .attrs = { + MAIL_ATTR_ACT_CLIENT_ADDR, "1.2.3.4", + MAIL_ATTR_ACT_CLIENT_PORT, "123", + MAIL_ATTR_ACT_SERVER_ADDR, ":::4.3.2.1", + MAIL_ATTR_ACT_SERVER_PORT, "321", + }, + }, + { + .base = { + .label = "propagates_error_from_missing_server_addr", + .want_hangup = 1, + .want_warning = "missing server address", + }, + .attrs = { + MAIL_ATTR_ACT_CLIENT_ADDR, "1.2.3.4", + MAIL_ATTR_ACT_CLIENT_PORT, "123", + MAIL_ATTR_ACT_SERVER_PORT, "321", + }, + }, + { + .base = { + .label = "propagates_error_from_bad_TCP_server_port", + .want_hangup = 1, + .want_warning = "bad TCP server port", + }, + .attrs = { + MAIL_ATTR_ACT_CLIENT_ADDR, "1.2.3.4", + MAIL_ATTR_ACT_CLIENT_PORT, "123", + MAIL_ATTR_ACT_SERVER_ADDR, "4.3.2.1", + MAIL_ATTR_ACT_SERVER_PORT, "A21", + }, + }, + { + .base = { + .label = "propagates_error_from_missing_server_port", + .want_hangup = 1, + .want_warning = "missing server port", + }, + .attrs = { + MAIL_ATTR_ACT_CLIENT_ADDR, "1.2.3.4", + MAIL_ATTR_ACT_CLIENT_PORT, "123", + MAIL_ATTR_ACT_SERVER_ADDR, "4.3.2.1", + }, + }, + 0, +}; + +static void test_peer_from_pass_attr(void) +{ + int test_passed; + const PEER_FROM_PASS_ATTR_CASE *tp; + + reset_global_variables(); + + for (tp = peer_from_pass_attr_cases; tp->base.label != 0; tp++) { + msg_info("RUN test_peer_from_pass_attr/%s", tp->base.label); + { + SMTPD_STATE state; + VSTREAM *fp; + int sock; + HTABLE *attr_table = htable_create(PASS_ATTR_COUNT); + const struct PASS_ATTR *p; + + if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) + msg_fatal("socket: %m"); + if ((fp = vstream_fdopen(sock, O_RDWR)) == 0) + msg_fatal("vstream_fdopen: %m"); + for (p = tp->attrs; p < tp->attrs + PASS_ATTR_COUNT && p->key != 0; p++) + htable_enter(attr_table, p->key, (void *) p->value); + vstream_control(fp, + VSTREAM_CTL_CONTEXT, (void *) attr_table, + VSTREAM_CTL_END); + + test_passed = test_smtpd_peer_init((TEST_BASE *) tp, fp, &state); + + htable_free(attr_table, (void (*) (void *)) 0); + (void) vstream_fclose(fp); + } + if (test_passed) { + msg_info("PASS test_peer_from_pass_attr/%s", tp->base.label); + tests_passed += 1; + } else { + msg_info("FAIL test_peer_from_pass_attr/%s", tp->base.label); + tests_failed += 1; + } + } +} + + /* + * Tests that smtpd_peer_from_haproxy() updates the SMTPD_STATE structure + * with the expected error or endpoint information. + */ +typedef struct PEER_FROM_HAPROXY_CASE { + const TEST_BASE base; + const char *proxy_header; +} PEER_FROM_HAPROXY_CASE; + + /* + * We need only two tests to show that smtpd_haproxy.c propagates errors and + * non-error endpoint info. We don't need to duplicate each individual test in + * haproxy_srvr_test.c for different IP protocols, HaProxy protocol + * versions, and error modes. + */ +static const PEER_FROM_HAPROXY_CASE peer_from_haproxy_caes[] = { + { + .base = { + .label = "propagates_endpoint_info_from_good_proxy_header", + .want_client_addr = "1.2.3.4", + .want_client_port = "123", + .want_client_addr_family = AF_INET, + .want_server_addr = "4.3.2.1", + .want_server_port = "321", + .want_sockaddr_len = 1, + .want_dest_sockaddr_len = 1, + }, + .proxy_header = "PROXY TCP4 1.2.3.4 4.3.2.1 123 321\n", + }, + { + .base = { + .label = "propagates_error_from_bad_proxy_header", + .want_hangup = 1, + .want_warning = "short protocol header", + /* TODO(wietse) Should we verify the surrogate endpoint info? */ + }, + .proxy_header = "bad", + }, + 0, +}; + +static void test_peer_from_haproxy(void) +{ + int test_passed; + const PEER_FROM_HAPROXY_CASE *tp; + + reset_global_variables(); + var_smtpd_uproxy_proto = HAPROXY_PROTO_NAME; + + for (tp = peer_from_haproxy_caes; tp->proxy_header != 0; tp++) { + msg_info("RUN test_peer_from_haproxy/%s", tp->base.label); + { + SMTPD_STATE state; + VSTREAM *fp; + int pair[2]; + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) + msg_fatal("socketpair: %m"); + if (write_buf(pair[1], tp->proxy_header, strlen(tp->proxy_header), + TEST_TIMEOUT) < 0) + msg_fatal("write_buf: %m"); + if ((fp = vstream_fdopen(pair[0], O_RDONLY)) == 0) + msg_fatal("vstream_fdopen: %m"); + + test_passed = test_smtpd_peer_init((TEST_BASE *) tp, fp, &state); + + (void) vstream_fdclose(fp); + (void) close(pair[0]); + (void) close(pair[1]); + } + if (test_passed) { + msg_info("PASS test_peer_from_haproxy/%s", tp->base.label); + tests_passed += 1; + } else { + msg_info("FAIL test_peer_from_haproxy/%s", tp->base.label); + tests_failed += 1; + } + } +} + +int main(int argc, char **argv) +{ + msg_vstream_init(sane_basename((VSTRING *) 0, argv[0]), VSTREAM_ERR); + + test_peer_from_non_socket(); + + test_peer_from_unconn_socket(); + + test_peer_from_pass_attr(); + + test_peer_from_haproxy(); + + /* + * TODO(wietse) tests for a connected socket. This will require mock + * get_peername/get/sockname() and getnameinfo/getaddrinfo() + * infrastructure. + */ + + msg_info("PASS=%d FAIL=%d", tests_passed, tests_failed); + exit(tests_failed != 0); +}