dev
Threads by month
- ----- 2026 -----
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2025 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- 8 participants
- 1798 discussions
Hi!
I am currently working on a project involving `crosvm`. To allow for
rootless networking plumbing I am relying on `passt` (ver
2026_10_02.cba3570).
I have found that prolonged (>5 seconds, max. 20 seconds) `https`
transfers crash with `crosvm` claiming that the backend device (`passt`)
was exiting early.
In the instant the program crashes I find this in my dmesg:
```
[Sun Nov 8 21:26:32 2026] audit: type=1326 audit(1794169582.685:13):
auid=1005 uid=1005 gid=998 ses=1 subj=unconfined pid=15127
comm="passt.avx2"
exe="/gnu/store/cjdgl8d27afhnw34x2z3pwmaxa4sw4jg-passt-2026_10_02.cba3570/bin/passt.avx2"
sig=31 arch=c000003e syscall=318 compat=0 ip=0x7f87c63b8735
code=0x80000000
```
Debugging further my friend KI/AI, they said this expected under the
seccomp confinement (no get_random, no brk).
Rebuilding with `EXTRA_SYSCALLS=getrandom brk`, shows:
```
60.4309: Flow 2 (TCP connection): STALLED dropped
passt.avx2: tcp_vu.c:294: tcp_vu_sock_recv: Assertion `cnt <
ARRAY_SIZE(iov_msg) - j' failed.
Bad system call
```
The log:
```
... spamy output:
342.5609: Flow 2 (TCP connection): timer expires in 1.000s
342.5609: Flow 2 (TCP connection): STALLED
342.5612: Flow 2 (TCP connection): ACK_FROM_TAP_DUE dropped
342.5612: Flow 2 (TCP connection): STALLED dropped
342.5645: Flow 2 (TCP connection): ACK_FROM_TAP_DUE
342.5646: Flow 2 (TCP connection): timer expires in 1.000s
342.5646: Flow 2 (TCP connection): STALLED
342.5647: Flow 2 (TCP connection): ACK_FROM_TAP_DUE dropped
342.5647: Flow 2 (TCP connection): STALLED dropped
... lots more of the same spammy output...
343.0239: Flow 2 (TCP connection): timer expires in 1.000s
343.0249: Flow 2 (TCP connection): timer expires in 1.000s
343.0250: Flow 2 (TCP connection): timer expires in 1.000s
343.0252: Flow 2 (TCP connection): STALLED
343.0252: Flow 2 (TCP connection): ACK_FROM_TAP_DUE dropped
343.0252: Flow 2 (TCP connection): STALLED dropped
343.0257: Flow 2 (TCP connection): ACK_FROM_TAP_DUE
343.0257: Flow 2 (TCP connection): timer expires in 1.000s
343.0258: Flow 2 (TCP connection): STALLED
343.0259: Flow 2 (TCP connection): STALLED dropped
343.0260: Flow 2 (TCP connection): timer expires in 1.000s
343.0261: Flow 2 (TCP connection): ACK_FROM_TAP_DUE dropped
passt.avx2: tcp_vu.c:294: tcp_vu_sock_recv: Assertion `cnt <
ARRAY_SIZE(iov_msg) - j' failed.
Bad system call
```
Working diagnosis:
1. passt's tcp_vu.c assert in tcp_vu_sock_recv() compares a signed cnt
against a size_t capacity, so a negative error return trips the assert
and kills passt — which crosvm then reports as the backend disconnecting
and stops the VM — while the underlying iov_tail_clone() failure never
gets printed;
2. seccomp profiles omit getrandom/brk/gettid, so the process dies under
SECCOMP_RET_KILL with no message at all.
This was all I had time for today, hope this helps!
Kind regards,
John
2
1
Fixes: #221
Eliminate the route entries capacity limitations of nl_route_dup
by copping them to the destination namespace in three successive
steps to bypass dependency issues. This approach is taken from
the handling used in the restore subcommand of iproute2.
Signed-off-by: Martin Schitter <ms+git(a)mur.at>
---
netlink.c | 207 ++++++++++++++++++++++++++----------------------------
1 file changed, 99 insertions(+), 108 deletions(-)
diff --git a/netlink.c b/netlink.c
index 225b666..803cb20 100644
--- a/netlink.c
+++ b/netlink.c
@@ -525,6 +525,20 @@ int nl_route_set_def(int s, unsigned int ifi, sa_family_t af, const void *gw)
return nl_do(s, &req, RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, len);
}
+/**
+ * nl_rtattr_cmp() -- compare two routing attributes
+ * @rta1: Routing attribute
+ * @rta2: Routing attribute
+ *
+ * Return: 0 if equal
+ */
+static int nl_rtattr_cmp(const struct rtattr *rta1, const struct rtattr *rta2)
+{
+ if (!rta1 || !rta2 || rta1->rta_len != rta2->rta_len)
+ return 1;
+ return memcmp(RTA_DATA(rta1), RTA_DATA(rta2), RTA_PAYLOAD(rta1));
+}
+
/**
* nl_route_dup() - Copy routes for given interface and address family
* @s_src: Netlink socket in source namespace
@@ -535,56 +549,73 @@ int nl_route_set_def(int s, unsigned int ifi, sa_family_t af, const void *gw)
*
* Return: 0 on success, negative error code on failure
*/
-int nl_route_dup(int s_src, unsigned int ifi_src,
- int s_dst, unsigned int ifi_dst, sa_family_t af)
+int nl_route_dup(int s_src, unsigned int ifi_src, int s_dst,
+ unsigned int ifi_dst, sa_family_t af)
{
- struct req_t {
- struct nlmsghdr nlh;
- struct rtmsg rtm;
- struct rtattr rta;
- unsigned int ifi;
- } req = {
- .rtm.rtm_family = af,
- .rtm.rtm_table = RT_TABLE_MAIN,
- .rtm.rtm_scope = RT_SCOPE_UNIVERSE,
- .rtm.rtm_type = RTN_UNICAST,
+ /* Duplicate routes in correct order using 3 succesive stages:
+ * 0. ones for local addresses,
+ * 1. ones for local networks,
+ * 2. others (remote networks/hosts).
+ */
+ for (int prio = 0; prio < 3; prio++) {
+ struct req_t {
+ struct nlmsghdr nlh;
+ struct rtmsg rtm;
+ struct rtattr rta;
+ unsigned int ifi;
+ } req = {
+ .rtm.rtm_family = af,
+ .rtm.rtm_table = RT_TABLE_MAIN,
+ .rtm.rtm_scope = RT_SCOPE_UNIVERSE,
+ .rtm.rtm_type = RTN_UNICAST,
+
+ .rta.rta_type = RTA_OIF,
+ .rta.rta_len = RTA_LENGTH(sizeof(unsigned int)),
+ .ifi = ifi_src,
+ };
+ ssize_t status;
+ struct nlmsghdr *nh;
+ char buf[NLBUFSIZ];
+ uint32_t seq;
+
+ seq = nl_send(s_src, &req, RTM_GETROUTE, NLM_F_DUMP,
+ sizeof(req));
+
+ nl_foreach(nh, status, s_src, buf, seq) {
+ struct rtmsg *rtm = (struct rtmsg *)NLMSG_DATA(nh);
+ struct rtattr *rta;
+ size_t na;
+ uint16_t flags = nh->nlmsg_flags;
+ int rc;
- .rta.rta_type = RTA_OIF,
- .rta.rta_len = RTA_LENGTH(sizeof(unsigned int)),
- .ifi = ifi_src,
- };
- ssize_t nlmsgs_size, left, status;
- unsigned dup_routes = 0;
- struct nlmsghdr *nh;
- char buf[NLBUFSIZ];
- uint32_t seq;
- unsigned i;
- seq = nl_send(s_src, &req, RTM_GETROUTE, NLM_F_DUMP, sizeof(req));
+ if (nh->nlmsg_type != RTM_NEWROUTE)
+ continue;
- /* nl_foreach() will step through multiple response datagrams,
- * which we don't want here because we need to have all the
- * routes in the buffer at once.
- */
- nh = nl_next(s_src, buf, NULL, &nlmsgs_size);
- for (left = nlmsgs_size;
- NLMSG_OK(nh, left) && (status = nl_status(nh, left, seq)) > 0;
- nh = NLMSG_NEXT(nh, left)) {
- struct rtmsg *rtm = (struct rtmsg *)NLMSG_DATA(nh);
- bool discard = false;
- struct rtattr *rta;
- size_t na;
+ struct rtattr *tb[RTA_MAX+1] = { 0 };
- if (nh->nlmsg_type != RTM_NEWROUTE)
- continue;
+ for (rta = RTM_RTA(rtm), na = RTM_PAYLOAD(nh);
+ RTA_OK(rta, na); rta = RTA_NEXT(rta, na)) {
+ tb[rta->rta_type] = rta;
+ }
- /* nexthop state flags don't apply to freshly created routes,
- * and the kernel will refuse our route if they are set.
- */
- rtm->rtm_flags &= ~RTNH_COMPARE_MASK;
+ /* Process only matching routes of current priority
+ * (variant of an approach used in iproute2s restore_handler)
+ */
+ if (!(prio == 2 && tb[RTA_GATEWAY]) &&
+ !(prio == 1 && !tb[RTA_GATEWAY] &&
+ tb[RTA_PREFSRC] &&
+ nl_rtattr_cmp(tb[RTA_PREFSRC], tb[RTA_DST])) &&
+ !(prio == 0 && !tb[RTA_GATEWAY] &&
+ (!tb[RTA_PREFSRC] ||
+ !nl_rtattr_cmp(tb[RTA_PREFSRC], tb[RTA_DST]))))
+ continue;
+
+ /* nexthop state flags don't apply to freshly created routes,
+ * and the kernel will refuse our route if they are set.
+ */
+ rtm->rtm_flags &= ~RTNH_COMPARE_MASK;
- for (rta = RTM_RTA(rtm), na = RTM_PAYLOAD(nh); RTA_OK(rta, na);
- rta = RTA_NEXT(rta, na)) {
/* RTA_OIF and RTA_MULTIPATH attributes carry the
* identifier of a host interface. If they match the
* host interface we're copying from, change them to
@@ -595,103 +626,63 @@ int nl_route_dup(int s_src, unsigned int ifi_src,
* available), or if any interface index in nexthop
* objects differ from the host interface, discard the
* route altogether.
- */
- if (rta->rta_type == RTA_OIF) {
- if (*(unsigned int *)RTA_DATA(rta) != ifi_src) {
- discard = true;
- break;
+ */
+ if (tb[RTA_OIF]) {
+ if (*(unsigned int *)RTA_DATA(tb[RTA_OIF]) !=
+ ifi_src) {
+ continue;
}
-
- *(unsigned int *)RTA_DATA(rta) = ifi_dst;
- } else if (rta->rta_type == RTA_MULTIPATH) {
- int nh_len = RTA_PAYLOAD(rta);
+ *(unsigned int *)RTA_DATA(tb[RTA_OIF]) =
+ ifi_dst;
+ } else if (tb[RTA_MULTIPATH]) {
+ int nh_len = RTA_PAYLOAD(tb[RTA_MULTIPATH]);
struct rtnexthop *rtnh;
-
- for (rtnh = (struct rtnexthop *)RTA_DATA(rta);
+ bool discard = false;
+ for (rtnh = (struct rtnexthop *)
+ RTA_DATA(tb[RTA_MULTIPATH]);
RTNH_OK(rtnh, nh_len);
- rtnh = RTNH_NEXT_AND_DEC(rtnh, nh_len)) {
+ rtnh = RTNH_NEXT_AND_DEC(rtnh,
+ nh_len)) {
int src = (int)ifi_src;
-
if (rtnh->rtnh_ifindex != src) {
discard = true;
break;
}
-
rtnh->rtnh_ifindex = ifi_dst;
}
-
if (discard)
- break;
- } else if (rta->rta_type == RTA_PREFSRC ||
- rta->rta_type == RTA_NH_ID) {
+ continue;
+ } else if (tb[RTA_PREFSRC]) {
/* Strip RTA_PREFSRC attributes: host routes
* might include a preferred source address,
* which must be one of the host's addresses.
* However, with -a, pasta will use a different
* namespace address, making such a route
* invalid in the namespace.
- *
- * Strip RTA_NH_ID attributes: host routes set
+ */
+ tb[RTA_PREFSRC]->rta_type = RTA_UNSPEC;
+ } else if (tb[RTA_NH_ID]) {
+ /* Strip RTA_NH_ID attributes: host routes set
* up via routing protocols (e.g. OSPF) might
* contain a nexthop ID (and not nexthop
* objects, which are taken care of in the
* RTA_MULTIPATH case above) that's not valid
* in the target namespace.
*/
- rta->rta_type = RTA_UNSPEC;
+ tb[RTA_NH_ID]->rta_type = RTA_UNSPEC;
}
- }
-
- if (discard)
- nh->nlmsg_type = NLMSG_NOOP;
- else
- dup_routes++;
- }
-
- if (!NLMSG_OK(nh, left)) {
- /* Process any remaining datagrams in a different
- * buffer so we don't overwrite the first one.
- */
- char tail[NLBUFSIZ];
- unsigned extra = 0;
-
- nl_foreach_oftype(nh, status, s_src, tail, seq, RTM_NEWROUTE)
- extra++;
-
- if (extra) {
- err("netlink: Too many routes to duplicate");
- return -E2BIG;
- }
- }
- if (status < 0)
- return status;
-
- /* Routes might have dependencies between each other, and the kernel
- * processes RTM_NEWROUTE messages sequentially. For n routes, we might
- * need to send the requests up to n times to get all of them inserted.
- * Routes that have been already inserted will return -EEXIST, but we
- * can safely ignore that and repeat the requests. This avoids the need
- * to calculate dependencies: let the kernel do that.
- */
- for (i = 0; i < dup_routes; i++) {
- for (nh = (struct nlmsghdr *)buf, left = nlmsgs_size;
- NLMSG_OK(nh, left);
- nh = NLMSG_NEXT(nh, left)) {
- uint16_t flags = nh->nlmsg_flags;
- int rc;
-
- if (nh->nlmsg_type != RTM_NEWROUTE)
- continue;
rc = nl_do(s_dst, nh, RTM_NEWROUTE,
- (flags & ~NLM_F_DUMP_FILTERED) | NLM_F_CREATE,
+ (flags & ~NLM_F_DUMP_FILTERED) |
+ NLM_F_CREATE,
nh->nlmsg_len);
if (rc < 0 && rc != -EEXIST &&
rc != -ENETUNREACH && rc != -EHOSTUNREACH)
return rc;
}
+ if (status < 0)
+ return status;
}
-
return 0;
}
--
2.53.0
2
4
Eliminate the route entries capacity limitations of nl_route_dup
by copying them to the destination namespace in three successive
steps to bypass dependency issues. This approach is inspired by
the code design used in the restore subcommand of iproute2.
Fixes: #221
Link: https://bugs.passt.top/show_bug.cgi?id=221
Signed-off-by: Martin Schitter <ms+git(a)mur.at>
---
netlink.c | 216 +++++++++++++++++++++++++++---------------------------
1 file changed, 107 insertions(+), 109 deletions(-)
diff --git a/netlink.c b/netlink.c
index 225b666..d5c704b 100644
--- a/netlink.c
+++ b/netlink.c
@@ -525,6 +525,21 @@ int nl_route_set_def(int s, unsigned int ifi, sa_family_t af, const void *gw)
return nl_do(s, &req, RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, len);
}
+/**
+ * nl_rtattr_cmp() - Compare two routing attributes
+ * @rta1: Routing attribute
+ * @rta2: Routing attribute
+ *
+ * Return: 0 if equal
+ */
+static int nl_rtattr_cmp(const struct rtattr *rta1, const struct rtattr *rta2)
+{
+ if (!rta1 || !rta2 || rta1->rta_len != rta2->rta_len)
+ return 1;
+
+ return memcmp(RTA_DATA(rta1), RTA_DATA(rta2), RTA_PAYLOAD(rta1));
+}
+
/**
* nl_route_dup() - Copy routes for given interface and address family
* @s_src: Netlink socket in source namespace
@@ -535,56 +550,81 @@ int nl_route_set_def(int s, unsigned int ifi, sa_family_t af, const void *gw)
*
* Return: 0 on success, negative error code on failure
*/
-int nl_route_dup(int s_src, unsigned int ifi_src,
- int s_dst, unsigned int ifi_dst, sa_family_t af)
+int nl_route_dup(int s_src, unsigned int ifi_src, int s_dst,
+ unsigned int ifi_dst, sa_family_t af)
{
- struct req_t {
- struct nlmsghdr nlh;
- struct rtmsg rtm;
- struct rtattr rta;
- unsigned int ifi;
- } req = {
- .rtm.rtm_family = af,
- .rtm.rtm_table = RT_TABLE_MAIN,
- .rtm.rtm_scope = RT_SCOPE_UNIVERSE,
- .rtm.rtm_type = RTN_UNICAST,
-
- .rta.rta_type = RTA_OIF,
- .rta.rta_len = RTA_LENGTH(sizeof(unsigned int)),
- .ifi = ifi_src,
+ /* Duplicate routes in correct order using 3 successive stages: */
+ enum tasks {
+ DUP_LOCAL_ADDR, /* Duplicate local route address entries */
+ DUP_LOCAL_NET, /* Duplicate local network entries */
+ DUP_OTHER, /* Duplicate all other entries */
+ NR_OF_TASKS,
};
- ssize_t nlmsgs_size, left, status;
- unsigned dup_routes = 0;
- struct nlmsghdr *nh;
- char buf[NLBUFSIZ];
- uint32_t seq;
- unsigned i;
- seq = nl_send(s_src, &req, RTM_GETROUTE, NLM_F_DUMP, sizeof(req));
+ for (enum tasks task = DUP_LOCAL_ADDR; task < NR_OF_TASKS; task++) {
+ struct req_t {
+ struct nlmsghdr nlh;
+ struct rtmsg rtm;
+ struct rtattr rta;
+ unsigned int ifi;
+ } req = {
+ .rtm.rtm_family = af,
+ .rtm.rtm_table = RT_TABLE_MAIN,
+ .rtm.rtm_scope = RT_SCOPE_UNIVERSE,
+ .rtm.rtm_type = RTN_UNICAST,
+
+ .rta.rta_type = RTA_OIF,
+ .rta.rta_len = RTA_LENGTH(sizeof(unsigned int)),
+ .ifi = ifi_src,
+ };
+ struct nlmsghdr *nh;
+ char buf[NLBUFSIZ];
+ ssize_t status;
+ uint32_t seq;
+
+ seq = nl_send(s_src, &req, RTM_GETROUTE, NLM_F_DUMP,
+ sizeof(req));
+
+ nl_foreach(nh, status, s_src, buf, seq) {
+ struct rtmsg *rtm = (struct rtmsg *)NLMSG_DATA(nh);
+ uint16_t flags = nh->nlmsg_flags;
+ struct rtattr *rta;
+ size_t na;
+ int rc;
- /* nl_foreach() will step through multiple response datagrams,
- * which we don't want here because we need to have all the
- * routes in the buffer at once.
- */
- nh = nl_next(s_src, buf, NULL, &nlmsgs_size);
- for (left = nlmsgs_size;
- NLMSG_OK(nh, left) && (status = nl_status(nh, left, seq)) > 0;
- nh = NLMSG_NEXT(nh, left)) {
- struct rtmsg *rtm = (struct rtmsg *)NLMSG_DATA(nh);
- bool discard = false;
- struct rtattr *rta;
- size_t na;
+ if (nh->nlmsg_type != RTM_NEWROUTE)
+ continue;
- if (nh->nlmsg_type != RTM_NEWROUTE)
- continue;
+ struct rtattr *tb[RTA_MAX + 1] = { 0 };
- /* nexthop state flags don't apply to freshly created routes,
- * and the kernel will refuse our route if they are set.
- */
- rtm->rtm_flags &= ~RTNH_COMPARE_MASK;
+ for (rta = RTM_RTA(rtm), na = RTM_PAYLOAD(nh);
+ RTA_OK(rta, na); rta = RTA_NEXT(rta, na)) {
+ tb[rta->rta_type] = rta;
+ }
+
+ /* Duplicate only those routes that belong
+ * to the current processing stage.
+ * An approach inspired by iproute2's
+ * restore_handler() function.
+ * https://github.com/iproute2/iproute2/blob/e6471d7/ip/iproute.c#L2314
+ */
+ if (task == DUP_OTHER && !tb[RTA_GATEWAY])
+ continue;
+ if (task == DUP_LOCAL_NET &&
+ (tb[RTA_GATEWAY] || !tb[RTA_PREFSRC] ||
+ !nl_rtattr_cmp(tb[RTA_PREFSRC], tb[RTA_DST])))
+ continue;
+ if (task == DUP_LOCAL_ADDR && (tb[RTA_GATEWAY] ||
+ (tb[RTA_PREFSRC] &&
+ nl_rtattr_cmp(tb[RTA_PREFSRC], tb[RTA_DST]))))
+ continue;
+
+ /* nexthop state flags don't apply to freshly created
+ * routes, and the kernel will refuse our route if they
+ * are set.
+ */
+ rtm->rtm_flags &= ~RTNH_COMPARE_MASK;
- for (rta = RTM_RTA(rtm), na = RTM_PAYLOAD(nh); RTA_OK(rta, na);
- rta = RTA_NEXT(rta, na)) {
/* RTA_OIF and RTA_MULTIPATH attributes carry the
* identifier of a host interface. If they match the
* host interface we're copying from, change them to
@@ -595,103 +635,61 @@ int nl_route_dup(int s_src, unsigned int ifi_src,
* available), or if any interface index in nexthop
* objects differ from the host interface, discard the
* route altogether.
- */
- if (rta->rta_type == RTA_OIF) {
- if (*(unsigned int *)RTA_DATA(rta) != ifi_src) {
- discard = true;
- break;
- }
-
- *(unsigned int *)RTA_DATA(rta) = ifi_dst;
- } else if (rta->rta_type == RTA_MULTIPATH) {
- int nh_len = RTA_PAYLOAD(rta);
+ */
+ if (tb[RTA_OIF]) {
+ if (*(unsigned int *)RTA_DATA(tb[RTA_OIF]) !=
+ ifi_src)
+ continue;
+ *(unsigned int *)RTA_DATA(tb[RTA_OIF]) =
+ ifi_dst;
+ } else if (tb[RTA_MULTIPATH]) {
+ int nh_len = RTA_PAYLOAD(tb[RTA_MULTIPATH]);
struct rtnexthop *rtnh;
-
- for (rtnh = (struct rtnexthop *)RTA_DATA(rta);
+ bool discard = false;
+ for (rtnh = (struct rtnexthop *)
+ RTA_DATA(tb[RTA_MULTIPATH]);
RTNH_OK(rtnh, nh_len);
rtnh = RTNH_NEXT_AND_DEC(rtnh, nh_len)) {
int src = (int)ifi_src;
-
if (rtnh->rtnh_ifindex != src) {
discard = true;
break;
}
-
rtnh->rtnh_ifindex = ifi_dst;
}
-
if (discard)
- break;
- } else if (rta->rta_type == RTA_PREFSRC ||
- rta->rta_type == RTA_NH_ID) {
+ continue;
+ } else if (tb[RTA_PREFSRC]) {
/* Strip RTA_PREFSRC attributes: host routes
* might include a preferred source address,
* which must be one of the host's addresses.
* However, with -a, pasta will use a different
* namespace address, making such a route
* invalid in the namespace.
- *
- * Strip RTA_NH_ID attributes: host routes set
+ */
+ tb[RTA_PREFSRC]->rta_type = RTA_UNSPEC;
+ } else if (tb[RTA_NH_ID]) {
+ /* Strip RTA_NH_ID attributes: host routes set
* up via routing protocols (e.g. OSPF) might
* contain a nexthop ID (and not nexthop
* objects, which are taken care of in the
* RTA_MULTIPATH case above) that's not valid
* in the target namespace.
*/
- rta->rta_type = RTA_UNSPEC;
+ tb[RTA_NH_ID]->rta_type = RTA_UNSPEC;
}
- }
-
- if (discard)
- nh->nlmsg_type = NLMSG_NOOP;
- else
- dup_routes++;
- }
-
- if (!NLMSG_OK(nh, left)) {
- /* Process any remaining datagrams in a different
- * buffer so we don't overwrite the first one.
- */
- char tail[NLBUFSIZ];
- unsigned extra = 0;
-
- nl_foreach_oftype(nh, status, s_src, tail, seq, RTM_NEWROUTE)
- extra++;
-
- if (extra) {
- err("netlink: Too many routes to duplicate");
- return -E2BIG;
- }
- }
- if (status < 0)
- return status;
-
- /* Routes might have dependencies between each other, and the kernel
- * processes RTM_NEWROUTE messages sequentially. For n routes, we might
- * need to send the requests up to n times to get all of them inserted.
- * Routes that have been already inserted will return -EEXIST, but we
- * can safely ignore that and repeat the requests. This avoids the need
- * to calculate dependencies: let the kernel do that.
- */
- for (i = 0; i < dup_routes; i++) {
- for (nh = (struct nlmsghdr *)buf, left = nlmsgs_size;
- NLMSG_OK(nh, left);
- nh = NLMSG_NEXT(nh, left)) {
- uint16_t flags = nh->nlmsg_flags;
- int rc;
-
- if (nh->nlmsg_type != RTM_NEWROUTE)
- continue;
rc = nl_do(s_dst, nh, RTM_NEWROUTE,
- (flags & ~NLM_F_DUMP_FILTERED) | NLM_F_CREATE,
- nh->nlmsg_len);
+ (flags & ~NLM_F_DUMP_FILTERED) |
+ NLM_F_CREATE,
+ nh->nlmsg_len);
if (rc < 0 && rc != -EEXIST &&
rc != -ENETUNREACH && rc != -EHOSTUNREACH)
return rc;
}
+ if (status < 0)
+ return status;
}
-
return 0;
}
--
2.53.0
1
0
[PATCH v2 3/7] virtio: Add the pasta vhost-net interface and implementation
by aerosouund 06 Nov '26
by aerosouund 06 Nov '26
06 Nov '26
From: Ammar Yasser <aerosound161(a)gmail.com>
Add vhost.c and vhost.h which define the api and implementation for
pasta vhost acceleration
- vq_state/vhost_vq_state[]: our own view of each virtqueue, which the
kernel doesn't track for us: descriptors ready to hand back, how far
we've read the used ring, and the next descriptor to allocate
- vring_desc, vring_avail_all, vring_used_all, vhost_memory: the rings
and the region table shared with the kernel
- vhost_setup_net: open /dev/vhost-net and negotiate VIRTIO_F_VERSION_1
and VHOST_NET_F_VIRTIO_NET_HDR. Failure is fatal only if
--vhost-kernel is set to 'on'.
- vhost_setup_eventfds: create, register and watch the call, kick and
error eventfds for one queue
- vhost_setup_memory_table: register the buffers the kernel may touch.
pasta has no guest, so each region maps guest_phys_addr onto the
identical userspace_addr, making GPA translation an identity mapping.
This function uses the *_register_memory_regions functions created per
protocol and for general purpose buffers.
- vhost_set_vring: publish a vring's addresses, bind the tap fd as its
backend and lay out its descriptors
- vhost_rx_descriptor_handoff, vhost_kick: hand consumed descriptors back
and notify the kernel unless it says it's already polling
Signed-off-by: Eugenio Pérez <eperezma(a)redhat.com>
Signed-off-by: Ammar Yasser <aerosound161(a)gmail.com>
---
Makefile | 2 +-
tcp_buf.c | 22 +++-
tcp_buf.h | 4 +
udp.c | 20 ++++
udp.h | 3 +
util.c | 18 ++++
util.h | 2 +
vhost.c | 295 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
vhost.h | 91 +++++++++++++++++
9 files changed, 455 insertions(+), 2 deletions(-)
create mode 100644 vhost.c
create mode 100644 vhost.h
diff --git a/Makefile b/Makefile
index f6c87b1..6f11e4b 100644
--- a/Makefile
+++ b/Makefile
@@ -38,7 +38,7 @@ PASST_SRCS = arch.c arp.c bitmap.c checksum.c conf.c dhcp.c dhcpv6.c \
isolation.c lineread.c log.c mld.c ndp.c netlink.c migrate.c packet.c \
parse.c passt.c pasta.c pcap.c pif.c repair.c serialise.c tap.c tcp.c \
tcp_buf.c tcp_splice.c tcp_vu.c udp.c udp_flow.c udp_vu.c util.c \
- vhost_user.c virtio.c vu_common.c
+ vhost.c vhost_user.c virtio.c vu_common.c
PASST_REPAIR_SRCS = passt-repair.c
PESTO_SRCS = pesto.c bitmap.c fwd_rule.c inany.c ip.c lineread.c parse.c \
serialise.c
diff --git a/tcp_buf.c b/tcp_buf.c
index 72c4541..dd80384 100644
--- a/tcp_buf.c
+++ b/tcp_buf.c
@@ -31,12 +31,14 @@
#include "inany.h"
#include "tcp_conn.h"
#include "tcp_internal.h"
-#include "tcp_buf.h"
+#include "vhost.h"
#define TCP_FRAMES_MEM 128
#define TCP_FRAMES \
(c->mode == MODE_PASTA ? 1 : TCP_FRAMES_MEM)
+#define TCP_NREGIONS 5
+
/* Static buffers */
/* Ethernet header for IPv4 and IPv6 frames */
@@ -75,6 +77,24 @@ void tcp_update_l2_buf(const unsigned char *eth_d)
eth_update_mac(&tcp_eth_hdr[i], eth_d, NULL);
}
+/**
+ * tcp_register_memory_region() - Register the TCP specific buffers into a memory
+ * struct so it can be shared with the kernel.
+ * @vhost_mem: The memory struct to register regions into
+ * @last_idx: The last index at which memory region has been placed
+ */
+void tcp_register_memory_regions(union vhost_memory_u *vhost_mem, size_t *last_idx) {
+ if ((*last_idx + TCP_NREGIONS) > N_VHOST_REGIONS)
+ die("tcp: memory region overflow. maximum is %d, got %zu",
+ N_VHOST_REGIONS, *last_idx + TCP_NREGIONS);
+
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(tcp_payload_tap_hdr);
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(tcp4_payload_ip);
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(tcp6_payload_ip);
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(tcp_payload);
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(tcp_eth_hdr);
+}
+
/**
* tcp_sock_iov_init() - Initialise scatter-gather L2 buffers for IPv4 sockets
* @c: Execution context
diff --git a/tcp_buf.h b/tcp_buf.h
index 5d31cea..5e341ed 100644
--- a/tcp_buf.h
+++ b/tcp_buf.h
@@ -6,11 +6,15 @@
#ifndef TCP_BUF_H
#define TCP_BUF_H
+union vhost_memory_u;
+struct tcp_tap_conn;
+
void tcp_sock_iov_init(const struct ctx *c);
void tcp_payload_flush(const struct ctx *c, const struct timespec *now);
int tcp_buf_data_from_sock(const struct ctx *c, struct tcp_tap_conn *conn,
uint32_t already_sent, const struct timespec *now);
int tcp_buf_send_flag(const struct ctx *c, struct tcp_tap_conn *conn, int flags,
const struct timespec *now);
+void tcp_register_memory_regions(union vhost_memory_u *vhost_mem, size_t *last_idx);
#endif /*TCP_BUF_H */
diff --git a/udp.c b/udp.c
index 505e554..a4355a1 100644
--- a/udp.c
+++ b/udp.c
@@ -118,6 +118,7 @@
#include "udp_internal.h"
#include "udp_vu.h"
#include "epoll_ctl.h"
+#include "vhost.h"
#define UDP_MAX_FRAMES 32 /* max # of frames to receive at once */
@@ -128,6 +129,8 @@
#define UDP_TIMEOUT_DEFAULT 30 /* s */
#define UDP_TIMEOUT_STREAM_DEFAULT 120 /* s */
+#define UDP_NREGIONS 5
+
/* Maximum UDP data to be returned in ICMP messages */
#define ICMP4_MAX_DLEN 8
#define ICMP6_MAX_DLEN (IPV6_MIN_MTU \
@@ -448,6 +451,23 @@ static void udp_send_tap_icmp4(const struct ctx *c,
tap_icmp4_send(c, saddr, eaddr, &msg, tap_omac, msglen);
}
+/**
+ * udp_register_memory_region() - Register the UDP specific buffers into a memory
+ * struct so it can be shared with the kernel.
+ * @vhost_mem: The memory struct to register regions into
+ * @last_idx: The last index at which memory region has been placed
+ */
+void udp_register_memory_regions(union vhost_memory_u *vhost_mem, size_t *last_idx) {
+ if ((*last_idx + UDP_NREGIONS) > N_VHOST_REGIONS)
+ die("udp: memory region overflow. maximum is %d, got %zu",
+ N_VHOST_REGIONS, *last_idx + UDP_NREGIONS);
+
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(udp_payload);
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(udp_eth_hdr);
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(udp_iov_recv);
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(udp_mh_recv);
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(udp_meta);
+}
/**
* udp_send_tap_icmp6() - Construct and send ICMPv6 to local peer
diff --git a/udp.h b/udp.h
index b50283e..ebf1a4e 100644
--- a/udp.h
+++ b/udp.h
@@ -11,6 +11,8 @@
#include "fwd.h"
+union vhost_memory_u;
+
void udp_listen_sock_handler(const struct ctx *c, union epoll_ref ref,
uint32_t events, const struct timespec *now);
void udp_sock_handler(const struct ctx *c, union epoll_ref ref,
@@ -21,6 +23,7 @@ int udp_tap_handler(const struct ctx *c, uint8_t pif,
const struct timespec *now);
int udp_init(struct ctx *c);
void udp_update_l2_buf(const unsigned char *eth_d);
+void udp_register_memory_regions(union vhost_memory_u *vhost_mem, size_t *last_idx);
/**
* struct udp_ctx - Execution context for UDP
diff --git a/util.c b/util.c
index ff1f19f..cafe0ec 100644
--- a/util.c
+++ b/util.c
@@ -33,10 +33,13 @@
#include "log.h"
#include "pcap.h"
#include "serialise.h"
+#include "vhost.h"
#ifdef HAS_GETRANDOM
#include <sys/random.h>
#endif
+#define GENERAL_NREGIONS 2
+
/* Zero-filled buffer to pad 802.3 frames, up to 60 (ETH_ZLEN) bytes */
uint8_t eth_pad[ETH_ZLEN] = { 0 };
@@ -453,6 +456,21 @@ int open_in_ns(const struct ctx *c, const char *path, int flags)
return arg.fd;
}
+/**
+ * general_register_memory_region() - Register the general (non protocol specific) buffers
+ * into a memory struct so it can be shared with the kernel.
+ * @vhost_mem: The memory struct to register regions into
+ * @last_idx: The last index at which memory region has been placed
+ */
+void general_register_memory_regions(union vhost_memory_u *vhost_mem, size_t *last_idx) {
+ if ((*last_idx + GENERAL_NREGIONS) > N_VHOST_REGIONS)
+ die("general: memory region overflow. maximum is %d, got %zu",
+ N_VHOST_REGIONS, *last_idx + GENERAL_NREGIONS);
+
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(pkt_buf);
+ vhost_mem->mem.regions[(*last_idx)++] = VHOST_MEMORY_REGION(eth_pad);
+}
+
/**
* pidfile_write() - Write PID to file, if requested to do so, and close it
* @fd: Open PID file descriptor, closed on exit, -1 to skip writing it
diff --git a/util.h b/util.h
index ec2d9ac..11f6877 100644
--- a/util.h
+++ b/util.h
@@ -145,6 +145,7 @@ int do_clone(int (*fn)(void *), char *stack_area, size_t stack_size, int flags,
struct ctx;
union sockaddr_inany;
+union vhost_memory_u;
int sock_l4(const struct ctx *c, enum epoll_type type,
const union sockaddr_inany *sa, const char *ifname);
@@ -170,6 +171,7 @@ int write_remainder(int fd, const struct iovec *iov, size_t iovcnt,
int read_remainder(int fd, const struct iovec *iov, size_t cnt, size_t skip);
bool snprintf_check(char *str, size_t size, const char *format, ...);
long clamped_scale(long x, long y, long lo, long hi, long f);
+void general_register_memory_regions(union vhost_memory_u *vhost_mem, size_t *last_idx);
/**
* af_name() - Return name of an address family
diff --git a/vhost.c b/vhost.c
new file mode 100644
index 0000000..a69c4b5
--- /dev/null
+++ b/vhost.c
@@ -0,0 +1,295 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Copyright Red Hat
+ * Author: Ammar Yasser <aerosound161(a)gmail.com>
+ *
+ * vhost.c - vhost-net (vhost-kernel) acceleration for pasta mode
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/eventfd.h>
+#include <sys/ioctl.h>
+
+#include "util.h"
+#include "passt.h"
+#include "vhost.h"
+#include "epoll_ctl.h"
+#include "udp.h"
+#include "tcp_buf.h"
+
+struct vq_state vhost_vq_state[2];
+
+struct vring_desc_pasta vring_desc[2][VHOST_NDESCS]
+ __attribute__((aligned(PAGE_SIZE)));
+
+struct vring_avail_pasta vring_avail_all[2]
+ __attribute__((aligned(PAGE_SIZE)));
+struct vring_used_pasta vring_used_all[2]
+ __attribute__((aligned(PAGE_SIZE)));
+
+union vhost_memory_u vhost_memory = {
+ .mem = {
+ .nregions = N_VHOST_REGIONS,
+ },
+};
+
+/**
+ * vhost_setup_net() - Open and negotiate features on /dev/vhost-net
+ * @c: Execution context; c->vhost.fd and c->vhost.features are set
+ * on success, and left untouched on failure
+ *
+ * Failure here means vhost-net isn't usable, not that pasta can't run: the
+ * caller falls back to plain tap operation unless acceleration was required.
+ * That's why nothing in here is fatal.
+ *
+ * Return: 0 on success, -1 if vhost-net is unavailable or unusable
+ */
+int vhost_setup_net(struct ctx *c)
+{
+ const uint64_t req_features = (1ULL << VIRTIO_F_VERSION_1) |
+ (1ULL << VHOST_NET_F_VIRTIO_NET_HDR);
+ uint64_t features;
+ int vhost_fd;
+
+ vhost_fd = open("/dev/vhost-net", O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (vhost_fd < 0) {
+ debug_perror("Couldn't open /dev/vhost-net");
+ return -1;
+ }
+
+ if (ioctl(vhost_fd, VHOST_SET_OWNER, NULL) < 0) {
+ debug_perror("VHOST_SET_OWNER ioctl failed");
+ goto close_fd;
+ }
+
+ if (ioctl(vhost_fd, VHOST_GET_FEATURES, &features) < 0) {
+ debug_perror("VHOST_GET_FEATURES ioctl failed");
+ goto close_fd;
+ }
+
+ if ((features & req_features) != req_features) {
+ debug("vhost-net is missing features: 0x%016" PRIx64,
+ req_features & ~features);
+ goto close_fd;
+ }
+
+ features = req_features;
+ if (ioctl(vhost_fd, VHOST_SET_FEATURES, &features) < 0) {
+ debug_perror("VHOST_SET_FEATURES ioctl failed");
+ goto close_fd;
+ }
+
+ c->vhost.features = features;
+ c->vhost.fd = vhost_fd;
+
+ return 0;
+
+close_fd:
+ close(vhost_fd);
+
+ return -1;
+}
+
+/**
+ * vhost_setup_eventfds() - Set up one queue's eventfds and ring size
+ * @c: Execution context; c->vhost.fd must already be set
+ * @queue_idx: Index of the queue (vring) to configure
+ *
+ */
+void vhost_setup_eventfds(struct ctx *c, int queue_idx)
+{
+ struct vhost_vring_file vring_file = { .index = queue_idx };
+ struct vhost_vring_state state = { .index = queue_idx };
+ union epoll_ref ref = { 0 };
+ int vhost_fd = c->vhost.fd;
+ int rc;
+
+ state.num = VHOST_NDESCS;
+ ref.queue = queue_idx;
+
+ vring_file.fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
+ if (vring_file.fd < 0)
+ die_perror("Failed to create vhost call eventfd, queue %d",
+ queue_idx);
+ ref.fd = vring_file.fd;
+
+ rc = ioctl(vhost_fd, VHOST_SET_VRING_CALL, &vring_file);
+ if (rc < 0)
+ die_perror("VHOST_SET_VRING_CALL ioctl failed, queue %d",
+ queue_idx);
+
+ ref.type = EPOLL_TYPE_VHOST_CALL;
+ rc = epoll_add(c->epollfd, EPOLLIN, ref);
+ if (rc < 0)
+ die_perror("Failed to watch vhost call eventfd, queue %d",
+ queue_idx);
+ c->vhost.vq[queue_idx].call_fd = vring_file.fd;
+
+ vring_file.fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
+ if (vring_file.fd < 0)
+ die_perror("Failed to create vhost error eventfd, queue %d",
+ queue_idx);
+
+ rc = ioctl(vhost_fd, VHOST_SET_VRING_ERR, &vring_file);
+ if (rc < 0)
+ die_perror("VHOST_SET_VRING_ERR ioctl failed, queue %d",
+ queue_idx);
+
+ ref.type = EPOLL_TYPE_VHOST_ERROR;
+ ref.fd = vring_file.fd;
+ rc = epoll_add(c->epollfd, EPOLLIN, ref);
+ if (rc < 0)
+ die_perror("Failed to watch vhost error eventfd, queue %d",
+ queue_idx);
+ c->vhost.vq[queue_idx].err_fd = vring_file.fd;
+
+ rc = ioctl(vhost_fd, VHOST_SET_VRING_NUM, &state);
+ if (rc < 0)
+ die_perror("VHOST_SET_VRING_NUM ioctl failed, queue %d",
+ queue_idx);
+
+ vring_file.fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
+ if (vring_file.fd < 0)
+ die_perror("Failed to create vhost kick eventfd, queue %d",
+ queue_idx);
+
+ rc = ioctl(vhost_fd, VHOST_SET_VRING_KICK, &vring_file);
+ if (rc < 0)
+ die_perror("VHOST_SET_VRING_KICK ioctl failed, queue %d",
+ queue_idx);
+
+ c->vhost.vq[queue_idx].kick_fd = vring_file.fd;
+
+ vhost_vq_state[queue_idx].num_free = VHOST_NDESCS;
+}
+
+/**
+ * vhost_setup_memory_table() - Register the GPA/HVA translation table
+ * @c: Execution context; c->vhost.fd must already be set
+ *
+ * vhost-net reads the addresses we put in descriptors as guest physical
+ * addresses, and translates them through this table before touching the
+ * memory they refer to. pasta has no guest and no second address space:
+ * the addresses we put there are our own virtual addresses. Every region
+ * below therefore sets guest_phys_addr equal to userspace_addr, so that
+ * GPA == HVA and the translation is an identity mapping.
+ *
+ * Return: 0 on success, -1 on error with errno set
+ */
+int vhost_setup_memory_table(struct ctx *c) {
+ size_t region_idx = 0;
+
+ /* general purpose buffers */
+ general_register_memory_regions(&vhost_memory, ®ion_idx);
+
+ /* tcp specific buffers */
+ tcp_register_memory_regions(&vhost_memory, ®ion_idx);
+
+ /* udp specific buffers */
+ udp_register_memory_regions(&vhost_memory, ®ion_idx);
+
+ vhost_memory.mem.nregions = region_idx;
+
+ return ioctl(c->vhost.fd, VHOST_SET_MEM_TABLE, &vhost_memory.mem);
+}
+
+/**
+ * vhost_set_vring() - Register a vring's addresses and bind its backend
+ * @c: Execution context; c->vhost.fd must already be set
+ * @queue_idx: Index of the queue (vring) to configure
+ * @tap_fd: Tap fd to bind as this queue's backend
+ */
+void vhost_set_vring(struct ctx *c, int queue_idx, int tap_fd)
+{
+ int vhost_fd = c->vhost.fd;
+ struct vhost_vring_addr addr = {
+ .index = queue_idx,
+ .desc_user_addr = (unsigned long)vring_desc[queue_idx],
+ .avail_user_addr = (unsigned long)&vring_avail_all[queue_idx],
+ .used_user_addr = (unsigned long)&vring_used_all[queue_idx],
+ .log_guest_addr = (unsigned long)&vring_used_all[queue_idx],
+ };
+ struct vhost_vring_file file = {
+ .index = queue_idx,
+ .fd = tap_fd,
+ };
+ unsigned int i;
+ int rc;
+
+ rc = ioctl(vhost_fd, VHOST_SET_VRING_ADDR, &addr);
+ if (rc < 0)
+ die_perror("VHOST_SET_VRING_ADDR ioctl failed, queue %d",
+ queue_idx);
+
+ if (queue_idx == VHOST_AVAIL_Q_IDX) {
+ for (i = 0; i < VHOST_NDESCS; ++i) {
+ vring_desc[VHOST_AVAIL_Q_IDX][i].addr =
+ (uintptr_t)pkt_buf + i * VHOST_DESC_BYTES;
+ vring_desc[VHOST_AVAIL_Q_IDX][i].len = VHOST_DESC_BYTES;
+ vring_desc[VHOST_AVAIL_Q_IDX][i].flags =
+ VRING_DESC_F_WRITE;
+ }
+
+ for (i = 0; i < VHOST_NDESCS; ++i)
+ vring_avail_all[VHOST_AVAIL_Q_IDX].ring[i] =
+ htole16(i);
+
+ vhost_rx_descriptor_handoff(c);
+ }
+
+ if (queue_idx == VHOST_USED_Q_IDX) {
+ for (i = 0; i < (VHOST_NDESCS - 1); ++i)
+ vring_desc[VHOST_USED_Q_IDX][i].next = i + 1;
+ }
+
+ rc = ioctl(vhost_fd, VHOST_NET_SET_BACKEND, &file);
+ if (rc < 0)
+ die_perror("VHOST_NET_SET_BACKEND ioctl failed, queue %d",
+ queue_idx);
+}
+
+/**
+ * vhost_rx_descriptor_handoff() - Announce freed from-guest descriptors
+ * @c: Execution context
+ *
+ * Bumps avail.idx by the number of descriptors accumulated in
+ * vhost_vq_state[VHOST_AVAIL_Q_IDX].num_free (from prior
+ * consume_one_rx_descriptor() calls),
+ * then resets the counter to zero. The kernel will see the new
+ * avail.idx and consume the freshly-available descriptors.
+ */
+void vhost_rx_descriptor_handoff(struct ctx *c)
+{
+ smp_wmb();
+
+ if (!vhost_vq_state[VHOST_AVAIL_Q_IDX].num_free)
+ return;
+
+ vring_avail_all[VHOST_AVAIL_Q_IDX].idx +=
+ vhost_vq_state[VHOST_AVAIL_Q_IDX].num_free;
+ vhost_vq_state[VHOST_AVAIL_Q_IDX].num_free = 0;
+ vhost_kick(&vring_used_all[VHOST_AVAIL_Q_IDX],
+ c->vhost.vq[VHOST_AVAIL_Q_IDX].kick_fd);
+}
+
+/**
+ * vhost_kick() - Notify the kernel that new descriptors are available
+ * @used: Used ring of the queue we're announcing on, checked to see
+ * whether the kernel's virtio thread is already reading
+ * descriptors and doesn't want to be notified
+ * @kick_fd: Kick eventfd of that same queue
+ */
+void vhost_kick(struct vring_used_pasta *used, int kick_fd)
+{
+ /* Ensure that the read of used->flags doesn't get reordered to be
+ * above the avail.idx update
+ */
+ smp_mb();
+
+ if (!(used->flags & VRING_USED_F_NO_NOTIFY))
+ eventfd_write(kick_fd, 1);
+}
diff --git a/vhost.h b/vhost.h
new file mode 100644
index 0000000..bd555f9
--- /dev/null
+++ b/vhost.h
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright Red Hat
+ * Author: Ammar Yasser <aerosound161(a)gmail.com>
+ *
+ * vhost.h - vhost-net (vhost-kernel) acceleration for pasta mode
+ */
+
+#ifndef VHOST_H
+#define VHOST_H
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include <linux/vhost.h>
+#include <linux/vhost_types.h>
+#include <linux/virtio_config.h>
+#include <linux/virtio_net.h>
+#include <linux/virtio_ring.h>
+
+#include "passt.h"
+
+#define VHOST_AVAIL_Q_IDX 0
+#define VHOST_USED_Q_IDX 1
+
+/**
+ * struct vq_state - Per-virtqueue local descriptor tracking
+ * @num_free: Number of descriptors ready to be announced to the
+ * kernel via vhost_rx_descriptor_handoff()
+ * @last_used_idx: Number of used-ring entries consumed so far;
+ * lagging read cursor vs. vring_used->idx (the
+ * kernel's write cursor)
+ */
+extern struct vq_state {
+ uint16_t num_free;
+ uint16_t last_used_idx;
+ uint16_t next_free;
+} vhost_vq_state[2];
+
+struct vring_desc_pasta {
+ __virtio64 addr;
+ __virtio32 len;
+ __virtio16 flags;
+ __virtio16 next;
+};
+
+extern struct vring_desc_pasta vring_desc[2][VHOST_NDESCS];
+
+struct vring_avail_pasta {
+ __virtio16 flags;
+ __virtio16 idx;
+ __virtio16 ring[VHOST_NDESCS];
+};
+static_assert(sizeof(struct vring_avail_pasta) ==
+ offsetof(struct vring_avail, ring[VHOST_NDESCS]),
+ "vring_avail_pasta doesn't match the kernel avail ring layout");
+extern struct vring_avail_pasta vring_avail_all[2];
+
+struct vring_used_pasta {
+ __virtio16 flags;
+ __virtio16 idx;
+ vring_used_elem_t ring[VHOST_NDESCS];
+};
+static_assert(sizeof(struct vring_used_pasta) ==
+ offsetof(struct vring_used, ring[VHOST_NDESCS]),
+ "vring_used_pasta doesn't match the kernel used ring layout");
+extern struct vring_used_pasta vring_used_all[2];
+
+#define N_VHOST_REGIONS 12
+union vhost_memory_u {
+ struct vhost_memory mem;
+ char buf[offsetof(struct vhost_memory, regions[N_VHOST_REGIONS])];
+};
+extern union vhost_memory_u vhost_memory;
+
+#define VHOST_MEMORY_REGION_PTR(addr, size) \
+ (struct vhost_memory_region) { \
+ .guest_phys_addr = (uintptr_t)addr, \
+ .memory_size = size, \
+ .userspace_addr = (uintptr_t)addr, \
+ }
+#define VHOST_MEMORY_REGION(buf) VHOST_MEMORY_REGION_PTR(&buf, sizeof(buf))
+
+void vhost_set_vring(struct ctx *c, int queue_idx, int tap_fd);
+int vhost_setup_memory_table(struct ctx *c);
+int vhost_setup_net(struct ctx *c);
+void vhost_setup_eventfds(struct ctx *c, int queue_idx);
+void vhost_rx_descriptor_handoff(struct ctx *c);
+void vhost_kick(struct vring_used_pasta *used, int kick_fd);
+
+#endif /* VHOST_H */
--
2.39.5 (Apple Git-154)
2
1
From: Ammar Yasser <aerosound161(a)gmail.com>
This work is based on the original v2 RFC by Eugenio Perez:
Link:
https://archives.passt.top/passt-dev/20250709174748.3514693-1-eperezma@redh…
It is the first finished patch series for vhost-kernel support after a
third RFC was submitted:
Link:
https://archives.passt.top/passt-dev/20260802132155.870796-1-aerosound161@g…
vhost-net is a kernel device that allows reading and writing packets to a
tap device using virtqueues instead of read(2), write(2), and their
vectored counterparts. Frames are handed over through a shared descriptor
ring, so several of them can be passed to the kernel at once rather than
issuing a syscall per frame.
The changes reliably yield a roughly 2x speedup on the TX path (container
to host).
Numbers were obtained through iperf3 with the following methodology and
results:
Pasta command:
./pasta -f --runas 0:0 --map-guest-addr 192.168.0.5
--vhost-kernel on --config-net 2729785
Host:
iperf3 -s1J
Container:
iperf3 -c 192.168.0.5 -t 10 -P4 -l 1M -w 32M -i1
With --vhost-kernel on:
[SUM] 0.00-10.00 sec 1.36 GBytes 1.17 Gbits/sec 3643 sender
[SUM] 0.00-10.00 sec 1.16 GBytes 999 Mbits/sec receiver
With --vhost-kernel off:
[SUM] 0.00-10.00 sec 700 MBytes 587 Mbits/sec 3556 sender
[SUM] 0.00-10.10 sec 495 MBytes 411 Mbits/sec receiver
v2:
- removed all use of epoll_ctl in vhost_setup_eventfds in favor of epoll_add
- prevent all functions that register memory regions into the vhost_memory
object from overflowing
- remove the unions with flexible array members in vring_avail_pasta and
vring_used_pasta in favor of hardcoding the ring array to have VHOST_NDESCS
entries to avoid having to ignore Wpedantic
- remove the && operator with TCP_CURR_IDX when computing no_csum
- make desc_id in tx_reap a uint32_t
- don't make vhost-kernel a required argument
Ammar Yasser (7):
tap: Move the tap_hdr file to a separate file
conf: Add context fields, epoll types and --vhost-kernel flag to pasta
virtio: Add the pasta vhost-net interface and implementation
tap: Implement the pasta vhost-net from-guest path
tap, tcp, udp: Prepare the to-guest path for vhost-net
tap: Implement the pasta vhost-net to-guest path
tap/tcp: Replace tcp_payload_used with a ring buffer style index
Makefile | 2 +-
conf.c | 19 +++
epoll_type.h | 4 +
passt.c | 9 ++
passt.h | 79 ++++++++++-
tap.c | 368 +++++++++++++++++++++++++++++++++++++++++++++------
tap.h | 47 ++-----
tap_hdr.h | 23 ++++
tcp_buf.c | 149 ++++++++++++++++-----
tcp_buf.h | 4 +
udp.c | 47 +++++--
udp.h | 3 +
util.c | 18 +++
util.h | 2 +
vhost.c | 295 +++++++++++++++++++++++++++++++++++++++++
vhost.h | 91 +++++++++++++
16 files changed, 1036 insertions(+), 124 deletions(-)
create mode 100644 tap_hdr.h
create mode 100644 vhost.c
create mode 100644 vhost.h
--
2.39.5 (Apple Git-154)
2
3
From: Ammar Yasser <aerosound161(a)gmail.com>
It was defined in tap.h, put it on its own so that future callers won't
need to depend on all definitions in tap.h.
Also turn it into a union of a vnet_len and virtio_net_mrg_rxbuf
because for vhost acceleration the frames will have this virtio net
header prepended to them.
Signed-off-by: Ammar Yasser <aerosound161(a)gmail.com>
---
tap.h | 9 +--------
tap_hdr.h | 23 +++++++++++++++++++++++
2 files changed, 24 insertions(+), 8 deletions(-)
create mode 100644 tap_hdr.h
diff --git a/tap.h b/tap.h
index b335933..1625975 100644
--- a/tap.h
+++ b/tap.h
@@ -10,6 +10,7 @@
#include <stdint.h>
#include "passt.h"
+#include "tap_hdr.h"
/** L2_MAX_LEN_PASTA - Maximum frame length for pasta mode (with L2 header)
*
@@ -38,14 +39,6 @@
struct udphdr;
-/**
- * struct tap_hdr - tap backend specific headers
- * @vnet_len: Frame length (for qemu socket transport)
- */
-struct tap_hdr {
- uint32_t vnet_len;
-} __attribute__((packed));
-
/**
* tap_hdr_iov() - struct iovec for a tap header
* @c: Execution context
diff --git a/tap_hdr.h b/tap_hdr.h
new file mode 100644
index 0000000..4156f44
--- /dev/null
+++ b/tap_hdr.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright (c) 2021 Red Hat GmbH
+ * Author: David Gibson <david(a)gibson.dropbear.id.au>
+ */
+
+#ifndef TAP_HDR_H
+#define TAP_HDR_H
+
+#include <stdint.h>
+#include <linux/virtio_net.h>
+
+/**
+ * struct tap_hdr - tap backend specific headers
+ * @vnet_len: Frame length (for qemu socket transport)
+ */
+struct tap_hdr {
+ union {
+ uint32_t vnet_len;
+ struct virtio_net_hdr_mrg_rxbuf hdr;
+ };
+};
+
+#endif /* TAP_HDR_H */
--
2.39.5 (Apple Git-154)
2
1
05 Nov '26
As preparation for supporting multiple addresses per interface,
we replace the single addr/prefix_len fields with an array. The
array consists of a new struct inany_addr_entry containing an
address and prefix length, both in inany_addr format.
Despite some code refactoring, there are only two real functional
changes:
- The indicated IPv6 prefix length is now properly stored, instead
of being ignored and overridden with the hardcoded value 64, as
has been the case until now.
- Since even IPv4 addresses now are stored in IPv6 format, we
also store the corresponding prefix length in that format,
i.e. using the range [96,128] instead of [0,32].
In conf_ip6(), the explicit IN6_IS_ADDR_UNSPECIFIED() check on the
address at the end of the function is no longer needed: for
host-discovered addresses, it is now checked inside the if (!a) block
before calling fwd_set_addr(); for user-provided addresses (via -a),
validation already rejects unspecified addresses at parse time.
Signed-off-by: Jon Maloy <jmaloy(a)redhat.com>
---
v2: -Using inany_addr instead of protocol specific addresses as
entry address field.
v3: -Merging into one array, directly in struct ctx
-Changed prefix_len and flags fields in struct inany_addr_entry
to uint8_t, since that makes the struct directly migratable
v4: -Updated according to changes in previous commits
-Updated according to feedback from David G.
-Squashed IP4_MASK macro commit into this one
v6: -Renamed and moved some definitions
-Introduced fwd_set_addr() and fwd_get_addr() already in this commit
-Eliminated first_v4/v6() functions, replaced with fwd_get_addr()
-Some other changes as suggested by David G.
-I kept the flag CONF_ADDR_LINKLOCAL, since it will be
needed later in an address selection function.
v7: -Introduced CONF_ADDR_GENERATED flag
-Other fixes based on feedback from David and Stefano.
-I changed signature of inany_prefix_len(), but I did not change
its semantics, since the premise of David's comment is wrong: the
caller does *not* explicitly know he is dealing with an IPv4 address.
In fact, there are examples later in this series where it may be an
IPv6 address, and the caller just trusts he gets the return value in
the appropriate format.
-Intoduced the inverse of inany_prefix_len(), called inany_prefix_len6()
which always returns the prefix in IPv6 or mapped IPv4 format.
The name of the function isn't great, but any alternative I came up
with became too long to be practical.
v8: -Added inany_prefix_v4() (David G.) to extract IPv4 address and
prefix length simultaneously, replacing inany_prefix_len().
-Removed both inany_prefix_len() and inany_prefix_len6()
auto-detecting functions. Prefix length attached to an inany is
now always stored in IPv6 format (96-128 for IPv4-mapped).
-fwd_set_addr() now requires prefix_len in IPv6 format; callers
updated to pass prefix_len + 96 for IPv4 addresses.
-Fixed -n option to store prefix_len in IPv6 format.
-Fixed addr_seen regression: when address was set via -a,
addr_seen was not initialised in conf_ip4().
-Replaced assert() with graceful no-op in dhcp.c and ndp.c when
no address is configured (David G.).
-Note: addr_seen is still initialised both at conf_ip*() time
and at DHCP/DHCPv6 send time (David's concern). This
inconsistency will be addressed in a follow-up.
-Introduced a new CONF_ADDR_ANY macro to be used for address
selection, as suggested by Stefano.
-Deferred introduction of an explicit scope field (replacing
CONF_ADDR_LINKLOCAL flag) to a later commit or series.
v9: -Rebased on changes made by David Gibson.
-Some minor changes based on feedback from David.
-Defined a new macro CONF_ADDR_GLOBAL to supplant CONF_ADDR_ANY.
-I did not follow up David's suggestion to introduce three
functions to replace the current fwd_get_addr(), because I
don't think it would solve any problems. The 'flags' field
encode both provenance and type properties orthogonally, and
callers need different combinations of them. That is the very
point of that field, and I see nothing confusing in it.
It is correct that some of the calls to fwd_get_addr() go away
in later commits, but enough of them remain to justify its
existence. Defining three new functions to replace it would only
make them wrappers around fwd_get_addr(), and serve no purpose in
my view.
v10: -Redefined and refactored function fwd_get_addr().
v11: -Changes after feedback from Stefano.
- Fixed coverity false positives.
- Fixed bug introduced by wrongly handling ipv4 link local
addresses analogously to ipv6 ditto. These addresses should in
practice be handled like global IPv4 address. Removed now
redundant inany_is_linklocal() function and let fwd_get_addr()
only use inany_is_linklocal6().
- In the same function the removal of the fallback does not
imply a functional change:
* -a without a prefix still gets the class-based default from
inany_default_prefix_len()
* host-discovered addresses get their prefix_len from netlink.
- The changes in conf_print() is not unrelated: inet_ntop() may
fail and leave a stale buffer.
- Re-introduced CONF_ADDR_ANY, this time to 0
- Kept next_addr_idx_(). There is a working solution without it,
but (kernel) checkpatch complains about it.
solution for this.
- Some other minor changes
v12: - Re-defined CONF_ADDR_ANY to be a bitmap
- Added assert() in fwd_set_addr() to ensure no address is added
without at least one flag.
v13: - Renamed 'v4' to 'a4' all through the commit
- In dhcpv6(): set resp.ia_addr.addr dynamically at send time from
the address fetched by fwd_get_addr(), instead of caching a
potentially stale value in dhcpv6_init(). We drop non-
INFORMATION_REQUEST messages silently if no address is available.
- Removed address setup from dhcpv6_init() accordingly.
v14: - In dhcpv6() we now drop all messages when no address is available,
as suggested by Stefano.
v15: - Moved fwd_get_addr() call and the !a early return to just after the
no_dhcpv6 check, so 'a' is guaranteed non-NULL for the remainder of
dhcpv6().
v16: - Fixed a missed struct in_addr *v4 renaming to a4 in
pasta_conf_addr().
---
arp.c | 21 +++++++-
conf.c | 157 +++++++++++++++++++++++++++++++++----------------------
dhcp.c | 20 +++++--
dhcpv6.c | 20 +++++--
fwd.c | 114 +++++++++++++++++++++++++++++++++-------
fwd.h | 4 ++
inany.h | 34 +++++++++++-
ip.h | 2 +
ndp.c | 19 +++++--
passt.h | 63 +++++++++++++++++++---
pasta.c | 23 +++++---
tap.c | 8 ++-
12 files changed, 373 insertions(+), 112 deletions(-)
diff --git a/arp.c b/arp.c
index bb042e95..08d5a9f9 100644
--- a/arp.c
+++ b/arp.c
@@ -41,6 +41,9 @@
static bool ignore_arp(const struct ctx *c,
const struct arphdr *ah, const struct arpmsg *am)
{
+ const struct guest_addr *a;
+ const struct in_addr *a4;
+
if (ah->ar_hrd != htons(ARPHRD_ETHER) ||
ah->ar_pro != htons(ETH_P_IP) ||
ah->ar_hln != ETH_ALEN ||
@@ -54,7 +57,11 @@ static bool ignore_arp(const struct ctx *c,
return true;
/* Don't resolve the guest's assigned address, either. */
- if (!memcmp(am->tip, &c->ip4.addr, sizeof(am->tip)))
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ if (!a)
+ return false;
+ a4 = inany_v4(&a->addr);
+ if (a4 && !memcmp(am->tip, a4, sizeof(am->tip)))
return true;
return false;
@@ -123,12 +130,22 @@ int arp(const struct ctx *c, struct iov_tail *data)
*/
void arp_send_init_req(const struct ctx *c)
{
+ const struct guest_addr *a;
+ const struct in_addr *a4;
struct {
struct ethhdr eh;
struct arphdr ah;
struct arpmsg am;
} __attribute__((__packed__)) req;
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ if (!a)
+ return;
+
+ a4 = inany_v4(&a->addr);
+ if (!a4)
+ return;
+
/* Ethernet header */
req.eh.h_proto = htons(ETH_P_ARP);
memcpy(req.eh.h_dest, MAC_BROADCAST, sizeof(req.eh.h_dest));
@@ -145,7 +162,7 @@ void arp_send_init_req(const struct ctx *c)
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));
+ memcpy(req.am.tip, a4, sizeof(req.am.tip));
debug("Sending initial ARP request for guest MAC address");
tap_send_single(c, &req, sizeof(req));
diff --git a/conf.c b/conf.c
index 149a4c79..66aa524e 100644
--- a/conf.c
+++ b/conf.c
@@ -372,13 +372,19 @@ static uint8_t conf_ip4_prefix(const char *arg)
/**
* conf_ip4() - Verify or detect IPv4 support, get relevant addresses
+ * @c: Execution context
* @ifi: Host interface to attempt (0 to determine one)
- * @ip4: IPv4 context (will be written)
*
* Return: interface index for IPv4, or 0 on failure.
*/
-static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
+static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
{
+ struct ip4_ctx *ip4 = &c->ip4;
+ const struct guest_addr *a;
+ const struct in_addr *a4;
+
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+
if (!ifi)
ifi = nl_get_ext_if(nl_sock, AF_INET);
@@ -397,67 +403,75 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
}
}
- if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr)) {
+ if (!a) {
+ struct in_addr addr = { 0 };
+ int prefix_len = 0;
int rc = nl_addr_get(nl_sock, ifi, AF_INET,
- &ip4->addr, &ip4->prefix_len, NULL);
+ &addr, &prefix_len, NULL);
if (rc < 0) {
debug("Couldn't discover IPv4 address: %s",
strerror_(-rc));
return 0;
}
+ if (IN4_IS_ADDR_UNSPECIFIED(&addr)) {
+ warn("Host returned address 0.0.0.0");
+ return 0;
+ }
+ fwd_set_addr(c, &inany_from_v4(addr), CONF_ADDR_HOST,
+ prefix_len + 96);
}
- if (!ip4->prefix_len) {
- in_addr_t addr = ntohl(ip4->addr.s_addr);
- if (IN_CLASSA(addr))
- ip4->prefix_len = (32 - IN_CLASSA_NSHIFT);
- else if (IN_CLASSB(addr))
- ip4->prefix_len = (32 - IN_CLASSB_NSHIFT);
- else if (IN_CLASSC(addr))
- ip4->prefix_len = (32 - IN_CLASSC_NSHIFT);
- else
- ip4->prefix_len = 32;
- }
-
- ip4->addr_seen = ip4->addr;
-
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ assert(a);
+ a4 = inany_v4(&a->addr);
+ assert(a4);
+ ip4->addr_seen = *a4;
ip4->our_tap_addr = ip4->guest_gw;
- if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
- return 0;
-
return ifi;
}
/**
* conf_ip4_local() - Configure IPv4 addresses and attributes for local mode
- * @ip4: IPv4 context (will be written)
+ * @c: Execution context (will be written)
*/
-static void conf_ip4_local(struct ip4_ctx *ip4)
+static void conf_ip4_local(struct ctx *c)
{
- if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
- ip4->addr = IP4_LL_GUEST_ADDR;
- ip4->addr_seen = ip4->addr;
+ struct ip4_ctx *ip4 = &c->ip4;
+ const struct guest_addr *a;
+
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+
+ if (!a) {
+ fwd_set_addr(c, &inany_from_v4(IP4_LL_GUEST_ADDR),
+ CONF_ADDR_GEN, IP4_LL_PREFIX_LEN + 96);
+ ip4->addr_seen = IP4_LL_GUEST_ADDR;
+ } else {
+ const struct in_addr *a4 = inany_v4(&a->addr);
+
+ assert(a4);
+ ip4->addr_seen = *a4;
+ }
if (IN4_IS_ADDR_UNSPECIFIED(&ip4->guest_gw))
ip4->guest_gw = IP4_LL_GUEST_GW;
ip4->our_tap_addr = ip4->guest_gw;
- if (!ip4->prefix_len)
- ip4->prefix_len = IP4_LL_PREFIX_LEN;
-
ip4->no_copy_addrs = ip4->no_copy_routes = true;
}
/**
* conf_ip6() - Verify or detect IPv6 support, get relevant addresses
+ * @c: Execution context
* @ifi: Host interface to attempt (0 to determine one)
- * @ip6: IPv6 context (will be written)
*
* Return: interface index for IPv6, or 0 on failure.
*/
-static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
+static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
{
+ struct ip6_ctx *ip6 = &c->ip6;
+ const struct guest_addr *a;
+ union inany_addr addr;
int prefix_len = 0;
int rc;
@@ -478,23 +492,30 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
}
}
- rc = nl_addr_get(nl_sock, ifi, AF_INET6,
- IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ? &ip6->addr : NULL,
+ rc = nl_addr_get(nl_sock, ifi, AF_INET6, &addr.a6,
&prefix_len, &ip6->our_tap_ll);
if (rc < 0) {
debug("Couldn't discover IPv6 address: %s", strerror_(-rc));
return 0;
}
- ip6->addr_seen = ip6->addr;
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (!a) {
+ if (IN6_IS_ADDR_UNSPECIFIED(&addr))
+ return 0;
+
+ fwd_set_addr(c, &addr, CONF_ADDR_HOST, prefix_len);
+ ip6->addr_seen = addr.a6;
+ } else {
+ ip6->addr_seen = a->addr.a6;
+ }
if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
ip6->our_tap_ll = ip6->guest_gw;
else
ip6->our_tap_addr = ip6->guest_gw;
- if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ||
- IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
+ if (IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
return 0;
return ifi;
@@ -502,10 +523,12 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
/**
* conf_ip6_local() - Configure IPv6 addresses and attributes for local mode
- * @ip6: IPv6 context (will be written)
+ * @c: Execution context (will be written)
*/
-static void conf_ip6_local(struct ip6_ctx *ip6)
+static void conf_ip6_local(struct ctx *c)
{
+ struct ip6_ctx *ip6 = &c->ip6;
+
if (IN6_IS_ADDR_UNSPECIFIED(&ip6->guest_gw))
ip6->guest_gw = IP6_LL_GUEST_GW;
@@ -801,6 +824,7 @@ enum passt_modes conf_mode(int argc, char *argv[])
static void conf_print(const struct ctx *c)
{
char buf[INANY_ADDRSTRLEN];
+ const struct guest_addr *a;
int i;
if (c->fd_control_listen >= 0)
@@ -848,16 +872,19 @@ static void conf_print(const struct ctx *c)
inet_ntop(AF_INET, &c->ip4.map_host_loopback,
buf, sizeof(buf)));
- if (!c->no_dhcp) {
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ if (a && !c->no_dhcp) {
uint32_t mask;
+ int plen = 0;
- mask = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+ inany_prefix_v4(&a->addr, a->prefix_len, &plen);
+ mask = IN4_MASK(plen);
info("DHCP:");
info(" assign: %s",
- inet_ntop(AF_INET, &c->ip4.addr, buf, sizeof(buf)));
+ inany_ntop(&a->addr, buf, sizeof(buf)));
info(" mask: %s",
- inet_ntop(AF_INET, &mask, buf, sizeof(buf)));
+ inet_ntop(AF_INET, &mask, buf, sizeof(buf)));
info(" router: %s",
inet_ntop(AF_INET, &c->ip4.guest_gw,
buf, sizeof(buf)));
@@ -868,8 +895,8 @@ static void conf_print(const struct ctx *c)
break;
if (!i)
info("DNS:");
- inet_ntop(AF_INET, &c->ip4.dns[i], buf, sizeof(buf));
- info(" %s", buf);
+ info(" %s", inet_ntop(AF_INET, &c->ip4.dns[i],
+ buf, sizeof(buf)));
}
for (i = 0; *c->dns_search[i].n; i++) {
@@ -894,13 +921,14 @@ static void conf_print(const struct ctx *c)
else
goto dns6;
- info(" assign: %s",
- inet_ntop(AF_INET6, &c->ip6.addr, buf, sizeof(buf)));
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (a)
+ info(" assign: %s",
+ inany_ntop(&a->addr, buf, sizeof(buf)));
info(" router: %s",
inet_ntop(AF_INET6, &c->ip6.guest_gw, buf, sizeof(buf)));
info(" our link-local: %s",
- inet_ntop(AF_INET6, &c->ip6.our_tap_ll,
- buf, sizeof(buf)));
+ inet_ntop(AF_INET6, &c->ip6.our_tap_ll, buf, sizeof(buf)));
dns6:
for (i = 0; i < ARRAY_SIZE(c->ip6.dns); i++) {
@@ -908,8 +936,10 @@ dns6:
break;
if (!i)
info("DNS:");
- inet_ntop(AF_INET6, &c->ip6.dns[i], buf, sizeof(buf));
- info(" %s", buf);
+ info(" %s",
+ inet_ntop(AF_INET6, &c->ip6.dns[i],
+ buf, sizeof(buf)));
+
}
for (i = 0; *c->dns_search[i].n; i++) {
@@ -1213,15 +1243,13 @@ static bool conf_addr(struct ctx *c, char *arg, uint8_t opt_n)
inany_is_loopback(&addr))
goto bad;
+ fwd_set_addr(c, &addr, CONF_ADDR_USER, prefix_len);
if (a4) {
- c->ip4.addr = *a4;
- c->ip4.prefix_len = prefix_len - 96;
- c->ip4.addr_fixed = true;
c->ip4.no_copy_addrs = true;
+ c->ip4.addr_fixed = true;
} else {
- c->ip6.addr = addr.a6;
- c->ip6.addr_fixed = true;
c->ip6.no_copy_addrs = true;
+ c->ip6.addr_fixed = true;
}
return is_prefix;
@@ -1331,6 +1359,7 @@ void conf(struct ctx *c, int argc, char **argv)
unsigned int ifi4 = 0, ifi6 = 0;
bool opt_a_is_prefix = false;
const char *logfile = NULL;
+ struct guest_addr *a;
char *runas = NULL;
size_t logsize = 0;
uint8_t opt_n = 0;
@@ -1652,8 +1681,12 @@ void conf(struct ctx *c, int argc, char **argv)
if (opt_a_is_prefix)
die("Redundant prefix length specification");
- c->ip4.prefix_len = conf_ip4_prefix(optarg);
- opt_n = c->ip4.prefix_len + 96;
+ opt_n = conf_ip4_prefix(optarg) + 96;
+
+ for_each_addr(a, c->addrs, c->addr_count, AF_INET) {
+ a->prefix_len = opt_n;
+ break;
+ }
break;
case 'M':
p = optarg;
@@ -1853,9 +1886,9 @@ void conf(struct ctx *c, int argc, char **argv)
nl_sock_init(c, false);
if (!v6_only && !c->splice_only)
- c->ifi4 = conf_ip4(ifi4, &c->ip4);
+ c->ifi4 = conf_ip4(c, ifi4);
if (!v4_only && !c->splice_only)
- c->ifi6 = conf_ip6(ifi6, &c->ip6);
+ c->ifi6 = conf_ip6(c, ifi6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) {
warn("MTU %"PRIu16" is too small for IPv4 (minimum %u)",
@@ -1878,7 +1911,7 @@ void conf(struct ctx *c, int argc, char **argv)
if (!c->ifi4 && !v6_only) {
if (!c->splice_only) {
info("IPv4: no external interface as template, use local mode");
- conf_ip4_local(&c->ip4);
+ conf_ip4_local(c);
}
c->ifi4 = -1;
}
@@ -1886,7 +1919,7 @@ void conf(struct ctx *c, int argc, char **argv)
if (!c->ifi6 && !v4_only) {
if (!c->splice_only) {
info("IPv6: no external interface as template, use local mode");
- conf_ip6_local(&c->ip6);
+ conf_ip6_local(c);
}
c->ifi6 = -1;
}
@@ -1959,7 +1992,7 @@ void conf(struct ctx *c, int argc, char **argv)
if (!c->ifi6) {
c->no_ndp = 1;
c->no_dhcpv6 = 1;
- } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
+ } else if (!fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY)) {
c->no_dhcpv6 = 1;
}
diff --git a/dhcp.c b/dhcp.c
index c3c74224..9336f504 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -303,7 +303,9 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
int dhcp(const struct ctx *c, struct iov_tail *data)
{
char macstr[ETH_ADDRSTRLEN];
+ const struct guest_addr *a;
size_t mlen, dlen, opt_len;
+ const struct in_addr *addr;
struct in_addr mask, dst;
struct ethhdr eh_storage;
struct iphdr iph_storage;
@@ -315,6 +317,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
struct msg const *m;
struct msg reply;
unsigned int i;
+ int plen;
eh = IOV_REMOVE_HEADER(data, eh_storage);
iph = IOV_PEEK_HEADER(data, iph_storage);
@@ -344,6 +347,15 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
m->op != BOOTREQUEST)
return -1;
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_USER);
+ if (!a)
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ if (!a)
+ return -1;
+
+ addr = inany_prefix_v4(&a->addr, a->prefix_len, &plen);
+ assert(addr);
+
reply.op = BOOTREPLY;
reply.htype = m->htype;
reply.hlen = m->hlen;
@@ -352,7 +364,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
reply.secs = 0;
reply.flags = m->flags;
reply.ciaddr = m->ciaddr;
- reply.yiaddr = c->ip4.addr;
+ reply.yiaddr = *addr;
reply.siaddr = 0;
reply.giaddr = m->giaddr;
memcpy(&reply.chaddr, m->chaddr, sizeof(reply.chaddr));
@@ -408,7 +420,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
info(" from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));
- mask.s_addr = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+ mask.s_addr = IN4_MASK(plen);
memcpy(opts[1].s, &mask, sizeof(mask));
memcpy(opts[3].s, &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
memcpy(opts[54].s, &c->ip4.our_tap_addr, sizeof(c->ip4.our_tap_addr));
@@ -416,7 +428,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
/* If the gateway is not on the assigned subnet, send an option 121
* (Classless Static Routing) adding a dummy route to it.
*/
- if ((c->ip4.addr.s_addr & mask.s_addr)
+ if ((addr->s_addr & mask.s_addr)
!= (c->ip4.guest_gw.s_addr & mask.s_addr)) {
/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
opts[121].slen = 14;
@@ -475,7 +487,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
if (m->flags & FLAG_BROADCAST)
dst = in4addr_broadcast;
else
- dst = c->ip4.addr;
+ dst = *addr;
tap_udp4_send(c, c->ip4.our_tap_addr, 67, dst, 68, &reply, dlen);
diff --git a/dhcpv6.c b/dhcpv6.c
index 29c7e320..21b0e6bd 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -318,7 +318,7 @@ static bool dhcpv6_opt(struct iov_tail *data, uint16_t type)
* false otherwise and @data is unmodified
*/
static bool dhcpv6_ia_notonlink(struct iov_tail *data,
- struct in6_addr *la)
+ const struct in6_addr *la)
{
int ia_types[2] = { OPT_IA_NA, OPT_IA_TA };
struct opt_ia_addr opt_addr_storage;
@@ -567,6 +567,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
struct opt_hdr client_id_storage;
/* cppcheck-suppress [variableScope,unmatchedSuppression] */
struct opt_ia_na ia_storage;
+ const struct guest_addr *a;
const struct in6_addr *src;
struct msg_hdr mh_storage;
const struct msg_hdr *mh;
@@ -584,6 +585,15 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
if (c->no_dhcpv6)
return 1;
+ /* If no address is available, drop all messages rather than sending a
+ * bogus address. Information-request messages could ideally still be
+ * answered, but since we unconditionally include an IA_NA option in all
+ * replies (RFC 9915, Appendix B.), dropping them is safer.
+ */
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (!a)
+ return 1;
+
if (!IN6_IS_ADDR_MULTICAST(daddr))
return -1;
@@ -627,7 +637,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
if (mh->type == TYPE_CONFIRM && server_id)
return -1;
- if (dhcpv6_ia_notonlink(data, &c->ip6.addr)) {
+ if (dhcpv6_ia_notonlink(data, &a->addr.a6)) {
dhcpv6_send_ia_notonlink(c, saddr, data,
&client_id_base,
@@ -667,6 +677,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
default:
return -1;
}
+
if (ia)
resp.ia_na.iaid = ((struct opt_ia_na *)ia)->iaid;
@@ -681,8 +692,9 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
resp.hdr.xid = mh->xid;
+ resp.ia_addr.addr = a->addr.a6;
tap_udp6_send(c, src, 547, saddr, 546, mh->xid, &resp, n);
- c->ip6.addr_seen = c->ip6.addr;
+ c->ip6.addr_seen = a->addr.a6;
return 1;
}
@@ -705,6 +717,4 @@ void dhcpv6_init(const struct ctx *c)
c->our_tap_mac, sizeof(c->our_tap_mac));
memcpy(resp_not_on_link.server_id.duid_lladdr,
c->our_tap_mac, sizeof(c->our_tap_mac));
-
- resp.ia_addr.addr = c->ip6.addr;
}
diff --git a/fwd.c b/fwd.c
index 5679a3d6..e3f25438 100644
--- a/fwd.c
+++ b/fwd.c
@@ -243,6 +243,62 @@ void fwd_neigh_table_init(const struct ctx *c)
fwd_neigh_table_update(c, &mga, c->our_tap_mac, true);
}
+/**
+ * fwd_set_addr() - Add or update an address in the unified address array
+ * @c: Execution context
+ * @addr: Address to add (IPv4-mapped or IPv6)
+ * @flags: CONF_ADDR_* flags for this address
+ * @prefix_len: Prefix length in IPv6 format (96-128 for IPv4-mapped)
+ *
+ * Find the first existing entry of the same address family and
+ * overwrite it, or create a new one if none exists
+ */
+void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
+ uint8_t flags, int prefix_len)
+{
+ struct guest_addr *a;
+
+ assert(flags);
+
+ for_each_addr(a, c->addrs, c->addr_count, inany_af(addr))
+ goto found;
+
+ if (c->addr_count >= MAX_GUEST_ADDRS)
+ return;
+
+ a = &c->addrs[c->addr_count++];
+
+found:
+ a->addr = *addr;
+ a->prefix_len = prefix_len;
+ a->flags = flags;
+}
+
+/**
+ * fwd_get_addr() - Get guest address entry matching criteria
+ * @c: Execution context
+ * @af: Address family (AF_INET, AF_INET6, or AF_UNSPEC for any)
+ * @global: If true, match global only; if false, match link-local only
+ * @flags: Required provenance flags (any-match), 0 for any provenance
+ *
+ * Return: first address entry matching criteria, or NULL
+ */
+const struct guest_addr *fwd_get_addr(const struct ctx *c, sa_family_t af,
+ bool global, uint8_t flags)
+{
+ const struct guest_addr *a;
+
+ for_each_addr(a, c->addrs, c->addr_count, af) {
+ if (inany_is_linklocal6(&a->addr) == global)
+ continue;
+ if (!(a->flags & flags))
+ continue;
+ return a;
+ }
+
+ return NULL;
+}
+
/* Forwarding table storage, generally accessed via pointers in struct ctx */
static struct fwd_table fwd_in;
static struct fwd_table fwd_out;
@@ -799,8 +855,11 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
* translation, false otherwise
*/
static bool fwd_guest_accessible4(const struct ctx *c,
- const struct in_addr *addr)
+ const struct in_addr *addr)
{
+ const struct guest_addr *a;
+ const struct in_addr *a4;
+
if (IN4_IS_ADDR_LOOPBACK(addr))
return false;
@@ -815,7 +874,9 @@ static bool fwd_guest_accessible4(const struct ctx *c,
/* For IPv4, addr_seen is initialised to addr, so is always a valid
* address
*/
- if (IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr) ||
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ a4 = a ? inany_v4(&a->addr) : NULL;
+ if ((a4 && IN4_ARE_ADDR_EQUAL(addr, a4)) ||
IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
return false;
@@ -833,10 +894,13 @@ static bool fwd_guest_accessible4(const struct ctx *c,
static bool fwd_guest_accessible6(const struct ctx *c,
const struct in6_addr *addr)
{
+ const struct guest_addr *a;
+
if (IN6_IS_ADDR_LOOPBACK(addr))
return false;
- if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addr))
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (a && IN6_ARE_ADDR_EQUAL(addr, &a->addr.a6))
return false;
/* For IPv6, addr_seen starts unspecified, because we don't know what LL
@@ -881,16 +945,21 @@ static bool fwd_guest_accessible(const struct ctx *c,
static void nat_outbound(const struct ctx *c, const union inany_addr *addr,
union inany_addr *translated)
{
- if (inany_equals4(addr, &c->ip4.map_host_loopback))
+ const struct guest_addr *ga;
+
+ if (inany_equals4(addr, &c->ip4.map_host_loopback)) {
*translated = inany_loopback4;
- else if (inany_equals6(addr, &c->ip6.map_host_loopback))
+ } else if (inany_equals6(addr, &c->ip6.map_host_loopback)) {
*translated = inany_loopback6;
- else if (inany_equals4(addr, &c->ip4.map_guest_addr))
- *translated = inany_from_v4(c->ip4.addr);
- else if (inany_equals6(addr, &c->ip6.map_guest_addr))
- translated->a6 = c->ip6.addr;
- else
+ } else if (inany_equals4(addr, &c->ip4.map_guest_addr)) {
+ ga = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ *translated = ga ? ga->addr : inany_any4;
+ } else if (inany_equals6(addr, &c->ip6.map_guest_addr)) {
+ ga = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ translated->a6 = ga ? ga->addr.a6 : in6addr_any;
+ } else {
*translated = *addr;
+ }
}
/**
@@ -995,16 +1064,23 @@ bool nat_inbound(const struct ctx *c, const union inany_addr *addr,
} else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback) &&
inany_equals6(addr, &in6addr_loopback)) {
translated->a6 = c->ip6.map_host_loopback;
- } else if (!IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_guest_addr) &&
- inany_equals4(addr, &c->ip4.addr)) {
- *translated = inany_from_v4(c->ip4.map_guest_addr);
- } else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_guest_addr) &&
- inany_equals6(addr, &c->ip6.addr)) {
- translated->a6 = c->ip6.map_guest_addr;
- } else if (fwd_guest_accessible(c, addr)) {
- *translated = *addr;
} else {
- return false;
+ const struct guest_addr *ga4, *ga6;
+
+ ga4 = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ ga6 = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+
+ if (!IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_guest_addr) &&
+ ga4 && inany_equals(addr, &ga4->addr)) {
+ *translated = inany_from_v4(c->ip4.map_guest_addr);
+ } else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_guest_addr) &&
+ ga6 && inany_equals(addr, &ga6->addr)) {
+ translated->a6 = c->ip6.map_guest_addr;
+ } else if (fwd_guest_accessible(c, addr)) {
+ *translated = *addr;
+ } else {
+ return false;
+ }
}
return true;
diff --git a/fwd.h b/fwd.h
index b60697d9..d001d8ae 100644
--- a/fwd.h
+++ b/fwd.h
@@ -79,5 +79,9 @@ void fwd_neigh_table_free(const struct ctx *c,
void fwd_neigh_mac_get(const struct ctx *c, const union inany_addr *addr,
uint8_t *mac);
void fwd_neigh_table_init(const struct ctx *c);
+void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
+ uint8_t flags, int prefix_len);
+const struct guest_addr *fwd_get_addr(const struct ctx *c, sa_family_t af,
+ bool global, uint8_t flags);
#endif /* FWD_H */
diff --git a/inany.h b/inany.h
index d0befb15..ecd845a5 100644
--- a/inany.h
+++ b/inany.h
@@ -104,6 +104,36 @@ static inline struct in_addr *inany_v4(const union inany_addr *addr)
return (struct in_addr *)&addr->v4mapped.a4;
}
+/** inany_prefix_v4() - Extract IPv4 address and prefix length from inany
+ * @addr: IPv4 (as mapped) or IPv6 address
+ * @prefix_len: Prefix length in IPv6 format (96-128 for IPv4)
+ * @v4_plen: Set to IPv4 prefix length (0-32) on success
+ *
+ * Return: IPv4 address if @addr is IPv4, NULL otherwise
+ */
+static inline struct in_addr *inany_prefix_v4(const union inany_addr *addr,
+ int prefix_len, int *v4_plen)
+{
+ struct in_addr *a4 = inany_v4(addr);
+
+ if (!a4)
+ return NULL;
+
+ *v4_plen = prefix_len - 96;
+
+ return a4;
+}
+
+/** inany_af() - Get address family of IPv[46] address
+ * @addr: IPv4 or IPv6 address
+ *
+ * Return: AF_INET for IPv4, AF_INET6 for IPv6
+ */
+static inline sa_family_t inany_af(const union inany_addr *addr)
+{
+ return inany_v4(addr) ? AF_INET : AF_INET6;
+}
+
/** inany_default_prefix_len() - Get default prefix length for address
* @addr: IPv4 or iPv6 address
*
@@ -112,9 +142,9 @@ static inline struct in_addr *inany_v4(const union inany_addr *addr)
*/
static inline int inany_default_prefix_len(const union inany_addr *addr)
{
- const struct in_addr *v4 = inany_v4(addr);
+ const struct in_addr *a4 = inany_v4(addr);
- return v4 ? ip4_class_prefix_len(v4) + 96 : 64;
+ return a4 ? ip4_class_prefix_len(a4) + 96 : 64;
}
/** inany_equals - Compare two IPv[46] addresses
diff --git a/ip.h b/ip.h
index aab9b86a..ac93c543 100644
--- a/ip.h
+++ b/ip.h
@@ -19,6 +19,8 @@
(ntohl(((struct in_addr *)(a))->s_addr) >> IN_CLASSA_NSHIFT == IN_LOOPBACKNET)
#define IN4_IS_ADDR_MULTICAST(a) \
(IN_MULTICAST(ntohl(((struct in_addr *)(a))->s_addr)))
+#define IN4_MASK(prefix) \
+ ((prefix) <= 0 ? 0 : htonl(0xffffffff << (32 - (prefix))))
#define IN4_ARE_ADDR_EQUAL(a, b) \
(((struct in_addr *)(a))->s_addr == ((struct in_addr *)b)->s_addr)
#define IN4ADDR_LOOPBACK_INIT \
diff --git a/ndp.c b/ndp.c
index 1f2bcb0c..86df4798 100644
--- a/ndp.c
+++ b/ndp.c
@@ -257,7 +257,6 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
.valid_lifetime = ~0U,
.pref_lifetime = ~0U,
},
- .prefix = c->ip6.addr,
.source_ll = {
.header = {
.type = OPT_SRC_L2_ADDR,
@@ -265,8 +264,15 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
},
},
};
+ const struct guest_addr *a;
unsigned char *ptr = NULL;
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (!a)
+ return;
+
+ ra.prefix = a->addr.a6;
+
ptr = &ra.var[0];
if (c->mtu) {
@@ -460,6 +466,7 @@ first:
*/
void ndp_send_init_req(const struct ctx *c)
{
+ const struct guest_addr *a;
struct ndp_ns ns = {
.ih = {
.icmp6_type = NS,
@@ -468,8 +475,14 @@ void ndp_send_init_req(const struct ctx *c)
.icmp6_solicited = 0, /* Reserved */
.icmp6_override = 0, /* Reserved */
},
- .target_addr = c->ip6.addr
+ .target_addr = IN6ADDR_ANY_INIT
};
+
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (!a)
+ return;
+
+ ns.target_addr = a->addr.a6;
debug("Sending initial NDP NS request for guest MAC address");
- ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
+ ndp_send(c, &a->addr.a6, &ns, sizeof(ns));
}
diff --git a/passt.h b/passt.h
index afd8e9f0..5f68ab91 100644
--- a/passt.h
+++ b/passt.h
@@ -64,11 +64,30 @@ enum passt_modes {
MODE_VU,
};
+/* Maximum number of addresses in context address array */
+#define MAX_GUEST_ADDRS 32
+
+/**
+ * struct guest_addr - Unified IPv4/IPv6 address entry
+ * @addr: IPv4 (as mapped) or IPv6 address
+ * @prefix_len: Prefix length in IPv6/IPv4-mapped [0,128]/[96,128] format
+ * @flags: CONF_ADDR_* flags
+ */
+struct guest_addr {
+ union inany_addr addr;
+ uint8_t prefix_len;
+ uint8_t flags;
+#define CONF_ADDR_USER BIT(0) /* User set via -a */
+#define CONF_ADDR_HOST BIT(1) /* From host */
+#define CONF_ADDR_GEN BIT(2) /* Generated by code */
+#define CONF_ADDR_ANY (CONF_ADDR_USER | \
+ CONF_ADDR_HOST | \
+ CONF_ADDR_GEN) /* Any provenance */
+};
+
/**
* struct ip4_ctx - IPv4 execution context
- * @addr: IPv4 address assigned to guest
* @addr_seen: Latest IPv4 address seen as source from tap
- * @prefixlen: IPv4 prefix length (netmask)
* @guest_gw: IPv4 gateway as seen by the guest
* @map_host_loopback: Outbound connections to this address are NATted to the
* host's 127.0.0.1
@@ -87,9 +106,7 @@ enum passt_modes {
*/
struct ip4_ctx {
/* PIF_TAP addresses */
- struct in_addr addr;
struct in_addr addr_seen;
- int prefix_len;
struct in_addr guest_gw;
struct in_addr map_host_loopback;
struct in_addr map_guest_addr;
@@ -110,7 +127,6 @@ struct ip4_ctx {
/**
* struct ip6_ctx - IPv6 execution context
- * @addr: IPv6 address assigned to guest
* @addr_seen: Latest IPv6 global/site address seen as source from tap
* @addr_ll_seen: Latest IPv6 link-local address seen as source from tap
* @guest_gw: IPv6 gateway as seen by the guest
@@ -132,7 +148,6 @@ struct ip4_ctx {
*/
struct ip6_ctx {
/* PIF_TAP addresses */
- struct in6_addr addr;
struct in6_addr addr_seen;
struct in6_addr addr_ll_seen;
struct in6_addr guest_gw;
@@ -192,6 +207,8 @@ struct ip6_ctx {
* @fqdn: Guest FQDN
* @ifi6: Template interface for IPv6, -1: none, 0: IPv6 disabled
* @ip6: IPv6 configuration
+ * @addrs: Unified address array for both IPv4 (mapped) and IPv6
+ * @addr_count: Number of active entries in @addrs array
* @pasta_ifn: Name of namespace interface for pasta
* @pasta_ifi: Index of namespace interface for pasta
* @pasta_conf_ns: Configure namespace after creating it
@@ -276,6 +293,9 @@ struct ctx {
int ifi6;
struct ip6_ctx ip6;
+ struct guest_addr addrs[MAX_GUEST_ADDRS];
+ int addr_count;
+
char pasta_ifn[IF_NAMESIZE];
unsigned int pasta_ifi;
int pasta_conf_ns;
@@ -321,6 +341,37 @@ struct ctx {
extern struct ctx passt_ctx;
+/**
+ * next_addr_idx_() - Find next address index matching family filter
+ * @addrs: Array of guest addresses
+ * @count: Number of addresses in array
+ * @i: Starting index
+ * @af: Address family filter: AF_INET, AF_INET6, or AF_UNSPEC for all
+ *
+ * Return: next matching index, or count if none found
+ */
+static inline int next_addr_idx_(const struct guest_addr *addrs, int count,
+ int i, sa_family_t af)
+{
+ for (; i < count; i++) {
+ if (af == AF_UNSPEC || af == inany_af(&addrs[i].addr))
+ return i;
+ }
+ return i;
+}
+
+/**
+ * for_each_addr() - Iterate over addresses in array
+ * @a: Pointer variable for current entry (struct guest_addr *)
+ * @addrs: Array of guest addresses (e.g., c->addrs)
+ * @count: Number of addresses (e.g., c->addr_count)
+ * @af: Address family filter: AF_INET, AF_INET6, or 0 for all
+ */
+#define for_each_addr(a, addrs, count, af) \
+ for (int i_ = next_addr_idx_((addrs), (count), 0, (af)); \
+ i_ < (count) && ((a) = &(addrs)[i_], true); \
+ i_ = next_addr_idx_((addrs), (count), i_ + 1, (af)))
+
void proto_update_l2_buf(const unsigned char *eth_d);
#endif /* PASST_H */
diff --git a/pasta.c b/pasta.c
index 10f3ec8d..4cd58fd7 100644
--- a/pasta.c
+++ b/pasta.c
@@ -299,23 +299,32 @@ void pasta_start_ns(struct ctx *c, int argc, char *argv[])
*
* Return: 0 on success, negative error code on failure
*/
-static int pasta_conf_addrs(struct ctx *c, sa_family_t af, int ifi,
+static int pasta_conf_addrs(const struct ctx *c, sa_family_t af, int ifi,
bool no_copy)
{
+ const struct guest_addr *a;
+
if (!ifi)
return 0;
if (!no_copy)
return nl_addr_dup(nl_sock, ifi, nl_sock_ns, c->pasta_ifi, af);
- if (af == AF_INET && !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.addr))
+ a = fwd_get_addr(c, af, true, CONF_ADDR_ANY);
+ if (!a)
+ return 0;
+
+ if (af == AF_INET) {
+ const struct in_addr *a4;
+ int plen = 0;
+
+ a4 = inany_prefix_v4(&a->addr, a->prefix_len, &plen);
return nl_addr_set(nl_sock_ns, c->pasta_ifi, AF_INET,
- &c->ip4.addr, c->ip4.prefix_len);
- if (af == AF_INET6 && !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
- return nl_addr_set(nl_sock_ns, c->pasta_ifi, AF_INET6,
- &c->ip6.addr, 64);
+ a4, plen);
+ }
- return 0;
+ return nl_addr_set(nl_sock_ns, c->pasta_ifi, AF_INET6,
+ &a->addr.a6, a->prefix_len);
}
/**
diff --git a/tap.c b/tap.c
index dfa66c71..dd055a4e 100644
--- a/tap.c
+++ b/tap.c
@@ -1005,8 +1005,12 @@ resume:
c->ip6.addr_seen = *saddr;
}
- if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
- c->ip6.addr = *saddr;
+ if (!fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY) &&
+ !fwd_get_addr(c, AF_INET6, false, CONF_ADDR_ANY)) {
+ union inany_addr addr = { .a6 = *saddr };
+
+ fwd_set_addr(c, &addr, CONF_ADDR_GEN, 64);
+ }
} else if (!c->ip6.addr_fixed &&
!IN6_IS_ADDR_UNSPECIFIED(saddr)) {
c->ip6.addr_seen = *saddr;
--
2.52.0
1
0
[PATCH v4] pasta: Add --pass-fds option to skip closing file descriptors
by Richard Lawrence 04 Nov '26
by Richard Lawrence 04 Nov '26
04 Nov '26
When pasta spawns a command (`pasta [options] -- [command]`), all open file
descriptors inherited from the parent process except standard streams (0-2)
and the tap descriptor (`--fd`) are normally closed by `isolate_fds()`.
However, when pasta is used for testing, development, or launching tools like
Bubblewrap (`bwrap`), the spawned command may require file descriptors
passed directly from the parent process. In this case, closing inherited file
descriptors breaks expected behavior.
Add a boolean `--pass-fds` flag for pasta mode that skips the `close_range()`
cleanup step in `isolate_fds()`, leaving all open file descriptors intact
for the child command.
Summary of changes:
- Update `conf_tap_fd()` to `conf_fds()`, allowing early parsing of both
`-F`/`--fd` and `--pass-fds` before main option processing.
- Update `isolate_fds()` to check `c->pass_fds`: if set, skip calling
`close_range()` while still relocating the tap fd out of standard stream
range (0-2) if needed.
- Enforce validation in `conf()` ensuring `--pass-fds` is restricted to pasta
mode when spawning a command (incompatible with attaching to an
existing netns).
- Update man page and usage help output.
Link: https://bugs.passt.top/show_bug.cgi?id=204
Signed-off-by: Richard Lawrence <rlawrence(a)tamu.edu>
---
conf.c | 55 ++++++++++++++++++++++++++----------------
conf.h | 2 +-
isolation.c | 69 +++++++++++++++++++++++++++++++----------------------
isolation.h | 2 +-
passt.1 | 5 ++++
passt.c | 2 +-
passt.h | 2 ++
7 files changed, 84 insertions(+), 53 deletions(-)
diff --git a/conf.c b/conf.c
index 149a4c7..9f7eafb 100644
--- a/conf.c
+++ b/conf.c
@@ -750,7 +750,8 @@ pasta_opts:
" Don't copy all addresses to namespace\n"
" --ns-mac-addr ADDR Set MAC address on tap interface\n"
" --no-splice Disable inbound socket splicing\n"
- " --splice-only Only enable loopback forwarding\n");
+ " --splice-only Only enable loopback forwarding\n"
+ " --pass-fds Preserve open file descriptors\n");
passt_exit(status);
}
@@ -1137,35 +1138,38 @@ static void conf_sock_listen(const struct ctx *c)
}
/**
- * conf_tap_fd() - Read tap fd as supplied by -F command line option
+ * conf_fds() - Read fds as supplied by -F/--fd and --pass-fds options
+ * @c: Execution context
* @argc: Argument count
* @argv: Command line options
- *
- * Return: fd number from --fd option, or -1 if not supplied
*/
-int conf_tap_fd(int argc, char **argv)
+void conf_fds(struct ctx *c, int argc, char **argv)
{
- const struct option optfd[] = { { "fd", required_argument, NULL, 'F' },
- { 0 }, };
- const char *fdarg = NULL, *p;
- unsigned long val;
+ const struct option opt[] = {
+ { "fd", required_argument, NULL, 'F' },
+ { "pass-fds", no_argument, NULL, 2 },
+ { 0 },
+ };
int name;
+ c->fd_tap = -1;
+ c->pass_fds = false;
optind = 0;
do {
- name = getopt_long(argc, argv, "-:F:", optfd, NULL);
- if (name == 'F')
- fdarg = optarg;
- } while (name != -1);
-
- if (!fdarg)
- return -1;
+ name = getopt_long(argc, argv, "-:F:", opt, NULL);
+ if (name == 'F') {
+ const char *p = optarg;
+ unsigned long val;
- p = fdarg;
- if (!parse_unsigned(&p, 0, &val) || !parse_eoi(p) || val > INT_MAX)
- die("Invalid --fd: %s", fdarg);
+ if (!parse_unsigned(&p, 0, &val) || !parse_eoi(p) ||
+ val > INT_MAX)
+ die("Invalid --fd: %s", optarg);
- return val;
+ c->fd_tap = (int)val;
+ } else if (name == 2) {
+ c->pass_fds = true;
+ }
+ } while (name != -1);
}
/**
@@ -1317,6 +1321,7 @@ void conf(struct ctx *c, int argc, char **argv)
{"conf-path", required_argument, NULL, 'c' },
{"chroot-fallback", no_argument, NULL, 32 },
{"no-pidns", no_argument, NULL, 33 },
+ {"pass-fds", no_argument, NULL, 34 },
{ 0 },
};
const char *optstring = "+dqfel:hs:c:F:I:p:P:m:a:n:M:g:i:o:D:S:H:461t:u:T:U:";
@@ -1567,6 +1572,11 @@ void conf(struct ctx *c, int argc, char **argv)
c->no_pidns = true;
break;
+ case 34:
+ /* --pass-fds was parsed early */
+ if (c->mode != MODE_PASTA)
+ die("--pass-fds is for pasta mode only");
+ break;
case 'd':
c->debug = 1;
c->quiet = 0;
@@ -1603,7 +1613,7 @@ void conf(struct ctx *c, int argc, char **argv)
c->fd_control_listen = c->fd_control = -1;
break;
case 'F':
- /* --fd was parsed early and c->fd_tap set in main() */
+ /* --fd was parsed early */
c->one_off = true;
*c->sock_path = 0;
break;
@@ -1929,6 +1939,9 @@ void conf(struct ctx *c, int argc, char **argv)
else if (optind != argc)
die("Extra non-option argument: %s", argv[optind]);
+ if (c->pass_fds && *netns)
+ die("--pass-fds can only be used when spawning a command");
+
if (c->no_pidns && *netns)
die("--no-pidns is incompatible with PID or --netns");
diff --git a/conf.h b/conf.h
index 19bf9bc..654fd16 100644
--- a/conf.h
+++ b/conf.h
@@ -7,7 +7,7 @@
#define CONF_H
enum passt_modes conf_mode(int argc, char *argv[]);
-int conf_tap_fd(int argc, char **argv);
+void conf_fds(struct ctx *c, int argc, char **argv);
void conf(struct ctx *c, int argc, char **argv);
void conf_listen_handler(struct ctx *c, uint32_t events);
void conf_handler(struct ctx *c, uint32_t events);
diff --git a/isolation.c b/isolation.c
index a00475f..1875e6b 100644
--- a/isolation.c
+++ b/isolation.c
@@ -250,46 +250,57 @@ void isolate_initial(void)
}
/*
- * isolate_fds() - Close leaked files, but not --fd, stdin, stdout, stderr
+ * isolate_fds() - Close leaked files if applicable
+ * @c: Execution context
* @argc: Argument count
- * @argv: Command line options, as we need to skip any file given via --fd
+ * @argv: Command line options
*
* Should:
- * - close all open files except for standard streams and the one from --fd
+ * - close all open files except for standard streams and the one from --fd,
+ unless --pass-fds is used, in which case all fds are preserved
* - move the --fd descriptor out of the range 0-2
- *
- * Return: new fd number for descriptor from --fd, or -1 if not specified
+ * - update the execution context
*/
-int isolate_fds(int argc, char **argv)
+void isolate_fds(struct ctx *c, int argc, char **argv)
{
- int fd, close_from = STDERR_FILENO + 1;
+ conf_fds(c, argc, argv);
- fd = conf_tap_fd(argc, argv);
+ if (c->pass_fds) {
+ if (c->fd_tap >= 0 && c->fd_tap < STDERR_FILENO + 1) {
+ /* Move the tap fd to a safer location */
+ int new_fd = fcntl(c->fd_tap, F_DUPFD, STDERR_FILENO + 1);
- if (fd >= 0) {
- /* Move the passed fd to a more convenient location */
- if (fd != close_from &&
- (dup2(fd, close_from) != close_from ||
- close(fd)))
- die_perror("Could not move --fd descriptor");
- fd = close_from++;
- }
+ if (new_fd < 0 || close(c->fd_tap))
+ die_perror("Could not relocate --fd descriptor");
+ c->fd_tap = new_fd;
+ }
+ } else {
+ int close_from = STDERR_FILENO + 1;
+
+ if (c->fd_tap >= 0) {
+ /* Move the tap fd to a more convenient location */
+ if (c->fd_tap != close_from &&
+ (dup2(c->fd_tap, close_from) != close_from ||
+ close(c->fd_tap)))
+ die_perror("Could not move --fd descriptor");
+ c->fd_tap = close_from++;
+ }
- if (close_range(close_from, ~0U, CLOSE_RANGE_UNSHARE)) {
- if (errno == ENOSYS || errno == EINVAL) {
- /* This probably means close_range() or the
- * CLOSE_RANGE_UNSHARE flag is not supported by the
- * kernel. Not much we can do here except carry on and
- * hope for the best.
- */
- warn(
-"Can't use close_range() to ensure no files leaked by parent");
- } else {
- die_perror("Failed to close files leaked by parent");
+ /* Close leaked file descriptors */
+ if (close_range(close_from, ~0U, CLOSE_RANGE_UNSHARE)) {
+ if (errno == ENOSYS || errno == EINVAL) {
+ /* This probably means close_range() or the
+ * CLOSE_RANGE_UNSHARE flag is not supported by the
+ * kernel. Not much we can do here except carry on and
+ * hope for the best.
+ */
+ warn(
+ "Can't use close_range() to ensure no files leaked by parent");
+ } else {
+ die_perror("Failed to close files leaked by parent");
+ }
}
}
-
- return fd;
}
/**
diff --git a/isolation.h b/isolation.h
index ec47038..fe8d7d5 100644
--- a/isolation.h
+++ b/isolation.h
@@ -11,7 +11,7 @@
#include <unistd.h>
void isolate_initial(void);
-int isolate_fds(int argc, char **argv);
+void isolate_fds(struct ctx *c, int argc, char **argv);
void isolate_user(const struct ctx *c, uid_t uid, gid_t gid, bool use_userns,
const char *userns);
int isolate_prefork(const struct ctx *c);
diff --git a/passt.1 b/passt.1
index 0b780fe..56e241f 100644
--- a/passt.1
+++ b/passt.1
@@ -766,6 +766,11 @@ of local traffic in pasta\fR in the \fBNOTES\fR for more details.
Do not create a tap device in the namespace. In this mode, \fIpasta\fR only
forwards loopback traffic between namespaces.
+.TP
+.BR \-\-pass\-fds
+Preserve all open file descriptors and pass them to the spawned command.
+This option may only be used when \fBpasta\fR spawns a command.
+
.SH EXAMPLES
.SS \fBpasta
diff --git a/passt.c b/passt.c
index 5054551..fa7afc5 100644
--- a/passt.c
+++ b/passt.c
@@ -345,7 +345,7 @@ int main(int argc, char **argv)
arch_avx2_exec(argv);
isolate_initial();
- c->fd_tap = isolate_fds(argc, argv);
+ isolate_fds(c, argc, argv);
if ((devnull_fd = open("/dev/null", O_RDWR | O_CLOEXEC)) < 0)
die_perror("Failed to open /dev/null");
diff --git a/passt.h b/passt.h
index afd8e9f..947e859 100644
--- a/passt.h
+++ b/passt.h
@@ -196,6 +196,7 @@ struct ip6_ctx {
* @pasta_ifi: Index of namespace interface for pasta
* @pasta_conf_ns: Configure namespace after creating it
* @no_pidns: Don't create a new PID namespace for spawned command
+ * @pass_fds: Don't close leaked file descriptors
* @fwd: Forwarding tables
* @fwd_pending: Pending forward tables
* @no_tcp: Disable TCP operation
@@ -280,6 +281,7 @@ struct ctx {
unsigned int pasta_ifi;
int pasta_conf_ns;
bool no_pidns;
+ bool pass_fds;
struct fwd_table *fwd[PIF_NUM_TYPES];
struct fwd_table *fwd_pending[PIF_NUM_TYPES];
--
2.52.0
1
0
04 Nov '26
As preparation for supporting multiple addresses per interface,
we replace the single addr/prefix_len fields with an array. The
array consists of a new struct inany_addr_entry containing an
address and prefix length, both in inany_addr format.
Despite some code refactoring, there are only two real functional
changes:
- The indicated IPv6 prefix length is now properly stored, instead
of being ignored and overridden with the hardcoded value 64, as
has been the case until now.
- Since even IPv4 addresses now are stored in IPv6 format, we
also store the corresponding prefix length in that format,
i.e. using the range [96,128] instead of [0,32].
In conf_ip6(), the explicit IN6_IS_ADDR_UNSPECIFIED() check on the
address at the end of the function is no longer needed: for
host-discovered addresses, it is now checked inside the if (!a) block
before calling fwd_set_addr(); for user-provided addresses (via -a),
validation already rejects unspecified addresses at parse time.
Signed-off-by: Jon Maloy <jmaloy(a)redhat.com>
---
v2: -Using inany_addr instead of protocol specific addresses as
entry address field.
v3: -Merging into one array, directly in struct ctx
-Changed prefix_len and flags fields in struct inany_addr_entry
to uint8_t, since that makes the struct directly migratable
v4: -Updated according to changes in previous commits
-Updated according to feedback from David G.
-Squashed IP4_MASK macro commit into this one
v6: -Renamed and moved some definitions
-Introduced fwd_set_addr() and fwd_get_addr() already in this commit
-Eliminated first_v4/v6() functions, replaced with fwd_get_addr()
-Some other changes as suggested by David G.
-I kept the flag CONF_ADDR_LINKLOCAL, since it will be
needed later in an address selection function.
v7: -Introduced CONF_ADDR_GENERATED flag
-Other fixes based on feedback from David and Stefano.
-I changed signature of inany_prefix_len(), but I did not change
its semantics, since the premise of David's comment is wrong: the
caller does *not* explicitly know he is dealing with an IPv4 address.
In fact, there are examples later in this series where it may be an
IPv6 address, and the caller just trusts he gets the return value in
the appropriate format.
-Intoduced the inverse of inany_prefix_len(), called inany_prefix_len6()
which always returns the prefix in IPv6 or mapped IPv4 format.
The name of the function isn't great, but any alternative I came up
with became too long to be practical.
v8: -Added inany_prefix_v4() (David G.) to extract IPv4 address and
prefix length simultaneously, replacing inany_prefix_len().
-Removed both inany_prefix_len() and inany_prefix_len6()
auto-detecting functions. Prefix length attached to an inany is
now always stored in IPv6 format (96-128 for IPv4-mapped).
-fwd_set_addr() now requires prefix_len in IPv6 format; callers
updated to pass prefix_len + 96 for IPv4 addresses.
-Fixed -n option to store prefix_len in IPv6 format.
-Fixed addr_seen regression: when address was set via -a,
addr_seen was not initialised in conf_ip4().
-Replaced assert() with graceful no-op in dhcp.c and ndp.c when
no address is configured (David G.).
-Note: addr_seen is still initialised both at conf_ip*() time
and at DHCP/DHCPv6 send time (David's concern). This
inconsistency will be addressed in a follow-up.
-Introduced a new CONF_ADDR_ANY macro to be used for address
selection, as suggested by Stefano.
-Deferred introduction of an explicit scope field (replacing
CONF_ADDR_LINKLOCAL flag) to a later commit or series.
v9: -Rebased on changes made by David Gibson.
-Some minor changes based on feedback from David.
-Defined a new macro CONF_ADDR_GLOBAL to supplant CONF_ADDR_ANY.
-I did not follow up David's suggestion to introduce three
functions to replace the current fwd_get_addr(), because I
don't think it would solve any problems. The 'flags' field
encode both provenance and type properties orthogonally, and
callers need different combinations of them. That is the very
point of that field, and I see nothing confusing in it.
It is correct that some of the calls to fwd_get_addr() go away
in later commits, but enough of them remain to justify its
existence. Defining three new functions to replace it would only
make them wrappers around fwd_get_addr(), and serve no purpose in
my view.
v10: -Redefined and refactored function fwd_get_addr().
v11: -Changes after feedback from Stefano.
- Fixed coverity false positives.
- Fixed bug introduced by wrongly handling ipv4 link local
addresses analogously to ipv6 ditto. These addresses should in
practice be handled like global IPv4 address. Removed now
redundant inany_is_linklocal() function and let fwd_get_addr()
only use inany_is_linklocal6().
- In the same function the removal of the fallback does not
imply a functional change:
* -a without a prefix still gets the class-based default from
inany_default_prefix_len()
* host-discovered addresses get their prefix_len from netlink.
- The changes in conf_print() is not unrelated: inet_ntop() may
fail and leave a stale buffer.
- Re-introduced CONF_ADDR_ANY, this time to 0
- Kept next_addr_idx_(). There is a working solution without it,
but (kernel) checkpatch complains about it.
solution for this.
- Some other minor changes
v12: - Re-defined CONF_ADDR_ANY to be a bitmap
- Added assert() in fwd_set_addr() to ensure no address is added
without at least one flag.
v13: - Renamed 'v4' to 'a4' all through the commit
- In dhcpv6(): set resp.ia_addr.addr dynamically at send time from
the address fetched by fwd_get_addr(), instead of caching a
potentially stale value in dhcpv6_init(). We drop non-
INFORMATION_REQUEST messages silently if no address is available.
- Removed address setup from dhcpv6_init() accordingly.
v14: - In dhcpv6() we now drop all messages when no address is available,
as suggested by Stefano.
v15: - Moved fwd_get_addr() call and the !a early return to just after the
no_dhcpv6 check, so 'a' is guaranteed non-NULL for the remainder of
dhcpv6().
---
arp.c | 21 +++++++-
conf.c | 157 +++++++++++++++++++++++++++++++++----------------------
dhcp.c | 20 +++++--
dhcpv6.c | 20 +++++--
fwd.c | 114 +++++++++++++++++++++++++++++++++-------
fwd.h | 4 ++
inany.h | 34 +++++++++++-
ip.h | 2 +
ndp.c | 19 +++++--
passt.h | 63 +++++++++++++++++++---
pasta.c | 23 +++++---
tap.c | 8 ++-
12 files changed, 373 insertions(+), 112 deletions(-)
diff --git a/arp.c b/arp.c
index bb042e95..08d5a9f9 100644
--- a/arp.c
+++ b/arp.c
@@ -41,6 +41,9 @@
static bool ignore_arp(const struct ctx *c,
const struct arphdr *ah, const struct arpmsg *am)
{
+ const struct guest_addr *a;
+ const struct in_addr *a4;
+
if (ah->ar_hrd != htons(ARPHRD_ETHER) ||
ah->ar_pro != htons(ETH_P_IP) ||
ah->ar_hln != ETH_ALEN ||
@@ -54,7 +57,11 @@ static bool ignore_arp(const struct ctx *c,
return true;
/* Don't resolve the guest's assigned address, either. */
- if (!memcmp(am->tip, &c->ip4.addr, sizeof(am->tip)))
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ if (!a)
+ return false;
+ a4 = inany_v4(&a->addr);
+ if (a4 && !memcmp(am->tip, a4, sizeof(am->tip)))
return true;
return false;
@@ -123,12 +130,22 @@ int arp(const struct ctx *c, struct iov_tail *data)
*/
void arp_send_init_req(const struct ctx *c)
{
+ const struct guest_addr *a;
+ const struct in_addr *a4;
struct {
struct ethhdr eh;
struct arphdr ah;
struct arpmsg am;
} __attribute__((__packed__)) req;
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ if (!a)
+ return;
+
+ a4 = inany_v4(&a->addr);
+ if (!a4)
+ return;
+
/* Ethernet header */
req.eh.h_proto = htons(ETH_P_ARP);
memcpy(req.eh.h_dest, MAC_BROADCAST, sizeof(req.eh.h_dest));
@@ -145,7 +162,7 @@ void arp_send_init_req(const struct ctx *c)
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));
+ memcpy(req.am.tip, a4, sizeof(req.am.tip));
debug("Sending initial ARP request for guest MAC address");
tap_send_single(c, &req, sizeof(req));
diff --git a/conf.c b/conf.c
index 149a4c79..66aa524e 100644
--- a/conf.c
+++ b/conf.c
@@ -372,13 +372,19 @@ static uint8_t conf_ip4_prefix(const char *arg)
/**
* conf_ip4() - Verify or detect IPv4 support, get relevant addresses
+ * @c: Execution context
* @ifi: Host interface to attempt (0 to determine one)
- * @ip4: IPv4 context (will be written)
*
* Return: interface index for IPv4, or 0 on failure.
*/
-static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
+static unsigned int conf_ip4(struct ctx *c, unsigned int ifi)
{
+ struct ip4_ctx *ip4 = &c->ip4;
+ const struct guest_addr *a;
+ const struct in_addr *a4;
+
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+
if (!ifi)
ifi = nl_get_ext_if(nl_sock, AF_INET);
@@ -397,67 +403,75 @@ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4)
}
}
- if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr)) {
+ if (!a) {
+ struct in_addr addr = { 0 };
+ int prefix_len = 0;
int rc = nl_addr_get(nl_sock, ifi, AF_INET,
- &ip4->addr, &ip4->prefix_len, NULL);
+ &addr, &prefix_len, NULL);
if (rc < 0) {
debug("Couldn't discover IPv4 address: %s",
strerror_(-rc));
return 0;
}
+ if (IN4_IS_ADDR_UNSPECIFIED(&addr)) {
+ warn("Host returned address 0.0.0.0");
+ return 0;
+ }
+ fwd_set_addr(c, &inany_from_v4(addr), CONF_ADDR_HOST,
+ prefix_len + 96);
}
- if (!ip4->prefix_len) {
- in_addr_t addr = ntohl(ip4->addr.s_addr);
- if (IN_CLASSA(addr))
- ip4->prefix_len = (32 - IN_CLASSA_NSHIFT);
- else if (IN_CLASSB(addr))
- ip4->prefix_len = (32 - IN_CLASSB_NSHIFT);
- else if (IN_CLASSC(addr))
- ip4->prefix_len = (32 - IN_CLASSC_NSHIFT);
- else
- ip4->prefix_len = 32;
- }
-
- ip4->addr_seen = ip4->addr;
-
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ assert(a);
+ a4 = inany_v4(&a->addr);
+ assert(a4);
+ ip4->addr_seen = *a4;
ip4->our_tap_addr = ip4->guest_gw;
- if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
- return 0;
-
return ifi;
}
/**
* conf_ip4_local() - Configure IPv4 addresses and attributes for local mode
- * @ip4: IPv4 context (will be written)
+ * @c: Execution context (will be written)
*/
-static void conf_ip4_local(struct ip4_ctx *ip4)
+static void conf_ip4_local(struct ctx *c)
{
- if (IN4_IS_ADDR_UNSPECIFIED(&ip4->addr))
- ip4->addr = IP4_LL_GUEST_ADDR;
- ip4->addr_seen = ip4->addr;
+ struct ip4_ctx *ip4 = &c->ip4;
+ const struct guest_addr *a;
+
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+
+ if (!a) {
+ fwd_set_addr(c, &inany_from_v4(IP4_LL_GUEST_ADDR),
+ CONF_ADDR_GEN, IP4_LL_PREFIX_LEN + 96);
+ ip4->addr_seen = IP4_LL_GUEST_ADDR;
+ } else {
+ const struct in_addr *a4 = inany_v4(&a->addr);
+
+ assert(a4);
+ ip4->addr_seen = *a4;
+ }
if (IN4_IS_ADDR_UNSPECIFIED(&ip4->guest_gw))
ip4->guest_gw = IP4_LL_GUEST_GW;
ip4->our_tap_addr = ip4->guest_gw;
- if (!ip4->prefix_len)
- ip4->prefix_len = IP4_LL_PREFIX_LEN;
-
ip4->no_copy_addrs = ip4->no_copy_routes = true;
}
/**
* conf_ip6() - Verify or detect IPv6 support, get relevant addresses
+ * @c: Execution context
* @ifi: Host interface to attempt (0 to determine one)
- * @ip6: IPv6 context (will be written)
*
* Return: interface index for IPv6, or 0 on failure.
*/
-static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
+static unsigned int conf_ip6(struct ctx *c, unsigned int ifi)
{
+ struct ip6_ctx *ip6 = &c->ip6;
+ const struct guest_addr *a;
+ union inany_addr addr;
int prefix_len = 0;
int rc;
@@ -478,23 +492,30 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
}
}
- rc = nl_addr_get(nl_sock, ifi, AF_INET6,
- IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ? &ip6->addr : NULL,
+ rc = nl_addr_get(nl_sock, ifi, AF_INET6, &addr.a6,
&prefix_len, &ip6->our_tap_ll);
if (rc < 0) {
debug("Couldn't discover IPv6 address: %s", strerror_(-rc));
return 0;
}
- ip6->addr_seen = ip6->addr;
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (!a) {
+ if (IN6_IS_ADDR_UNSPECIFIED(&addr))
+ return 0;
+
+ fwd_set_addr(c, &addr, CONF_ADDR_HOST, prefix_len);
+ ip6->addr_seen = addr.a6;
+ } else {
+ ip6->addr_seen = a->addr.a6;
+ }
if (IN6_IS_ADDR_LINKLOCAL(&ip6->guest_gw))
ip6->our_tap_ll = ip6->guest_gw;
else
ip6->our_tap_addr = ip6->guest_gw;
- if (IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ||
- IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
+ if (IN6_IS_ADDR_UNSPECIFIED(&ip6->our_tap_ll))
return 0;
return ifi;
@@ -502,10 +523,12 @@ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6)
/**
* conf_ip6_local() - Configure IPv6 addresses and attributes for local mode
- * @ip6: IPv6 context (will be written)
+ * @c: Execution context (will be written)
*/
-static void conf_ip6_local(struct ip6_ctx *ip6)
+static void conf_ip6_local(struct ctx *c)
{
+ struct ip6_ctx *ip6 = &c->ip6;
+
if (IN6_IS_ADDR_UNSPECIFIED(&ip6->guest_gw))
ip6->guest_gw = IP6_LL_GUEST_GW;
@@ -801,6 +824,7 @@ enum passt_modes conf_mode(int argc, char *argv[])
static void conf_print(const struct ctx *c)
{
char buf[INANY_ADDRSTRLEN];
+ const struct guest_addr *a;
int i;
if (c->fd_control_listen >= 0)
@@ -848,16 +872,19 @@ static void conf_print(const struct ctx *c)
inet_ntop(AF_INET, &c->ip4.map_host_loopback,
buf, sizeof(buf)));
- if (!c->no_dhcp) {
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ if (a && !c->no_dhcp) {
uint32_t mask;
+ int plen = 0;
- mask = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+ inany_prefix_v4(&a->addr, a->prefix_len, &plen);
+ mask = IN4_MASK(plen);
info("DHCP:");
info(" assign: %s",
- inet_ntop(AF_INET, &c->ip4.addr, buf, sizeof(buf)));
+ inany_ntop(&a->addr, buf, sizeof(buf)));
info(" mask: %s",
- inet_ntop(AF_INET, &mask, buf, sizeof(buf)));
+ inet_ntop(AF_INET, &mask, buf, sizeof(buf)));
info(" router: %s",
inet_ntop(AF_INET, &c->ip4.guest_gw,
buf, sizeof(buf)));
@@ -868,8 +895,8 @@ static void conf_print(const struct ctx *c)
break;
if (!i)
info("DNS:");
- inet_ntop(AF_INET, &c->ip4.dns[i], buf, sizeof(buf));
- info(" %s", buf);
+ info(" %s", inet_ntop(AF_INET, &c->ip4.dns[i],
+ buf, sizeof(buf)));
}
for (i = 0; *c->dns_search[i].n; i++) {
@@ -894,13 +921,14 @@ static void conf_print(const struct ctx *c)
else
goto dns6;
- info(" assign: %s",
- inet_ntop(AF_INET6, &c->ip6.addr, buf, sizeof(buf)));
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (a)
+ info(" assign: %s",
+ inany_ntop(&a->addr, buf, sizeof(buf)));
info(" router: %s",
inet_ntop(AF_INET6, &c->ip6.guest_gw, buf, sizeof(buf)));
info(" our link-local: %s",
- inet_ntop(AF_INET6, &c->ip6.our_tap_ll,
- buf, sizeof(buf)));
+ inet_ntop(AF_INET6, &c->ip6.our_tap_ll, buf, sizeof(buf)));
dns6:
for (i = 0; i < ARRAY_SIZE(c->ip6.dns); i++) {
@@ -908,8 +936,10 @@ dns6:
break;
if (!i)
info("DNS:");
- inet_ntop(AF_INET6, &c->ip6.dns[i], buf, sizeof(buf));
- info(" %s", buf);
+ info(" %s",
+ inet_ntop(AF_INET6, &c->ip6.dns[i],
+ buf, sizeof(buf)));
+
}
for (i = 0; *c->dns_search[i].n; i++) {
@@ -1213,15 +1243,13 @@ static bool conf_addr(struct ctx *c, char *arg, uint8_t opt_n)
inany_is_loopback(&addr))
goto bad;
+ fwd_set_addr(c, &addr, CONF_ADDR_USER, prefix_len);
if (a4) {
- c->ip4.addr = *a4;
- c->ip4.prefix_len = prefix_len - 96;
- c->ip4.addr_fixed = true;
c->ip4.no_copy_addrs = true;
+ c->ip4.addr_fixed = true;
} else {
- c->ip6.addr = addr.a6;
- c->ip6.addr_fixed = true;
c->ip6.no_copy_addrs = true;
+ c->ip6.addr_fixed = true;
}
return is_prefix;
@@ -1331,6 +1359,7 @@ void conf(struct ctx *c, int argc, char **argv)
unsigned int ifi4 = 0, ifi6 = 0;
bool opt_a_is_prefix = false;
const char *logfile = NULL;
+ struct guest_addr *a;
char *runas = NULL;
size_t logsize = 0;
uint8_t opt_n = 0;
@@ -1652,8 +1681,12 @@ void conf(struct ctx *c, int argc, char **argv)
if (opt_a_is_prefix)
die("Redundant prefix length specification");
- c->ip4.prefix_len = conf_ip4_prefix(optarg);
- opt_n = c->ip4.prefix_len + 96;
+ opt_n = conf_ip4_prefix(optarg) + 96;
+
+ for_each_addr(a, c->addrs, c->addr_count, AF_INET) {
+ a->prefix_len = opt_n;
+ break;
+ }
break;
case 'M':
p = optarg;
@@ -1853,9 +1886,9 @@ void conf(struct ctx *c, int argc, char **argv)
nl_sock_init(c, false);
if (!v6_only && !c->splice_only)
- c->ifi4 = conf_ip4(ifi4, &c->ip4);
+ c->ifi4 = conf_ip4(c, ifi4);
if (!v4_only && !c->splice_only)
- c->ifi6 = conf_ip6(ifi6, &c->ip6);
+ c->ifi6 = conf_ip6(c, ifi6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) {
warn("MTU %"PRIu16" is too small for IPv4 (minimum %u)",
@@ -1878,7 +1911,7 @@ void conf(struct ctx *c, int argc, char **argv)
if (!c->ifi4 && !v6_only) {
if (!c->splice_only) {
info("IPv4: no external interface as template, use local mode");
- conf_ip4_local(&c->ip4);
+ conf_ip4_local(c);
}
c->ifi4 = -1;
}
@@ -1886,7 +1919,7 @@ void conf(struct ctx *c, int argc, char **argv)
if (!c->ifi6 && !v4_only) {
if (!c->splice_only) {
info("IPv6: no external interface as template, use local mode");
- conf_ip6_local(&c->ip6);
+ conf_ip6_local(c);
}
c->ifi6 = -1;
}
@@ -1959,7 +1992,7 @@ void conf(struct ctx *c, int argc, char **argv)
if (!c->ifi6) {
c->no_ndp = 1;
c->no_dhcpv6 = 1;
- } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) {
+ } else if (!fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY)) {
c->no_dhcpv6 = 1;
}
diff --git a/dhcp.c b/dhcp.c
index c3c74224..9336f504 100644
--- a/dhcp.c
+++ b/dhcp.c
@@ -303,7 +303,9 @@ static void opt_set_dns_search(const struct ctx *c, size_t max_len)
int dhcp(const struct ctx *c, struct iov_tail *data)
{
char macstr[ETH_ADDRSTRLEN];
+ const struct guest_addr *a;
size_t mlen, dlen, opt_len;
+ const struct in_addr *addr;
struct in_addr mask, dst;
struct ethhdr eh_storage;
struct iphdr iph_storage;
@@ -315,6 +317,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
struct msg const *m;
struct msg reply;
unsigned int i;
+ int plen;
eh = IOV_REMOVE_HEADER(data, eh_storage);
iph = IOV_PEEK_HEADER(data, iph_storage);
@@ -344,6 +347,15 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
m->op != BOOTREQUEST)
return -1;
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_USER);
+ if (!a)
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ if (!a)
+ return -1;
+
+ addr = inany_prefix_v4(&a->addr, a->prefix_len, &plen);
+ assert(addr);
+
reply.op = BOOTREPLY;
reply.htype = m->htype;
reply.hlen = m->hlen;
@@ -352,7 +364,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
reply.secs = 0;
reply.flags = m->flags;
reply.ciaddr = m->ciaddr;
- reply.yiaddr = c->ip4.addr;
+ reply.yiaddr = *addr;
reply.siaddr = 0;
reply.giaddr = m->giaddr;
memcpy(&reply.chaddr, m->chaddr, sizeof(reply.chaddr));
@@ -408,7 +420,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
info(" from %s", eth_ntop(m->chaddr, macstr, sizeof(macstr)));
- mask.s_addr = htonl(0xffffffff << (32 - c->ip4.prefix_len));
+ mask.s_addr = IN4_MASK(plen);
memcpy(opts[1].s, &mask, sizeof(mask));
memcpy(opts[3].s, &c->ip4.guest_gw, sizeof(c->ip4.guest_gw));
memcpy(opts[54].s, &c->ip4.our_tap_addr, sizeof(c->ip4.our_tap_addr));
@@ -416,7 +428,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
/* If the gateway is not on the assigned subnet, send an option 121
* (Classless Static Routing) adding a dummy route to it.
*/
- if ((c->ip4.addr.s_addr & mask.s_addr)
+ if ((addr->s_addr & mask.s_addr)
!= (c->ip4.guest_gw.s_addr & mask.s_addr)) {
/* a.b.c.d/32:0.0.0.0, 0:a.b.c.d */
opts[121].slen = 14;
@@ -475,7 +487,7 @@ int dhcp(const struct ctx *c, struct iov_tail *data)
if (m->flags & FLAG_BROADCAST)
dst = in4addr_broadcast;
else
- dst = c->ip4.addr;
+ dst = *addr;
tap_udp4_send(c, c->ip4.our_tap_addr, 67, dst, 68, &reply, dlen);
diff --git a/dhcpv6.c b/dhcpv6.c
index 29c7e320..21b0e6bd 100644
--- a/dhcpv6.c
+++ b/dhcpv6.c
@@ -318,7 +318,7 @@ static bool dhcpv6_opt(struct iov_tail *data, uint16_t type)
* false otherwise and @data is unmodified
*/
static bool dhcpv6_ia_notonlink(struct iov_tail *data,
- struct in6_addr *la)
+ const struct in6_addr *la)
{
int ia_types[2] = { OPT_IA_NA, OPT_IA_TA };
struct opt_ia_addr opt_addr_storage;
@@ -567,6 +567,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
struct opt_hdr client_id_storage;
/* cppcheck-suppress [variableScope,unmatchedSuppression] */
struct opt_ia_na ia_storage;
+ const struct guest_addr *a;
const struct in6_addr *src;
struct msg_hdr mh_storage;
const struct msg_hdr *mh;
@@ -584,6 +585,15 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
if (c->no_dhcpv6)
return 1;
+ /* If no address is available, drop all messages rather than sending a
+ * bogus address. Information-request messages could ideally still be
+ * answered, but since we unconditionally include an IA_NA option in all
+ * replies (RFC 9915, Appendix B.), dropping them is safer.
+ */
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (!a)
+ return 1;
+
if (!IN6_IS_ADDR_MULTICAST(daddr))
return -1;
@@ -627,7 +637,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
if (mh->type == TYPE_CONFIRM && server_id)
return -1;
- if (dhcpv6_ia_notonlink(data, &c->ip6.addr)) {
+ if (dhcpv6_ia_notonlink(data, &a->addr.a6)) {
dhcpv6_send_ia_notonlink(c, saddr, data,
&client_id_base,
@@ -667,6 +677,7 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
default:
return -1;
}
+
if (ia)
resp.ia_na.iaid = ((struct opt_ia_na *)ia)->iaid;
@@ -681,8 +692,9 @@ int dhcpv6(struct ctx *c, struct iov_tail *data,
resp.hdr.xid = mh->xid;
+ resp.ia_addr.addr = a->addr.a6;
tap_udp6_send(c, src, 547, saddr, 546, mh->xid, &resp, n);
- c->ip6.addr_seen = c->ip6.addr;
+ c->ip6.addr_seen = a->addr.a6;
return 1;
}
@@ -705,6 +717,4 @@ void dhcpv6_init(const struct ctx *c)
c->our_tap_mac, sizeof(c->our_tap_mac));
memcpy(resp_not_on_link.server_id.duid_lladdr,
c->our_tap_mac, sizeof(c->our_tap_mac));
-
- resp.ia_addr.addr = c->ip6.addr;
}
diff --git a/fwd.c b/fwd.c
index 5679a3d6..e3f25438 100644
--- a/fwd.c
+++ b/fwd.c
@@ -243,6 +243,62 @@ void fwd_neigh_table_init(const struct ctx *c)
fwd_neigh_table_update(c, &mga, c->our_tap_mac, true);
}
+/**
+ * fwd_set_addr() - Add or update an address in the unified address array
+ * @c: Execution context
+ * @addr: Address to add (IPv4-mapped or IPv6)
+ * @flags: CONF_ADDR_* flags for this address
+ * @prefix_len: Prefix length in IPv6 format (96-128 for IPv4-mapped)
+ *
+ * Find the first existing entry of the same address family and
+ * overwrite it, or create a new one if none exists
+ */
+void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
+ uint8_t flags, int prefix_len)
+{
+ struct guest_addr *a;
+
+ assert(flags);
+
+ for_each_addr(a, c->addrs, c->addr_count, inany_af(addr))
+ goto found;
+
+ if (c->addr_count >= MAX_GUEST_ADDRS)
+ return;
+
+ a = &c->addrs[c->addr_count++];
+
+found:
+ a->addr = *addr;
+ a->prefix_len = prefix_len;
+ a->flags = flags;
+}
+
+/**
+ * fwd_get_addr() - Get guest address entry matching criteria
+ * @c: Execution context
+ * @af: Address family (AF_INET, AF_INET6, or AF_UNSPEC for any)
+ * @global: If true, match global only; if false, match link-local only
+ * @flags: Required provenance flags (any-match), 0 for any provenance
+ *
+ * Return: first address entry matching criteria, or NULL
+ */
+const struct guest_addr *fwd_get_addr(const struct ctx *c, sa_family_t af,
+ bool global, uint8_t flags)
+{
+ const struct guest_addr *a;
+
+ for_each_addr(a, c->addrs, c->addr_count, af) {
+ if (inany_is_linklocal6(&a->addr) == global)
+ continue;
+ if (!(a->flags & flags))
+ continue;
+ return a;
+ }
+
+ return NULL;
+}
+
/* Forwarding table storage, generally accessed via pointers in struct ctx */
static struct fwd_table fwd_in;
static struct fwd_table fwd_out;
@@ -799,8 +855,11 @@ static bool is_dns_flow(uint8_t proto, const struct flowside *ini)
* translation, false otherwise
*/
static bool fwd_guest_accessible4(const struct ctx *c,
- const struct in_addr *addr)
+ const struct in_addr *addr)
{
+ const struct guest_addr *a;
+ const struct in_addr *a4;
+
if (IN4_IS_ADDR_LOOPBACK(addr))
return false;
@@ -815,7 +874,9 @@ static bool fwd_guest_accessible4(const struct ctx *c,
/* For IPv4, addr_seen is initialised to addr, so is always a valid
* address
*/
- if (IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr) ||
+ a = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ a4 = a ? inany_v4(&a->addr) : NULL;
+ if ((a4 && IN4_ARE_ADDR_EQUAL(addr, a4)) ||
IN4_ARE_ADDR_EQUAL(addr, &c->ip4.addr_seen))
return false;
@@ -833,10 +894,13 @@ static bool fwd_guest_accessible4(const struct ctx *c,
static bool fwd_guest_accessible6(const struct ctx *c,
const struct in6_addr *addr)
{
+ const struct guest_addr *a;
+
if (IN6_IS_ADDR_LOOPBACK(addr))
return false;
- if (IN6_ARE_ADDR_EQUAL(addr, &c->ip6.addr))
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (a && IN6_ARE_ADDR_EQUAL(addr, &a->addr.a6))
return false;
/* For IPv6, addr_seen starts unspecified, because we don't know what LL
@@ -881,16 +945,21 @@ static bool fwd_guest_accessible(const struct ctx *c,
static void nat_outbound(const struct ctx *c, const union inany_addr *addr,
union inany_addr *translated)
{
- if (inany_equals4(addr, &c->ip4.map_host_loopback))
+ const struct guest_addr *ga;
+
+ if (inany_equals4(addr, &c->ip4.map_host_loopback)) {
*translated = inany_loopback4;
- else if (inany_equals6(addr, &c->ip6.map_host_loopback))
+ } else if (inany_equals6(addr, &c->ip6.map_host_loopback)) {
*translated = inany_loopback6;
- else if (inany_equals4(addr, &c->ip4.map_guest_addr))
- *translated = inany_from_v4(c->ip4.addr);
- else if (inany_equals6(addr, &c->ip6.map_guest_addr))
- translated->a6 = c->ip6.addr;
- else
+ } else if (inany_equals4(addr, &c->ip4.map_guest_addr)) {
+ ga = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ *translated = ga ? ga->addr : inany_any4;
+ } else if (inany_equals6(addr, &c->ip6.map_guest_addr)) {
+ ga = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ translated->a6 = ga ? ga->addr.a6 : in6addr_any;
+ } else {
*translated = *addr;
+ }
}
/**
@@ -995,16 +1064,23 @@ bool nat_inbound(const struct ctx *c, const union inany_addr *addr,
} else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback) &&
inany_equals6(addr, &in6addr_loopback)) {
translated->a6 = c->ip6.map_host_loopback;
- } else if (!IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_guest_addr) &&
- inany_equals4(addr, &c->ip4.addr)) {
- *translated = inany_from_v4(c->ip4.map_guest_addr);
- } else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_guest_addr) &&
- inany_equals6(addr, &c->ip6.addr)) {
- translated->a6 = c->ip6.map_guest_addr;
- } else if (fwd_guest_accessible(c, addr)) {
- *translated = *addr;
} else {
- return false;
+ const struct guest_addr *ga4, *ga6;
+
+ ga4 = fwd_get_addr(c, AF_INET, true, CONF_ADDR_ANY);
+ ga6 = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+
+ if (!IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_guest_addr) &&
+ ga4 && inany_equals(addr, &ga4->addr)) {
+ *translated = inany_from_v4(c->ip4.map_guest_addr);
+ } else if (!IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_guest_addr) &&
+ ga6 && inany_equals(addr, &ga6->addr)) {
+ translated->a6 = c->ip6.map_guest_addr;
+ } else if (fwd_guest_accessible(c, addr)) {
+ *translated = *addr;
+ } else {
+ return false;
+ }
}
return true;
diff --git a/fwd.h b/fwd.h
index b60697d9..d001d8ae 100644
--- a/fwd.h
+++ b/fwd.h
@@ -79,5 +79,9 @@ void fwd_neigh_table_free(const struct ctx *c,
void fwd_neigh_mac_get(const struct ctx *c, const union inany_addr *addr,
uint8_t *mac);
void fwd_neigh_table_init(const struct ctx *c);
+void fwd_set_addr(struct ctx *c, const union inany_addr *addr,
+ uint8_t flags, int prefix_len);
+const struct guest_addr *fwd_get_addr(const struct ctx *c, sa_family_t af,
+ bool global, uint8_t flags);
#endif /* FWD_H */
diff --git a/inany.h b/inany.h
index d0befb15..ecd845a5 100644
--- a/inany.h
+++ b/inany.h
@@ -104,6 +104,36 @@ static inline struct in_addr *inany_v4(const union inany_addr *addr)
return (struct in_addr *)&addr->v4mapped.a4;
}
+/** inany_prefix_v4() - Extract IPv4 address and prefix length from inany
+ * @addr: IPv4 (as mapped) or IPv6 address
+ * @prefix_len: Prefix length in IPv6 format (96-128 for IPv4)
+ * @v4_plen: Set to IPv4 prefix length (0-32) on success
+ *
+ * Return: IPv4 address if @addr is IPv4, NULL otherwise
+ */
+static inline struct in_addr *inany_prefix_v4(const union inany_addr *addr,
+ int prefix_len, int *v4_plen)
+{
+ struct in_addr *a4 = inany_v4(addr);
+
+ if (!a4)
+ return NULL;
+
+ *v4_plen = prefix_len - 96;
+
+ return a4;
+}
+
+/** inany_af() - Get address family of IPv[46] address
+ * @addr: IPv4 or IPv6 address
+ *
+ * Return: AF_INET for IPv4, AF_INET6 for IPv6
+ */
+static inline sa_family_t inany_af(const union inany_addr *addr)
+{
+ return inany_v4(addr) ? AF_INET : AF_INET6;
+}
+
/** inany_default_prefix_len() - Get default prefix length for address
* @addr: IPv4 or iPv6 address
*
@@ -112,9 +142,9 @@ static inline struct in_addr *inany_v4(const union inany_addr *addr)
*/
static inline int inany_default_prefix_len(const union inany_addr *addr)
{
- const struct in_addr *v4 = inany_v4(addr);
+ const struct in_addr *a4 = inany_v4(addr);
- return v4 ? ip4_class_prefix_len(v4) + 96 : 64;
+ return a4 ? ip4_class_prefix_len(a4) + 96 : 64;
}
/** inany_equals - Compare two IPv[46] addresses
diff --git a/ip.h b/ip.h
index aab9b86a..ac93c543 100644
--- a/ip.h
+++ b/ip.h
@@ -19,6 +19,8 @@
(ntohl(((struct in_addr *)(a))->s_addr) >> IN_CLASSA_NSHIFT == IN_LOOPBACKNET)
#define IN4_IS_ADDR_MULTICAST(a) \
(IN_MULTICAST(ntohl(((struct in_addr *)(a))->s_addr)))
+#define IN4_MASK(prefix) \
+ ((prefix) <= 0 ? 0 : htonl(0xffffffff << (32 - (prefix))))
#define IN4_ARE_ADDR_EQUAL(a, b) \
(((struct in_addr *)(a))->s_addr == ((struct in_addr *)b)->s_addr)
#define IN4ADDR_LOOPBACK_INIT \
diff --git a/ndp.c b/ndp.c
index 1f2bcb0c..86df4798 100644
--- a/ndp.c
+++ b/ndp.c
@@ -257,7 +257,6 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
.valid_lifetime = ~0U,
.pref_lifetime = ~0U,
},
- .prefix = c->ip6.addr,
.source_ll = {
.header = {
.type = OPT_SRC_L2_ADDR,
@@ -265,8 +264,15 @@ static void ndp_ra(const struct ctx *c, const struct in6_addr *dst)
},
},
};
+ const struct guest_addr *a;
unsigned char *ptr = NULL;
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (!a)
+ return;
+
+ ra.prefix = a->addr.a6;
+
ptr = &ra.var[0];
if (c->mtu) {
@@ -460,6 +466,7 @@ first:
*/
void ndp_send_init_req(const struct ctx *c)
{
+ const struct guest_addr *a;
struct ndp_ns ns = {
.ih = {
.icmp6_type = NS,
@@ -468,8 +475,14 @@ void ndp_send_init_req(const struct ctx *c)
.icmp6_solicited = 0, /* Reserved */
.icmp6_override = 0, /* Reserved */
},
- .target_addr = c->ip6.addr
+ .target_addr = IN6ADDR_ANY_INIT
};
+
+ a = fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY);
+ if (!a)
+ return;
+
+ ns.target_addr = a->addr.a6;
debug("Sending initial NDP NS request for guest MAC address");
- ndp_send(c, &c->ip6.addr, &ns, sizeof(ns));
+ ndp_send(c, &a->addr.a6, &ns, sizeof(ns));
}
diff --git a/passt.h b/passt.h
index afd8e9f0..5f68ab91 100644
--- a/passt.h
+++ b/passt.h
@@ -64,11 +64,30 @@ enum passt_modes {
MODE_VU,
};
+/* Maximum number of addresses in context address array */
+#define MAX_GUEST_ADDRS 32
+
+/**
+ * struct guest_addr - Unified IPv4/IPv6 address entry
+ * @addr: IPv4 (as mapped) or IPv6 address
+ * @prefix_len: Prefix length in IPv6/IPv4-mapped [0,128]/[96,128] format
+ * @flags: CONF_ADDR_* flags
+ */
+struct guest_addr {
+ union inany_addr addr;
+ uint8_t prefix_len;
+ uint8_t flags;
+#define CONF_ADDR_USER BIT(0) /* User set via -a */
+#define CONF_ADDR_HOST BIT(1) /* From host */
+#define CONF_ADDR_GEN BIT(2) /* Generated by code */
+#define CONF_ADDR_ANY (CONF_ADDR_USER | \
+ CONF_ADDR_HOST | \
+ CONF_ADDR_GEN) /* Any provenance */
+};
+
/**
* struct ip4_ctx - IPv4 execution context
- * @addr: IPv4 address assigned to guest
* @addr_seen: Latest IPv4 address seen as source from tap
- * @prefixlen: IPv4 prefix length (netmask)
* @guest_gw: IPv4 gateway as seen by the guest
* @map_host_loopback: Outbound connections to this address are NATted to the
* host's 127.0.0.1
@@ -87,9 +106,7 @@ enum passt_modes {
*/
struct ip4_ctx {
/* PIF_TAP addresses */
- struct in_addr addr;
struct in_addr addr_seen;
- int prefix_len;
struct in_addr guest_gw;
struct in_addr map_host_loopback;
struct in_addr map_guest_addr;
@@ -110,7 +127,6 @@ struct ip4_ctx {
/**
* struct ip6_ctx - IPv6 execution context
- * @addr: IPv6 address assigned to guest
* @addr_seen: Latest IPv6 global/site address seen as source from tap
* @addr_ll_seen: Latest IPv6 link-local address seen as source from tap
* @guest_gw: IPv6 gateway as seen by the guest
@@ -132,7 +148,6 @@ struct ip4_ctx {
*/
struct ip6_ctx {
/* PIF_TAP addresses */
- struct in6_addr addr;
struct in6_addr addr_seen;
struct in6_addr addr_ll_seen;
struct in6_addr guest_gw;
@@ -192,6 +207,8 @@ struct ip6_ctx {
* @fqdn: Guest FQDN
* @ifi6: Template interface for IPv6, -1: none, 0: IPv6 disabled
* @ip6: IPv6 configuration
+ * @addrs: Unified address array for both IPv4 (mapped) and IPv6
+ * @addr_count: Number of active entries in @addrs array
* @pasta_ifn: Name of namespace interface for pasta
* @pasta_ifi: Index of namespace interface for pasta
* @pasta_conf_ns: Configure namespace after creating it
@@ -276,6 +293,9 @@ struct ctx {
int ifi6;
struct ip6_ctx ip6;
+ struct guest_addr addrs[MAX_GUEST_ADDRS];
+ int addr_count;
+
char pasta_ifn[IF_NAMESIZE];
unsigned int pasta_ifi;
int pasta_conf_ns;
@@ -321,6 +341,37 @@ struct ctx {
extern struct ctx passt_ctx;
+/**
+ * next_addr_idx_() - Find next address index matching family filter
+ * @addrs: Array of guest addresses
+ * @count: Number of addresses in array
+ * @i: Starting index
+ * @af: Address family filter: AF_INET, AF_INET6, or AF_UNSPEC for all
+ *
+ * Return: next matching index, or count if none found
+ */
+static inline int next_addr_idx_(const struct guest_addr *addrs, int count,
+ int i, sa_family_t af)
+{
+ for (; i < count; i++) {
+ if (af == AF_UNSPEC || af == inany_af(&addrs[i].addr))
+ return i;
+ }
+ return i;
+}
+
+/**
+ * for_each_addr() - Iterate over addresses in array
+ * @a: Pointer variable for current entry (struct guest_addr *)
+ * @addrs: Array of guest addresses (e.g., c->addrs)
+ * @count: Number of addresses (e.g., c->addr_count)
+ * @af: Address family filter: AF_INET, AF_INET6, or 0 for all
+ */
+#define for_each_addr(a, addrs, count, af) \
+ for (int i_ = next_addr_idx_((addrs), (count), 0, (af)); \
+ i_ < (count) && ((a) = &(addrs)[i_], true); \
+ i_ = next_addr_idx_((addrs), (count), i_ + 1, (af)))
+
void proto_update_l2_buf(const unsigned char *eth_d);
#endif /* PASST_H */
diff --git a/pasta.c b/pasta.c
index 10f3ec8d..1410a8c3 100644
--- a/pasta.c
+++ b/pasta.c
@@ -299,23 +299,32 @@ void pasta_start_ns(struct ctx *c, int argc, char *argv[])
*
* Return: 0 on success, negative error code on failure
*/
-static int pasta_conf_addrs(struct ctx *c, sa_family_t af, int ifi,
+static int pasta_conf_addrs(const struct ctx *c, sa_family_t af, int ifi,
bool no_copy)
{
+ const struct guest_addr *a;
+
if (!ifi)
return 0;
if (!no_copy)
return nl_addr_dup(nl_sock, ifi, nl_sock_ns, c->pasta_ifi, af);
- if (af == AF_INET && !IN4_IS_ADDR_UNSPECIFIED(&c->ip4.addr))
+ a = fwd_get_addr(c, af, true, CONF_ADDR_ANY);
+ if (!a)
+ return 0;
+
+ if (af == AF_INET) {
+ const struct in_addr *v4;
+ int plen = 0;
+
+ v4 = inany_prefix_v4(&a->addr, a->prefix_len, &plen);
return nl_addr_set(nl_sock_ns, c->pasta_ifi, AF_INET,
- &c->ip4.addr, c->ip4.prefix_len);
- if (af == AF_INET6 && !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
- return nl_addr_set(nl_sock_ns, c->pasta_ifi, AF_INET6,
- &c->ip6.addr, 64);
+ v4, plen);
+ }
- return 0;
+ return nl_addr_set(nl_sock_ns, c->pasta_ifi, AF_INET6,
+ &a->addr.a6, a->prefix_len);
}
/**
diff --git a/tap.c b/tap.c
index dfa66c71..dd055a4e 100644
--- a/tap.c
+++ b/tap.c
@@ -1005,8 +1005,12 @@ resume:
c->ip6.addr_seen = *saddr;
}
- if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr))
- c->ip6.addr = *saddr;
+ if (!fwd_get_addr(c, AF_INET6, true, CONF_ADDR_ANY) &&
+ !fwd_get_addr(c, AF_INET6, false, CONF_ADDR_ANY)) {
+ union inany_addr addr = { .a6 = *saddr };
+
+ fwd_set_addr(c, &addr, CONF_ADDR_GEN, 64);
+ }
} else if (!c->ip6.addr_fixed &&
!IN6_IS_ADDR_UNSPECIFIED(saddr)) {
c->ip6.addr_seen = *saddr;
--
2.52.0
1
0
tcp: peer stuck in low_rtt_dst table loses window clamping, every new connection stalls on RTOs
by Lynxes Exe 04 Nov '26
by Lynxes Exe 04 Nov '26
04 Nov '26
Hi,
A disclaimer before anything is owed: what you're about to read was
written by Claude, and is the result of a bug fixing session that occurred
on my personal home infrastructure. It was not intended as a bug hunt.
Since it seems like this issue has been unreported and I couldn't find
anything similar to it, and since what Claude reported seems potentially
plausible, I'm putting it here.
If AI assisted/generated bug reports are unwanted, I apologize.
Original report
--------------------------
I think a peer address can get stuck in pasta's low-RTT destination
table and then lose window clamping for good. That caused every new
TCP connection from our LAN router's address to stall on RTOs until
pasta was restarted. Details below. I haven't found an existing
report on Bugzilla or this list.
Version: pasta 2026_10_02.cba3570-3-g8ba4132 (the logic below is
unchanged in current master)
Setup: rootless Docker 29.6.1 via rootlesskit, --net=pasta,
--port-driver=implicit, --mtu=65520 (pasta --tcp-ports=auto
--udp-ports=auto --no-map-gw --ipv4-only ...)
Kernel: 6.18.57 (Arch linux-lts), x86_64
What we think is happening
--------------------------
We suspect the low-RTT destination table (low_rtt_dst[],
tcp_rtt_dst_check() / tcp_rtt_dst_low()). Our LAN router's address,
10.0.0.1, ended up in it even though the router is several
milliseconds away.
For a peer in that table, tcp_update_seqack_wnd() advertises the host
socket's tcpi_snd_wnd to the guest instead of tcp_wnd_from_sndbuf().
The guest then sends more than the host socket can queue, pasta
discards the excess, and the guest only recovers through RTOs.
Entries are never removed while pasta runs, so one bad insertion
degraded every connection from that address for days, until pasta
was restarted.
Network layout (all private addresses)
--------------------------------------
- LAN 10.0.0.0/8. Host: 10.0.0.181 (DHCP) and 10.0.0.80/32.
Default gateway: the router, 10.0.0.1, 2-6 ms away over Wi-Fi.
- An nginx container's port is published by pasta on
10.0.0.80:4444.
- The router forwards WAN 443 to 10.0.0.80:4444 with NAT
reflection, so LAN clients using the public name reach the host
with source address 10.0.0.1. To pasta, every LAN client is the
same peer, 10.0.0.1.
- A desktop at 10.0.0.187 can also connect straight to
10.0.0.80:4444 (no NAT, its own address).
- A monitoring container opens an HTTPS connection to
https://10.0.0.1/ every minute (outbound through pasta), so pasta
also sees regular outbound flows to 10.0.0.1.
Symptom
-------
The first large response on every new connection from 10.0.0.1
stalled for 1.5-3.2 s (3-4 RTOs with exponential backoff). Later
responses on the same connection were fine. Connections from
10.0.0.187 to the same port and container never stalled.
Test: one connection fetching a 520 KB static file 25 times,
counting TCPTimeouts in the container's network namespace.
- peer 10.0.0.1, LAN client through NAT reflection:
+4 timeouts and one ~3.1 s fetch, every run
- peer 10.0.0.1, the router itself, direct, no NAT:
11 of 13 runs stalled the same way
- peer 10.0.0.187, desktop, direct:
0 timeouts in every run
So the trigger is the peer address, not the NAT path.
ss -ti on both legs during one transfer:
peer 10.0.0.1 (stalls) 10.0.0.187 (fine)
host socket (pasta)
retransmits 0 0
tcpi_snd_wnd 252928 146432
container socket
snd_wnd 251904 1280
rwnd_limited 7 % 80 %
retransmits 186 (backoff 4) 1
transfer time 3.2 s 0.26 s
For 10.0.0.1 the window pasta advertises to the container is the
host socket's tcpi_snd_wnd, shifted by the window scale
(252928 -> 251904). That is the table branch in
tcp_update_seqack_wnd(). For 10.0.0.187 it is clamped to buffer
space.
The host-side socket never retransmits, and the tap device's drop
counter does not move. The segments the container retransmits are
ones pasta took off the tap and discarded.
After restarting pasta (by restarting the rootless Docker daemon),
the same tests from 10.0.0.1 gave: snd_wnd 1280, rwnd_limited 73 %,
0 retransmissions, 0 timeouts, about 0.06 s per fetch. Nothing else
changed.
We could not read the table directly (no ptrace into pasta), so
"10.0.0.1 is in the table" is inferred from this behaviour and the
code path.
Why we think 10.0.0.1 got into the table
----------------------------------------
A real RTT of 10 us or less to the router is impossible here
(measured RTT 2-6 ms). Our suspicion comes from reading the kernel
source; we have not caught it on a live socket:
- tcp_rtt_dst_check() tests
(int)tinfo->tcpi_min_rtt > LOW_RTT_THRESHOLD.
- The kernel initialises rtt_min to ~0U, both in tcp_init_sock()
(net/ipv4/tcp.c) and for accepted sockets in
tcp_create_openreq_child() (tcp_minisocks.c). tcpi_min_rtt stays
~0U until the first valid RTT sample.
- tcp_synack_rtt_meas() takes no sample when the SYN-ACK was
retransmitted (rtt_us = -1 unless !req->num_retrans). The same
applies on the active side when the SYN was retransmitted (Karn).
- Cast to int, ~0U becomes -1, which passes the "<= 10 us" test.
- pasta runs this check in tcp_prepare_flags() when it sends the SYN
(inbound) or SYN-ACK (outbound) to the guest. That is right after
the host-side handshake, before any data ACK could provide a
sample.
We measured exactly such handshake retransmissions on this
Wi-Fi/NAT path: 6 of 1,800 fresh connections took ~1.05 s, i.e. one
1 s retry. With hundreds of inbound and outbound connections to
10.0.0.1 per day, one retried handshake would be enough to insert it
permanently.
The night the stalls became severe, the published container was also
restarted about ten times while clients kept connecting, which made
unusual handshakes more likely. We cannot tell exactly when the
insertion happened.
Suggested fixes
---------------
1. In tcp_rtt_dst_check(), treat tcpi_min_rtt == ~0U (and 0) as
"no sample" and skip the insertion. Compare as unsigned, or check
tcpi_rtt / tcpi_rcv_rtt as well.
2. Don't trust a single sample forever: give entries a lifetime, or
evict an address when a later connection to it shows a min RTT
above the threshold.
3. Log (at debug level) when an address enters the table. It was
invisible here; the only visible signs were retransmissions
inside the namespace.
Workaround: restarting pasta clears the table.
Happy to test a patch or collect more data.
Thanks,
Lynxes
3
2