From edcf6687d68cbc2d612d9ae2dd36028ec99503d1 Mon Sep 17 00:00:00 2001 From: Willy Tarreau Date: Sun, 30 Nov 2008 23:15:34 +0100 Subject: [PATCH] [MEDIUM] extract TCP request processing from HTTP The TCP analyser has moved to proto_tcp.c. Breaking the function has required finer use of the return value and adding some tests to process_session(). --- include/proto/proto_tcp.h | 1 + src/proto_http.c | 124 +++++------------------------------- src/proto_tcp.c | 130 ++++++++++++++++++++++++++++++++++++-- src/session.c | 24 ++++++- 4 files changed, 163 insertions(+), 116 deletions(-) diff --git a/include/proto/proto_tcp.h b/include/proto/proto_tcp.h index ee7586c61..109bc09ac 100644 --- a/include/proto/proto_tcp.h +++ b/include/proto/proto_tcp.h @@ -32,6 +32,7 @@ int tcpv4_bind_socket(int fd, int flags, struct sockaddr_in *local, struct socka void tcpv4_add_listener(struct listener *listener); void tcpv6_add_listener(struct listener *listener); int tcp_bind_listener(struct listener *listener, char *errmsg, int errlen); +int tcp_inspect_request(struct session *s, struct buffer *req); #endif /* _PROTO_PROTO_TCP_H */ diff --git a/src/proto_http.c b/src/proto_http.c index b52607675..a3750bd18 100644 --- a/src/proto_http.c +++ b/src/proto_http.c @@ -1542,32 +1542,30 @@ void http_msg_analyzer(struct buffer *buf, struct http_msg *msg, struct hdr_idx } /* This function performs all the processing enabled for the current request. - * It normally returns zero, but may return 1 if it absolutely needs to be - * called again after other functions. It relies on buffers flags, and updates - * t->req->analysers. It might make sense to explode it into several other - * functions. Its behaviour is rather simple : + * It returns 1 if the processing can continue on next analysers, or zero if it + * needs more data, encounters an error, or wants to immediately abort the + * request. It relies on buffers flags, and updates s->req->analysers. Its + * behaviour is rather simple: * - all enabled analysers are called in turn from the lower to the higher * bit. - * - if an analyser does not have enough data, it must return without calling - * other ones. It should also probably reset the BF_WRITE_ENA bit to ensure + * - the analyser must check for errors and timeouts, and react as expected. + * It does not have to close anything upon error, the caller will. + * - if the analyser does not have enough data, it must return 0without calling + * other ones. It should also probably do a buffer_write_dis() to ensure * that unprocessed data will not be forwarded. But that probably depends on - * the protocol. Generally it is not reset in case of errors. + * the protocol. * - if an analyser has enough data, it just has to pass on to the next - * analyser without touching BF_WRITE_ENA (it is enabled prior to - * analysis). + * analyser without using buffer_write_dis() (enabled by default). * - if an analyser thinks it has no added value anymore staying here, it must * reset its bit from the analysers flags in order not to be called anymore. * * In the future, analysers should be able to indicate that they want to be * called after XXX bytes have been received (or transfered), and the min of * all's wishes will be used to ring back (unless a special condition occurs). - * - * */ int process_request(struct session *t) { struct buffer *req = t->req; - struct buffer *rep = t->rep; DPRINTF(stderr,"[%u] %s: session=%p b=%p, exp(r,w)=%u,%u bf=%08x bl=%d analysers=%02x\n", now_ms, __FUNCTION__, @@ -1578,98 +1576,6 @@ int process_request(struct session *t) req->l, req->analysers); - /* The tcp-inspect analyser is always called alone */ - if (req->analysers & AN_REQ_INSPECT) { - struct tcp_rule *rule; - int partial; - - /* We will abort if we encounter a read error. In theory, we - * should not abort if we get a close, it might be valid, - * although very unlikely. FIXME: we'll abort for now, this - * will be easier to change later. - */ - if (req->flags & BF_READ_ERROR) { - req->analysers = 0; - //t->fe->failed_req++; - if (!(t->flags & SN_ERR_MASK)) - t->flags |= SN_ERR_CLICL; - if (!(t->flags & SN_FINST_MASK)) - t->flags |= SN_FINST_R; - return 0; - } - - /* Abort if client read timeout has expired */ - else if (req->flags & BF_READ_TIMEOUT) { - req->analysers = 0; - t->fe->failed_req++; - if (!(t->flags & SN_ERR_MASK)) - t->flags |= SN_ERR_CLITO; - if (!(t->flags & SN_FINST_MASK)) - t->flags |= SN_FINST_R; - return 0; - } - - /* We don't know whether we have enough data, so must proceed - * this way : - * - iterate through all rules in their declaration order - * - if one rule returns MISS, it means the inspect delay is - * not over yet, then return immediately, otherwise consider - * it as a non-match. - * - if one rule returns OK, then return OK - * - if one rule returns KO, then return KO - */ - - if (req->flags & BF_SHUTR || tick_is_expired(req->analyse_exp, now_ms)) - partial = 0; - else - partial = ACL_PARTIAL; - - list_for_each_entry(rule, &t->fe->tcp_req.inspect_rules, list) { - int ret = ACL_PAT_PASS; - - if (rule->cond) { - ret = acl_exec_cond(rule->cond, t->fe, t, NULL, ACL_DIR_REQ | partial); - if (ret == ACL_PAT_MISS) { - buffer_write_dis(req); - /* just set the request timeout once at the beginning of the request */ - if (!tick_isset(req->analyse_exp)) - req->analyse_exp = tick_add_ifset(now_ms, t->fe->tcp_req.inspect_delay); - return 0; - } - - ret = acl_pass(ret); - if (rule->cond->pol == ACL_COND_UNLESS) - ret = !ret; - } - - if (ret) { - /* we have a matching rule. */ - if (rule->action == TCP_ACT_REJECT) { - buffer_abort(req); - buffer_abort(rep); - //FIXME: this delete this - //fd_delete(t->cli_fd); - //t->cli_state = CL_STCLOSE; - req->analysers = 0; - t->fe->failed_req++; - if (!(t->flags & SN_ERR_MASK)) - t->flags |= SN_ERR_PRXCOND; - if (!(t->flags & SN_FINST_MASK)) - t->flags |= SN_FINST_R; - return 0; - } - /* otherwise accept */ - break; - } - } - - /* if we get there, it means we have no rule which matches, or - * we have an explicit accept, so we apply the default accept. - */ - req->analysers &= ~AN_REQ_INSPECT; - req->analyse_exp = TICK_ETERNITY; - } - if (req->analysers & AN_REQ_HTTP_HDR) { /* * Now parse the partial (or complete) lines. @@ -2141,8 +2047,10 @@ int process_request(struct session *t) * FIXME!!! that one is rather dangerous, we want to * make it follow standard rules (eg: clear req->analysers). */ - if (stats_check_uri_auth(t, rule_set)) - return 1; + if (stats_check_uri_auth(t, rule_set)) { + req->analysers = 0; + return 0; + } } /* now check whether we have some switching rules for this request */ @@ -2193,7 +2101,6 @@ int process_request(struct session *t) t->flags |= SN_BE_ASSIGNED; } } while (t->be != cur_proxy); /* we loop only if t->be has changed */ - if (!(t->flags & SN_BE_ASSIGNED)) { /* To ensure correct connection accounting on @@ -2305,7 +2212,7 @@ int process_request(struct session *t) memcpy(trash, t->fe->fwdfor_hdr_name, len); } len += sprintf(trash + len, ": %s", pn); - + if (unlikely(http_header_add_tail2(req, &txn->req, &txn->hdr_idx, trash, len)) < 0) goto return_bad_req; @@ -2574,7 +2481,6 @@ int process_request(struct session *t) ABORT_NOW(); } #endif - req->analysers &= AN_REQ_INSPECT | AN_REQ_HTTP_HDR | AN_REQ_HTTP_TARPIT | AN_REQ_HTTP_BODY; return 0; } diff --git a/src/proto_tcp.c b/src/proto_tcp.c index adc32d1e0..ce0c43ac6 100644 --- a/src/proto_tcp.c +++ b/src/proto_tcp.c @@ -203,7 +203,7 @@ int tcp_bind_listener(struct listener *listener, char *errmsg, int errlen) msg = "cannot create listening socket"; goto tcp_return; } - + if (fd >= global.maxsock) { err |= ERR_FATAL | ERR_ABORT | ERR_ALERT; msg = "not enough free sockets (raise '-n' parameter)"; @@ -226,7 +226,7 @@ int tcp_bind_listener(struct listener *listener, char *errmsg, int errlen) if (listener->options & LI_O_NOLINGER) setsockopt(fd, SOL_SOCKET, SO_LINGER, (struct linger *) &nolinger, sizeof(struct linger)); - + #ifdef SO_REUSEPORT /* OpenBSD supports this. As it's present in old libc versions of Linux, * it might return an error that we will silently ignore. @@ -234,7 +234,7 @@ int tcp_bind_listener(struct listener *listener, char *errmsg, int errlen) setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, (char *) &one, sizeof(one)); #endif #ifdef CONFIG_HAP_LINUX_TPROXY - if ((listener->options & LI_O_FOREIGN) + if ((listener->options & LI_O_FOREIGN) && (setsockopt(fd, SOL_IP, IP_TRANSPARENT, (char *) &one, sizeof(one)) == -1) && (setsockopt(fd, SOL_IP, IP_FREEBIND, (char *) &one, sizeof(one)) == -1)) { msg = "cannot make listening socket transparent"; @@ -246,13 +246,13 @@ int tcp_bind_listener(struct listener *listener, char *errmsg, int errlen) msg = "cannot bind socket"; goto tcp_close_return; } - + if (listen(fd, listener->backlog ? listener->backlog : listener->maxconn) == -1) { err |= ERR_RETRYABLE | ERR_ALERT; msg = "cannot listen to socket"; goto tcp_close_return; } - + /* the socket is ready */ listener->fd = fd; listener->state = LI_LISTEN; @@ -325,6 +325,126 @@ void tcpv6_add_listener(struct listener *listener) proto_tcpv6.nb_listeners++; } +/* This function performs the TCP request analysis on the current request. It + * returns 1 if the processing can continue on next analysers, or zero if it + * needs more data, encounters an error, or wants to immediately abort the + * request. It relies on buffers flags, and updates s->req->analysers. Its + * behaviour is rather simple: + * - the analyser must check for errors and timeouts, and react as expected. + * It does not have to close anything upon error, the caller will. + * - if the analyser does not have enough data, it must return 0without calling + * other ones. It should also probably do a buffer_write_dis() to ensure + * that unprocessed data will not be forwarded. But that probably depends on + * the protocol. + * - if an analyser has enough data, it just has to pass on to the next + * analyser without using buffer_write_dis() (enabled by default). + * - if an analyser thinks it has no added value anymore staying here, it must + * reset its bit from the analysers flags in order not to be called anymore. + * + * In the future, analysers should be able to indicate that they want to be + * called after XXX bytes have been received (or transfered), and the min of + * all's wishes will be used to ring back (unless a special condition occurs). + */ +int tcp_inspect_request(struct session *s, struct buffer *req) +{ + struct tcp_rule *rule; + int partial; + + DPRINTF(stderr,"[%u] %s: session=%p b=%p, exp(r,w)=%u,%u bf=%08x bl=%d analysers=%02x\n", + now_ms, __FUNCTION__, + s, + req, + req->rex, req->wex, + req->flags, + req->l, + req->analysers); + + /* We will abort if we encounter a read error. In theory, we + * should not abort if we get a close, it might be valid, + * although very unlikely. FIXME: we'll abort for now, this + * will be easier to change later. + */ + if (req->flags & BF_READ_ERROR) { + req->analysers = 0; + s->fe->failed_req++; + if (!(s->flags & SN_ERR_MASK)) + s->flags |= SN_ERR_CLICL; + if (!(s->flags & SN_FINST_MASK)) + s->flags |= SN_FINST_R; + return 0; + } + + /* Abort if client read timeout has expired */ + else if (req->flags & BF_READ_TIMEOUT) { + req->analysers = 0; + s->fe->failed_req++; + if (!(s->flags & SN_ERR_MASK)) + s->flags |= SN_ERR_CLITO; + if (!(s->flags & SN_FINST_MASK)) + s->flags |= SN_FINST_R; + return 0; + } + + /* We don't know whether we have enough data, so must proceed + * this way : + * - iterate through all rules in their declaration order + * - if one rule returns MISS, it means the inspect delay is + * not over yet, then return immediately, otherwise consider + * it as a non-match. + * - if one rule returns OK, then return OK + * - if one rule returns KO, then return KO + */ + + if (req->flags & BF_SHUTR || tick_is_expired(req->analyse_exp, now_ms)) + partial = 0; + else + partial = ACL_PARTIAL; + + list_for_each_entry(rule, &s->fe->tcp_req.inspect_rules, list) { + int ret = ACL_PAT_PASS; + + if (rule->cond) { + ret = acl_exec_cond(rule->cond, s->fe, s, NULL, ACL_DIR_REQ | partial); + if (ret == ACL_PAT_MISS) { + buffer_write_dis(req); + /* just set the request timeout once at the beginning of the request */ + if (!tick_isset(req->analyse_exp)) + req->analyse_exp = tick_add_ifset(now_ms, s->fe->tcp_req.inspect_delay); + return 0; + } + + ret = acl_pass(ret); + if (rule->cond->pol == ACL_COND_UNLESS) + ret = !ret; + } + + if (ret) { + /* we have a matching rule. */ + if (rule->action == TCP_ACT_REJECT) { + buffer_abort(req); + buffer_abort(s->rep); + req->analysers = 0; + s->fe->failed_req++; + if (!(s->flags & SN_ERR_MASK)) + s->flags |= SN_ERR_PRXCOND; + if (!(s->flags & SN_FINST_MASK)) + s->flags |= SN_FINST_R; + return 0; + } + /* otherwise accept */ + break; + } + } + + /* if we get there, it means we have no rule which matches, or + * we have an explicit accept, so we apply the default accept. + */ + req->analysers &= ~AN_REQ_INSPECT; + req->analyse_exp = TICK_ETERNITY; + return 1; +} + + /* This function should be called to parse a line starting with the "tcp-request" * keyword. */ diff --git a/src/session.c b/src/session.c index 54b312b51..e13bca648 100644 --- a/src/session.c +++ b/src/session.c @@ -693,8 +693,28 @@ resync_stream_interface: if (s->req->prod->state >= SI_ST_EST) { /* it's up to the analysers to reset write_ena */ buffer_write_ena(s->req); - if (s->req->analysers) - process_request(s); + + /* We will call all analysers for which a bit is set in + * s->req->analysers, following the bit order from LSB + * to MSB. The analysers must remove themselves from + * the list when not needed. This while() loop is in + * fact a cleaner if(). + */ + while (s->req->analysers) { + if (s->req->analysers & AN_REQ_INSPECT) + if (!tcp_inspect_request(s, s->req)) + break; + + if (s->req->analysers) + if (!process_request(s)) + break; + + /* Just make sure that nobody set a wrong flag causing an endless loop */ + s->req->analysers &= AN_REQ_INSPECT | AN_REQ_HTTP_HDR | AN_REQ_HTTP_TARPIT | AN_REQ_HTTP_BODY; + + /* we don't want to loop anyway */ + break; + } } s->req->flags &= BF_CLEAR_READ & BF_CLEAR_WRITE & BF_CLEAR_TIMEOUT; flags &= BF_CLEAR_READ & BF_CLEAR_WRITE & BF_CLEAR_TIMEOUT;