]> git.ipfire.org Git - thirdparty/postfix.git/commitdiff
postfix-3.11-20250418
authorWietse Z Venema <wietse@porcupine.org>
Fri, 18 Apr 2025 05:00:00 +0000 (00:00 -0500)
committerViktor Dukhovni <ietf-dane@dukhovni.org>
Sat, 19 Apr 2025 11:27:00 +0000 (21:27 +1000)
13 files changed:
postfix/.indent.pro
postfix/HISTORY
postfix/Makefile.in
postfix/README_FILES/XCLIENT_README
postfix/html/XCLIENT_README.html
postfix/proto/XCLIENT_README.html
postfix/proto/stop
postfix/proto/stop.spell-history
postfix/src/global/mail_version.h
postfix/src/smtpd/Makefile.in
postfix/src/smtpd/smtpd_haproxy.c
postfix/src/smtpd/smtpd_peer.c
postfix/src/smtpd/smtpd_peer_test.c [new file with mode: 0644]

index ebb7b727011462cb5f25a09fa8125fc8c17cb723..f1671a6ec4e4207256e2cfa6c1d9aed1784061b7 100644 (file)
 -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
 -TSTRING_LIST
 -TSTRING_TABLE
 -TSYS_EXITS_DETAIL
+-TTEST_BASE
 -TTEST_CASE
 -TTLSMGR_SCACHE
 -TTLSP_STATE
index 449cbad12ba626157a12d1ad9caa3aa13087e667..489e0f9e98acd75fea2f22a1d5bc24fadd9aa9aa 100644 (file)
@@ -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.
index a37f89f3b502225329bd64b8a7fd58e548f58ec2..12789cad7eb8298cee9299c643de229448375510 100644 (file)
@@ -115,7 +115,8 @@ manpages:
        done </dev/null
 
 # Some checks require a bin/postconf executable.
-pre-release-checks: typo-check double-check missing-proxy-read-maps-check \
+pre-release-checks: update typo-check double-check \
+       missing-proxy-read-maps-check \
        postlink-check postfix-files-check \
        postconf-unimplemented-check postconf-undocumented-check \
        check-table-proto check-see-postconf-d-output \
index 89b11bff6d83a89da98cc3b2e810663915701ada..28d3dee42d5a077d5fd2c0ef3d343b1ce602a61f 100644 (file)
@@ -100,8 +100,8 @@ Note 3: Postfix implementations prior to version 2.3 do not 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.
 
 X\bXC\bCL\bLI\bIE\bEN\bNT\bT S\bSe\ber\brv\bve\ber\br r\bre\bes\bsp\bpo\bon\bns\bse\be
 
index 96897f99ae20dfe9eff70e20f47a7c656283a512..c8131635c257325b78c252394df5214fcfbf2233 100644 (file)
@@ -145,8 +145,8 @@ xtext encode attribute values. Servers that wish to interoperate
 with these older implementations should be prepared to receive
 unencoded information. </p>
 
-<p> Note 4: Some Postfix implementations do not implement the PORT
-or LOGIN attributes.  </p>
+<p> Note 4: The PORT attribute is implemented in Postfix 2.5 and
+later; the LOGIN attribute in Postfix 2.9 and later. </p>
 
 <h2>XCLIENT Server response</h2>
 
index 36a42bc91542b564bcde79e08ead892202210cd3..ed228f49f9b4df912e3d5c1a97631c9aa12da1fd 100644 (file)
@@ -145,8 +145,8 @@ xtext encode attribute values. Servers that wish to interoperate
 with these older implementations should be prepared to receive
 unencoded information. </p>
 
-<p> Note 4: Some Postfix implementations do not implement the PORT
-or LOGIN attributes.  </p>
+<p> Note 4: The PORT attribute is implemented in Postfix 2.5 and
+later; the LOGIN attribute in Postfix 2.9 and later. </p>
 
 <h2>XCLIENT Server response</h2>
 
index 3ac8e4bb8d2cd6b6914439d037cbb8cd1ad6d341..7c06d7ee4c5561187b8a3f60d73e78f67e58d9c1 100644 (file)
@@ -1673,3 +1673,4 @@ bugfix
 MLKEM
 cleartext
 redacted
+subclassed
index 1a66bdb7f69fc212fb5c39579b9ee6d10b5ce7e1..da067ab974517b75105dc61bace894f6d157a845 100644 (file)
@@ -105,3 +105,4 @@ Oemer
 Kozmenko
 Oleksandr
 Bataille
+balancers
index 5e48134915070cbd6a5f3d03eb6c026f2986ff0b..1d3ab0d81f265cd07bca339d88cf462cfec813cd 100644 (file)
@@ -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
index 139918f4138bf42a233e235e2980b6670eb24f0c..5bd1a78f6c29404478fcd0b4abac6530a1e03cb4 100644 (file)
@@ -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
index 542c3fe3d5e3e724bc46e32df3d68444c66d1e2a..2b1d269155943b7c8a471df5d62a9f77a4f8362d 100644 (file)
 /*
 /*     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
 /*     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) {
index b77d5e2a18ed379d53eae2b766ba428e398a59a3..3ca3ce5955697fd93eeee0b3ec1b6e336b3f7147 100644 (file)
 /*     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,
 /*     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
 /* .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().
 /*
 
 #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 (file)
index 0000000..d1e42cc
--- /dev/null
@@ -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 <sys_defs.h>
+
+ /*
+  * Utility library.
+  */
+#include <htable.h>
+#include <msg.h>
+#include <msg_vstream.h>
+#include <stringops.h>
+#include <vstream.h>
+
+ /*
+  * Global library.
+  */
+#include <haproxy_srvr.h>
+#include <inet_proto.h>
+#include <mail_params.h>
+#include <mail_proto.h>
+
+ /*
+  * 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 <smtpd.h>
+
+int     tests_failed = 0;
+int     tests_passed = 0;
+
+ /*
+  * Fakes to satisfy some dependencies.
+  */
+#include <smtpd_chat.h>
+#include <smtpd_sasl_glue.h>
+
+#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);
+}