DPDK patches and discussions
 help / color / mirror / Atom feed
From: Mairtin o Loingsigh <mairtin.oloingsigh@intel.com>
To: shreyansh.jain@nxp.com
Cc: dev@dpdk.org, Mairtin o Loingsigh <mairtin.oloingsigh@intel.com>
Subject: [dpdk-dev] [PATCH] test/rawdev: add Multi-function test
Date: Mon, 16 Mar 2020 20:03:43 +0000	[thread overview]
Message-ID: <1584389023-32456-1-git-send-email-mairtin.oloingsigh@intel.com> (raw)

Function test for Multi-function library

Signed-off-by: Mairtin o Loingsigh <mairtin.oloingsigh@intel.com>
---
 app/test/Makefile                            |    5 +
 app/test/test_cryptodev.h                    |    1 +
 app/test/test_rawdev.c                       |  418 ++++++++++++++++++++++++++
 app/test/test_rawdev_multi_fn_test_vectors.h |   83 +++++
 4 files changed, 507 insertions(+), 0 deletions(-)
 create mode 100644 app/test/test_rawdev_multi_fn_test_vectors.h

diff --git a/app/test/Makefile b/app/test/Makefile
index 1f080d1..57e9b23 100644
--- a/app/test/Makefile
+++ b/app/test/Makefile
@@ -243,6 +243,7 @@ CFLAGS += -O3
 CFLAGS += $(WERROR_FLAGS)
 
 LDLIBS += -lm
+LDLIBS += -lrte_multi_fn
 
 ifeq ($(CONFIG_RTE_LIBRTE_PDUMP),y)
 LDLIBS += -lpthread
@@ -286,6 +287,10 @@ ifeq ($(CONFIG_RTE_APP_TEST_RESOURCE_TAR),y)
 LDLIBS += -larchive
 endif
 
+ifeq ($(CONFIG_RTE_LIBRTE_PMD_AESNI_MB_RAWDEV),y)
+LDLIBS += -lrte_pmd_aesni_mb_rawdev
+endif
+
 include $(RTE_SDK)/mk/rte.app.mk
 
 endif
diff --git a/app/test/test_cryptodev.h b/app/test/test_cryptodev.h
index def304e..424184b 100644
--- a/app/test/test_cryptodev.h
+++ b/app/test/test_cryptodev.h
@@ -69,6 +69,7 @@
 #define CRYPTODEV_NAME_OCTEONTX2_PMD	crypto_octeontx2
 #define CRYPTODEV_NAME_CAAM_JR_PMD	crypto_caam_jr
 #define CRYPTODEV_NAME_NITROX_PMD	crypto_nitrox_sym
