MINOR: listener: add the "accept-netscaler-cip" option to the "bind" keyword
When NetScaler application switch is used as L3+ switch, informations
regarding the original IP and TCP headers are lost as a new TCP
connection is created between the NetScaler and the backend server.
NetScaler provides a feature to insert in the TCP data the original data
that can then be consumed by the backend server.
Specifications and documentations from NetScaler:
https://support.citrix.com/article/CTX205670
https://www.citrix.com/blogs/2016/04/25/how-to-enable-client-ip-in-tcpip-option-of-netscaler/
When CIP is enabled on the NetScaler, then a TCP packet is inserted just after
the TCP handshake. This is composed as:
- CIP magic number : 4 bytes
Both sender and receiver have to agree on a magic number so that
they both handle the incoming data as a NetScaler Client IP insertion
packet.
- Header length : 4 bytes
Defines the length on the remaining data.
- IP header : >= 20 bytes if IPv4, 40 bytes if IPv6
Contains the header of the last IP packet sent by the client during TCP
handshake.
- TCP header : >= 20 bytes
Contains the header of the last TCP packet sent by the client during TCP
handshake.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 8b35a02..8a8055d 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -9736,6 +9736,18 @@
The currently supported settings are the following ones.
+accept-netscaler-cip <magic number>
+ Enforces the use of the NetScaler Client IP insertion protocol over any
+ connection accepted by any of the TCP sockets declared on the same line. The
+ NetScaler Client IP insertion protocol dictates the layer 3/4 addresses of
+ the incoming connection to be used everywhere an address is used, with the
+ only exception of "tcp-request connection" rules which will only see the
+ real connection address. Logs will reflect the addresses indicated in the
+ protocol, unless it is violated, in which case the real address will still
+ be used. This keyword combined with support from external components can be
+ used as an efficient and reliable alternative to the X-Forwarded-For
+ mechanism which is not always reliable and not even always usable.
+
accept-proxy
Enforces the use of the PROXY protocol over any connection accepted by any of
the sockets declared on the same line. Versions 1 and 2 of the PROXY protocol
@@ -10794,16 +10806,18 @@
connection established to this server. The PROXY protocol informs the other
end about the layer 3/4 addresses of the incoming connection, so that it can
know the client's address or the public address it accessed to, whatever the
- upper layer protocol. For connections accepted by an "accept-proxy" listener,
- the advertised address will be used. Only TCPv4 and TCPv6 address families
- are supported. Other families such as Unix sockets, will report an UNKNOWN
- family. Servers using this option can fully be chained to another instance of
- haproxy listening with an "accept-proxy" setting. This setting must not be
- used if the server isn't aware of the protocol. When health checks are sent
- to the server, the PROXY protocol is automatically used when this option is
- set, unless there is an explicit "port" or "addr" directive, in which case an
- explicit "check-send-proxy" directive would also be needed to use the PROXY
- protocol. See also the "accept-proxy" option of the "bind" keyword.
+ upper layer protocol. For connections accepted by an "accept-proxy" or
+ "accept-netscaler-cip" listener, the advertised address will be used. Only
+ TCPv4 and TCPv6 address families are supported. Other families such as
+ Unix sockets, will report an UNKNOWN family. Servers using this option can
+ fully be chained to another instance of haproxy listening with an
+ "accept-proxy" setting. This setting must not be used if the server isn't
+ aware of the protocol. When health checks are sent to the server, the PROXY
+ protocol is automatically used when this option is set, unless there is an
+ explicit "port" or "addr" directive, in which case an explicit
+ "check-send-proxy" directive would also be needed to use the PROXY protocol.
+ See also the "accept-proxy" and "accept-netscaler-cip" option of the "bind"
+ keyword.
Supported in default-server: No
@@ -12904,9 +12918,10 @@
IP and works on both IPv4 and IPv6 tables. On IPv6 tables, IPv4 addresses are
mapped to their IPv6 equivalent, according to RFC 4291. Note that it is the
TCP-level source address which is used, and not the address of a client
- behind a proxy. However if the "accept-proxy" bind directive is used, it can
- be the address of a client behind another PROXY-protocol compatible component
- for all rule sets except "tcp-request connection" which sees the real address.
+ behind a proxy. However if the "accept-proxy" or "accept-netscaler-cip" bind
+ directive is used, it can be the address of a client behind another
+ PROXY-protocol compatible component for all rule sets except
+ "tcp-request connection" which sees the real address.
Example:
# add an HTTP header in requests with the originating address' country
@@ -14362,8 +14377,9 @@
connection to haproxy. If the connection was accepted on a UNIX socket
instead, the IP address would be replaced with the word "unix". Note that
when the connection is accepted on a socket configured with "accept-proxy"
- and the PROXY protocol is correctly used, then the logs will reflect the
- forwarded connection's information.
+ and the PROXY protocol is correctly used, or with a "accept-netscaler-cip"
+ and the NetScaler Client IP insetion protocol is correctly used, then the
+ logs will reflect the forwarded connection's information.
- "client_port" is the TCP port of the client which initiated the connection.
If the connection was accepted on a UNIX socket instead, the port would be
@@ -14542,8 +14558,9 @@
connection to haproxy. If the connection was accepted on a UNIX socket
instead, the IP address would be replaced with the word "unix". Note that
when the connection is accepted on a socket configured with "accept-proxy"
- and the PROXY protocol is correctly used, then the logs will reflect the
- forwarded connection's information.
+ and the PROXY protocol is correctly used, or with a "accept-netscaler-cip"
+ and the NetScaler Client IP insetion protocol is correctly used, then the
+ logs will reflect the forwarded connection's information.
- "client_port" is the TCP port of the client which initiated the connection.
If the connection was accepted on a UNIX socket instead, the port would be
diff --git a/doc/netscaler-client-ip-insertion-protocol.txt b/doc/netscaler-client-ip-insertion-protocol.txt
new file mode 100644
index 0000000..6f77f65
--- /dev/null
+++ b/doc/netscaler-client-ip-insertion-protocol.txt
@@ -0,0 +1,29 @@
+When NetScaler application switch is used as L3+ switch, informations
+regarding the original IP and TCP headers are lost as a new TCP
+connection is created between the NetScaler and the backend server.
+
+NetScaler provides a feature to insert in the TCP data the original data
+that can then be consumed by the backend server.
+
+Specifications and documentations from NetScaler:
+ https://support.citrix.com/article/CTX205670
+ https://www.citrix.com/blogs/2016/04/25/how-to-enable-client-ip-in-tcpip-option-of-netscaler/
+
+When CIP is enabled on the NetScaler, then a TCP packet is inserted just after
+the TCP handshake. This is composed as:
+
+ - CIP magic number : 4 bytes
+ Both sender and receiver have to agree on a magic number so that
+ they both handle the incoming data as a NetScaler Client IP insertion
+ packet.
+
+ - Header length : 4 bytes
+ Defines the length on the remaining data.
+
+ - IP header : >= 20 bytes if IPv4, 40 bytes if IPv6
+ Contains the header of the last IP packet sent by the client during TCP
+ handshake.
+
+ - TCP header : >= 20 bytes
+ Contains the header of the last TCP packet sent by the client during TCP
+ handshake.
diff --git a/include/proto/connection.h b/include/proto/connection.h
index f50329c..ef078ad 100644
--- a/include/proto/connection.h
+++ b/include/proto/connection.h
@@ -45,6 +45,9 @@
int make_proxy_line_v1(char *buf, int buf_len, struct sockaddr_storage *src, struct sockaddr_storage *dst);
int make_proxy_line_v2(char *buf, int buf_len, struct server *srv, struct connection *remote);
+/* receive a NetScaler Client IP insertion header over a connection */
+int conn_recv_netscaler_cip(struct connection *conn, int flag);
+
/* raw send() directly on the socket */
int conn_sock_send(struct connection *conn, const void *buf, int len, int flags);
@@ -570,6 +573,13 @@
case CO_ER_PRX_NOT_HDR: return "Received something which does not look like a PROXY protocol header";
case CO_ER_PRX_BAD_HDR: return "Received an invalid PROXY protocol header";
case CO_ER_PRX_BAD_PROTO: return "Received an unhandled protocol in the PROXY protocol header";
+
+ case CO_ER_CIP_EMPTY: return "Connection closed while waiting for NetScaler Client IP header";
+ case CO_ER_CIP_ABORT: return "Connection error while waiting for NetScaler Client IP header";
+ case CO_ER_CIP_TRUNCATED: return "Truncated NetScaler Client IP header received";
+ case CO_ER_CIP_BAD_MAGIC: return "Received an invalid NetScaler Client IP magic number";
+ case CO_ER_CIP_BAD_PROTO: return "Received an unhandled protocol in the NetScaler Client IP header";
+
case CO_ER_SSL_EMPTY: return "Connection closed during SSL handshake";
case CO_ER_SSL_ABORT: return "Connection error during SSL handshake";
case CO_ER_SSL_TIMEOUT: return "Timeout during SSL handshake";
diff --git a/include/types/connection.h b/include/types/connection.h
index dfbff6a..292ca2b 100644
--- a/include/types/connection.h
+++ b/include/types/connection.h
@@ -32,6 +32,10 @@
#include <types/port_range.h>
#include <types/protocol.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/tcp.h>
+
/* referenced below */
struct connection;
struct buffer;
@@ -107,10 +111,10 @@
CO_FL_SEND_PROXY = 0x01000000, /* send a valid PROXY protocol header */
CO_FL_SSL_WAIT_HS = 0x02000000, /* wait for an SSL handshake to complete */
CO_FL_ACCEPT_PROXY = 0x04000000, /* receive a valid PROXY protocol header */
- /* unused : 0x08000000 */
+ CO_FL_ACCEPT_CIP = 0x08000000, /* receive a valid NetScaler Client IP header */
/* below we have all handshake flags grouped into one */
- CO_FL_HANDSHAKE = CO_FL_SEND_PROXY | CO_FL_SSL_WAIT_HS | CO_FL_ACCEPT_PROXY,
+ CO_FL_HANDSHAKE = CO_FL_SEND_PROXY | CO_FL_SSL_WAIT_HS | CO_FL_ACCEPT_PROXY | CO_FL_ACCEPT_CIP,
/* when any of these flags is set, polling is defined by socket-layer
* operations, as opposed to data-layer. Transport is explicitly not
@@ -156,6 +160,13 @@
CO_ER_PRX_BAD_HDR, /* bad PROXY protocol header */
CO_ER_PRX_BAD_PROTO, /* unsupported protocol in PROXY header */
+ CO_ER_CIP_EMPTY, /* nothing received in NetScaler Client IP header */
+ CO_ER_CIP_ABORT, /* client abort during NetScaler Client IP header */
+ CO_ER_CIP_TIMEOUT, /* timeout while waiting for a NetScaler Client IP header */
+ CO_ER_CIP_TRUNCATED, /* truncated NetScaler Client IP header */
+ CO_ER_CIP_BAD_MAGIC, /* bad magic number in NetScaler Client IP header */
+ CO_ER_CIP_BAD_PROTO, /* unsupported protocol in NetScaler Client IP header */
+
CO_ER_SSL_EMPTY, /* client closed during SSL handshake */
CO_ER_SSL_ABORT, /* client abort during SSL handshake */
CO_ER_SSL_TIMEOUT, /* timeout during SSL handshake */
diff --git a/include/types/listener.h b/include/types/listener.h
index 4da6cac..afe2ad8 100644
--- a/include/types/listener.h
+++ b/include/types/listener.h
@@ -92,6 +92,7 @@
#define LI_O_TCP_FO 0x0100 /* enable TCP Fast Open (linux >= 3.7) */
#define LI_O_V6ONLY 0x0200 /* bind to IPv6 only on Linux >= 2.4.21 */
#define LI_O_V4V6 0x0400 /* bind to IPv4/IPv6 on Linux >= 2.4.21 */
+#define LI_O_ACC_CIP 0x0800 /* find the proxied address in the NetScaler Client IP header */
/* Note: if a listener uses LI_O_UNLIMITED, it is highly recommended that it adds its own
* maxconn setting to the global.maxsock value so that its resources are reserved.
@@ -151,6 +152,7 @@
int level; /* stats access level (ACCESS_LVL_*) */
struct list by_fe; /* next binding for the same frontend, or NULL */
struct list listeners; /* list of listeners using this bind config */
+ uint32_t ns_cip_magic; /* Excepted NetScaler Client IP magic number */
char *arg; /* argument passed to "bind" for better error reporting */
char *file; /* file where the section appears */
int line; /* line where the section appears */
diff --git a/src/connection.c b/src/connection.c
index 5515188..358c9bc 100644
--- a/src/connection.c
+++ b/src/connection.c
@@ -62,6 +62,10 @@
if (unlikely(conn->flags & CO_FL_ERROR))
goto leave;
+ if (conn->flags & CO_FL_ACCEPT_CIP)
+ if (!conn_recv_netscaler_cip(conn, CO_FL_ACCEPT_CIP))
+ goto leave;
+
if (conn->flags & CO_FL_ACCEPT_PROXY)
if (!conn_recv_proxy(conn, CO_FL_ACCEPT_PROXY))
goto leave;
@@ -624,6 +628,202 @@
return 0;
}
+/* This handshake handler waits a NetScaler Client IP insertion header
+ * at the beginning of the raw data stream. The header looks like this:
+ *
+ * 4 bytes: CIP magic number
+ * 4 bytes: Header length
+ * 20+ bytes: Header of the last IP packet sent by the client during
+ * TCP handshake.
+ * 20+ bytes: Header of the last TCP packet sent by the client during
+ * TCP handshake.
+ *
+ * This line MUST be at the beginning of the buffer and MUST NOT be
+ * fragmented.
+ *
+ * The header line is small and in all cases smaller than the smallest normal
+ * TCP MSS. So it MUST always be delivered as one segment, which ensures we
+ * can safely use MSG_PEEK and avoid buffering.
+ *
+ * Once the data is fetched, the values are set in the connection's address
+ * fields, and data are removed from the socket's buffer. The function returns
+ * zero if it needs to wait for more data or if it fails, or 1 if it completed
+ * and removed itself.
+ */
+int conn_recv_netscaler_cip(struct connection *conn, int flag)
+{
+ char *line;
+ uint32_t cip_magic;
+ uint32_t cip_len;
+ uint8_t ip_v;
+
+ /* we might have been called just after an asynchronous shutr */
+ if (conn->flags & CO_FL_SOCK_RD_SH)
+ goto fail;
+
+ if (!conn_ctrl_ready(conn))
+ goto fail;
+
+ if (!fd_recv_ready(conn->t.sock.fd))
+ return 0;
+
+ do {
+ trash.len = recv(conn->t.sock.fd, trash.str, trash.size, MSG_PEEK);
+ if (trash.len < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno == EAGAIN) {
+ fd_cant_recv(conn->t.sock.fd);
+ return 0;
+ }
+ goto recv_abort;
+ }
+ } while (0);
+
+ if (!trash.len) {
+ /* client shutdown */
+ conn->err_code = CO_ER_CIP_EMPTY;
+ goto fail;
+ }
+
+ /* Fail if buffer length is not large enough to contain
+ * CIP magic, CIP length */
+ if (trash.len < 8)
+ goto missing;
+
+ line = trash.str;
+
+ cip_magic = ntohl(*(uint32_t *)line);
+ cip_len = ntohl(*(uint32_t *)(line+4));
+
+ /* Decode a possible NetScaler Client IP request, fail early if
+ * it does not match */
+ if (cip_magic != objt_listener(conn->target)->bind_conf->ns_cip_magic)
+ goto bad_magic;
+
+ /* Fail if buffer length is not large enough to contain
+ * CIP magic, CIP length, minimal IP header */
+ if (trash.len < 28)
+ goto missing;
+
+ line += 8;
+
+ /* Get IP version from the first four bits */
+ ip_v = (*line & 0xf0) >> 4;
+
+ if (ip_v == 4) {
+ struct ip *hdr_ip4;
+ struct tcphdr *hdr_tcp;
+
+ hdr_ip4 = (struct ip *)line;
+
+ if (trash.len < (8 + ntohs(hdr_ip4->ip_len))) {
+ /* Fail if buffer length is not large enough to contain
+ * CIP magic, CIP length, IPv4 header */
+ goto missing;
+ } else if (hdr_ip4->ip_p != IPPROTO_TCP) {
+ /* The protocol does not include a TCP header */
+ conn->err_code = CO_ER_CIP_BAD_PROTO;
+ goto fail;
+ } else if (trash.len < (28 + ntohs(hdr_ip4->ip_len))) {
+ /* Fail if buffer length is not large enough to contain
+ * CIP magic, CIP length, IPv4 header, TCP header */
+ goto missing;
+ }
+
+ hdr_tcp = (struct tcphdr *)(line + (hdr_ip4->ip_hl * 4));
+
+ /* update the session's addresses and mark them set */
+ ((struct sockaddr_in *)&conn->addr.from)->sin_family = AF_INET;
+ ((struct sockaddr_in *)&conn->addr.from)->sin_addr.s_addr = hdr_ip4->ip_src.s_addr;
+ ((struct sockaddr_in *)&conn->addr.from)->sin_port = hdr_tcp->source;
+
+ ((struct sockaddr_in *)&conn->addr.to)->sin_family = AF_INET;
+ ((struct sockaddr_in *)&conn->addr.to)->sin_addr.s_addr = hdr_ip4->ip_dst.s_addr;
+ ((struct sockaddr_in *)&conn->addr.to)->sin_port = hdr_tcp->dest;
+
+ conn->flags |= CO_FL_ADDR_FROM_SET | CO_FL_ADDR_TO_SET;
+ }
+ else if (ip_v == 6) {
+ struct ip6_hdr *hdr_ip6;
+ struct tcphdr *hdr_tcp;
+
+ hdr_ip6 = (struct ip6_hdr *)line;
+
+ if (trash.len < 28) {
+ /* Fail if buffer length is not large enough to contain
+ * CIP magic, CIP length, IPv6 header */
+ goto missing;
+ } else if (hdr_ip6->ip6_nxt != IPPROTO_TCP) {
+ /* The protocol does not include a TCP header */
+ conn->err_code = CO_ER_CIP_BAD_PROTO;
+ goto fail;
+ } else if (trash.len < 48) {
+ /* Fail if buffer length is not large enough to contain
+ * CIP magic, CIP length, IPv6 header, TCP header */
+ goto missing;
+ }
+
+ hdr_tcp = (struct tcphdr *)(line + sizeof(struct ip6_hdr));
+
+ /* update the session's addresses and mark them set */
+ ((struct sockaddr_in6 *)&conn->addr.from)->sin6_family = AF_INET6;
+ ((struct sockaddr_in6 *)&conn->addr.from)->sin6_addr = hdr_ip6->ip6_src;
+ ((struct sockaddr_in6 *)&conn->addr.from)->sin6_port = hdr_tcp->source;
+
+ ((struct sockaddr_in6 *)&conn->addr.to)->sin6_family = AF_INET6;
+ ((struct sockaddr_in6 *)&conn->addr.to)->sin6_addr = hdr_ip6->ip6_dst;
+ ((struct sockaddr_in6 *)&conn->addr.to)->sin6_port = hdr_tcp->dest;
+
+ conn->flags |= CO_FL_ADDR_FROM_SET | CO_FL_ADDR_TO_SET;
+ }
+ else {
+ /* The protocol does not match something known (IPv4/IPv6) */
+ conn->err_code = CO_ER_CIP_BAD_PROTO;
+ goto fail;
+ }
+
+ line += cip_len;
+ trash.len = line - trash.str;
+
+ /* remove the NetScaler Client IP header from the request. For this
+ * we re-read the exact line at once. If we don't get the exact same
+ * result, we fail.
+ */
+ do {
+ int len2 = recv(conn->t.sock.fd, trash.str, trash.len, 0);
+ if (len2 < 0 && errno == EINTR)
+ continue;
+ if (len2 != trash.len)
+ goto recv_abort;
+ } while (0);
+
+ conn->flags &= ~flag;
+ return 1;
+
+ missing:
+ /* Missing data. Since we're using MSG_PEEK, we can only poll again if
+ * we have not read anything. Otherwise we need to fail because we won't
+ * be able to poll anymore.
+ */
+ conn->err_code = CO_ER_CIP_TRUNCATED;
+ goto fail;
+
+ bad_magic:
+ conn->err_code = CO_ER_CIP_BAD_MAGIC;
+ goto fail;
+
+ recv_abort:
+ conn->err_code = CO_ER_CIP_ABORT;
+ conn->flags |= CO_FL_SOCK_RD_SH | CO_FL_SOCK_WR_SH;
+ goto fail;
+
+ fail:
+ __conn_sock_stop_both(conn);
+ conn->flags |= CO_FL_ERROR;
+ return 0;
+}
+
int make_proxy_line(char *buf, int buf_len, struct server *srv, struct connection *remote)
{
int ret = 0;
diff --git a/src/listener.c b/src/listener.c
index 59385f0..c2ce413 100644
--- a/src/listener.c
+++ b/src/listener.c
@@ -618,6 +618,31 @@
return 0;
}
+/* parse the "accept-netscaler-cip" bind keyword */
+static int bind_parse_accept_netscaler_cip(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
+{
+ struct listener *l;
+ uint32_t val;
+
+ if (!*args[cur_arg + 1]) {
+ memprintf(err, "'%s' : missing value", args[cur_arg]);
+ return ERR_ALERT | ERR_FATAL;
+ }
+
+ val = atol(args[cur_arg + 1]);
+ if (val <= 0) {
+ memprintf(err, "'%s' : invalid value %d, must be > 0", args[cur_arg], val);
+ return ERR_ALERT | ERR_FATAL;
+ }
+
+ list_for_each_entry(l, &conf->listeners, by_bind) {
+ l->options |= LI_O_ACC_CIP;
+ conf->ns_cip_magic = val;
+ }
+
+ return 0;
+}
+
/* parse the "backlog" bind keyword */
static int bind_parse_backlog(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)
{
@@ -814,6 +839,7 @@
* not enabled.
*/
static struct bind_kw_list bind_kws = { "ALL", { }, {
+ { "accept-netscaler-cip", bind_parse_accept_netscaler_cip, 1 }, /* enable NetScaler Client IP insertion protocol */
{ "accept-proxy", bind_parse_accept_proxy, 0 }, /* enable PROXY protocol */
{ "backlog", bind_parse_backlog, 1 }, /* set backlog of listening socket */
{ "id", bind_parse_id, 1 }, /* set id of listening socket */
diff --git a/src/session.c b/src/session.c
index fdb2404..0c23364 100644
--- a/src/session.c
+++ b/src/session.c
@@ -142,6 +142,12 @@
conn_sock_want_recv(cli_conn);
}
+ /* wait for a NetScaler client IP insertion protocol header */
+ if (l->options & LI_O_ACC_CIP) {
+ cli_conn->flags |= CO_FL_ACCEPT_CIP;
+ conn_sock_want_recv(cli_conn);
+ }
+
conn_data_want_recv(cli_conn);
if (conn_xprt_init(cli_conn) < 0)
goto out_free_conn;
@@ -346,6 +352,7 @@
/* with "option dontlognull", we don't log connections with no transfer */
if (!conn->err_code ||
conn->err_code == CO_ER_PRX_EMPTY || conn->err_code == CO_ER_PRX_ABORT ||
+ conn->err_code == CO_ER_CIP_EMPTY || conn->err_code == CO_ER_CIP_ABORT ||
conn->err_code == CO_ER_SSL_EMPTY || conn->err_code == CO_ER_SSL_ABORT)
log = 0;
}
@@ -354,6 +361,8 @@
if (!conn->err_code && (task->state & TASK_WOKEN_TIMER)) {
if (conn->flags & CO_FL_ACCEPT_PROXY)
conn->err_code = CO_ER_PRX_TIMEOUT;
+ else if (conn->flags & CO_FL_ACCEPT_CIP)
+ conn->err_code = CO_ER_CIP_TIMEOUT;
else if (conn->flags & CO_FL_SSL_WAIT_HS)
conn->err_code = CO_ER_SSL_TIMEOUT;
}