Skip to content

Commit

Permalink
Issue 3527 - Add PROXY protocol support (#5762)
Browse files Browse the repository at this point in the history
Description: Add support to 389-base for the PROXY protocol
for ACI evaluation and also for logging client queries.

The proxy protocol is described here:
http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt

Fixes: #3527

Reviewed by: @Firstyear, @progier389, @mreynolds389 (Thanks!)
  • Loading branch information
droideck committed Jan 11, 2024
1 parent 727cccc commit 0710d82
Show file tree
Hide file tree
Showing 19 changed files with 1,369 additions and 4 deletions.
3 changes: 3 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ dist_noinst_HEADERS = \
ldap/servers/slapd/getopt_ext.h \
ldap/servers/slapd/getsocketpeer.h \
ldap/servers/slapd/http.h \
ldap/servers/slapd/haproxy.h \
ldap/servers/slapd/intrinsics.h \
ldap/servers/slapd/log.h \
ldap/servers/slapd/openldapber.h \
Expand Down Expand Up @@ -1537,6 +1538,7 @@ libslapd_la_SOURCES = ldap/servers/slapd/add.c \
ldap/servers/slapd/filterentry.c \
ldap/servers/slapd/generation.c \
ldap/servers/slapd/getfilelist.c \
ldap/servers/slapd/haproxy.c \
ldap/servers/slapd/ldaputil.c \
ldap/servers/slapd/lenstr.c \
ldap/servers/slapd/libglobs.c \
Expand Down Expand Up @@ -2408,6 +2410,7 @@ test_slapd_SOURCES = test/main.c \
test/libslapd/schema/filter_validate.c \
test/libslapd/operation/v3_compat.c \
test/libslapd/spal/meminfo.c \
test/libslapd/haproxy/parse.c \
test/plugins/test.c \
test/plugins/pwdstorage/pbkdf2.c

Expand Down
96 changes: 96 additions & 0 deletions dirsrvtests/tests/suites/basic/haproxy_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2023 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---

import ldap
import logging
import pytest
from lib389._constants import DEFAULT_SUFFIX, PASSWORD
from lib389.topologies import topology_st as topo
from lib389.idm.user import UserAccount, UserAccounts
from lib389.idm.account import Anonymous

log = logging.getLogger(__name__)
DN = "uid=common,ou=people," + DEFAULT_SUFFIX
HOME_DIR = '/home/common'

@pytest.fixture(scope="function")
def setup_test(topo, request):
"""Setup test environment"""
log.info("Add nsslapd-haproxy-trusted-ip attribute")
topo.standalone.config.set('nsslapd-haproxy-trusted-ip', '192.168.0.1')
assert topo.standalone.config.present('nsslapd-haproxy-trusted-ip', '192.168.0.1')

log.info("Add a user")
users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
try:
users.create(properties={
'uid': 'common',
'cn': 'common',
'sn': 'common',
'uidNumber': '3000',
'gidNumber': '4000',
'homeDirectory': HOME_DIR,
'description': 'test haproxy with this user',
'userPassword': PASSWORD
})
except ldap.ALREADY_EXISTS:
log.info("User already exists")
pass


def test_haproxy_trust_ip_attribute(topo, setup_test):
"""Test nsslapd-haproxy-trusted-ip attribute set and delete
:id: 8a0789a6-3ede-40e2-966c-9a2c87eaac05
:setup: Standalone instance with nsslapd-haproxy-trusted-ip attribute and a user
:steps:
1. Check that nsslapd-haproxy-trusted-ip attribute is present
2. Delete nsslapd-haproxy-trusted-ip attribute
3. Check that nsslapd-haproxy-trusted-ip attribute is not present
:expectedresults:
1. Success
2. Success
3. Success
"""

log.info("Check that nsslapd-haproxy-trusted-ip attribute is present")
assert topo.standalone.config.present('nsslapd-haproxy-trusted-ip', '192.168.0.1')

log.info("Delete nsslapd-haproxy-trusted-ip attribute")
topo.standalone.config.remove_all('nsslapd-haproxy-trusted-ip')

log.info("Check that nsslapd-haproxy-trusted-ip attribute is not present")
assert not topo.standalone.config.present('nsslapd-haproxy-trusted-ip', '192.168.0.1')


def test_binds_with_haproxy_trust_ip_attribute(topo, setup_test):
"""Test that non-proxy binds are not blocked when nsslapd-haproxy-trusted-ip attribute is set
:id: 14273c16-fed9-497e-8ebb-09e3dabc7914
:setup: Standalone instance with nsslapd-haproxy-trusted-ip attribute and a user
:steps:
1. Try to bind as anonymous user
2. Try to bind as a user
3. Check that userPassword is correct and we can get it
:expectedresults:
1. Success
2. Success
3. Success
"""

log.info("Bind as anonymous user")
Anonymous(topo.standalone).bind()

log.info("Bind as a user")
user_entry = UserAccount(topo.standalone, DN)
user_conn = user_entry.bind(PASSWORD)

log.info("Check that userPassword is correct and we can get it")
user_entry = UserAccount(user_conn, DN)
home = user_entry.get_attr_val_utf8('homeDirectory')
assert home == HOME_DIR
100 changes: 100 additions & 0 deletions ldap/servers/slapd/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ connection_cleanup(Connection *conn)
slapi_ch_free((void **)&conn->cin_destaddr);
slapi_ch_free((void **)&conn->cin_addr_aclip);
slapi_ch_free_string(&conn->c_ipaddr);
slapi_ch_free_string(&conn->c_serveripaddr);
if (conn->c_domain != NULL) {
ber_bvecfree(conn->c_domain);
conn->c_domain = NULL;
Expand All @@ -213,6 +214,7 @@ connection_cleanup(Connection *conn)
conn->c_idlesince = 0;
conn->c_flags = 0;
conn->c_needpw = 0;
conn->c_haproxyheader_read = 0;
conn->c_prfd = NULL;
/* c_ci stays as it is */
conn->c_fdi = SLAPD_INVALID_SOCKET_INDEX;
Expand Down Expand Up @@ -420,6 +422,7 @@ connection_reset(Connection *conn, int ns, PRNetAddr *from, int fromLen __attrib
conn->c_ssl_ssf = 0;
conn->c_local_ssf = 0;
conn->c_ipaddr = slapi_ch_strdup(str_ip);
conn->c_serveripaddr = slapi_ch_strdup(str_destip);
}

/* Create a pool of threads for handling the operations */
Expand Down Expand Up @@ -1132,6 +1135,28 @@ conn_buffered_data_avail_nolock(Connection *conn, int *conn_closed)
}
}

