Hi, attached is a patch which adds a new option to HAProxy called 'inject' for the mode 'tcp'. In the current version of this patch you can only add data at the beginning of the session. I think this is very useful - at least for me it is. :))
The configuration syntax is the following (I will update the documentation with a more recent patch): option inject req 1 forwardfor option inject req 2 data "EHLO localhost.localdomain" option inject req 3 data "GET / HTTP/1.1" option inject rsp 1 data "500 Service Unavailable" There are still some things open which I want to add later. If someone has ideas about things which should be implemented, feel free to reply. :) - remove code duplication. * X-Forward-For, X-Original-Dst in src/cfgparse.c and src/proto_tcp.c, maybe src/proto_http.c - add support for inject data on response. * currently it works only on request. - add session injection based on request or response. * currently injection only works at the beginning of request and first response. - modify session information based on regular expression for request or response. * currently this isn't possible. This will make it possible to inject data at every time of the session without knowing the underlying protocol. The attached patch is very basic, but I send it in that early stage, to get some feedback if it is a feature for HAProxy or not. Comments are welcome. :) Also it should be possible to transfer some very minimalistic html pages like: option inject rsp 1 data "<html>" option inject rsp 2 data "<head>" option inject rsp 3 data "<title>Error</title>" option inject rsp 4 data "</head>" option inject rsp 5 data "<body>Access denied</body>" option inject rsp 6 data "</html> For example: With a later version of this patch it is possible to create a service named 'smtp-mail-me' and at connect via telnet it can do the following: >> EHLO localhost.localdomain << 250-mydomain.de Hello localhost.localdomain [127.0.0.1] << 250-SIZE 52428800 << 250-PIPELINING >> AUTH PLAIN XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== << 235 Authentication succeeded >> MAIL FROM:<mbroe...@plusserver.de> << 250 Ok >> RCPT TO:<mbroe...@plusserver.de> << 250 Accepted >> DATA >> ... >> . << 250 Ok id=1LudBO-0000SQ-Qa >> QUIT Every other text-based protocol can be handled in such case too. I want to mention that I don't know exactly how the timeout handling is done in HAProxy. The source is very anoying to me there, some hints would be nice. :) --Maik
diff -Nur haproxy-1.3.17/include/proto/proto_tcp.h haproxy-1.3.17-tcp-inject/include/proto/proto_tcp.h --- haproxy-1.3.17/include/proto/proto_tcp.h 2009-03-29 15:26:57.000000000 +0200 +++ haproxy-1.3.17-tcp-inject/include/proto/proto_tcp.h 2009-04-17 02:37:44.000000000 +0200 @@ -33,6 +33,7 @@ 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); +int tcp_inject_request(struct session *s, struct buffer *req); #endif /* _PROTO_PROTO_TCP_H */ diff -Nur haproxy-1.3.17/include/types/buffers.h haproxy-1.3.17-tcp-inject/include/types/buffers.h --- haproxy-1.3.17/include/types/buffers.h 2009-03-29 15:26:57.000000000 +0200 +++ haproxy-1.3.17-tcp-inject/include/types/buffers.h 2009-04-17 02:37:44.000000000 +0200 @@ -111,6 +111,7 @@ #define AN_REQ_HTTP_TARPIT 0x00000008 /* wait for end of HTTP tarpit */ #define AN_RTR_HTTP_HDR 0x00000010 /* inspect HTTP response headers */ #define AN_REQ_UNIX_STATS 0x00000020 /* process unix stats socket request */ +#define AN_REQ_INJECT 0x00000040 /* inject header lines into tcp session */ /* describes a chunk of string */ struct chunk { diff -Nur haproxy-1.3.17/src/cfgparse.c haproxy-1.3.17-tcp-inject/src/cfgparse.c --- haproxy-1.3.17/src/cfgparse.c 2009-03-29 15:26:57.000000000 +0200 +++ haproxy-1.3.17-tcp-inject/src/cfgparse.c 2009-04-17 04:12:18.000000000 +0200 @@ -1604,6 +1604,78 @@ } } } + else if (!strcmp(args[1], "inject")) { + int cur_arg; + + /* inject x-forwarded-for field at the beginning of tcp session. */ + curproxy->options |= PR_O_FWDFOR; + + free(curproxy->fwdfor_hdr_name); + curproxy->fwdfor_hdr_name = strdup(DEF_XFORWARDFOR_HDR); + curproxy->fwdfor_hdr_len = strlen(DEF_XFORWARDFOR_HDR); + + /* next argument must be "req" or "rep" */ + if (!strcmp(args[2], "req")) { + curproxy->listen->analysers |= AN_REQ_INJECT; + } else if (!strcmp(args[2], "rsp")) { +// TODO: implement it + Alert("parsing [%s:%d] : '%s %s %s' not yet implemented.\n", + file, linenum, args[0], args[1], args[2]); + return -1; + } else { + Alert("parsing [%s:%d] : '%s %s' expects 'req' or 'rsp' as argument.\n", + file, linenum, args[0], args[1]); + return -1; + } + + /* next argument is the inject order. */ +// TODO: implement it atol(args[3]); + + /* check what data should be injected. */ + if (!strcmp(args[4], "forwardfor")) { + + /* loop to go through arguments - start at 2, since 0+1 = "option" "inject" */ + cur_arg = 5; + while (*(args[cur_arg])) { + if (!strcmp(args[cur_arg], "except")) { + /* suboption except - needs additional argument for it */ + if (!*(args[cur_arg+1]) || !str2net(args[cur_arg+1], &curproxy->except_net, &curproxy->except_mask)) { + Alert("parsing [%s:%d] : '%s %s %s %s %s %s' expects <address>[/mask] as argument.\n", + file, linenum, args[0], args[1], args[2], args[3], args[4], args[cur_arg]); + return -1; + } + /* flush useless bits */ + curproxy->except_net.s_addr &= curproxy->except_mask.s_addr; + cur_arg += 2; + } else if (!strcmp(args[cur_arg], "header")) { + /* suboption header - needs additional argument for it */ + if (*(args[cur_arg+1]) == 0) { + Alert("parsing [%s:%d] : '%s %s %s %s %s %s' expects <header_name> as argument.\n", + file, linenum, args[0], args[1], args[2], args[3], args[4], args[cur_arg]); + return -1; + } + free(curproxy->fwdfor_hdr_name); + curproxy->fwdfor_hdr_name = strdup(args[cur_arg+1]); + curproxy->fwdfor_hdr_len = strlen(curproxy->fwdfor_hdr_name); + cur_arg += 2; + } else { + /* unknown suboption - catchall */ + Alert("parsing [%s:%d] : '%s %s' only supports optional values: 'except' and 'header'.\n", + file, linenum, args[0], args[1]); + return -1; + } + } /* end while loop */ + } else if (!strcmp(args[4], "data")) { +// TODO: implement it + Alert("parsing [%s:%d] : '%s %s %s %s %s' not yet implemented.\n", + file, linenum, args[0], args[1], args[2], args[3], args[4]); + return -1; + } else { + Alert("parsing [%s:%d] : '%s %s %s %s' expects 'forwardfor' or 'data' as argument.\n", + file, linenum, args[0], args[1], args[2], args[3]); + return -1; + } + } else if (!strcmp(args[1], "forwardfor")) { int cur_arg; diff -Nur haproxy-1.3.17/src/proto_tcp.c haproxy-1.3.17-tcp-inject/src/proto_tcp.c --- haproxy-1.3.17/src/proto_tcp.c 2009-03-29 15:26:57.000000000 +0200 +++ haproxy-1.3.17-tcp-inject/src/proto_tcp.c 2009-04-17 02:37:44.000000000 +0200 @@ -40,6 +40,7 @@ #include <proto/acl.h> #include <proto/backend.h> #include <proto/buffers.h> +#include <proto/client.h> #include <proto/fd.h> #include <proto/protocols.h> #include <proto/proto_tcp.h> @@ -428,6 +429,136 @@ return 1; } +/* Add some data in the current server TCP session. This makes a backend + * behind us able to parse them and make decisions based on it. + */ +int tcp_inject_request(struct session *s, struct buffer *req) +{ + + /* position of buffer. */ + int buf_pos = 0; + + 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); + + /* FIXME: I have no real idea how the timeouts are handled here, + * so is it required to add some handler here? + */ + + /* 1: add X-Forward-For if either the frontend or the backend asks for it. (IPv4) */ + if (((s->fe->options | s->be->options) & PR_O_FWDFOR) != 0 && s->cli_addr.ss_family == AF_INET) { + + /* add an X-Forward-For header unless the source ip is in the 'except' network range. */ + if (((s->fe->except_mask.s_addr == 0) || (((struct sockaddr_in *)&s->cli_addr)->sin_addr.s_addr & s->fe->except_mask.s_addr) != s->fe->except_net.s_addr) && + ((s->be->except_mask.s_addr == 0) || (((struct sockaddr_in *)&s->cli_addr)->sin_addr.s_addr & s->be->except_mask.s_addr) != s->be->except_net.s_addr)) { + + /* variables to build the header entry. */ + int len; + unsigned char *pn; + pn = (unsigned char *)&((struct sockaddr_in *)&s->cli_addr)->sin_addr; + + /* Note: we rely on the backend to get the header name to be used for + * X-Forward-For, because the header is really meant for the backends. + * However, if the backend did not specify any option, we have to rely + * on the frontend's header name. + */ + if (s->be->fwdfor_hdr_len) { + len = s->be->fwdfor_hdr_len; + memcpy(trash, s->be->fwdfor_hdr_name, len); + } else { + len = s->fe->fwdfor_hdr_len; + memcpy(trash, s->fe->fwdfor_hdr_name, len); + } + + /* fetch length of complete header. */ + len += sprintf(trash + len, ": %d.%d.%d.%d", pn[0], pn[1], pn[2], pn[3]); + + /* copy the header into connection. */ + if ((buf_pos = buffer_insert_line2(req, req->data, trash, len)) < 0) { + + /* marks the buffer as "shutdown" ASAP in both directions. */ + buffer_abort(req); + buffer_abort(s->rep); + req->analysers = 0; + s->fe->failed_req++; + + if ((s->flags & SN_ERR_MASK) == 0) { + s->flags |= SN_ERR_PRXCOND; + } + + if ((s->flags & SN_FINST_MASK) == 0) { + s->flags |= SN_FINST_R; + } + + /* error occured. */ + return 0; + } + } + } + + /* IPv6. */ + else if (((s->fe->options | s->be->options) & PR_O_FWDFOR) != 0 && s->cli_addr.ss_family == AF_INET6) { + + /* FIXME: for the sake of completeness, we should also support + * 'except' here, although it is mostly useless in this case. + */ + + /* variables to build the header entry. */ + int len; + char pn[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, (const void *)&((struct sockaddr_in6 *)(&s->cli_addr))->sin6_addr, pn, sizeof(pn)); + + /* Note: we rely on the backend to get the header name to be used for + * X-Forward-For, because the header is really meant for the backends. + * However, if the backend did not specify any option, we have to rely + * on the frontend's header name. + */ + if (s->be->fwdfor_hdr_len) { + len = s->be->fwdfor_hdr_len; + memcpy(trash, s->be->fwdfor_hdr_name, len); + } else { + len = s->fe->fwdfor_hdr_len; + memcpy(trash, s->fe->fwdfor_hdr_name, len); + } + + /* fetch length of complete header. */ + len += sprintf(trash + len, ": %s", pn); + + /* copy the header into connection. */ + if ((buf_pos = buffer_insert_line2(req, req->data, trash, len)) < 0) { + + /* marks the buffer as "shutdown" ASAP in both directions. */ + buffer_abort(req); + buffer_abort(s->rep); + req->analysers = 0; + s->fe->failed_req++; + + if ((s->flags & SN_ERR_MASK) == 0) { + s->flags |= SN_ERR_PRXCOND; + } + + if ((s->flags & SN_FINST_MASK) == 0) { + s->flags |= SN_FINST_R; + } + + /* error occured. */ + return 0; + } + } + + /* 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_INJECT; + req->analyse_exp = TICK_ETERNITY; + return 1; +} /* This function should be called to parse a line starting with the "tcp-request" * keyword. diff -Nur haproxy-1.3.17/src/session.c haproxy-1.3.17-tcp-inject/src/session.c --- haproxy-1.3.17/src/session.c 2009-03-29 15:26:57.000000000 +0200 +++ haproxy-1.3.17-tcp-inject/src/session.c 2009-04-17 02:37:44.000000000 +0200 @@ -755,8 +755,12 @@ if (!http_process_request_body(s, s->req)) break; + if (s->req->analysers & AN_REQ_INJECT) + if (!tcp_inject_request(s, s->req)) + 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; + s->req->analysers &= AN_REQ_INSPECT | AN_REQ_HTTP_HDR | AN_REQ_HTTP_TARPIT | AN_REQ_HTTP_BODY | AN_REQ_INJECT; /* we don't want to loop anyway */ break;