MINOR: action: new '(http-request|tcp-request content) do-resolve' action

The 'do-resolve' action is an http-request or tcp-request content action
which allows to run DNS resolution at run time in HAProxy.
The name to be resolved can be picked up in the request sent by the
client and the result of the resolution is stored in a variable.
The time the resolution is being performed, the request is on pause.
If the resolution can't provide a suitable result, then the variable
will be empty. It's up to the admin to take decisions based on this
statement (return 503 to prevent loops).

Read carefully the documentation concerning this feature, to ensure your
setup is secure and safe to be used in production.

This patch creates a global counter to track various errors reported by
the action 'do-resolve'.
This commit is contained in:
Baptiste Assmann 2019-01-21 08:34:50 +01:00 committed by Willy Tarreau
parent 0b9ce82dfa
commit 333939c2ee
11 changed files with 431 additions and 0 deletions

View File

@ -4267,6 +4267,60 @@ http-request deny [deny_status <status>] [ { if | unless } <condition> ]
those that can be overridden by the "errorfile" directive.
No further "http-request" rules are evaluated.
http-request do-resolve(<var>,<resolvers>,[ipv4,ipv6]) <expr> :
This action performs a DNS resolution of the output of <expr> and stores
the result in the variable <var>. It uses the DNS resolvers section
pointed by <resolvers>.
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 <var>. If any kind of
error occurs, then <var> 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 <name> <fmt> [ { if | unless } <condition> ]
This is used to build an HTTP 103 Early Hints response prior to any other one.
@ -9804,6 +9858,7 @@ tcp-request content <action> [{if | unless} <condition>]
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 <expr> | set-priority-offset <expr>
@ -9820,6 +9875,8 @@ tcp-request content <action> [{if | unless} <condition>]
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

View File

@ -24,6 +24,9 @@
#include <types/action.h>
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;

View File

@ -22,9 +22,11 @@
#ifndef _PROTO_DNS_H
#define _PROTO_DNS_H
#include <types/action.h>
#include <types/dns.h>
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

View File

@ -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" */

View File

@ -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

View File

@ -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 */

View File

@ -16,8 +16,10 @@
#include <common/standard.h>
#include <proto/action.h>
#include <proto/obj_type.h>
#include <proto/proxy.h>
#include <proto/stick_table.h>
#include <proto/task.h>
/* 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;
}

301
src/dns.c
View File

@ -26,23 +26,30 @@
#include <common/ticks.h>
#include <common/net_helper.h>
#include <types/action.h>
#include <types/applet.h>
#include <types/cli.h>
#include <types/global.h>
#include <types/dns.h>
#include <types/stats.h>
#include <proto/action.h>
#include <proto/channel.h>
#include <proto/cli.h>
#include <proto/checks.h>
#include <proto/dns.h>
#include <proto/fd.h>
#include <proto/proto_http.h>
#include <proto/http_rules.h>
#include <proto/log.h>
#include <proto/sample.h>
#include <proto/server.h>
#include <proto/task.h>
#include <proto/proto_udp.h>
#include <proto/proxy.h>
#include <proto/stream_interface.h>
#include <proto/tcp_rules.h>
#include <proto/vars.h>
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 <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 <rule> 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(<varName>,<resolversSectionName>,<resolvePrefer>) <expr>
*
* - <varName> is the variable name where the result of the DNS resolution will be stored
* (mandatory)
* - <resolversSectionName> is the name of the resolvers section to use to perform the resolution
* (mandatory)
* - <resolvePrefer> can be either 'ipv4' or 'ipv6' and is the IP family we would like to resolve first
* (optional), defaults to ipv6
* - <expr> 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(<varname>,<resolvers>[,<options>]) <expr>'. 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);

View File

@ -53,6 +53,7 @@
#include <proto/checks.h>
#include <proto/cli.h>
#include <proto/compression.h>
#include <proto/dns.h>
#include <proto/stats.h>
#include <proto/fd.h>
#include <proto/filters.h>

View File

@ -51,6 +51,7 @@
#include <proto/checks.h>
#include <proto/cli.h>
#include <proto/compression.h>
#include <proto/dns.h>
#include <proto/stats.h>
#include <proto/fd.h>
#include <proto/freq_ctr.h>
@ -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;
}

View File

@ -39,6 +39,7 @@
#include <proto/checks.h>
#include <proto/cli.h>
#include <proto/connection.h>
#include <proto/dns.h>
#include <proto/stats.h>
#include <proto/fd.h>
#include <proto/filters.h>
@ -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);