diff --git a/doc/configuration.txt b/doc/configuration.txt index 3c6d8fd67..7f88178fa 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -20492,6 +20492,7 @@ in_table([table]) any boolean ip.data binary binary ip.df binary integer ip.dst binary address +ip.fp binary binary ip.hdr binary binary ip.proto binary integer ip.src binary address @@ -21110,6 +21111,49 @@ ip.dst address from the IPv4/v6 header. See also "fc_saved_syn", "tcp-ss", and "eth.data". +ip.fp([]) + 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 inspects various parts of the IP header + and the TCP header to construct sort of a fingerprint of invariant parts that + can be used to distinguish between multiple apparently identical hosts. The + real-world use case is to refine the identification of misbehaving hosts + between a shared IP address to avoid blocking legitimate users when only one + is misbehaving and needs to be blocked. The converter builds a 7-byte binary + block based on the input. The bytes of the fingerprint are arranged like + this: + - byte 0: IP TOS field (see ip.tos) + - byte 1: + - bit 7: IPv6 (1) / IPv4 (0) + - bit 6: ip.df + - bit 5..4: 0:ip.ttl<=32; 1:ip.ttl<=64; 2:ip.ttl<=128; 3:ip.ttl<=255 + - bit 3: IP options present (1) / absent (0) + - bit 2: TCP data present (1) / absent (0) + - bit 1: TCP.flags has CWR set (1) / cleared (0) + - bit 0: TCP.flags has ECE set (1) / cleared (0) + - byte 2: + - bits 7..4: TCP header length in 4-byte words + - bits 3..0: TCP window scaling + 1 (1..15) / 0 (no WS advertised) + - byte 3..4: tcp.win + - byte 5..6: tcp.options.mss, or zero if absent + + When the argument is not set or is zero, the fingerprint is solely + made of the 7 bytes described above. When the is 1, it starts by the + 7-byte block above, and is followed by the list of TCP option kinds, for 0 + to 40 extra bytes, as returned by "tcp.options_list". + + Example: + + frontend test + mode http + bind :4445 tcp-ss 1 + tcp-request connection set-var(sess.syn) fc_saved_syn + http-request return status 200 content-type text/plain lf-string \ + "src=%[var(sess.syn),ip.src] fp=%[var(sess.syn),ip.fp,hex]\n" + + See also "fc_saved_syn", "tcp-ss", "eth.data", "ip.df", "ip.ttl", "tcp.win", + "tcp.options.mss", and "tcp.options_list". + 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", diff --git a/src/net_helper.c b/src/net_helper.c index 415f7eebd..a3181fbe3 100644 --- a/src/net_helper.c +++ b/src/net_helper.c @@ -649,6 +649,159 @@ static int sample_conv_tcp_win(const struct arg *arg_p, struct sample *smp, void return 1; } +/* Builds a binary fingerprint of the IP+TCP input contents that are supposed + * to rely essentially on the client stack's settings. This can be used for + * example to selectively block bad behaviors at one IP address without + * blocking others. The resulting fingerprint is a binary block of 56 to 376 + * bytes long (56 being the fixed part and the rest depending on the provided + * TCP extensions). + */ +static int sample_conv_ip_fp(const struct arg *arg_p, struct sample *smp, void *private) +{ + struct buffer *trash = get_trash_chunk(); + uchar ipver; + uchar iptos; + uchar ipttl; + uchar ipdf; + uchar ipext; + uchar tcpflags; + uchar tcplen; + uchar tcpws; + ushort pktlen; + ushort tcpwin; + ushort tcpmss; + size_t iplen; + size_t ofs; + int mode; + + /* check arg for mode > 0 */ + if (arg_p[0].type == ARGT_SINT) + mode = arg_p[0].data.sint; + else + mode = 0; + + /* retrieve IP version */ + if (smp->data.u.str.data < 1) + return 0; + + ipver = (uchar)smp->data.u.str.area[0] >> 4; + if (ipver == 4) { + /* check fields for IPv4 */ + + // extension present if header length != 5 words. + ipext = (smp->data.u.str.area[0] & 0xF) != 5; + iplen = (smp->data.u.str.area[0] & 0xF) * 4; + if (smp->data.u.str.data < iplen) + return 0; + + iptos = smp->data.u.str.area[1]; + pktlen = read_n16(smp->data.u.str.area + 2); + ipdf = !!(smp->data.u.str.area[6] & 0x40); + ipttl = smp->data.u.str.area[8]; + } + else if (ipver == 6) { + /* check fields for IPv6 */ + if (smp->data.u.str.data < 40) + return 0; + + pktlen = read_n16(smp->data.u.str.area + 4); + // extension/next proto => ext present if !tcp && !udp + ipext = smp->data.u.str.area[6]; + ipext = ipext != 6 && ipext != 17; + + iptos = read_n16(smp->data.u.str.area) >> 4; + ipdf = 1; // no fragments by default in IPv6 + ipttl = smp->data.u.str.area[7]; + } + else + return 0; + + /* prepare trash to contain at least 7 bytes */ + trash->data = 7; + + /* store the TOS in the FP's first byte */ + trash->area[0] = iptos; + + /* keep only two bits for TTL: <=32, <=64, <=128, <=255 */ + ipttl = (ipttl > 64) ? ((ipttl > 128) ? 3 : 2) : ((ipttl > 32) ? 1 : 0); + + /* OK we've collected required IP fields, let's advance to TCP now */ + iplen = ip_header_length(smp); + if (!iplen || iplen > pktlen) + return 0; + + /* advance buffer by */ + smp->data.u.str.area += iplen; + smp->data.u.str.data -= iplen; + pktlen -= iplen; + + /* now SMP points to the TCP header. It must be complete */ + tcplen = tcp_fullhdr_length(smp); + if (!tcplen || tcplen > pktlen) + return 0; + + pktlen -= tcplen; // remaining data length (e.g. TFO) + tcpflags = smp->data.u.str.area[13]; + tcpwin = read_n16(smp->data.u.str.area + 14); + + /* second byte of FP contains: + * - bit 7..4: IP.v6(1), IP.DF(1), IP.TTL(2), + * - bit 3..0: IP.ext(1), TCP.have_data(1), TCP.CWR(1), TCP.ECE(1) + */ + trash->area[1] = + ((ipver == 6) << 7) | + (ipdf << 6) | + (ipttl << 4) | + (ipext << 3) | + ((pktlen > 0) << 2) | // data present (TFO) + (tcpflags >> 6 << 0); // CWR, ECE + + tcpmss = tcpws = 0; + ofs = 20; + while (ofs < tcplen) { + size_t next; + + if (smp->data.u.str.area[ofs] == 0) // kind0=end of options + break; + + /* kind1 = NOP and is a single byte, others have a length field */ + if (smp->data.u.str.area[ofs] == 1) + next = ofs + 1; + else if (ofs + 1 <= tcplen) + next = ofs + smp->data.u.str.area[ofs + 1]; + else + break; + + if (next > tcplen) + break; + + /* option is complete, take a copy of it */ + if (mode > 0) + trash->area[trash->data++] = smp->data.u.str.area[ofs]; + + if (smp->data.u.str.area[ofs] == 2 /* MSS */) { + tcpmss = read_n16(smp->data.u.str.area + ofs + 2); + } + else if (smp->data.u.str.area[ofs] == 3 /* WS */) { + tcpws = (uchar)smp->data.u.str.area[ofs + 2]; + /* output from 1 to 15, thus 0=not found */ + tcpws = tcpws > 14 ? 15 : tcpws + 1; + } + ofs = next; + } + + /* third byte contains hdrlen(4) and wscale(4) */ + trash->area[2] = (tcplen << 2) | tcpws; + + /* then tcpwin(16) then tcpmss(16) */ + write_n16(trash->area + 3, tcpwin); + write_n16(trash->area + 5, tcpmss); + + /* option kinds if any are stored starting at offset 7 */ + smp->data.u.str = *trash; + 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, { @@ -662,6 +815,7 @@ static struct sample_conv_kw_list sample_conv_kws = {ILH, { { "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.fp", sample_conv_ip_fp, ARG1(0,SINT), NULL, SMP_T_BIN, SMP_T_BIN }, { "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 },