Pārlūkot izejas kodu

core: Add PROXY protocol implementation (#1765)

- introduce new global variable `ksr_tcp_accept_haproxy`.
- this variable can be modified by using the `tcp_accept_haproxy=yes` core
  configuration parameter.
- when active, inbound TCP connections are expected to behave according
  to the PROXY protocol[1].
- Both the v1 (human-readable) and v2 (binary) versions of the protocol
  are supported.
- [1]: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
Sebastian Lauwers 6 gadi atpakaļ
vecāks
revīzija
10be01a414
5 mainītis faili ar 258 papildinājumiem un 11 dzēšanām
  1. 3 0
      src/core/cfg.lex
  2. 9 0
      src/core/cfg.y
  3. 1 0
      src/core/globals.h
  4. 244 10
      src/core/tcp_main.c
  5. 1 1
      src/core/tcp_read.c

+ 3 - 0
src/core/cfg.lex

@@ -399,6 +399,7 @@ TCP_OPT_KEEPCNT		"tcp_keepcnt"
 TCP_OPT_CRLF_PING	"tcp_crlf_ping"
 TCP_OPT_ACCEPT_NO_CL	"tcp_accept_no_cl"
 TCP_OPT_ACCEPT_HEP3	"tcp_accept_hep3"
+TCP_OPT_ACCEPT_HAPROXY	"tcp_accept_haproxy"
 TCP_CLONE_RCVBUF	"tcp_clone_rcvbuf"
 TCP_REUSE_PORT		"tcp_reuse_port"
 DISABLE_TLS		"disable_tls"|"tls_disable"
@@ -856,6 +857,8 @@ IMPORTFILE      "import_file"
 									return TCP_OPT_ACCEPT_NO_CL; }
 <INITIAL>{TCP_OPT_ACCEPT_HEP3}	{ count(); yylval.strval=yytext;
 									return TCP_OPT_ACCEPT_HEP3; }
+<INITIAL>{TCP_OPT_ACCEPT_HAPROXY}	{ count(); yylval.strval=yytext;
+									return TCP_OPT_ACCEPT_HAPROXY; }
 <INITIAL>{TCP_CLONE_RCVBUF}		{ count(); yylval.strval=yytext;
 									return TCP_CLONE_RCVBUF; }
 <INITIAL>{TCP_REUSE_PORT}	{ count(); yylval.strval=yytext; return TCP_REUSE_PORT; }

+ 9 - 0
src/core/cfg.y

@@ -433,6 +433,7 @@ extern char *default_routename;
 %token TCP_OPT_CRLF_PING
 %token TCP_OPT_ACCEPT_NO_CL
 %token TCP_OPT_ACCEPT_HEP3
+%token TCP_OPT_ACCEPT_HAPROXY
 %token TCP_CLONE_RCVBUF
 %token TCP_REUSE_PORT
 %token DISABLE_TLS
@@ -1230,6 +1231,14 @@ assign_stm:
 		#endif
 	}
 	| TCP_OPT_ACCEPT_HEP3 EQUAL error { yyerror("boolean value expected"); }
+	| TCP_OPT_ACCEPT_HAPROXY EQUAL NUMBER {
+		#ifdef USE_TCP
+			ksr_tcp_accept_haproxy=$3;
+		#else
+			warn("tcp support not compiled in");
+		#endif
+	}
+	| TCP_OPT_ACCEPT_HAPROXY EQUAL error { yyerror("boolean value expected"); }
 
 	| TCP_CLONE_RCVBUF EQUAL NUMBER {
 		#ifdef USE_TCP

+ 1 - 0
src/core/globals.h

@@ -100,6 +100,7 @@ extern int tcp_max_connections; /* maximum tcp connections, hard limit */
 extern int tls_max_connections; /* maximum tls connections, hard limit */
 #endif
 extern int ksr_tcp_accept_hep3;
+extern int ksr_tcp_accept_haproxy;
 #ifdef USE_TLS
 extern int tls_disable;
 extern unsigned short tls_port_no;

+ 244 - 10
src/core/tcp_main.c

@@ -46,6 +46,7 @@
 #define BSD_COMP  /* needed on older solaris for FIONREAD */
 #endif /* HAVE_FILIO_H / __OS_solaris */
 #include <sys/ioctl.h>  /* ioctl() used on write error */
+#include <arpa/inet.h>  /* for inet_pton() */
 #include <netinet/in.h>
 #include <netinet/in_systm.h>
 #include <netinet/ip.h>
@@ -957,7 +958,227 @@ end:
 }
 #endif
 
