diff --git a/doc/reference.conf b/doc/reference.conf index 749fc48f..d385cba4 100755 --- a/doc/reference.conf +++ b/doc/reference.conf @@ -1316,11 +1316,25 @@ general { away_interval = 30; /* certfp_method: the method that should be used for computing certificate fingerprints. - * Acceptable options are sha1, sha256 and sha512. Networks running versions of charybdis - * prior to charybdis 3.5 MUST use sha1 for certfp_method. + * Acceptable options are sha1, sha256, spki_sha256, sha512 and spki_sha512. Networks + * running versions of charybdis prior to charybdis 3.5 MUST use sha1 for certfp_method. + * + * The spki_* variants operate on the SubjectPublicKeyInfo of the certificate, which does + * not change unless the private key is changed. This allows the fingerprint to stay + * constant even if the certificate is reissued. These fingerprints will be prefixed with + * "SPKI:SHA2-256:" or "SPKI:SHA2-512:" depending on the hash type. These fingerprints + * are not supported on servers running charybdis 3.5.3 or earlier. * * To generate a fingerprint from a certificate file, run the following: * $ openssl x509 -outform DER < your.crt | sha1sum (or sha256sum, or sha512sum) + * + * To generate a SPKI SHA-256 fingerprint, run the following: + * $ openssl x509 -pubkey -noout < your.crt | grep -v 'PUBLIC KEY' | base64 -d | \ + * sha256sum | awk '{ print $1 }' | sed -r -e 's/^/SPKI:SHA2-256:/' + * + * To generate a SPKI SHA-512 fingerprint, run the following: + * $ openssl x509 -pubkey -noout < your.crt | grep -v 'PUBLIC KEY' | base64 -d | \ + * sha512sum | awk '{ print $1 }' | sed -r -e 's/^/SPKI:SHA2-512:/' */ certfp_method = sha1; }; diff --git a/include/sslproc.h b/include/sslproc.h index 838bff75..ee5ba1b3 100644 --- a/include/sslproc.h +++ b/include/sslproc.h @@ -32,7 +32,7 @@ int start_ssldaemon(int count, const char *ssl_cert, const char *ssl_private_key ssl_ctl_t *start_ssld_accept(rb_fde_t *sslF, rb_fde_t *plainF, uint32_t id); ssl_ctl_t *start_ssld_connect(rb_fde_t *sslF, rb_fde_t *plainF, uint32_t id); void start_zlib_session(void *data); -void send_new_ssl_certs(const char *ssl_cert, const char *ssl_private_key, const char *ssl_dh_params, const char *ssl_cipher_list); +void send_new_ssl_certs(const char *ssl_cert, const char *ssl_private_key, const char *ssl_dh_params, const char *ssl_cipher_list, const int method); void ssld_decrement_clicount(ssl_ctl_t *ctl); int get_ssld_count(void); diff --git a/libratbox/include/rb_commio.h b/libratbox/include/rb_commio.h index 92e75950..f0b7dbec 100644 --- a/libratbox/include/rb_commio.h +++ b/libratbox/include/rb_commio.h @@ -103,9 +103,27 @@ void rb_note(rb_fde_t *, const char *); #define RB_SSL_CERTFP_LEN 64 /* Methods for certfp */ -#define RB_SSL_CERTFP_METH_SHA1 0 -#define RB_SSL_CERTFP_METH_SHA256 1 -#define RB_SSL_CERTFP_METH_SHA512 2 +/* Digest of full X.509 certificate */ +#define RB_SSL_CERTFP_METH_CERT_SHA1 0x0000 +#define RB_SSL_CERTFP_METH_CERT_SHA256 0x0001 +#define RB_SSL_CERTFP_METH_CERT_SHA512 0x0002 +/* Digest of SubjectPublicKeyInfo (RFC 5280), used by DANE (RFC 6698) */ +#define RB_SSL_CERTFP_METH_SPKI_SHA256 0x1001 +#define RB_SSL_CERTFP_METH_SPKI_SHA512 0x1002 + +/* Names for certfp */ +#define CERTFP_NAME_CERT_SHA1 "sha1" +#define CERTFP_PREFIX_CERT_SHA1 "" +#define CERTFP_NAME_CERT_SHA256 "sha256" +#define CERTFP_PREFIX_CERT_SHA256 "" +#define CERTFP_NAME_CERT_SHA512 "sha512" +#define CERTFP_PREFIX_CERT_SHA512 "" +/* These prefixes are copied from RFC 7218 */ +#define CERTFP_NAME_SPKI_SHA256 "spki_sha256" +#define CERTFP_PREFIX_SPKI_SHA256 "SPKI:SHA2-256:" +#define CERTFP_NAME_SPKI_SHA512 "spki_sha512" +#define CERTFP_PREFIX_SPKI_SHA512 "SPKI:SHA2-512:" + #define RB_SSL_CERTFP_LEN_SHA1 20 #define RB_SSL_CERTFP_LEN_SHA256 32 diff --git a/libratbox/src/gnutls.c b/libratbox/src/gnutls.c index 17beb440..ec2c49fc 100644 --- a/libratbox/src/gnutls.c +++ b/libratbox/src/gnutls.c @@ -367,6 +367,73 @@ rb_load_file_into_datum_t(const char *const file) return datum; } +static int +make_certfp(gnutls_x509_crt_t cert, uint8_t certfp[const RB_SSL_CERTFP_LEN], const int method) +{ + int hashlen; + gnutls_digest_algorithm_t md_type; + + int spki = 0; + + switch(method) + { + case RB_SSL_CERTFP_METH_CERT_SHA1: + hashlen = RB_SSL_CERTFP_LEN_SHA1; + md_type = GNUTLS_DIG_SHA1; + break; + case RB_SSL_CERTFP_METH_SPKI_SHA256: + spki = 1; + case RB_SSL_CERTFP_METH_CERT_SHA256: + hashlen = RB_SSL_CERTFP_LEN_SHA256; + md_type = GNUTLS_DIG_SHA256; + break; + case RB_SSL_CERTFP_METH_SPKI_SHA512: + spki = 1; + case RB_SSL_CERTFP_METH_CERT_SHA512: + hashlen = RB_SSL_CERTFP_LEN_SHA512; + md_type = GNUTLS_DIG_SHA512; + break; + default: + return 0; + } + + if(! spki) + { + size_t digest_size = (size_t) hashlen; + + if(gnutls_x509_crt_get_fingerprint(cert, md_type, certfp, &digest_size) != 0) + return 0; + + return hashlen; + } + + gnutls_pubkey_t pubkey; + + if(gnutls_pubkey_init(&pubkey) != 0) + return 0; + + if(gnutls_pubkey_import_x509(pubkey, cert, 0) != 0) + { + gnutls_pubkey_deinit(pubkey); + return 0; + } + + unsigned char derkey[262144]; // Should be big enough to hold any SubjectPublicKeyInfo structure + size_t derkey_len = sizeof derkey; + + if(gnutls_pubkey_export(pubkey, GNUTLS_X509_FMT_DER, derkey, &derkey_len) != 0) + { + gnutls_pubkey_deinit(pubkey); + return 0; + } + + gnutls_pubkey_deinit(pubkey); + + if(gnutls_hash_fast(md_type, derkey, derkey_len, certfp) != 0) + return 0; + + return hashlen; +} /* @@ -589,53 +656,29 @@ rb_get_ssl_certfp(rb_fde_t *const F, uint8_t certfp[const RB_SSL_CERTFP_LEN], co if(F == NULL || F->ssl == NULL) return 0; - gnutls_digest_algorithm_t md_type; - - switch(method) - { - case RB_SSL_CERTFP_METH_SHA1: - md_type = GNUTLS_DIG_SHA1; - break; - case RB_SSL_CERTFP_METH_SHA256: - md_type = GNUTLS_DIG_SHA256; - break; - case RB_SSL_CERTFP_METH_SHA512: - md_type = GNUTLS_DIG_SHA512; - break; - default: - return 0; - } - if(gnutls_certificate_type_get(SSL_P(F)) != GNUTLS_CRT_X509) return 0; unsigned int cert_list_size = 0; const gnutls_datum_t *const cert_list = gnutls_certificate_get_peers(SSL_P(F), &cert_list_size); - if(cert_list == NULL || cert_list_size <= 0) + if(cert_list == NULL || cert_list_size < 1) return 0; gnutls_x509_crt_t peer_cert; - if(gnutls_x509_crt_init(&peer_cert) != GNUTLS_E_SUCCESS) + if(gnutls_x509_crt_init(&peer_cert) != 0) return 0; - if(gnutls_x509_crt_import(peer_cert, &cert_list[0], GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) + if(gnutls_x509_crt_import(peer_cert, &cert_list[0], GNUTLS_X509_FMT_DER) < 0) { gnutls_x509_crt_deinit(peer_cert); return 0; } - int ret; - size_t hashlen; - if((ret = gnutls_x509_crt_get_fingerprint(peer_cert, md_type, certfp, &hashlen)) != 0) - { - rb_lib_log("%s: gnutls_x509_crt_get_fingerprint: %s", __func__, rb_ssl_strerror(ret)); - gnutls_x509_crt_deinit(peer_cert); - return 0; - } + const int len = make_certfp(peer_cert, certfp, method); gnutls_x509_crt_deinit(peer_cert); - return (int) hashlen; + return len; } void diff --git a/libratbox/src/mbedtls.c b/libratbox/src/mbedtls.c index 89b97718..539a6673 100644 --- a/libratbox/src/mbedtls.c +++ b/libratbox/src/mbedtls.c @@ -325,6 +325,65 @@ rb_ssl_strerror(const int err) return errbuf; } +static int +rb_make_certfp(const mbedtls_x509_crt *const peer_cert, uint8_t certfp[const RB_SSL_CERTFP_LEN], const int method) +{ + size_t hashlen = 0; + mbedtls_md_type_t md_type; + int spki = 0; + + switch(method) + { + case RB_SSL_CERTFP_METH_CERT_SHA1: + md_type = MBEDTLS_MD_SHA1; + hashlen = RB_SSL_CERTFP_LEN_SHA1; + break; + case RB_SSL_CERTFP_METH_SPKI_SHA256: + spki = 1; + case RB_SSL_CERTFP_METH_CERT_SHA256: + md_type = MBEDTLS_MD_SHA256; + hashlen = RB_SSL_CERTFP_LEN_SHA256; + break; + case RB_SSL_CERTFP_METH_SPKI_SHA512: + spki = 1; + case RB_SSL_CERTFP_METH_CERT_SHA512: + md_type = MBEDTLS_MD_SHA512; + hashlen = RB_SSL_CERTFP_LEN_SHA512; + break; + default: + return 0; + } + + const mbedtls_md_info_t *const md_info = mbedtls_md_info_from_type(md_type); + if(md_info == NULL) + return 0; + + int ret; + void* data = peer_cert->raw.p; + size_t datalen = peer_cert->raw.len; + + if(spki) + { + unsigned char der_pubkey[8192]; + if((ret = mbedtls_pk_write_pubkey_der((mbedtls_pk_context *)&peer_cert->pk, + der_pubkey, sizeof der_pubkey)) < 0) + { + rb_lib_log("%s: pk_write_pubkey_der: %s", __func__, rb_ssl_strerror(ret)); + return 0; + } + data = der_pubkey + (sizeof(der_pubkey) - (size_t)ret); + datalen = (size_t)ret; + } + + if((ret = mbedtls_md(md_info, data, datalen, certfp)) != 0) + { + rb_lib_log("%s: mbedtls_md: %s", __func__, rb_ssl_strerror(ret)); + return 0; + } + + return (int) hashlen; +} + /* @@ -560,43 +619,12 @@ rb_get_ssl_certfp(rb_fde_t *const F, uint8_t certfp[const RB_SSL_CERTFP_LEN], co if(F == NULL || F->ssl == NULL) return 0; - mbedtls_md_type_t md_type; - int hashlen; - - switch(method) - { - case RB_SSL_CERTFP_METH_SHA1: - md_type = MBEDTLS_MD_SHA1; - hashlen = RB_SSL_CERTFP_LEN_SHA1; - break; - case RB_SSL_CERTFP_METH_SHA256: - md_type = MBEDTLS_MD_SHA256; - hashlen = RB_SSL_CERTFP_LEN_SHA256; - break; - case RB_SSL_CERTFP_METH_SHA512: - md_type = MBEDTLS_MD_SHA512; - hashlen = RB_SSL_CERTFP_LEN_SHA512; - break; - default: - return 0; - } - const mbedtls_x509_crt *const peer_cert = mbedtls_ssl_get_peer_cert(SSL_P(F)); + if(peer_cert == NULL) return 0; - const mbedtls_md_info_t *const md_info = mbedtls_md_info_from_type(md_type); - if(md_info == NULL) - return 0; - - int ret; - if((ret = mbedtls_md(md_info, peer_cert->raw.p, peer_cert->raw.len, certfp)) != 0) - { - rb_lib_log("%s: mbedtls_md: %s", __func__, rb_ssl_strerror(ret)); - return 0; - } - - return hashlen; + return rb_make_certfp(peer_cert, certfp, method); } void diff --git a/libratbox/src/openssl.c b/libratbox/src/openssl.c index 851668b8..0a5e183d 100644 --- a/libratbox/src/openssl.c +++ b/libratbox/src/openssl.c @@ -245,6 +245,59 @@ rb_ssl_read_or_write(const int r_or_w, rb_fde_t *const F, void *const rbuf, cons return ret; } +static int +make_certfp(X509 *const cert, uint8_t certfp[const RB_SSL_CERTFP_LEN], const int method) +{ + unsigned int hashlen = 0; + const EVP_MD *md_type = NULL; + const ASN1_ITEM *item = NULL; + void *data = NULL; + + switch(method) + { + case RB_SSL_CERTFP_METH_CERT_SHA1: + hashlen = RB_SSL_CERTFP_LEN_SHA1; + md_type = EVP_sha1(); + item = ASN1_ITEM_rptr(X509); + data = cert; + break; + case RB_SSL_CERTFP_METH_CERT_SHA256: + hashlen = RB_SSL_CERTFP_LEN_SHA256; + md_type = EVP_sha256(); + item = ASN1_ITEM_rptr(X509); + data = cert; + break; + case RB_SSL_CERTFP_METH_CERT_SHA512: + hashlen = RB_SSL_CERTFP_LEN_SHA512; + md_type = EVP_sha512(); + item = ASN1_ITEM_rptr(X509); + data = cert; + break; + case RB_SSL_CERTFP_METH_SPKI_SHA256: + hashlen = RB_SSL_CERTFP_LEN_SHA256; + md_type = EVP_sha256(); + item = ASN1_ITEM_rptr(X509_PUBKEY); + data = X509_get_X509_PUBKEY(cert); + break; + case RB_SSL_CERTFP_METH_SPKI_SHA512: + hashlen = RB_SSL_CERTFP_LEN_SHA512; + md_type = EVP_sha512(); + item = ASN1_ITEM_rptr(X509_PUBKEY); + data = X509_get_X509_PUBKEY(cert); + break; + default: + return 0; + } + + if(ASN1_item_digest(item, md_type, data, certfp, &hashlen) != 1) + { + rb_lib_log("%s: ASN1_item_digest: %s", __func__, rb_ssl_strerror(rb_ssl_last_err())); + return 0; + } + + return (int) hashlen; +} + /* @@ -470,31 +523,12 @@ rb_get_ssl_certfp(rb_fde_t *const F, uint8_t certfp[const RB_SSL_CERTFP_LEN], co if(F == NULL || F->ssl == NULL) return 0; - const EVP_MD *md_type; - unsigned int hashlen; - - switch(method) - { - case RB_SSL_CERTFP_METH_SHA1: - md_type = EVP_sha1(); - hashlen = RB_SSL_CERTFP_LEN_SHA1; - break; - case RB_SSL_CERTFP_METH_SHA256: - md_type = EVP_sha256(); - hashlen = RB_SSL_CERTFP_LEN_SHA256; - break; - case RB_SSL_CERTFP_METH_SHA512: - md_type = EVP_sha512(); - hashlen = RB_SSL_CERTFP_LEN_SHA512; - break; - default: - return 0; - } - X509 *const peer_cert = SSL_get_peer_certificate(SSL_P(F)); if(peer_cert == NULL) return 0; + int len = 0; + switch(SSL_get_verify_result(SSL_P(F))) { case X509_V_OK: @@ -503,16 +537,11 @@ rb_get_ssl_certfp(rb_fde_t *const F, uint8_t certfp[const RB_SSL_CERTFP_LEN], co case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: case X509_V_ERR_CERT_UNTRUSTED: - break; + len = make_certfp(peer_cert, certfp, method); default: X509_free(peer_cert); - return 0; + return len; } - - X509_digest(peer_cert, md_type, certfp, &hashlen); - X509_free(peer_cert); - - return (int) hashlen; } void diff --git a/src/newconf.c b/src/newconf.c index 717cfcf4..9edcb262 100644 --- a/src/newconf.c +++ b/src/newconf.c @@ -1650,15 +1650,19 @@ conf_set_general_certfp_method(void *data) { char *method = data; - if (!strcasecmp(method, "sha1")) - ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_SHA1; - else if (!strcasecmp(method, "sha256")) - ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_SHA256; - else if (!strcasecmp(method, "sha512")) - ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_SHA512; + if (!strcasecmp(method, CERTFP_NAME_CERT_SHA1)) + ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_CERT_SHA1; + else if (!strcasecmp(method, CERTFP_NAME_CERT_SHA256)) + ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_CERT_SHA256; + else if (!strcasecmp(method, CERTFP_NAME_CERT_SHA512)) + ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_CERT_SHA512; + else if (!strcasecmp(method, CERTFP_NAME_SPKI_SHA256)) + ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_SPKI_SHA256; + else if (!strcasecmp(method, CERTFP_NAME_SPKI_SHA512)) + ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_SPKI_SHA512; else { - ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_SHA1; + ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_CERT_SHA1; conf_report_error("Ignoring general::certfp_method -- bogus certfp method %s", method); } } diff --git a/src/s_conf.c b/src/s_conf.c index 305a673b..dde9a67f 100644 --- a/src/s_conf.c +++ b/src/s_conf.c @@ -822,7 +822,7 @@ set_default_conf(void) ServerInfo.default_max_clients = MAXCONNECTIONS; ConfigFileEntry.nicklen = NICKLEN; - ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_SHA1; + ConfigFileEntry.certfp_method = RB_SSL_CERTFP_METH_CERT_SHA1; if (!alias_dict) alias_dict = irc_dictionary_create(strcasecmp); @@ -877,7 +877,7 @@ validate_conf(void) ircd_ssl_ok = 0; } else { ircd_ssl_ok = 1; - send_new_ssl_certs(ServerInfo.ssl_cert, ServerInfo.ssl_private_key, ServerInfo.ssl_dh_params, ServerInfo.ssl_cipher_list); + send_new_ssl_certs(ServerInfo.ssl_cert, ServerInfo.ssl_private_key, ServerInfo.ssl_dh_params, ServerInfo.ssl_cipher_list, ConfigFileEntry.certfp_method); } if(ServerInfo.ssld_count > get_ssld_count()) diff --git a/src/sslproc.c b/src/sslproc.c index de94646e..f34619b8 100644 --- a/src/sslproc.c +++ b/src/sslproc.c @@ -409,24 +409,50 @@ ssl_process_certfp(ssl_ctl_t * ctl, ssl_ctl_buf_t * ctl_buf) { struct Client *client_p; uint32_t fd; + uint32_t certfp_method; uint32_t len; uint8_t *certfp; char *certfp_string; - int i; + const char *method_string; + int method_len; - if(ctl_buf->buflen > 9 + RB_SSL_CERTFP_LEN) + if(ctl_buf->buflen > 13 + RB_SSL_CERTFP_LEN) return; /* bogus message..drop it.. XXX should warn here */ fd = buf_to_uint32(&ctl_buf->buf[1]); - len = buf_to_uint32(&ctl_buf->buf[5]); - certfp = (uint8_t *)&ctl_buf->buf[9]; + certfp_method = buf_to_uint32(&ctl_buf->buf[5]); + len = buf_to_uint32(&ctl_buf->buf[9]); + certfp = (uint8_t *)&ctl_buf->buf[13]; client_p = find_cli_connid_hash(fd); if(client_p == NULL) return; + + switch (certfp_method) { + case RB_SSL_CERTFP_METH_CERT_SHA1: + method_string = CERTFP_PREFIX_CERT_SHA1; + break; + case RB_SSL_CERTFP_METH_CERT_SHA256: + method_string = CERTFP_PREFIX_CERT_SHA256; + break; + case RB_SSL_CERTFP_METH_CERT_SHA512: + method_string = CERTFP_PREFIX_CERT_SHA512; + break; + case RB_SSL_CERTFP_METH_SPKI_SHA256: + method_string = CERTFP_PREFIX_SPKI_SHA256; + break; + case RB_SSL_CERTFP_METH_SPKI_SHA512: + method_string = CERTFP_PREFIX_SPKI_SHA512; + break; + default: + return; + } + method_len = strlen(method_string); + rb_free(client_p->certfp); - certfp_string = rb_malloc(len * 2 + 1); - for(i = 0; i < len; i++) - rb_snprintf(certfp_string + 2 * i, 3, "%02x", + certfp_string = rb_malloc(method_len + len * 2 + 1); + rb_strlcpy(certfp_string, method_string, method_len + len * 2 + 1); + for(uint32_t i = 0; i < len; i++) + rb_snprintf(certfp_string + method_len + 2 * i, 3, "%02x", certfp[i]); client_p->certfp = certfp_string; } @@ -674,7 +700,7 @@ send_certfp_method(ssl_ctl_t *ctl, int method) } void -send_new_ssl_certs(const char *ssl_cert, const char *ssl_private_key, const char *ssl_dh_params, const char *ssl_cipher_list) +send_new_ssl_certs(const char *ssl_cert, const char *ssl_private_key, const char *ssl_dh_params, const char *ssl_cipher_list, const int method) { rb_dlink_node *ptr; if(ssl_cert == NULL) @@ -685,6 +711,7 @@ send_new_ssl_certs(const char *ssl_cert, const char *ssl_private_key, const char RB_DLINK_FOREACH(ptr, ssl_daemons.head) { ssl_ctl_t *ctl = ptr->data; + send_certfp_method(ctl, method); send_new_ssl_certs_one(ctl, ssl_cert, ssl_private_key, ssl_dh_params, ssl_cipher_list); } } diff --git a/ssld/ssld.c b/ssld/ssld.c index a680b790..090570d7 100644 --- a/ssld/ssld.c +++ b/ssld/ssld.c @@ -154,7 +154,7 @@ static void conn_plain_read_shutdown_cb(rb_fde_t *fd, void *data); static void mod_cmd_write_queue(mod_ctl_t * ctl, const void *data, size_t len); static const char *remote_closed = "Remote host closed the connection"; static int ssld_ssl_ok; -static int certfp_method = RB_SSL_CERTFP_METH_SHA1; +static int certfp_method = RB_SSL_CERTFP_METH_CERT_SHA1; #ifdef HAVE_LIBZ static int ssld_zlib_ok = 1; #else @@ -688,17 +688,18 @@ ssl_send_cipher(conn_t *conn) static void ssl_send_certfp(conn_t *conn) { - uint8_t buf[9 + RB_SSL_CERTFP_LEN]; + uint8_t buf[13 + RB_SSL_CERTFP_LEN]; - int len = rb_get_ssl_certfp(conn->mod_fd, &buf[9], certfp_method); + int len = rb_get_ssl_certfp(conn->mod_fd, &buf[13], certfp_method); if (!len) return; lrb_assert(len <= RB_SSL_CERTFP_LEN); buf[0] = 'F'; uint32_to_buf(&buf[1], conn->id); - uint32_to_buf(&buf[5], len); - mod_cmd_write_queue(conn->ctl, buf, 9 + len); + uint32_to_buf(&buf[5], certfp_method); + uint32_to_buf(&buf[9], len); + mod_cmd_write_queue(conn->ctl, buf, 13 + len); } static void