From: Radu Nicolau <radu.nicolau@intel.com>
To: dev@dpdk.org
Cc: konstantin.ananyev@intel.com, wenzhuo.lu@intel.com,
declan.doherty@intel.com, Radu Nicolau <radu.nicolau@intel.com>
Subject: [dpdk-dev] [PATCH] net/ixgbe: add security statistics
Date: Fri, 6 Sep 2019 17:41:56 +0100 [thread overview]
Message-ID: <1567788116-16952-1-git-send-email-radu.nicolau@intel.com> (raw)
Update IXGBE PMD with support for IPsec statistics.
Signed-off-by: Radu Nicolau <radu.nicolau@intel.com>
---
drivers/net/ixgbe/ixgbe_ipsec.c | 244 ++++++++++++++++++++++++++++++++-
drivers/net/ixgbe/ixgbe_ipsec.h | 17 ++-
drivers/net/ixgbe/ixgbe_rxtx.c | 18 +++
drivers/net/ixgbe/ixgbe_rxtx.h | 4 +
drivers/net/ixgbe/ixgbe_rxtx_vec_sse.c | 6 +
5 files changed, 285 insertions(+), 4 deletions(-)
diff --git a/drivers/net/ixgbe/ixgbe_ipsec.c b/drivers/net/ixgbe/ixgbe_ipsec.c
index 48f5082..94daf92 100644
--- a/drivers/net/ixgbe/ixgbe_ipsec.c
+++ b/drivers/net/ixgbe/ixgbe_ipsec.c
@@ -9,6 +9,7 @@
#include <rte_security_driver.h>
#include <rte_cryptodev.h>
#include <rte_flow.h>
+#include <rte_hash_crc.h>
#include "base/ixgbe_type.h"
#include "base/ixgbe_api.h"
@@ -94,6 +95,7 @@ ixgbe_crypto_add_sa(struct ixgbe_crypto_session *ic_session)
dev->data->dev_private);
uint32_t reg_val;
int sa_index = -1;
+ struct ixgbe_crypto_sess_htable_key tbl_key = {0};
if (ic_session->op == IXGBE_OP_AUTHENTICATED_DECRYPTION) {
int i, ip_index = -1;
@@ -158,9 +160,14 @@ ixgbe_crypto_add_sa(struct ixgbe_crypto_session *ic_session)
if (ic_session->dst_ip.type == IPv6) {
priv->rx_sa_tbl[sa_index].mode |= IPSRXMOD_IPV6;
priv->rx_ip_tbl[ip_index].ip.type = IPv6;
- } else if (ic_session->dst_ip.type == IPv4)
+ memcpy(&tbl_key.ip.ipv6, &ic_session->dst_ip.ipv6,
+ sizeof(ic_session->dst_ip.ipv6));
+ } else if (ic_session->dst_ip.type == IPv4) {
priv->rx_ip_tbl[ip_index].ip.type = IPv4;
+ tbl_key.ip.ipv4 = ic_session->dst_ip.ipv4;
+ }
+ tbl_key.spi = priv->rx_sa_tbl[sa_index].spi;
priv->rx_sa_tbl[sa_index].used = 1;
/* write IP table entry*/
@@ -238,6 +245,7 @@ ixgbe_crypto_add_sa(struct ixgbe_crypto_session *ic_session)
priv->tx_sa_tbl[sa_index].spi =
rte_cpu_to_be_32(ic_session->spi);
+ tbl_key.spi = priv->tx_sa_tbl[sa_index].spi;
priv->tx_sa_tbl[i].used = 1;
ic_session->sa_index = sa_index;
@@ -264,6 +272,7 @@ ixgbe_crypto_add_sa(struct ixgbe_crypto_session *ic_session)
free(key);
}
+ rte_hash_add_key_data(priv->session_tbl, &tbl_key, ic_session);
return 0;
}
@@ -276,6 +285,7 @@ ixgbe_crypto_remove_sa(struct rte_eth_dev *dev,
IXGBE_DEV_PRIVATE_TO_IPSEC(dev->data->dev_private);
uint32_t reg_val;
int sa_index = -1;
+ struct ixgbe_crypto_sess_htable_key tbl_key = {0};
if (ic_session->op == IXGBE_OP_AUTHENTICATED_DECRYPTION) {
int i, ip_index = -1;
@@ -324,6 +334,13 @@ ixgbe_crypto_remove_sa(struct rte_eth_dev *dev,
IXGBE_WRITE_REG(hw, IXGBE_IPSRXMOD, 0);
IXGBE_WAIT_RWRITE;
priv->rx_sa_tbl[sa_index].used = 0;
+ if (priv->rx_sa_tbl[sa_index].mode & IPSRXMOD_IPV6) {
+ memcpy(&tbl_key.ip.ipv6, &ic_session->dst_ip.ipv6,
+ sizeof(ic_session->dst_ip.ipv6));
+ } else {
+ tbl_key.ip.ipv4 = ic_session->dst_ip.ipv4;
+ }
+ tbl_key.spi = priv->rx_sa_tbl[sa_index].spi;
/* If last used then clear the IP table entry*/
priv->rx_ip_tbl[ip_index].ref_count--;
@@ -361,8 +378,10 @@ ixgbe_crypto_remove_sa(struct rte_eth_dev *dev,
IXGBE_WAIT_TWRITE;
priv->tx_sa_tbl[sa_index].used = 0;
+ tbl_key.spi = priv->tx_sa_tbl[sa_index].spi;
}
+ rte_hash_del_key(priv->session_tbl, &tbl_key);
return 0;
}
@@ -376,6 +395,8 @@ ixgbe_crypto_create_session(void *device,
struct ixgbe_crypto_session *ic_session = NULL;
struct rte_crypto_aead_xform *aead_xform;
struct rte_eth_conf *dev_conf = ð_dev->data->dev_conf;
+ struct ixgbe_ipsec *priv =
+ IXGBE_DEV_PRIVATE_TO_IPSEC(eth_dev->data->dev_private);
if (rte_mempool_get(mempool, (void **)&ic_session)) {
PMD_DRV_LOG(ERR, "Cannot get object from ic_session mempool");
@@ -426,6 +447,40 @@ ixgbe_crypto_create_session(void *device,
}
}
+ if (conf->ipsec.options.stats) {
+ ic_session->stats_enabled = 1;
+ priv->per_session_stats_active++;
+ }
+
+ return 0;
+}
+
+static int
+ixgbe_crypto_stats_get(__rte_unused void *device,
+ struct rte_security_session *sess,
+ struct rte_security_stats *stats)
+{
+ struct rte_eth_dev *eth_dev = device;
+ struct ixgbe_ipsec *priv =
+ IXGBE_DEV_PRIVATE_TO_IPSEC(eth_dev->data->dev_private);
+ volatile struct rte_security_ipsec_stats *ixgbe_stats;
+
+ if (sess) {
+ struct ixgbe_crypto_session *ic_session =
+ (struct ixgbe_crypto_session *)
+ get_sec_session_private_data(sess);
+ ixgbe_stats = &ic_session->stats;
+ } else {
+ ixgbe_stats = &priv->stats;
+ }
+
+ stats->ipsec.ipackets = ixgbe_stats->ipackets;
+ stats->ipsec.opackets = ixgbe_stats->opackets;
+ stats->ipsec.ibytes = ixgbe_stats->ibytes;
+ stats->ipsec.obytes = ixgbe_stats->obytes;
+ stats->ipsec.ierrors = ixgbe_stats->ierrors;
+ stats->ipsec.oerrors = ixgbe_stats->oerrors;
+
return 0;
}
@@ -444,6 +499,8 @@ ixgbe_crypto_remove_session(void *device,
(struct ixgbe_crypto_session *)
get_sec_session_private_data(session);
struct rte_mempool *mempool = rte_mempool_from_obj(ic_session);
+ struct ixgbe_ipsec *priv =
+ IXGBE_DEV_PRIVATE_TO_IPSEC(eth_dev->data->dev_private);
if (eth_dev != ic_session->dev) {
PMD_DRV_LOG(ERR, "Session not bound to this device\n");
@@ -455,11 +512,44 @@ ixgbe_crypto_remove_session(void *device,
return -EFAULT;
}
+ if (ic_session->stats_enabled && priv->per_session_stats_active > 0)
+ priv->per_session_stats_active--;
+
rte_mempool_put(mempool, (void *)ic_session);
return 0;
}
+static int
+ixgbe_crypto_update_session(void *device,
+ struct rte_security_session *session,
+ struct rte_security_session_conf *conf)
+{
+ struct rte_eth_dev *eth_dev = device;
+ struct ixgbe_crypto_session *ic_session =
+ (struct ixgbe_crypto_session *)
+ get_sec_session_private_data(session);
+ struct ixgbe_ipsec *priv =
+ IXGBE_DEV_PRIVATE_TO_IPSEC(eth_dev->data->dev_private);
+
+ if (eth_dev != ic_session->dev) {
+ PMD_DRV_LOG(ERR, "Session not bound to this device\n");
+ return -ENODEV;
+ }
+
+ /* Enable/disable per session stats */
+ if (ic_session->stats_enabled && !conf->ipsec.options.stats) {
+ ic_session->stats_enabled = 0;
+ if (priv->per_session_stats_active > 0)
+ priv->per_session_stats_active--;
+ } else if (!ic_session->stats_enabled && conf->ipsec.options.stats) {
+ ic_session->stats_enabled = 1;
+ priv->per_session_stats_active++;
+ }
+
+ return 0;
+}
+
static inline uint8_t
ixgbe_crypto_compute_pad_len(struct rte_mbuf *m)
{
@@ -624,6 +714,8 @@ int
ixgbe_crypto_enable_ipsec(struct rte_eth_dev *dev)
{
struct ixgbe_hw *hw = IXGBE_DEV_PRIVATE_TO_HW(dev->data->dev_private);
+ struct ixgbe_ipsec *priv =
+ IXGBE_DEV_PRIVATE_TO_IPSEC(dev->data->dev_private);
uint32_t reg;
uint64_t rx_offloads;
uint64_t tx_offloads;
@@ -676,6 +768,23 @@ ixgbe_crypto_enable_ipsec(struct rte_eth_dev *dev)
ixgbe_crypto_clear_ipsec_tables(dev);
+ if (priv->session_tbl == NULL) {
+ char session_tbl_hash_name[RTE_HASH_NAMESIZE];
+ struct rte_hash_parameters params = {
+ .name = session_tbl_hash_name,
+ .entries = IPSEC_MAX_SA_COUNT * 2,
+ .key_len = sizeof(struct ixgbe_crypto_sess_htable_key),
+ .hash_func = rte_hash_crc,
+ .socket_id = rte_socket_id(),
+ };
+ snprintf(session_tbl_hash_name, RTE_HASH_NAMESIZE,
+ "session_tbl_hash_%s", dev->device->name);
+
+ priv->session_tbl = rte_hash_create(¶ms);
+ } else {
+ rte_hash_reset(priv->session_tbl);
+ }
+
return 0;
}
@@ -709,11 +818,140 @@ ixgbe_crypto_add_ingress_sa_from_flow(const void *sess,
return 0;
}
+void
+ixgbe_crypto_update_rx_stats(struct ixgbe_ipsec *ixgbe_ipsec,
+ struct rte_mbuf **mbufs,
+ uint16_t count)
+{
+ uint16_t i;
+ uint32_t ipackets = 0, ibytes = 0, ierrors = 0;
+
+ if (ixgbe_ipsec->per_session_stats_active) {
+ struct ip *ip;
+ struct rte_esp_hdr *esp;
+ struct ixgbe_crypto_session *sess;
+ struct ixgbe_crypto_sess_htable_key tbl_key = {0};
+
+ for (i = 0; i < count; i++) {
+ if ((mbufs[i]->ol_flags & PKT_RX_SEC_OFFLOAD) == 0)
+ continue;
+ ip = rte_pktmbuf_mtod_offset(mbufs[i],
+ struct ip *,
+ sizeof(struct rte_ether_hdr));
+ if (ip->ip_v == IPVERSION) {
+ tbl_key.ip.ipv4 = ip->ip_dst.s_addr;
+ esp = rte_pktmbuf_mtod_offset(mbufs[i],
+ struct rte_esp_hdr *,
+ sizeof(struct rte_ether_hdr) +
+ ip->ip_hl * 4);
+ } else {
+ struct ip6_hdr *ip6 = (struct ip6_hdr *)ip;
+ memcpy(&tbl_key.ip.ipv6, &ip6->ip6_dst, 16);
+ esp = rte_pktmbuf_mtod_offset(mbufs[i],
+ struct rte_esp_hdr *,
+ sizeof(struct rte_ether_hdr) +
+ sizeof(struct ip6_hdr));
+ }
+ tbl_key.spi = esp->spi;
+ if (rte_hash_lookup_data(ixgbe_ipsec->session_tbl,
+ &tbl_key, (void **)&sess) < 0)
+ sess = NULL;
+ if (mbufs[i]->ol_flags & PKT_RX_SEC_OFFLOAD_FAILED) {
+ if (sess && sess->stats_enabled)
+ sess->stats.ierrors++;
+ ierrors++;
+ } else {
+ if (sess && sess->stats_enabled) {
+ sess->stats.ipackets++;
+ sess->stats.ibytes +=
+ rte_pktmbuf_pkt_len(mbufs[i]);
+ }
+ ipackets++;
+ ibytes += rte_pktmbuf_pkt_len(mbufs[i]);
+ }
+ }
+ } else { /* Global stats only */
+ for (i = 0; i < count; i++) {
+ if (mbufs[i]->ol_flags & PKT_RX_SEC_OFFLOAD) {
+ if (mbufs[i]->ol_flags &
+ PKT_RX_SEC_OFFLOAD_FAILED) {
+ ierrors++;
+ } else {
+ ipackets++;
+ ibytes += rte_pktmbuf_pkt_len(mbufs[i]);
+ }
+ }
+ }
+ }
+
+ /* Update global stats */
+ ixgbe_ipsec->stats.ipackets += ipackets;
+ ixgbe_ipsec->stats.ibytes += ibytes;
+ ixgbe_ipsec->stats.ierrors += ierrors;
+}
+
+void
+ixgbe_crypto_update_tx_stats(struct ixgbe_ipsec *ixgbe_ipsec,
+ struct rte_mbuf **mbufs,
+ uint16_t count)
+{
+ uint16_t i;
+ uint32_t opackets = 0, obytes = 0;
+
+ if (ixgbe_ipsec->per_session_stats_active) {
+ struct ip *ip;
+ struct rte_esp_hdr *esp;
+ struct ixgbe_crypto_session *sess;
+ struct ixgbe_crypto_sess_htable_key tbl_key = {0};
+
+ for (i = 0; i < count; i++) {
+ if ((mbufs[i]->ol_flags & PKT_TX_SEC_OFFLOAD) == 0)
+ continue;
+ ip = rte_pktmbuf_mtod_offset(mbufs[i],
+ struct ip *,
+ sizeof(struct rte_ether_hdr));
+ if (ip->ip_v == IPVERSION) {
+ esp = rte_pktmbuf_mtod_offset(mbufs[i],
+ struct rte_esp_hdr *,
+ sizeof(struct rte_ether_hdr) +
+ ip->ip_hl * 4);
+ } else {
+ esp = rte_pktmbuf_mtod_offset(mbufs[i],
+ struct rte_esp_hdr *,
+ sizeof(struct rte_ether_hdr) +
+ sizeof(struct ip6_hdr));
+ }
+ tbl_key.spi = esp->spi;
+ if (rte_hash_lookup_data(ixgbe_ipsec->session_tbl,
+ &tbl_key, (void **)&sess) < 0)
+ sess = NULL;
+ if (sess && sess->stats_enabled) {
+ sess->stats.opackets++;
+ sess->stats.obytes +=
+ rte_pktmbuf_pkt_len(mbufs[i]);
+ }
+ opackets++;
+ obytes += rte_pktmbuf_pkt_len(mbufs[i]);
+ }
+ } else { /* Global stats only */
+ for (i = 0; i < count; i++) {
+ if (mbufs[i]->ol_flags & PKT_RX_SEC_OFFLOAD) {
+ opackets++;
+ obytes += rte_pktmbuf_pkt_len(mbufs[i]);
+ }
+ }
+ }
+
+ /* Update global stats */
+ ixgbe_ipsec->stats.opackets += opackets;
+ ixgbe_ipsec->stats.obytes += obytes;
+}
+
static struct rte_security_ops ixgbe_security_ops = {
.session_create = ixgbe_crypto_create_session,
- .session_update = NULL,
+ .session_update = ixgbe_crypto_update_session,
.session_get_size = ixgbe_crypto_session_get_size,
- .session_stats_get = NULL,
+ .session_stats_get = ixgbe_crypto_stats_get,
.session_destroy = ixgbe_crypto_remove_session,
.set_pkt_metadata = ixgbe_crypto_update_mb,
.capabilities_get = ixgbe_crypto_capabilities_get
diff --git a/drivers/net/ixgbe/ixgbe_ipsec.h b/drivers/net/ixgbe/ixgbe_ipsec.h
index e218c0a..a4ff8b6 100644
--- a/drivers/net/ixgbe/ixgbe_ipsec.h
+++ b/drivers/net/ixgbe/ixgbe_ipsec.h
@@ -70,6 +70,8 @@ struct ixgbe_crypto_session {
struct ipaddr src_ip;
struct ipaddr dst_ip;
struct rte_eth_dev *dev;
+ volatile struct rte_security_ipsec_stats stats;
+ uint8_t stats_enabled;
} __rte_cache_aligned;
struct ixgbe_crypto_rx_ip_table {
@@ -100,10 +102,18 @@ union ixgbe_crypto_tx_desc_md {
};
};
+struct ixgbe_crypto_sess_htable_key {
+ uint32_t spi;
+ struct ipaddr ip;
+};
+
struct ixgbe_ipsec {
struct ixgbe_crypto_rx_ip_table rx_ip_tbl[IPSEC_MAX_RX_IP_COUNT];
struct ixgbe_crypto_rx_sa_table rx_sa_tbl[IPSEC_MAX_SA_COUNT];
struct ixgbe_crypto_tx_sa_table tx_sa_tbl[IPSEC_MAX_SA_COUNT];
+ volatile struct rte_security_ipsec_stats stats;
+ volatile uint16_t per_session_stats_active;
+ struct rte_hash *session_tbl;
};
@@ -112,7 +122,12 @@ int ixgbe_crypto_enable_ipsec(struct rte_eth_dev *dev);
int ixgbe_crypto_add_ingress_sa_from_flow(const void *sess,
const void *ip_spec,
uint8_t is_ipv6);
-
+void ixgbe_crypto_update_rx_stats(struct ixgbe_ipsec *ixgbe_ipsec,
+ struct rte_mbuf **mbufs,
+ uint16_t count);
+void ixgbe_crypto_update_tx_stats(struct ixgbe_ipsec *ixgbe_ipsec,
+ struct rte_mbuf **mbufs,
+ uint16_t count);
#endif /*IXGBE_IPSEC_H_*/
diff --git a/drivers/net/ixgbe/ixgbe_rxtx.c b/drivers/net/ixgbe/ixgbe_rxtx.c
index edcfa60..cde012f 100644
--- a/drivers/net/ixgbe/ixgbe_rxtx.c
+++ b/drivers/net/ixgbe/ixgbe_rxtx.c
@@ -956,6 +956,12 @@ ixgbe_xmit_pkts(void *tx_queue, struct rte_mbuf **tx_pkts,
IXGBE_PCI_REG_WRITE_RELAXED(txq->tdt_reg_addr, tx_id);
txq->tx_tail = tx_id;
+#ifdef RTE_LIBRTE_SECURITY
+ if (unlikely(txq->using_ipsec))
+ ixgbe_crypto_update_tx_stats(txq->ixgbe_ipsec,
+ &tx_pkts[-nb_tx], nb_tx);
+#endif
+
return nb_tx;
}
@@ -1643,6 +1649,12 @@ ixgbe_rx_fill_from_stage(struct ixgbe_rx_queue *rxq, struct rte_mbuf **rx_pkts,
rxq->rx_nb_avail = (uint16_t)(rxq->rx_nb_avail - nb_pkts);
rxq->rx_next_avail = (uint16_t)(rxq->rx_next_avail + nb_pkts);
+#ifdef RTE_LIBRTE_SECURITY
+ if (unlikely(rxq->using_ipsec))
+ ixgbe_crypto_update_rx_stats(rxq->ixgbe_ipsec,
+ rx_pkts, nb_pkts);
+#endif
+
return nb_pkts;
}
@@ -2618,6 +2630,9 @@ ixgbe_dev_tx_queue_setup(struct rte_eth_dev *dev,
#ifdef RTE_LIBRTE_SECURITY
txq->using_ipsec = !!(dev->data->dev_conf.txmode.offloads &
DEV_TX_OFFLOAD_SECURITY);
+ if (txq->using_ipsec)
+ txq->ixgbe_ipsec =
+ IXGBE_DEV_PRIVATE_TO_IPSEC(dev->data->dev_private);
#endif
/*
@@ -4730,6 +4745,9 @@ ixgbe_set_rx_function(struct rte_eth_dev *dev)
#ifdef RTE_LIBRTE_SECURITY
rxq->using_ipsec = !!(dev->data->dev_conf.rxmode.offloads &
DEV_RX_OFFLOAD_SECURITY);
+ if (rxq->using_ipsec)
+ rxq->ixgbe_ipsec =
+ IXGBE_DEV_PRIVATE_TO_IPSEC(dev->data->dev_private);
#endif
}
}
diff --git a/drivers/net/ixgbe/ixgbe_rxtx.h b/drivers/net/ixgbe/ixgbe_rxtx.h
index 505d344..b5f130d 100644
--- a/drivers/net/ixgbe/ixgbe_rxtx.h
+++ b/drivers/net/ixgbe/ixgbe_rxtx.h
@@ -114,6 +114,8 @@ struct ixgbe_rx_queue {
#ifdef RTE_LIBRTE_SECURITY
uint8_t using_ipsec;
/**< indicates that IPsec RX feature is in use */
+ struct ixgbe_ipsec *ixgbe_ipsec;
+ /**< IXGBE IPsec internals */
#endif
#ifdef RTE_IXGBE_INC_VECTOR
uint16_t rxrearm_nb; /**< number of remaining to be re-armed */
@@ -231,6 +233,8 @@ struct ixgbe_tx_queue {
#ifdef RTE_LIBRTE_SECURITY
uint8_t using_ipsec;
/**< indicates that IPsec TX feature is in use */
+ struct ixgbe_ipsec *ixgbe_ipsec;
+ /**< IXGBE IPsec internals */
#endif
};
diff --git a/drivers/net/ixgbe/ixgbe_rxtx_vec_sse.c b/drivers/net/ixgbe/ixgbe_rxtx_vec_sse.c
index 599ba30..b92d1a9 100644
--- a/drivers/net/ixgbe/ixgbe_rxtx_vec_sse.c
+++ b/drivers/net/ixgbe/ixgbe_rxtx_vec_sse.c
@@ -553,6 +553,12 @@ _recv_raw_pkts_vec(struct ixgbe_rx_queue *rxq, struct rte_mbuf **rx_pkts,
rxq->rx_tail = (uint16_t)(rxq->rx_tail & (rxq->nb_rx_desc - 1));
rxq->rxrearm_nb = (uint16_t)(rxq->rxrearm_nb + nb_pkts_recd);
+#ifdef RTE_LIBRTE_SECURITY
+ if (unlikely(use_ipsec))
+ ixgbe_crypto_update_rx_stats(rxq->ixgbe_ipsec,
+ rx_pkts, nb_pkts_recd);
+#endif
+
return nb_pkts_recd;
}
--
2.7.4
next reply other threads:[~2019-09-06 16:42 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2019-09-06 16:41 Radu Nicolau [this message]
2019-09-08 11:45 ` Ananyev, Konstantin
2019-09-09 11:00 ` Nicolau, Radu
2019-09-10 13:02 ` Ananyev, Konstantin
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=1567788116-16952-1-git-send-email-radu.nicolau@intel.com \
--to=radu.nicolau@intel.com \
--cc=declan.doherty@intel.com \
--cc=dev@dpdk.org \
--cc=konstantin.ananyev@intel.com \
--cc=wenzhuo.lu@intel.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).