+/* Attempt to extract real connection information from an upstream load
+ * balancer or reverse proxy. This should be called right after accept()ing the
+ * connection, and before TLS negotiation.
+ *
+ * Returns:
+ *    -1 on parsing error (connection should be closed)
+ *    0 on parser success, and connection information was extracted
+ *    1 on parser success, but no connection information was provided by the
+ *      upstream load balancer or reverse proxy.
+ */
+int tcpconn_read_haproxy(struct tcp_connection *c) {
+	int bytes, retval = 0;
+	uint32_t size, port;
+	char *p, *end;
+	struct ip_addr *src_ip, *dst_ip;
+
+	const char v2sig[12] = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
+
+	// proxy header union
+	union {
+		// v1 struct
+		struct {
+			char line[108];
+		} v1;
+
+		// v2 struct
+		struct {
+			uint8_t sig[12];
+			uint8_t ver_cmd;
+			uint8_t fam;
+			uint16_t len;
+
+			union {
+				struct { /* for TCP/UDP over IPv4, len = 12 */
+					uint32_t src_addr;
+					uint32_t dst_addr;
+					uint16_t src_port;
+					uint16_t dst_port;
+				} ip4;
+
+				struct { /* for TCP/UDP over IPv6, len = 36 */
+					 uint8_t  src_addr[16];
+					 uint8_t  dst_addr[16];
+					 uint16_t src_port;
+					 uint16_t dst_port;
+				} ip6;
+
+				struct { /* for AF_UNIX sockets, len = 216 */
+					 uint8_t src_addr[108];
+					 uint8_t dst_addr[108];
+				} unx;
+			} addr;
+		} v2;
+
+	} hdr;
+
+	do {
+		bytes = recv(c->s, &hdr, sizeof(hdr), MSG_PEEK);
+	} while (bytes == -1 && (errno == EINTR || errno == EAGAIN));
+
+	src_ip = &c->rcv.src_ip;
+	dst_ip = &c->rcv.dst_ip;
+
+	if (bytes >= 16 && memcmp(&hdr.v2, v2sig, 12) == 0 &&
+		(hdr.v2.ver_cmd & 0xF0) == 0x20) {
+		LM_DBG("received PROXY protocol v2 header\n");
+		size = 16 + ntohs(hdr.v2.len);
+
+		if (bytes < size) {
+			return -1; /* truncated or too large header */
+		}
+
+		switch (hdr.v2.ver_cmd & 0xF) {
+			case 0x01: /* PROXY command */
+				switch (hdr.v2.fam) {
+					case 0x11: /* TCPv4 */
+						src_ip->af = AF_INET;
+						src_ip->len = 4;
+						src_ip->u.addr32[0] =
+							hdr.v2.addr.ip4.src_addr;
+						c->rcv.src_port =
+							hdr.v2.addr.ip4.src_port;
+
+						dst_ip->af = AF_INET;
+						dst_ip->len = 4;
+						dst_ip->u.addr32[0] =
+							hdr.v2.addr.ip4.dst_addr;
+						c->rcv.dst_port =
+							hdr.v2.addr.ip4.dst_port;
+
+						goto done;
+
+					case 0x21: /* TCPv6 */
+						src_ip->af = AF_INET6;
+						src_ip->len = 16;
+						memcpy(src_ip->u.addr,
+							hdr.v2.addr.ip6.src_addr, 16);
+						c->rcv.src_port =
+							hdr.v2.addr.ip6.src_port;
+
+						dst_ip->af = AF_INET6;
+						dst_ip->len = 16;
+						memcpy(dst_ip->u.addr,
+							hdr.v2.addr.ip6.src_addr, 16);
+						c->rcv.dst_port =
+							hdr.v2.addr.ip6.dst_port;
+
+						goto done;
+
+					default: /* unsupported protocol */
+						return -1;
+				}
+
+			case 0x00: /* LOCAL command */
+				retval = 1; /* keep local connection address for LOCAL */
+				goto done;
+
+			default:
+				return -1; /* not a supported command */
+		}
+	}
+	else if (bytes >= 8 && memcmp(hdr.v1.line, "PROXY", 5) == 0) {
+		LM_DBG("received PROXY protocol v1 header\n");
+		end = memchr(hdr.v1.line, '\r', bytes - 1);
+		if (!end || end[1] != '\n') {
+			return -1; /* partial or invalid header */
+		}
+		*end = '\0'; /* terminate the string to ease parsing */
+		size = end + 2 - hdr.v1.line;
+		p = hdr.v1.line + 5;
+
+		if (strncmp(p, " TCP", 4) == 0) {
+			switch (p[4]) {
+				case '4':
+					src_ip->af  = dst_ip->af  = AF_INET;
+					src_ip->len = dst_ip->len = 4;
+					break;
+				case '6':
+					src_ip->af  = dst_ip->af  = AF_INET6;
+					src_ip->len = dst_ip->len = 16;
+					break;
+				default:
+					return -1; /* unknown TCP version */
+			}
+
+			if (p[5] != ' ') {
+				return -1; /* misformatted header */
+			}
+			p += 6; /* skip over the already-parsed bytes */
+
+			/* Parse the source IP address */
+			end = strchr(p, ' ');
+			if (!end) {
+				return -1; /* truncated header */
+			}
+			*end = '\0'; /* mark the end of the IP address */
+			if (inet_pton(src_ip->af, p, src_ip->u.addr) != 1) {
+				return -1; /* missing IP address */
+			}
+			p = end + 1;
+
+			/* Parse the destination IP address */
+			end = strchr(p, ' ');
+			if (!end) {
+				return -1;
+			}
+			*end = '\0'; /* mark the end of the IP address */
+			if (inet_pton(dst_ip->af, p, dst_ip->u.addr) != 1) {
+				return -1;
+			}
+			p = end + 1;
+
+			/* Parse the source port */
+			port = strtoul(p, &end, 10);
+			if (port == ULONG_MAX || port == 0 || port >= (1 << 16)) {
+				return -1; /* invalid port number */
+			}
+			c->rcv.src_port = port;
+
+			if (*end != ' ') {
+				return -1; /* invalid header */
+			}
+			p = end + 1;
+
+			/* Parse the destination port */
+			port = strtoul(p, NULL, 10);
+			if (port == ULONG_MAX || port == 0 || port >= (1 << 16)) {
+				return -1; /* invalid port number */
+			}
+			c->rcv.dst_port = port;
+
+			goto done;
+		}
+		else if (strncmp(p, " UNKNOWN", 8) == 0) {
+			/* We know that the sender speaks the correct PROXY protocol with the
+			 * appropriate version, and we SHOULD accept the connection and use the
+			 * real connection's parameters as if there were no PROXY protocol header
+			 * on the wire.
+			 */
+			retval = 1; /* PROXY protocol parsed, but no IP override */
+			goto done;
+		}
+		else {
+			return -1; /* invalid header */
+		}
+	} else if (bytes == 0) {
+		return 1; /* EOF? Return "no IP change" in any case */
+	}
+	else {
+		/* Wrong protocol */
+		return -1;
+	}
 
+done:
+	/* we need to consume the appropriate amount of data from the socket */
+	do {
+		bytes = recv(c->s, &hdr, size, 0);
+	} while (bytes == -1 && errno == EINTR);
+
+	return (bytes >= 0) ? retval : -1;
+}
 
 struct tcp_connection* tcpconn_new(int sock, union sockaddr_union* su,
 									union sockaddr_union* local_addr,
@@ -965,7 +1186,7 @@ struct tcp_connection* tcpconn_new(int sock, union sockaddr_union* su,
 									int state)
 {
 	struct tcp_connection *c;
-	int rd_b_size;
+	int rd_b_size, ret;
 
 	rd_b_size=cfg_get(tcp, tcp_cfg, rd_buf_size);
 	c=shm_malloc(sizeof(struct tcp_connection) + rd_b_size);
@@ -985,16 +1206,29 @@ struct tcp_connection* tcpconn_new(int sock, union sockaddr_union* su,
 
 	atomic_set(&c->refcnt, 0);
 	local_timer_init(&c->timer, tcpconn_main_timeout, c, 0);
-	su2ip_addr(&c->rcv.src_ip, su);
-	c->rcv.src_port=su_getport(su);
-	c->rcv.bind_address=ba;
-	if (likely(local_addr)){
-		su2ip_addr(&c->rcv.dst_ip, local_addr);
-		c->rcv.dst_port=su_getport(local_addr);
-	}else if (ba){
-		c->rcv.dst_ip=ba->address;
-		c->rcv.dst_port=ba->port_no;
+	if (unlikely(ksr_tcp_accept_haproxy && state == S_CONN_ACCEPT)) {
+		ret = tcpconn_read_haproxy(c);
+
+		if (ret == -1) {
+			LM_ERR("invalid PROXY protocol header\n");
+			goto error;
+		} else if (ret == 1) {
+			LM_DBG("PROXY protocol did not override IP addresses\n");
+			goto read_ip_info;
+		}
+	} else {
+read_ip_info:
+		su2ip_addr(&c->rcv.src_ip, su);
+		c->rcv.src_port=su_getport(su);
+		if (likely(local_addr)){
+			su2ip_addr(&c->rcv.dst_ip, local_addr);
+			c->rcv.dst_port=su_getport(local_addr);
+		}else if (ba){
+			c->rcv.dst_ip=ba->address;
+			c->rcv.dst_port=ba->port_no;
+		}
 	}
+	c->rcv.bind_address=ba;
 	print_ip("tcpconn_new: new tcp connection: ", &c->rcv.src_ip, "\n");
 	LM_DBG("on port %d, type %d\n", c->rcv.src_port, type);
 	init_tcp_req(&c->req, (char*)c+sizeof(struct tcp_connection), rd_b_size);

+ 1 - 1
src/core/tcp_read.c

@@ -39,7 +39,6 @@
 #include <unistd.h>
 #include <stdlib.h> /* for abort() */
 
-
 #include "dprint.h"
 #include "tcp_conn.h"
 #include "tcp_read.h"
@@ -93,6 +92,7 @@ static ticks_t tcp_reader_prev_ticks;
 int is_msg_complete(struct tcp_req* r);
 
 int ksr_tcp_accept_hep3=0;
+int ksr_tcp_accept_haproxy=0;
 /**
  * control cloning of TCP receive buffer
  * - needed for operations working directly inside the buffer