diff --git a/doc/configuration.txt b/doc/configuration.txt index a505f70ad..228e8132e 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -2771,6 +2771,7 @@ contimeout (deprecated) cookie [ rewrite | insert | prefix ] [ indirect ] [ nocache ] [ postonly ] [ preserve ] [ httponly ] [ secure ] [ domain ]* [ maxidle ] [ maxlife ] + [ dynamic ] Enable cookie-based persistence in a backend. May be used in sections : defaults | frontend | listen | backend yes | no | yes | yes @@ -2922,6 +2923,13 @@ cookie [ rewrite | insert | prefix ] [ indirect ] [ nocache ] is stronger than the maxidle method in that it forces a redispatch after some absolute delay. + dynamic Activate dynamic cookies. When used, a session cookie is + dynamically created for each server, based on the IP and port + of the server, and a secret key, specified in the + "dynamic-cookie-key" backend directive. + The cookie will be regenerated each time the IP address change, + and is only generated for IPv4/IPv6. + There can be only one persistence cookie per HTTP backend, and it can be declared in a defaults section. The value of the cookie will be the value indicated after the "cookie" keyword in a "server" statement. If no cookie @@ -3042,6 +3050,19 @@ dispatch
: See also : "server" +dynamic-cookie-key + Set the dynamic cookie secret key for a backend. + May be used in sections : defaults | frontend | listen | backend + yes | no | yes | yes + Arguments : The secret key to be used. + + When dynamic cookies are enabled (see the "dynamic" directive for cookie), + a dynamic cookie is created for each server (unless one is explicitely + specified on the "server" line), using a hash of the IP address of the + server, the TCP port, and the secret key. + That way, we can ensure session persistence accross multiple load-balancers, + even if servers are dynamically added or removed. + enabled Enable a proxy, frontend or backend. May be used in sections : defaults | frontend | listen | backend diff --git a/include/proto/server.h b/include/proto/server.h index 7c9574ee4..6e3ccf39f 100644 --- a/include/proto/server.h +++ b/include/proto/server.h @@ -204,6 +204,11 @@ void srv_set_admin_flag(struct server *s, enum srv_admin mode, const char *cause */ void srv_clr_admin_flag(struct server *s, enum srv_admin mode); +/* Calculates the dynamic persitent cookie for a server, if a secret key has + * been provided. + */ +void srv_set_dyncookie(struct server *s); + /* Puts server into maintenance mode, and propagate that status down to all * tracking servers. */ diff --git a/include/types/proxy.h b/include/types/proxy.h index 3f848a0dc..5306a3b6c 100644 --- a/include/types/proxy.h +++ b/include/types/proxy.h @@ -189,6 +189,7 @@ enum PR_SRV_STATE_FILE { #define PR_CK_PSV 0x00000040 /* cookie ... preserve */ #define PR_CK_HTTPONLY 0x00000080 /* emit the "HttpOnly" attribute */ #define PR_CK_SECURE 0x00000100 /* emit the "Secure" attribute */ +#define PR_CK_DYNAMIC 0x00000200 /* create dynamic cookies for each server */ /* bits for sticking rules */ #define STK_IS_MATCH 0x00000001 /* match on request fetch */ @@ -282,6 +283,7 @@ struct proxy { char *cookie_domain; /* domain used to insert the cookie */ char *cookie_name; /* name of the cookie to look for */ int cookie_len; /* strlen(cookie_name), computed only once */ + char *dyncookie_key; /* Secret key used to generate dynamic persistent cookies */ unsigned int cookie_maxidle; /* max idle time for this cookie */ unsigned int cookie_maxlife; /* max life time for this cookie */ int rdp_cookie_len; /* strlen(rdp_cookie_name), computed only once */ diff --git a/include/types/server.h b/include/types/server.h index 51b7e535c..feede6d97 100644 --- a/include/types/server.h +++ b/include/types/server.h @@ -117,6 +117,7 @@ enum srv_initaddr { #define SRV_F_CHECKADDR 0x0020 /* this server has a check addr configured */ #define SRV_F_CHECKPORT 0x0040 /* this server has a check port configured */ #define SRV_F_AGENTADDR 0x0080 /* this server has a agent addr configured */ +#define SRV_F_COOKIESET 0x0100 /* this server has a cookie configured, so don't generate dynamic cookies */ /* configured server options for send-proxy (server->pp_opts) */ #define SRV_PP_V1 0x0001 /* proxy protocol version 1 */ diff --git a/src/cfgparse.c b/src/cfgparse.c index 9d04f0284..5a0958973 100644 --- a/src/cfgparse.c +++ b/src/cfgparse.c @@ -2632,6 +2632,9 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm) if (defproxy.cookie_name) curproxy->cookie_name = strdup(defproxy.cookie_name); curproxy->cookie_len = defproxy.cookie_len; + + if (defproxy.dyncookie_key) + curproxy->dyncookie_key = strdup(defproxy.dyncookie_key); if (defproxy.cookie_domain) curproxy->cookie_domain = strdup(defproxy.cookie_domain); @@ -2793,6 +2796,7 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm) free(defproxy.check_path); free(defproxy.cookie_name); free(defproxy.rdp_cookie_name); + free(defproxy.dyncookie_key); free(defproxy.cookie_domain); free(defproxy.url_param_name); free(defproxy.hh_name); @@ -3159,6 +3163,20 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm) goto out; } } + else if (!strcmp(args[0], "dynamic-cookie-key")) { /* Dynamic cookies secret key */ + + if (warnifnotcap(curproxy, PR_CAP_BE, file, linenum, args[0], NULL)) + err_code |= ERR_WARN; + + if (*(args[1]) == 0) { + Alert("parsing [%s:%d] : '%s' expects as argument.\n", + file, linenum, args[0]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + free(curproxy->dyncookie_key); + curproxy->dyncookie_key = strdup(args[1]); + } else if (!strcmp(args[0], "cookie")) { /* cookie name */ int cur_arg; @@ -3292,8 +3310,15 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm) curproxy->cookie_maxlife = maxlife; cur_arg++; } + else if (!strcmp(args[cur_arg], "dynamic")) { /* Dynamic persitent cookies secret key */ + + if (warnifnotcap(curproxy, PR_CAP_BE, file, linenum, args[cur_arg], NULL)) + err_code |= ERR_WARN; + curproxy->ck_opts |= PR_CK_DYNAMIC; + } + else { - Alert("parsing [%s:%d] : '%s' supports 'rewrite', 'insert', 'prefix', 'indirect', 'nocache', 'postonly', 'domain', 'maxidle, and 'maxlife' options.\n", + Alert("parsing [%s:%d] : '%s' supports 'rewrite', 'insert', 'prefix', 'indirect', 'nocache', 'postonly', 'domain', 'maxidle', 'dynamic' and 'maxlife' options.\n", file, linenum, args[0]); err_code |= ERR_ALERT | ERR_FATAL; goto out; @@ -8442,6 +8467,21 @@ out_uri_auth_compat: newsrv = newsrv->next; } + /* + * Try to generate dynamic cookies for servers now. + * It couldn't be done earlier, since at the time we parsed + * the server line, we may not have known yet that we + * should use dynamic cookies, or the secret key may not + * have been provided yet. + */ + if (curproxy->ck_opts & PR_CK_DYNAMIC) { + newsrv = curproxy->srv; + while (newsrv != NULL) { + srv_set_dyncookie(newsrv); + newsrv = newsrv->next; + } + + } /* We have to initialize the server lookup mechanism depending * on what LB algorithm was choosen. */ diff --git a/src/server.c b/src/server.c index 9cc02f74f..4e03e50a4 100644 --- a/src/server.c +++ b/src/server.c @@ -14,6 +14,8 @@ #include #include +#include + #include #include #include @@ -77,6 +79,73 @@ int srv_getinter(const struct check *check) return (check->fastinter)?(check->fastinter):(check->inter); } +void srv_set_dyncookie(struct server *s) +{ + struct proxy *p = s->proxy; + struct server *tmpserv; + char *tmpbuf; + unsigned long long hash_value; + size_t key_len = strlen(p->dyncookie_key); + size_t buffer_len; + int addr_len; + int port; + + if ((s->flags & SRV_F_COOKIESET) || + !(s->proxy->ck_opts & PR_CK_DYNAMIC) || + s->proxy->dyncookie_key == NULL) + return; + + if (s->addr.ss_family != AF_INET && + s->addr.ss_family != AF_INET6) + return; + /* + * Buffer to calculate the cookie value. + * The buffer contains the secret key + the server IP address + * + the TCP port. + */ + addr_len = (s->addr.ss_family == AF_INET) ? 4 : 16; + /* + * The TCP port should use only 2 bytes, but is stored in + * an unsigned int in struct server, so let's use 4, to be + * on the safe side. + */ + buffer_len = key_len + addr_len + 4; + tmpbuf = trash.str; + memcpy(tmpbuf, p->dyncookie_key, key_len); + memcpy(&(tmpbuf[key_len]), + s->addr.ss_family == AF_INET ? + (void *)&((struct sockaddr_in *)&s->addr)->sin_addr.s_addr : + (void *)&(((struct sockaddr_in6 *)&s->addr)->sin6_addr.s6_addr), + addr_len); + /* + * Make sure it's the same across all the load balancers, + * no matter their endianness. + */ + port = htonl(s->svc_port); + memcpy(&tmpbuf[key_len + addr_len], &port, 4); + hash_value = XXH64(tmpbuf, buffer_len, 0); + memprintf(&s->cookie, "%016llx", hash_value); + if (!s->cookie) + return; + s->cklen = 16; + /* + * Check that we did not get a hash collision. + * Unlikely, but it can happen. + */ + for (p = proxy; p != NULL; p = p->next) + for (tmpserv = proxy->srv; tmpserv != NULL; + tmpserv = tmpserv->next) { + if (tmpserv == s) + continue; + if (tmpserv->cookie && + strcmp(tmpserv->cookie, s->cookie) == 0) { + Warning("We generated two equal cookies for two different servers.\n" + "Please change the secret key for '%s'.\n", + s->proxy->id); + } + } +} + /* * Registers the server keyword list as a list of valid keywords for next * parsing sessions. @@ -1175,6 +1244,7 @@ int parse_server(const char *file, int linenum, char **args, struct proxy *curpr else if (!defsrv && !strcmp(args[cur_arg], "cookie")) { newsrv->cookie = strdup(args[cur_arg + 1]); newsrv->cklen = strlen(args[cur_arg + 1]); + newsrv->flags |= SRV_F_COOKIESET; cur_arg += 2; } else if (!strcmp(args[cur_arg], "init-addr")) { @@ -2730,6 +2800,7 @@ int update_server_addr(struct server *s, void *ip, int ip_sin_family, const char memcpy(((struct sockaddr_in6 *)&s->addr)->sin6_addr.s6_addr, ip, 16); break; }; + srv_set_dyncookie(s); return 0; } @@ -2758,6 +2829,7 @@ const char *update_server_addr_port(struct server *s, const char *addr, const ch char current_addr[INET6_ADDRSTRLEN]; uint16_t current_port, new_port; struct chunk *msg; + int changed = 0; msg = get_trash_chunk(); chunk_reset(msg); @@ -2792,6 +2864,7 @@ const char *update_server_addr_port(struct server *s, const char *addr, const ch goto port; } ipcpy(&sa, &s->addr); + changed = 1; /* we also need to update check's ADDR only if it uses the server's one */ if ((s->check.state & CHK_ST_CONFIGURED) && (s->flags & SRV_F_CHECKADDR)) { @@ -2859,6 +2932,7 @@ const char *update_server_addr_port(struct server *s, const char *addr, const ch if (port_change_required) { /* apply new port */ s->svc_port = new_port; + changed = 1; /* prepare message */ chunk_appendf(msg, "port changed from '"); @@ -2893,6 +2967,8 @@ const char *update_server_addr_port(struct server *s, const char *addr, const ch } out: + if (changed) + srv_set_dyncookie(s); if (updater) chunk_appendf(msg, " by '%s'", updater); chunk_appendf(msg, "\n"); @@ -3279,7 +3355,7 @@ static int srv_iterate_initaddr(struct server *srv) if (!srv->lastaddr) continue; if (srv_apply_lastaddr(srv, &err_code) == 0) - return return_code; + goto out; return_code |= err_code; break; @@ -3287,7 +3363,7 @@ static int srv_iterate_initaddr(struct server *srv) if (!srv->hostname) continue; if (srv_set_addr_via_libc(srv, &err_code) == 0) - return return_code; + goto out; return_code |= err_code; break; @@ -3305,7 +3381,7 @@ static int srv_iterate_initaddr(struct server *srv) Warning("parsing [%s:%d] : 'server %s' : could not resolve address '%s', falling back to configured address.\n", srv->conf.file, srv->conf.line, srv->id, srv->hostname); } - return return_code; + goto out; default: /* unhandled method */ break; @@ -3323,6 +3399,9 @@ static int srv_iterate_initaddr(struct server *srv) return_code |= ERR_ALERT | ERR_FATAL; return return_code; +out: + srv_set_dyncookie(srv); + return return_code; } /*