From: Willy Tarreau Date: Fri, 31 Aug 2012 15:43:29 +0000 (+0200) Subject: MAJOR: connection: make the PROXY decoder a handshake handler X-Git-Tag: v1.5-dev12~50 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=22cda21ad5683c59fafd2de4367fe2699b8e8591;p=thirdparty%2Fhaproxy.git MAJOR: connection: make the PROXY decoder a handshake handler The PROXY protocol is now decoded in the connection before other handshakes. This means that it may be extracted from a TCP stream before SSL is decoded from this stream. --- diff --git a/include/proto/connection.h b/include/proto/connection.h index 007f9e2638..a575e69177 100644 --- a/include/proto/connection.h +++ b/include/proto/connection.h @@ -30,6 +30,9 @@ */ int conn_fd_handler(int fd); +/* receive a PROXY protocol header over a connection */ +int conn_recv_proxy(struct connection *conn, int flag); + /* calls the init() function of the data layer if any. Returns <0 in case of * error. */ diff --git a/include/proto/session.h b/include/proto/session.h index d88a19ee30..b9eb447717 100644 --- a/include/proto/session.h +++ b/include/proto/session.h @@ -47,7 +47,7 @@ int parse_track_counters(char **args, int *arg, int section_type, struct proxy *curpx, struct track_ctr_prm *prm, struct proxy *defpx, char **err); -int conn_session_initialize(struct connection *conn, int flag); +int conn_session_complete(struct connection *conn, int flag); /* Remove the refcount from the session to the tracked counters, and clear the * pointer to ensure this is only performed once. The caller is responsible for diff --git a/include/types/connection.h b/include/types/connection.h index b74c71ae38..6a36dc7534 100644 --- a/include/types/connection.h +++ b/include/types/connection.h @@ -71,9 +71,10 @@ enum { /* flags below are used for connection handshakes */ CO_FL_SI_SEND_PROXY = 0x00000020, /* send a valid PROXY protocol header */ + CO_FL_ACCEPT_PROXY = 0x00000080, /* send a valid PROXY protocol header */ /* below we have all handshake flags grouped into one */ - CO_FL_HANDSHAKE = CO_FL_SI_SEND_PROXY, + CO_FL_HANDSHAKE = CO_FL_SI_SEND_PROXY | CO_FL_ACCEPT_PROXY, CO_FL_INIT_SESS = 0x00000800, /* initialize a session before using data */ diff --git a/src/cfgparse.c b/src/cfgparse.c index 978178345e..28a9751e91 100644 --- a/src/cfgparse.c +++ b/src/cfgparse.c @@ -6600,9 +6600,6 @@ out_uri_auth_compat: listener->handler = process_session; listener->analysers |= curproxy->fe_req_ana; - if (listener->options & LI_O_ACC_PROXY) - listener->analysers |= AN_REQ_DECODE_PROXY; - if (!LIST_ISEMPTY(&curproxy->tcp_req.l4_rules)) listener->options |= LI_O_TCP_RULES; diff --git a/src/connection.c b/src/connection.c index 9e3d79eb95..44e675b289 100644 --- a/src/connection.c +++ b/src/connection.c @@ -39,6 +39,10 @@ int conn_fd_handler(int fd) if (unlikely(conn->flags & CO_FL_ERROR)) goto leave; + if (conn->flags & CO_FL_ACCEPT_PROXY) + if (!conn_recv_proxy(conn, CO_FL_ACCEPT_PROXY)) + goto leave; + if (conn->flags & CO_FL_SI_SEND_PROXY) if (!conn_si_send_proxy(conn, CO_FL_SI_SEND_PROXY)) goto leave; @@ -53,7 +57,7 @@ int conn_fd_handler(int fd) * we must not use it anymore and should immediately leave instead. */ if ((conn->flags & CO_FL_INIT_SESS) && - conn_session_initialize(conn, CO_FL_INIT_SESS) < 0) + conn_session_complete(conn, CO_FL_INIT_SESS) < 0) return 0; if (fdtab[fd].ev & (FD_POLL_IN | FD_POLL_HUP | FD_POLL_ERR)) diff --git a/src/frontend.c b/src/frontend.c index 415960a51a..89c37528c0 100644 --- a/src/frontend.c +++ b/src/frontend.c @@ -419,6 +419,199 @@ int frontend_decode_proxy_request(struct session *s, struct channel *req, int an return 0; } +/* This handshake handler waits a PROXY protocol header at the beginning of the + * raw data stream. The header looks like this : + * + * "PROXY" PROTO SRC3 DST3 SRC4 "\r\n" + * + * There must be exactly one space between each field. Fields are : + * - PROTO : layer 4 protocol, which must be "TCP4" or "TCP6". + * - SRC3 : layer 3 (eg: IP) source address in standard text form + * - DST3 : layer 3 (eg: IP) destination address in standard text form + * - SRC4 : layer 4 (eg: TCP port) source address in standard text form + * - DST4 : layer 4 (eg: TCP port) destination address in standard text form + * + * This line MUST be at the beginning of the buffer and MUST NOT wrap. + * + * 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_proxy(struct connection *conn, int flag) +{ + char *line, *end; + int len; + + /* we might have been called just after an asynchronous shutr */ + if (conn->flags & CO_FL_SOCK_RD_SH) + goto fail; + + do { + len = recv(conn->t.sock.fd, trash, trashlen, MSG_PEEK); + if (len < 0) { + if (errno == EINTR) + continue; + if (errno == EAGAIN) { + conn_sock_poll_recv(conn); + return 0; + } + goto fail; + } + } while (0); + + if (len < 6) + goto missing; + + line = trash; + end = trash + len; + + /* Decode a possible proxy request, fail early if it does not match */ + if (strncmp(line, "PROXY ", 6) != 0) + goto fail; + + line += 6; + if (len < 18) /* shortest possible line */ + goto missing; + + if (!memcmp(line, "TCP4 ", 5) != 0) { + u32 src3, dst3, sport, dport; + + line += 5; + + src3 = inetaddr_host_lim_ret(line, end, &line); + if (line == end) + goto missing; + if (*line++ != ' ') + goto fail; + + dst3 = inetaddr_host_lim_ret(line, end, &line); + if (line == end) + goto missing; + if (*line++ != ' ') + goto fail; + + sport = read_uint((const char **)&line, end); + if (line == end) + goto missing; + if (*line++ != ' ') + goto fail; + + dport = read_uint((const char **)&line, end); + if (line > end - 2) + goto missing; + if (*line++ != '\r') + goto fail; + if (*line++ != '\n') + goto fail; + + /* 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 = htonl(src3); + ((struct sockaddr_in *)&conn->addr.from)->sin_port = htons(sport); + + ((struct sockaddr_in *)&conn->addr.to)->sin_family = AF_INET; + ((struct sockaddr_in *)&conn->addr.to)->sin_addr.s_addr = htonl(dst3); + ((struct sockaddr_in *)&conn->addr.to)->sin_port = htons(dport); + conn->flags |= CO_FL_ADDR_FROM_SET | CO_FL_ADDR_TO_SET; + } + else if (!memcmp(line, "TCP6 ", 5) != 0) { + u32 sport, dport; + char *src_s; + char *dst_s, *sport_s, *dport_s; + struct in6_addr src3, dst3; + + line += 5; + + src_s = line; + dst_s = sport_s = dport_s = NULL; + while (1) { + if (line > end - 2) { + goto missing; + } + else if (*line == '\r') { + *line = 0; + line++; + if (*line++ != '\n') + goto fail; + break; + } + + if (*line == ' ') { + *line = 0; + if (!dst_s) + dst_s = line + 1; + else if (!sport_s) + sport_s = line + 1; + else if (!dport_s) + dport_s = line + 1; + } + line++; + } + + if (!dst_s || !sport_s || !dport_s) + goto fail; + + sport = read_uint((const char **)&sport_s,dport_s - 1); + if (*sport_s != 0) + goto fail; + + dport = read_uint((const char **)&dport_s,line - 2); + if (*dport_s != 0) + goto fail; + + if (inet_pton(AF_INET6, src_s, (void *)&src3) != 1) + goto fail; + + if (inet_pton(AF_INET6, dst_s, (void *)&dst3) != 1) + goto fail; + + /* update the session's addresses and mark them set */ + ((struct sockaddr_in6 *)&conn->addr.from)->sin6_family = AF_INET6; + memcpy(&((struct sockaddr_in6 *)&conn->addr.from)->sin6_addr, &src3, sizeof(struct in6_addr)); + ((struct sockaddr_in6 *)&conn->addr.from)->sin6_port = htons(sport); + + ((struct sockaddr_in6 *)&conn->addr.to)->sin6_family = AF_INET6; + memcpy(&((struct sockaddr_in6 *)&conn->addr.to)->sin6_addr, &dst3, sizeof(struct in6_addr)); + ((struct sockaddr_in6 *)&conn->addr.to)->sin6_port = htons(dport); + conn->flags |= CO_FL_ADDR_FROM_SET | CO_FL_ADDR_TO_SET; + } + else { + goto fail; + } + + /* remove the PROXY line from the request. For this we re-read the + * exact line at once. If we don't get the exact same result, we + * fail. + */ + len = line - trash; + do { + int len2 = recv(conn->t.sock.fd, trash, len, 0); + if (len2 < 0 && errno == EINTR) + continue; + if (len2 != len) + goto fail; + } 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. + */ + fail: + conn_sock_stop_both(conn); + conn->flags |= CO_FL_ERROR; + conn->flags &= ~flag; + return 0; +} + /* Makes a PROXY protocol line from the two addresses. The output is sent to * buffer for a maximum size of (including the trailing zero). * It returns the number of bytes composing this line (including the trailing diff --git a/src/session.c b/src/session.c index eb32496358..9c5920dd6a 100644 --- a/src/session.c +++ b/src/session.c @@ -125,6 +125,12 @@ int session_accept(struct listener *l, int cfd, struct sockaddr_storage *addr) goto out_free_session; } + /* wait for a PROXY protocol header */ + if (l->options & LI_O_ACC_PROXY) { + s->si[0].conn.flags |= CO_FL_ACCEPT_PROXY; + conn_sock_want_recv(&s->si[0].conn); + } + /* Adjust some socket options */ if (unlikely(fcntl(cfd, F_SETFL, O_NONBLOCK) == -1)) goto out_free_session; @@ -231,14 +237,14 @@ static void kill_mini_session(struct session *s) pool_free2(pool2_session, s); } -/* Finish initializing a session from a connection. Returns <0 if the - * connection was killed. +/* Finish initializing a session from a connection, or kills it if the + * connection shows and error. Returns <0 if the connection was killed. */ -int conn_session_initialize(struct connection *conn, int flag) +int conn_session_complete(struct connection *conn, int flag) { struct session *s = container_of(conn, struct session, si[0].conn); - if (session_complete(s) > 0) { + if (!(conn->flags & CO_FL_ERROR) && (session_complete(s) > 0)) { conn->flags &= ~flag; return 0; }