* [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP
@ 2025-09-10 11:35 Volker Diels-Grabsch
2025-09-10 11:35 ` [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP address Volker Diels-Grabsch
2025-09-10 14:10 ` [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP Stefano Brivio
0 siblings, 2 replies; 4+ messages in thread
From: Volker Diels-Grabsch @ 2025-09-10 11:35 UTC (permalink / raw)
To: passt-dev
Changes from PATCH v3: (... which wasn't marked as "v3", sorry for that)
1. Simply remove the "e.g." part from the comment, as it's misleading anyways
2. Make "bufmac[]" as local as possible
3. Demote the "Sending initial ..." log message from info() to debug()
4. Log "Sending initial ..." separately for ARP and NDP (because of 5. and 6.)
5. Send initial ARP request only if IPv4 is enabled
6. Send initial NDP request only if IPv6, NDP and ICMP are enabled
7. Update description of "--no-ndp" in man page
8. Fix and update description of "--no-icmp" in man page
9. Break long line for MAC_BROADCAST
One final thought regarding 8.:
Would it make sense to state in the man page that "--no-icmp" implies
"--no-ndp", and to implement it rigorously that way? Or, would that
be going to far?
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP address
2025-09-10 11:35 [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP Volker Diels-Grabsch
@ 2025-09-10 11:35 ` Volker Diels-Grabsch
2025-09-10 14:32 ` Stefano Brivio
2025-09-10 14:10 ` [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP Stefano Brivio
1 sibling, 1 reply; 4+ messages in thread
From: Volker Diels-Grabsch @ 2025-09-10 11:35 UTC (permalink / raw)
To: passt-dev; +Cc: Volker Diels-Grabsch
When restarting passt while QEMU keeps running with a configured
"reconnect-ms" setting, the port forwardings will stop working until
the guest sends some outgoing network traffic.
Reason: Although QEMU reconnects successfully to the unix domain
socket of the new passt process, that one no longer knows the guest's
MAC address and uses instead the broadcast MAC address. However, this
is ignored by the guest, at least if the guest runs Linux. Only after
the guest sends some network package on its own initiative, passt will
know the MAC address and will be able to establish forwarded
connections.
This change fixes this issue by sending an ARP and an NDP request to
resolve the guest's MAC address via its IPv4 and IPv6 address, which
we do know, right after the unix domain socket (re)connection.
The only case where the IP is "wrong" would be if the configuration
changed, or on the very first start right after qemu started. But in
those cases, we just wouldn't get an ARP/NDP response, and can't do
anything until we receive the guest's DHCP request - just as before.
In other words, in the worst case the ARP/NDP requests would be
harmless.
Signed-off-by: Volker Diels-Grabsch <v@njh.eu>
---
arp.c | 35 +++++++++++++++++++++++++++++++++++
arp.h | 1 +
ndp.c | 21 +++++++++++++++++++++
ndp.h | 1 +
passt.1 | 9 +++++----
tap.c | 15 +++++++++++----
util.h | 2 ++
7 files changed, 76 insertions(+), 8 deletions(-)
diff --git a/arp.c b/arp.c
index 44677ad..b263419 100644
--- a/arp.c
+++ b/arp.c
@@ -112,3 +112,38 @@ int arp(const struct ctx *c, struct iov_tail *data)
return 1;
}
+
+/**
+ * arp_send_init_req() - Send initial ARP request to retrieve guest MAC address
+ * @c: Execution context
+ */
+void arp_send_init_req(const struct ctx *c)
+{
+ struct {
+ struct ethhdr eh;
+ struct arphdr ah;
+ struct arpmsg am;
+ } __attribute__((__packed__)) req;
+
+ /* Ethernet header */
+ req.eh.h_proto = htons(ETH_P_ARP);
+ memcpy(req.eh.h_dest, MAC_BROADCAST, sizeof(req.eh.h_dest));
+ memcpy(req.eh.h_source, c->our_tap_mac, sizeof(req.eh.h_source));
+
+ /* ARP header */
+ req.ah.ar_op = htons(ARPOP_REQUEST);
+ req.ah.ar_hrd = htons(ARPHRD_ETHER);
+ req.ah.ar_pro = htons(ETH_P_IP);
+ req.ah.ar_hln = ETH_ALEN;
+ req.ah.ar_pln = 4;
+
+ /* ARP message */
+ memcpy(req.am.sha, c->our_tap_mac, sizeof(req.am.sha));
+ memcpy(req.am.sip, &c->ip4.our_tap_addr, sizeof(req.am.sip));
+ memcpy(req.am.tha, MAC_BROADCAST, sizeof(req.am.tha));
+ memcpy(req.am.tip, &c->ip4.addr, sizeof(req.am.tip));
+
+ debug("Sending initial ARP request to retrieve"
+ " guest MAC address after reconnect");
+ tap_send_single(c, &req, sizeof(req));
+}
diff --git a/arp.h b/arp.h
index 86bcbf8..d5ad0e1 100644
--- a/arp.h
+++ b/arp.h
@@ -21,5 +21,6 @@ struct arpmsg {
} __attribute__((__packed__));
int arp(const struct ctx *c, struct iov_tail *data);
+void arp_send_init_req(const struct ctx *c);
#endif /* ARP_H */
diff --git a/ndp.c b/ndp.c
index eb090cd..a47fe42 100644
--- a/ndp.c
+++ b/ndp.c
@@ -438,3 +438,24 @@ void ndp_timer(const struct ctx *c, const struct timespec *now)
first:
next_ra = now->tv_sec + interval;
}
+
+/**
+ * ndp_send_init_req() - Send initial NDP NS to retrieve guest MAC address
+ * @c: Execution context
+ */
+void ndp_send_init_req(const struct ctx *c)
+{
+ struct ndp_ns ns = {
+ .ih = {
+ .icmp6_type = NS,
+ .icmp6_code = 0,
+ .icmp6_router = 0, /* Reserved */
+ .icmp6_solicited = 0, /* Reserved */
+ .icmp6_override = 0, /* Reserved */
+ },
+ .target_addr = c->ip6.addr
+ };
+ debug("Sending initial NDP NS request to retrieve"
+ " guest MAC address after reconnect");
+ ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
+}
diff --git a/ndp.h b/ndp.h
index b1dd5e8..781ea86 100644
--- a/ndp.h
+++ b/ndp.h
@@ -11,5 +11,6 @@ struct icmp6hdr;
int ndp(const struct ctx *c, const struct in6_addr *saddr,
struct iov_tail *data);
void ndp_timer(const struct ctx *c, const struct timespec *now);
+void ndp_send_init_req(const struct ctx *c);
#endif /* NDP_H */
diff --git a/passt.1 b/passt.1
index cef98b2..7000377 100644
--- a/passt.1
+++ b/passt.1
@@ -319,8 +319,9 @@ silently dropped.
.TP
.BR \-\-no-icmp
-Disable the ICMP/ICMPv6 echo handler. ICMP and ICMPv6 echo requests coming from
-guest or target namespace will be silently dropped.
+Disable the ICMP/ICMPv6 handler, and hence also NDP. ICMP and ICMPv6 requests
+coming from guest or target namespace will be silently dropped. No initial NDP
+message will be sent.
.TP
.BR \-\-no-dhcp
@@ -330,8 +331,8 @@ selected IPv4 default route.
.TP
.BR \-\-no-ndp
-Disable NDP responses. NDP messages coming from guest or target namespace will
-be ignored.
+Disable Neighbor Discovery. NDP messages coming from guest or target
+namespace will be ignored. No initial NDP message will be sent.
.TP
.BR \-\-no-dhcpv6
diff --git a/tap.c b/tap.c
index 7ba6399..25c32f9 100644
--- a/tap.c
+++ b/tap.c
@@ -1096,7 +1096,9 @@ void tap_add_packet(struct ctx *c, struct iov_tail *data,
return;
if (memcmp(c->guest_mac, eh->h_source, ETH_ALEN)) {
+ char bufmac[ETH_ADDRSTRLEN];
memcpy(c->guest_mac, eh->h_source, ETH_ALEN);
+ info("Guest MAC address: %s", eth_ntop(c->guest_mac, bufmac, sizeof(bufmac)));
proto_update_l2_buf(c->guest_mac, NULL);
}
@@ -1355,6 +1357,11 @@ static void tap_start_connection(const struct ctx *c)
ev.events = EPOLLIN | EPOLLRDHUP;
ev.data.u64 = ref.u64;
epoll_ctl(c->epollfd, EPOLL_CTL_ADD, c->fd_tap, &ev);
+
+ if (c->ifi4)
+ arp_send_init_req(c);
+ if (c->ifi6 && !c->no_ndp && !c->no_icmp)
+ ndp_send_init_req(c);
}
/**
@@ -1503,11 +1510,11 @@ void tap_backend_init(struct ctx *c)
case MODE_PASST:
tap_sock_unix_init(c);
- /* In passt mode, we don't know the guest's MAC address until it
- * sends us packets. Use the broadcast address so that our
- * first packets will reach it.
+ /* In passt mode, we don't know the guest's MAC address until
+ * it sends us packets. Until then, use the broadcast address
+ * so that our first packets will have a chance to reach it.
*/
- memset(&c->guest_mac, 0xff, sizeof(c->guest_mac));
+ memcpy(&c->guest_mac, MAC_BROADCAST, sizeof(c->guest_mac));
break;
}
diff --git a/util.h b/util.h
index 2a8c38f..22eaac5 100644
--- a/util.h
+++ b/util.h
@@ -97,6 +97,8 @@ void abort_with_msg(const char *fmt, ...)
#define FD_PROTO(x, proto) \
(IN_INTERVAL(c->proto.fd_min, c->proto.fd_max, (x)))
+#define MAC_BROADCAST \
+ ((uint8_t [ETH_ALEN]){ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff })
#define MAC_ZERO ((uint8_t [ETH_ALEN]){ 0 })
#define MAC_IS_ZERO(addr) (!memcmp((addr), MAC_ZERO, ETH_ALEN))
--
@@ -97,6 +97,8 @@ void abort_with_msg(const char *fmt, ...)
#define FD_PROTO(x, proto) \
(IN_INTERVAL(c->proto.fd_min, c->proto.fd_max, (x)))
+#define MAC_BROADCAST \
+ ((uint8_t [ETH_ALEN]){ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff })
#define MAC_ZERO ((uint8_t [ETH_ALEN]){ 0 })
#define MAC_IS_ZERO(addr) (!memcmp((addr), MAC_ZERO, ETH_ALEN))
--
2.47.3
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP
2025-09-10 11:35 [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP Volker Diels-Grabsch
2025-09-10 11:35 ` [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP address Volker Diels-Grabsch
@ 2025-09-10 14:10 ` Stefano Brivio
1 sibling, 0 replies; 4+ messages in thread
From: Stefano Brivio @ 2025-09-10 14:10 UTC (permalink / raw)
To: Volker Diels-Grabsch; +Cc: passt-dev
On Wed, 10 Sep 2025 13:35:40 +0200
Volker Diels-Grabsch <v@njh.eu> wrote:
> Changes from PATCH v3: (... which wasn't marked as "v3", sorry for that)
>
> 1. Simply remove the "e.g." part from the comment, as it's misleading anyways
> 2. Make "bufmac[]" as local as possible
> 3. Demote the "Sending initial ..." log message from info() to debug()
> 4. Log "Sending initial ..." separately for ARP and NDP (because of 5. and 6.)
> 5. Send initial ARP request only if IPv4 is enabled
> 6. Send initial NDP request only if IPv6, NDP and ICMP are enabled
> 7. Update description of "--no-ndp" in man page
> 8. Fix and update description of "--no-icmp" in man page
> 9. Break long line for MAC_BROADCAST
By the way, same as most kernel subsystems, we commonly write change
revisions after the marker for the end of commit message, say:
text text text
Tag: x
Signed-off-by: y
---
v3:
- a
- b
- c
or whatever variation one might find convenient. But it's nice to have
it there, because it's not polluting the commit message, and at the
same time it comes in the same email as the change.
This is different from Linux kernel net.git / net-next.git submissions,
where there's a general request to include version changes in commit
messages, but I don't think that's a good idea, as sometimes net.git
commit messages get rather long and unreadable because of that.
> One final thought regarding 8.:
>
> Would it make sense to state in the man page that "--no-icmp" implies
> "--no-ndp", and to implement it rigorously that way? Or, would that
> be going to far?
I'm not sure if it's going too far, but given that we currently disable
just a bit of NDP on --no-icmp, I'd say it's better to make it
consistent at this point with the strict interpretation you added in v4.
--
Stefano
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP address
2025-09-10 11:35 ` [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP address Volker Diels-Grabsch
@ 2025-09-10 14:32 ` Stefano Brivio
0 siblings, 0 replies; 4+ messages in thread
From: Stefano Brivio @ 2025-09-10 14:32 UTC (permalink / raw)
To: Volker Diels-Grabsch; +Cc: passt-dev
On Wed, 10 Sep 2025 13:35:42 +0200
Volker Diels-Grabsch <v@njh.eu> wrote:
> When restarting passt while QEMU keeps running with a configured
> "reconnect-ms" setting, the port forwardings will stop working until
> the guest sends some outgoing network traffic.
>
> Reason: Although QEMU reconnects successfully to the unix domain
> socket of the new passt process, that one no longer knows the guest's
> MAC address and uses instead the broadcast MAC address. However, this
> is ignored by the guest, at least if the guest runs Linux. Only after
> the guest sends some network package on its own initiative, passt will
> know the MAC address and will be able to establish forwarded
> connections.
>
> This change fixes this issue by sending an ARP and an NDP request to
> resolve the guest's MAC address via its IPv4 and IPv6 address, which
> we do know, right after the unix domain socket (re)connection.
>
> The only case where the IP is "wrong" would be if the configuration
> changed, or on the very first start right after qemu started. But in
> those cases, we just wouldn't get an ARP/NDP response, and can't do
> anything until we receive the guest's DHCP request - just as before.
> In other words, in the worst case the ARP/NDP requests would be
> harmless.
>
> Signed-off-by: Volker Diels-Grabsch <v@njh.eu>
> ---
> arp.c | 35 +++++++++++++++++++++++++++++++++++
> arp.h | 1 +
> ndp.c | 21 +++++++++++++++++++++
> ndp.h | 1 +
> passt.1 | 9 +++++----
> tap.c | 15 +++++++++++----
> util.h | 2 ++
> 7 files changed, 76 insertions(+), 8 deletions(-)
>
> diff --git a/arp.c b/arp.c
> index 44677ad..b263419 100644
> --- a/arp.c
> +++ b/arp.c
> @@ -112,3 +112,38 @@ int arp(const struct ctx *c, struct iov_tail *data)
>
> return 1;
> }
> +
> +/**
> + * arp_send_init_req() - Send initial ARP request to retrieve guest MAC address
> + * @c: Execution context
> + */
> +void arp_send_init_req(const struct ctx *c)
> +{
> + struct {
> + struct ethhdr eh;
> + struct arphdr ah;
> + struct arpmsg am;
> + } __attribute__((__packed__)) req;
> +
> + /* Ethernet header */
> + req.eh.h_proto = htons(ETH_P_ARP);
> + memcpy(req.eh.h_dest, MAC_BROADCAST, sizeof(req.eh.h_dest));
> + memcpy(req.eh.h_source, c->our_tap_mac, sizeof(req.eh.h_source));
> +
> + /* ARP header */
> + req.ah.ar_op = htons(ARPOP_REQUEST);
> + req.ah.ar_hrd = htons(ARPHRD_ETHER);
> + req.ah.ar_pro = htons(ETH_P_IP);
> + req.ah.ar_hln = ETH_ALEN;
> + req.ah.ar_pln = 4;
> +
> + /* ARP message */
> + memcpy(req.am.sha, c->our_tap_mac, sizeof(req.am.sha));
> + memcpy(req.am.sip, &c->ip4.our_tap_addr, sizeof(req.am.sip));
> + memcpy(req.am.tha, MAC_BROADCAST, sizeof(req.am.tha));
> + memcpy(req.am.tip, &c->ip4.addr, sizeof(req.am.tip));
> +
> + debug("Sending initial ARP request to retrieve"
> + " guest MAC address after reconnect");
I should have mentioned this along with an earlier comment of mine I
guess: user-visible strings are an exception to the 80-column limit,
rationale:
https://docs.kernel.org/process/coding-style.html#breaking-long-lines-and-strings
...by the way, note that this doesn't necessarily happen after a
reconnection, because if the user specifies a given address for the
guest (-a 192.0.2.1), the ARP request here makes sense even on the
first connection. So maybe you could just say something like:
debug("Sending initial ARP request for guest MAC address");
?
> + tap_send_single(c, &req, sizeof(req));
> +}
> diff --git a/arp.h b/arp.h
> index 86bcbf8..d5ad0e1 100644
> --- a/arp.h
> +++ b/arp.h
> @@ -21,5 +21,6 @@ struct arpmsg {
> } __attribute__((__packed__));
>
> int arp(const struct ctx *c, struct iov_tail *data);
> +void arp_send_init_req(const struct ctx *c);
>
> #endif /* ARP_H */
> diff --git a/ndp.c b/ndp.c
> index eb090cd..a47fe42 100644
> --- a/ndp.c
> +++ b/ndp.c
> @@ -438,3 +438,24 @@ void ndp_timer(const struct ctx *c, const struct timespec *now)
> first:
> next_ra = now->tv_sec + interval;
> }
> +
> +/**
> + * ndp_send_init_req() - Send initial NDP NS to retrieve guest MAC address
> + * @c: Execution context
> + */
> +void ndp_send_init_req(const struct ctx *c)
> +{
> + struct ndp_ns ns = {
> + .ih = {
> + .icmp6_type = NS,
> + .icmp6_code = 0,
> + .icmp6_router = 0, /* Reserved */
> + .icmp6_solicited = 0, /* Reserved */
> + .icmp6_override = 0, /* Reserved */
> + },
> + .target_addr = c->ip6.addr
> + };
> + debug("Sending initial NDP NS request to retrieve"
> + " guest MAC address after reconnect");
Same as above.
> + ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
> +}
> diff --git a/ndp.h b/ndp.h
> index b1dd5e8..781ea86 100644
> --- a/ndp.h
> +++ b/ndp.h
> @@ -11,5 +11,6 @@ struct icmp6hdr;
> int ndp(const struct ctx *c, const struct in6_addr *saddr,
> struct iov_tail *data);
> void ndp_timer(const struct ctx *c, const struct timespec *now);
> +void ndp_send_init_req(const struct ctx *c);
>
> #endif /* NDP_H */
> diff --git a/passt.1 b/passt.1
> index cef98b2..7000377 100644
> --- a/passt.1
> +++ b/passt.1
> @@ -319,8 +319,9 @@ silently dropped.
>
> .TP
> .BR \-\-no-icmp
> -Disable the ICMP/ICMPv6 echo handler. ICMP and ICMPv6 echo requests coming from
> -guest or target namespace will be silently dropped.
> +Disable the ICMP/ICMPv6 handler, and hence also NDP.
Maybe we could make the code directly reflect this and simplify checks?
That is, in conf() (conf.c), you could set c->no_ndp if c->no_icmp
(after all options have been parsed).
> ICMP and ICMPv6 requests
> +coming from guest or target namespace will be silently dropped. No initial NDP
> +message will be sent.
>
> .TP
> .BR \-\-no-dhcp
> @@ -330,8 +331,8 @@ selected IPv4 default route.
>
> .TP
> .BR \-\-no-ndp
> -Disable NDP responses. NDP messages coming from guest or target namespace will
> -be ignored.
> +Disable Neighbor Discovery. NDP messages coming from guest or target
> +namespace will be ignored. No initial NDP message will be sent.
>
> .TP
> .BR \-\-no-dhcpv6
> diff --git a/tap.c b/tap.c
> index 7ba6399..25c32f9 100644
> --- a/tap.c
> +++ b/tap.c
> @@ -1096,7 +1096,9 @@ void tap_add_packet(struct ctx *c, struct iov_tail *data,
> return;
>
> if (memcmp(c->guest_mac, eh->h_source, ETH_ALEN)) {
> + char bufmac[ETH_ADDRSTRLEN];
Customary, for readability: one newline here (between declaration and
code).
> memcpy(c->guest_mac, eh->h_source, ETH_ALEN);
> + info("Guest MAC address: %s", eth_ntop(c->guest_mac, bufmac, sizeof(bufmac)));
This is one line you can break easily instead. But I don't think it
should be info(), otherwise the guest can happily spam system logs on
the host.
I'm undecided between suggesting debug() or trace(), because on one
hand it's a rather relevant information (maybe say "New guest MAC
address"?), on the other hand nothing prevents a guest from using a
different MAC address every other frame, and make debugging with --debug
impossible if this happens.
We don't have (yet) a generic rate limiting mechanism for prints.
Maybe we could go with trace() for the moment, and the day we have it,
switch this to a rate-limited debug() (or even info(), at that point).
> proto_update_l2_buf(c->guest_mac, NULL);
> }
>
> @@ -1355,6 +1357,11 @@ static void tap_start_connection(const struct ctx *c)
> ev.events = EPOLLIN | EPOLLRDHUP;
> ev.data.u64 = ref.u64;
> epoll_ctl(c->epollfd, EPOLL_CTL_ADD, c->fd_tap, &ev);
> +
> + if (c->ifi4)
> + arp_send_init_req(c);
> + if (c->ifi6 && !c->no_ndp && !c->no_icmp)
...here you could just check for c->no_ndp, if you set it on
c->no_icmp.
> + ndp_send_init_req(c);
> }
>
> /**
> @@ -1503,11 +1510,11 @@ void tap_backend_init(struct ctx *c)
> case MODE_PASST:
> tap_sock_unix_init(c);
>
> - /* In passt mode, we don't know the guest's MAC address until it
> - * sends us packets. Use the broadcast address so that our
> - * first packets will reach it.
> + /* In passt mode, we don't know the guest's MAC address until
> + * it sends us packets. Until then, use the broadcast address
> + * so that our first packets will have a chance to reach it.
> */
> - memset(&c->guest_mac, 0xff, sizeof(c->guest_mac));
> + memcpy(&c->guest_mac, MAC_BROADCAST, sizeof(c->guest_mac));
> break;
> }
>
> diff --git a/util.h b/util.h
> index 2a8c38f..22eaac5 100644
> --- a/util.h
> +++ b/util.h
> @@ -97,6 +97,8 @@ void abort_with_msg(const char *fmt, ...)
> #define FD_PROTO(x, proto) \
> (IN_INTERVAL(c->proto.fd_min, c->proto.fd_max, (x)))
>
> +#define MAC_BROADCAST \
> + ((uint8_t [ETH_ALEN]){ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff })
> #define MAC_ZERO ((uint8_t [ETH_ALEN]){ 0 })
> #define MAC_IS_ZERO(addr) (!memcmp((addr), MAC_ZERO, ETH_ALEN))
>
The rest all looks good to me.
--
Stefano
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2025-09-10 14:32 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-09-10 11:35 [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP Volker Diels-Grabsch
2025-09-10 11:35 ` [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP address Volker Diels-Grabsch
2025-09-10 14:32 ` Stefano Brivio
2025-09-10 14:10 ` [PATCH v4] Send an initial ARP and NDP request to resolve the guest IP Stefano Brivio
Code repositories for project(s) associated with this public inbox
https://passt.top/passt
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 IMAP folder(s).