Hi Willy, Thanks for your feedback once again.
TLDR: I agree with your proposed changes. So, here is my corresponding proposal: We can ignore the last two commits for now (LOW: connection: Add TLV update function and MEDIUM: tcp-act: Add new set-tlv TCP action for PPv2 TLVs). Based on the first two commits, I created a diff that would implement something like you requested (new parser and no use of remote). It should be fully working, so you could even try it out locally if you want. If you think it looks promising, I will adjust the tests and the docs, and update the series. Now to address your feedback in more detail: > I'm really having an issue with using "the remote frontend connection" > here. We've done this mistake in the past with the transparent mode that > connects to the original destination address, that resulted in "set-dst" > having to be implemented, then being broken by multiplexed streams (e.g. > h2), then having to be internally emulated at various layers (connection, > session, stream, transaction etc). Same for src. The solution is not durable > at all, and as you noticed, you already had to implement the ability to > modify these fields before passing them, hence reintroducing that class of > trouble on a new area. I choose to use the remote connection since it is already done that way for other TLV types like SRV_PP_V2_AUTHORITY (https://github.com/haproxy/haproxy/commit/8a4ffa0aabf8509098f95ac78fa48b18f415c42c). It simply forwards what is found the remote connection, if there is something set. Similarly, PP2_TYPE_NETNS and SRV_PP_V2_SSL depend on remote. First, I did not like the method with an extra fetch since it creates potential overhead. However, I am likely a bit excessive here. I am not opposed to not using remote at all. I simply was not aware of the intricacies. > Also what will be left in the logs after you modify the contents ? etc. You mean that the log does not match the content of the actual TLV? It could happen, I suppose. But why doesn't this problem with multiplexed connections apply to all the TCP actions? As far as I know they all alter fields of the connection directly. Doesn't really matter though, I don't plan to use it anymore. > I'm seeing another issue with passing type=fmt like this. The option is > currently defined as cutting around commas, and that's what it does. This > means that the format will not be parsable (no sample fetch functions with > more than one argument, no converters). Example: > > proxy-v2-options 0xEE=%[var(txn.clientname,_unknown_)] > ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ > First option Second option I see, that is a problem. Of course, we could track whether we are currently in a log format expression. But I think I have come up with a better implementation, see below. > Why not something like "set-proxy-v2-tlv"? Maybe we then could also leave out > the v2. I would propose to go with the set-proxy-v2-tlv-fmt as you suggested later. I would leave in the v2 as other server options that concern v2 also leave it in. I don't mind being more verbose here. Realistically, there are only 2-3 of these set-proxy-v2-tlv-fmt anyway. Expression could make sense at some later point in time. > That's also why I'm extremely careful about the config shortcomings that can > come with it, because I suspect it could become popular and we really want to > avoid new fall traps. Sure, that is completely understandable. > I've been wondering if we want to use a format string or raw binary data > here, because some options are binary and nothing indicates that this will > not continue in the future. However since fc_pp_tlv() returns a string, I > guess most of them will continue this way. I also think type string is fine. > Also once this is merged, a new problem will emerge. There's still no > registry for the PPv2 TLV types. I guess this should be okay for most cases where it is just used internally. But yeah, most people will just come up with TLVs off the top their head. I will definitely add a note about the ranges. > Given that you had previously contributed the ability to fetch a TLV field > from the front connection's proxy protocol using fc_pp_tlv(), I'd rather make > use of these ones explicitly. If you need to change them, then you just > assign them to a variable and edit that variable, then send the variable. Or > you can change them in-flight using a converter. Of course it would make > configs slightly more verbose, but they would always do the right thing and > we would not have to continue to special-case them over time because they > break expectations like set-src/set-dst did. Generally, I wouldn't mind a more verbose config. As usually there shouldn't be too many TLV types that need to be considered. I have no problem leaving out the setter. It was just merely an (premature) optimization approach. > I think that most of your patch will remain as-is, it's only the parsing part > which needs to change a little bit to take such points into account. Ok, great! Here is how I adjusted the first and the second commit (besides Doc and Tests). It works quite nicely with the keyword. An example server config works like this now: server dummy_server 127.0.0.1:2319 send-proxy-v2 set-proxy-v2-tlv-fmt(0x10) %[str("hi")] set-proxy-v2-tlv-fmt(0x11) %[fc_pp_tlv(0x11)] I could perfectly replicate the forwarding behavior with the fc_pp_tlv. I hope you are fine with the changes. If so, I would send the updated series. Then, we could hopefully get into the final, detailed part of review. BTW, I've read that there is a feature freeze for 2.9, so I guess this change comes too late now? Best, Alexander ------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/src/connection.c b/src/connection.c index 07da8a5e2..6446a8fed 100644 --- a/src/connection.c +++ b/src/connection.c @@ -1927,7 +1927,6 @@ static int make_proxy_line_v2(char *buf, int buf_len, struct server *srv, struct const struct sockaddr_storage *dst = &null_addr; const char *value = ""; int value_len = 0; - int found = 0; if (buf_len < PP2_HEADER_LEN) return 0; @@ -2003,6 +2002,9 @@ static int make_proxy_line_v2(char *buf, int buf_len, struct server *srv, struct list_for_each_entry(srv_tlv, &srv->pp_tlvs, list) { replace = NULL; + /* Users will always need to provide a value, in case of forwarding, they should use fc_pp_tlv. + * for generic types. Otherwise, we will send an empty TLV. + */ if (!LIST_ISEMPTY(&srv_tlv->fmt)) { replace = alloc_trash_chunk(); if (unlikely(!replace)) @@ -2017,23 +2019,8 @@ static int make_proxy_line_v2(char *buf, int buf_len, struct server *srv, struct free_trash_chunk(replace); } else { - found = 0; - /* Fall back to use TLV value from remote connection, if available */ - if (remote) { - struct conn_tlv_list *remote_tlv = NULL; - - /* Search in remote connection TLV list */ - list_for_each_entry(remote_tlv, &remote->tlv_list, list) { - if (remote_tlv->type != srv_tlv->type) - continue; - ret += make_tlv(&buf[ret], (buf_len - ret), srv_tlv->type, remote_tlv->len, remote_tlv->value); - found = 1; - break; - } - } - if (!found) - /* Create empty TLV as no value was specified */ - ret += make_tlv(&buf[ret], (buf_len - ret), srv_tlv->type, 0, NULL); + /* Create empty TLV as no value was specified */ + ret += make_tlv(&buf[ret], (buf_len - ret), srv_tlv->type, 0, NULL); } } } diff --git a/src/server.c b/src/server.c index 08f86d030..02084314a 100644 --- a/src/server.c +++ b/src/server.c @@ -1036,13 +1036,7 @@ static int srv_parse_proto(char **args, int *cur_arg, static int srv_parse_proxy_v2_options(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err) { - char *p = NULL, *n = NULL, *m = NULL; - char *key = NULL, *val = NULL; - char *endp = NULL; - unsigned char idx = 0; - size_t key_length = 0, val_length = 0; - struct srv_pp_tlv_list *srv_tlv = NULL; - + char *p, *n; for (p = args[*cur_arg+1]; p; p = n) { n = strchr(p, ','); if (n) @@ -1067,61 +1061,75 @@ static int srv_parse_proxy_v2_options(char **args, int *cur_arg, newsrv->pp_opts |= SRV_PP_V2_CRC32C; } else if (strcmp(p, "unique-id") == 0) { newsrv->pp_opts |= SRV_PP_V2_UNIQUE_ID; - } else { - /* reset val in case it was set before */ - val = NULL; - - /* try to split by '=' into generic pair key value pair (<id>=<expr>) */ - m = strchr(p, '='); - if (m) { - key_length = m - p; - key = (char *) malloc(key_length + 1); - if (unlikely(!key)) - goto fail; - snprintf(key, key_length + 1, "%s", p); - - val_length = strlen(p) - key_length; - val = (char *) malloc(val_length + 1); - if (unlikely(!val)) - goto fail; - snprintf(val, val_length + 1, "%s", m + 1); - } - else { - key = (char *) malloc(strlen(p) + 1); - if (unlikely(!key)) - goto fail; - snprintf(key, strlen(p) + 1, "%s", p); - } + } else + goto fail; + } + return 0; + fail: + if (err) + memprintf(err, "'%s' : proxy v2 option not implemented", p); + return ERR_ALERT | ERR_FATAL; +} - errno = 0; - idx = strtoul(key, &endp, 0); - if (unlikely((endp && *endp) != '\0' || (key == endp) || (errno != 0))) - goto fail; +/* Parse the "set-proxy-v2-tlv-fmt" server keyword */ +static int srv_parse_set_proxy_v2_tlv_fmt(char **args, int *cur_arg, + struct proxy *px, struct server *newsrv, char **err) +{ + char *error = NULL, *cmd_name = NULL, *tlv_fmt = NULL; + unsigned int tlv_type = 0; + struct srv_pp_tlv_list *srv_tlv = NULL; - /* processing is done in connection.c */ - srv_tlv = malloc(sizeof(struct srv_pp_tlv_list)); - if (unlikely(!srv_tlv)) - goto fail; + cmd_name = args[*cur_arg]; + if (!*cmd_name) { + goto fail; + } - srv_tlv->type = idx; - LIST_INIT(&srv_tlv->fmt); - if (val && unlikely(!parse_logformat_string(val, px, &srv_tlv->fmt, LOG_OPT_HTTP, PR_CAP_LISTEN, err))) - goto fail; + cmd_name += strlen("set-proxy-v2-tlv-fmt"); - LIST_APPEND(&newsrv->pp_tlvs, &srv_tlv->list); - free(key); - free(val); + if (*cmd_name == '(') { + cmd_name++; /* skip the '(' */ + errno = 0; + tlv_type = strtoul(cmd_name, &error, 0); /* convert TLV ID */ + if (unlikely((cmd_name == error) || (errno != 0))) { + memprintf(err, "'%s' : could not convert TLV ID", args[*cur_arg]); + goto fail; + } + if (errno == EINVAL) { + memprintf(err, "'%s' : could not find a valid number for the TLV ID", args[*cur_arg]); + goto fail; + } + if (*error != ')') { + memprintf(err, "'%s' : expects set-proxy-v2-tlv(<TLV ID>)", args[*cur_arg]); + goto fail; } + if (tlv_type > 0xFF) { + memprintf(err, "'%s' : the maximum allowed TLV ID is %d", args[*cur_arg], 0xFF); + goto fail; + } + } + + srv_tlv = malloc(sizeof(struct srv_pp_tlv_list)); + if (unlikely(!srv_tlv)) { + memprintf(err, "'%s' : failed to parse allocate TLV entry", args[*cur_arg]); + goto fail; } + srv_tlv->type = tlv_type; + + tlv_fmt = args[*cur_arg + 1]; + LIST_INIT(&srv_tlv->fmt); + if (tlv_fmt && unlikely(!parse_logformat_string(tlv_fmt, px, &srv_tlv->fmt, LOG_OPT_HTTP, PR_CAP_LISTEN, err))) { + memprintf(err, "'%s' : failed to parse format string", args[*cur_arg]); + goto fail; + } + LIST_APPEND(&newsrv->pp_tlvs, &srv_tlv->list); + + (*cur_arg)++; + return 0; -fail: - free(key); - free(val); + + fail: free(srv_tlv); errno = 0; - - if (err) - memprintf(err, "'%s' : failed to set proxy v2 option", p); return ERR_ALERT | ERR_FATAL; } @@ -1933,50 +1941,51 @@ void srv_compute_all_admin_states(struct proxy *px) * Note: -1 as ->skip value means that the number of arguments are variable. */ static struct srv_kw_list srv_kws = { "ALL", { }, { - { "backup", srv_parse_backup, 0, 1, 1 }, /* Flag as backup server */ - { "cookie", srv_parse_cookie, 1, 1, 0 }, /* Assign a cookie to the server */ - { "disabled", srv_parse_disabled, 0, 1, 1 }, /* Start the server in 'disabled' state */ - { "enabled", srv_parse_enabled, 0, 1, 1 }, /* Start the server in 'enabled' state */ - { "error-limit", srv_parse_error_limit, 1, 1, 1 }, /* Configure the consecutive count of check failures to consider a server on error */ - { "ws", srv_parse_ws, 1, 1, 1 }, /* websocket protocol */ - { "id", srv_parse_id, 1, 0, 1 }, /* set id# of server */ - { "init-addr", srv_parse_init_addr, 1, 1, 0 }, /* */ - { "log-proto", srv_parse_log_proto, 1, 1, 0 }, /* Set the protocol for event messages, only relevant in a ring section */ - { "maxconn", srv_parse_maxconn, 1, 1, 1 }, /* Set the max number of concurrent connection */ - { "maxqueue", srv_parse_maxqueue, 1, 1, 1 }, /* Set the max number of connection to put in queue */ - { "max-reuse", srv_parse_max_reuse, 1, 1, 0 }, /* Set the max number of requests on a connection, -1 means unlimited */ - { "minconn", srv_parse_minconn, 1, 1, 1 }, /* Enable a dynamic maxconn limit */ - { "namespace", srv_parse_namespace, 1, 1, 0 }, /* Namespace the server socket belongs to (if supported) */ - { "no-backup", srv_parse_no_backup, 0, 1, 1 }, /* Flag as non-backup server */ - { "no-send-proxy", srv_parse_no_send_proxy, 0, 1, 1 }, /* Disable use of PROXY V1 protocol */ - { "no-send-proxy-v2", srv_parse_no_send_proxy_v2, 0, 1, 1 }, /* Disable use of PROXY V2 protocol */ - { "no-tfo", srv_parse_no_tfo, 0, 1, 1 }, /* Disable use of TCP Fast Open */ - { "non-stick", srv_parse_non_stick, 0, 1, 0 }, /* Disable stick-table persistence */ - { "observe", srv_parse_observe, 1, 1, 1 }, /* Enables health adjusting based on observing communication with the server */ - { "on-error", srv_parse_on_error, 1, 1, 1 }, /* Configure the action on check failure */ - { "on-marked-down", srv_parse_on_marked_down, 1, 1, 1 }, /* Configure the action when a server is marked down */ - { "on-marked-up", srv_parse_on_marked_up, 1, 1, 1 }, /* Configure the action when a server is marked up */ - { "pool-low-conn", srv_parse_pool_low_conn, 1, 1, 1 }, /* Set the min number of orphan idle connecbefore being allowed to pick from other threads */ - { "pool-max-conn", srv_parse_pool_max_conn, 1, 1, 1 }, /* Set the max number of orphan idle connections, -1 means unlimited */ - { "pool-purge-delay", srv_parse_pool_purge_delay, 1, 1, 1 }, /* Set the time before we destroy orphan idle connections, defaults to 1s */ - { "proto", srv_parse_proto, 1, 1, 1 }, /* Set the proto to use for all outgoing connections */ - { "proxy-v2-options", srv_parse_proxy_v2_options, 1, 1, 1 }, /* options for send-proxy-v2 */ - { "redir", srv_parse_redir, 1, 1, 0 }, /* Enable redirection mode */ - { "resolve-net", srv_parse_resolve_net, 1, 1, 0 }, /* Set the preferred network range for name resolution */ - { "resolve-opts", srv_parse_resolve_opts, 1, 1, 0 }, /* Set options for name resolution */ - { "resolve-prefer", srv_parse_resolve_prefer, 1, 1, 0 }, /* Set the preferred family for name resolution */ - { "resolvers", srv_parse_resolvers, 1, 1, 0 }, /* Configure the resolver to use for name resolution */ - { "send-proxy", srv_parse_send_proxy, 0, 1, 1 }, /* Enforce use of PROXY V1 protocol */ - { "send-proxy-v2", srv_parse_send_proxy_v2, 0, 1, 1 }, /* Enforce use of PROXY V2 protocol */ - { "shard", srv_parse_shard, 1, 1, 1 }, /* Server shard (only in peers protocol context) */ - { "slowstart", srv_parse_slowstart, 1, 1, 1 }, /* Set the warm-up timer for a previously failed server */ - { "source", srv_parse_source, -1, 1, 1 }, /* Set the source address to be used to connect to the server */ - { "stick", srv_parse_stick, 0, 1, 0 }, /* Enable stick-table persistence */ - { "tfo", srv_parse_tfo, 0, 1, 1 }, /* enable TCP Fast Open of server */ - { "track", srv_parse_track, 1, 1, 1 }, /* Set the current state of the server, tracking another one */ - { "socks4", srv_parse_socks4, 1, 1, 0 }, /* Set the socks4 proxy of the server*/ - { "usesrc", srv_parse_usesrc, 0, 1, 1 }, /* safe-guard against usesrc without preceding <source> keyword */ - { "weight", srv_parse_weight, 1, 1, 1 }, /* Set the load-balancing weight */ + { "backup", srv_parse_backup, 0, 1, 1 }, /* Flag as backup server */ + { "cookie", srv_parse_cookie, 1, 1, 0 }, /* Assign a cookie to the server */ + { "disabled", srv_parse_disabled, 0, 1, 1 }, /* Start the server in 'disabled' state */ + { "enabled", srv_parse_enabled, 0, 1, 1 }, /* Start the server in 'enabled' state */ + { "error-limit", srv_parse_error_limit, 1, 1, 1 }, /* Configure the consecutive count of check failures to consider a server on error */ + { "ws", srv_parse_ws, 1, 1, 1 }, /* websocket protocol */ + { "id", srv_parse_id, 1, 0, 1 }, /* set id# of server */ + { "init-addr", srv_parse_init_addr, 1, 1, 0 }, /* */ + { "log-proto", srv_parse_log_proto, 1, 1, 0 }, /* Set the protocol for event messages, only relevant in a ring section */ + { "maxconn", srv_parse_maxconn, 1, 1, 1 }, /* Set the max number of concurrent connection */ + { "maxqueue", srv_parse_maxqueue, 1, 1, 1 }, /* Set the max number of connection to put in queue */ + { "max-reuse", srv_parse_max_reuse, 1, 1, 0 }, /* Set the max number of requests on a connection, -1 means unlimited */ + { "minconn", srv_parse_minconn, 1, 1, 1 }, /* Enable a dynamic maxconn limit */ + { "namespace", srv_parse_namespace, 1, 1, 0 }, /* Namespace the server socket belongs to (if supported) */ + { "no-backup", srv_parse_no_backup, 0, 1, 1 }, /* Flag as non-backup server */ + { "no-send-proxy", srv_parse_no_send_proxy, 0, 1, 1 }, /* Disable use of PROXY V1 protocol */ + { "no-send-proxy-v2", srv_parse_no_send_proxy_v2, 0, 1, 1 }, /* Disable use of PROXY V2 protocol */ + { "no-tfo", srv_parse_no_tfo, 0, 1, 1 }, /* Disable use of TCP Fast Open */ + { "non-stick", srv_parse_non_stick, 0, 1, 0 }, /* Disable stick-table persistence */ + { "observe", srv_parse_observe, 1, 1, 1 }, /* Enables health adjusting based on observing communication with the server */ + { "on-error", srv_parse_on_error, 1, 1, 1 }, /* Configure the action on check failure */ + { "on-marked-down", srv_parse_on_marked_down, 1, 1, 1 }, /* Configure the action when a server is marked down */ + { "on-marked-up", srv_parse_on_marked_up, 1, 1, 1 }, /* Configure the action when a server is marked up */ + { "pool-low-conn", srv_parse_pool_low_conn, 1, 1, 1 }, /* Set the min number of orphan idle connecbefore being allowed to pick from other threads */ + { "pool-max-conn", srv_parse_pool_max_conn, 1, 1, 1 }, /* Set the max number of orphan idle connections, -1 means unlimited */ + { "pool-purge-delay", srv_parse_pool_purge_delay, 1, 1, 1 }, /* Set the time before we destroy orphan idle connections, defaults to 1s */ + { "proto", srv_parse_proto, 1, 1, 1 }, /* Set the proto to use for all outgoing connections */ + { "proxy-v2-options", srv_parse_proxy_v2_options, 1, 1, 1 }, /* options for send-proxy-v2 */ + { "redir", srv_parse_redir, 1, 1, 0 }, /* Enable redirection mode */ + { "resolve-net", srv_parse_resolve_net, 1, 1, 0 }, /* Set the preferred network range for name resolution */ + { "resolve-opts", srv_parse_resolve_opts, 1, 1, 0 }, /* Set options for name resolution */ + { "resolve-prefer", srv_parse_resolve_prefer, 1, 1, 0 }, /* Set the preferred family for name resolution */ + { "resolvers", srv_parse_resolvers, 1, 1, 0 }, /* Configure the resolver to use for name resolution */ + { "send-proxy", srv_parse_send_proxy, 0, 1, 1 }, /* Enforce use of PROXY V1 protocol */ + { "send-proxy-v2", srv_parse_send_proxy_v2, 0, 1, 1 }, /* Enforce use of PROXY V2 protocol */ + { "set-proxy-v2-tlv-fmt", srv_parse_set_proxy_v2_tlv_fmt, 0, 1, 1 }, /* Set TLV of PROXY V2 protocol */ + { "shard", srv_parse_shard, 1, 1, 1 }, /* Server shard (only in peers protocol context) */ + { "slowstart", srv_parse_slowstart, 1, 1, 1 }, /* Set the warm-up timer for a previously failed server */ + { "source", srv_parse_source, -1, 1, 1 }, /* Set the source address to be used to connect to the server */ + { "stick", srv_parse_stick, 0, 1, 0 }, /* Enable stick-table persistence */ + { "tfo", srv_parse_tfo, 0, 1, 1 }, /* enable TCP Fast Open of server */ + { "track", srv_parse_track, 1, 1, 1 }, /* Set the current state of the server, tracking another one */ + { "socks4", srv_parse_socks4, 1, 1, 0 }, /* Set the socks4 proxy of the server*/ + { "usesrc", srv_parse_usesrc, 0, 1, 1 }, /* safe-guard against usesrc without preceding <source> keyword */ + { "weight", srv_parse_weight, 1, 1, 1 }, /* Set the load-balancing weight */ { NULL, NULL, 0 }, }}; -----Original Message----- From: Willy Tarreau <w...@1wt.eu> Sent: Wednesday, October 18, 2023 9:33 AM To: Stephan, Alexander <alexander.step...@sap.com> Cc: haproxy@formilux.org Subject: Re: [PATCH 2/4] MEDIUM: connection: Send out generically allocated proxy-v2-options Hi Alexander, I'm starting from the doc as it eases the discussion. On Thu, Oct 05, 2023 at 11:05:50AM +0000, Stephan, Alexander wrote: > --- a/doc/configuration.txt > +++ b/doc/configuration.txt > @@ -16671,6 +16671,26 @@ proxy-v2-options <option>[,<option>]* > generated unique ID is also used for the first HTTP request > within a Keep-Alive connection. > > + Besides, you can specify the type of TLV numerically instead. > + > + Example 1: > + server example 127.0.0.1:2319 send-proxy-v2 proxy-v2-options > + crc32c,0xEE > + > + The following logic applies: By default, the remote frontend > + connection is searched for the value 0xEE. If found, it is simply > + forwarded. Otherwise, a TLV with the ID "0xEE" and empty payload is > + sent out. In addition, crc32c is also set here. > + > + You can also specify a key-value pair <id>=<fmt> syntax. <id> > + represents the > + 8 bit TLV type field in the range of 0 to 255. It can be expressed > + in decimal or hexadecimal format (prefixed by "0x"). > + > + Example 2: > + server example_server 127.0.0.1:2319 send-proxy-v2 proxy-v2-options > + 0xEE=%[str("foo")] > + > + This will always send out the value "foo". Another common use case > + would be to reference a variable. I'm really having an issue with using "the remote frontend connection" here. We've done this mistake in the past with the transparent mode that connects to the original destination address, that resulted in "set-dst" having to be implemented, then being broken by multiplexed streams (e.g. h2), then having to be internally emulated at various layers (connection, session, stream, transaction etc). Same for src. The solution is not durable at all, and as you noticed, you already had to implement the ability to modify these fields before passing them, hence reintroducing that class of trouble on a new area. Also what will be left in the logs after you modify the contents ? etc. Given that you had previously contributed the ability to fetch a TLV field from the front connection's proxy protocol using fc_pp_tlv(), I'd rather make use of these ones explicitly. If you need to change them, then you just assign them to a variable and edit that variable, then send the variable. Or you can change them in-flight using a converter. Of course it would make configs slightly more verbose, but they would always do the right thing and we would not have to continue to special-case them over time because they break expectations like set-src/set-dst did. I'm seeing another issue with passing type=fmt like this. The option is currently defined as cutting around commas, and that's what it does. This means that the format will not be parsable (no sample fetch functions with more than one argument, no converters). Example: proxy-v2-options 0xEE=%[var(txn.clientname,_unknown_)] ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ First option Second option Also the syntax in the format something=value is not that great for config parsers and generators. However it looks like our server keywords support parenthesis, so I think that instead you could do the following: proxy-v2-options(0xEE) %[var(txn.clientname,_unknown_)] In this case the proxy-v2-options keyword would only add one option and take a log-format, and you'd add as many such keywords for each option. Note that from a configuration perspective this is cleaner as it's easier to maintain value pairs like this for config management. But I don't think the option name is that great especially to send a single one. So maybe this one instead should be "proxy-v2-option(type) <fmt>" though it may maintain some confusion (option vs options). The other solution might be to have a shorter one, which is also nicer when using many of these, e.g. "proxy-v2-opt()". Such an approach would basically split the proxy-v2 usage in two groups: - options that are either there or not there (proxy-v2-options) - options that need to take a value (proxy-v2-opt()). I've been wondering if we want to use a format string or raw binary data here, because some options are binary and nothing indicates that this will not continue in the future. However since fc_pp_tlv() returns a string, I guess most of them will continue this way. For set-var() we're using a raw typed expression, but then we created set-var-fmt() to have one that takes a log format. Just thinking loud, but then what about "proxy-v2-optfmt()" or even "proxy-v2-tlvfmt()" or "pp-tlv-fmt()" (after all when speaking about tlv we imply v2, right?). That would allow us later to implement "pp-tlv()" taking an expression if really needed, e.g. to pass a DER certificate, a SHA256, a sub-tlv or any such a thing. Generally speaking I'm fine with the approach of making the output PP TLV configurable, I think it does bring some value and flexibility in general. That's also why I'm extremely careful about the config shortcomings that can come with it, because I suspect it could become popular and we really want to avoid new fall traps. I think that most of your patch will remain as-is, it's only the parsing part which needs to change a little bit to take such points into account. Also once this is merged, a new problem will emerge. There's still no registry for the PPv2 TLV types. Years ago I've been suggested to write an RFC about it but I just can't take two more hours of my daily sleep time to work on that. I'm seeing that the proxy-protocol doc already reserves a two ranges (one for locally-administered values, one for experiment). I think that the doc for the keyword should put a big caution about this, suggesting to have a look at that section and not to pick random values out of nowhere. Willy