DPDK patches and discussions
 help / color / mirror / Atom feed
From: Konstantin Ananyev <konstantin.ananyev@huawei.com>
To: Stephen Hemminger <stephen@networkplumber.org>,
	"dev@dpdk.org" <dev@dpdk.org>
Cc: Marat Khalili <marat.khalili@huawei.com>
Subject: RE: [PATCH v6 2/2] bpf: add test for Rx and Tx filtering
Date: Wed, 12 Nov 2025 15:08:09 +0000	[thread overview]
Message-ID: <bb1bce77ccfd46b5850439be9bcb5607@huawei.com> (raw)
In-Reply-To: <20251111225719.540140-3-stephen@networkplumber.org>



> 
> New test using null device to test filtering with BPF.
> 
> If libelf library is not available, then DPDK bpf
> will return -ENOTSUP to the test and the test will be skipped.
> 
> Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
> Acked-by: Marat Khalili <marat.khalili@huawei.com>
> ---
>  app/test/bpf/filter.c    |  53 +++++++
>  app/test/bpf/meson.build |   1 +
>  app/test/test_bpf.c      | 321 ++++++++++++++++++++++++++++++++++++++-
>  3 files changed, 373 insertions(+), 2 deletions(-)
>  create mode 100644 app/test/bpf/filter.c
> 
> diff --git a/app/test/bpf/filter.c b/app/test/bpf/filter.c
> new file mode 100644
> index 0000000000..d47233a47a
> --- /dev/null
> +++ b/app/test/bpf/filter.c
> @@ -0,0 +1,53 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * BPF TX filter program for testing rte_bpf_eth_tx_elf_load
> + */
> +
> +typedef unsigned char uint8_t;
> +typedef unsigned short uint16_t;
> +typedef unsigned int uint32_t;
> +typedef unsigned long uint64_t;
> +
> +/*
> + * Simple TX filter that accepts TCP packets
> + *
> + * BPF TX programs receive pointer to data and should return:
> + *   0 = drop packet
> + *   non-zero = rx/tx packet
> + *
> + * This filter checks:
> + * 1. Packet is IPv4
> + * 2. Protocol is TCP (IPPROTO_TCP = 6)
> + */
> +__attribute__((section("filter"), used))
> +uint64_t
> +test_filter(void *pkt)
> +{
> +	uint8_t *data = pkt;
> +
> +	/* Read version and IHL (first byte of IP header) */
> +	uint8_t version_ihl = data[14];
> +
> +	/* Check IPv4 version (upper 4 bits should be 4) */
> +	if ((version_ihl >> 4) != 4)
> +		return 0;
> +
> +	/* Protocol field (byte 9 of IP header) must be TCP (6) */
> +	uint8_t proto = data[14 + 9];
> +	return (proto == 6);
> +}
> +
> +__attribute__((section("drop"), used))
> +uint64_t
> +test_drop(void *pkt)
> +{
> +	(void)pkt;
> +	return 0;
> +}
> +
> +__attribute__((section("allow"), used))
> +uint64_t
> +test_allow(void *pkt)
> +{
> +	(void)pkt;
> +	return 1;
> +}
> diff --git a/app/test/bpf/meson.build b/app/test/bpf/meson.build
> index b4f54aa976..19fec05521 100644
> --- a/app/test/bpf/meson.build
> +++ b/app/test/bpf/meson.build
> @@ -32,6 +32,7 @@ cflags += '-DTEST_BPF_ELF_LOAD'
>  # BPF sources to compile
>  bpf_progs = {
>      'load' : 'test_bpf_load',
> +    'filter' : 'test_bpf_filter',
>  }
> 
>  foreach bpf_src, bpf_hdr : bpf_progs
> diff --git a/app/test/test_bpf.c b/app/test/test_bpf.c
> index c460002358..6bbcbb5eb3 100644
> --- a/app/test/test_bpf.c
> +++ b/app/test/test_bpf.c
> @@ -3424,10 +3424,326 @@ test_bpf_elf_load(void)
>  	printf("%s: ELF load test passed\n", __func__);
>  	return TEST_SUCCESS;
>  }
> +
> +#include <rte_ethdev.h>
> +#include <rte_bpf_ethdev.h>
> +#include <rte_bus_vdev.h>
> +
> +#include "test_bpf_filter.h"
> +
> +#define BPF_TEST_BURST		128
> +#define BPF_TEST_POOLSIZE	256 /* at least 2x burst */
> +#define BPF_TEST_PKT_LEN	64 /* Ether + IP + TCP */
> +
> +static int null_vdev_setup(const char *name, uint16_t *port, struct rte_mempool
> *pool)
> +{
> +	int ret;
> +
> +	/* Make a null device */
> +	ret = rte_vdev_init(name, NULL);
> +	TEST_ASSERT(ret == 0, "rte_vdev_init(%s) failed: %d", name, ret);
> +
> +	ret = rte_eth_dev_get_port_by_name(name, port);
> +	TEST_ASSERT(ret == 0, "failed to get port id for %s: %d", name, ret);
> +
> +	struct rte_eth_conf conf = { };
> +	ret = rte_eth_dev_configure(*port, 1, 1, &conf);
> +	TEST_ASSERT(ret == 0, "failed to configure port %u: %d", *port, ret);
> +
> +	struct rte_eth_txconf txconf = { };
> +	ret = rte_eth_tx_queue_setup(*port, 0, BPF_TEST_BURST, SOCKET_ID_ANY,
> &txconf);
> +	TEST_ASSERT(ret == 0, "failed to setup tx queue port %u: %d", *port, ret);
> +
> +	struct rte_eth_rxconf rxconf = { };
> +	ret = rte_eth_rx_queue_setup(*port, 0, BPF_TEST_BURST, SOCKET_ID_ANY,
> +				     &rxconf, pool);
> +	TEST_ASSERT(ret == 0, "failed to setup rx queue port %u: %d", *port, ret);
> +
> +	ret = rte_eth_dev_start(*port);
> +	TEST_ASSERT(ret == 0, "failed to start port %u: %d", *port, ret);
> +
> +	return 0;
> +}
> +
> +static unsigned int
> +setup_mbufs(struct rte_mbuf *burst[], unsigned int n)
> +{
> +	struct rte_ether_hdr eh = {
> +		.ether_type = rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4),
> +	};
> +	const struct rte_ipv4_hdr iph = {
> +		.version_ihl = RTE_IPV4_VHL_DEF,
> +		.total_length = rte_cpu_to_be_16(BPF_TEST_PKT_LEN - sizeof(eh)),
> +		.time_to_live = IPDEFTTL,
> +		.src_addr = rte_cpu_to_be_32(ip_src_addr),
> +		.dst_addr = rte_cpu_to_be_32(ip_dst_addr),
> +	};
> +	unsigned int tcp_count = 0;
> +
> +	rte_eth_random_addr(eh.dst_addr.addr_bytes);
> +
> +	for (unsigned int i = 0; i < n; i++) {
> +		struct rte_mbuf *mb = burst[i];
> +
> +		/* Setup Ethernet header */
> +		*rte_pktmbuf_mtod(mb, struct rte_ether_hdr *) = eh;
> +
> +		/* Setup IP header */
> +		struct rte_ipv4_hdr *ip
> +			= rte_pktmbuf_mtod_offset(mb, struct rte_ipv4_hdr *,
> sizeof(eh));
> +		*ip = iph;
> +
> +		if (rte_rand() & 1) {
> +			struct rte_udp_hdr *udp
> +				= rte_pktmbuf_mtod_offset(mb, struct rte_udp_hdr
> *,
> +							  sizeof(eh) + sizeof(iph));
> +
> +			ip->next_proto_id = IPPROTO_UDP;
> +			*udp = (struct rte_udp_hdr) {
> +				.src_port = rte_cpu_to_be_16(9),	/* discard
> */
> +				.dst_port = rte_cpu_to_be_16(9),	/* discard
> */
> +				.dgram_len = BPF_TEST_PKT_LEN - sizeof(eh) -
> sizeof(iph),
> +			};
> +
> +		} else {
> +			struct rte_tcp_hdr *tcp
> +				= rte_pktmbuf_mtod_offset(mb, struct rte_tcp_hdr
> *,
> +							  sizeof(eh) + sizeof(iph));
> +
> +			ip->next_proto_id = IPPROTO_TCP;
> +			*tcp = (struct rte_tcp_hdr) {
> +				.src_port = rte_cpu_to_be_16(9),	/* discard
> */
> +				.dst_port = rte_cpu_to_be_16(9),	/* discard
> */
> +				.tcp_flags = RTE_TCP_RST_FLAG,
> +			};
> +			++tcp_count;
> +		}
> +	}
> +
> +	return tcp_count;
> +}
> +
> +static int bpf_tx_test(uint16_t port, const char *tmpfile, struct rte_mempool *pool,
> +		       const char *section, uint32_t flags)
> +{
> +	const struct rte_bpf_prm prm = {
> +		.prog_arg = {
> +			.type = RTE_BPF_ARG_PTR,
> +			.size = sizeof(struct rte_mbuf),
> +		},
> +	};
> +	int ret;
> +
> +	/* Try to load BPF TX program from temp file */
> +	ret = rte_bpf_eth_tx_elf_load(port, 0, &prm, tmpfile, section, flags);
> +	if (ret != 0) {
> +		printf("%s@%d: failed to load BPF filter from file=%s
> error=%d:(%s)\n",
> +		       __func__, __LINE__, tmpfile, rte_errno,
> rte_strerror(rte_errno));
> +		return ret;
> +	}
> +
> +	struct rte_mbuf *pkts[BPF_TEST_BURST] = { };
> +	ret = rte_pktmbuf_alloc_bulk(pool, pkts, BPF_TEST_BURST);
> +	TEST_ASSERT(ret == 0, "failed to allocate mbufs");
> +
> +	uint16_t expect = setup_mbufs(pkts, BPF_TEST_BURST);
> +
> +	uint16_t sent = rte_eth_tx_burst(port, 0, pkts, BPF_TEST_BURST);
> +	TEST_ASSERT_EQUAL(sent, expect, "rte_eth_tx_burst returned: %u
> expected %u",
> +			  sent, expect);
> +
> +	/* The unsent packets should be dropped */
> +	rte_pktmbuf_free_bulk(pkts + sent, BPF_TEST_BURST - sent);
> +
> +	/* Pool should have same number of packets avail */
> +	unsigned int avail = rte_mempool_avail_count(pool);
> +	TEST_ASSERT_EQUAL(avail, BPF_TEST_POOLSIZE,
> +			  "Mempool available %u != %u leaks?", avail,
> BPF_TEST_POOLSIZE);
> +
> +	rte_bpf_eth_tx_unload(port, 0);
> +	return TEST_SUCCESS;
> +}
> +
> +/* Test loading a transmit filter which only allows IPv4 packets */
> +static int
> +test_bpf_elf_tx_load(void)
> +{
> +	static const char null_dev[] = "net_null_bpf0";
> +	char *tmpfile = NULL;
> +	struct rte_mempool *mb_pool = NULL;
> +	uint16_t port = UINT16_MAX;
> +	int ret;
> +
> +	printf("%s start\n", __func__);
> +
> +	/* Make a pool for packets */
> +	mb_pool = rte_pktmbuf_pool_create("bpf_tx_test_pool",
> BPF_TEST_POOLSIZE,
> +					  0, 0, RTE_MBUF_DEFAULT_BUF_SIZE,
> +					  SOCKET_ID_ANY);
> +
> +	ret = null_vdev_setup(null_dev, &port, mb_pool);
> +	if (ret != 0)
> +		goto fail;
> +
> +	/* Create temp file from embedded BPF object */
> +	tmpfile = create_temp_bpf_file(app_test_bpf_filter_o,
> app_test_bpf_filter_o_len, "tx");
> +	if (tmpfile == NULL)
> +		goto fail;
> +
> +	/* Do test with VM */
> +	ret = bpf_tx_test(port, tmpfile, mb_pool, "filter", 0);
> +	if (ret != 0)
> +		goto fail;
> +
> +	/* Repeat with JIT */
> +	ret = bpf_tx_test(port, tmpfile, mb_pool, "filter", RTE_BPF_ETH_F_JIT);
> +	if (ret == 0)
> +		printf("%s: TX ELF load test passed\n", __func__);
> +
> +fail:
> +	if (tmpfile) {
> +		unlink(tmpfile);
> +		free(tmpfile);
> +	}
> +
> +	if (port != UINT16_MAX)
> +		rte_vdev_uninit(null_dev);
> +
> +	rte_mempool_free(mb_pool);
> +
> +	if (ret == 0)
> +		return TEST_SUCCESS;
> +	else if (ret == -ENOTSUP)
> +		return TEST_SKIPPED;
> +	else
> +		return TEST_FAILED;
> +}
> +
> +/* Test loading a receive filter */
> +static int bpf_rx_test(uint16_t port, const char *tmpfile, struct rte_mempool *pool,
> +		       const char *section, uint32_t flags, uint16_t expected)
> +{
> +	struct rte_mbuf *pkts[BPF_TEST_BURST];
> +	const struct rte_bpf_prm prm = {
> +		.prog_arg = {
> +			.type = RTE_BPF_ARG_PTR,
> +			.size = sizeof(struct rte_mbuf),
> +		},
> +	};
> +	int ret;
> +
> +	/* Load BPF program to drop all packets */
> +	ret = rte_bpf_eth_rx_elf_load(port, 0, &prm, tmpfile, section, flags);
> +	if (ret != 0) {
> +		printf("%s@%d: failed to load BPF filter from file=%s
> error=%d:(%s)\n",
> +		       __func__, __LINE__, tmpfile, rte_errno,
> rte_strerror(rte_errno));
> +		return ret;
> +	}
> +
> +	uint16_t rcvd = rte_eth_rx_burst(port, 0, pkts, BPF_TEST_BURST);
> +	TEST_ASSERT_EQUAL(rcvd, expected,
> +			  "rte_eth_rx_burst returned: %u expect: %u", rcvd,
> expected);
> +
> +	/* Drop the received packets */
> +	rte_pktmbuf_free_bulk(pkts, rcvd);
> +
> +	rte_bpf_eth_rx_unload(port, 0);
> +
> +	/* Pool should now be full */
> +	unsigned int avail = rte_mempool_avail_count(pool);
> +	TEST_ASSERT_EQUAL(avail, BPF_TEST_POOLSIZE,
> +			  "Mempool available %u != %u leaks?", avail,
> BPF_TEST_POOLSIZE);
> +
> +	return TEST_SUCCESS;
> +}
> +
> +/* Test loading a receive filters, first with drop all and then with allow all packets */
> +static int
> +test_bpf_elf_rx_load(void)
> +{
> +	static const char null_dev[] = "net_null_bpf0";
> +	struct rte_mempool *pool = NULL;
> +	char *tmpfile = NULL;
> +	uint16_t port;
> +	int ret;
> +
> +	printf("%s start\n", __func__);
> +
> +	/* Make a pool for packets */
> +	pool = rte_pktmbuf_pool_create("bpf_rx_test_pool", 2 * BPF_TEST_BURST,
> +					  0, 0, RTE_MBUF_DEFAULT_BUF_SIZE,
> +					  SOCKET_ID_ANY);
> +	TEST_ASSERT(pool != NULL, "failed to create mempool");
> +
> +	ret = null_vdev_setup(null_dev, &port, pool);
> +	if (ret != 0)
> +		goto fail;
> +
> +	/* Create temp file from embedded BPF object */
> +	tmpfile = create_temp_bpf_file(app_test_bpf_filter_o,
> app_test_bpf_filter_o_len, "rx");
> +	if (tmpfile == NULL)
> +		goto fail;
> +
> +	/* Do test with VM */
> +	ret = bpf_rx_test(port, tmpfile, pool, "drop", 0, 0);
> +	if (ret != 0)
> +		goto fail;
> +
> +	/* Repeat with JIT */
> +	ret = bpf_rx_test(port, tmpfile, pool, "drop", RTE_BPF_ETH_F_JIT, 0);
> +	if (ret != 0)
> +		goto fail;
> +
> +	/* Repeat with allow all */
> +	ret = bpf_rx_test(port, tmpfile, pool, "allow", 0, BPF_TEST_BURST);
> +	if (ret != 0)
> +		goto fail;
> +
> +	/* Repeat with JIT */
> +	ret = bpf_rx_test(port, tmpfile, pool, "allow", RTE_BPF_ETH_F_JIT,
> BPF_TEST_BURST);
> +	if (ret != 0)
> +		goto fail;
> +
> +	printf("%s: RX ELF load test passed\n", __func__);
> +
> +	/* The filter should free the mbufs */
> +	unsigned int avail = rte_mempool_avail_count(pool);
> +	TEST_ASSERT_EQUAL(avail, BPF_TEST_POOLSIZE,
> +			  "Mempool available %u != %u leaks?", avail,
> BPF_TEST_POOLSIZE);
> +
> +fail:
> +	if (tmpfile) {
> +		unlink(tmpfile);
> +		free(tmpfile);
> +	}
> +
> +	if (port != UINT16_MAX)
> +		rte_vdev_uninit(null_dev);
> +
> +	rte_mempool_free(pool);
> +
> +	return ret == 0 ? TEST_SUCCESS : TEST_FAILED;
> +}
> +
> +
> +static int
> +test_bpf_elf(void)
> +{
> +	int ret;
> +
> +	ret = test_bpf_elf_load();
> +	if (ret == TEST_SUCCESS)
> +		ret = test_bpf_elf_tx_load();
> +	if (ret == TEST_SUCCESS)
> +		ret = test_bpf_elf_rx_load();
> +
> +	return ret;
> +}
> +
>  #else
> 
>  static int
> -test_bpf_elf_load(void)
> +test_bpf_elf(void)
>  {
>  	printf("BPF compile not supported, skipping test\n");
>  	return TEST_SKIPPED;
> @@ -3435,7 +3751,8 @@ test_bpf_elf_load(void)
> 
>  #endif /* !TEST_BPF_ELF_LOAD */
> 
> -REGISTER_FAST_TEST(bpf_elf_load_autotest, true, true, test_bpf_elf_load);
> +
> +REGISTER_FAST_TEST(bpf_elf_autotest, true, true, test_bpf_elf);
> 
>  #ifndef RTE_HAS_LIBPCAP
> 
> --
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
> 2.51.0


  reply	other threads:[~2025-11-12 15:08 UTC|newest]

Thread overview: 60+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-10-30 17:34 [PATCH 0/5] bpf enhancements Stephen Hemminger
2025-10-30 17:34 ` [PATCH 1/5] bpf: add allocation annotations to functions Stephen Hemminger
2025-10-30 17:34 ` [PATCH 2/5] bpf: use rte_pktmbuf_free_bulk Stephen Hemminger
2025-10-30 17:34 ` [PATCH 3/5] bpf: add a test for BPF ELF load Stephen Hemminger
2025-10-30 17:34 ` [PATCH 4/5] bpf: add test for rx and tx filtering Stephen Hemminger
2025-10-30 17:34 ` [PATCH 5/5] bpf: remove use of vla Stephen Hemminger
2025-10-31 11:39 ` [PATCH 0/5] bpf enhancements Marat Khalili
2025-10-31 16:37   ` Stephen Hemminger
2025-10-31 16:41 ` [PATCH v2 0/5] BPF enhancements Stephen Hemminger
2025-10-31 16:41   ` [PATCH v2 1/5] bpf: add allocation annotations to functions Stephen Hemminger
2025-10-31 16:41   ` [PATCH v2 2/5] bpf: use bulk free on filtered packets Stephen Hemminger
2025-10-31 16:41   ` [PATCH v2 3/5] bpf: add a test for BPF ELF load Stephen Hemminger
2025-10-31 16:41   ` [PATCH v2 4/5] bpf: add test for Rx and Tx filtering Stephen Hemminger
2025-10-31 16:41   ` [PATCH v2 5/5] bpf: remove use of VLA Stephen Hemminger
2025-11-01 18:04 ` [PATCH v3 0/5] BPF enhancements Stephen Hemminger
2025-11-01 18:04   ` [PATCH v3 1/5] bpf: add allocation annotations to functions Stephen Hemminger
2025-11-02 21:42     ` Konstantin Ananyev
2025-11-01 18:04   ` [PATCH v3 2/5] bpf: use bulk free on filtered packets Stephen Hemminger
2025-11-01 18:04   ` [PATCH v3 3/5] bpf: add a test for BPF ELF load Stephen Hemminger
2025-11-01 18:04   ` [PATCH v3 4/5] bpf: add test for Rx and Tx filtering Stephen Hemminger
2025-11-01 18:04   ` [PATCH v3 5/5] bpf: remove use of VLA Stephen Hemminger
2025-11-03  9:21     ` Konstantin Ananyev
2025-11-04 16:07 ` [PATCH v4 0/5] BPF enhancements Stephen Hemminger
2025-11-04 16:07   ` [PATCH v4 1/5] bpf: add allocation annotations to functions Stephen Hemminger
2025-11-07 17:35     ` Marat Khalili
2025-11-04 16:07   ` [PATCH v4 2/5] bpf: use bulk free on filtered packets Stephen Hemminger
2025-11-06  7:25     ` Konstantin Ananyev
2025-11-07 17:36     ` Marat Khalili
2025-11-04 16:07   ` [PATCH v4 3/5] bpf: add a test for BPF ELF load Stephen Hemminger
2025-11-07 17:33     ` Marat Khalili
2025-11-07 17:45       ` Marat Khalili
2025-11-08  1:09         ` Stephen Hemminger
2025-11-10 15:34           ` Marat Khalili
2025-11-08  1:08       ` Stephen Hemminger
2025-11-04 16:07   ` [PATCH v4 4/5] bpf: add test for Rx and Tx filtering Stephen Hemminger
2025-11-07 17:30     ` Marat Khalili
2025-11-08  1:11       ` Stephen Hemminger
2025-11-10 15:43         ` Marat Khalili
2025-11-04 16:07   ` [PATCH v4 5/5] bpf: replace use of VLA Stephen Hemminger
2025-11-06  7:26     ` Konstantin Ananyev
2025-11-07 17:36     ` Marat Khalili
2025-11-09 20:07 ` [PATCH v5 0/5] BPF cleanup and tests Stephen Hemminger
2025-11-09 20:07   ` [PATCH v5 1/5] bpf: add allocation annotations to functions Stephen Hemminger
2025-11-09 20:07   ` [PATCH v5 2/5] bpf: use bulk free on filtered packets Stephen Hemminger
2025-11-09 20:07   ` [PATCH v5 3/5] bpf: add a test for BPF ELF load Stephen Hemminger
2025-11-10 16:38     ` Marat Khalili
2025-11-10 17:07       ` Stephen Hemminger
2025-11-10 17:17         ` Marat Khalili
2025-11-10 17:08       ` Stephen Hemminger
2025-11-11  9:46     ` Marat Khalili
2025-11-09 20:07   ` [PATCH v5 4/5] bpf: add test for Rx and Tx filtering Stephen Hemminger
2025-11-11  9:46     ` Marat Khalili
2025-11-09 20:07   ` [PATCH v5 5/5] bpf: replace use of VLA Stephen Hemminger
2025-11-11 12:13   ` [PATCH v5 0/5] BPF cleanup and tests Thomas Monjalon
2025-11-11 22:55   ` [PATCH v6 0/2] BPF tests Stephen Hemminger
2025-11-11 22:55     ` [PATCH v6 1/2] bpf: add a test for BPF ELF load Stephen Hemminger
2025-11-12 15:06       ` Konstantin Ananyev
2025-11-11 22:55     ` [PATCH v6 2/2] bpf: add test for Rx and Tx filtering Stephen Hemminger
2025-11-12 15:08       ` Konstantin Ananyev [this message]
2025-11-12 15:03     ` [PATCH v6 0/2] BPF tests Marat Khalili

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=bb1bce77ccfd46b5850439be9bcb5607@huawei.com \
    --to=konstantin.ananyev@huawei.com \
    --cc=dev@dpdk.org \
    --cc=marat.khalili@huawei.com \
    --cc=stephen@networkplumber.org \
    /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).