[PATCH] conf, pasta: Add --no-tap option
This patch introduces a mode where we only forward loopback connections
and traffic between two namespaces (via the loopback interface, 'lo'),
without a tap device.
With this, podman can support forwarding ::1 in custom networks when using
rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically
enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr,
--config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149
Signed-off-by: Yumei Huang
Cc'ing Paul with full quote for review, as he's the one who will
probably take care of using this from Podman.
By the way, I didn't finish reviewing this yet, but, so far, it looks
simpler than I thought!
I still need to find a moment to use my little hammer on it, and find
out if it's perhaps a bit too simple. :)
On Mon, 29 Dec 2025 17:55:58 +0800
Yumei Huang
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice"); + if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr"); + if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net"); + + c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1; + } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) { strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw && IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw && IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1; } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice. + .SH EXAMPLES
.SS \fBpasta diff --git a/passt.h b/passt.h index 79d01dd..0c1ec4c 100644 --- a/passt.h +++ b/passt.h @@ -200,6 +200,7 @@ struct ip6_ctx { * @no_ndp: Disable NDP handler altogether * @no_ra: Disable router advertisements * @no_splice: Disable socket splicing for inbound traffic + * @no_tap: Do not create tap device * @host_lo_to_ns_lo: Map host loopback addresses to ns loopback addresses * @freebind: Allow binding of non-local addresses for forwarding * @low_wmem: Low probed net.core.wmem_max @@ -277,6 +278,7 @@ struct ctx { int no_ndp; int no_ra; int no_splice; + int no_tap; int host_lo_to_ns_lo; int freebind;
diff --git a/pasta.c b/pasta.c index 0ddd6b0..3510ec5 100644 --- a/pasta.c +++ b/pasta.c @@ -316,6 +316,9 @@ void pasta_ns_conf(struct ctx *c) die("Couldn't bring up loopback interface in namespace: %s", strerror_(-rc));
+ if (c->no_tap) + return; + /* Get or set MAC in target namespace */ if (MAC_IS_ZERO(c->guest_mac)) nl_link_get_mac(nl_sock_ns, c->pasta_ifi, c->guest_mac); diff --git a/tap.c b/tap.c index 9d1344b..9b4eedc 100644 --- a/tap.c +++ b/tap.c @@ -1491,13 +1491,16 @@ static int tap_ns_tun(void *arg) */ static void tap_sock_tun_init(struct ctx *c) { - NS_CALL(tap_ns_tun, c); - if (c->fd_tap == -1) - die("Failed to set up tap device in namespace"); + if (!c->no_tap) { + NS_CALL(tap_ns_tun, c); + if (c->fd_tap == -1) + die("Failed to set up tap device in namespace"); + }
pasta_ns_conf(c);
- tap_start_connection(c); + if (!c->no_tap) + tap_start_connection(c); }
/**
-- Stefano
On Mon, Dec 29, 2025 at 05:55:58PM +0800, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
Nice work. There are some things that need polish, but overall this looks pretty good to me. Like Stefano, I'm pleasantly surprised at how simple it turned out to be.
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-)
diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
I feel like this description can be improved, but I'm not exactly sure how, yet.
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice"); + if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr");
These all make sense. It might also make sense to exclude the -i option - setting a template interface also makes no sense in --no-tap mode.
+ if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I don't think this is right. We still can and should bring up 'lo' in the --no-tap case.
+ c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1;
The reasoning for the last two items is a bit unclear to me. IIUC, no_dns and no_dns_search aren't so much about "support" for DNS itself but for advertising DNS settings via DHCP. Since DHCP will be unsupported, so are these as a consequence. Is that right?
+ } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) { strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw &&
This isn't quite right. ifi4 == -1 now occurs in two cases: local mode, and --no-tap mode. Not setting map_host_loopback makes sense for --no-tap mode, but it's still needed for local mode.
IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw &&
Same here.
IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1;
And here. Local mode can still use NDP and DHCP, even though --no-tap mode can't. It might be simpler to force no_ndp, no_dhcp etc. along with no_ra and the rest above.
} else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice.
This probably wants some work, because I'm not sure "tap device" and "splice" are sufficiently clear in this context.
+ .SH EXAMPLES
.SS \fBpasta diff --git a/passt.h b/passt.h index 79d01dd..0c1ec4c 100644 --- a/passt.h +++ b/passt.h @@ -200,6 +200,7 @@ struct ip6_ctx { * @no_ndp: Disable NDP handler altogether * @no_ra: Disable router advertisements * @no_splice: Disable socket splicing for inbound traffic + * @no_tap: Do not create tap device * @host_lo_to_ns_lo: Map host loopback addresses to ns loopback addresses * @freebind: Allow binding of non-local addresses for forwarding * @low_wmem: Low probed net.core.wmem_max @@ -277,6 +278,7 @@ struct ctx { int no_ndp; int no_ra; int no_splice; + int no_tap; int host_lo_to_ns_lo; int freebind;
diff --git a/pasta.c b/pasta.c index 0ddd6b0..3510ec5 100644 --- a/pasta.c +++ b/pasta.c @@ -316,6 +316,9 @@ void pasta_ns_conf(struct ctx *c) die("Couldn't bring up loopback interface in namespace: %s", strerror_(-rc));
+ if (c->no_tap) + return; + /* Get or set MAC in target namespace */ if (MAC_IS_ZERO(c->guest_mac)) nl_link_get_mac(nl_sock_ns, c->pasta_ifi, c->guest_mac); diff --git a/tap.c b/tap.c index 9d1344b..9b4eedc 100644 --- a/tap.c +++ b/tap.c @@ -1491,13 +1491,16 @@ static int tap_ns_tun(void *arg) */ static void tap_sock_tun_init(struct ctx *c) { - NS_CALL(tap_ns_tun, c); - if (c->fd_tap == -1) - die("Failed to set up tap device in namespace"); + if (!c->no_tap) { + NS_CALL(tap_ns_tun, c); + if (c->fd_tap == -1) + die("Failed to set up tap device in namespace"); + }
pasta_ns_conf(c);
- tap_start_connection(c); + if (!c->no_tap) + tap_start_connection(c); }
/** -- 2.49.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Mon, Jan 5, 2026 at 12:18 PM David Gibson
On Mon, Dec 29, 2025 at 05:55:58PM +0800, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
Nice work. There are some things that need polish, but overall this looks pretty good to me. Like Stefano, I'm pleasantly surprised at how simple it turned out to be.
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-)
diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
I feel like this description can be improved, but I'm not exactly sure how, yet.
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice"); + if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr");
These all make sense. It might also make sense to exclude the -i option - setting a template interface also makes no sense in --no-tap mode.
Sure, I can add an if condition with if4 (as if4=if6 in that case).
+ if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I don't think this is right. We still can and should bring up 'lo' in the --no-tap case.
I see your point, but seems c->pasta_conf_ns is only used for tap as https://passt.top/passt/tree/pasta.c#n328, 'lo' is configured before that line.
+ c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1;
The reasoning for the last two items is a bit unclear to me. IIUC, no_dns and no_dns_search aren't so much about "support" for DNS itself but for advertising DNS settings via DHCP. Since DHCP will be unsupported, so are these as a consequence. Is that right?
Yeah, I think so. Actually I added c->no_dhcp, c->no_ndp here as well, then removed them as they are set in later changes(conditions about c->ifi4/c->ifi6), though they turn out to be not quite right :'\
+ } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) { strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw &&
This isn't quite right. ifi4 == -1 now occurs in two cases: local mode, and --no-tap mode. Not setting map_host_loopback makes sense for --no-tap mode, but it's still needed for local mode.
I'm a bit confused by map_host_loopback. I don't quite understand the use scenario. IIUC, either in --no-tap mode or local mode, guest can only communicate with host. Then why do we need to set map_host_loopback? What's the benefit?
IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw &&
Same here.
IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1;
And here. Local mode can still use NDP and DHCP, even though --no-tap mode can't. It might be simpler to force no_ndp, no_dhcp etc. along with no_ra and the rest above.
Sure, I will add them.
} else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice.
This probably wants some work, because I'm not sure "tap device" and "splice" are sufficiently clear in this context.
Yeah, I will think about that. Thanks.
+ .SH EXAMPLES
.SS \fBpasta diff --git a/passt.h b/passt.h index 79d01dd..0c1ec4c 100644 --- a/passt.h +++ b/passt.h @@ -200,6 +200,7 @@ struct ip6_ctx { * @no_ndp: Disable NDP handler altogether * @no_ra: Disable router advertisements * @no_splice: Disable socket splicing for inbound traffic + * @no_tap: Do not create tap device * @host_lo_to_ns_lo: Map host loopback addresses to ns loopback addresses * @freebind: Allow binding of non-local addresses for forwarding * @low_wmem: Low probed net.core.wmem_max @@ -277,6 +278,7 @@ struct ctx { int no_ndp; int no_ra; int no_splice; + int no_tap; int host_lo_to_ns_lo; int freebind;
diff --git a/pasta.c b/pasta.c index 0ddd6b0..3510ec5 100644 --- a/pasta.c +++ b/pasta.c @@ -316,6 +316,9 @@ void pasta_ns_conf(struct ctx *c) die("Couldn't bring up loopback interface in namespace: %s", strerror_(-rc));
+ if (c->no_tap) + return; + /* Get or set MAC in target namespace */ if (MAC_IS_ZERO(c->guest_mac)) nl_link_get_mac(nl_sock_ns, c->pasta_ifi, c->guest_mac); diff --git a/tap.c b/tap.c index 9d1344b..9b4eedc 100644 --- a/tap.c +++ b/tap.c @@ -1491,13 +1491,16 @@ static int tap_ns_tun(void *arg) */ static void tap_sock_tun_init(struct ctx *c) { - NS_CALL(tap_ns_tun, c); - if (c->fd_tap == -1) - die("Failed to set up tap device in namespace"); + if (!c->no_tap) { + NS_CALL(tap_ns_tun, c); + if (c->fd_tap == -1) + die("Failed to set up tap device in namespace"); + }
pasta_ns_conf(c);
- tap_start_connection(c); + if (!c->no_tap) + tap_start_connection(c); }
/** -- 2.49.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
-- Thanks, Yumei Huang
Sorry I was out for a while so I didn't had time to clarify on the bug before. On 29/12/2025 10:55, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
I guess I didn't really communicate my requirements well. When we use rootlessport (rootlesskit) today for custom networks we only do so as rootless user and it forwards ::1 (by possibly mapping this to v4 inside the container) fine. My main point for this feature was using as root (requires further changes to allow pasta running as root). Because as root podman does port forwarding via DNAT firewall rules (i.e. custom nftables rules we add). The kernel however never added support for DNAT on ::1 meaning clients trying to access that are not getting forwarded. The only way to support this is using a user space helper. Right now this doesn't work and we do not use rootlessport for this either so I was just thinking ahead because we do have these users requests who want ::1 to work as root. For the current rootlessport use case we also must bind all ports as given (i.e. also addresses 0.0.0.0 bind address), just forwarding loopback to loopback is not what we want or do for security reasons, see CVE-2021-20199. And logically it would not really work to have another process bind 0.0.0.0 and this pasta helper bind lo on the same port at the same time. The way I am thinking is bind ports as normal, add the no-tap option and add two options to give the v4 and v6 namespace (container) side connect addresses so we never actually connect to lo. Then we also should have a dynamic way to update the connect addresses at runtime which is required for podman network connect/disconnect to work which changes the addresses inside the namespace, see https://github.com/containers/podman/commit/e88d8dbeae2aebd2d816f16a21891764.... Overall none of this is a blocker for removing rootlessport. I think our plan was and still is to use the dynamic port forwarding logic David is working on to replace the rootless custom network port forwarding case with that.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice"); + if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr"); + if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net"); + + c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1; + } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) { strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw && IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw && IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1; } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice. + .SH EXAMPLES
.SS \fBpasta diff --git a/passt.h b/passt.h index 79d01dd..0c1ec4c 100644 --- a/passt.h +++ b/passt.h @@ -200,6 +200,7 @@ struct ip6_ctx { * @no_ndp: Disable NDP handler altogether * @no_ra: Disable router advertisements * @no_splice: Disable socket splicing for inbound traffic + * @no_tap: Do not create tap device * @host_lo_to_ns_lo: Map host loopback addresses to ns loopback addresses * @freebind: Allow binding of non-local addresses for forwarding * @low_wmem: Low probed net.core.wmem_max @@ -277,6 +278,7 @@ struct ctx { int no_ndp; int no_ra; int no_splice; + int no_tap; int host_lo_to_ns_lo; int freebind;
diff --git a/pasta.c b/pasta.c index 0ddd6b0..3510ec5 100644 --- a/pasta.c +++ b/pasta.c @@ -316,6 +316,9 @@ void pasta_ns_conf(struct ctx *c) die("Couldn't bring up loopback interface in namespace: %s", strerror_(-rc));
+ if (c->no_tap) + return; + /* Get or set MAC in target namespace */ if (MAC_IS_ZERO(c->guest_mac)) nl_link_get_mac(nl_sock_ns, c->pasta_ifi, c->guest_mac); diff --git a/tap.c b/tap.c index 9d1344b..9b4eedc 100644 --- a/tap.c +++ b/tap.c @@ -1491,13 +1491,16 @@ static int tap_ns_tun(void *arg) */ static void tap_sock_tun_init(struct ctx *c) { - NS_CALL(tap_ns_tun, c); - if (c->fd_tap == -1) - die("Failed to set up tap device in namespace"); + if (!c->no_tap) { + NS_CALL(tap_ns_tun, c); + if (c->fd_tap == -1) + die("Failed to set up tap device in namespace"); + }
pasta_ns_conf(c);
- tap_start_connection(c); + if (!c->no_tap) + tap_start_connection(c); }
/**
-- Paul Holzinger
On Mon, 5 Jan 2026 14:48:15 +0100
Paul Holzinger
Sorry I was out for a while so I didn't had time to clarify on the bug before.
On 29/12/2025 10:55, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
I guess I didn't really communicate my requirements well.
I guess it's more likely that you actually did, but I mixed up the association between requirements and use cases, sorry for that. In any case, good that we need this anyway, just for another use case. :)
When we use rootlessport (rootlesskit) today for custom networks we only do so as rootless user and it forwards ::1 (by possibly mapping this to v4 inside the container) fine.
So, wait a moment, is my comment at: https://github.com/containers/podman/issues/14491#issuecomment-2898191772 actually wrong? I don't have time right now to test that but from user reports and some vague memory I thought ::1 forwarding wouldn't work with custom networks regardless of root or rootless, because rootlesskit didn't handle that anyway.
My main point for this feature was using as root (requires further changes to allow pasta running as root).
...which should be entirely on Podman side and it's still on my plate, by the way: https://github.com/containers/podman/issues/17840 https://pad.passt.top/p/Features_2025#L40
Because as root podman does port forwarding via DNAT firewall rules (i.e. custom nftables rules we add). The kernel however never added support for DNAT on ::1 meaning clients trying to access that are not getting forwarded. The only way to support this is using a user space helper. Right now this doesn't work and we do not use rootlessport for this either so I was just thinking ahead because we do have these users requests who want ::1 to work as root.
For the current rootlessport use case we also must bind all ports as given (i.e. also addresses 0.0.0.0 bind address), just forwarding loopback to loopback is not what we want or do for security reasons, see CVE-2021-20199. And logically it would not really work to have another process bind 0.0.0.0 and this pasta helper bind lo on the same port at the same time.
The way I am thinking is bind ports as normal, add the no-tap option and add two options to give the v4 and v6 namespace (container) side connect addresses so we never actually connect to lo. Then we also should have a dynamic way to update the connect addresses at runtime which is required for podman network connect/disconnect to work which changes the addresses inside the namespace, see https://github.com/containers/podman/commit/e88d8dbeae2aebd2d816f16a21891764....
Overall none of this is a blocker for removing rootlessport. I think our plan was and still is to use the dynamic port forwarding logic David is working on to replace the rootless custom network port forwarding case with that.
Regardless of other requirements that are needed as well to support forwarding ::1 for root containers (or rootless with --userns=auto), this feature by itself makes sense as it is and we'll need it as it is, right? By the way we routinely get requests for this feature by pasta (and Podman) users, regardless of any specific Podman integration, so I think the feature is generic enough as to make sense regardless of your plan for root containers. -- Stefano
On Mon, 5 Jan 2026 14:48:15 +0100 Paul Holzinger
wrote: Sorry I was out for a while so I didn't had time to clarify on the bug before.
On 29/12/2025 10:55, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports. I guess I didn't really communicate my requirements well. I guess it's more likely that you actually did, but I mixed up the association between requirements and use cases, sorry for that.
In any case, good that we need this anyway, just for another use case. :)
When we use rootlessport (rootlesskit) today for custom networks we only do so as rootless user and it forwards ::1 (by possibly mapping this to v4 inside the container) fine. So, wait a moment, is my comment at:
https://github.com/containers/podman/issues/14491#issuecomment-2898191772
actually wrong? I don't have time right now to test that but from user reports and some vague memory I thought ::1 forwarding wouldn't work with custom networks regardless of root or rootless, because rootlesskit didn't handle that anyway. yes, rootlesskit handles ipv6 just fine, it is just that our rootlessport code remaps that to v4 inside the container.
My main point for this feature was using as root (requires further changes to allow pasta running as root). ...which should be entirely on Podman side and it's still on my plate, by the way:
https://github.com/containers/podman/issues/17840 https://pad.passt.top/p/Features_2025#L40 I don't see how this can be fixed on the podman side, the network namespace of a rootful container (not userns=auto) is owned by the root user. If you configure something in there you must have real CAP_NET_ADMIN from the host init userns. So pasta must not drop this
On 05/01/2026 22:10, Stefano Brivio wrote: privilege before configuring the netns. And even then with the future netlink monitor work we would need to keep that privilege level to modify the netns even during runtime?
Because as root podman does port forwarding via DNAT firewall rules (i.e. custom nftables rules we add). The kernel however never added support for DNAT on ::1 meaning clients trying to access that are not getting forwarded. The only way to support this is using a user space helper. Right now this doesn't work and we do not use rootlessport for this either so I was just thinking ahead because we do have these users requests who want ::1 to work as root.
For the current rootlessport use case we also must bind all ports as given (i.e. also addresses 0.0.0.0 bind address), just forwarding loopback to loopback is not what we want or do for security reasons, see CVE-2021-20199. And logically it would not really work to have another process bind 0.0.0.0 and this pasta helper bind lo on the same port at the same time.
The way I am thinking is bind ports as normal, add the no-tap option and add two options to give the v4 and v6 namespace (container) side connect addresses so we never actually connect to lo. Then we also should have a dynamic way to update the connect addresses at runtime which is required for podman network connect/disconnect to work which changes the addresses inside the namespace, see https://github.com/containers/podman/commit/e88d8dbeae2aebd2d816f16a21891764....
Overall none of this is a blocker for removing rootlessport. I think our plan was and still is to use the dynamic port forwarding logic David is working on to replace the rootless custom network port forwarding case with that. Regardless of other requirements that are needed as well to support forwarding ::1 for root containers (or rootless with --userns=auto), this feature by itself makes sense as it is and we'll need it as it is, right?
By the way we routinely get requests for this feature by pasta (and Podman) users, regardless of any specific Podman integration, so I think the feature is generic enough as to make sense regardless of your plan for root containers.
I am not sure how I would use or integrate a loopback to loopback forwarder in podman so I don't think we would need or can use that as is. I think the use case itself is still interesting and if there are end users asking for it sure not objections from me. I guess it could be interesting to expose a service without giving it access to the full internet and without having to deal with complicated firewall rules, i.e. with this we get a container that only could communicate by replying to the forwarded ports. -- Paul Holzinger
[Cc'ing Jon for awareness around the part about netlink monitor and
capabilities, four paragraphs down]
On Wed, 7 Jan 2026 16:20:18 +0100
Paul Holzinger
On 05/01/2026 22:10, Stefano Brivio wrote:
On Mon, 5 Jan 2026 14:48:15 +0100 Paul Holzinger
wrote: Sorry I was out for a while so I didn't had time to clarify on the bug before.
On 29/12/2025 10:55, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports. I guess I didn't really communicate my requirements well. I guess it's more likely that you actually did, but I mixed up the association between requirements and use cases, sorry for that.
In any case, good that we need this anyway, just for another use case. :)
When we use rootlessport (rootlesskit) today for custom networks we only do so as rootless user and it forwards ::1 (by possibly mapping this to v4 inside the container) fine. So, wait a moment, is my comment at:
https://github.com/containers/podman/issues/14491#issuecomment-2898191772
actually wrong? I don't have time right now to test that but from user reports and some vague memory I thought ::1 forwarding wouldn't work with custom networks regardless of root or rootless, because rootlesskit didn't handle that anyway.
yes, rootlesskit handles ipv6 just fine, it is just that our rootlessport code remaps that to v4 inside the container.
Actually, at a glance, I don't think that this could be fixed entirely in the rootlessport implementation, as rootlesskit doesn't seem to look at the destination address of the original connection at all.
My main point for this feature was using as root (requires further changes to allow pasta running as root). ...which should be entirely on Podman side and it's still on my plate, by the way:
https://github.com/containers/podman/issues/17840 https://pad.passt.top/p/Features_2025#L40
I don't see how this can be fixed on the podman side, the network namespace of a rootful container (not userns=auto) is owned by the root user. If you configure something in there you must have real CAP_NET_ADMIN from the host init userns. So pasta must not drop this privilege before configuring the netns.
Oops, right. My starting point was this change, which is actually trivial (at least as a test) and something I already tried out, but then I hit a number of issues in Podman I never really figured out. So yes, it takes one change in pasta, but the substantial part left for me to figure out is why Podman didn't just work with it. It's not necessarily complicated, I spent just a couple of hours on it, so maybe there's something simple I missed.
And even then with the future netlink monitor work we would need to keep that privilege level to modify the netns even during runtime?
This just reminded me that, somewhat surprisingly, for netlink operations, the check on capabilities is not just performed on the process creating the socket when the socket is created, but also later *on the sender of the message*. This is inconsistent with other operations on other types of sockets where the whole context is checked and assigned at the time of the creation, and was introduced because of a specific behaviour of Zebra (the routing daemon) in 2014, see discussion around: https://lore.kernel.org/all/87d2g7d9ag.fsf_-_@x220.int.ebiederm.org/#r and I stumbled upon it a while ago while preparing a seitan demo replaying nft messages for an unprivileged container: https://seitan.rocks/seitan/tree/demo/nft.hjson#n38 So, my blanket answer "we create that socket at the beginning" doesn't apply here. However, assuming that this RFC patch from Jon actually works (I haven't tested it): https://archives.passt.top/passt-dev/20251215015441.887736-11-jmaloy@redhat.... I would say we're fine with it. Well, there's still the possibility that it doesn't work if Podman originally detached the network namespace, I'm not sure. If it doesn't work, we'll need to retain more capabilities, or even keep a cloned process around for this kind of stuff. We could also fix that in the kernel, Zebra doesn't need that quirk anymore.
Because as root podman does port forwarding via DNAT firewall rules (i.e. custom nftables rules we add). The kernel however never added support for DNAT on ::1 meaning clients trying to access that are not getting forwarded. The only way to support this is using a user space helper. Right now this doesn't work and we do not use rootlessport for this either so I was just thinking ahead because we do have these users requests who want ::1 to work as root.
For the current rootlessport use case we also must bind all ports as given (i.e. also addresses 0.0.0.0 bind address), just forwarding loopback to loopback is not what we want or do for security reasons, see CVE-2021-20199. And logically it would not really work to have another process bind 0.0.0.0 and this pasta helper bind lo on the same port at the same time.
The way I am thinking is bind ports as normal, add the no-tap option and add two options to give the v4 and v6 namespace (container) side connect addresses so we never actually connect to lo. Then we also should have a dynamic way to update the connect addresses at runtime which is required for podman network connect/disconnect to work which changes the addresses inside the namespace, see https://github.com/containers/podman/commit/e88d8dbeae2aebd2d816f16a21891764....
Overall none of this is a blocker for removing rootlessport. I think our plan was and still is to use the dynamic port forwarding logic David is working on to replace the rootless custom network port forwarding case with that. Regardless of other requirements that are needed as well to support forwarding ::1 for root containers (or rootless with --userns=auto), this feature by itself makes sense as it is and we'll need it as it is, right?
By the way we routinely get requests for this feature by pasta (and Podman) users, regardless of any specific Podman integration, so I think the feature is generic enough as to make sense regardless of your plan for root containers.
I am not sure how I would use or integrate a loopback to loopback forwarder in podman so I don't think we would need or can use that as is.
Well, I'm not sure, I just remember that you had in mind some use cases that could be fixed with this (and even noted them down in the references from the ticket). Sorry Yumei, I should have checked more recently, as it looks like this doesn't currently have as much priority as I thought, at least in Podman's perspective. In any case it's definitely useful. By the way, if it's for the root case, we'll still need it the day we support operation when started as root. If it's to fix up IPv4 / IPv6 loopback mapping in the rootless case, it would be usable right away.
I think the use case itself is still interesting and if there are end users asking for it sure not objections from me. I guess it could be interesting to expose a service without giving it access to the full internet and without having to deal with complicated firewall rules, i.e. with this we get a container that only could communicate by replying to the forwarded ports.
Right, yes, it might also be one way to implement "isolated" containers as described in https://bugs.passt.top/show_bug.cgi?id=139 (I still have to follow up on comments there, and that might take a while, but let me quickly mention that it has little/nothing to do with local mode). -- Stefano
On Mon, 5 Jan 2026 16:53:49 +0800
Yumei Huang
On Mon, Jan 5, 2026 at 12:18 PM David Gibson
wrote: On Mon, Dec 29, 2025 at 05:55:58PM +0800, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
Nice work. There are some things that need polish, but overall this looks pretty good to me. Like Stefano, I'm pleasantly surprised at how simple it turned out to be.
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-)
diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
I feel like this description can be improved, but I'm not exactly sure how, yet.
A few possible alternatives: - "Only enable loopback forwarding" - "Loopback only from/to namespace" - call it --splice-only, and use one of the descriptions above - call it --loopback-only, and use one of the descriptions above
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice"); + if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr");
These all make sense. It might also make sense to exclude the -i option - setting a template interface also makes no sense in --no-tap mode.
Sure, I can add an if condition with if4 (as if4=if6 in that case).
+ if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I don't think this is right. We still can and should bring up 'lo' in the --no-tap case.
I see your point, but seems c->pasta_conf_ns is only used for tap as https://passt.top/passt/tree/pasta.c#n328, 'lo' is configured before that line.
Right, and the reason is that there are basic bits of functionality (probing pipe sizes if I recall correctly, or anyway probing for some kind of capability) that need the loopback interface to be up. On the other hand, checks we're adding here are kind of fragile because we'll add other options in the future and probably forget to check which ones are incompatible, so I would try a slightly different approach: only check the options that are *obviously* conflicting with --no-tap. That is, the main thing "--config-net" does is to "Configure networking in the namespace", which we still do with "--no-tap". Now, I see that making sure c->pasta_conf_ns is false saves you checks elsewhere in the implementation, which is, I think, a good reason to have this check here. But in general we don't need to exclude all the possible options that make no sense with --no-tap. We don't really confuse users if we allow them (or, at least, some of them).
+ c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1;
The reasoning for the last two items is a bit unclear to me. IIUC, no_dns and no_dns_search aren't so much about "support" for DNS itself but for advertising DNS settings via DHCP. Since DHCP will be unsupported, so are these as a consequence. Is that right?
Yeah, I think so. Actually I added c->no_dhcp, c->no_ndp here as well, then removed them as they are set in later changes(conditions about c->ifi4/c->ifi6), though they turn out to be not quite right :'\
Do we care about them, though? That code won't be reachable anyway, unless I'm missing something. Or is it to make the output of conf_print() nicer? In that case I guess it makes sense to go and disable things.
+ } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) { strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw &&
This isn't quite right. ifi4 == -1 now occurs in two cases: local mode, and --no-tap mode. Not setting map_host_loopback makes sense for --no-tap mode, but it's still needed for local mode.
I'm a bit confused by map_host_loopback. I don't quite understand the use scenario. IIUC, either in --no-tap mode or local mode, guest can only communicate with host.
That's not the case for local mode, the guest can communicate with any other host. Local mode is just about addresses and routes, and the fact that, when pasta started, there was no template interface.
Then why do we need to set map_host_loopback? What's the benefit?
Example: guest has 169.254.2.1 (default in local mode), and wants to use 192.0.2.1 to refer to the host, via loopback interface.
IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw &&
Same here.
IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1;
And here. Local mode can still use NDP and DHCP, even though --no-tap mode can't. It might be simpler to force no_ndp, no_dhcp etc. along with no_ra and the rest above.
Sure, I will add them.
} else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice.
This probably wants some work, because I'm not sure "tap device" and "splice" are sufficiently clear in this context.
Yeah, I will think about that. Thanks.
+ .SH EXAMPLES
.SS \fBpasta diff --git a/passt.h b/passt.h index 79d01dd..0c1ec4c 100644 --- a/passt.h +++ b/passt.h @@ -200,6 +200,7 @@ struct ip6_ctx { * @no_ndp: Disable NDP handler altogether * @no_ra: Disable router advertisements * @no_splice: Disable socket splicing for inbound traffic + * @no_tap: Do not create tap device * @host_lo_to_ns_lo: Map host loopback addresses to ns loopback addresses * @freebind: Allow binding of non-local addresses for forwarding * @low_wmem: Low probed net.core.wmem_max @@ -277,6 +278,7 @@ struct ctx { int no_ndp; int no_ra; int no_splice; + int no_tap; int host_lo_to_ns_lo; int freebind;
diff --git a/pasta.c b/pasta.c index 0ddd6b0..3510ec5 100644 --- a/pasta.c +++ b/pasta.c @@ -316,6 +316,9 @@ void pasta_ns_conf(struct ctx *c) die("Couldn't bring up loopback interface in namespace: %s", strerror_(-rc));
+ if (c->no_tap) + return; + /* Get or set MAC in target namespace */ if (MAC_IS_ZERO(c->guest_mac)) nl_link_get_mac(nl_sock_ns, c->pasta_ifi, c->guest_mac); diff --git a/tap.c b/tap.c index 9d1344b..9b4eedc 100644 --- a/tap.c +++ b/tap.c @@ -1491,13 +1491,16 @@ static int tap_ns_tun(void *arg) */ static void tap_sock_tun_init(struct ctx *c) { - NS_CALL(tap_ns_tun, c); - if (c->fd_tap == -1) - die("Failed to set up tap device in namespace"); + if (!c->no_tap) { + NS_CALL(tap_ns_tun, c); + if (c->fd_tap == -1) + die("Failed to set up tap device in namespace"); + }
pasta_ns_conf(c);
- tap_start_connection(c); + if (!c->no_tap) + tap_start_connection(c); }
/** -- 2.49.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
-- Stefano
On Mon, 29 Dec 2025 17:55:58 +0800
Yumei Huang
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice");
I'm not sure if you need this for other reasons, but as long as it's called --no-tap, it's not really incompatible with --no-splice. Maybe users just want to get a disconnected namespace for whatever reason ('pasta' is shorter to type than 'unshare -rUn').
+ if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr"); + if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I guess all these checks are to save some checks later, which looks like
a good reason to have them here.
If not, though, I don't think we *really* need to tell the user that
--ns-ifname will be ignored with --no-tap.
One thing that might confuse users, though, is this:
$ ./pasta --no-tap --mtu 1500 -- ip l
1: lo:
+ + c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1; + } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) {
You already checked that !*c->pasta_ifn above.
strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw && IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw && IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1; } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice.
"Using splice" isn't really clear, and it's not entirely correct in the case of UDP: there's no splice() system call there, even if we call some UDP flows "spliced" for analogy with TCP. Maybe just omit it, say: [...] In this mode, \fIpasta\fR only forwards loopback traffic between namespaces. ?
+ .SH EXAMPLES
.SS \fBpasta diff --git a/passt.h b/passt.h index 79d01dd..0c1ec4c 100644 --- a/passt.h +++ b/passt.h @@ -200,6 +200,7 @@ struct ip6_ctx { * @no_ndp: Disable NDP handler altogether * @no_ra: Disable router advertisements * @no_splice: Disable socket splicing for inbound traffic + * @no_tap: Do not create tap device * @host_lo_to_ns_lo: Map host loopback addresses to ns loopback addresses * @freebind: Allow binding of non-local addresses for forwarding * @low_wmem: Low probed net.core.wmem_max @@ -277,6 +278,7 @@ struct ctx { int no_ndp; int no_ra; int no_splice; + int no_tap; int host_lo_to_ns_lo; int freebind;
diff --git a/pasta.c b/pasta.c index 0ddd6b0..3510ec5 100644 --- a/pasta.c +++ b/pasta.c @@ -316,6 +316,9 @@ void pasta_ns_conf(struct ctx *c) die("Couldn't bring up loopback interface in namespace: %s", strerror_(-rc));
+ if (c->no_tap) + return; + /* Get or set MAC in target namespace */ if (MAC_IS_ZERO(c->guest_mac)) nl_link_get_mac(nl_sock_ns, c->pasta_ifi, c->guest_mac); diff --git a/tap.c b/tap.c index 9d1344b..9b4eedc 100644 --- a/tap.c +++ b/tap.c @@ -1491,13 +1491,16 @@ static int tap_ns_tun(void *arg) */ static void tap_sock_tun_init(struct ctx *c) { - NS_CALL(tap_ns_tun, c); - if (c->fd_tap == -1) - die("Failed to set up tap device in namespace"); + if (!c->no_tap) { + NS_CALL(tap_ns_tun, c); + if (c->fd_tap == -1) + die("Failed to set up tap device in namespace"); + }
pasta_ns_conf(c);
- tap_start_connection(c); + if (!c->no_tap) + tap_start_connection(c); }
/**
Other than that, minus pending comments, it all looks good to me. -- Stefano
On Sat, Jan 10, 2026 at 07:12:19PM +0100, Stefano Brivio wrote:
On Mon, 5 Jan 2026 16:53:49 +0800 Yumei Huang
wrote: On Mon, Jan 5, 2026 at 12:18 PM David Gibson
wrote: On Mon, Dec 29, 2025 at 05:55:58PM +0800, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
Nice work. There are some things that need polish, but overall this looks pretty good to me. Like Stefano, I'm pleasantly surprised at how simple it turned out to be.
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-)
diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
I feel like this description can be improved, but I'm not exactly sure how, yet.
A few possible alternatives:
- "Only enable loopback forwarding"
I prefer this one to the one below.
- "Loopback only from/to namespace"
- call it --splice-only, and use one of the descriptions above
- call it --loopback-only, and use one of the descriptions above
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice"); + if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr");
These all make sense. It might also make sense to exclude the -i option - setting a template interface also makes no sense in --no-tap mode.
Sure, I can add an if condition with if4 (as if4=if6 in that case).
+ if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I don't think this is right. We still can and should bring up 'lo' in the --no-tap case.
I see your point, but seems c->pasta_conf_ns is only used for tap as https://passt.top/passt/tree/pasta.c#n328, 'lo' is configured before that line.
Right, and the reason is that there are basic bits of functionality (probing pipe sizes if I recall correctly, or anyway probing for some kind of capability) that need the loopback interface to be up.
Ah, right. Drat. In general I don't like us touching the guest netlink at all if we don't have --config-net. Hrm.. now what exactly needs this. It's not anything in sock_probe_features() - that runs in the host ns. Not pipe sizes, either - that also takes place in the host ns (and netns is irrelevant to pipes, anyway). There could well be something, but I'm not sure what it is.
On the other hand, checks we're adding here are kind of fragile because we'll add other options in the future and probably forget to check which ones are incompatible, so I would try a slightly different approach: only check the options that are *obviously* conflicting with --no-tap.
I agree.
That is, the main thing "--config-net" does is to "Configure networking in the namespace", which we still do with "--no-tap".
Agreed.
Now, I see that making sure c->pasta_conf_ns is false saves you checks elsewhere in the implementation, which is, I think, a good reason to have this check here.
But in general we don't need to exclude all the possible options that make no sense with --no-tap. We don't really confuse users if we allow them (or, at least, some of them).
+ c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1;
The reasoning for the last two items is a bit unclear to me. IIUC, no_dns and no_dns_search aren't so much about "support" for DNS itself but for advertising DNS settings via DHCP. Since DHCP will be unsupported, so are these as a consequence. Is that right?
Yeah, I think so. Actually I added c->no_dhcp, c->no_ndp here as well, then removed them as they are set in later changes(conditions about c->ifi4/c->ifi6), though they turn out to be not quite right :'\
Do we care about them, though? That code won't be reachable anyway, unless I'm missing something. Or is it to make the output of conf_print() nicer? In that case I guess it makes sense to go and disable things.
I think we want to avoid making conf_print() output misleading, and this seems a reasonable way of doing it. -- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
[Cc'ing Jon for awareness around the part about netlink monitor and capabilities, four paragraphs down]
On Wed, 7 Jan 2026 16:20:18 +0100 Paul Holzinger
wrote: On 05/01/2026 22:10, Stefano Brivio wrote:
On Mon, 5 Jan 2026 14:48:15 +0100 Paul Holzinger
wrote: Sorry I was out for a while so I didn't had time to clarify on the bug before.
On 29/12/2025 10:55, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports. I guess I didn't really communicate my requirements well. I guess it's more likely that you actually did, but I mixed up the association between requirements and use cases, sorry for that.
In any case, good that we need this anyway, just for another use case. :)
When we use rootlessport (rootlesskit) today for custom networks we only do so as rootless user and it forwards ::1 (by possibly mapping this to v4 inside the container) fine. So, wait a moment, is my comment at:
https://github.com/containers/podman/issues/14491#issuecomment-2898191772
actually wrong? I don't have time right now to test that but from user reports and some vague memory I thought ::1 forwarding wouldn't work with custom networks regardless of root or rootless, because rootlesskit didn't handle that anyway.
yes, rootlesskit handles ipv6 just fine, it is just that our rootlessport code remaps that to v4 inside the container.
Actually, at a glance, I don't think that this could be fixed entirely in the rootlessport implementation, as rootlesskit doesn't seem to look at the destination address of the original connection at all.
My main point for this feature was using as root (requires further changes to allow pasta running as root). ...which should be entirely on Podman side and it's still on my plate, by the way:
https://github.com/containers/podman/issues/17840 https://pad.passt.top/p/Features_2025#L40
I don't see how this can be fixed on the podman side, the network namespace of a rootful container (not userns=auto) is owned by the root user. If you configure something in there you must have real CAP_NET_ADMIN from the host init userns. So pasta must not drop this privilege before configuring the netns.
Oops, right. My starting point was this change, which is actually trivial (at least as a test) and something I already tried out, but then I hit a number of issues in Podman I never really figured out.
So yes, it takes one change in pasta, but the substantial part left for me to figure out is why Podman didn't just work with it. It's not necessarily complicated, I spent just a couple of hours on it, so maybe there's something simple I missed.
And even then with the future netlink monitor work we would need to keep that privilege level to modify the netns even during runtime?
This just reminded me that, somewhat surprisingly, for netlink operations, the check on capabilities is not just performed on the process creating the socket when the socket is created, but also later *on the sender of the message*.
This is inconsistent with other operations on other types of sockets where the whole context is checked and assigned at the time of the creation, and was introduced because of a specific behaviour of Zebra (the routing daemon) in 2014, see discussion around:
https://lore.kernel.org/all/87d2g7d9ag.fsf_-_@x220.int.ebiederm.org/#r
and I stumbled upon it a while ago while preparing a seitan demo replaying nft messages for an unprivileged container:
https://seitan.rocks/seitan/tree/demo/nft.hjson#n38
So, my blanket answer "we create that socket at the beginning" doesn't apply here.
However, assuming that this RFC patch from Jon actually works (I haven't tested it):
https://archives.passt.top/passt-dev/20251215015441.887736-11-jmaloy@redhat....
I would say we're fine with it. Well, there's still the possibility that it doesn't work if Podman originally detached the network namespace, I'm not sure.
If it doesn't work, we'll need to retain more capabilities, or even keep a cloned process around for this kind of stuff. We could also fix that in the kernel, Zebra doesn't need that quirk anymore.
Because as root podman does port forwarding via DNAT firewall rules (i.e. custom nftables rules we add). The kernel however never added support for DNAT on ::1 meaning clients trying to access that are not getting forwarded. The only way to support this is using a user space helper. Right now this doesn't work and we do not use rootlessport for this either so I was just thinking ahead because we do have these users requests who want ::1 to work as root.
For the current rootlessport use case we also must bind all ports as given (i.e. also addresses 0.0.0.0 bind address), just forwarding loopback to loopback is not what we want or do for security reasons, see CVE-2021-20199. And logically it would not really work to have another process bind 0.0.0.0 and this pasta helper bind lo on the same port at the same time.
The way I am thinking is bind ports as normal, add the no-tap option and add two options to give the v4 and v6 namespace (container) side connect addresses so we never actually connect to lo. Then we also should have a dynamic way to update the connect addresses at runtime which is required for podman network connect/disconnect to work which changes the addresses inside the namespace, see https://github.com/containers/podman/commit/e88d8dbeae2aebd2d816f16a21891764....
Overall none of this is a blocker for removing rootlessport. I think our plan was and still is to use the dynamic port forwarding logic David is working on to replace the rootless custom network port forwarding case with that. Regardless of other requirements that are needed as well to support forwarding ::1 for root containers (or rootless with --userns=auto), this feature by itself makes sense as it is and we'll need it as it is, right?
By the way we routinely get requests for this feature by pasta (and Podman) users, regardless of any specific Podman integration, so I think the feature is generic enough as to make sense regardless of your plan for root containers.
I am not sure how I would use or integrate a loopback to loopback forwarder in podman so I don't think we would need or can use that as is.
Well, I'm not sure, I just remember that you had in mind some use cases that could be fixed with this (and even noted them down in the references from the ticket).
Sorry Yumei, I should have checked more recently, as it looks like this doesn't currently have as much priority as I thought, at least in Podman's perspective. In any case it's definitely useful.
No worries at all :)
By the way, if it's for the root case, we'll still need it the day we support operation when started as root. If it's to fix up IPv4 / IPv6 loopback mapping in the rootless case, it would be usable right away.
I think the use case itself is still interesting and if there are end users asking for it sure not objections from me. I guess it could be interesting to expose a service without giving it access to the full internet and without having to deal with complicated firewall rules, i.e. with this we get a container that only could communicate by replying to the forwarded ports.
Right, yes, it might also be one way to implement "isolated" containers as described in https://bugs.passt.top/show_bug.cgi?id=139 (I still have to follow up on comments there, and that might take a while, but let me quickly mention that it has little/nothing to do with local mode).
-- Stefano
-- Thanks, Yumei Huang
On Mon, 12 Jan 2026 15:26:14 +1100
David Gibson
On Sat, Jan 10, 2026 at 07:12:19PM +0100, Stefano Brivio wrote:
On Mon, 5 Jan 2026 16:53:49 +0800 Yumei Huang
wrote: On Mon, Jan 5, 2026 at 12:18 PM David Gibson
wrote: On Mon, Dec 29, 2025 at 05:55:58PM +0800, Yumei Huang wrote:
+ if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I don't think this is right. We still can and should bring up 'lo' in the --no-tap case.
I see your point, but seems c->pasta_conf_ns is only used for tap as https://passt.top/passt/tree/pasta.c#n328, 'lo' is configured before that line.
Right, and the reason is that there are basic bits of functionality (probing pipe sizes if I recall correctly, or anyway probing for some kind of capability) that need the loopback interface to be up.
Ah, right. Drat. In general I don't like us touching the guest netlink at all if we don't have --config-net. Hrm.. now what exactly needs this. It's not anything in sock_probe_features() - that runs in the host ns. Not pipe sizes, either - that also takes place in the host ns (and netns is irrelevant to pipes, anyway). There could well be something, but I'm not sure what it is.
Actually, I tried, and I don't get any trouble (but I think I had some error when I added that in 2021). But we implicitly break any outbound forwarding because our listening sockets will be unreachable (bind() succeeds though). So... I would be wary of changing that at this point. There might be users relying on it, and it's otherwise harmless I guess. -- Stefano
On Tue, Jan 13, 2026 at 01:12:09AM +0100, Stefano Brivio wrote:
On Mon, 12 Jan 2026 15:26:14 +1100 David Gibson
wrote: On Sat, Jan 10, 2026 at 07:12:19PM +0100, Stefano Brivio wrote:
On Mon, 5 Jan 2026 16:53:49 +0800 Yumei Huang
wrote: On Mon, Jan 5, 2026 at 12:18 PM David Gibson
wrote: On Mon, Dec 29, 2025 at 05:55:58PM +0800, Yumei Huang wrote:
+ if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I don't think this is right. We still can and should bring up 'lo' in the --no-tap case.
I see your point, but seems c->pasta_conf_ns is only used for tap as https://passt.top/passt/tree/pasta.c#n328, 'lo' is configured before that line.
Right, and the reason is that there are basic bits of functionality (probing pipe sizes if I recall correctly, or anyway probing for some kind of capability) that need the loopback interface to be up.
Ah, right. Drat. In general I don't like us touching the guest netlink at all if we don't have --config-net. Hrm.. now what exactly needs this. It's not anything in sock_probe_features() - that runs in the host ns. Not pipe sizes, either - that also takes place in the host ns (and netns is irrelevant to pipes, anyway). There could well be something, but I'm not sure what it is.
Actually, I tried, and I don't get any trouble (but I think I had some error when I added that in 2021).
Ok.
But we implicitly break any outbound forwarding because our listening sockets will be unreachable (bind() succeeds though).
Networking doesn't work until you configure networking, that's the normal state for !--config-net. I don't see why that should be different for outbound forwards than anything else.
So... I would be wary of changing that at this point. There might be users relying on it, and it's otherwise harmless I guess.
I mean.. probably? Almost certainly when pasta is creating the ns - but in that case there's very little reason not to use --config-net anyway. The case I'm concerned about is attaching this to an existing netns: this can alter the existing network config there. -- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
On Mon, 5 Jan 2026 16:53:49 +0800 Yumei Huang
wrote: On Mon, Jan 5, 2026 at 12:18 PM David Gibson
wrote: On Mon, Dec 29, 2025 at 05:55:58PM +0800, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
Nice work. There are some things that need polish, but overall this looks pretty good to me. Like Stefano, I'm pleasantly surprised at how simple it turned out to be.
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-)
diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
I feel like this description can be improved, but I'm not exactly sure how, yet.
A few possible alternatives:
- "Only enable loopback forwarding"
Thanks, I will go with this and --splice-only.
- "Loopback only from/to namespace"
- call it --splice-only, and use one of the descriptions above
- call it --loopback-only, and use one of the descriptions above
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice"); + if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr");
These all make sense. It might also make sense to exclude the -i option - setting a template interface also makes no sense in --no-tap mode.
Sure, I can add an if condition with if4 (as if4=if6 in that case).
+ if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I don't think this is right. We still can and should bring up 'lo' in the --no-tap case.
I see your point, but seems c->pasta_conf_ns is only used for tap as https://passt.top/passt/tree/pasta.c#n328, 'lo' is configured before that line.
Right, and the reason is that there are basic bits of functionality (probing pipe sizes if I recall correctly, or anyway probing for some kind of capability) that need the loopback interface to be up.
On the other hand, checks we're adding here are kind of fragile because we'll add other options in the future and probably forget to check which ones are incompatible, so I would try a slightly different approach: only check the options that are *obviously* conflicting with --no-tap.
That is, the main thing "--config-net" does is to "Configure networking in the namespace", which we still do with "--no-tap".
I added that because I thought --config-net is for tap only (as lo is configured always), and there is no tap for this mode. I can remove it, it still works without it.
Now, I see that making sure c->pasta_conf_ns is false saves you checks elsewhere in the implementation, which is, I think, a good reason to have this check here.
But in general we don't need to exclude all the possible options that make no sense with --no-tap. We don't really confuse users if we allow them (or, at least, some of them).
I see. From a tester's perspective, we used to uncover a few bugs by combining certain options in different ways. I feel it's more user-friendly to explicitly state what is unsupported and what will be ignored, although this is not a strong preference. I can update with only excluding obvious ones.
+ c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1;
The reasoning for the last two items is a bit unclear to me. IIUC, no_dns and no_dns_search aren't so much about "support" for DNS itself but for advertising DNS settings via DHCP. Since DHCP will be unsupported, so are these as a consequence. Is that right?
Yeah, I think so. Actually I added c->no_dhcp, c->no_ndp here as well, then removed them as they are set in later changes(conditions about c->ifi4/c->ifi6), though they turn out to be not quite right :'\
Do we care about them, though? That code won't be reachable anyway, unless I'm missing something. Or is it to make the output of conf_print() nicer? In that case I guess it makes sense to go and disable things.
Just tried, without c->no_dns we would hit a warning "Couldn't get any nameserver address" , without c->no_dns_search, the conf_print will have lines as below, 0.0021: DNS search list: 0.0021: . 0.0021: DNS search list: 0.0022: . Apart from that, seems there is no difference with or without c->ra, c->no_dhcp, c->no_ndp. I guess we only need c->no_dns and c->no_dns_search here.
+ } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) { strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw &&
This isn't quite right. ifi4 == -1 now occurs in two cases: local mode, and --no-tap mode. Not setting map_host_loopback makes sense for --no-tap mode, but it's still needed for local mode.
I'm a bit confused by map_host_loopback. I don't quite understand the use scenario. IIUC, either in --no-tap mode or local mode, guest can only communicate with host.
That's not the case for local mode, the guest can communicate with any other host. Local mode is just about addresses and routes, and the fact that, when pasta started, there was no template interface.
Oh, I thought local mode is only for host without network connectivity. Thanks for the explanation.
Then why do we need to set map_host_loopback? What's the benefit?
Example: guest has 169.254.2.1 (default in local mode), and wants to use 192.0.2.1 to refer to the host, via loopback interface.
IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw &&
Same here.
IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1;
And here. Local mode can still use NDP and DHCP, even though --no-tap mode can't. It might be simpler to force no_ndp, no_dhcp etc. along with no_ra and the rest above.
Sure, I will add them.
} else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice.
This probably wants some work, because I'm not sure "tap device" and "splice" are sufficiently clear in this context.
Yeah, I will think about that. Thanks.
+ .SH EXAMPLES
.SS \fBpasta diff --git a/passt.h b/passt.h index 79d01dd..0c1ec4c 100644 --- a/passt.h +++ b/passt.h @@ -200,6 +200,7 @@ struct ip6_ctx { * @no_ndp: Disable NDP handler altogether * @no_ra: Disable router advertisements * @no_splice: Disable socket splicing for inbound traffic + * @no_tap: Do not create tap device * @host_lo_to_ns_lo: Map host loopback addresses to ns loopback addresses * @freebind: Allow binding of non-local addresses for forwarding * @low_wmem: Low probed net.core.wmem_max @@ -277,6 +278,7 @@ struct ctx { int no_ndp; int no_ra; int no_splice; + int no_tap; int host_lo_to_ns_lo; int freebind;
diff --git a/pasta.c b/pasta.c index 0ddd6b0..3510ec5 100644 --- a/pasta.c +++ b/pasta.c @@ -316,6 +316,9 @@ void pasta_ns_conf(struct ctx *c) die("Couldn't bring up loopback interface in namespace: %s", strerror_(-rc));
+ if (c->no_tap) + return; + /* Get or set MAC in target namespace */ if (MAC_IS_ZERO(c->guest_mac)) nl_link_get_mac(nl_sock_ns, c->pasta_ifi, c->guest_mac); diff --git a/tap.c b/tap.c index 9d1344b..9b4eedc 100644 --- a/tap.c +++ b/tap.c @@ -1491,13 +1491,16 @@ static int tap_ns_tun(void *arg) */ static void tap_sock_tun_init(struct ctx *c) { - NS_CALL(tap_ns_tun, c); - if (c->fd_tap == -1) - die("Failed to set up tap device in namespace"); + if (!c->no_tap) { + NS_CALL(tap_ns_tun, c); + if (c->fd_tap == -1) + die("Failed to set up tap device in namespace"); + }
pasta_ns_conf(c);
- tap_start_connection(c); + if (!c->no_tap) + tap_start_connection(c); }
/** -- 2.49.0
-- David Gibson (he or they) | I'll have my music baroque, and my code david AT gibson.dropbear.id.au | minimalist, thank you, not the other way | around. http://www.ozlabs.org/~dgibson
-- Stefano
-- Thanks, Yumei Huang
On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
On Mon, 29 Dec 2025 17:55:58 +0800 Yumei Huang
wrote: This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice");
I'm not sure if you need this for other reasons, but as long as it's called --no-tap, it's not really incompatible with --no-splice.
I will update it to --splice-only
Maybe users just want to get a disconnected namespace for whatever reason ('pasta' is shorter to type than 'unshare -rUn').
+ if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr"); + if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I guess all these checks are to save some checks later, which looks like a good reason to have them here.
If not, though, I don't think we *really* need to tell the user that --ns-ifname will be ignored with --no-tap.
One thing that might confuse users, though, is this:
$ ./pasta --no-tap --mtu 1500 -- ip l 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 or even this:
$ ./pasta --no-tap -a 192.0.2.1 -- ip a 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host proto kernel_lo valid_lft forever preferred_lft forever but I would rather *not* add conditions and checks for those even if there's a *slight* potential for confusion, otherwise this becomes really long. And it's really not worth it, I think.
Then I guess we only need the c->no_splice check, right?
+ + c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1; + } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) {
You already checked that !*c->pasta_ifn above.
I guess the check above (aka. if (*c->pasta_ifn && c->no_tap)) doesn't affect this one? If c->pasta_ifn is assigned, we won't come to the check !c->no_tap here. Otherwise, we do need to check !c->no_tap.
strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw && IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw && IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1; } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice.
"Using splice" isn't really clear, and it's not entirely correct in the case of UDP: there's no splice() system call there, even if we call some UDP flows "spliced" for analogy with TCP.
Maybe just omit it, say:
[...] In this mode, \fIpasta\fR only forwards loopback traffic between namespaces.
Do you think we still need "Do not create a tap device in the namespace" after updating it to --splice-only?
?
+ .SH EXAMPLES
.SS \fBpasta diff --git a/passt.h b/passt.h index 79d01dd..0c1ec4c 100644 --- a/passt.h +++ b/passt.h @@ -200,6 +200,7 @@ struct ip6_ctx { * @no_ndp: Disable NDP handler altogether * @no_ra: Disable router advertisements * @no_splice: Disable socket splicing for inbound traffic + * @no_tap: Do not create tap device * @host_lo_to_ns_lo: Map host loopback addresses to ns loopback addresses * @freebind: Allow binding of non-local addresses for forwarding * @low_wmem: Low probed net.core.wmem_max @@ -277,6 +278,7 @@ struct ctx { int no_ndp; int no_ra; int no_splice; + int no_tap; int host_lo_to_ns_lo; int freebind;
diff --git a/pasta.c b/pasta.c index 0ddd6b0..3510ec5 100644 --- a/pasta.c +++ b/pasta.c @@ -316,6 +316,9 @@ void pasta_ns_conf(struct ctx *c) die("Couldn't bring up loopback interface in namespace: %s", strerror_(-rc));
+ if (c->no_tap) + return; + /* Get or set MAC in target namespace */ if (MAC_IS_ZERO(c->guest_mac)) nl_link_get_mac(nl_sock_ns, c->pasta_ifi, c->guest_mac); diff --git a/tap.c b/tap.c index 9d1344b..9b4eedc 100644 --- a/tap.c +++ b/tap.c @@ -1491,13 +1491,16 @@ static int tap_ns_tun(void *arg) */ static void tap_sock_tun_init(struct ctx *c) { - NS_CALL(tap_ns_tun, c); - if (c->fd_tap == -1) - die("Failed to set up tap device in namespace"); + if (!c->no_tap) { + NS_CALL(tap_ns_tun, c); + if (c->fd_tap == -1) + die("Failed to set up tap device in namespace"); + }
pasta_ns_conf(c);
- tap_start_connection(c); + if (!c->no_tap) + tap_start_connection(c); }
/**
Other than that, minus pending comments, it all looks good to me.
Thank for the review and comments!
-- Stefano
-- Thanks, Yumei Huang
On Tue, 13 Jan 2026 13:39:34 +1100
David Gibson
On Tue, Jan 13, 2026 at 01:12:09AM +0100, Stefano Brivio wrote:
On Mon, 12 Jan 2026 15:26:14 +1100 David Gibson
wrote: On Sat, Jan 10, 2026 at 07:12:19PM +0100, Stefano Brivio wrote:
On Mon, 5 Jan 2026 16:53:49 +0800 Yumei Huang
wrote: On Mon, Jan 5, 2026 at 12:18 PM David Gibson
wrote: On Mon, Dec 29, 2025 at 05:55:58PM +0800, Yumei Huang wrote:
> + if (c->pasta_conf_ns) > + die("--no-tap is incompatible with --config-net");
I don't think this is right. We still can and should bring up 'lo' in the --no-tap case.
I see your point, but seems c->pasta_conf_ns is only used for tap as https://passt.top/passt/tree/pasta.c#n328, 'lo' is configured before that line.
Right, and the reason is that there are basic bits of functionality (probing pipe sizes if I recall correctly, or anyway probing for some kind of capability) that need the loopback interface to be up.
Ah, right. Drat. In general I don't like us touching the guest netlink at all if we don't have --config-net. Hrm.. now what exactly needs this. It's not anything in sock_probe_features() - that runs in the host ns. Not pipe sizes, either - that also takes place in the host ns (and netns is irrelevant to pipes, anyway). There could well be something, but I'm not sure what it is.
Actually, I tried, and I don't get any trouble (but I think I had some error when I added that in 2021).
Ok.
But we implicitly break any outbound forwarding because our listening sockets will be unreachable (bind() succeeds though).
Networking doesn't work until you configure networking, that's the normal state for !--config-net. I don't see why that should be different for outbound forwards than anything else.
Yes, I understand your point, and even agree in general. I'm just saying that that part worked until now without bringing the loopback interface up, and we'd break it. By the way, --config-net configures a lot of things, at least by default, that might be done in a different way, even though it's not the default simply because the functionality didn't exist at the beginning, as I implemented passt with its DHCP server first. Bringing up the loopback interface is much more universal than that. I guess virtually all users would want to have it brought up. And especially with --splice-only, *not* doing that would become quite a nuisance I think. You would start pasta as: pasta --splice-only and nothing would work, unless you do: pasta --splice-only --config-net where "--config-net" now means... bring up the loopback interface. It might be considered more correct, strictly speaking, but not really intuitive. Just let users bring the loopback interface down I'd say, if they really need that. There's by the way a small asymmetry in all this: the kernel adds all addresses one normally needs to the loopback interface as you bring it up, without external intervention. It's not exactly something we would "configure" separately.
So... I would be wary of changing that at this point. There might be users relying on it, and it's otherwise harmless I guess.
I mean.. probably? Almost certainly when pasta is creating the ns - but in that case there's very little reason not to use --config-net anyway. The case I'm concerned about is attaching this to an existing netns: this can alter the existing network config there.
Strictly speaking, yes, but I can't really picture anybody using namespaces relying on the fact that the loopback interface is down. I mean, given the choice between: Bug 190 - Port forwarding doesn't work with --splice-only and: Bug 190 - pasta unexpectedly brings up loopback interface in container ...the first one, you could explain why and close it. But the second one would never be reported. Well, I guess. -- Stefano
On Tue, 13 Jan 2026 17:57:11 +0800
Yumei Huang
On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
wrote: On Mon, 5 Jan 2026 16:53:49 +0800 Yumei Huang
wrote: On Mon, Jan 5, 2026 at 12:18 PM David Gibson
wrote: On Mon, Dec 29, 2025 at 05:55:58PM +0800, Yumei Huang wrote:
This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
Nice work. There are some things that need polish, but overall this looks pretty good to me. Like Stefano, I'm pleasantly surprised at how simple it turned out to be.
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-)
diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
I feel like this description can be improved, but I'm not exactly sure how, yet.
A few possible alternatives:
- "Only enable loopback forwarding"
Thanks, I will go with this and --splice-only.
- "Loopback only from/to namespace"
- call it --splice-only, and use one of the descriptions above
- call it --loopback-only, and use one of the descriptions above
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice"); + if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr");
These all make sense. It might also make sense to exclude the -i option - setting a template interface also makes no sense in --no-tap mode.
Sure, I can add an if condition with if4 (as if4=if6 in that case).
+ if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I don't think this is right. We still can and should bring up 'lo' in the --no-tap case.
I see your point, but seems c->pasta_conf_ns is only used for tap as https://passt.top/passt/tree/pasta.c#n328, 'lo' is configured before that line.
Right, and the reason is that there are basic bits of functionality (probing pipe sizes if I recall correctly, or anyway probing for some kind of capability) that need the loopback interface to be up.
On the other hand, checks we're adding here are kind of fragile because we'll add other options in the future and probably forget to check which ones are incompatible, so I would try a slightly different approach: only check the options that are *obviously* conflicting with --no-tap.
That is, the main thing "--config-net" does is to "Configure networking in the namespace", which we still do with "--no-tap".
I added that because I thought --config-net is for tap only (as lo is configured always), and there is no tap for this mode. I can remove it, it still works without it.
If it's harmless, I would just pick what's simpler for the implementation. For users it should be exactly the same: - slight advantage of allowing it: no need to edit the command line if one specified both by mistake - slight disadvantage of disallowing it: users might try to do something with it that won't really happen (but I guess it's unlikely)
Now, I see that making sure c->pasta_conf_ns is false saves you checks elsewhere in the implementation, which is, I think, a good reason to have this check here.
But in general we don't need to exclude all the possible options that make no sense with --no-tap. We don't really confuse users if we allow them (or, at least, some of them).
I see. From a tester's perspective, we used to uncover a few bugs by combining certain options in different ways.
In this case, we don't know in which category those bugs might be: - *added* by allowing conflicting options (bad) - *discovered* by allowing conflicting options (good) - *not discovered* by not allowing conflicting options (bad, but not as bad) ...maybe others?
I feel it's more user-friendly to explicitly state what is unsupported and what will be ignored, although this is not a strong preference. I can update with only excluding obvious ones.
I think it's relevant if there are subtle combinations that are not actually working but they might look like it. Other than that, it *might* be more user-friendly to leave users as much choice as possible, because: 1. you don't need to re-type things if you obviously added an option that didn't make sense (especially relevant if you use the reverse-search function in a shell) 2. some options are set in integrations (Podman, rootlesskit/moby), for example --config-net. Let's say you want to try things out with Podman and --splice-only: --config-net doesn't make sense in that case, but Podman sets it regardless. So if you let pasta start with both, you can try things out without rebuilding and reinstalling, otherwise you have to rebuild and reinstall, but there's no added value, it's just us being picky. -- Stefano
On Tue, 13 Jan 2026 19:20:47 +0800
Yumei Huang
On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
wrote: On Mon, 29 Dec 2025 17:55:58 +0800 Yumei Huang
wrote: This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice");
I'm not sure if you need this for other reasons, but as long as it's called --no-tap, it's not really incompatible with --no-splice.
I will update it to --splice-only
Maybe users just want to get a disconnected namespace for whatever reason ('pasta' is shorter to type than 'unshare -rUn').
+ if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr"); + if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I guess all these checks are to save some checks later, which looks like a good reason to have them here.
If not, though, I don't think we *really* need to tell the user that --ns-ifname will be ignored with --no-tap.
One thing that might confuse users, though, is this:
$ ./pasta --no-tap --mtu 1500 -- ip l 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 or even this:
$ ./pasta --no-tap -a 192.0.2.1 -- ip a 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host proto kernel_lo valid_lft forever preferred_lft forever but I would rather *not* add conditions and checks for those even if there's a *slight* potential for confusion, otherwise this becomes really long. And it's really not worth it, I think.
Then I guess we only need the c->no_splice check, right?
...maybe? About *needing*, yes, I guess so, but if other checks save more checks later, I would keep them.
+ + c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1; + } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) {
You already checked that !*c->pasta_ifn above.
I guess the check above (aka. if (*c->pasta_ifn && c->no_tap)) doesn't affect this one? If c->pasta_ifn is assigned, we won't come to the check !c->no_tap here. Otherwise, we do need to check !c->no_tap.
Right, but you don't care about resetting c->pasta_ifn to the default value if !c->no_tap, because in that case you know that c->pasta_ifn wasn't set, so you can happily override it. I guess, at least, I haven't thoroughly checked what might happen later with it.
strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw && IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw && IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1; } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice.
"Using splice" isn't really clear, and it's not entirely correct in the case of UDP: there's no splice() system call there, even if we call some UDP flows "spliced" for analogy with TCP.
Maybe just omit it, say:
[...] In this mode, \fIpasta\fR only forwards loopback traffic between namespaces.
Do you think we still need "Do not create a tap device in the namespace" after updating it to --splice-only?
Yes, I think so, because otherwise it might look like we create the tap device anyway, but discard all traffic from/to it. -- Stefano
On Wed, Jan 14, 2026 at 7:34 AM Stefano Brivio
On Tue, 13 Jan 2026 19:20:47 +0800 Yumei Huang
wrote: On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
wrote: On Mon, 29 Dec 2025 17:55:58 +0800 Yumei Huang
wrote: This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice");
I'm not sure if you need this for other reasons, but as long as it's called --no-tap, it's not really incompatible with --no-splice.
I will update it to --splice-only
Maybe users just want to get a disconnected namespace for whatever reason ('pasta' is shorter to type than 'unshare -rUn').
+ if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr"); + if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I guess all these checks are to save some checks later, which looks like a good reason to have them here.
If not, though, I don't think we *really* need to tell the user that --ns-ifname will be ignored with --no-tap.
One thing that might confuse users, though, is this:
$ ./pasta --no-tap --mtu 1500 -- ip l 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 or even this:
$ ./pasta --no-tap -a 192.0.2.1 -- ip a 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host proto kernel_lo valid_lft forever preferred_lft forever but I would rather *not* add conditions and checks for those even if there's a *slight* potential for confusion, otherwise this becomes really long. And it's really not worth it, I think.
Then I guess we only need the c->no_splice check, right?
...maybe? About *needing*, yes, I guess so, but if other checks save more checks later, I would keep them.
+ + c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1; + } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) {
You already checked that !*c->pasta_ifn above.
I guess the check above (aka. if (*c->pasta_ifn && c->no_tap)) doesn't affect this one? If c->pasta_ifn is assigned, we won't come to the check !c->no_tap here. Otherwise, we do need to check !c->no_tap.
Right, but you don't care about resetting c->pasta_ifn to the default value if !c->no_tap, because in that case you know that c->pasta_ifn wasn't set, so you can happily override it.
I'm not sure I fully understand it. If !c->no_tap, the condition is the same as before without this patch, which is to not reset it if it's specified in cmd line. We won't know if c->pasta_ifn is set until this check, do we?
I guess, at least, I haven't thoroughly checked what might happen later with it.
strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw && IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw && IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1; } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice.
"Using splice" isn't really clear, and it's not entirely correct in the case of UDP: there's no splice() system call there, even if we call some UDP flows "spliced" for analogy with TCP.
Maybe just omit it, say:
[...] In this mode, \fIpasta\fR only forwards loopback traffic between namespaces.
Do you think we still need "Do not create a tap device in the namespace" after updating it to --splice-only?
Yes, I think so, because otherwise it might look like we create the tap device anyway, but discard all traffic from/to it.
-- Stefano
-- Thanks, Yumei Huang
On Wed, Jan 14, 2026 at 2:31 PM Yumei Huang
On Wed, Jan 14, 2026 at 7:34 AM Stefano Brivio
wrote: On Tue, 13 Jan 2026 19:20:47 +0800 Yumei Huang
wrote: On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
wrote: On Mon, 29 Dec 2025 17:55:58 +0800 Yumei Huang
wrote: This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice");
I'm not sure if you need this for other reasons, but as long as it's called --no-tap, it's not really incompatible with --no-splice.
I will update it to --splice-only
Maybe users just want to get a disconnected namespace for whatever reason ('pasta' is shorter to type than 'unshare -rUn').
+ if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr"); + if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I guess all these checks are to save some checks later, which looks like a good reason to have them here.
If not, though, I don't think we *really* need to tell the user that --ns-ifname will be ignored with --no-tap.
One thing that might confuse users, though, is this:
$ ./pasta --no-tap --mtu 1500 -- ip l 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 or even this:
$ ./pasta --no-tap -a 192.0.2.1 -- ip a 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host proto kernel_lo valid_lft forever preferred_lft forever but I would rather *not* add conditions and checks for those even if there's a *slight* potential for confusion, otherwise this becomes really long. And it's really not worth it, I think.
Then I guess we only need the c->no_splice check, right?
...maybe? About *needing*, yes, I guess so, but if other checks save more checks later, I would keep them.
+ + c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1; + } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) {
You already checked that !*c->pasta_ifn above.
I guess the check above (aka. if (*c->pasta_ifn && c->no_tap)) doesn't affect this one? If c->pasta_ifn is assigned, we won't come to the check !c->no_tap here. Otherwise, we do need to check !c->no_tap.
Right, but you don't care about resetting c->pasta_ifn to the default value if !c->no_tap, because in that case you know that c->pasta_ifn wasn't set, so you can happily override it.
I just realized that you probably meant when c->no_tap is set. Actually it would affect conf_print, info("Namespace interface: %s", c->pasta_ifn). But I will add a condition about c->splice_only before this line, so yes, it doesn't matter whether reset it or not. I will remove the check in v2.
I'm not sure I fully understand it. If !c->no_tap, the condition is the same as before without this patch, which is to not reset it if it's specified in cmd line. We won't know if c->pasta_ifn is set until this check, do we?
I guess, at least, I haven't thoroughly checked what might happen later with it.
strncpy(c->pasta_ifn, pasta_default_ifn, sizeof(c->pasta_ifn) - 1); }
if (!c->ifi4 && !v6_only) { - info("IPv4: no external interface as template, use local mode"); - - conf_ip4_local(&c->ip4); + if (!c->no_tap) { + info("IPv4: no external interface as template, use local mode"); + conf_ip4_local(&c->ip4); + } c->ifi4 = -1; }
if (!c->ifi6 && !v4_only) { - info("IPv6: no external interface as template, use local mode"); - - conf_ip6_local(&c->ip6); + if (!c->no_tap) { + info("IPv6: no external interface as template, use local mode"); + conf_ip6_local(&c->ip6); + } c->ifi6 = -1; }
- if (c->ifi4 && !no_map_gw && + if (c->ifi4 > 0 && !no_map_gw && IN4_IS_ADDR_UNSPECIFIED(&c->ip4.map_host_loopback)) c->ip4.map_host_loopback = c->ip4.guest_gw;
- if (c->ifi6 && !no_map_gw && + if (c->ifi6 > 0 && !no_map_gw && IN6_IS_ADDR_UNSPECIFIED(&c->ip6.map_host_loopback)) c->ip6.map_host_loopback = c->ip6.guest_gw;
@@ -2116,10 +2142,10 @@ void conf(struct ctx *c, int argc, char **argv) conf_ports(c, name, optarg, &c->udp.fwd_out); } while (name != -1);
- if (!c->ifi4) + if (c->ifi4 <= 0) c->no_dhcp = 1;
- if (!c->ifi6) { + if (c->ifi6 <= 0) { c->no_ndp = 1; c->no_dhcpv6 = 1; } else if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr)) { diff --git a/fwd.c b/fwd.c index 44a0e10..2f4a89a 100644 --- a/fwd.c +++ b/fwd.c @@ -780,6 +780,9 @@ uint8_t fwd_nat_from_host(const struct ctx *c, uint8_t proto, return PIF_SPLICE; }
+ if (c->no_tap) + return PIF_NONE; + if (!nat_inbound(c, &ini->eaddr, &tgt->oaddr)) { if (inany_v4(&ini->eaddr)) { if (IN4_IS_ADDR_UNSPECIFIED(&c->ip4.our_tap_addr)) diff --git a/passt.1 b/passt.1 index db0d662..2d643f7 100644 --- a/passt.1 +++ b/passt.1 @@ -755,6 +755,11 @@ Default is to let the tap driver build a pseudorandom hardware address. Disable the bypass path for inbound, local traffic. See the section \fBHandling of local traffic in pasta\fR in the \fBNOTES\fR for more details.
+.TP +.BR \-\-no-tap +Do not create a tap device in the namespace. In this mode, only local loopback +traffic between namespaces is forwarded using splice.
"Using splice" isn't really clear, and it's not entirely correct in the case of UDP: there's no splice() system call there, even if we call some UDP flows "spliced" for analogy with TCP.
Maybe just omit it, say:
[...] In this mode, \fIpasta\fR only forwards loopback traffic between namespaces.
Do you think we still need "Do not create a tap device in the namespace" after updating it to --splice-only?
Yes, I think so, because otherwise it might look like we create the tap device anyway, but discard all traffic from/to it.
-- Stefano
-- Thanks,
Yumei Huang
-- Thanks, Yumei Huang
On Wed, 14 Jan 2026 15:28:31 +0800
Yumei Huang
On Wed, Jan 14, 2026 at 2:31 PM Yumei Huang
wrote: On Wed, Jan 14, 2026 at 7:34 AM Stefano Brivio
wrote: On Tue, 13 Jan 2026 19:20:47 +0800 Yumei Huang
wrote: On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
wrote: On Mon, 29 Dec 2025 17:55:58 +0800 Yumei Huang
wrote: This patch introduces a mode where we only forward loopback connections and traffic between two namespaces (via the loopback interface, 'lo'), without a tap device.
With this, podman can support forwarding ::1 in custom networks when using rootlesskit for forwarding ports.
In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, --config-net, --outbound-if4/6) are rejected.
Link: https://bugs.passt.top/show_bug.cgi?id=149 Signed-off-by: Yumei Huang
--- conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- fwd.c | 3 +++ passt.1 | 5 +++++ passt.h | 2 ++ pasta.c | 3 +++ tap.c | 11 +++++++---- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/conf.c b/conf.c index 84ae12b..353d0a5 100644 --- a/conf.c +++ b/conf.c @@ -1049,7 +1049,8 @@ pasta_opts: " --no-copy-addrs DEPRECATED:\n" " 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"); + " --no-splice Disable inbound socket splicing\n" + " --no-tap Don't create tap device\n");
passt_exit(status); } @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-splice", no_argument, &c->no_splice, 1 }, + {"no-tap", no_argument, &c->no_tap, 1 }, {"freebind", no_argument, &c->freebind, 1 }, {"no-map-gw", no_argument, &no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) } } while (name != -1);
- if (c->mode != MODE_PASTA) + if (c->mode != MODE_PASTA) { c->no_splice = 1; + if (c->no_tap) + die("--no-tap is for pasta mode only"); + }
if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { if (copy_routes_opt) @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) die("--no-copy-addrs needs --config-net"); }
+ if (c->mode == MODE_PASTA && c->no_tap) { + if (c->no_splice) + die("--no-tap is incompatible with --no-splice");
I'm not sure if you need this for other reasons, but as long as it's called --no-tap, it's not really incompatible with --no-splice.
I will update it to --splice-only
Maybe users just want to get a disconnected namespace for whatever reason ('pasta' is shorter to type than 'unshare -rUn').
+ if (*c->ip4.ifname_out || *c->ip6.ifname_out) + die("--no-tap is incompatible with --outbound-if4/6"); + if (*c->pasta_ifn) + die("--no-tap is incompatible with --ns-ifname"); + if (*c->guest_mac) + die("--no-tap is incompatible with --ns-mac-addr"); + if (c->pasta_conf_ns) + die("--no-tap is incompatible with --config-net");
I guess all these checks are to save some checks later, which looks like a good reason to have them here.
If not, though, I don't think we *really* need to tell the user that --ns-ifname will be ignored with --no-tap.
One thing that might confuse users, though, is this:
$ ./pasta --no-tap --mtu 1500 -- ip l 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 or even this:
$ ./pasta --no-tap -a 192.0.2.1 -- ip a 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host proto kernel_lo valid_lft forever preferred_lft forever but I would rather *not* add conditions and checks for those even if there's a *slight* potential for confusion, otherwise this becomes really long. And it's really not worth it, I think.
Then I guess we only need the c->no_splice check, right?
...maybe? About *needing*, yes, I guess so, but if other checks save more checks later, I would keep them.
+ + c->host_lo_to_ns_lo = 1; + c->no_icmp = 1; + c->no_ra = 1; + c->no_dns = 1; + c->no_dns_search = 1; + } + if (!ifi4 && *c->ip4.ifname_out) ifi4 = if_nametoindex(c->ip4.ifname_out);
@@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) log_conf_parsed = true; /* Stop printing everything */
nl_sock_init(c, false); - if (!v6_only) + if (!v6_only && !c->no_tap) c->ifi4 = conf_ip4(ifi4, &c->ip4); - if (!v4_only) + if (!v4_only && !c->no_tap) c->ifi6 = conf_ip6(ifi6, &c->ip6);
if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) (*c->ip6.ifname_out && !c->ifi6)) die("External interface not usable");
- if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) {
You already checked that !*c->pasta_ifn above.
I guess the check above (aka. if (*c->pasta_ifn && c->no_tap)) doesn't affect this one? If c->pasta_ifn is assigned, we won't come to the check !c->no_tap here. Otherwise, we do need to check !c->no_tap.
Right, but you don't care about resetting c->pasta_ifn to the default value if !c->no_tap, because in that case you know that c->pasta_ifn wasn't set, so you can happily override it.
I just realized that you probably meant when c->no_tap is set. Actually it would affect conf_print, info("Namespace interface: %s", c->pasta_ifn). But I will add a condition about c->splice_only before this line, so yes, it doesn't matter whether reset it or not. I will remove the check in v2.
I'm not sure I fully understand it. If !c->no_tap, the condition is the same as before without this patch, which is to not reset it if it's specified in cmd line. We won't know if c->pasta_ifn is set until this check, do we?
Let's assume !c->ifi4 && !c->ifi6. Then we have 2 variables and 2^2 possible cases: 1. !*c->pasta_ifn && !c->no_tap: we need to override c->pasta_ifn 2. !*c->pasta_ifn && c->no_tap: we don't need to override c->pasta_ifn, *but it's harmless if we do* 3. *c->pasta_ifn && !c->no_tap: we must not override c->pasta_ifn 4. *c->pasta_ifn && c->no_tap: we must not override c->pasta_ifn Now, if we make 1. and 2. the same and decide to override c->pasta_ifn also in case 2. (when it's not necessary, but harmless), 1. and 2. as well as 3. and 4. are pairwise the same, so you don't strictly need to add a condition on c->no_tap, I think. On the other hand... if it's obvious just to me, maybe it's actually simpler to keep the check. :) I realise that my observation is not as clear as I initially thought. -- Stefano
On Wed, Jan 14, 2026 at 6:00 PM Stefano Brivio
On Wed, 14 Jan 2026 15:28:31 +0800 Yumei Huang
wrote: On Wed, Jan 14, 2026 at 2:31 PM Yumei Huang
wrote: On Wed, Jan 14, 2026 at 7:34 AM Stefano Brivio
wrote: On Tue, 13 Jan 2026 19:20:47 +0800 Yumei Huang
wrote: On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
wrote: On Mon, 29 Dec 2025 17:55:58 +0800 Yumei Huang
wrote: > This patch introduces a mode where we only forward loopback connections > and traffic between two namespaces (via the loopback interface, 'lo'), > without a tap device. > > With this, podman can support forwarding ::1 in custom networks when using > rootlesskit for forwarding ports. > > In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically > enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, > --config-net, --outbound-if4/6) are rejected. > > Link: https://bugs.passt.top/show_bug.cgi?id=149 > Signed-off-by: Yumei Huang
> --- > conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- > fwd.c | 3 +++ > passt.1 | 5 +++++ > passt.h | 2 ++ > pasta.c | 3 +++ > tap.c | 11 +++++++---- > 6 files changed, 61 insertions(+), 19 deletions(-) > > diff --git a/conf.c b/conf.c > index 84ae12b..353d0a5 100644 > --- a/conf.c > +++ b/conf.c > @@ -1049,7 +1049,8 @@ pasta_opts: > " --no-copy-addrs DEPRECATED:\n" > " 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"); > + " --no-splice Disable inbound socket splicing\n" > + " --no-tap Don't create tap device\n"); > > passt_exit(status); > } > @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) > {"no-ndp", no_argument, &c->no_ndp, 1 }, > {"no-ra", no_argument, &c->no_ra, 1 }, > {"no-splice", no_argument, &c->no_splice, 1 }, > + {"no-tap", no_argument, &c->no_tap, 1 }, > {"freebind", no_argument, &c->freebind, 1 }, > {"no-map-gw", no_argument, &no_map_gw, 1 }, > {"ipv4-only", no_argument, NULL, '4' }, > @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) > } > } while (name != -1); > > - if (c->mode != MODE_PASTA) > + if (c->mode != MODE_PASTA) { > c->no_splice = 1; > + if (c->no_tap) > + die("--no-tap is for pasta mode only"); > + } > > if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { > if (copy_routes_opt) > @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) > die("--no-copy-addrs needs --config-net"); > } > > + if (c->mode == MODE_PASTA && c->no_tap) { > + if (c->no_splice) > + die("--no-tap is incompatible with --no-splice"); I'm not sure if you need this for other reasons, but as long as it's called --no-tap, it's not really incompatible with --no-splice.
I will update it to --splice-only
Maybe users just want to get a disconnected namespace for whatever reason ('pasta' is shorter to type than 'unshare -rUn').
> + if (*c->ip4.ifname_out || *c->ip6.ifname_out) > + die("--no-tap is incompatible with --outbound-if4/6"); > + if (*c->pasta_ifn) > + die("--no-tap is incompatible with --ns-ifname"); > + if (*c->guest_mac) > + die("--no-tap is incompatible with --ns-mac-addr"); > + if (c->pasta_conf_ns) > + die("--no-tap is incompatible with --config-net");
I guess all these checks are to save some checks later, which looks like a good reason to have them here.
If not, though, I don't think we *really* need to tell the user that --ns-ifname will be ignored with --no-tap.
One thing that might confuse users, though, is this:
$ ./pasta --no-tap --mtu 1500 -- ip l 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 or even this:
$ ./pasta --no-tap -a 192.0.2.1 -- ip a 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host proto kernel_lo valid_lft forever preferred_lft forever but I would rather *not* add conditions and checks for those even if there's a *slight* potential for confusion, otherwise this becomes really long. And it's really not worth it, I think.
Then I guess we only need the c->no_splice check, right?
...maybe? About *needing*, yes, I guess so, but if other checks save more checks later, I would keep them.
> + > + c->host_lo_to_ns_lo = 1; > + c->no_icmp = 1; > + c->no_ra = 1; > + c->no_dns = 1; > + c->no_dns_search = 1; > + } > + > if (!ifi4 && *c->ip4.ifname_out) > ifi4 = if_nametoindex(c->ip4.ifname_out); > > @@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) > log_conf_parsed = true; /* Stop printing everything */ > > nl_sock_init(c, false); > - if (!v6_only) > + if (!v6_only && !c->no_tap) > c->ifi4 = conf_ip4(ifi4, &c->ip4); > - if (!v4_only) > + if (!v4_only && !c->no_tap) > c->ifi6 = conf_ip6(ifi6, &c->ip6); > > if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { > @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) > (*c->ip6.ifname_out && !c->ifi6)) > die("External interface not usable"); > > - if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { > + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) {
You already checked that !*c->pasta_ifn above.
I guess the check above (aka. if (*c->pasta_ifn && c->no_tap)) doesn't affect this one? If c->pasta_ifn is assigned, we won't come to the check !c->no_tap here. Otherwise, we do need to check !c->no_tap.
Right, but you don't care about resetting c->pasta_ifn to the default value if !c->no_tap, because in that case you know that c->pasta_ifn wasn't set, so you can happily override it.
I just realized that you probably meant when c->no_tap is set. Actually it would affect conf_print, info("Namespace interface: %s", c->pasta_ifn). But I will add a condition about c->splice_only before this line, so yes, it doesn't matter whether reset it or not. I will remove the check in v2.
I'm not sure I fully understand it. If !c->no_tap, the condition is the same as before without this patch, which is to not reset it if it's specified in cmd line. We won't know if c->pasta_ifn is set until this check, do we?
Let's assume !c->ifi4 && !c->ifi6. Then we have 2 variables and 2^2 possible cases:
1. !*c->pasta_ifn && !c->no_tap: we need to override c->pasta_ifn
2. !*c->pasta_ifn && c->no_tap: we don't need to override c->pasta_ifn, *but it's harmless if we do*
This will lead conf_print to print "Namespace interface: tap0" which is not correct. But I plan to add a check with c->no_tap in conf_print, so it won't be a problem.
3. *c->pasta_ifn && !c->no_tap: we must not override c->pasta_ifn
4. *c->pasta_ifn && c->no_tap: we must not override c->pasta_ifn
Now, if we make 1. and 2. the same and decide to override c->pasta_ifn also in case 2. (when it's not necessary, but harmless), 1. and 2. as well as 3. and 4. are pairwise the same, so you don't strictly need to add a condition on c->no_tap, I think.
On the other hand... if it's obvious just to me, maybe it's actually simpler to keep the check. :) I realise that my observation is not as clear as I initially thought.
-- Stefano
-- Thanks, Yumei Huang
On Wed, 14 Jan 2026 18:35:18 +0800
Yumei Huang
On Wed, Jan 14, 2026 at 6:00 PM Stefano Brivio
wrote: On Wed, 14 Jan 2026 15:28:31 +0800 Yumei Huang
wrote: On Wed, Jan 14, 2026 at 2:31 PM Yumei Huang
wrote: On Wed, Jan 14, 2026 at 7:34 AM Stefano Brivio
wrote: On Tue, 13 Jan 2026 19:20:47 +0800 Yumei Huang
wrote: On Sun, Jan 11, 2026 at 2:12 AM Stefano Brivio
wrote: > > On Mon, 29 Dec 2025 17:55:58 +0800 > Yumei Huang wrote: > > > This patch introduces a mode where we only forward loopback connections > > and traffic between two namespaces (via the loopback interface, 'lo'), > > without a tap device. > > > > With this, podman can support forwarding ::1 in custom networks when using > > rootlesskit for forwarding ports. > > > > In --no-tap mode, --host-lo-to-ns-lo, --no-icmp and --no-ra is automatically > > enabled. Options requiring a tap device (--ns-ifname, --ns-mac-addr, > > --config-net, --outbound-if4/6) are rejected. > > > > Link: https://bugs.passt.top/show_bug.cgi?id=149 > > Signed-off-by: Yumei Huang > > --- > > conf.c | 56 +++++++++++++++++++++++++++++++++++++++++--------------- > > fwd.c | 3 +++ > > passt.1 | 5 +++++ > > passt.h | 2 ++ > > pasta.c | 3 +++ > > tap.c | 11 +++++++---- > > 6 files changed, 61 insertions(+), 19 deletions(-) > > > > diff --git a/conf.c b/conf.c > > index 84ae12b..353d0a5 100644 > > --- a/conf.c > > +++ b/conf.c > > @@ -1049,7 +1049,8 @@ pasta_opts: > > " --no-copy-addrs DEPRECATED:\n" > > " 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"); > > + " --no-splice Disable inbound socket splicing\n" > > + " --no-tap Don't create tap device\n"); > > > > passt_exit(status); > > } > > @@ -1451,6 +1452,7 @@ void conf(struct ctx *c, int argc, char **argv) > > {"no-ndp", no_argument, &c->no_ndp, 1 }, > > {"no-ra", no_argument, &c->no_ra, 1 }, > > {"no-splice", no_argument, &c->no_splice, 1 }, > > + {"no-tap", no_argument, &c->no_tap, 1 }, > > {"freebind", no_argument, &c->freebind, 1 }, > > {"no-map-gw", no_argument, &no_map_gw, 1 }, > > {"ipv4-only", no_argument, NULL, '4' }, > > @@ -1947,8 +1949,11 @@ void conf(struct ctx *c, int argc, char **argv) > > } > > } while (name != -1); > > > > - if (c->mode != MODE_PASTA) > > + if (c->mode != MODE_PASTA) { > > c->no_splice = 1; > > + if (c->no_tap) > > + die("--no-tap is for pasta mode only"); > > + } > > > > if (c->mode == MODE_PASTA && !c->pasta_conf_ns) { > > if (copy_routes_opt) > > @@ -1957,6 +1962,25 @@ void conf(struct ctx *c, int argc, char **argv) > > die("--no-copy-addrs needs --config-net"); > > } > > > > + if (c->mode == MODE_PASTA && c->no_tap) { > > + if (c->no_splice) > > + die("--no-tap is incompatible with --no-splice"); > > I'm not sure if you need this for other reasons, but as long as it's > called --no-tap, it's not really incompatible with --no-splice. I will update it to --splice-only
> > Maybe users just want to get a disconnected namespace for whatever > reason ('pasta' is shorter to type than 'unshare -rUn'). > > > + if (*c->ip4.ifname_out || *c->ip6.ifname_out) > > + die("--no-tap is incompatible with --outbound-if4/6"); > > + if (*c->pasta_ifn) > > + die("--no-tap is incompatible with --ns-ifname"); > > + if (*c->guest_mac) > > + die("--no-tap is incompatible with --ns-mac-addr"); > > + if (c->pasta_conf_ns) > > + die("--no-tap is incompatible with --config-net"); > > I guess all these checks are to save some checks later, which looks like > a good reason to have them here. > > If not, though, I don't think we *really* need to tell the user that > --ns-ifname will be ignored with --no-tap. > > One thing that might confuse users, though, is this: > > $ ./pasta --no-tap --mtu 1500 -- ip l > 1: lo:
mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 > link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 > > or even this: > > $ ./pasta --no-tap -a 192.0.2.1 -- ip a > 1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 > link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 > inet 127.0.0.1/8 scope host lo > valid_lft forever preferred_lft forever > inet6 ::1/128 scope host proto kernel_lo > valid_lft forever preferred_lft forever > > but I would rather *not* add conditions and checks for those even if > there's a *slight* potential for confusion, otherwise this becomes > really long. And it's really not worth it, I think. Then I guess we only need the c->no_splice check, right?
...maybe? About *needing*, yes, I guess so, but if other checks save more checks later, I would keep them.
> > + > > + c->host_lo_to_ns_lo = 1; > > + c->no_icmp = 1; > > + c->no_ra = 1; > > + c->no_dns = 1; > > + c->no_dns_search = 1; > > + } > > + > > if (!ifi4 && *c->ip4.ifname_out) > > ifi4 = if_nametoindex(c->ip4.ifname_out); > > > > @@ -1980,9 +2004,9 @@ void conf(struct ctx *c, int argc, char **argv) > > log_conf_parsed = true; /* Stop printing everything */ > > > > nl_sock_init(c, false); > > - if (!v6_only) > > + if (!v6_only && !c->no_tap) > > c->ifi4 = conf_ip4(ifi4, &c->ip4); > > - if (!v4_only) > > + if (!v4_only && !c->no_tap) > > c->ifi6 = conf_ip6(ifi6, &c->ip6); > > > > if (c->ifi4 && c->mtu < IPV4_MIN_MTU) { > > @@ -1998,30 +2022,32 @@ void conf(struct ctx *c, int argc, char **argv) > > (*c->ip6.ifname_out && !c->ifi6)) > > die("External interface not usable"); > > > > - if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn) { > > + if (!c->ifi4 && !c->ifi6 && !*c->pasta_ifn && !c->no_tap) { > > You already checked that !*c->pasta_ifn above.
I guess the check above (aka. if (*c->pasta_ifn && c->no_tap)) doesn't affect this one? If c->pasta_ifn is assigned, we won't come to the check !c->no_tap here. Otherwise, we do need to check !c->no_tap.
Right, but you don't care about resetting c->pasta_ifn to the default value if !c->no_tap, because in that case you know that c->pasta_ifn wasn't set, so you can happily override it.
I just realized that you probably meant when c->no_tap is set. Actually it would affect conf_print, info("Namespace interface: %s", c->pasta_ifn). But I will add a condition about c->splice_only before this line, so yes, it doesn't matter whether reset it or not. I will remove the check in v2.
I'm not sure I fully understand it. If !c->no_tap, the condition is the same as before without this patch, which is to not reset it if it's specified in cmd line. We won't know if c->pasta_ifn is set until this check, do we?
Let's assume !c->ifi4 && !c->ifi6. Then we have 2 variables and 2^2 possible cases:
1. !*c->pasta_ifn && !c->no_tap: we need to override c->pasta_ifn
2. !*c->pasta_ifn && c->no_tap: we don't need to override c->pasta_ifn, *but it's harmless if we do*
This will lead conf_print to print "Namespace interface: tap0" which is not correct.
Well, but right now it's printed unconditionally, regardless of whether that's tap0 or not, so that doesn't make it more wrong.
But I plan to add a check with c->no_tap in conf_print, so it won't be a problem.
Right... but that's needed anyway, that was my point. -- Stefano
participants (4)
-
David Gibson
-
Paul Holzinger
-
Stefano Brivio
-
Yumei Huang