From: Willy Tarreau Date: Sat, 27 Dec 2025 17:56:00 +0000 (+0100) Subject: MINOR: net_helper: add sample converters to decode TCP headers X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6e46d1345b0a8c90e16735682821ba6b8debbc8e;p=thirdparty%2Fhaproxy.git MINOR: net_helper: add sample converters to decode TCP headers This adds the following converters, used to decode fields in an incoming tcp header: tcp.dst, tcp.flags, tcp.seq, tcp.src, tcp.win, tcp.options.mss, tcp.options.tsopt, tcp.options.tsval, tcp.options.wscale, tcp.options_list, These can be used with the tcp-ss bind option. The doc was updated accordingly. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index dfc4a929a..3c6d8fd67 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -20583,6 +20583,18 @@ table_server_id([table]) any integer table_sess_cnt([table]) any integer table_sess_rate([table]) any integer table_trackers([table]) any integer +tcp.dst binary integer +tcp.flags binary integer +tcp.options.mss binary integer +tcp.options.sack binary integer +tcp.options.tsopt binary integer +tcp.options.tsval binary integer +tcp.options.wscale binary integer +tcp.options.wsopt binary integer +tcp.options_list binary binary +tcp.seq binary integer +tcp.src binary integer +tcp.win binary integer ub64dec string string ub64enc string string ungrpc(field_number[,field_type]) binary binary / int @@ -22359,6 +22371,88 @@ table_trackers([]) concurrent connections there are from a given address for example. See also the sc_trackers sample fetch keyword. +tcp.dst + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It returns an integer representing the destination + port present in the TCP header. See also "fc_saved_syn", "tcp-ss", and + "ip.data". + +tcp.flags + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It returns an integer representing the TCP flags + from this TCP header. All 8 flags from FIN to CWR are retrieved. Each flag + may be tested using the "and()" converter. Please refer to RFC9293 for the + value of each flag. See also "fc_saved_syn", "tcp-ss", and "ip.data". + +tcp.options.mss + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It looks for a TCP option of kind "MSS", and if found, + it returns an integer value corresponding to the advertised value in that + option, otherwise zero. The MSS is the Maximum Segment Size and indicates the + largest segment the peer may receive, in bytes. See also "fc_saved_syn", + "tcp-ss", and "ip.data". + +tcp.options.sack + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It looks for a TCP option of kind "Sack-Permitted", + and if found, returns 1, otherwise zero. See also "fc_saved_syn", "tcp-ss", + and "ip.data". + +tcp.options.tsopt + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It looks for a TCP option of kind "Timestamp", and if + found, returns 1, otherwise zero. See also "fc_saved_syn", "tcp-ss", and + "ip.data". + +tcp.options.tsval + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It looks for a TCP option of kind "Timestamp", and if + found, returns the timestamp value emitted by the peer, otherwise does not + return anything. Note that timestamps are 32-bit unsigned values with no + particular unit that only the peer decides on, and timestamps are expected to + be independent between different connections. See also "fc_saved_syn", + "tcp-ss", and "ip.data". + +tcp.options.wscale + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It looks for a TCP option of kind "Window Scale", and + if found, returns the window scaling value emitted by the peer, otherwise + zero. Note that values are not expected to be beyond 14 though no technical + limitation prevents them from being sent. In order to detect if the window + scale option was used, please use "tcp.options.wsopt". See also "tcp-ss", + "fc_saved_syn", "ip.data", and "tcp.options.wsopt". + +tcp.options.wsopt + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It looks for a TCP option of kind "Window Scale", and + if found, returns 1 otherwise 0. See also "fc_saved_syn", "tcp-ss", "ip.data" + "tcp.options.wscale". + +tcp.options_list + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It builds a binary sequence of all TCP option kinds in + the same order as they appear in the TCP header. It can produce from 0 to 60 + bytes (in the worst case). The End-of-options is not emitted. See also + "fc_saved_syn", "tcp-ss", and "ip.data". + +tcp.seq + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It returns an integer representing the sequence number + used by the peer in the TCP header. Sequence numbers are 32-bit unsigned + values. See also "fc_saved_syn", "tcp-ss", and "ip.data". + +tcp.src + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It returns an integer representing the source port + present in the TCP header. See also "fc_saved_syn", "tcp-ss", and "ip.data". + +tcp.win + This is used with an input sample representing a binary TCP header, as + returned by "ip.data". It returns an integer representing the window size + advertised by the peer in the TCP header. The value is provided as-is, as a + 16-bit unsigned quantity, without applying the window scaling factor. See + also "fc_saved_syn", "tcp-ss", and "ip.data". + ub64dec This converter is the base64url variant of b64dec converter. base64url encoding is the "URL and Filename Safe Alphabet" variant of base64 encoding. @@ -24069,10 +24163,10 @@ fc_saved_syn : binary ipv4h=%[var(sess.syn),eth.data,bytes(0,12),hex] \ ipv4_src=%[var(sess.syn),eth.data,ip.src] \ ipv4_dst=%[var(sess.syn),eth.data,ip.dst] \ - tcp_spt=%[var(sess.syn),bytes(34,2),be2dec(,2)] \ - tcp_dpt=%[var(sess.syn),bytes(36,2),be2dec(,2)] \ - tcp_win=%[var(sess.syn),bytes(48,2),be2dec(,2)] \ - tcp_opt=%[var(sess.syn),bytes(54),hex]\n" + tcp_spt=%[var(sess.syn),eth.data,ip.data,tcp.src] \ + tcp_dpt=%[var(sess.syn),eth.data,ip.data,tcp.dst] \ + tcp_win=%[var(sess.syn),eth.data,ip.data,tcp.win] \ + tcp_opt=%[var(sess.syn),eth.data,ip.data,bytes(20),hex]\n" $ curl '0:4445' mac_dst=000000000000 mac_src=000000000000 proto=0800 \ @@ -24080,8 +24174,8 @@ fc_saved_syn : binary tcp_spt=43970 tcp_dpt=4445 tcp_win=65495 \ tcp_opt=0204FFD70402080A01DC0D410000000001030307 - See also the "set-var" action, the "be2dec", "bytes", "hex", "eth.XXX" and - "ip.XXX" converters. + See also the "set-var" action, the "be2dec", "bytes", "hex", "eth.XXX", + "ip.XXX", and "tcp.XXX" converters. fc_settings_streams_limit : integer Returns the maximum number of streams allowed on the frontend connection. For diff --git a/src/net_helper.c b/src/net_helper.c index 5f22287df..415f7eebd 100644 --- a/src/net_helper.c +++ b/src/net_helper.c @@ -415,6 +415,240 @@ static int sample_conv_ip_ver(const struct arg *arg_p, struct sample *smp, void return 1; } +/******************************************/ +/* Converters used to process TCP headers */ +/******************************************/ + +/* returns the TCP header length in bytes if complete, otherwise zero */ +static int tcp_fullhdr_length(const struct sample *smp) +{ + size_t ofs; + + if (smp->data.u.str.data < 20) + return 0; + + /* check that header is complete */ + ofs = ((uchar)smp->data.u.str.area[12] >> 4) * 4; + if (ofs < 20 || smp->data.u.str.data < ofs) + return 0; + return ofs; +} + +/* returns the offset in the input TCP header where option kind is first + * seen, otherwise 0 if not found. NOP and END cannot be searched. + */ +static size_t tcp_fullhdr_find_opt(const struct sample *smp, uint8_t opt) +{ + size_t len = tcp_fullhdr_length(smp); + size_t next = 20, curr; + + while ((curr = next) < len) { + if (smp->data.u.str.area[next] == 0) // kind0=end of options + break; + /* kind1 = NOP and is a single byte, others have a length field */ + next += (smp->data.u.str.area[next] == 1) ? 1 : smp->data.u.str.area[next + 1]; + if (smp->data.u.str.area[curr] == opt && next <= len) + return curr; + } + + return 0; +} + +/* returns the destination port field found in an input TCP header */ +static int sample_conv_tcp_dst(const struct arg *arg_p, struct sample *smp, void *private) +{ + if (smp->data.u.str.data < 20) + return 0; + + smp->data.u.sint = read_n16(smp->data.u.str.area + 2); + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the flags field found in an input TCP header */ +static int sample_conv_tcp_flags(const struct arg *arg_p, struct sample *smp, void *private) +{ + if (smp->data.u.str.data < 20) + return 0; + + smp->data.u.sint = (uchar)smp->data.u.str.area[13]; + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the MSS value of an input TCP header, or 0 if absent. Returns + * nothing if the header is incomplete. + */ +static int sample_conv_tcp_options_mss(const struct arg *arg_p, struct sample *smp, void *private) +{ + size_t len = tcp_fullhdr_length(smp); + size_t ofs = tcp_fullhdr_find_opt(smp, 2 /* MSS */); + + if (!len) + return 0; + + smp->data.u.sint = ofs ? read_n16(smp->data.u.str.area + ofs + 2) : 0; + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns 1 if the SackPerm option is present in an input TCP header, + * otherwise 0. Returns nothing if the header is incomplete. + */ +static int sample_conv_tcp_options_sack(const struct arg *arg_p, struct sample *smp, void *private) +{ + size_t len = tcp_fullhdr_length(smp); + size_t ofs = tcp_fullhdr_find_opt(smp, 4 /* sackperm */); + + if (!len) + return 0; + + smp->data.u.sint = !!ofs; + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns 1 if the TimeStamp option is present in an input TCP header, + * otherwise 0. Returns nothing if the header is incomplete. + */ +static int sample_conv_tcp_options_tsopt(const struct arg *arg_p, struct sample *smp, void *private) +{ + size_t len = tcp_fullhdr_length(smp); + size_t ofs = tcp_fullhdr_find_opt(smp, 8 /* TS */); + + if (!len) + return 0; + + smp->data.u.sint = !!ofs; + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the TSval value in the TimeStamp option found in an input TCP + * header, if found, otherwise 0. Returns nothing if the header is incomplete + * (see also tsopt). + */ +static int sample_conv_tcp_options_tsval(const struct arg *arg_p, struct sample *smp, void *private) +{ + size_t len = tcp_fullhdr_length(smp); + size_t ofs = tcp_fullhdr_find_opt(smp, 8 /* TS */); + + if (!len) + return 0; + + smp->data.u.sint = ofs ? read_n32(smp->data.u.str.area + ofs + 2) : 0; + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the window scaling shift count from an input TCP header, otherwise 0 + * if option not found (see also wsopt). Returns nothing if the header is + * incomplete. + */ +static int sample_conv_tcp_options_wscale(const struct arg *arg_p, struct sample *smp, void *private) +{ + size_t len = tcp_fullhdr_length(smp); + size_t ofs = tcp_fullhdr_find_opt(smp, 3 /* wscale */); + + if (!len) + return 0; + + smp->data.u.sint = ofs ? (uchar)smp->data.u.str.area[ofs + 2] : 0; + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns 1 if the WScale option is present in an input TCP header, + * otherwise 0. Returns nothing if the header is incomplete. + */ +static int sample_conv_tcp_options_wsopt(const struct arg *arg_p, struct sample *smp, void *private) +{ + size_t len = tcp_fullhdr_length(smp); + size_t ofs = tcp_fullhdr_find_opt(smp, 3 /* wscale */); + + if (!len) + return 0; + + smp->data.u.sint = !!ofs; + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns only the TCP options kinds of an input TCP header, as a binary + * block of one byte per option. + */ +static int sample_conv_tcp_options_list(const struct arg *arg_p, struct sample *smp, void *private) +{ + struct buffer *trash = get_trash_chunk(); + size_t len = tcp_fullhdr_length(smp); + size_t ofs = 20; + + if (!len) + return 0; + + while (ofs < len) { + if (smp->data.u.str.area[ofs] == 0) // kind0=end of options + break; + trash->area[trash->data++] = smp->data.u.str.area[ofs]; + /* kind1 = NOP and is a single byte, others have a length field */ + if (smp->data.u.str.area[ofs] == 1) + ofs++; + else if (ofs + 1 <= len) + ofs += smp->data.u.str.area[ofs + 1]; + else + break; + } + + /* returns a binary block of 1 byte per option */ + smp->data.u.str = *trash; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the sequence number field found in an input TCP header */ +static int sample_conv_tcp_seq(const struct arg *arg_p, struct sample *smp, void *private) +{ + if (smp->data.u.str.data < 20) + return 0; + + smp->data.u.sint = read_n32(smp->data.u.str.area + 4); + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the source port field found in an input TCP header */ +static int sample_conv_tcp_src(const struct arg *arg_p, struct sample *smp, void *private) +{ + if (smp->data.u.str.data < 20) + return 0; + + smp->data.u.sint = read_n16(smp->data.u.str.area); + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the window field found in an input TCP header */ +static int sample_conv_tcp_win(const struct arg *arg_p, struct sample *smp, void *private) +{ + if (smp->data.u.str.data < 20) + return 0; + + smp->data.u.sint = read_n16(smp->data.u.str.area + 14); + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + /* Note: must not be declared as its list will be overwritten */ static struct sample_conv_kw_list sample_conv_kws = {ILH, { @@ -435,6 +669,19 @@ static struct sample_conv_kw_list sample_conv_kws = {ILH, { { "ip.ttl", sample_conv_ip_ttl, 0, NULL, SMP_T_BIN, SMP_T_SINT }, { "ip.ver", sample_conv_ip_ver, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.dst", sample_conv_tcp_dst, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.flags", sample_conv_tcp_flags, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.options.mss", sample_conv_tcp_options_mss, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.options.sack", sample_conv_tcp_options_sack, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.options.tsopt", sample_conv_tcp_options_tsopt, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.options.tsval", sample_conv_tcp_options_tsval, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.options.wscale", sample_conv_tcp_options_wscale, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.options.wsopt", sample_conv_tcp_options_wsopt, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.options_list", sample_conv_tcp_options_list, 0, NULL, SMP_T_BIN, SMP_T_BIN }, + { "tcp.seq", sample_conv_tcp_seq, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.src", sample_conv_tcp_src, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "tcp.win", sample_conv_tcp_win, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { NULL, NULL, 0, 0, 0 }, }};