diff --git a/doc/configuration.txt b/doc/configuration.txt index 4fb90332a..832373785 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -4267,6 +4267,60 @@ http-request deny [deny_status ] [ { if | unless } ] those that can be overridden by the "errorfile" directive. No further "http-request" rules are evaluated. +http-request do-resolve(,,[ipv4,ipv6]) : + + This action performs a DNS resolution of the output of and stores + the result in the variable . It uses the DNS resolvers section + pointed by . + It is possible to choose a resolution preference using the optional + arguments 'ipv4' or 'ipv6'. + When performing the DNS resolution, the client side connection is on + pause waiting till the end of the resolution. + If an IP address can be found, it is stored into . If any kind of + error occurs, then is not set. + One can use this action to discover a server IP address at run time and + based on information found in the request (IE a Host header). + If this action is used to find the server's IP address (using the + "set-dst" action), then the server IP address in the backend must be set + to 0.0.0.0. + + Example: + resolvers mydns + nameserver local 127.0.0.53:53 + nameserver google 8.8.8.8:53 + timeout retry 1s + hold valid 10s + hold nx 3s + hold other 3s + hold obsolete 0s + accepted_payload_size 8192 + + frontend fe + bind 10.42.0.1:80 + http-request do-resolve(txn.myip,mydns,ipv4) hdr(Host),lower + http-request capture var(txn.myip) len 40 + + # return 503 when the variable is not set, + # which mean DNS resolution error + use_backend b_503 unless { var(txn.myip) -m found } + + default_backend be + + backend b_503 + # dummy backend used to return 503. + # one can use the errorfile directive to send a nice + # 503 error page to end users + + backend be + # rule to prevent HAProxy from reconnecting to services + # on the local network (forged DNS name used to scan the network) + http-request deny if { var(txn.myip) -m ip 127.0.0.0/8 10.0.0.0/8 } + http-request set-dst var(txn.myip) + server clear 0.0.0.0:0 + + NOTE: Don't forget to set the "protection" rules to ensure HAProxy won't + be used to scan the network or worst won't loop over itself... + http-request early-hint [ { if | unless } ] This is used to build an HTTP 103 Early Hints response prior to any other one. @@ -9804,6 +9858,7 @@ tcp-request content [{if | unless} ] Several types of actions are supported : - accept : the request is accepted + - do-resolve: perform a DNS resolution - reject : the request is rejected and the connection is closed - capture : the specified sample expression is captured - set-priority-class | set-priority-offset @@ -9820,6 +9875,8 @@ tcp-request content [{if | unless} ] They have the same meaning as their counter-parts in "tcp-request connection" so please refer to that section for a complete description. + For "do-resolve" action, please check the "http-request do-resolve" + configuration section. While there is nothing mandatory about it, it is recommended to use the track-sc0 in "tcp-request connection" rules, track-sc1 for "tcp-request diff --git a/include/proto/action.h b/include/proto/action.h index 19312db40..d0b2c5d2a 100644 --- a/include/proto/action.h +++ b/include/proto/action.h @@ -24,6 +24,9 @@ #include +int act_resolution_cb(struct dns_requester *requester, struct dns_nameserver *nameserver); +int act_resolution_error_cb(struct dns_requester *requester, int error_code); + static inline struct action_kw *action_lookup(struct list *keywords, const char *kw) { struct action_kw_list *kw_list; diff --git a/include/proto/dns.h b/include/proto/dns.h index 3ad79c3a4..7ce5a098e 100644 --- a/include/proto/dns.h +++ b/include/proto/dns.h @@ -22,9 +22,11 @@ #ifndef _PROTO_DNS_H #define _PROTO_DNS_H +#include #include extern struct list dns_resolvers; +extern unsigned int dns_failed_resolutions; struct dns_resolvers *find_resolvers_by_id(const char *id); struct dns_srvrq *find_srvrq_by_name(const char *name, struct proxy *px); @@ -43,6 +45,8 @@ int dns_get_ip_from_response(struct dns_response_packet *dns_p, int dns_link_resolution(void *requester, int requester_type, int requester_locked); void dns_unlink_resolution(struct dns_requester *requester); void dns_trigger_resolution(struct dns_requester *requester); +enum act_parse_ret dns_parse_do_resolve(const char **args, int *orig_arg, struct proxy *px, struct act_rule *rule, char **err); +int check_action_do_resolve(struct act_rule *rule, struct proxy *px, char **err); #endif // _PROTO_DNS_H diff --git a/include/types/action.h b/include/types/action.h index 0367ea9f5..6c5fccf4c 100644 --- a/include/types/action.h +++ b/include/types/action.h @@ -107,6 +107,13 @@ struct act_rule { struct action_kw *kw; struct applet applet; /* used for the applet registration. */ union { + struct { + struct sample_expr *expr; + char *varname; + char *resolvers_id; + struct dns_resolvers *resolvers; + struct dns_options dns_opts; + } dns; /* dns resolution */ struct { char *realm; } auth; /* arg used by "auth" */ diff --git a/include/types/stats.h b/include/types/stats.h index 043c37747..648b03241 100644 --- a/include/types/stats.h +++ b/include/types/stats.h @@ -312,6 +312,7 @@ enum info_field { INF_CONNECTED_PEERS, INF_DROPPED_LOGS, INF_BUSY_POLLING, + INF_FAILED_RESOLUTIONS, /* must always be the last one */ INF_TOTAL_FIELDS diff --git a/include/types/stream.h b/include/types/stream.h index b6a3e8495..96a034549 100644 --- a/include/types/stream.h +++ b/include/types/stream.h @@ -180,6 +180,15 @@ struct stream { struct list *current_rule_list; /* this is used to store the current executed rule list. */ void *current_rule; /* this is used to store the current rule to be resumed. */ struct hlua *hlua; /* lua runtime context */ + + /* Context */ + struct { + struct dns_requester *dns_requester; /* owner of the resolution */ + char *hostname_dn; /* hostname being resolve, in domain name format */ + int hostname_dn_len; /* size of hostname_dn */ + /* 4 unused bytes here */ + struct act_rule *parent; /* rule which requested this resolution */ + } dns_ctx; /* context information for DNS resolution */ }; #endif /* _TYPES_STREAM_H */ diff --git a/src/action.c b/src/action.c index 7574fba03..9b6dcfd80 100644 --- a/src/action.c +++ b/src/action.c @@ -16,8 +16,10 @@ #include #include +#include #include #include +#include /* Find and check the target table used by an action ACT_ACTION_TRK_*. This @@ -67,3 +69,35 @@ int check_trk_action(struct act_rule *rule, struct proxy *px, char **err) return 1; } +int act_resolution_cb(struct dns_requester *requester, struct dns_nameserver *nameserver) +{ + struct stream *stream; + + if (requester->resolution == NULL) + return 0; + + stream = objt_stream(requester->owner); + if (stream == NULL) + return 0; + + task_wakeup(stream->task, TASK_WOKEN_MSG); + + return 0; +} + +int act_resolution_error_cb(struct dns_requester *requester, int error_code) +{ + struct stream *stream; + + if (requester->resolution == NULL) + return 0; + + stream = objt_stream(requester->owner); + if (stream == NULL) + return 0; + + task_wakeup(stream->task, TASK_WOKEN_MSG); + + return 0; +} + diff --git a/src/dns.c b/src/dns.c index a0da346cf..30fc93c67 100644 --- a/src/dns.c +++ b/src/dns.c @@ -26,23 +26,30 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include #include #include +#include +#include #include +#include #include #include #include #include #include +#include +#include struct list dns_resolvers = LIST_HEAD_INIT(dns_resolvers); struct list dns_srvrq_list = LIST_HEAD_INIT(dns_srvrq_list); @@ -54,6 +61,7 @@ DECLARE_STATIC_POOL(dns_resolution_pool, "dns_resolution", sizeof(struct dns_r DECLARE_POOL(dns_requester_pool, "dns_requester", sizeof(struct dns_requester)); static unsigned int resolution_uuid = 1; +unsigned int dns_failed_resolutions = 0; /* Returns a pointer to the resolvers matching the id . NULL is returned if * no match is found. @@ -1351,6 +1359,7 @@ int dns_link_resolution(void *requester, int requester_type, int requester_locke struct dns_resolvers *resolvers; struct server *srv = NULL; struct dns_srvrq *srvrq = NULL; + struct stream *stream = NULL; char **hostname_dn; int hostname_dn_len, query_type; @@ -1373,6 +1382,15 @@ int dns_link_resolution(void *requester, int requester_type, int requester_locke query_type = DNS_RTYPE_SRV; break; + case OBJ_TYPE_STREAM: + stream = (struct stream *)requester; + hostname_dn = &stream->dns_ctx.hostname_dn; + hostname_dn_len = stream->dns_ctx.hostname_dn_len; + resolvers = stream->dns_ctx.parent->arg.dns.resolvers; + query_type = ((stream->dns_ctx.parent->arg.dns.dns_opts.family_prio == AF_INET) + ? DNS_RTYPE_A + : DNS_RTYPE_AAAA); + break; default: goto err; } @@ -1414,6 +1432,19 @@ int dns_link_resolution(void *requester, int requester_type, int requester_locke req->requester_cb = snr_resolution_cb; req->requester_error_cb = snr_resolution_error_cb; } + else if (stream) { + if (stream->dns_ctx.dns_requester == NULL) { + if ((req = pool_alloc(dns_requester_pool)) == NULL) + goto err; + req->owner = &stream->obj_type; + stream->dns_ctx.dns_requester = req; + } + else + req = stream->dns_ctx.dns_requester; + + req->requester_cb = act_resolution_cb; + req->requester_error_cb = act_resolution_error_cb; + } else goto err; @@ -1463,6 +1494,10 @@ void dns_unlink_resolution(struct dns_requester *requester) res->hostname_dn = __objt_dns_srvrq(req->owner)->hostname_dn; res->hostname_dn_len = __objt_dns_srvrq(req->owner)->hostname_dn_len; break; + case OBJ_TYPE_STREAM: + res->hostname_dn = __objt_stream(req->owner)->dns_ctx.hostname_dn; + res->hostname_dn_len = __objt_stream(req->owner)->dns_ctx.hostname_dn_len; + break; default: res->hostname_dn = NULL; res->hostname_dn_len = 0; @@ -2070,5 +2105,271 @@ static struct cli_kw_list cli_kws = {{ }, { INITCALL1(STG_REGISTER, cli_register_kw, &cli_kws); +/* + * Prepare for hostname resolution. + * Returns -1 in case of any allocation failure, 0 if not. + * On error, a global failure counter is also incremented. + */ +static int action_prepare_for_resolution(struct stream *stream, const char *hostname) +{ + char *hostname_dn; + int hostname_len, hostname_dn_len; + struct buffer *tmp = get_trash_chunk(); + + if (!hostname) + return 0; + + hostname_len = strlen(hostname); + hostname_dn = tmp->area; + hostname_dn_len = dns_str_to_dn_label(hostname, hostname_len + 1, + hostname_dn, tmp->size); + if (hostname_dn_len == -1) + goto err; + + + stream->dns_ctx.hostname_dn = strdup(hostname_dn); + stream->dns_ctx.hostname_dn_len = hostname_dn_len; + if (!stream->dns_ctx.hostname_dn) + goto err; + + return 0; + + err: + free(stream->dns_ctx.hostname_dn); stream->dns_ctx.hostname_dn = NULL; + dns_failed_resolutions += 1; + return -1; +} + + +/* + * Execute the "do-resolution" action. May be called from {tcp,http}request. + */ +enum act_return dns_action_do_resolve(struct act_rule *rule, struct proxy *px, + struct session *sess, struct stream *s, int flags) +{ + struct connection *cli_conn; + struct dns_resolution *resolution; + + /* we have a response to our DNS resolution */ + if (s->dns_ctx.dns_requester && s->dns_ctx.dns_requester->resolution != NULL) { + resolution = s->dns_ctx.dns_requester->resolution; + if (resolution->step == RSLV_STEP_NONE) { + /* We update the variable only if we have a valid response. */ + if (resolution->status == RSLV_STATUS_VALID) { + struct sample smp; + short ip_sin_family = 0; + void *ip = NULL; + + dns_get_ip_from_response(&resolution->response, &rule->arg.dns.dns_opts, NULL, + 0, &ip, &ip_sin_family, NULL); + + switch (ip_sin_family) { + case AF_INET: + smp.data.type = SMP_T_IPV4; + memcpy(&smp.data.u.ipv4, ip, 4); + break; + case AF_INET6: + smp.data.type = SMP_T_IPV6; + memcpy(&smp.data.u.ipv6, ip, 16); + break; + default: + ip = NULL; + } + + if (ip) { + smp.px = px; + smp.sess = sess; + smp.strm = s; + + vars_set_by_name(rule->arg.dns.varname, strlen(rule->arg.dns.varname), &smp); + } + } + } + + free(s->dns_ctx.hostname_dn); s->dns_ctx.hostname_dn = NULL; + s->dns_ctx.hostname_dn_len = 0; + dns_unlink_resolution(s->dns_ctx.dns_requester); + + pool_free(dns_requester_pool, s->dns_ctx.dns_requester); + s->dns_ctx.dns_requester = NULL; + + return ACT_RET_CONT; + } + + /* need to configure and start a new DNS resolution */ + cli_conn = objt_conn(sess->origin); + if (cli_conn && conn_ctrl_ready(cli_conn)) { + struct sample *smp; + char *fqdn; + + conn_get_from_addr(cli_conn); + + smp = sample_fetch_as_type(px, sess, s, SMP_OPT_DIR_REQ|SMP_OPT_FINAL, rule->arg.dns.expr, SMP_T_STR); + if (smp == NULL) + return ACT_RET_CONT; + + fqdn = smp->data.u.str.area; + if (action_prepare_for_resolution(s, fqdn) == -1) { + return ACT_RET_ERR; + } + + s->dns_ctx.parent = rule; + dns_link_resolution(s, OBJ_TYPE_STREAM, 0); + dns_trigger_resolution(s->dns_ctx.dns_requester); + } + return ACT_RET_YIELD; +} + + +/* parse "do-resolve" action + * This action takes the following arguments: + * do-resolve(,,) + * + * - is the variable name where the result of the DNS resolution will be stored + * (mandatory) + * - is the name of the resolvers section to use to perform the resolution + * (mandatory) + * - can be either 'ipv4' or 'ipv6' and is the IP family we would like to resolve first + * (optional), defaults to ipv6 + * - is an HAProxy expression used to fetch the name to be resolved + */ +enum act_parse_ret dns_parse_do_resolve(const char **args, int *orig_arg, struct proxy *px, struct act_rule *rule, char **err) +{ + int cur_arg; + struct sample_expr *expr; + unsigned int where; + const char *beg, *end; + + /* orig_arg points to the first argument, but we need to analyse the command itself first */ + cur_arg = *orig_arg - 1; + + /* locate varName, which is mandatory */ + beg = strchr(args[cur_arg], '('); + if (beg == NULL) + goto do_resolve_parse_error; + beg = beg + 1; /* beg should points to the first character after opening parenthesis '(' */ + end = strchr(beg, ','); + if (end == NULL) + goto do_resolve_parse_error; + rule->arg.dns.varname = my_strndup(beg, end - beg); + if (rule->arg.dns.varname == NULL) + goto do_resolve_parse_error; + + + /* locate resolversSectionName, which is mandatory. + * Since next parameters are optional, the delimiter may be comma ',' + * or closing parenthesis ')' + */ + beg = end + 1; + end = strchr(beg, ','); + if (end == NULL) + end = strchr(beg, ')'); + if (end == NULL) + goto do_resolve_parse_error; + rule->arg.dns.resolvers_id = my_strndup(beg, end - beg); + if (rule->arg.dns.resolvers_id == NULL) + goto do_resolve_parse_error; + + + /* Default priority is ipv6 */ + rule->arg.dns.dns_opts.family_prio = AF_INET6; + + /* optional arguments accepted for now: + * ipv4 or ipv6 + */ + while (*end != ')') { + beg = end + 1; + end = strchr(beg, ','); + if (end == NULL) + end = strchr(beg, ')'); + if (end == NULL) + goto do_resolve_parse_error; + + if (strncmp(beg, "ipv4", end - beg) == 0) { + rule->arg.dns.dns_opts.family_prio = AF_INET; + } + else if (strncmp(beg, "ipv6", end - beg) == 0) { + rule->arg.dns.dns_opts.family_prio = AF_INET6; + } + else { + goto do_resolve_parse_error; + } + } + + cur_arg = cur_arg + 1; + + expr = sample_parse_expr((char **)args, &cur_arg, px->conf.args.file, px->conf.args.line, err, &px->conf.args); + if (!expr) + goto do_resolve_parse_error; + + + where = 0; + if (px->cap & PR_CAP_FE) + where |= SMP_VAL_FE_HRQ_HDR; + if (px->cap & PR_CAP_BE) + where |= SMP_VAL_BE_HRQ_HDR; + + if (!(expr->fetch->val & where)) { + memprintf(err, + "fetch method '%s' extracts information from '%s', none of which is available here", + args[cur_arg-1], sample_src_names(expr->fetch->use)); + free(expr); + return ACT_RET_PRS_ERR; + } + rule->arg.dns.expr = expr; + rule->action = ACT_CUSTOM; + rule->action_ptr = dns_action_do_resolve; + *orig_arg = cur_arg; + + rule->check_ptr = check_action_do_resolve; + + return ACT_RET_PRS_OK; + + do_resolve_parse_error: + free(rule->arg.dns.varname); rule->arg.dns.varname = NULL; + free(rule->arg.dns.resolvers_id); rule->arg.dns.resolvers_id = NULL; + memprintf(err, "Can't parse '%s'. Expects 'do-resolve(,[,]) '. Available options are 'ipv4' and 'ipv6'", + args[cur_arg]); + return ACT_RET_PRS_ERR; +} + +static struct action_kw_list http_req_kws = { { }, { + { "do-resolve", dns_parse_do_resolve, 1 }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, http_req_keywords_register, &http_req_kws); + +static struct action_kw_list tcp_req_cont_actions = {ILH, { + { "do-resolve", dns_parse_do_resolve, 1 }, + { /* END */ } +}}; + +INITCALL1(STG_REGISTER, tcp_req_cont_keywords_register, &tcp_req_cont_actions); + +/* Check an "http-request do-resolve" action. + * + * The function returns 1 in success case, otherwise, it returns 0 and err is + * filled. + */ +int check_action_do_resolve(struct act_rule *rule, struct proxy *px, char **err) +{ + struct dns_resolvers *resolvers = NULL; + + if (rule->arg.dns.resolvers_id == NULL) { + memprintf(err,"Proxy '%s': %s", px->id, "do-resolve action without resolvers"); + return 0; + } + + resolvers = find_resolvers_by_id(rule->arg.dns.resolvers_id); + if (resolvers == NULL) { + memprintf(err,"Can't find resolvers section '%s' for do-resolve action", rule->arg.dns.resolvers_id); + return 0; + } + rule->arg.dns.resolvers = resolvers; + + return 1; +} + REGISTER_POST_DEINIT(dns_deinit); REGISTER_CONFIG_POSTPARSER("dns runtime resolver", dns_finalize_config); diff --git a/src/proto_http.c b/src/proto_http.c index 695ff7e33..83293241d 100644 --- a/src/proto_http.c +++ b/src/proto_http.c @@ -53,6 +53,7 @@ #include #include #include +#include #include #include #include diff --git a/src/stats.c b/src/stats.c index 346c62912..6965ad247 100644 --- a/src/stats.c +++ b/src/stats.c @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -154,6 +155,7 @@ const char *info_field_names[INF_TOTAL_FIELDS] = { [INF_CONNECTED_PEERS] = "ConnectedPeers", [INF_DROPPED_LOGS] = "DroppedLogs", [INF_BUSY_POLLING] = "BusyPolling", + [INF_FAILED_RESOLUTIONS] = "FailedResolutions", }; const char *stat_field_names[ST_F_TOTAL_FIELDS] = { @@ -3657,6 +3659,7 @@ int stats_fill_info(struct field *info, int len) info[INF_CONNECTED_PEERS] = mkf_u32(0, connected_peers); info[INF_DROPPED_LOGS] = mkf_u32(0, dropped_logs); info[INF_BUSY_POLLING] = mkf_u32(0, !!(global.tune.options & GTUNE_BUSY_POLLING)); + info[INF_FAILED_RESOLUTIONS] = mkf_u32(0, dns_failed_resolutions); return 1; } diff --git a/src/stream.c b/src/stream.c index 774028945..af15d47a1 100644 --- a/src/stream.c +++ b/src/stream.c @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -151,6 +152,7 @@ struct stream *stream_new(struct session *sess, enum obj_type *origin) s->logs.bytes_in = s->logs.bytes_out = 0; s->logs.prx_queue_pos = 0; /* we get the number of pending conns before us */ s->logs.srv_queue_pos = 0; /* we will get this number soon */ + s->obj_type = OBJ_TYPE_STREAM; csinfo = si_get_cs_info(cs); if (csinfo) { @@ -418,6 +420,15 @@ static void stream_free(struct stream *s) s->txn = NULL; } + if (s->dns_ctx.dns_requester) { + free(s->dns_ctx.hostname_dn); s->dns_ctx.hostname_dn = NULL; + s->dns_ctx.hostname_dn_len = 0; + dns_unlink_resolution(s->dns_ctx.dns_requester); + + pool_free(dns_requester_pool, s->dns_ctx.dns_requester); + s->dns_ctx.dns_requester = NULL; + } + flt_stream_stop(s); flt_stream_release(s, 0);