From: Willy Tarreau Date: Sat, 27 Dec 2025 16:46:44 +0000 (+0100) Subject: MINOR: net_helper: add sample converters to decode IP packet headers X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e0a7a7ca43f9493399f7cda87f94809c4ef62431;p=thirdparty%2Fhaproxy.git MINOR: net_helper: add sample converters to decode IP packet headers This adds a few converters that help decode parts of IP packets: - ip.data : returns the next header (typically TCP) - ip.df : returns the dont-fragment flags - ip.dst : returns the destination IPv4/v6 address - ip.hdr : returns only the IP header - ip.proto: returns the upper level protocol (udp/tcp) - ip.src : returns the source IPv4/v6 address - ip.tos : returns the TOS / TC field - ip.ttl : returns the TTL/HL value - ip.ver : returns the IP version (4 or 6) 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 5612d0d20..dfc4a929a 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -20489,6 +20489,15 @@ htonl integer integer http_date([offset[,unit]]) integer string iif(true,false) boolean string in_table([table]) any boolean +ip.data binary binary +ip.df binary integer +ip.dst binary address +ip.hdr binary binary +ip.proto binary integer +ip.src binary address +ip.tos binary integer +ip.ttl binary integer +ip.ver binary integer ipmask(mask4[,mask6]) address address json([input-code]) string string json_query(json_path[,output_type]) string _outtype_ @@ -21066,6 +21075,81 @@ in_table([]) elements (e.g. whether or not a source IP address or an Authorization header was already seen). +ip.data + This is used with an input sample representing a binary Ethernet frame, as + returned by "fc_saved_syn" combined with the "tcp-ss" bind option set to "1", + or with the output of "eth.data". It skips the IP header and any optional + options or extensions, and returns a block of binary data starting at the + transport protocol (usually TCP or UDP). See also "fc_saved_syn", "tcp-ss", + and "eth.data". + +ip.df + This is used with an input sample representing a binary Ethernet frame, as + returned by "fc_saved_syn" combined with the "tcp-ss" bind option set to "1", + or with the output of "eth.data". It returns integer value 1 if the DF (don't + fragment) flag is set in the IP header, 0 otherwise. IPv6 does not have a DF + flag, and doesn't fragment by default so it always returns 1. See also + "fc_saved_syn", "tcp-ss", and "eth.data". + +ip.dst + This is used with an input sample representing a binary Ethernet frame, as + returned by "fc_saved_syn" combined with the "tcp-ss" bind option set to "1", + or with the output of "eth.data". It returns the IPv4 or IPv6 destination + address from the IPv4/v6 header. See also "fc_saved_syn", "tcp-ss", and + "eth.data". + +ip.hdr + This is used with an input sample representing a binary Ethernet frame, as + returned by "fc_saved_syn" combined with the "tcp-ss" bind option set to "1", + or with the output of "eth.data". It returns a block of binary data starting + with the IP header and stopping after the last option or extension, and + before the transport protocol header. See also "fc_saved_syn", "tcp-ss", and + "eth.data". + +ip.proto + This is used with an input sample representing a binary Ethernet frame, as + returned by "fc_saved_syn" combined with the "tcp-ss" bind option set to "1", + or with the output of "eth.data". It returns the transport protocol number, + usually 6 for TCP or 17 for UDP. See also "fc_saved_syn", "tcp-ss", and + "eth.data". + +ip.src + This is used with an input sample representing a binary Ethernet frame, as + returned by "fc_saved_syn" combined with the "tcp-ss" bind option set to "1", + or with the output of "eth.data". It returns the IPv4 or IPv6 source address + from the IPv4/v6 header. See also "fc_saved_syn", "tcp-ss", and "eth.data". + +ip.tos + This is used with an input sample representing a binary Ethernet frame, as + returned by "fc_saved_syn" combined with the "tcp-ss" bind option set to "1", + or with the output of "eth.data". It returns an integer corresponding to the + value of the type-of-service (TOS) field in the IPv4 header or traffic class + (TC) field in the IPv6 header. Note that in the modern internet, this field + most often contains a DSCP (Differentiated Services Codepoint) value in the + 6 upper bits and the two lower are either not used, or used by IP ECN. Please + refer to RFC2474 and RFC8436 for DSCP values, and RFC3168 for IP ECN fields. + See also "fc_saved_syn", "tcp-ss", and "eth.data". + +ip.ttl + This is used with an input sample representing a binary Ethernet frame, as + returned by "fc_saved_syn" combined with the "tcp-ss" bind option set to "1", + or with the output of "eth.data". This returns an integer corresponding to + the TTL (Time To Live) or HL (Hop Limit) field in the IPv4/IPv6 header. This + value is usually preset to a fixed value and decremented by each router that + the packet crosses. It can help infer how far a client connects from when the + initial value is known. Note that most modern operating systems start with an + initial value of 64. See also "fc_saved_syn", "tcp-ss", and "eth.data". + +ip.ver + This is used with an input sample representing a binary Ethernet frame, as + returned by "fc_saved_syn" combined with the "tcp-ss" bind option set to "1", + or with the output of "eth.data". This returns the IP version from the IP + header, normally either 4 or 6. Note that this doesn't check whether the + protocol number in the upper layer Ethernet frame matches, but since this is + expected to be used with valid packets, it is expected that the operating + system has already verified this. See also "fc_saved_syn", "tcp-ss", and + "eth.data". + ipmask([,]) Apply a mask to an IP address, and use the result for lookups and storage. This can be used to make all hosts within a certain mask to share the same @@ -23983,8 +24067,8 @@ fc_saved_syn : binary mac_src=%[var(sess.syn),eth.src,hex] \ proto=%[var(sess.syn),eth.proto,bytes(6),be2hex(,2)] \ ipv4h=%[var(sess.syn),eth.data,bytes(0,12),hex] \ - ipv4_src=%[var(sess.syn),bytes(26,4),be2dec(.,1)] \ - ipv4_dst=%[var(sess.syn),bytes(30,4),be2dec(.,1)] \ + 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)] \ @@ -23996,8 +24080,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", and - "eth.XXX" converters. + See also the "set-var" action, the "be2dec", "bytes", "hex", "eth.XXX" and + "ip.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 fbb1e3255..5f22287df 100644 --- a/src/net_helper.c +++ b/src/net_helper.c @@ -119,6 +119,303 @@ static int sample_conv_eth_vlan(const struct arg *arg_p, struct sample *smp, voi return 0; } +/*******************************************************/ +/* Converters used to process IPv4/IPv6 packet headers */ +/*******************************************************/ + +/* returns the total header length for the input IP packet header (v4 or v6), + * including all extensions if any. It corresponds to the length to skip to + * find the TCP or UDP header. If data are missing or unparsable, it returns + * 0. + */ +static size_t ip_header_length(const struct sample *smp) +{ + size_t len; + uchar next; + uchar ver; + + if (smp->data.u.str.data < 1) + return 0; + + ver = (uchar)smp->data.u.str.area[0] >> 4; + if (ver == 4) { + len = (smp->data.u.str.area[0] & 0xF) * 4; + if (smp->data.u.str.data < len) + return 0; + } + else if (ver == 6) { + if (smp->data.u.str.data < 40) + return 0; + + len = 40; + next = smp->data.u.str.area[6]; + + while (next != 6 && next != 17) { + if (smp->data.u.str.data < len + 2) + return 0; + next = smp->data.u.str.area[len]; + len += (uchar)smp->data.u.str.area[len + 1] * 8 + 8; + } + + if (smp->data.u.str.data < len) + return 0; + } + else { + return 0; + } + + return len; +} + +/* returns the payload following the input IP packet header (v4 or v6) skipping + * all extensions if any. For IPv6, it returns the TCP or UDP next header. + */ +static int sample_conv_ip_data(const struct arg *arg_p, struct sample *smp, void *private) +{ + size_t len; + + len = ip_header_length(smp); + if (!len) + return 0; + + /* advance buffer by */ + smp->data.u.str.area += len; + smp->data.u.str.data -= len; + return 1; +} + +/* returns the DF (don't fragment) flag from an IPv4 header, as 0 or 1. The + * value is always one for IPv6 since DF is implicit. + */ +static int sample_conv_ip_df(const struct arg *arg_p, struct sample *smp, void *private) +{ + uchar ver; + uchar df; + + if (smp->data.u.str.data < 1) + return 0; + + ver = (uchar)smp->data.u.str.area[0] >> 4; + if (ver == 4) { + if (smp->data.u.str.data < 6) + return 0; + df = !!(smp->data.u.str.area[6] & 0x40); + } + else if (ver == 6) { + df = 1; + } + else { + return 0; + } + + smp->data.u.sint = df; + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the IP DST address found in an input IP packet header (v4 or v6). */ +static int sample_conv_ip_dst(const struct arg *arg_p, struct sample *smp, void *private) +{ + uchar ver; + + if (smp->data.u.str.data < 1) + return 0; + + ver = (uchar)smp->data.u.str.area[0] >> 4; + if (ver == 4) { + if (smp->data.u.str.data < 20) + return 0; + + smp->data.u.ipv4.s_addr = read_u32(smp->data.u.str.area + 16); + smp->data.type = SMP_T_IPV4; + } + else if (ver == 6) { + if (smp->data.u.str.data < 40) + return 0; + + memcpy(&smp->data.u.ipv6, smp->data.u.str.area + 24, 16); + smp->data.type = SMP_T_IPV6; + } + else { + return 0; + } + + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the IP header only for an input IP packet header (v4 or v6), including + * all extensions if any. For IPv6, it includes every extension before TCP/UDP. + */ +static int sample_conv_ip_hdr(const struct arg *arg_p, struct sample *smp, void *private) +{ + size_t len; + + len = ip_header_length(smp); + if (!len) + return 0; + + /* truncate buffer to */ + smp->data.u.str.data = len; + return 1; +} + +/* returns the upper layer protocol number (TCP/UDP) for an input IP packet + * header (v4 or v6). + */ +static int sample_conv_ip_proto(const struct arg *arg_p, struct sample *smp, void *private) +{ + size_t len; + uchar next; + uchar ver; + + if (smp->data.u.str.data < 1) + return 0; + + ver = (uchar)smp->data.u.str.area[0] >> 4; + if (ver == 4) { + if (smp->data.u.str.data < 10) + return 0; + next = smp->data.u.str.area[9]; + } + else if (ver == 6) { + /* skip all extensions */ + if (smp->data.u.str.data < 40) + return 0; + + len = 40; + next = smp->data.u.str.area[6]; + + while (next != 6 && next != 17) { + if (smp->data.u.str.data < len + 2) + return 0; + next = smp->data.u.str.area[len]; + len += (uchar)smp->data.u.str.area[len + 1] * 8 + 8; + } + + if (smp->data.u.str.data < len) + return 0; + } + else { + return 0; + } + + /* protocol number is in */ + smp->data.u.sint = next; + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the IP SRC address found in an input IP packet header (v4 or v6). */ +static int sample_conv_ip_src(const struct arg *arg_p, struct sample *smp, void *private) +{ + uchar ver; + + if (smp->data.u.str.data < 1) + return 0; + + ver = (uchar)smp->data.u.str.area[0] >> 4; + if (ver == 4) { + if (smp->data.u.str.data < 20) + return 0; + + smp->data.u.ipv4.s_addr = read_u32(smp->data.u.str.area + 12); + smp->data.type = SMP_T_IPV4; + } + else if (ver == 6) { + if (smp->data.u.str.data < 40) + return 0; + + memcpy(&smp->data.u.ipv6, smp->data.u.str.area + 8, 16); + smp->data.type = SMP_T_IPV6; + } + else { + return 0; + } + + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the IP TOS/TC field found in an input IP packet header (v4 or v6). */ +static int sample_conv_ip_tos(const struct arg *arg_p, struct sample *smp, void *private) +{ + uchar ver; + + if (smp->data.u.str.data < 1) + return 0; + + ver = (uchar)smp->data.u.str.area[0] >> 4; + if (ver == 4) { + /* TOS field is at offset 1 */ + if (smp->data.u.str.data < 2) + return 0; + + smp->data.u.sint = (uchar)smp->data.u.str.area[1]; + } + else if (ver == 6) { + /* TOS field is between offset 0 and 1 */ + if (smp->data.u.str.data < 2) + return 0; + + smp->data.u.sint = (uchar)(read_n16(smp->data.u.str.area) >> 4); + } + else { + return 0; + } + + /* OK we have the value in data.u.sint */ + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the IP TTL/HL field found in an input IP packet header (v4 or v6). */ +static int sample_conv_ip_ttl(const struct arg *arg_p, struct sample *smp, void *private) +{ + uchar ver; + + if (smp->data.u.str.data < 1) + return 0; + + ver = (uchar)smp->data.u.str.area[0] >> 4; + if (ver == 4) { + if (smp->data.u.str.data < 20) + return 0; + + smp->data.u.sint = (uchar)smp->data.u.str.area[8]; + } + else if (ver == 6) { + if (smp->data.u.str.data < 40) + return 0; + + smp->data.u.sint = (uchar)smp->data.u.str.area[7]; + } + else { + return 0; + } + + /* OK we have the value in data.u.sint */ + smp->data.type = SMP_T_SINT; + smp->flags &= ~SMP_F_CONST; + return 1; +} + +/* returns the IP version found in an input IP packet header (v4 or v6). */ +static int sample_conv_ip_ver(const struct arg *arg_p, struct sample *smp, void *private) +{ + if (smp->data.u.str.data < 1) + return 0; + + smp->data.u.sint = (uchar)smp->data.u.str.area[0] >> 4; + 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, { { "eth.data", sample_conv_eth_data, 0, NULL, SMP_T_BIN, SMP_T_BIN }, @@ -128,6 +425,16 @@ static struct sample_conv_kw_list sample_conv_kws = {ILH, { { "eth.src", sample_conv_eth_src, 0, NULL, SMP_T_BIN, SMP_T_BIN }, { "eth.vlan", sample_conv_eth_vlan, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "ip.data", sample_conv_ip_data, 0, NULL, SMP_T_BIN, SMP_T_BIN }, + { "ip.df", sample_conv_ip_df, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "ip.dst", sample_conv_ip_dst, 0, NULL, SMP_T_BIN, SMP_T_ADDR }, + { "ip.hdr", sample_conv_ip_hdr, 0, NULL, SMP_T_BIN, SMP_T_BIN }, + { "ip.proto", sample_conv_ip_proto, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "ip.src", sample_conv_ip_src, 0, NULL, SMP_T_BIN, SMP_T_ADDR }, + { "ip.tos", sample_conv_ip_tos, 0, NULL, SMP_T_BIN, SMP_T_SINT }, + { "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 }, + { NULL, NULL, 0, 0, 0 }, }};