]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: net_helper: add ip.fp() to build a simplified fingerprint of a SYN
authorWilly Tarreau <w@1wt.eu>
Tue, 30 Dec 2025 18:38:22 +0000 (19:38 +0100)
committerWilly Tarreau <w@1wt.eu>
Wed, 31 Dec 2025 16:17:38 +0000 (17:17 +0100)
Here we collect all the stuff that depends on the sender's settings,
such as TOS, IP version, TTL range, presence of DF bit or IP options,
presence of DATA in the SYN, CWR+ECE flags, TCP header length, wscale,
initial window, mss, as well as the list of TCP extension kinds. It's
obviously fairly limited but can allows to avoid blacklisting certain
valid clients sharing the same IP address as a misbehaving one.

It supports both a short and a long mode depending on the argument.
These can be used with the tcp-ss bind option. The doc was updated
accordingly.

doc/configuration.txt
src/net_helper.c

index 3c6d8fd6760f3132ef4674a251075d861544edc8..7f88178fab707be1b7009ffe76c4dbc7af4f303f 100644 (file)
@@ -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([<mode>])
+  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 <mode> argument is not set or is zero, the fingerprint is solely
+  made of the 7 bytes described above. When the <mode> 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",
index 415f7eebda198e146ae981f4ea7104c175912b5a..a3181fbe347f8e39afb5803afc36292c4aa8a8e9 100644 (file)
@@ -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 <len> */
+       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 <const> 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 },