confd: allow both ends of a veth pair to be assigned to containers

Previously at least one end of a veth pair had to remain in the host
namespace, because that end created and destroyed the pair.  Assigning
both ends to containers left no one to create it.

Select a deterministic primary end so exactly one side creates the pair.
When the primary is itself a container interface, create the pair in the
host namespace before the container starts; CNI host-device then moves
each end into its container.  Teardown is deferred to the container
removal script so the pair does not linger and block re-creation.

Drop the now-obsolete limitation notes from the documentation and YANG,
and add a regression test connecting two containers over a veth pair.

Fixes: #941

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-06-20 19:26:22 +02:00
parent 0229057612
commit 674fd6c396
14 changed files with 241 additions and 28 deletions
+10
View File
@@ -414,6 +414,16 @@ int cni_netdag_gen_iface(struct dagger *net, const char *ifname,
return -EIO;
fprintf(fp, "container -a -f delete network %s >/dev/null\n", ifname);
/* If this end belongs to a veth pair, the kernel keeps the pair
* alive after CNI host-device returns the interface to the host
* namespace. Remove it here, once the container is gone, so the
* pair does not linger and block a later re-creation. Tolerant:
* the peer's teardown may already have removed it.
*/
if (lydx_get_child(dif, "veth"))
fprintf(fp, "ip link del dev %s 2>/dev/null || true\n", ifname);
fclose(fp);
if (cni_type == IFT_BRIDGE)
+15 -10
View File
@@ -21,23 +21,28 @@
bool veth_is_primary(struct lyd_node *cif)
{
struct lyd_node *peer, *veth;
bool self_cni, peer_cni;
const char *peername;
veth = lydx_get_child(cif, "veth");
peername = lydx_get_cattr(veth, "peer");
peer = lydx_find_by_name(lyd_parent(cif), "interface", peername);
/* At the moment, CNI code relies on one side of the pair
* remaining in the host namespace, and that that interface
* takes care of creating the pair.
*/
if (lydx_get_child(cif, "container-network"))
return false;
if (lydx_get_child(peer, "container-network"))
return true;
self_cni = lydx_get_child(cif, "container-network") != NULL;
peer_cni = lydx_get_child(peer, "container-network") != NULL;
return strcmp(lydx_get_cattr(cif, "name"),
lydx_get_cattr(veth, "peer")) < 0;
/* When exactly one end is handed to a container (CNI host-device),
* the other end stays in the host namespace and creates the pair.
*/
if (self_cni != peer_cni)
return peer_cni;
/* Neither or both ends are container interfaces: pick a stable
* primary by name so exactly one end creates the pair. When both
* ends are containers the pair is still created in the host
* namespace first, then moved into each container by CNI host-device.
*/
return strcmp(lydx_get_cattr(cif, "name"), peername) < 0;
}
int ifchange_cand_infer_veth(sr_session_ctx_t *session, const char *path)
+36 -1
View File
@@ -518,6 +518,14 @@ static int veth_gen_del(struct lyd_node *dif, FILE *sh)
if (!veth_is_primary(dif))
return 0;
/* When the primary end is itself a container interface it currently
* lives in the container's namespace, so a host-namespace delete here
* would fail and abort the teardown. Its removal is handled after the
* container is gone, see cni_netdag_gen_iface().
*/
if (lydx_get_child(dif, "container-network"))
return 0;
return link_gen_del(dif, sh);
}
@@ -571,6 +579,28 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif,
return 0;
}
/*
* Both ends of a veth pair can be handed to containers, leaving no
* host-side interface to create the pair. Have the primary end create it
* in the host namespace early (NETDAG_INIT_PHYS, before the container is
* (re)started); CNI host-device then moves each end into its container.
*/
static int veth_gen_host(struct dagger *net, struct lyd_node *dif, struct lyd_node *cif)
{
const char *ifname = lydx_get_cattr(cif, "name");
FILE *ip;
int err;
ip = dagger_fopen_net_init(net, ifname, NETDAG_INIT_PHYS, "init.ip");
if (!ip)
return -EIO;
err = veth_gen(dif, cif, ip);
fclose(ip);
return err;
}
static sr_error_t netdag_gen_iface_timeout(struct dagger *net, const char *ifname, const char *iftype)
{
if (!strcmp(iftype, "infix-if-type:ethernet")) {
@@ -604,8 +634,13 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net
if ((err = cni_netdag_gen_iface(net, ifname, dif, cif))) {
/* error or managed by CNI/podman */
if (err > 0)
if (err > 0) {
err = 0; /* done, nothing more to do here */
if (op == LYDX_OP_CREATE && lydx_get_child(cif, "veth") &&
veth_is_primary(cif))
err = veth_gen_host(net, dif, cif);
}
goto err;
}
+1 -5
View File
@@ -59,11 +59,7 @@ submodule infix-if-container {
identity host {
base container-network;
description "Host device, e.g., one end of a VETH pair or other host interface.
Note: When using VETH pairs, at least one side must remain in the
host namespace. Both ends of a VETH pair cannot be assigned to
different containers.";
description "Host device, e.g., one end of a VETH pair or other host interface.";
}
/*
+1 -5
View File
@@ -13,11 +13,7 @@ submodule infix-if-veth {
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Linux virtual Ethernet pair extension for ietf-interfaces.
Note: When using VETH pairs with containers, at least one side
of the pair must remain in the host namespace. Both ends of a
VETH pair cannot be assigned to different containers.";
description "Linux virtual Ethernet pair extension for ietf-interfaces.";
revision 2023-06-05 {
description "Initial revision.";