Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package haproxy for openSUSE:Factory checked in at 2026-10-09 21:43:28 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/haproxy (Old) and /work/SRC/openSUSE:Factory/.haproxy.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "haproxy" Fri Oct 9 21:43:28 2026 rev:194 rq:1383351 version:3.4.6+git1.3ce8f6169 Changes: -------- --- /work/SRC/openSUSE:Factory/haproxy/haproxy.changes 2026-09-24 23:04:35.954169858 +0200 +++ /work/SRC/openSUSE:Factory/.haproxy.new.3698736/haproxy.changes 2026-10-09 21:43:31.254621460 +0200 @@ -1,0 +2,20 @@ +Thu Oct 01 15:49:07 UTC 2026 - Marcus Rueckert <[email protected]> + +- Update to version 2.3.0+git2.b031728: + * DOC: otel: added a security policy to the repository + * MINOR: otel: test: added a test with an optional upstream context + +------------------------------------------------------------------- +Thu Oct 01 15:49:06 UTC 2026 - Marcus Rueckert <[email protected]> + +- Update to version 3.4.6+git1.3ce8f6169: + * MEDIUM: jwt: Reject "none" alg in jwt_verify by default + * [RELEASE] Released version 3.4.6 + * REGTESTS: log: check that a ring's internal server may target a UNIX socket + * BUG/MEDIUM: server: skip log backend addr checks for internal proxies + * BUG/MEDIUM: sink: reconnect attempt not working after session lasted more than ~25 days + * BUG/MAJOR: mux_quic: fix potential crash on RESET_STREAM receive + * MINOR: log: add the +utf8 encoding option to let valid UTF-8 pass + * MINOR: log: pass the input end to the _lf_encode_bytes() byte encoders + +------------------------------------------------------------------- Old: ---- haproxy-3.4.5+git0.c28c410a7.tar.gz haproxy-opentelemetry-2.3.0+git0.4d52326.tar.gz New: ---- haproxy-3.4.6+git1.3ce8f6169.tar.gz haproxy-opentelemetry-2.3.0+git2.b031728.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ haproxy.spec ++++++ --- /var/tmp/diff_new_pack.fZX0vD/_old 2026-10-09 21:43:32.406669743 +0200 +++ /var/tmp/diff_new_pack.fZX0vD/_new 2026-10-09 21:43:32.409669869 +0200 @@ -44,10 +44,10 @@ %bcond_with opentelemetry Name: haproxy -Version: 3.4.5+git0.c28c410a7 +Version: 3.4.6+git1.3ce8f6169 Release: 0 %if %{with opentelemetry} -%global otel_revision 2.3.0+git0.4d52326 +%global otel_revision 2.3.0+git2.b031728 %global otel_subdir haproxy-opentelemetry-%{otel_revision} %global otel_additional_source -a9 %endif ++++++ _service ++++++ --- /var/tmp/diff_new_pack.fZX0vD/_old 2026-10-09 21:43:32.483672970 +0200 +++ /var/tmp/diff_new_pack.fZX0vD/_new 2026-10-09 21:43:32.486673096 +0200 @@ -1,25 +1,26 @@ <services> <service name="tar_scm" mode="manual"> - <param name="url">http://git.haproxy.org/git/haproxy-3.4.git/</param> + <param name="url">https://github.com/haproxytech/haproxy-opentelemetry.git/</param> <param name="scm">git</param> - <param name="filename">haproxy</param> + <param name="filename">haproxy-opentelemetry</param> <param name="versionformat">@PARENT_TAG@+git@TAG_OFFSET@.%h</param> - <param name="revision">master</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="versionrewrite-replacement">\1</param> + <param name="revision">main</param> <param name="changesgenerate">enable</param> </service> <service name="tar_scm" mode="manual"> - <param name="url">https://github.com/haproxytech/haproxy-opentelemetry.git/</param> + <param name="url">http://git.haproxy.org/git/haproxy-3.4.git/</param> <param name="scm">git</param> - <param name="filename">haproxy-opentelemetry</param> + <param name="filename">haproxy</param> <param name="versionformat">@PARENT_TAG@+git@TAG_OFFSET@.%h</param> + <param name="revision">master</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="versionrewrite-replacement">\1</param> - <param name="revision">main</param> <param name="changesgenerate">enable</param> </service> + <service name="recompress" mode="manual"> <param name="file">haproxy*.tar</param> <param name="compression">gz</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.fZX0vD/_old 2026-10-09 21:43:32.512674186 +0200 +++ /var/tmp/diff_new_pack.fZX0vD/_new 2026-10-09 21:43:32.516674353 +0200 @@ -1,11 +1,11 @@ <servicedata> <service name="tar_scm"> <param name="url">http://git.haproxy.org/git/haproxy-3.4.git/</param> - <param name="changesrevision">c28c410a73af38da8300ff7d95b2af3c30bdc68d</param> + <param name="changesrevision">3ce8f616974b28fdb289cf179818696cd8a6abf7</param> </service> <service name="tar_scm"> <param name="url">https://github.com/haproxytech/haproxy-opentelemetry.git/</param> - <param name="changesrevision">4d52326b45c6211436b76e609030568974017db7</param> + <param name="changesrevision">b03172819d4180e71b8c6e436c562573491cd707</param> </service> </servicedata> (No newline at EOF) ++++++ haproxy-3.4.5+git0.c28c410a7.tar.gz -> haproxy-3.4.6+git1.3ce8f6169.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/CHANGELOG new/haproxy-3.4.6+git1.3ce8f6169/CHANGELOG --- old/haproxy-3.4.5+git0.c28c410a7/CHANGELOG 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/CHANGELOG 2026-09-30 17:24:40.000000000 +0200 @@ -1,6 +1,14 @@ ChangeLog : =========== +2026/09/28 : 3.4.6 + - MINOR: log: pass the input end to the _lf_encode_bytes() byte encoders + - MINOR: log: add the +utf8 encoding option to let valid UTF-8 pass + - BUG/MAJOR: mux_quic: fix potential crash on RESET_STREAM receive + - BUG/MEDIUM: sink: reconnect attempt not working after session lasted more than ~25 days + - BUG/MEDIUM: server: skip log backend addr checks for internal proxies + - REGTESTS: log: check that a ring's internal server may target a UNIX socket + 2026/09/24 : 3.4.5 - BUG/MINOR: cache: Manage collisions on primary key - BUG/MINOR: cache: Seed cache primary hash diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/VERDATE new/haproxy-3.4.6+git1.3ce8f6169/VERDATE --- old/haproxy-3.4.5+git0.c28c410a7/VERDATE 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/VERDATE 2026-09-30 17:24:40.000000000 +0200 @@ -1,2 +1,2 @@ $Format:%ci$ -2026/09/24 +2026/09/28 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/VERSION new/haproxy-3.4.6+git1.3ce8f6169/VERSION --- old/haproxy-3.4.5+git0.c28c410a7/VERSION 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/VERSION 2026-09-30 17:24:40.000000000 +0200 @@ -1 +1 @@ -3.4.5 +3.4.6 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/doc/configuration.txt new/haproxy-3.4.6+git1.3ce8f6169/doc/configuration.txt --- old/haproxy-3.4.5+git0.c28c410a7/doc/configuration.txt 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/doc/configuration.txt 2026-09-30 17:24:40.000000000 +0200 @@ -3,7 +3,7 @@ Configuration Manual ---------------------- version 3.4 - 2026/09/24 + 2026/09/28 This document covers the configuration language as implemented in the version @@ -1791,6 +1791,7 @@ - insecure-fork-wanted - insecure-setuid-wanted - issuers-chain-path + - jwt.allow_none_alg - jwt.decrypt_alg_list - jwt.decrypt_enc_list - key-base @@ -2907,6 +2908,12 @@ The OCSP features are able to use the completed chain when no .issuer was used, or no chain was provided in the PEM. +jwt.allow_none_alg + Allow JWT signed token with the "none" algorithm in the 'jwt_verify' + converter. They will always be considered valid when this option is set. By + default unsigned tokens are rejected since they are considered unsafe (see + RFC8725 section 3.1). + jwt.decrypt_alg_list <list> Set the list of algorithms allowed in the jwt_decrypt_XXX converters. JWT tokens using an unsupported or disabled algorithms will never be decrypted. @@ -22302,9 +22309,13 @@ | PS256 | RSASSA-PSS using SHA-256 and MGF1 with SHA-256 | | PS384 | RSASSA-PSS using SHA-384 and MGF1 with SHA-384 | | PS512 | RSASSA-PSS using SHA-512 and MGF1 with SHA-512 | - | none | No digital signature or MAC performed | + | none | No digital signature or MAC performed (see note below) | +--------------+---------------------------------------------------------+ + Please note that the "none" algorithm is not allowed by default to follow + the recommendations in RFC8725 section 3.2. Set global option + 'jwt.allow_none_alg' to explicitely allow it. + - <key> can be either a string or a variable name (See also "set-var") that holds a secret or a public key path. @@ -29497,6 +29508,24 @@ Incomplete numerical values (e.g.: '%B' when logasap is used), which are normally prefixed with '+' without encoding, will be encoded as-is. Also, '+E' option will be ignored. + As described in section 8.6, characters outside the 32..126 range + are escaped one byte at a time, using the JSON escape sequences, + so that the output remains pure printable ASCII whatever the + input. This includes valid UTF-8 sequences, meaning that non-ASCII + text will appear as its individual bytes once decoded (e.g. + U+00E9, encoded as 0xC3 0xA9, is logged as "\u00C3\u00A9"). See + "utf8" below to preserve such text. + * utf8: combined with "json", permits to let valid multi-byte UTF-8 sequences + pass unescaped. The C1 control characters (U+0080 to U+009F) are + still escaped, represented by their code point. Invalid sequences are + escaped one byte at a time so that the output always remains valid + UTF-8. Note that since valid UTF-8 is emitted as-is, a client may use + some special characters to alter the rendering of a log line on a + terminal or in a log viewer, such as direction changes, zero-width + characters, or line and paragraph separators. Thus it is recommended + to only use it for contents coming from trusted sources, or when logs + are not meant to be displayed on a terminal. It has no effect without + "json". * cbor: automatically encode value in CBOR format (when set globally, only named logformat items are considered) By default, cbor encoded data is represented in HEX form so @@ -29517,6 +29546,7 @@ log-format-sd %{+Q,+E}o\ [exampleSDID@1234\ header=%[capture.req.hdr(0)]] log-format "%{+json}o %(request)r %(custom_expr)[str(custom)]" + log-format "%{+json,+utf8}o %(request)r %(custom_expr)[str(custom)]" log-format "%{+cbor}o %(request)r %(custom_expr)[str(custom)]" Please refer to the table below for currently defined aliases : @@ -30495,6 +30525,10 @@ digits, or properly delimited binary blocks that will not be sent to a terminal but will be consumed by a CBOR parser. +The json encoding uses the JSON escape sequences instead, and the +utf8 option +also lets valid multi-byte UTF-8 sequences pass as-is (see the "utf8" flag in +section 8.2.6). + Note that the space character (' ') is not encoded in headers, which can cause issues for tools relying on space count to locate fields. A typical header containing spaces is "User-Agent". diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/include/haproxy/log-t.h new/haproxy-3.4.6+git1.3ce8f6169/include/haproxy/log-t.h --- old/haproxy-3.4.5+git0.c28c410a7/include/haproxy/log-t.h 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/include/haproxy/log-t.h 2026-09-30 17:24:40.000000000 +0200 @@ -48,7 +48,8 @@ #define LOG_OPT_ESC 0x00000040 #define LOG_OPT_MERGE_SPACES 0x00000080 #define LOG_OPT_BIN 0x00000100 -/* unused: 0x00000200 ... 0x00000800 */ +#define LOG_OPT_UTF8 0x00000200 // only used with LOG_OPT_ENCODE_JSON (utf-8 pass-through) +/* unused: 0x00000400 ... 0x00000800 */ #define LOG_OPT_ENCODE_JSON 0x00001000 #define LOG_OPT_ENCODE_CBOR 0x00002000 #define LOG_OPT_ENCODE 0x00003000 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/include/haproxy/sink-t.h new/haproxy-3.4.6+git1.3ce8f6169/include/haproxy/sink-t.h --- old/haproxy-3.4.5+git0.c28c410a7/include/haproxy/sink-t.h 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/include/haproxy/sink-t.h 2026-09-30 17:24:40.000000000 +0200 @@ -40,7 +40,7 @@ struct sink_forward_target { struct server *srv; // used server struct appctx *appctx; // appctx of current session - uint last_conn; // copy of now_ms for last session establishment attempt + uint last_close; // copy of now_ms for last session destroy/close size_t ofs; // ring buffer reader offset size_t e_processed; // processed events struct sink *sink; // the associated sink diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/reg-tests/jwt/jws_verify.vtc new/haproxy-3.4.6+git1.3ce8f6169/reg-tests/jwt/jws_verify.vtc --- old/haproxy-3.4.5+git0.c28c410a7/reg-tests/jwt/jws_verify.vtc 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/reg-tests/jwt/jws_verify.vtc 2026-09-30 17:24:40.000000000 +0200 @@ -32,6 +32,7 @@ .endif tune.ssl.capture-buffer-size 1 stats socket "${tmpdir}/h1/stats" level admin + jwt.allow_none_alg defaults mode http diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/reg-tests/log/log_json.vtc new/haproxy-3.4.6+git1.3ce8f6169/reg-tests/log/log_json.vtc --- old/haproxy-3.4.5+git0.c28c410a7/reg-tests/log/log_json.vtc 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/reg-tests/log/log_json.vtc 2026-09-30 17:24:40.000000000 +0200 @@ -6,7 +6,9 @@ # JSON encoding has no reason to be more permissive than the plain one. # Escaping them all also makes the output valid UTF-8, which RFC 8259 #8.1 # requires, without having to guess what the logged bytes represent: a header -# value is arbitrary bytes, not text. +# value is arbitrary bytes, not text. The +utf8 option bypasses this, letting +# valid multi-byte UTF-8 sequences pass as-is, and still escapes all other +# bytes above 126 one at a time so that the output remains valid UTF-8. haproxy h1 -conf { global @@ -18,6 +20,10 @@ size 16384 format raw + ring rawring + size 16384 + format raw + defaults mode http timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" @@ -30,6 +36,13 @@ log ring@myring local0 log-format "{\"hdr\":%{+json}[capture.req.hdr(0)],\"bin\":%{+json}[str(Gw==),b64dec]}" http-request return status 200 + + frontend fe_raw + bind "fd@${fe_raw}" + capture request header x len 64 + log ring@rawring local0 + log-format "%{+json,+utf8}o %(hdr)[capture.req.hdr(0)] %(bin)[str(w6nCm+KC),b64dec]" + http-request return status 200 } -start # an ESC, which has no two-character form, a tab, which has one, a double quote @@ -48,6 +61,15 @@ expect resp.status == 200 } -run +# in raw mode: a valid UTF-8 sequence, a lone continuation byte, an overlong +# form, an encoded surrogate, a C1 control character, a 4-byte sequence, one +# beyond U+10FFFF and a sequence truncated by the end of the value +client c3 -connect ${h1_fe_raw_sock} { + txreq -url "/raw" -hdr "x: A\xc3\xa9B\xaaC\xc0\x81D\xed\xa0\x80E\xc2\x9bF\xf0\x9f\x98\x80G\xf4\x90\x80\x80H\xe2\x82" + rxresp + expect resp.status == 200 +} -run + delay 0.2 # the ESC comes out as the six-character form and the tab as the short one, in @@ -75,3 +97,18 @@ send "show events myring -s" expect !~ "\\?" } + +# in raw mode, the valid UTF-8 sequences are emitted as-is, the C1 control +# character as its code point's escape sequence, and all other bytes above the +# printable range get one escape per byte, so that the output remains valid +# UTF-8 +haproxy h1 -cli { + send "show events rawring" + expect ~ "\"hdr\": \"A\xc3\xa9B\\\\u00AAC\\\\u00C0\\\\u0081D\\\\u00ED\\\\u00A0\\\\u0080E\\\\u009BF\xf0\x9f\x98\x80G\\\\u00F4\\\\u0090\\\\u0080\\\\u0080H\\\\u00E2\\\\u0082\"" +} + +# same in the binary sample path, where the truncated sequence ends the sample +haproxy h1 -cli { + send "show events rawring" + expect ~ "\"bin\": \"\xc3\xa9\\\\u009B\\\\u00E2\\\\u0082\"" +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/reg-tests/log/ring_unix_log_forward.vtc new/haproxy-3.4.6+git1.3ce8f6169/reg-tests/log/ring_unix_log_forward.vtc --- old/haproxy-3.4.5+git0.c28c410a7/reg-tests/log/ring_unix_log_forward.vtc 1970-01-01 01:00:00.000000000 +0100 +++ new/haproxy-3.4.6+git1.3ce8f6169/reg-tests/log/ring_unix_log_forward.vtc 2026-09-30 17:24:40.000000000 +0200 @@ -0,0 +1,83 @@ +varnishtest "ring section's internal server may target a UNIX socket" + +# 23e5f18b8 ("MEDIUM: sink: change the sink mode type to PR_MODE_SYSLOG") +# tagged a sink's forward_px (the internal proxy backing a "ring" section) +# with PR_MODE_SYSLOG. As an unintended side effect, this made ring's own +# "server" line subject to _srv_check_proxy_mode()'s log-backend +# address-family check (added by 64e0b6344 for real "mode log" backends), +# which rejects AF_UNIX. This broke the common "ring -> UNIX socket -> +# log-forward" pattern, which worked fine before 23e5f18b8. +# +# This test declares a ring whose server targets a UNIX socket bound by a +# log-forward section, which itself relays to a vtest syslog receiver. It +# fails to parse on an affected, unpatched build ("log server address +# family not supported for log backend server"). + +feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(3.3-dev2)'" +feature ignore_unknown_macro + +syslog Slg1 -level info { + recv + expect ~ "GET /client_c1 HTTP/1.1" +} -start + +server s1 { + rxreq + txresp +} -start + +haproxy h1 -conf { + global + .if feature(THREAD) + thread-groups 1 + .endif + # when run as root without an explicit "chroot" directive, haproxy's + # automatic "chroot auto" (see 641fe4f11) isolates the process into + # an empty directory right after startup. That breaks the ring's own + # UNIX-socket "server" connect (a runtime, repeatedly-retried client + # connection), unlike the log-forward listener's "bind unix@..." + # which is already open before the chroot happens. Pin "chroot /" so + # this test behaves the same whether run as root or not. + chroot / + + defaults + mode http + option httplog + timeout connect "${HAPROXY_TEST_TIMEOUT-5s}" + timeout client "${HAPROXY_TEST_TIMEOUT-5s}" + timeout server "${HAPROXY_TEST_TIMEOUT-5s}" + + frontend fe1 + bind "fd@${fe_1}" + log ring@buf1 local0 + default_backend be + + backend be + server app1 ${s1_addr}:${s1_port} + + ring buf1 + description "ring fed via a UNIX-socket server" + format rfc5424 + maxlen 1200 + size 32764 + timeout connect 5s + timeout server 10s + # note: format/maxlen/size above are required for the ring's + # forwarder to actually flush to its server - omitting them + # leaves the ring silently unable to forward (unrelated to the + # fix under test, just a config-authoring trap) + # the whole point of this test: a UNIX socket server on a ring + server relay unix@"${tmpdir}/ring_unix_log_forward.sock" + + log-forward relay + bind unix@"${tmpdir}/ring_unix_log_forward.sock" mode 660 + log ${Slg1_addr}:${Slg1_port} local0 +} -start + +client c1 -connect ${h1_fe_1_sock} { + txreq -url "/client_c1" + rxresp + expect resp.status == 200 +} -run + +syslog Slg1 -wait diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/src/jwt.c new/haproxy-3.4.6+git1.3ce8f6169/src/jwt.c --- old/haproxy-3.4.5+git0.c28c410a7/src/jwt.c 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/src/jwt.c 2026-09-30 17:24:40.000000000 +0200 @@ -30,6 +30,8 @@ /* Tree into which the public certificates used to validate JWTs will be stored. */ static struct eb_root jwt_cert_tree = EB_ROOT_UNIQUE; +/* Set whether "none" algorithm should be allowed or not */ +static int jwt_verify_allow_none_alg = 0; /* * The possible algorithm strings that can be found in a JWS's JOSE header are @@ -508,6 +510,9 @@ if (ctx.alg == JWT_ALG_DEFAULT) return JWT_VRFY_UNKNOWN_ALG; + if (ctx.alg == JWS_ALG_NONE && !jwt_verify_allow_none_alg) + return JWT_VRFY_UNMANAGED_ALG; + if (jwt_tokenize(token, items, item_num)) return JWT_VRFY_INVALID_TOKEN; @@ -575,6 +580,23 @@ return retval; } +/* Parse 'jwt.allow_none_arg' option */ +static int cfg_parse_global_jwt_none_alg(char **args, int section_type, struct proxy *curpx, + const struct proxy *defpx, const char *file, int linenum, char **err) +{ + if (*args[1]) { + memprintf(err, "parsing [%s:%d]: keyword '%s' in '%s' section does not take any arguments.\n", file, linenum, args[0], cursection); + goto error; + } + + jwt_verify_allow_none_alg = 1; + + return 0; + +error: + return -1; +} + static void jwt_deinit(void) { @@ -592,5 +614,12 @@ } REGISTER_POST_DEINIT(jwt_deinit); +static struct cfg_kw_list cfg_kws = {ILH, { + { CFG_GLOBAL, "jwt.allow_none_alg", cfg_parse_global_jwt_none_alg }, + { 0, NULL, NULL }, +}}; + +INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws); + #endif /* USE_OPENSSL */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/src/log.c new/haproxy-3.4.6+git1.3ce8f6169/src/log.c --- old/haproxy-3.4.5+git0.c28c410a7/src/log.c 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/src/log.c 2026-09-30 17:24:40.000000000 +0200 @@ -428,6 +428,7 @@ { "X", LOG_OPT_HEXA }, { "E", LOG_OPT_ESC }, { "bin", LOG_OPT_BIN }, + { "utf8", LOG_OPT_UTF8 }, { "json", LOG_OPT_ENCODE_JSON }, { "cbor", LOG_OPT_ENCODE_CBOR }, { 0, 0 } @@ -2023,6 +2024,7 @@ int options; /* LOG_OPT_* options */ int typecast; /* same as logformat_node->typecast */ int in_text; /* inside variable-length text */ + int utf8_skip; /* JSON: continuation bytes of an already emitted UTF-8 sequence */ union { struct cbor_encode_ctx cbor; /* cbor-encode specific ctx */ } encode; @@ -2140,13 +2142,16 @@ /* helper function for _lf_encode_bytes() to encode a single byte * and escape it with <escape> if found in <map> * + * All these helpers receive the current byte in <byte> and the end of the + * input in <end>, which is NULL when the input is NUL-terminated. + * * The function assumes that at least 1 byte is available for writing * * Returns the address of the byte immediately after the last written byte * on success, or NULL on error. It will not append terminating NULL byte. */ static inline char *_lf_map_escape_byte(char *start, char *stop, - const char *byte, + const char *byte, const char *end, const char escape, const long *map, const char **pending, uint8_t cbor_string_prefix, struct lf_buildctx *ctx) @@ -2169,7 +2174,7 @@ * on success, or NULL on error. It will not append terminating NULL byte. */ static inline char *_lf_cbor_map_escape_byte(char *start, char *stop, - const char *byte, + const char *byte, const char *end, const char escape, const long *map, const char **pending, uint8_t cbor_string_prefix, struct lf_buildctx *ctx) @@ -2213,7 +2218,7 @@ * on success, or NULL on error. It will not append terminating NULL byte. */ static inline char *_lf_rfc5424_escape_byte(char *start, char *stop, - const char *byte, + const char *byte, const char *end, const char escape, const long *map, const char **pending, uint8_t cbor_string_prefix, struct lf_buildctx *ctx) @@ -2276,9 +2281,57 @@ return start; } +/* Checks whether <s> starts with a multi-byte UTF-8 sequence which is valid as + * per RFC 3629 and does not span more than <len> bytes: no overlong form, no + * surrogate (U+D800..U+Dfff), nothing beyond U+10FFFF (U+FFFE and U+FFFF, + * which utf8_next() rejects as well, are simply escaped). The sequence may be + * terminated by a NUL byte before <len> bytes, since it is not a continuation + * byte. On success the code point is stored into <c> and the sequence length + * (2 to 4) is returned, otherwise zero is returned. + */ +static inline int _lf_json_utf8_len(const char *s, size_t len, unsigned int *c) +{ + unsigned char ret; + + ret = utf8_next(s, MIN(len, 4), c); + if (utf8_return_code(ret) != UTF8_CODE_OK) + return 0; + + ret = utf8_return_length(ret); + if (ret < 2 || ret > 4 || *c > 0x10FFFF) + return 0; + + return ret; +} + +/* Writes at <start> the valid UTF-8 sequence of <len> bytes found at <seq> and + * representing code point <c>, as reported by _lf_json_utf8_len(). It is + * copied as-is, except for the C1 control characters (U+0080 .. U+009F) which, + * just like the C0 ones (U+0000 .. U+001F), could be interpreted by a + * terminal, and which are written as their \u00XX escape sequence instead. + * + * The function returns the address of the byte immediately after the last + * written byte, or NULL if the whole sequence does not fit before <stop>, so + * that a sequence is never truncated. It will not append a terminating NULL + * byte. + */ +static inline char *_lf_json_utf8_seq(char *start, char *stop, + const char *seq, int len, unsigned int c) +{ + if (c < 0xa0) + return _lf_json_escape_seq(start, stop, c); + + if (start + len > stop) + return NULL; + memcpy(start, seq, len); + return start + len; +} + /* helper function for _lf_encode_bytes() to encode a single byte * and escape it with <escape> if found in <map> or escape it with - * '\' if found in json_escape_map + * '\' if found in json_escape_map. With LOG_OPT_UTF8, valid multi-byte + * UTF-8 sequences are emitted at once when their first byte is met + * instead, the following ones being skipped. * * The function assumes that at least 1 byte is available for writing * @@ -2286,14 +2339,29 @@ * on success, or NULL on error. It will not append terminating NULL byte. */ static inline char *_lf_json_escape_byte(char *start, char *stop, - const char *byte, + const char *byte, const char *end, const char escape, const long *map, const char **pending, uint8_t cbor_string_prefix, struct lf_buildctx *ctx) { + unsigned int c; + int len; + + if (ctx->utf8_skip) { + /* already emitted with the sequence's first byte */ + ctx->utf8_skip--; + return start; + } + if (!ha_bit_test((unsigned char)(*byte), map)) { if (!ha_bit_test((unsigned char)(*byte), json_escape_map)) *start++ = *byte; + else if ((ctx->options & LOG_OPT_UTF8) && + (len = _lf_json_utf8_len(byte, end ? end - byte : 4, &c))) { + start = _lf_json_utf8_seq(start, stop, byte, len, c); + if (start) + ctx->utf8_skip = len - 1; + } else start = _lf_json_escape_seq(start, stop, *byte); } @@ -2330,13 +2398,15 @@ const char *pending; uint8_t cbor_string_prefix = 0; char *(*encode_byte)(char *start, char *stop, - const char *byte, + const char *byte, const char *bytes_stop, const char escape, const long *map, const char **pending, uint8_t cbor_string_prefix, struct lf_buildctx *ctx); - if (ctx->options & LOG_OPT_ENCODE_JSON) + if (ctx->options & LOG_OPT_ENCODE_JSON) { encode_byte = _lf_json_escape_byte; + ctx->utf8_skip = 0; + } else if (ctx->options & LOG_OPT_ENCODE_CBOR) encode_byte = _lf_cbor_map_escape_byte; else if (ctx->options & LOG_OPT_ESC) @@ -2374,7 +2444,8 @@ */ if (bytes && !bytes_stop) { while (start < stop && *bytes != '\0') { - ret = encode_byte(start, stop, bytes, escape, map, + ret = encode_byte(start, stop, bytes, bytes_stop, + escape, map, &pending, cbor_string_prefix, ctx); if (ret == NULL) @@ -2384,7 +2455,8 @@ } } else if (bytes) { while (start < stop && bytes < bytes_stop) { - ret = encode_byte(start, stop, bytes, escape, map, + ret = encode_byte(start, stop, bytes, bytes_stop, + escape, map, &pending, cbor_string_prefix, ctx); if (ret == NULL) @@ -2445,26 +2517,40 @@ /* Same as escape_string() but for a JSON string content: the bytes tagged in * json_escape_map cannot all be expressed with the two-character form that - * escape_string() emits, the others need the \u00XX one. + * escape_string() emits, the others need the \u00XX one. When <raw> is set, + * valid multi-byte UTF-8 sequences are emitted as-is instead (see + * _lf_json_utf8_seq()). * * Return the address of the \0 character, or NULL on error */ static char *_lf_json_escape_string(char *start, char *stop, - const char *string, const char *string_stop) + const char *string, const char *string_stop, + int raw) { if (start < stop) { stop--; /* reserve one byte for the final '\0' */ while (start < stop && string < string_stop && *string != '\0') { - if (!ha_bit_test((unsigned char)(*string), json_escape_map)) - *start++ = *string; - else { - char *next = _lf_json_escape_seq(start, stop, *string); + unsigned int c; + char *next; + int len; + + if (!ha_bit_test((unsigned char)(*string), json_escape_map)) { + *start++ = *string++; + continue; + } - if (!next) - break; /* does not fit, truncate here */ - start = next; + len = raw ? _lf_json_utf8_len(string, string_stop - string, &c) : 0; + if (len) + next = _lf_json_utf8_seq(start, stop, string, len, c); + else { + len = 1; + next = _lf_json_escape_seq(start, stop, *string); } - string++; + + if (!next) + break; /* does not fit, truncate here */ + start = next; + string += len; } *start = '\0'; return start; @@ -2516,7 +2602,8 @@ else if (ctx->options & LOG_OPT_ENCODE_JSON) { char *ret; - ret = _lf_json_escape_string(dst, dst + size, src, src + len); + ret = _lf_json_escape_string(dst, dst + size, src, src + len, + !!(ctx->options & LOG_OPT_UTF8)); if (ret == NULL) return NULL; len = ret - dst; @@ -3765,7 +3852,9 @@ * spares us from having to care about the UTF-8 validity that RFC 8259 * #8.1 demands, since a header value is made of arbitrary bytes and not * of text. On top of that, RFC 8259 #7 mandates escaping the double - * quote, the backslash and everything below 0x20. + * quote, the backslash and everything below 0x20. The raw JSON encoding + * is the only exception: its encoders detect the valid multi-byte UTF-8 + * sequences by themselves and emit them as-is. */ memset(json_escape_map, 0, sizeof(json_escape_map)); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/src/mux_quic.c new/haproxy-3.4.6+git1.3ce8f6169/src/mux_quic.c --- old/haproxy-3.4.5+git0.c28c410a7/src/mux_quic.c 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/src/mux_quic.c 2026-09-30 17:24:40.000000000 +0200 @@ -720,7 +720,17 @@ LIST_DEL_INIT(&qcs->el_send); TRACE_STATE("stream sent done", QMUX_EV_QCS_SEND, qcc->conn, qcs); - if (qcs->flags & (QC_SF_FIN_STREAM|QC_SF_DETACH)) { + /* Everything sent, close the stream if no new data will come. + * This is the case if either FIN has been sent or upper layer + * is detached. + * + * This is also performed if peer has resetted its input as no + * more transfer should occur on this QCS instance. + * TODO this assumption may be invalid depending on QUIC stream + * type - app proto should be responsible to deal with RS. + */ + if ((qcs->flags & (QC_SF_FIN_STREAM|QC_SF_DETACH)) || + (!qcs_sc(qcs) && (qcs->flags & QC_SF_RECV_RESET))) { /* Close stream locally. */ qcs_close_local(qcs); @@ -2356,13 +2366,11 @@ qcs_free_rxbuf(qcs, b); } - /* Check if RESET_STREAM received before stream layer initialization. - * If true, prepare QCS purgeing immediately. + /* No more data will be received after peer reset. QCS can be purged + * if there is nothing to sent on our side. */ - if (!qcs_sc(qcs) && !(qcs->flags & QC_SF_DETACH)) { + if (!qcs_sc(qcs) && !LIST_INLIST(&qcs->el_send)) { qcs_close_local(qcs); - - BUG_ON(LIST_INLIST(&qcs->el_send)); TRACE_STATE("add stream in purg_list", QMUX_EV_QCC_RECV|QMUX_EV_QCS_RECV, qcc->conn, qcs); LIST_APPEND(&qcs->qcc->purg_list, &qcs->el_send); tasklet_wakeup(qcs->qcc->wait_event.tasklet); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/src/server.c new/haproxy-3.4.6+git1.3ce8f6169/src/server.c --- old/haproxy-3.4.5+git0.c28c410a7/src/server.c 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/src/server.c 2026-09-30 17:24:40.000000000 +0200 @@ -3546,9 +3546,15 @@ goto out; } - if (srv->proxy->mode == PR_MODE_SYSLOG) { + if (srv->proxy->mode == PR_MODE_SYSLOG && !(srv->proxy->cap & PR_CAP_INT)) { /* log backend server (belongs to proxy with mode log enabled): - * perform some compatibility checks + * perform some compatibility checks. Internal proxies (PR_CAP_INT, + * e.g. a sink's forward_px backing a "ring" section) are excluded: + * they are tagged PR_MODE_SYSLOG for type-awareness purposes only + * (see 23e5f18b8) and are not real user-configured log backends, + * so they may legitimately target a UNIX socket (e.g. to feed a + * local "log-forward" section). They still go through the regular + * STREAM-transport check below, in the "else" branch. */ /* supported address family types are: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-3.4.5+git0.c28c410a7/src/sink.c new/haproxy-3.4.6+git1.3ce8f6169/src/sink.c --- old/haproxy-3.4.5+git0.c28c410a7/src/sink.c 2026-09-24 15:57:40.000000000 +0200 +++ new/haproxy-3.4.6+git1.3ce8f6169/src/sink.c 2026-09-30 17:24:40.000000000 +0200 @@ -612,6 +612,7 @@ HA_SPIN_LOCK(SFT_LOCK, &sft->lock); BUG_ON(sft->appctx != appctx); __sink_forward_session_deinit(sft); + sft->last_close = now_ms; HA_SPIN_UNLOCK(SFT_LOCK, &sft->lock); } @@ -686,7 +687,6 @@ goto out_close; appctx->svcctx = (void *)sft; appctx_wakeup(appctx); - sft->last_conn = now_ms; return appctx; /* Error unrolling */ @@ -714,11 +714,19 @@ * * We enforce a tempo to ensure we don't perform more than 1 session * establishment attempt per second. + * + * Note: last_close will wrap after 24.85 days, which may happen if + * the server remains unavailable for that long, in that case it will + * cause the tick_is_expired() test to always fail since tempo will be + * in the future. This means the applet will stop trying to reconnect + * to the server. We accept this pitfall because a server that was + * unavailable for that long can be considered as definitely dead, but + * this is something to keep in mind. */ if (!sft->appctx) { - int tempo = tick_add(sft->last_conn, MS_TO_TICKS(1000)); + int tempo = tick_add(sft->last_close, MS_TO_TICKS(1000)); - if (sft->last_conn == TICK_ETERNITY || tick_is_expired(tempo, now_ms)) + if (sft->last_close == TICK_ETERNITY || tick_is_expired(tempo, now_ms)) sft->appctx = sink_forward_session_create(sink, sft); else if (task->expire == TICK_ETERNITY) task->expire = tempo; ++++++ haproxy-opentelemetry-2.3.0+git0.4d52326.tar.gz -> haproxy-opentelemetry-2.3.0+git2.b031728.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/README new/haproxy-opentelemetry-2.3.0+git2.b031728/README --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/README 2026-09-09 00:38:11.000000000 +0200 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/README 2026-09-24 18:50:06.000000000 +0200 @@ -1,7 +1,7 @@ ----------------------------------------- The HAProxy OpenTelemetry filter (OTel) Version 2.3.0 - ( Last update: 2026-09-14 ) + ( Last update: 2026-09-16 ) ----------------------------------------- Author : Miroslav Zagorac Contact : mzagorac at haproxy dot com @@ -1973,6 +1973,7 @@ % ./run-updown.sh % ./run-err.sh % ./run-empty.sh + % ./run-optctx.sh The parser test runs the same way but starts no proxy. It checks a series of generated configuration files with 'haproxy -c' against the keyword rules of diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/README-configuration new/haproxy-opentelemetry-2.3.0+git2.b031728/README-configuration --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/README-configuration 2026-09-09 00:38:11.000000000 +0200 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/README-configuration 2026-09-24 18:50:06.000000000 +0200 @@ -1,7 +1,7 @@ ----------------------------------------- HAProxy OTel filter configuration guide Version 2.3.0 - ( Last update: 2026-09-14 ) + ( Last update: 2026-09-16 ) ----------------------------------------- Author : Miroslav Zagorac Contact : mzagorac at haproxy dot com @@ -1829,3 +1829,38 @@ This is useful for testing the OTel filter initialization behavior without any actual telemetry processing. + + +6.9. Optional upstream context example (optctx) +------------------------------------------------- + +Traces a request whether or not it arrives with an upstream context. An ACL +over the 'otel.context' fetch picks between two root spans, one that continues +the trace the request brings and one that opens a trace of its own. A 'span' +line takes no condition, so the two stand in scopes of their own, bound to the +same event, and the extraction goes into a third one placed before them, because +the fetch answers only once it has run: + +--- test/optctx/otel.cfg (excerpt) ------------------------------------- + + otel-instrumentation otel-test-instrumentation + acl otel-ctx-valid otel.context(-otel-ctx) + + otel-scope http_request_extract + extract "-otel-ctx" use-headers + otel-event on-frontend-http-request + + otel-scope http_request_child + span "HAProxy session" parent "-otel-ctx" root + otel-event on-frontend-http-request if otel-ctx-valid + + otel-scope http_request_new + span "HAProxy session" root + otel-event on-frontend-http-request unless otel-ctx-valid + +--------------------------------------------------------------------- + +The fetch reports the validity of the context rather than its presence, so a +malformed 'traceparent' takes the second branch as well. The 'require-context' +option has to stay off, since it suppresses the telemetry of every stream that +brings no context. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/README-implementation new/haproxy-opentelemetry-2.3.0+git2.b031728/README-implementation --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/README-implementation 2026-09-09 00:38:11.000000000 +0200 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/README-implementation 2026-09-24 18:50:06.000000000 +0200 @@ -67,6 +67,7 @@ |-- run-updown.sh Up-down counter test runner (symlink) |-- run-err.sh Error-logging test runner (symlink) |-- run-empty.sh Empty configuration test runner (symlink) + |-- run-optctx.sh Optional upstream context test runner (symlink) |-- run-parser.sh Configuration parser test driver (real file) |-- otlp_http-recorder.py OTLP/HTTP traffic recorder |-- otlp_http-replay.py OTLP/HTTP traffic replayer @@ -84,6 +85,7 @@ |-- updown/ Up-down counter test configs |-- err/ Error-logging test configs |-- empty/ Minimal/empty configuration test + |-- optctx/ Optional upstream context test configs `-- parser/ Parser test cases, tables and generators @@ -1244,8 +1246,9 @@ run-test-config.sh Single-instance runner: derives the configuration directory from the name it is invoked under (run-<dir>.sh). run-sa.sh, run-full.sh, run-cmp.sh, run-ctx.sh, run-tcp.sh, - run-updown.sh, run-err.sh and run-empty.sh are symlinks to it, - each starting one HAProxy instance with its own config. + run-updown.sh, run-err.sh, run-empty.sh and run-optctx.sh are + symlinks to it, each starting one HAProxy instance with its + own config. run-fe-be.sh Launches two HAProxy instances (frontend on port 10080, backend on port 11080) forming a trace propagation chain. Handles graceful shutdown via SIGUSR1. A separate script, not a diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/README.md new/haproxy-opentelemetry-2.3.0+git2.b031728/README.md --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/README.md 2026-09-09 00:38:11.000000000 +0200 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/README.md 2026-09-24 18:50:06.000000000 +0200 @@ -390,6 +390,7 @@ costs - [dummy/README](dummy/README) -- build-only stand-in for the wrapper library - [ChangeLog](ChangeLog) -- release notes +- [SECURITY.md](SECURITY.md) -- security policy and reporting a vulnerability ### License diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/SECURITY.md new/haproxy-opentelemetry-2.3.0+git2.b031728/SECURITY.md --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/SECURITY.md 1970-01-01 01:00:00.000000000 +0100 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/SECURITY.md 2026-09-24 18:50:06.000000000 +0200 @@ -0,0 +1,113 @@ +# Security Policy + +## Supported Versions + +Security fixes go into the next release on the current 2.x line. There are no +long term support branches and no backports to earlier releases, so the fix +for a confirmed issue reaches you by upgrading. + +| **Version** | **Supported** | +|:---------------------|:-------------:| +| Latest 2.x release | Yes | +| Earlier 2.x releases | No | +| 1.x | No | + +The filter runs inside HAProxy 3.2 or newer and builds against the OpenTelemetry +C Wrapper. A problem that turns out to sit in either of those is handled by +that project; see the dependencies section below. + +## Reporting a Vulnerability + +Do not report a suspected vulnerability through a public GitHub issue or a pull +request, and do not put the details in one. + +Report it through GitHub's private vulnerability reporting: + +https://github.com/haproxytech/haproxy-opentelemetry/security/advisories/new + +If that form is not reachable for you, open a plain issue saying only that you +have a security report, with no details in it, and a maintainer will arrange a +private channel. + +Include as much of this as you have: + +- What the issue is and what an attacker gets out of it. +- The affected release or commit, and the HAProxy version it runs in. +- The version of the OpenTelemetry C Wrapper the filter was built against. +- The HAProxy configuration and the OTel configuration section that set it off, + cut down as far as you can get them. +- The request or the response that triggers it, and the steps to reproduce. +- Whether the build was a debug or a release one. +- Any mitigation or fix you already have. + +## What to Expect + +A maintainer confirms the report, asks for whatever is missing to reproduce it, +and then says whether it is accepted, with the reasoning either way. A small +team maintains the project, so answers come in days rather than hours. + +An accepted report gets a severity, a fix, and a release: the next regular one, +or a release of its own when the issue is bad enough to warrant it. The fix +is announced in a GitHub security advisory and in the ChangeLog, and you are +credited in both unless you ask not to be. The project does not pay bounties. + +## Scope + +The filter runs inside the HAProxy process and on the data path. The header +values, the URL, the `traceparent`, `tracestate` and `baggage` headers, and +whatever the sample expressions fetch, all arrive from the client or from the +server, while the HAProxy configuration and the OTel configuration come from +the operator. A crash takes the worker down with it, so anything a remote peer +can set off counts. + +In scope: + +- Memory safety in the filter's code: buffer overflows, out of bounds access, + use after free and double free, and reads of uninitialized memory. +- A crash, a hang or unbounded memory growth that a client or a server can set + off through a request or a response. +- A crafted value that escapes the field the filter writes it into, whether a + header or a variable. +- Memory corruption in the configuration parsing, even though the configuration + itself is written by the operator. +- Data races between HAProxy threads that corrupt the filter's state or carry + data from one stream into another. +- Telemetry sent somewhere other than the configured endpoint. +- Process data that reaches exported telemetry although the configuration never + asked for it. +- A CLI command whose arguments corrupt memory, although CLI access is already + privileged. + +Out of scope: + +- Telemetry that is dropped or never arrives: delivery is best effort by design, + as [README.md](README.md) says. +- What the configuration asks for: an attribute that copies a header or a cookie + into a span, an exporter aimed at the wrong collector, or a sampling rate that + leaves events out, are deployment decisions. +- Findings in HAProxy itself. Those belong to the HAProxy project and go to + https://github.com/haproxy/haproxy/security. +- Findings in the OpenTelemetry C Wrapper, or in the OpenTelemetry C++ SDK it + carries. See below. +- Behaviour that appears only in a debug build. Report that as an ordinary + issue. +- The test configurations and scripts under `test/`, and the stand-in library + under `dummy/`, which are there for development. + +## Dependencies + +The filter is built against the OpenTelemetry C Wrapper library, which carries +the OpenTelemetry C++ SDK and everything that SDK links with it, and it runs +inside HAProxy 3.2 or newer. + +Report a flaw in the wrapper through the security policy of its repository at +https://github.com/haproxytech/opentelemetry-c-wrapper, and a flaw in HAProxy +to the HAProxy project. Report it here as well when the way the filter uses +either of them is what makes the flaw reachable. + +## Coordinated Disclosure + +Give the maintainers time to investigate and ship a fix before publishing the +details. A confirmed issue is normally fixed in a release within 30 days, and +the advisory goes out with that release, so the wait is usually shorter. Ninety +days from the report is the outside limit. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/README-optctx new/haproxy-opentelemetry-2.3.0+git2.b031728/test/README-optctx --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/README-optctx 1970-01-01 01:00:00.000000000 +0100 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/test/README-optctx 2026-09-24 18:50:06.000000000 +0200 @@ -0,0 +1,113 @@ +Optional upstream context test configuration (optctx/) +====================================================== + +The 'optctx' test traces a request whether or not it arrives with a W3C trace +context. A request that carries a valid 'traceparent' continues the trace it +names, a request that carries none, or one the propagator cannot read, opens a +trace of its own. This is the shape an HAProxy at the edge needs, where part of +the traffic comes from instrumented senders and the rest from browsers, health +checkers and hand-written clients. + +A 'span' line takes no 'if'/'unless' condition, so the two root spans stand in +two scopes bound to one event, and an ACL over the 'otel.context' fetch decides +which of them runs. The fetch answers only once the extraction has run, which +is why the 'extract' keeps a scope of its own, placed first; scopes run in the +order their sections appear in the file. + +This is the opposite of 'option require-context', which suppresses the telemetry +of a stream that brings no context instead of tracing it on its own. + + +Files +----- + + optctx/otel.cfg OTel filter configuration (two conditional root spans) + optctx/haproxy.cfg HAProxy frontend/backend configuration + optctx/otel.yml Exporter, processor, reader and provider definitions + + +Events +------ + + on-frontend-http-request read by four scopes: the extraction, the two root + span branches and the request span they share + on-http-response adds the response span + on-server-session-end closes the spans and the extracted context + + The closing waits for the on-server-session-end event, because a 'finish *' on + the response event would drop the context before the HTTP response rules of + the frontend, the ones that read it through 'otel.context', have run. + + +The decision +------------ + + The context name is written with a leading '-', which keeps the name out of + the headers and reads the bare 'traceparent' and 'tracestate' that an outside + sender emits: + + acl otel-ctx-valid otel.context(-otel-ctx) + + otel-scope http_request_extract + extract "-otel-ctx" use-headers + otel-event on-frontend-http-request + + otel-scope http_request_child + span "HAProxy session" parent "-otel-ctx" root + otel-event on-frontend-http-request if otel-ctx-valid + + otel-scope http_request_new + span "HAProxy session" root + otel-event on-frontend-http-request unless otel-ctx-valid + + The fetch reports the validity of the extracted context, not its presence, + so a malformed 'traceparent' takes the second branch as well. A '-m found' + match would not separate those two cases, since with a name that carries no + prefix the extraction registers a context whatever the request holds, and the + match then reads as true even for a request without any trace headers. + + Each branch marks its span with a 'haproxy.trace_continued' attribute, and + the frontend repeats the decision in an 'X-Otel-Trace-Continued' response + header, so a plain curl shows which one ran. + + +Sampling +-------- + + The SDK sampler is 'parent_based' with an 'always_on' delegate, which is what + this configuration needs, since a continued trace then follows the decision + its sender made and a trace opened here is sampled. + + Keep the 'rate-limit' at 100.0 and sample in the SDK instead when the sender + is another HAProxy running this filter. A rate limit below 100.0 skips the + filter for part of the streams, so the sender injects no context for them + and this configuration answers every one of those with a trace of its own. + A sampler drops the span instead of the filter, the context still travels + with the sampled flag cleared, and the two hosts stay on one trace. + + +Running the test +---------------- + +The run-optctx.sh script (a symlink to run-test-config.sh) runs this +configuration. It invokes haproxy with -f haproxy-common.cfg -f +optctx/haproxy.cfg, the same as the manual command shown below. From the +test/ directory: + + % ./run-optctx.sh + +Or run it manually: + + % /path/to/haproxy -f haproxy-common.cfg -f optctx/haproxy.cfg + +Then drive one request of each kind and read the response header that reports +the branch: + + % curl -sSD- -o /dev/null -H \ + 'traceparent: 00-11111111111111111111111111111111-2222222222222222-01' \ + http://127.0.0.1:10080/ + % curl -sSD- -o /dev/null http://127.0.0.1:10080/ + +The first request produces a "HAProxy session" span in trace 1111...1111 with +2222222222222222 as its parent, the second one a span in a trace of its own +with an empty parent. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/README.md new/haproxy-opentelemetry-2.3.0+git2.b031728/test/README.md --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/README.md 2026-09-09 00:38:11.000000000 +0200 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/test/README.md 2026-09-24 18:50:06.000000000 +0200 @@ -46,6 +46,7 @@ | `full` | Every filter event except `on-http-tarpit-request`, with all three signal types on every scope | [README-full](README-full) | | `tcp` | A `mode tcp` proxy: one session span per connection and the forwarded payload counted | [README-tcp](README-tcp) | | `updown` | Metrics only, an up-down counter recording a signed per-session delta | [README-updown](README-updown) | +| `optctx` | A root span picked from the request: the session span continues the trace the client sent, or opens one of its own when no usable context arrived | [README-optctx](README-optctx) | | `empty` | The filter loaded with an instrumentation section and no scopes, to prove it starts and stops cleanly | [README-empty](README-empty) | | `err` | The runtime error path: hard-error episodes, swallowed soft errors and the counters that tally them | [README-err](README-err) | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/optctx/haproxy.cfg new/haproxy-opentelemetry-2.3.0+git2.b031728/test/optctx/haproxy.cfg --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/optctx/haproxy.cfg 1970-01-01 01:00:00.000000000 +0100 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/test/optctx/haproxy.cfg 2026-09-24 18:50:06.000000000 +0200 @@ -0,0 +1,25 @@ +global + stats socket /tmp/haproxy.sock mode 666 level admin + +listen stats + mode http + bind *:8001 + stats uri / + stats admin if TRUE + stats refresh 10s + +frontend otel-test-optctx-frontend + bind *:10080 + default_backend servers-backend + + # OTel filter + filter opentelemetry id otel-test-optctx config optctx/otel.cfg + + # Report which of the two root-span scopes ran, so that a plain curl + # shows the decision the filter made. + acl otel-ctx-valid otel.context(-otel-ctx) + http-response set-header X-Otel-Trace-Continued 1 if otel-ctx-valid + http-response set-header X-Otel-Trace-Continued 0 unless otel-ctx-valid + +backend servers-backend + server server-1 127.0.0.1:8000 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/optctx/otel.cfg new/haproxy-opentelemetry-2.3.0+git2.b031728/test/optctx/otel.cfg --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/optctx/otel.cfg 1970-01-01 01:00:00.000000000 +0100 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/test/optctx/otel.cfg 2026-09-24 18:50:06.000000000 +0200 @@ -0,0 +1,58 @@ +[otel-test-optctx] + otel-instrumentation otel-test-instrumentation + config optctx/otel.yml +# log localhost:514 local7 debug + option dontlog-normal + option hard-errors + no option disabled + rate-limit 100.0 + + # Reports whether a valid upstream context arrived with the request. + # The fetch answers only once the extraction has run, so the scope + # that extracts is listed, and runs, before the two branches. + acl otel-ctx-valid otel.context(-otel-ctx) + + scopes http_request_extract + scopes http_request_child + scopes http_request_new + scopes http_request + scopes http_response + scopes session_end + + # The '-' prefix reads the bare 'traceparent' and 'tracestate' headers an + # instrumented client sends. A request without them is not an error, the + # extraction is simply skipped. + otel-scope http_request_extract + extract "-otel-ctx" use-headers + otel-event on-frontend-http-request + + # A valid context arrived: the session span continues that trace. + otel-scope http_request_child + span "HAProxy session" parent "-otel-ctx" root + attribute "haproxy.trace_continued" int(1) + otel-event on-frontend-http-request if otel-ctx-valid + + # Nothing usable arrived: the session span starts a trace of its own. + otel-scope http_request_new + span "HAProxy session" root + attribute "haproxy.trace_continued" int(0) + otel-event on-frontend-http-request unless otel-ctx-valid + + # From here on both cases are the same. + otel-scope http_request + span "HTTP request" parent "HAProxy session" + attribute "http.method" method + attribute "http.url" url + otel-event on-frontend-http-request + + otel-scope http_response + span "HTTP response" parent "HAProxy session" + attribute "http.status_code" status + otel-event on-http-response + + # The spans and the extracted context close here, after the HTTP rules of + # the frontend have run: a 'finish *' on on-http-response would drop the + # context before the response rules can read it with 'otel.context'. + otel-scope session_end + finish * + otel-event on-server-session-end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/optctx/otel.yml new/haproxy-opentelemetry-2.3.0+git2.b031728/test/optctx/otel.yml --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/optctx/otel.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/test/optctx/otel.yml 2026-09-24 18:50:06.000000000 +0200 @@ -0,0 +1,249 @@ +exporters: + exporter_traces_otlp_file: + type: otlp_file + thread_name: "OTLP/file trace" + file_pattern: "__optctx_traces_log-%F-%N" + alias_pattern: "__traces_log-latest" + flush_interval: 30000000 + flush_count: 256 + file_size: 134217728 + rotate_size: 5 + + exporter_traces_otlp_grpc: + type: otlp_grpc + thread_name: "OTLP/gRPC trace" + endpoint: "http://localhost:4317/v1/traces" + use_ssl_credentials: false +# ssl_credentials_cacert_path: "" +# ssl_credentials_cacert_as_string: "" +# ssl_client_key_path: "" +# ssl_client_key_string: "" +# ssl_client_cert_path: "" +# ssl_client_cert_string: "" +# timeout: 10 +# user_agent: "" +# max_threads: 0 +# compression: "" +# max_concurrent_requests: 0 + + exporter_traces_otlp_http: + type: otlp_http + thread_name: "OTLP/HTTP trace" + endpoint: "http://localhost:4318/v1/traces" + content_type: json + json_bytes_mapping: hexid + debug: false + timeout: 10 + http_headers: + - X-OTel-Header-1: "OTLP HTTP traces test header #1" + - X-OTel-Header-2: "OTLP HTTP traces test header #2" + max_concurrent_requests: 64 + max_requests_per_connection: 8 + ssl_insecure_skip_verify: true +# ssl_ca_cert_path: "" +# ssl_ca_cert_string: "" +# ssl_client_key_path: "" +# ssl_client_key_string: "" +# ssl_client_cert_path: "" +# ssl_client_cert_string: "" +# ssl_min_tls: "" +# ssl_max_tls: "" +# ssl_cipher: "" +# ssl_cipher_suite: "" +# compression: "" + + exporter_traces_dev_null: + type: ostream + filename: /dev/null + + exporter_traces_ostream: + type: ostream + filename: __optctx_traces + + exporter_traces_memory: + type: memory + buffer_size: 256 + + exporter_traces_zipkin: + type: zipkin + endpoint: "http://localhost:9411/api/v2/spans" + format: json + service_name: "zipkin-service" +# ipv4: "" +# ipv6: "" + + exporter_metrics_otlp_file: + type: otlp_file + thread_name: "OTLP/file metr" + file_pattern: "__optctx_metrics_log-%F-%N" + alias_pattern: "__metrics_log-latest" + flush_interval: 30000000 + flush_count: 256 + file_size: 134217728 + rotate_size: 5 + + exporter_metrics_otlp_grpc: + type: otlp_grpc + thread_name: "OTLP/gRPC metr" + endpoint: "http://localhost:4317/v1/metrics" + use_ssl_credentials: false + + exporter_metrics_otlp_http: + type: otlp_http + thread_name: "OTLP/HTTP metr" + endpoint: "http://localhost:4318/v1/metrics" + content_type: json + debug: false + timeout: 10 + http_headers: + - X-OTel-Header-1: "OTLP HTTP metrics test header #1" + - X-OTel-Header-2: "OTLP HTTP metrics test header #2" + max_concurrent_requests: 64 + max_requests_per_connection: 8 + ssl_insecure_skip_verify: true + + exporter_metrics_dev_null: + type: ostream + filename: /dev/null + + exporter_metrics_ostream: + type: ostream + filename: __optctx_metrics + + exporter_metrics_memory: + type: memory + buffer_size: 256 + + exporter_logs_otlp_file: + type: otlp_file + thread_name: "OTLP/file logs" + file_pattern: "__optctx_logs_log-%F-%N" + alias_pattern: "__logs_log-latest" + flush_interval: 30000000 + flush_count: 256 + file_size: 134217728 + rotate_size: 5 + + exporter_logs_otlp_grpc: + type: otlp_grpc + thread_name: "OTLP/gRPC logs" + endpoint: "http://localhost:4317/v1/logs" + use_ssl_credentials: false + + exporter_logs_otlp_http: + type: otlp_http + thread_name: "OTLP/HTTP logs" + endpoint: "http://localhost:4318/v1/logs" + content_type: json + debug: false + timeout: 10 + http_headers: + - X-OTel-Header-1: "OTLP HTTP logs test header #1" + - X-OTel-Header-2: "OTLP HTTP logs test header #2" + max_concurrent_requests: 64 + max_requests_per_connection: 8 + ssl_insecure_skip_verify: true + + exporter_logs_dev_null: + type: ostream + filename: /dev/null + + exporter_logs_ostream: + type: ostream + filename: __optctx_logs + + exporter_logs_elasticsearch: + type: elasticsearch + host: localhost + port: 9200 + index: logs + response_timeout: 30 + debug: false + http_headers: + - X-OTel-Header-1: "Elasticsearch logs test header #1" + - X-OTel-Header-2: "Elasticsearch logs test header #2" + +readers: + reader_metrics: + thread_name: "reader metr" + export_interval: 10000 + export_timeout: 5000 + +samplers: + sampler_traces: +# type: always_on +# type: always_off +# type: trace_id_ratio_based +# ratio: 1.0 + type: parent_based + delegate: always_on + +processors: + processor_traces_batch: + type: batch + thread_name: "proc/batch trac" + # Note: when the queue is half full, a preemptive notification is sent + # to start the export call. + max_queue_size: 2048 + # Time interval (in ms) between two consecutive exports + schedule_delay: 5000 + # Export 'max_export_batch_size' after every `schedule_delay' milliseconds. + max_export_batch_size: 512 + + processor_traces_single: + type: single + + processor_logs_batch: + type: batch + thread_name: "proc/batch logs" + max_queue_size: 2048 + schedule_delay: 5000 + max_export_batch_size: 512 + + processor_logs_single: + type: single + +providers: + provider_traces: + resources: + - service.version: "1.0.0" + - service.instance.id: "id-optctx" + - service.name: "optctx" + - service.namespace: "HAProxy traces test" + + provider_metrics: + resources: + - service.version: "1.0.0" + - service.instance.id: "id-optctx" + - service.name: "optctx" + - service.namespace: "HAProxy metrics test" + + provider_logs: + resources: + - service.version: "1.0.0" + - service.instance.id: "id-optctx" + - service.name: "optctx" + - service.namespace: "HAProxy logs test" + +signals: + traces: + default: + scope_name: "HAProxy OTEL - traces" + exporters: exporter_traces_otlp_http + samplers: sampler_traces + processors: processor_traces_batch + providers: provider_traces + + metrics: + default: + scope_name: "HAProxy OTEL - metrics" + exporters: exporter_metrics_otlp_http + readers: reader_metrics + providers: provider_metrics + + logs: + default: + scope_name: "HAProxy OTEL - logs" + exporters: exporter_logs_otlp_http + processors: processor_logs_batch + providers: provider_logs diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/run-optctx.sh new/haproxy-opentelemetry-2.3.0+git2.b031728/test/run-optctx.sh --- old/haproxy-opentelemetry-2.3.0+git0.4d52326/test/run-optctx.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/haproxy-opentelemetry-2.3.0+git2.b031728/test/run-optctx.sh 2026-10-09 21:43:34.232746274 +0200 @@ -0,0 +1 @@ +symbolic link to run-test-config.sh