/* Function to convert a PRNetAddr to a normalized IPv4 string and keep original address string. */
static void
normalize_IPv4(const PRNetAddr *addr, char *normalizedAddr, size_t normalizedAddrSize, char *originalAddr, size_t originalAddrSize)
{
/* Keep the original address string */
PR_NetAddrToString(addr, originalAddr, originalAddrSize);

if (PR_IsNetAddrType(addr, PR_IpAddrV4Mapped)) {
/* Handle IPv4-mapped-to-IPv6 */
PRNetAddr v4addr;
/* v4addr gets the lower 32 bits of addr6 (the IPv4 address in the IPv6 format) */
v4addr.inet.family = PR_AF_INET;
v4addr.inet.ip = addr->ipv6.ip.pr_s6_addr32[3];
PR_NetAddrToString(&v4addr, normalizedAddr, normalizedAddrSize);
} else {
/* If it's not an IPv4-mapped IPv6 address, just keep the original format */
strncpy(normalizedAddr, originalAddr, normalizedAddrSize - 1);
normalizedAddr[normalizedAddrSize - 1] = '\0';
}
originalAddr[originalAddrSize - 1] = '\0';
}

/* Upon returning from this function, we have either:
1. Read a PDU successfully.
2. Detected some error condition with the connection which requires closing it.
Expand All @@ -1146,6 +1171,7 @@ connection_read_operation(Connection *conn, Operation *op, ber_tag_t *tag, int *
{
ber_len_t len = 0;
int ret = 0;
int haproxy_rc = 0;
int32_t waits_done = 0;
ber_int_t msgid;
int new_operation = 1; /* Are we doing the first I/O read for a new operation ? */
Expand All @@ -1154,6 +1180,17 @@ connection_read_operation(Connection *conn, Operation *op, ber_tag_t *tag, int *
PRInt32 syserr = 0;
size_t buffer_data_avail;
int conn_closed = 0;
PRNetAddr pr_netaddr_from = {0};
PRNetAddr pr_netaddr_dest = {0};
char buf_ip[INET6_ADDRSTRLEN + 1] = {0};
char buf_haproxy_destip[INET6_ADDRSTRLEN + 1] = {0};
char str_ip[INET6_ADDRSTRLEN + 1] = {0};
char str_haproxy_ip[INET6_ADDRSTRLEN + 1] = {0};
char str_haproxy_destip[INET6_ADDRSTRLEN + 1] = {0};
PRStatus status = PR_SUCCESS;
struct berval **bvals = NULL;
int proxy_connection = 0;
int restrict_access = 0;

pthread_mutex_lock(&(conn->c_mutex));
/*
Expand All @@ -1179,13 +1216,76 @@ connection_read_operation(Connection *conn, Operation *op, ber_tag_t *tag, int *
}
new_operation = 0;
}

/* If we still haven't seen a complete PDU, read from the network */
while (*tag == LBER_DEFAULT) {
int32_t ioblocktimeout_waits = conn->c_ioblocktimeout / CONN_TURBO_TIMEOUT_INTERVAL;
/* We should never get here with data remaining in the buffer */
PR_ASSERT(!new_operation || !conn_buffered_data_avail_nolock(conn, &conn_closed));
/* We make a non-blocking read call */
if (CONNECTION_BUFFER_OFF != conn->c_private->use_buffer) {
/* Process HAProxy header */
if (conn->c_haproxyheader_read == 0) {
conn->c_haproxyheader_read = 1;
/*
* We only check for HAProxy header if nsslapd-haproxy-trusted-ip is configured.
* If it is we proceed with the connection only if it's comming from trusted
* proxy server with correct and complete header.
*/
if ((bvals = g_get_haproxy_trusted_ip()) != NULL) {
/* Can we have an unknown address at that point? */
if ((haproxy_rc = haproxy_receive(conn->c_sd, &proxy_connection, &pr_netaddr_from, &pr_netaddr_dest)) == HAPROXY_ERROR) {
slapi_log_err(SLAPI_LOG_CONNS, "connection_read_operation", "Error reading HAProxy header.\n");
disconnect_server_nomutex(conn, conn->c_connid, -1, SLAPD_DISCONNECT_PROXY_INVALID_HEADER, EPROTO);
ret = CONN_DONE;
goto done;
}

/* If bval is NULL and we don't have an error - we still want the proper log and update */
if ((haproxy_rc == HAPROXY_HEADER_PARSED) && (proxy_connection)) {
/* Normalize IP addresses */
normalize_IPv4(conn->cin_addr, buf_ip, sizeof(buf_ip), str_ip, sizeof(str_ip));
normalize_IPv4(&pr_netaddr_dest, buf_haproxy_destip, sizeof(buf_haproxy_destip),
str_haproxy_destip, sizeof(str_haproxy_destip));

/* Now, reset RC and set it to 0 only if a match is found */
haproxy_rc = -1;

/* Allow only:
* Trusted IP == Original Client IP == HAProxy Header Destination IP */
for (size_t i = 0; bvals[i] != NULL; ++i) {
if ((strlen(bvals[i]->bv_val) == strlen(buf_ip)) &&
(strlen(bvals[i]->bv_val) == strlen(buf_haproxy_destip)) &&
(strncasecmp(bvals[i]->bv_val, buf_ip, strlen(buf_ip)) == 0) &&
(strncasecmp(bvals[i]->bv_val, buf_haproxy_destip, strlen(buf_haproxy_destip)) == 0)) {
haproxy_rc = 0;
break;
}
}
if (haproxy_rc == -1) {
slapi_log_err(SLAPI_LOG_CONNS, "connection_read_operation", "HAProxy header received from unknown source.\n");
disconnect_server_nomutex(conn, conn->c_connid, -1, SLAPD_DISCONNECT_PROXY_UNKNOWN, EPROTO);
ret = CONN_DONE;
goto done;
}
/* Get the HAProxy header client IP address */
PR_NetAddrToString(&pr_netaddr_from, str_haproxy_ip, sizeof(str_haproxy_ip));

/* Replace cin_addr and cin_destaddr in the Connection struct with received addresses */
slapi_ch_free((void**)&conn->cin_addr);
slapi_ch_free((void**)&conn->cin_destaddr);
conn->cin_addr = (PRNetAddr*)malloc(sizeof(PRNetAddr));
conn->cin_destaddr = (PRNetAddr*)malloc(sizeof(PRNetAddr));
memcpy(conn->cin_addr, &pr_netaddr_from, sizeof(PRNetAddr));
memcpy(conn->cin_destaddr, &pr_netaddr_dest, sizeof(PRNetAddr));
conn->c_ipaddr = slapi_ch_strdup(str_haproxy_ip);
conn->c_serveripaddr = slapi_ch_strdup(str_haproxy_destip);
slapi_log_access(LDAP_DEBUG_STATS,
"conn=%" PRIu64 " fd=%d HAProxy new_address_from=%s to new_address_dest=%s\n",
conn->c_connid, conn->c_sd, str_haproxy_ip, str_haproxy_destip);
}
}
}
ret = connection_read_ldap_data(conn, &err);
} else {
ret = get_next_from_buffer(NULL, 0, &len, tag, op->o_ber, conn);
Expand Down
2 changes: 2 additions & 0 deletions ldap/servers/slapd/disconnect_error_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ ER2(SLAPD_DISCONNECT_UNBIND, "U1")
ER2(SLAPD_DISCONNECT_POLL, "P2")
ER2(SLAPD_DISCONNECT_NTSSL_TIMEOUT, "T2")
ER2(SLAPD_DISCONNECT_SASL_FAIL, "S1")
ER2(SLAPD_DISCONNECT_PROXY_INVALID_HEADER, "P3")
ER2(SLAPD_DISCONNECT_PROXY_UNKNOWN, "P4")


#endif /* __DISCONNECT_ERROR_STRINGS_H_ */
2 changes: 2 additions & 0 deletions ldap/servers/slapd/disconnect_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
#define SLAPD_DISCONNECT_POLL SLAPD_DISCONNECT_ERROR_BASE + 10
#define SLAPD_DISCONNECT_NTSSL_TIMEOUT SLAPD_DISCONNECT_ERROR_BASE + 11
#define SLAPD_DISCONNECT_SASL_FAIL SLAPD_DISCONNECT_ERROR_BASE + 12
#define SLAPD_DISCONNECT_PROXY_INVALID_HEADER SLAPD_DISCONNECT_ERROR_BASE + 13
#define SLAPD_DISCONNECT_PROXY_UNKNOWN SLAPD_DISCONNECT_ERROR_BASE + 14


#endif /* __DISCONNECT_ERRORS_H_ */
Loading

0 comments on commit 0710d82

Please sign in to comment.