+#define RAWDEV_NAME_AESNI_MB_PMD	rawdev_aesni_mb
 
 /**
  * Write (spread) data from buffer to mbuf data
diff --git a/app/test/test_rawdev.c b/app/test/test_rawdev.c
index 524a9d5..a1d03f9 100644
--- a/app/test/test_rawdev.c
+++ b/app/test/test_rawdev.c
@@ -6,10 +6,38 @@
 #include <rte_malloc.h>
 #include <rte_memcpy.h>
 #include <rte_dev.h>
+#include <rte_crypto.h>
 #include <rte_rawdev.h>
+#include <rte_multi_fn.h>
 #include <rte_bus_vdev.h>
 
+#include "test_rawdev_multi_fn_test_vectors.h"
 #include "test.h"
+#include "test_cryptodev.h"
+
+struct raw_testsuite_params {
+	struct rte_mempool *mbuf_pool;
+	struct rte_mempool *op_mpool;
+	struct rte_rawdev_info info;
+	struct rte_multi_fn_device_info priv;
+	struct rte_multi_fn_dev_config conf;
+	struct rte_multi_fn_qp_config qp_conf;
+	uint8_t valid_devs[RTE_RAWDEV_MAX_DEVS];
+	uint8_t valid_dev_count;
+};
+
+struct raw_unittest_params {
+	struct rte_crypto_sym_xform cipher_xform;
+	struct rte_crypto_sym_xform auth_xform;
+	struct rte_crypto_sym_xform aead_xform;
+	struct rte_multi_fn_err_detect_xform err_detect;
+	struct rte_multi_fn_session *sess;
+
+	struct rte_crypto_op *op;
+	struct rte_mbuf *obuf, *ibuf;
+
+	uint8_t *digest;
+};
 
 static int
 test_rawdev_selftest_impl(const char *pmd, const char *opts)
@@ -45,3 +73,393 @@
 }
 
 REGISTER_TEST_COMMAND(ioat_rawdev_autotest, test_rawdev_selftest_ioat);
+
+static struct raw_testsuite_params testsuite_raw_params = { NULL };
+static struct raw_unittest_params unittest_raw_params;
+
+static int
+testsuite_raw_setup(void)
+{
+	int ret;
+	uint32_t i = 0, nb_devs, dev_id;
+	struct raw_testsuite_params *ts_params = &testsuite_raw_params;
+	struct rte_rawdev_info *info = &ts_params->info;
+	struct rte_multi_fn_device_info *priv = &ts_params->priv;
+
+	memset(ts_params, 0, sizeof(*ts_params));
+
+	info->dev_private = priv;
+	priv->config = &ts_params->conf;
+
+	ts_params->mbuf_pool = rte_mempool_lookup("RAWDEV_MBUFPOOL");
+	if (ts_params->mbuf_pool == NULL) {
+		/* Not already created so create */
+		ts_params->mbuf_pool = rte_pktmbuf_pool_create(
+				"RAWDEV_MBUFPOOL",
+				NUM_MBUFS, MBUF_CACHE_SIZE, 0, MBUF_SIZE,
+				rte_socket_id());
+		if (ts_params->mbuf_pool == NULL) {
+			RTE_LOG(ERR, USER1, "Can't create RAWDEV_MBUFPOOL\n");
+			return TEST_FAILED;
+		}
+	}
+
+	ts_params->op_mpool = rte_pktmbuf_pool_create("RAWDEV_MULTI_FN_OP_POOL",
+			NUM_MBUFS, MBUF_CACHE_SIZE, 0,
+			sizeof(struct rte_multi_fn_op) +
+			MAXIMUM_IV_LENGTH,
+			rte_socket_id());
+
+	if (ts_params->op_mpool == NULL) {
+		RTE_LOG(ERR, USER1, "Can't create RAWDEV_MULTI_FN_OP_POOL\n");
+		return TEST_FAILED;
+	}
+
+	/* Find 1st aesni rawdev. */
+	for (i = 0; i < RTE_RAWDEV_MAX_DEVS; i++)
+		if (rte_rawdevs[i].driver_name &&
+		    (strncmp(rte_rawdevs[i].driver_name, "rawdev_aesni_mb",
+		    RTE_RAWDEV_NAME_MAX_LEN) == 0))
+			break;
+
+	if (i == RTE_RAWDEV_MAX_DEVS)
+		rte_exit(EXIT_FAILURE, "Cannot find any ntb device.\n");
+
+	nb_devs = rte_rawdev_count();
+	if (nb_devs < 1) {
+		RTE_LOG(WARNING, USER1, "No rawdev devices found?\n");
+		return TEST_SKIPPED;
+	}
+
+	/* Create list of valid crypto devs */
+	for (i = 0; i < nb_devs; i++) {
+		rte_rawdev_info_get(i, info);
+		if (strncmp(rte_rawdevs[i].driver_name, "rawdev_aesni_mb",
+				RTE_RAWDEV_NAME_MAX_LEN) == 0)
+			ts_params->valid_devs[ts_params->valid_dev_count++]
+					      = i;
+	}
+
+	if (ts_params->valid_dev_count < 1)
+		return TEST_FAILED;
+
+	/* Set up all the qps on the first of the valid devices found */
+
+	dev_id = ts_params->valid_devs[0];
+
+	ret = rte_rawdev_info_get(dev_id, info);
+	if (ret)
+		return ret;
+
+	return TEST_SUCCESS;
+}
+
+static void
+testsuite_raw_teardown(void)
+{
+	struct raw_testsuite_params *ts_params = &testsuite_raw_params;
+
+	if (ts_params->mbuf_pool != NULL) {
+		RTE_LOG(DEBUG, USER1, "CRYPTO_MBUFPOOL count %u\n",
+		rte_mempool_avail_count(ts_params->mbuf_pool));
+		rte_mempool_free(ts_params->mbuf_pool);
+		ts_params->mbuf_pool = NULL;
+	}
+
+	if (ts_params->op_mpool != NULL) {
+		RTE_LOG(DEBUG, USER1, "CRYPTO_OP_POOL count %u\n",
+		rte_mempool_avail_count(ts_params->op_mpool));
+		rte_mempool_free(ts_params->op_mpool);
+		ts_params->op_mpool = NULL;
+	}
+
+	rte_vdev_uninit(RTE_STR(RAWDEV_NAME_AESNI_MB_PMD));
+}
+
+static int
+ut_raw_setup(void)
+{
+	int ret;
+	struct raw_testsuite_params *ts_params = &testsuite_raw_params;
+	struct raw_unittest_params *ut_params = &unittest_raw_params;
+
+	uint16_t qp_id;
+
+	/* Clear unit test parameters before running test */
+	memset(ut_params, 0, sizeof(*ut_params));
+
+	/* Reconfigure device to default parameters */
+	ts_params->qp_conf.nb_descriptors = MAX_NUM_OPS_INFLIGHT;
+
+	TEST_ASSERT_SUCCESS(rte_rawdev_configure(ts_params->valid_devs[0],
+			&ts_params->info),
+			"Failed to configure rawdev %u",
+			ts_params->valid_devs[0]);
+
+	for (qp_id = 0; qp_id < ts_params->conf.nb_queues ; qp_id++) {
+		TEST_ASSERT_SUCCESS(rte_rawdev_queue_setup(
+			ts_params->valid_devs[0], qp_id,
+			&ts_params->qp_conf),
+			"Failed to setup queue pair %u on rawdev %u",
+			qp_id, ts_params->valid_devs[0]);
+	}
+
+	ret = rte_rawdev_xstats_reset(ts_params->valid_devs[0], NULL, 0);
+
+	TEST_ASSERT_SUCCESS(ret, "Failed to reset rawdev stats");
+
+	/* Start the device */
+	TEST_ASSERT_SUCCESS(rte_rawdev_start(ts_params->valid_devs[0]),
+			"Failed to start rawdev %u", ts_params->valid_devs[0]);
+
+	return 0;
+}
+
+static void
+ut_raw_teardown(void)
+{
+	struct raw_testsuite_params *ts_params = &testsuite_raw_params;
+	struct raw_unittest_params *ut_params = &unittest_raw_params;
+
+	/* free crypto operation structure */
+	if (ut_params->op)
+		rte_crypto_op_free(ut_params->op);
+
+	/*
+	 * free mbuf - both obuf and ibuf are usually the same,
+	 * so check if they point at the same address is necessary,
+	 * to avoid freeing the mbuf twice.
+	 */
+	if (ut_params->obuf) {
+		rte_pktmbuf_free(ut_params->obuf);
+		if (ut_params->ibuf == ut_params->obuf)
+			ut_params->ibuf = 0;
+		ut_params->obuf = 0;
+	}
+	if (ut_params->ibuf) {
+		rte_pktmbuf_free(ut_params->ibuf);
+		ut_params->ibuf = 0;
+	}
+
+	if (ts_params->mbuf_pool != NULL)
+		RTE_LOG(DEBUG, USER1, "CRYPTO_MBUFPOOL count %u\n",
+			rte_mempool_avail_count(ts_params->mbuf_pool));
+
+	/* Stop the device */
+	rte_rawdev_stop(ts_params->valid_devs[0]);
+}
+
+static int
+test_session_create_docsis_dl(struct docsis_test_data *d_tc)
+{
+	uint16_t qpid = 0, enqueued, dequeued = 0;
+	uint16_t output_vec_len = 16;
+	struct rte_multi_fn_op *ops[2];
+	struct rte_multi_fn_op *results;
+	struct rte_multi_fn_xform xfrm1 = {0};
+	struct rte_multi_fn_xform xfrm2 = {0};
+	uint8_t *plaintext = NULL, *ciphertext = NULL;
+	struct rte_mbuf *m1, *m2;
+
+	/* Operations */
+	struct rte_crypto_sym_op *crypto_sym_op;
+	struct rte_multi_fn_err_detect_op *err_detect_op;
+	struct raw_testsuite_params *t_param = &testsuite_raw_params;
+	struct raw_unittest_params *u_param = &unittest_raw_params;
+
+	int i, ret = TEST_SUCCESS;
+	int oop = 0;
+
+	uint8_t key_128bit[16] = {0};
+	uint64_t stats[4] = {0};
+	const unsigned int stats_id[4] = {1, 2, 3, 4};
+	int num_stats = 0;
+
+	/* Docsis test params */
+	uint8_t *cipher_iv = NULL;
+	uint8_t cipher_iv_len = 0;
+	unsigned int cipher_len = 0, auth_len = 0;
+
+	uint8_t dev_id = t_param->valid_devs[0];
+
+	/* Copy key from test vector */
+	memcpy(key_128bit, d_tc->key.data, d_tc->key.len);
+
+	debug_hexdump(stdout, "Key:", key_128bit, d_tc->key.len);
+
+	/* multi-operation type session creation */
+	xfrm1.type = RTE_MULTI_FN_XFORM_TYPE_CRYPTO_SYM;
+	xfrm1.next = &xfrm2;
+
+	xfrm1.crypto_sym.type = RTE_CRYPTO_SYM_XFORM_CIPHER;
+
+	struct rte_crypto_cipher_xform *xfrm_cipher = &xfrm1.crypto_sym.cipher;
+
+	xfrm_cipher->op = RTE_CRYPTO_CIPHER_OP_DECRYPT;
+	xfrm_cipher->algo = RTE_CRYPTO_CIPHER_AES_DOCSISBPI;
+	xfrm_cipher->key.data = key_128bit;
+	xfrm_cipher->key.length = RTE_DIM(key_128bit);
+	/*sizeof(key_128bit)/sizeof(key_128bit[0]);*/
+	xfrm_cipher->iv.offset = sizeof(struct rte_multi_fn_op);
+	xfrm_cipher->iv.length = d_tc->cipher_iv.len;
+
+	xfrm2.type = RTE_MULTI_FN_XFORM_TYPE_ERR_DETECT;
+	xfrm2.err_detect.algo = RTE_MULTI_FN_ERR_DETECT_CRC32_ETH;
+	xfrm2.err_detect.op = RTE_MULTI_FN_ERR_DETECT_OP_VERIFY;
+	xfrm2.next = NULL;
+
+	m1 = rte_pktmbuf_alloc(t_param->mbuf_pool);
+	if (!m1) {
+		printf("rte_pktmbuf_alloc failed\n");
+		return -1;
+	}
+
+	/* clear mbuf payload */
+	memset(rte_pktmbuf_mtod(m1, uint8_t *), 0, rte_pktmbuf_tailroom(m1));
+
+	if (oop) {
+		m2 = rte_pktmbuf_alloc(t_param->mbuf_pool);
+		rte_pktmbuf_append(m2, output_vec_len);
+	}
+
+	ciphertext = (uint8_t *)rte_pktmbuf_append(m1, d_tc->ciphertext.len);
+
+	memcpy(ciphertext, d_tc->ciphertext.data, d_tc->ciphertext.len);
+
+	debug_hexdump(stdout, "ciphertext:", ciphertext, d_tc->ciphertext.len);
+
+	u_param->sess = rte_multi_fn_session_create(dev_id,
+			&t_param->info, &xfrm1, rte_socket_id());
+
+	if ((u_param->sess == NULL) ||
+			(u_param->sess->sess_private_data == NULL)) {
+		printf("rte_multi_fn_session_create create failed\n");
+		return -1;
+	}
+
+	/* Create combined DOCSIS operation */
+	cipher_iv = d_tc->cipher_iv.data;
+	cipher_iv_len = d_tc->cipher_iv.len;
+
+	cipher_len = d_tc->ciphertext.no_cipher == false ?
+					(d_tc->ciphertext.len -
+					d_tc->ciphertext.cipher_offset) :
+					0;
+	auth_len = d_tc->ciphertext.no_auth == false ? (d_tc->ciphertext.len -
+						d_tc->ciphertext.auth_offset -
+						4) :	0;
+
+	ret = rte_mempool_get_bulk(t_param->op_mpool, (void **)ops, 2);
+	if (ret) {
+		printf("rte_mempool_get_bulk failed to alloc ops\n");
+		return -1;
+	}
+
+	ops[0]->next = ops[1];
+	ops[0]->m_src = m1;
+	ops[0]->m_dst = NULL;
+
+	uint8_t *iv_ptr = (uint8_t *)ops[0] + sizeof(
+		struct rte_multi_fn_op);
+	rte_memcpy(iv_ptr, cipher_iv, cipher_iv_len);
+
+	debug_hexdump(stdout, "iv:", iv_ptr, cipher_iv_len);
+
+	/* crypto decrypt op config */
+	crypto_sym_op = &ops[0]->crypto_sym;
+	crypto_sym_op->cipher.data.offset = d_tc->ciphertext.cipher_offset;
+	crypto_sym_op->cipher.data.length = cipher_len;
+
+	/* error detect op config */
+	err_detect_op = &ops[1]->err_detect;
+	err_detect_op->data.offset = d_tc->ciphertext.auth_offset;
+	err_detect_op->data.length = auth_len;
+
+	/* Attach session to op */
+	ops[0]->sess = u_param->sess;
+
+	enqueued = rte_rawdev_enqueue_buffers(dev_id,
+			(struct rte_rawdev_buf **) ops, 1,
+			(rte_rawdev_obj_t)&qpid);
+
+	if (enqueued != 1)
+		printf("rte_accelerator_ops_enqueue failed\n");
+
+	do {
+		dequeued = rte_rawdev_dequeue_buffers(dev_id,
+				(struct rte_rawdev_buf **)&results, 1,
+				(rte_rawdev_obj_t)&qpid);
+	} while (dequeued < 1);
+
+	/* Check results. in-place operation */
+	plaintext = ciphertext;
+	debug_hexdump(stdout, "plaintext:", plaintext, d_tc->plaintext.len);
+
+	/* Validate obuf */
+	TEST_ASSERT_BUFFERS_ARE_EQUAL(
+			plaintext,
+			d_tc->plaintext.data,
+			/* Check only plaintext, CRC is checked internally */
+			d_tc->plaintext.len - 4,
+			"DOCSIS Plaintext data not as expected");
+
+	TEST_ASSERT_EQUAL(results->op_status,
+			RTE_MULTI_FN_OP_STATUS_SUCCESS,
+			"crypto op processing failed");
+
+	num_stats = rte_rawdev_xstats_get(dev_id, stats_id, stats, 4);
+	for (i = 0; i < num_stats; i++)
+		printf("Stat num: %d = %"PRIu64"\n", i, stats[i]);
+
+	ret = rte_multi_fn_session_destroy(dev_id, &t_param->info,
+			u_param->sess);
+
+	return ret;
+}
+
+static int
+test_device_configure_docsis_decrypt(void)
+{
+	return test_session_create_docsis_dl(&docsis_test_case_1);
+}
+
+static struct unit_test_suite rawdev_aesni_testsuite  = {
+	.suite_name = "Raw Unit Test Suite",
+	.setup = testsuite_raw_setup,
+	.teardown = testsuite_raw_teardown,
+	.unit_test_cases = {
+		TEST_CASE_ST(ut_raw_setup, ut_raw_teardown,
+				test_device_configure_docsis_decrypt),
+		TEST_CASES_END() /**< NULL terminate unit test array */
+	}
+};
+
+static int
+test_rawdev_aesni(void /*argv __rte_unused, int argc __rte_unused*/)
+{
+	int i, ret;
+
+	ret = rte_vdev_init(RTE_STR(RAWDEV_NAME_AESNI_MB_PMD), NULL);
+	if (ret) {
+		RTE_LOG(ERR, USER1, "aesni raw vdev init failed\n");
+		return ret;
+	}
+
+	/* Find 1st ntb rawdev. */
+	for (i = 0; i < RTE_RAWDEV_MAX_DEVS; i++)
+		if (rte_rawdevs[i].driver_name &&
+		    (strncmp(rte_rawdevs[i].driver_name, "rawdev_aesni_mb",
+			RTE_RAWDEV_NAME_MAX_LEN) == 0) &&
+			(rte_rawdevs[i].attached == 1))
+			break;
+
+	if (i == RTE_RAWDEV_MAX_DEVS) {
+		RTE_LOG(ERR, USER1, "aesni raw driver needed to run test\n");
+		return TEST_SKIPPED;
+	}
+
+	return unit_test_suite_runner(&rawdev_aesni_testsuite);
+}
+
+REGISTER_TEST_COMMAND(rawdev_aesni_autotest, test_rawdev_aesni);
+
diff --git a/app/test/test_rawdev_multi_fn_test_vectors.h b/app/test/test_rawdev_multi_fn_test_vectors.h
new file mode 100644
index 0000000..f60400d
--- /dev/null
+++ b/app/test/test_rawdev_multi_fn_test_vectors.h
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (C) 2015-2016 Freescale Semiconductor,Inc.
+ * Copyright 2018-2019 NXP
+ */
+
+#ifndef TEST_RAWDEV_MULTI_FN_TEST_VECTORS_H_
+#define TEST_RAWDEV_MULTI_FN_TEST_VECTORS_H_
+
+#include <stdbool.h>
+
+struct docsis_test_data {
+	struct {
+		uint8_t data[16];
+		unsigned int len;
+	} key;
+
+	struct {
+		uint8_t data[16] __rte_aligned(16);
+		unsigned int len;
+	} cipher_iv;
+
+	struct {
+		uint8_t data[1024];
+		unsigned int len;
+		unsigned int cipher_offset;
+		unsigned int auth_offset;
+		bool no_cipher;
+		bool no_auth;
+	} plaintext;
+
+	struct {
+		uint8_t data[1024];
+		unsigned int len;
+		unsigned int cipher_offset;
+		unsigned int auth_offset;
+		bool no_cipher;
+		bool no_auth;
+	} ciphertext;
+};
+
+struct docsis_test_data docsis_test_case_1 = {
+	.key = {
+		.data = {
+			0x00, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD,
+			0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55
+		},
+		.len = 16
+	},
+	.cipher_iv = {
+		.data = {
+			0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+			0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11
+		},
+		.len = 16
+	},
+	.plaintext = {
+		.data = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02,
+			0x03, 0x04, 0x05, 0x06, 0x06, 0x05, 0x04, 0x03,
+			0x02, 0x01, 0x08, 0x00, 0xFF, 0xFF, 0xFF, 0xFF
+		},
+		.len = 24,
+		.cipher_offset = 18,
+		.auth_offset = 6,
+		.no_cipher = false,
+		.no_auth = false
+	},
+	.ciphertext = {
+		.data = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02,
+			0x03, 0x04, 0x05, 0x06, 0x06, 0x05, 0x04, 0x03,
+			0x02, 0x01, 0x7A, 0xF0, 0x61, 0xF8, 0x63, 0x42
+		},
+		.len = 24,
+		.cipher_offset = 18,
+		.auth_offset = 6,
+		.no_cipher = false,
+		.no_auth = false
+	}
+};
+
+#endif /* TEST_RAWDEV_MULTI_FN_TEST_VECTORS_H_ */
-- 
1.7.0.7


                 reply	other threads:[~2020-03-16 20:03 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=1584389023-32456-1-git-send-email-mairtin.oloingsigh@intel.com \
    --to=mairtin.oloingsigh@intel.com \
    --cc=dev@dpdk.org \
    --cc=shreyansh.jain@nxp.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).