Compare commits
3 Commits
master
...
20250417-c
Author | SHA1 | Date | |
---|---|---|---|
|
64c1811552 | ||
|
c97e499854 | ||
|
30c0ea832d |
@ -25651,6 +25651,10 @@ req_ssl_sni string
|
|||||||
req.ssl_st_ext integer
|
req.ssl_st_ext integer
|
||||||
req.ssl_ver integer
|
req.ssl_ver integer
|
||||||
req_ssl_ver integer
|
req_ssl_ver integer
|
||||||
|
req.ssl_cipherlist binary
|
||||||
|
req.ssl_sigalgs binary
|
||||||
|
req.ssl_keyshare_groups binary
|
||||||
|
req.ssl_supported_groups binary
|
||||||
res.len integer
|
res.len integer
|
||||||
res.payload(<offset>,<length>) binary
|
res.payload(<offset>,<length>) binary
|
||||||
res.payload_lv(<offset1>,<length>[,<offset2>]) binary
|
res.payload_lv(<offset1>,<length>[,<offset2>]) binary
|
||||||
@ -25855,6 +25859,68 @@ req_ssl_sni : string (deprecated)
|
|||||||
use_backend bk_allow if { req.ssl_sni -f allowed_sites }
|
use_backend bk_allow if { req.ssl_sni -f allowed_sites }
|
||||||
default_backend bk_sorry_page
|
default_backend bk_sorry_page
|
||||||
|
|
||||||
|
req.ssl_cipherlist binary
|
||||||
|
Returns the binary form of the list of symmetric cipher options supported by
|
||||||
|
the client as reported in the contents of a TLS ClientHello. Note that this
|
||||||
|
only applies to raw contents found in the request buffer and not to contents
|
||||||
|
deciphered via an SSL data layer, so this will not work with "bind" lines
|
||||||
|
having the "ssl" option. Refer to "ssl_fc_cipherlist_bin" which is the SSL
|
||||||
|
bind equivalent that can be used when the "ssl" option is specified.
|
||||||
|
|
||||||
|
Examples :
|
||||||
|
# Wait for a client hello for at most 5 seconds
|
||||||
|
tcp-request inspect-delay 5s
|
||||||
|
tcp-request content accept if { req.ssl_hello_type 1 }
|
||||||
|
use-server fe3 if { req.ssl_cipherlist,be2hex(:,2),lower -m sub 1302:009f }
|
||||||
|
server fe3 ${htst_fe3_addr}:${htst_fe3_port}
|
||||||
|
|
||||||
|
req.ssl_sigalgs binary
|
||||||
|
Returns the binary form of the list of signature algorithms supported by the
|
||||||
|
client as reported in the TLS ClientHello. This is available as a client hello
|
||||||
|
extension. Note that this only applies to raw contents found in the request
|
||||||
|
buffer and not to contents deciphered via an SSL data layer, so this will not
|
||||||
|
work with "bind" lines having the "ssl" option. Refer to "ssl_fc_sigalgs_bin"
|
||||||
|
which is the SSL bind equivalent that can be used when the "ssl" option is
|
||||||
|
specified.
|
||||||
|
|
||||||
|
Examples :
|
||||||
|
# Wait for a client hello for at most 5 seconds
|
||||||
|
tcp-request inspect-delay 5s
|
||||||
|
tcp-request content accept if { req.ssl_hello_type 1 }
|
||||||
|
use-server fe4 if { req.ssl_sigalgs,be2hex(:,2),lower -m sub 0403:0805 }
|
||||||
|
server fe4 ${htst_fe4_addr}:${htst_fe4_port}
|
||||||
|
|
||||||
|
req.ssl_keyshare_groups binary
|
||||||
|
Return the binary format of the list of cryptographic parameters for key exchange
|
||||||
|
supported by the client as reported in the TLS ClientHello. In TLS v1.3, keyshare
|
||||||
|
is part of the ClientHello message and is the final client hello extension. Note
|
||||||
|
that this only applies to raw contents found in the request buffer and not to
|
||||||
|
contents deciphered via an SSL data layer, so this will not work with "bind"
|
||||||
|
lines having the "ssl" option.
|
||||||
|
|
||||||
|
Examples :
|
||||||
|
# Wait for a client hello for at most 5 seconds
|
||||||
|
tcp-request inspect-delay 5s
|
||||||
|
tcp-request content accept if { req.ssl_hello_type 1 }
|
||||||
|
use-server fe3 if { req.ssl_keyshare_groups,be2hex(:,2),lower -m sub 001d }
|
||||||
|
server fe3 ${htst_fe3_addr}:${htst_fe3_port}
|
||||||
|
|
||||||
|
req.ssl_supported_groups binary
|
||||||
|
Returns the binary form of the list of supported groups supported by the client
|
||||||
|
as reported in the TLS ClientHello and used for key exchange which can include
|
||||||
|
both elliptic curve and non-EC key exchange. Note that this only applies to raw
|
||||||
|
contents found in the request buffer and not to contents deciphered via an SSL
|
||||||
|
data layer, so this will not work with "bind" lines having the "ssl" option.
|
||||||
|
Refer to "ssl_fc_eclist_bin" which is the SSL bind equivalent that can be used
|
||||||
|
when the "ssl" option is specified.
|
||||||
|
|
||||||
|
Examples :
|
||||||
|
# Wait for a client hello for at most 5 seconds
|
||||||
|
tcp-request inspect-delay 5s
|
||||||
|
tcp-request content accept if { req.ssl_hello_type 1 }
|
||||||
|
use-server fe3 if { req.ssl_supported_groups, be2hex(:,2),lower -m sub 0017 }
|
||||||
|
server fe3 ${htst_fe3_addr}:${htst_fe3_port}
|
||||||
|
|
||||||
req.ssl_st_ext : integer
|
req.ssl_st_ext : integer
|
||||||
Returns 0 if the client didn't send a SessionTicket TLS Extension (RFC5077)
|
Returns 0 if the client didn't send a SessionTicket TLS Extension (RFC5077)
|
||||||
Returns 1 if the client sent SessionTicket TLS Extension
|
Returns 1 if the client sent SessionTicket TLS Extension
|
||||||
|
81
reg-tests/checks/tcp-check-client-hello.vtc
Normal file
81
reg-tests/checks/tcp-check-client-hello.vtc
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#REGTEST_TYPE=devel
|
||||||
|
#EXCLUDE_TARGETS=osx,generic
|
||||||
|
|
||||||
|
varnishtest "Health checks: test enhanced observability of TLS ClientHello"
|
||||||
|
feature cmd "$HAPROXY_PROGRAM -cc 'feature(OPENSSL) && !ssllib_name_startswith(wolfSSL) && !ssllib_name_startswith(LibreSSL) && openssl_version_atleast(1.1.1)'"
|
||||||
|
feature ignore_unknown_macro
|
||||||
|
|
||||||
|
syslog S_ok -level notice {
|
||||||
|
recv
|
||||||
|
expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Health check for server be[0-9]+/srv succeeded, reason: Layer6 check passed.+check duration: [[:digit:]]+ms, status: 1/1 UP."
|
||||||
|
recv
|
||||||
|
expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Health check for server be[0-9]+/srv succeeded, reason: Layer6 check passed.+check duration: [[:digit:]]+ms, status: 1/1 UP."
|
||||||
|
recv
|
||||||
|
expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Health check for server be[0-9]+/srv succeeded, reason: Layer6 check passed.+check duration: [[:digit:]]+ms, status: 1/1 UP."
|
||||||
|
} -start
|
||||||
|
|
||||||
|
haproxy htst -conf {
|
||||||
|
global
|
||||||
|
ssl-default-bind-options ssl-min-ver TLSv1.2 ssl-max-ver TLSv1.3
|
||||||
|
|
||||||
|
defaults
|
||||||
|
log global
|
||||||
|
option tcplog
|
||||||
|
timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
|
||||||
|
timeout server "${HAPROXY_TEST_TIMEOUT-5s}"
|
||||||
|
timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
|
||||||
|
|
||||||
|
listen li1
|
||||||
|
mode tcp
|
||||||
|
bind "fd@${li1}"
|
||||||
|
tcp-request inspect-delay 100ms
|
||||||
|
|
||||||
|
acl check_sig_algs req.ssl_sigalgs,be2hex(:,2),lower -m found
|
||||||
|
acl check_key_shares req.ssl_keyshare_groups,be2hex(:,2),lower -m found
|
||||||
|
tcp-request content accept if check_sig_algs
|
||||||
|
tcp-request content accept if check_key_shares
|
||||||
|
|
||||||
|
# Refer to https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.8 && https://tls13.xargs.org/#client-hello/annotated to get the binary values
|
||||||
|
use-server fe3 if { req.ssl_cipherlist,be2hex(:,2),lower -m sub 1302:1303:1301:009f } || { req.ssl_supported_groups, be2hex(:,2),lower -m sub 001d }
|
||||||
|
server fe3 ${htst_fe3_addr}:${htst_fe3_port}
|
||||||
|
|
||||||
|
use-server fe1 if { req.ssl_supported_groups, be2hex(:,2),lower -m sub 0017 }
|
||||||
|
server fe1 ${htst_fe1_addr}:${htst_fe1_port}
|
||||||
|
|
||||||
|
frontend fe1
|
||||||
|
bind "fd@${fe1}" ssl crt ${testdir}/common.pem curves P-256:P-384
|
||||||
|
|
||||||
|
frontend fe3
|
||||||
|
bind "fd@${fe3}" ssl crt ${testdir}/common.pem
|
||||||
|
} -start
|
||||||
|
|
||||||
|
haproxy h1 -conf {
|
||||||
|
defaults
|
||||||
|
mode tcp
|
||||||
|
timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
|
||||||
|
timeout server "${HAPROXY_TEST_TIMEOUT-5s}"
|
||||||
|
timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
|
||||||
|
|
||||||
|
backend be1
|
||||||
|
mode tcp
|
||||||
|
log ${S_ok_addr}:${S_ok_port} daemon
|
||||||
|
option log-health-checks
|
||||||
|
option tcp-check
|
||||||
|
server srv ${htst_li1_addr}:${htst_li1_port} check inter 1s rise 1 fall 1 check-ssl verify none curves X25519
|
||||||
|
|
||||||
|
backend be2
|
||||||
|
mode tcp
|
||||||
|
log ${S_ok_addr}:${S_ok_port} daemon
|
||||||
|
option log-health-checks
|
||||||
|
option tcp-check
|
||||||
|
server srv ${htst_li1_addr}:${htst_li1_port} check inter 1s rise 1 fall 1 check-ssl verify none curves P-256:P-384
|
||||||
|
|
||||||
|
backend be3
|
||||||
|
mode tcp
|
||||||
|
log ${S_ok_addr}:${S_ok_port} daemon
|
||||||
|
option log-health-checks
|
||||||
|
option tcp-check
|
||||||
|
server srv ${htst_li1_addr}:${htst_li1_port} check inter 1s rise 1 fall 1 check-ssl verify none ciphers ECDHE-RSA-AES256-GCM-SHA384
|
||||||
|
} -start
|
||||||
|
|
||||||
|
syslog S_ok -wait
|
582
src/payload.c
582
src/payload.c
@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <haproxy/acl.h>
|
#include <haproxy/acl.h>
|
||||||
@ -26,11 +27,181 @@
|
|||||||
#include <haproxy/stconn.h>
|
#include <haproxy/stconn.h>
|
||||||
#include <haproxy/tools.h>
|
#include <haproxy/tools.h>
|
||||||
|
|
||||||
|
enum {
|
||||||
|
CLIENTHELLO_ERR_OK = 0,
|
||||||
|
CLIENTHELLO_ERR_UNAVAIL = 1,
|
||||||
|
CLIENTHELLO_ERR_TOO_SHORT = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* struct to hold variables used during generic client hello parsing
|
||||||
|
*/
|
||||||
|
struct clnt_hello_proc {
|
||||||
|
int hs_len;
|
||||||
|
int status;
|
||||||
|
unsigned char *data;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
/* All supported sample fetch functions must be declared here */
|
/* All supported sample fetch functions must be declared here */
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
/* Extract information presented in a TLS client hello handshake message.
|
||||||
|
* The format of the message is the following (cf RFC5246 + RFC6066) :
|
||||||
|
* TLS frame :
|
||||||
|
* - uint8 type = 0x16 (Handshake)
|
||||||
|
* - uint16 version >= 0x0301 (TLSv1)
|
||||||
|
* - uint16 length (frame length)
|
||||||
|
* - TLS handshake :
|
||||||
|
* - uint8 msg_type = 0x01 (ClientHello)
|
||||||
|
* - uint24 length (handshake message length)
|
||||||
|
* - ClientHello :
|
||||||
|
* - uint16 client_version >= 0x0301 (TLSv1)
|
||||||
|
* - uint8 Random[32] (4 first ones are timestamp)
|
||||||
|
* - SessionID :
|
||||||
|
* - uint8 session_id_len (0..32) (SessionID len in bytes)
|
||||||
|
* - uint8 session_id[session_id_len]
|
||||||
|
* - CipherSuite :
|
||||||
|
* - uint16 cipher_len >= 2 (Cipher length in bytes)
|
||||||
|
* - uint16 ciphers[cipher_len/2]
|
||||||
|
* - CompressionMethod :
|
||||||
|
* - uint8 compression_len >= 1 (# of supported methods)
|
||||||
|
* - uint8 compression_methods[compression_len]
|
||||||
|
* - optional client_extension_len (in bytes)
|
||||||
|
* - optional sequence of ClientHelloExtensions (as many bytes as above):
|
||||||
|
* - uint16 extension_type = 0 for server_name
|
||||||
|
* - uint16 extension_len
|
||||||
|
* - opaque extension_data[extension_len]
|
||||||
|
* - uint16 server_name_list_len (# of bytes here)
|
||||||
|
* - opaque server_names[server_name_list_len bytes]
|
||||||
|
* - uint8 name_type = 0 for host_name
|
||||||
|
* - uint16 name_len
|
||||||
|
* - opaque hostname[name_len bytes]
|
||||||
|
*/
|
||||||
|
struct clnt_hello_proc smp_client_hello_parse( struct sample *smp, bool parse_extensions)
|
||||||
|
{
|
||||||
|
int hs_len, ext_len, bleft;
|
||||||
|
int status = CLIENTHELLO_ERR_OK;
|
||||||
|
struct channel *chn;
|
||||||
|
struct clnt_hello_proc cp;
|
||||||
|
unsigned char *data;
|
||||||
|
|
||||||
|
if (!smp->strm)
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
/* meaningless for HTX buffers */
|
||||||
|
if (IS_HTX_STRM(smp->strm))
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
chn = ((smp->opt & SMP_OPT_DIR) == SMP_OPT_DIR_RES) ? &smp->strm->res : &smp->strm->req;
|
||||||
|
|
||||||
|
|
||||||
|
bleft = ci_data(chn);
|
||||||
|
data = (unsigned char *)ci_head(chn);
|
||||||
|
|
||||||
|
/* Check for SSL/TLS Handshake */
|
||||||
|
if (!bleft)
|
||||||
|
goto too_short;
|
||||||
|
if (*data != 0x16)
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
/* Check for SSLv3 or later (SSL version >= 3.0) in the record layer*/
|
||||||
|
if (bleft < 3)
|
||||||
|
goto too_short;
|
||||||
|
if (data[1] < 0x03)
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
if (bleft < 5)
|
||||||
|
goto too_short;
|
||||||
|
hs_len = (data[3] << 8) + data[4];
|
||||||
|
if (hs_len < 1 + 3 + 2 + 32 + 1 + 2 + 2 + 1 + 1 + 2 + 2)
|
||||||
|
goto not_ssl_hello; /* too short to have an extension */
|
||||||
|
|
||||||
|
data += 5; /* enter TLS handshake */
|
||||||
|
bleft -= 5;
|
||||||
|
|
||||||
|
/* Check for a complete client hello starting at <data> */
|
||||||
|
if (bleft < 1)
|
||||||
|
goto too_short;
|
||||||
|
if (data[0] != 0x01) /* msg_type = Client Hello */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
/* Check the Hello's length */
|
||||||
|
if (bleft < 4)
|
||||||
|
goto too_short;
|
||||||
|
hs_len = (data[1] << 16) + (data[2] << 8) + data[3];
|
||||||
|
if (hs_len < 2 + 32 + 1 + 2 + 2 + 1 + 1 + 2 + 2)
|
||||||
|
goto not_ssl_hello; /* too short to have an extension */
|
||||||
|
|
||||||
|
/* We want the full handshake here */
|
||||||
|
if (bleft < hs_len)
|
||||||
|
goto too_short;
|
||||||
|
|
||||||
|
data += 4;
|
||||||
|
/* Start of the ClientHello message */
|
||||||
|
if (data[0] < 0x03 || data[1] < 0x01) /* TLSv1 minimum */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
ext_len = data[34]; /* session_id_len */
|
||||||
|
if (ext_len > 32 || ext_len > (hs_len - 35)) /* check for correct session_id len */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
/* Jump to cipher suite */
|
||||||
|
hs_len -= 35 + ext_len;
|
||||||
|
data += 35 + ext_len;
|
||||||
|
|
||||||
|
if (hs_len < 4 || /* minimum one cipher */
|
||||||
|
(ext_len = (data[0] << 8) + data[1]) < 2 || /* minimum 2 bytes for a cipher */
|
||||||
|
ext_len > hs_len)
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
/* Jump to the compression methods. For fetching cipher list this processing is not required. */
|
||||||
|
if (parse_extensions)
|
||||||
|
goto parse_extn;
|
||||||
|
else
|
||||||
|
goto parse_cipher;
|
||||||
|
|
||||||
|
parse_extn:
|
||||||
|
hs_len -= 2 + ext_len;
|
||||||
|
data += 2 + ext_len;
|
||||||
|
|
||||||
|
if (hs_len < 2 || /* minimum one compression method */
|
||||||
|
data[0] < 1 || data[0] > hs_len) /* minimum 1 bytes for a method */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
/* Jump to the extensions */
|
||||||
|
hs_len -= 1 + data[0];
|
||||||
|
data += 1 + data[0];
|
||||||
|
|
||||||
|
if (hs_len < 2 || /* minimum one extension list length */
|
||||||
|
(ext_len = (data[0] << 8) + data[1]) > hs_len - 2) /* list too long */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
hs_len = ext_len; /* limit ourselves to the extension length */
|
||||||
|
data += 2;
|
||||||
|
|
||||||
|
cp.hs_len = hs_len;
|
||||||
|
cp.data = data;
|
||||||
|
cp.status = status;
|
||||||
|
return cp;
|
||||||
|
|
||||||
|
parse_cipher:
|
||||||
|
cp.hs_len = ext_len;
|
||||||
|
cp.data = data;
|
||||||
|
cp.status = status;
|
||||||
|
return cp;
|
||||||
|
|
||||||
|
not_ssl_hello:
|
||||||
|
cp.status = CLIENTHELLO_ERR_UNAVAIL;
|
||||||
|
|
||||||
|
too_short:
|
||||||
|
cp.status = CLIENTHELLO_ERR_TOO_SHORT;
|
||||||
|
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
/* wait for more data as long as possible, then return TRUE. This should be
|
/* wait for more data as long as possible, then return TRUE. This should be
|
||||||
* used with content inspection.
|
* used with content inspection.
|
||||||
*/
|
*/
|
||||||
@ -83,105 +254,21 @@ smp_fetch_len(const struct arg *args, struct sample *smp, const char *kw, void *
|
|||||||
static int
|
static int
|
||||||
smp_fetch_req_ssl_st_ext(const struct arg *args, struct sample *smp, const char *kw, void *private)
|
smp_fetch_req_ssl_st_ext(const struct arg *args, struct sample *smp, const char *kw, void *private)
|
||||||
{
|
{
|
||||||
int hs_len, ext_len, bleft;
|
struct clnt_hello_proc cp;
|
||||||
struct channel *chn;
|
cp = smp_client_hello_parse(smp, true);
|
||||||
unsigned char *data;
|
if (cp.status == CLIENTHELLO_ERR_UNAVAIL)
|
||||||
|
|
||||||
if (!smp->strm)
|
|
||||||
goto not_ssl_hello;
|
goto not_ssl_hello;
|
||||||
|
else if (cp.status == CLIENTHELLO_ERR_TOO_SHORT)
|
||||||
/* meaningless for HTX buffers */
|
|
||||||
if (IS_HTX_STRM(smp->strm))
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
chn = ((smp->opt & SMP_OPT_DIR) == SMP_OPT_DIR_RES) ? &smp->strm->res : &smp->strm->req;
|
|
||||||
|
|
||||||
|
|
||||||
bleft = ci_data(chn);
|
|
||||||
data = (unsigned char *)ci_head(chn);
|
|
||||||
|
|
||||||
/* Check for SSL/TLS Handshake */
|
|
||||||
if (!bleft)
|
|
||||||
goto too_short;
|
|
||||||
if (*data != 0x16)
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Check for SSLv3 or later (SSL version >= 3.0) in the record layer*/
|
|
||||||
if (bleft < 3)
|
|
||||||
goto too_short;
|
|
||||||
if (data[1] < 0x03)
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
if (bleft < 5)
|
|
||||||
goto too_short;
|
|
||||||
hs_len = (data[3] << 8) + data[4];
|
|
||||||
if (hs_len < 1 + 3 + 2 + 32 + 1 + 2 + 2 + 1 + 1 + 2 + 2)
|
|
||||||
goto not_ssl_hello; /* too short to have an extension */
|
|
||||||
|
|
||||||
data += 5; /* enter TLS handshake */
|
|
||||||
bleft -= 5;
|
|
||||||
|
|
||||||
/* Check for a complete client hello starting at <data> */
|
|
||||||
if (bleft < 1)
|
|
||||||
goto too_short;
|
|
||||||
if (data[0] != 0x01) /* msg_type = Client Hello */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Check the Hello's length */
|
|
||||||
if (bleft < 4)
|
|
||||||
goto too_short;
|
|
||||||
hs_len = (data[1] << 16) + (data[2] << 8) + data[3];
|
|
||||||
if (hs_len < 2 + 32 + 1 + 2 + 2 + 1 + 1 + 2 + 2)
|
|
||||||
goto not_ssl_hello; /* too short to have an extension */
|
|
||||||
|
|
||||||
/* We want the full handshake here */
|
|
||||||
if (bleft < hs_len)
|
|
||||||
goto too_short;
|
goto too_short;
|
||||||
|
|
||||||
data += 4;
|
|
||||||
/* Start of the ClientHello message */
|
|
||||||
if (data[0] < 0x03 || data[1] < 0x01) /* TLSv1 minimum */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
ext_len = data[34]; /* session_id_len */
|
while (cp.hs_len >= 4) {
|
||||||
if (ext_len > 32 || ext_len > (hs_len - 35)) /* check for correct session_id len */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Jump to cipher suite */
|
|
||||||
hs_len -= 35 + ext_len;
|
|
||||||
data += 35 + ext_len;
|
|
||||||
|
|
||||||
if (hs_len < 4 || /* minimum one cipher */
|
|
||||||
(ext_len = (data[0] << 8) + data[1]) < 2 || /* minimum 2 bytes for a cipher */
|
|
||||||
ext_len > hs_len)
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Jump to the compression methods */
|
|
||||||
hs_len -= 2 + ext_len;
|
|
||||||
data += 2 + ext_len;
|
|
||||||
|
|
||||||
if (hs_len < 2 || /* minimum one compression method */
|
|
||||||
data[0] < 1 || data[0] > hs_len) /* minimum 1 bytes for a method */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Jump to the extensions */
|
|
||||||
hs_len -= 1 + data[0];
|
|
||||||
data += 1 + data[0];
|
|
||||||
|
|
||||||
if (hs_len < 2 || /* minimum one extension list length */
|
|
||||||
(ext_len = (data[0] << 8) + data[1]) > hs_len - 2) /* list too long */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
hs_len = ext_len; /* limit ourselves to the extension length */
|
|
||||||
data += 2;
|
|
||||||
|
|
||||||
while (hs_len >= 4) {
|
|
||||||
int ext_type, ext_len;
|
int ext_type, ext_len;
|
||||||
|
|
||||||
ext_type = (data[0] << 8) + data[1];
|
ext_type = (cp.data[0] << 8) + cp.data[1];
|
||||||
ext_len = (data[2] << 8) + data[3];
|
ext_len = (cp.data[2] << 8) + cp.data[3];
|
||||||
|
|
||||||
if (ext_len > hs_len - 4) /* Extension too long */
|
if (ext_len > cp.hs_len - 4) /* Extension too long */
|
||||||
goto not_ssl_hello;
|
goto not_ssl_hello;
|
||||||
|
|
||||||
/* SesstionTicket extension */
|
/* SesstionTicket extension */
|
||||||
@ -197,8 +284,8 @@ smp_fetch_req_ssl_st_ext(const struct arg *args, struct sample *smp, const char
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
hs_len -= 4 + ext_len;
|
cp.hs_len -= 4 + ext_len;
|
||||||
data += 4 + ext_len;
|
cp.data += 4 + ext_len;
|
||||||
}
|
}
|
||||||
/* SessionTicket Extension not found */
|
/* SessionTicket Extension not found */
|
||||||
smp->data.type = SMP_T_SINT;
|
smp->data.type = SMP_T_SINT;
|
||||||
@ -219,103 +306,20 @@ smp_fetch_req_ssl_st_ext(const struct arg *args, struct sample *smp, const char
|
|||||||
static int
|
static int
|
||||||
smp_fetch_req_ssl_ec_ext(const struct arg *args, struct sample *smp, const char *kw, void *private)
|
smp_fetch_req_ssl_ec_ext(const struct arg *args, struct sample *smp, const char *kw, void *private)
|
||||||
{
|
{
|
||||||
int hs_len, ext_len, bleft;
|
struct clnt_hello_proc cp;
|
||||||
struct channel *chn;
|
cp = smp_client_hello_parse(smp, true);
|
||||||
unsigned char *data;
|
if (cp.status == CLIENTHELLO_ERR_UNAVAIL)
|
||||||
|
|
||||||
if (!smp->strm)
|
|
||||||
goto not_ssl_hello;
|
goto not_ssl_hello;
|
||||||
|
else if (cp.status == CLIENTHELLO_ERR_TOO_SHORT)
|
||||||
/* meaningless for HTX buffers */
|
|
||||||
if (IS_HTX_STRM(smp->strm))
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
chn = ((smp->opt & SMP_OPT_DIR) == SMP_OPT_DIR_RES) ? &smp->strm->res : &smp->strm->req;
|
|
||||||
bleft = ci_data(chn);
|
|
||||||
data = (unsigned char *)ci_head(chn);
|
|
||||||
|
|
||||||
/* Check for SSL/TLS Handshake */
|
|
||||||
if (!bleft)
|
|
||||||
goto too_short;
|
|
||||||
if (*data != 0x16)
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Check for SSLv3 or later (SSL version >= 3.0) in the record layer*/
|
|
||||||
if (bleft < 3)
|
|
||||||
goto too_short;
|
|
||||||
if (data[1] < 0x03)
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
if (bleft < 5)
|
|
||||||
goto too_short;
|
|
||||||
hs_len = (data[3] << 8) + data[4];
|
|
||||||
if (hs_len < 1 + 3 + 2 + 32 + 1 + 2 + 2 + 1 + 1 + 2 + 2)
|
|
||||||
goto not_ssl_hello; /* too short to have an extension */
|
|
||||||
|
|
||||||
data += 5; /* enter TLS handshake */
|
|
||||||
bleft -= 5;
|
|
||||||
|
|
||||||
/* Check for a complete client hello starting at <data> */
|
|
||||||
if (bleft < 1)
|
|
||||||
goto too_short;
|
|
||||||
if (data[0] != 0x01) /* msg_type = Client Hello */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Check the Hello's length */
|
|
||||||
if (bleft < 4)
|
|
||||||
goto too_short;
|
|
||||||
hs_len = (data[1] << 16) + (data[2] << 8) + data[3];
|
|
||||||
if (hs_len < 2 + 32 + 1 + 2 + 2 + 1 + 1 + 2 + 2)
|
|
||||||
goto not_ssl_hello; /* too short to have an extension */
|
|
||||||
|
|
||||||
/* We want the full handshake here */
|
|
||||||
if (bleft < hs_len)
|
|
||||||
goto too_short;
|
goto too_short;
|
||||||
|
|
||||||
data += 4;
|
while (cp.hs_len >= 4) {
|
||||||
/* Start of the ClientHello message */
|
|
||||||
if (data[0] < 0x03 || data[1] < 0x01) /* TLSv1 minimum */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
ext_len = data[34]; /* session_id_len */
|
|
||||||
if (ext_len > 32 || ext_len > (hs_len - 35)) /* check for correct session_id len */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Jump to cipher suite */
|
|
||||||
hs_len -= 35 + ext_len;
|
|
||||||
data += 35 + ext_len;
|
|
||||||
|
|
||||||
if (hs_len < 4 || /* minimum one cipher */
|
|
||||||
(ext_len = (data[0] << 8) + data[1]) < 2 || /* minimum 2 bytes for a cipher */
|
|
||||||
ext_len > hs_len)
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Jump to the compression methods */
|
|
||||||
hs_len -= 2 + ext_len;
|
|
||||||
data += 2 + ext_len;
|
|
||||||
|
|
||||||
if (hs_len < 2 || /* minimum one compression method */
|
|
||||||
data[0] < 1 || data[0] > hs_len) /* minimum 1 bytes for a method */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
/* Jump to the extensions */
|
|
||||||
hs_len -= 1 + data[0];
|
|
||||||
data += 1 + data[0];
|
|
||||||
|
|
||||||
if (hs_len < 2 || /* minimum one extension list length */
|
|
||||||
(ext_len = (data[0] << 8) + data[1]) > hs_len - 2) /* list too long */
|
|
||||||
goto not_ssl_hello;
|
|
||||||
|
|
||||||
hs_len = ext_len; /* limit ourselves to the extension length */
|
|
||||||
data += 2;
|
|
||||||
|
|
||||||
while (hs_len >= 4) {
|
|
||||||
int ext_type, ext_len;
|
int ext_type, ext_len;
|
||||||
|
|
||||||
ext_type = (data[0] << 8) + data[1];
|
ext_type = (cp.data[0] << 8) + cp.data[1];
|
||||||
ext_len = (data[2] << 8) + data[3];
|
ext_len = (cp.data[2] << 8) + cp.data[3];
|
||||||
|
|
||||||
if (ext_len > hs_len - 4) /* Extension too long */
|
if (ext_len > cp.hs_len - 4) /* Extension too long */
|
||||||
goto not_ssl_hello;
|
goto not_ssl_hello;
|
||||||
|
|
||||||
/* Elliptic curves extension */
|
/* Elliptic curves extension */
|
||||||
@ -326,8 +330,8 @@ smp_fetch_req_ssl_ec_ext(const struct arg *args, struct sample *smp, const char
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
hs_len -= 4 + ext_len;
|
cp.hs_len -= 4 + ext_len;
|
||||||
data += 4 + ext_len;
|
cp.data += 4 + ext_len;
|
||||||
}
|
}
|
||||||
/* server name not found */
|
/* server name not found */
|
||||||
goto not_ssl_hello;
|
goto not_ssl_hello;
|
||||||
@ -522,6 +526,202 @@ smp_fetch_req_ssl_ver(const struct arg *args, struct sample *smp, const char *kw
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extract the ciphers that may be presented in a TLS client hello handshake message.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
smp_fetch_ssl_cipherlist(const struct arg *args, struct sample *smp, const char *kw, void *private)
|
||||||
|
{
|
||||||
|
struct clnt_hello_proc cp;
|
||||||
|
cp = smp_client_hello_parse(smp, false);
|
||||||
|
if (cp.status == CLIENTHELLO_ERR_UNAVAIL)
|
||||||
|
goto not_ssl_hello;
|
||||||
|
else if (cp.status == CLIENTHELLO_ERR_TOO_SHORT)
|
||||||
|
goto too_short;
|
||||||
|
|
||||||
|
smp->data.type = SMP_T_BIN;
|
||||||
|
smp->data.u.str.area = (char *)cp.data + 2;
|
||||||
|
smp->data.u.str.data = cp.hs_len;
|
||||||
|
smp->flags = SMP_F_VOLATILE | SMP_F_CONST;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
too_short:
|
||||||
|
smp->flags = SMP_F_MAY_CHANGE;
|
||||||
|
|
||||||
|
not_ssl_hello:
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract the supported group that may be presented in a TLS client hello handshake
|
||||||
|
* message.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
smp_fetch_ssl_supported_groups(const struct arg *args, struct sample *smp, const char *kw, void *private)
|
||||||
|
{
|
||||||
|
struct clnt_hello_proc cp;
|
||||||
|
|
||||||
|
cp = smp_client_hello_parse(smp, true);
|
||||||
|
if (cp.status == CLIENTHELLO_ERR_UNAVAIL)
|
||||||
|
goto not_ssl_hello;
|
||||||
|
else if (cp.status == CLIENTHELLO_ERR_TOO_SHORT)
|
||||||
|
goto too_short;
|
||||||
|
|
||||||
|
while (cp.hs_len >= 4) {
|
||||||
|
int ext_type, ext_len, grp_len;
|
||||||
|
|
||||||
|
ext_type = (cp.data[0] << 8) + cp.data[1]; /* Extension type */
|
||||||
|
ext_len = (cp.data[2] << 8) + cp.data[3]; /* Extension length */
|
||||||
|
|
||||||
|
if (ext_len > cp.hs_len - 4) /* Extension too long */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
if (ext_type == 10) { /* Supported groups extension type ID is 10dec */
|
||||||
|
if (ext_len < 2) /* need at least one entry of 2 bytes in the list length */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
grp_len = (cp.data[4] << 8) + cp.data[5]; /* Supported group list length */
|
||||||
|
if (grp_len < 2 || grp_len > cp.hs_len - 6)
|
||||||
|
goto not_ssl_hello; /* at least 2 bytes per supported group */
|
||||||
|
|
||||||
|
smp->data.type = SMP_T_BIN;
|
||||||
|
smp->data.u.str.area = (char *)cp.data + 6;
|
||||||
|
smp->data.u.str.data = grp_len;
|
||||||
|
smp->flags = SMP_F_VOL_SESS | SMP_F_CONST;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
cp.hs_len -= 4 + ext_len;
|
||||||
|
cp.data += 4 + ext_len;
|
||||||
|
}
|
||||||
|
/* supported groups not found */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
too_short:
|
||||||
|
smp->flags = SMP_F_MAY_CHANGE;
|
||||||
|
|
||||||
|
not_ssl_hello:
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract the signature algorithms that may be presented in a TLS client hello
|
||||||
|
* handshake message.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
smp_fetch_ssl_sigalgs(const struct arg *args, struct sample *smp, const char *kw, void *private)
|
||||||
|
{
|
||||||
|
struct clnt_hello_proc cp;
|
||||||
|
|
||||||
|
cp = smp_client_hello_parse(smp, true);
|
||||||
|
if (cp.status == CLIENTHELLO_ERR_UNAVAIL)
|
||||||
|
goto not_ssl_hello;
|
||||||
|
else if (cp.status == CLIENTHELLO_ERR_TOO_SHORT)
|
||||||
|
goto too_short;
|
||||||
|
|
||||||
|
while (cp.hs_len >= 4) {
|
||||||
|
int ext_type, ext_len, sigalg_len;
|
||||||
|
|
||||||
|
ext_type = (cp.data[0] << 8) + cp.data[1]; /* Extension type */
|
||||||
|
ext_len = (cp.data[2] << 8) + cp.data[3]; /* Extension length */
|
||||||
|
|
||||||
|
if (ext_len > cp.hs_len - 4) /* Extension too long */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
if (ext_type == 13) { /* Sigalgs extension type ID is 13dec */
|
||||||
|
if (ext_len < 2) /* need at least one entry of 2 bytes in the list length */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
sigalg_len = (cp.data[4] << 8) + cp.data[5]; /* Sigalgs list length */
|
||||||
|
if (sigalg_len < 2 || sigalg_len > cp.hs_len - 6)
|
||||||
|
goto not_ssl_hello; /* at least 2 bytes per sigalg */
|
||||||
|
|
||||||
|
smp->data.type = SMP_T_BIN;
|
||||||
|
smp->data.u.str.area = (char *)cp.data + 6;
|
||||||
|
smp->data.u.str.data = sigalg_len;
|
||||||
|
smp->flags = SMP_F_VOLATILE | SMP_F_CONST;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
cp.hs_len -= 4 + ext_len;
|
||||||
|
cp.data += 4 + ext_len;
|
||||||
|
}
|
||||||
|
/* sigalgs not found */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
too_short:
|
||||||
|
smp->flags = SMP_F_MAY_CHANGE;
|
||||||
|
|
||||||
|
not_ssl_hello:
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extract the key shares that may be presented in a TLS client hello handshake message.
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
smp_fetch_ssl_keyshare_groups(const struct arg *args, struct sample *smp, const char *kw, void *private)
|
||||||
|
{
|
||||||
|
int readPosition, numberOfKeyshares;
|
||||||
|
struct buffer *smp_trash = NULL;
|
||||||
|
unsigned char *dataPointer;
|
||||||
|
struct clnt_hello_proc cp;
|
||||||
|
|
||||||
|
cp = smp_client_hello_parse(smp, true);
|
||||||
|
if (cp.status == CLIENTHELLO_ERR_UNAVAIL)
|
||||||
|
goto not_ssl_hello;
|
||||||
|
else if (cp.status == CLIENTHELLO_ERR_TOO_SHORT)
|
||||||
|
goto too_short;
|
||||||
|
|
||||||
|
while (cp.hs_len >= 4) {
|
||||||
|
int ext_type, ext_len, keyshare_len;
|
||||||
|
|
||||||
|
ext_type = (cp.data[0] << 8) + cp.data[1]; /* Extension type */
|
||||||
|
ext_len = (cp.data[2] << 8) + cp.data[3]; /* Extension length */
|
||||||
|
|
||||||
|
if (ext_len > cp.hs_len - 4) /* Extension too long */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
if (ext_type == 51) { /* Keyshare extension type ID is 51dec */
|
||||||
|
if (ext_len < 2) /* need at least one entry of 2 bytes in the list length */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
keyshare_len = (cp.data[4] << 8) + cp.data[5]; /* Client keyshare length */
|
||||||
|
if (keyshare_len < 2 || keyshare_len > cp.hs_len - 6)
|
||||||
|
goto not_ssl_hello; /* at least 2 bytes per keyshare */
|
||||||
|
dataPointer = cp.data + 6; /* start of keyshare entries */
|
||||||
|
readPosition = 0;
|
||||||
|
numberOfKeyshares = 0;
|
||||||
|
smp_trash = get_trash_chunk();
|
||||||
|
while (readPosition < keyshare_len) {
|
||||||
|
/* Get the binary value of the keyshare group and move the offset to the end of the related keyshare */
|
||||||
|
memmove(b_orig(smp_trash) + (2*numberOfKeyshares), &dataPointer[readPosition], 2);
|
||||||
|
numberOfKeyshares++;
|
||||||
|
readPosition += ((int)dataPointer[readPosition+2] << 8) + (int)dataPointer[readPosition+3] + 4;
|
||||||
|
}
|
||||||
|
smp->data.type = SMP_T_BIN;
|
||||||
|
smp->data.u.str.area = smp_trash->area;
|
||||||
|
smp->data.u.str.data = 2*numberOfKeyshares;
|
||||||
|
smp->flags = SMP_F_VOLATILE | SMP_F_CONST;
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
cp.hs_len -= 4 + ext_len;
|
||||||
|
cp.data += 4 + ext_len;
|
||||||
|
}
|
||||||
|
/* keyshare groups not found */
|
||||||
|
goto not_ssl_hello;
|
||||||
|
|
||||||
|
too_short:
|
||||||
|
smp->flags = SMP_F_MAY_CHANGE;
|
||||||
|
not_ssl_hello:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Try to extract the Server Name Indication that may be presented in a TLS
|
/* Try to extract the Server Name Indication that may be presented in a TLS
|
||||||
* client hello handshake message. The format of the message is the following
|
* client hello handshake message. The format of the message is the following
|
||||||
* (cf RFC5246 + RFC6066) :
|
* (cf RFC5246 + RFC6066) :
|
||||||
@ -858,10 +1058,10 @@ smp_fetch_ssl_hello_alpn(const struct arg *args, struct sample *smp, const char
|
|||||||
/* alpn not found */
|
/* alpn not found */
|
||||||
goto not_ssl_hello;
|
goto not_ssl_hello;
|
||||||
|
|
||||||
too_short:
|
too_short:
|
||||||
smp->flags = SMP_F_MAY_CHANGE;
|
smp->flags = SMP_F_MAY_CHANGE;
|
||||||
|
|
||||||
not_ssl_hello:
|
not_ssl_hello:
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -1412,6 +1612,10 @@ static struct sample_fetch_kw_list smp_kws = {ILH, {
|
|||||||
{ "req.ssl_st_ext", smp_fetch_req_ssl_st_ext, 0, NULL, SMP_T_SINT, SMP_USE_L6REQ },
|
{ "req.ssl_st_ext", smp_fetch_req_ssl_st_ext, 0, NULL, SMP_T_SINT, SMP_USE_L6REQ },
|
||||||
{ "req.ssl_hello_type", smp_fetch_ssl_hello_type, 0, NULL, SMP_T_SINT, SMP_USE_L6REQ },
|
{ "req.ssl_hello_type", smp_fetch_ssl_hello_type, 0, NULL, SMP_T_SINT, SMP_USE_L6REQ },
|
||||||
{ "req.ssl_sni", smp_fetch_ssl_hello_sni, 0, NULL, SMP_T_STR, SMP_USE_L6REQ },
|
{ "req.ssl_sni", smp_fetch_ssl_hello_sni, 0, NULL, SMP_T_STR, SMP_USE_L6REQ },
|
||||||
|
{ "req.ssl_cipherlist", smp_fetch_ssl_cipherlist, 0, NULL, SMP_T_BIN, SMP_USE_L6REQ|SMP_USE_L4CLI|SMP_USE_L5CLI|SMP_USE_FTEND },
|
||||||
|
{ "req.ssl_supported_groups", smp_fetch_ssl_supported_groups, 0, NULL, SMP_T_BIN, SMP_USE_L6REQ|SMP_USE_L4CLI|SMP_USE_L5CLI|SMP_USE_FTEND },
|
||||||
|
{ "req.ssl_sigalgs", smp_fetch_ssl_sigalgs, 0, NULL, SMP_T_BIN, SMP_USE_L6REQ|SMP_USE_L4CLI|SMP_USE_L5CLI|SMP_USE_FTEND },
|
||||||
|
{ "req.ssl_keyshare_groups", smp_fetch_ssl_keyshare_groups, 0, NULL, SMP_T_BIN, SMP_USE_L6REQ|SMP_USE_L4CLI|SMP_USE_L5CLI|SMP_USE_FTEND },
|
||||||
{ "req.ssl_alpn", smp_fetch_ssl_hello_alpn, 0, NULL, SMP_T_STR, SMP_USE_L6REQ },
|
{ "req.ssl_alpn", smp_fetch_ssl_hello_alpn, 0, NULL, SMP_T_STR, SMP_USE_L6REQ },
|
||||||
{ "req.ssl_ver", smp_fetch_req_ssl_ver, 0, NULL, SMP_T_SINT, SMP_USE_L6REQ },
|
{ "req.ssl_ver", smp_fetch_req_ssl_ver, 0, NULL, SMP_T_SINT, SMP_USE_L6REQ },
|
||||||
{ "res.len", smp_fetch_len, 0, NULL, SMP_T_SINT, SMP_USE_L6RES },
|
{ "res.len", smp_fetch_len, 0, NULL, SMP_T_SINT, SMP_USE_L6RES },
|
||||||
|
Loading…
x
Reference in New Issue
Block a user