diff options
| author | Jakub Kicinski <kuba@kernel.org> | 2026-06-13 04:31:35 +0300 |
|---|---|---|
| committer | Jakub Kicinski <kuba@kernel.org> | 2026-06-13 04:31:36 +0300 |
| commit | 5184fd34214fffa6316cee4f90179cf43d956c39 (patch) | |
| tree | 9d5bab29de411dfb347b7a3293942e0a94726904 | |
| parent | eee2d0676d141f7b5c8227895fa8a6467502ab21 (diff) | |
| parent | 50d3bdfb84c88408934f75430d0e3d2baa4f5d7a (diff) | |
| download | linux-5184fd34214fffa6316cee4f90179cf43d956c39.tar.xz | |
Merge branch 'psp-add-support-for-dev-assoc-disassoc'
Wei Wang says:
====================
psp: Add support for dev-assoc/disassoc
The main purpose of this feature is to associate virtual devices like
veth or netkit with a real PSP device, so we could provide PSP
functionality to the application running with virtual devices.
A typical deployment that works with this feature is as follows:
Host Namespace:
psp_dev_local ←──physically linked──→ psp_dev_peer
(PSP device)
│
│ BPF on psp_dev_local ingress: bpf_redirect_peer() to nk_guest
│
nk_host / veth_host
│
│ BPF on nk_host ingress: bpf_redirect_neigh() to psp_dev_local
│
Guest Namespace (netns):
│
nk_guest / veth_guest
★ PSP application run here
Remote Namespace (_netns):
psp_dev_peer
★ PSP server application runs here
Note:
The general requirement for this feature to work:
For PSP to work correctly, the egress device at validate_xmit_skb()
time must have psp_dev matching the association's psd. Any device
stacking or traffic redirection that changes the egress device will
cause either:
1. TX validation failure (SKB_DROP_REASON_PSP_OUTPUT) - fail-safe
2. RX policy failure after tx-assoc - packets without PSP extension
are rejected by receiver expecting encrypted traffic
Here are a few examples that this feature would not work:
- Bonding with load balancing in round-robin, XOR, 802.3ad mode across
multiple PSP devices, or mixed PSP and non-PSP devices
- Bonding with active-backup mode might work without PSP migration for
failover case.
- ipvlan/macvlan in bridge mode would not work given packets are
loopbacked locally without going through the PSP device.
====================
Link: https://patch.msgid.link/20260608233118.2694144-1-weibunny.kernel@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
| -rw-r--r-- | Documentation/netlink/specs/psp.yaml | 73 | ||||
| -rw-r--r-- | include/net/psp/types.h | 23 | ||||
| -rw-r--r-- | include/uapi/linux/psp.h | 13 | ||||
| -rw-r--r-- | net/psp/psp-nl-gen.c | 36 | ||||
| -rw-r--r-- | net/psp/psp-nl-gen.h | 7 | ||||
| -rw-r--r-- | net/psp/psp.h | 4 | ||||
| -rw-r--r-- | net/psp/psp_main.c | 97 | ||||
| -rw-r--r-- | net/psp/psp_nl.c | 387 | ||||
| -rw-r--r-- | tools/testing/selftests/drivers/net/config | 1 | ||||
| -rwxr-xr-x | tools/testing/selftests/drivers/net/hw/nk_qlease.py | 4 | ||||
| -rw-r--r-- | tools/testing/selftests/drivers/net/lib/py/env.py | 50 | ||||
| -rwxr-xr-x | tools/testing/selftests/drivers/net/psp.py | 423 |
12 files changed, 1041 insertions, 77 deletions
diff --git a/Documentation/netlink/specs/psp.yaml b/Documentation/netlink/specs/psp.yaml index bfcd6e4ecb85..e9c2ee7e28e0 100644 --- a/Documentation/netlink/specs/psp.yaml +++ b/Documentation/netlink/specs/psp.yaml @@ -14,6 +14,17 @@ definitions: attribute-sets: - + name: assoc-dev-info + attributes: + - + name: ifindex + doc: ifindex of an associated network device. + type: u32 + - + name: nsid + doc: Network namespace ID of the associated device. + type: s32 + - name: dev attributes: - @@ -24,7 +35,9 @@ attribute-sets: min: 1 - name: ifindex - doc: ifindex of the main netdevice linked to the PSP device. + doc: | + ifindex of the main netdevice linked to the PSP device, + or the ifindex to associate with the PSP device. type: u32 - name: psp-versions-cap @@ -38,6 +51,28 @@ attribute-sets: type: u32 enum: version enum-as-flags: true + - + name: assoc-list + doc: List of associated virtual devices. + type: nest + nested-attributes: assoc-dev-info + multi-attr: true + - + name: nsid + doc: | + Network namespace ID for the device to associate/disassociate. + Optional for dev-assoc and dev-disassoc; if not present, the + device is looked up in the caller's network namespace. + type: s32 + - + name: by-association + doc: | + Flag indicating the PSP device is an associated device from a + different network namespace. + Present when in associated namespace, absent when in primary/host + namespace. + type: flag + - name: assoc attributes: @@ -170,6 +205,8 @@ operations: - ifindex - psp-versions-cap - psp-versions-ena + - assoc-list + - by-association pre: psp-device-get-locked post: psp-device-unlock dump: @@ -196,7 +233,7 @@ operations: - psp-versions-ena reply: attributes: [] - pre: psp-device-get-locked + pre: psp-device-get-locked-admin post: psp-device-unlock - name: dev-change-ntf @@ -216,7 +253,7 @@ operations: reply: attributes: - id - pre: psp-device-get-locked + pre: psp-device-get-locked-admin post: psp-device-unlock - name: key-rotate-ntf @@ -281,6 +318,36 @@ operations: post: psp-device-unlock dump: reply: *stats-all + - + name: dev-assoc + doc: Associate a network device with a PSP device. + attribute-set: dev + flags: [admin-perm] + do: + request: + attributes: + - id + - ifindex + - nsid + reply: + attributes: [] + pre: psp-device-get-locked-dev-assoc + post: psp-device-unlock + - + name: dev-disassoc + doc: Disassociate a network device from a PSP device. + attribute-set: dev + flags: [admin-perm] + do: + request: + attributes: + - id + - ifindex + - nsid + reply: + attributes: [] + pre: psp-device-get-locked + post: psp-device-unlock mcast-groups: list: diff --git a/include/net/psp/types.h b/include/net/psp/types.h index 25a9096d4e7d..87991a1ea02d 100644 --- a/include/net/psp/types.h +++ b/include/net/psp/types.h @@ -5,6 +5,7 @@ #include <linux/mutex.h> #include <linux/refcount.h> +#include <net/net_trackers.h> struct netlink_ext_ack; @@ -43,9 +44,29 @@ struct psp_dev_config { u32 versions; }; +/* Max number of devices that can be associated with a single PSP device. + * Each entry consumes ~24 bytes in the netlink dev-get response, and the + * response must fit in GENLMSG_DEFAULT_SIZE (~3.7KB). + */ +#define PSP_ASSOC_DEV_MAX 128 + +/** + * struct psp_assoc_dev - wrapper for associated net_device + * @dev_list: list node for psp_dev::assoc_dev_list + * @assoc_dev: the associated net_device + * @dev_tracker: tracker for the net_device reference + */ +struct psp_assoc_dev { + struct list_head dev_list; + struct net_device *assoc_dev; + netdevice_tracker dev_tracker; +}; + /** * struct psp_dev - PSP device struct * @main_netdev: original netdevice of this PSP device + * @assoc_dev_list: list of psp_assoc_dev entries associated with this PSP device + * @assoc_dev_cnt: number of entries in @assoc_dev_list * @ops: driver callbacks * @caps: device capabilities * @drv_priv: driver priv pointer @@ -67,6 +88,8 @@ struct psp_dev_config { */ struct psp_dev { struct net_device *main_netdev; + struct list_head assoc_dev_list; + int assoc_dev_cnt; struct psp_dev_ops *ops; struct psp_dev_caps *caps; diff --git a/include/uapi/linux/psp.h b/include/uapi/linux/psp.h index a3a336488dc3..1c8899cd4da5 100644 --- a/include/uapi/linux/psp.h +++ b/include/uapi/linux/psp.h @@ -18,10 +18,21 @@ enum psp_version { }; enum { + PSP_A_ASSOC_DEV_INFO_IFINDEX = 1, + PSP_A_ASSOC_DEV_INFO_NSID, + + __PSP_A_ASSOC_DEV_INFO_MAX, + PSP_A_ASSOC_DEV_INFO_MAX = (__PSP_A_ASSOC_DEV_INFO_MAX - 1) +}; + +enum { PSP_A_DEV_ID = 1, PSP_A_DEV_IFINDEX, PSP_A_DEV_PSP_VERSIONS_CAP, PSP_A_DEV_PSP_VERSIONS_ENA, + PSP_A_DEV_ASSOC_LIST, + PSP_A_DEV_NSID, + PSP_A_DEV_BY_ASSOCIATION, __PSP_A_DEV_MAX, PSP_A_DEV_MAX = (__PSP_A_DEV_MAX - 1) @@ -74,6 +85,8 @@ enum { PSP_CMD_RX_ASSOC, PSP_CMD_TX_ASSOC, PSP_CMD_GET_STATS, + PSP_CMD_DEV_ASSOC, + PSP_CMD_DEV_DISASSOC, __PSP_CMD_MAX, PSP_CMD_MAX = (__PSP_CMD_MAX - 1) diff --git a/net/psp/psp-nl-gen.c b/net/psp/psp-nl-gen.c index 953309952cef..0e426ffac398 100644 --- a/net/psp/psp-nl-gen.c +++ b/net/psp/psp-nl-gen.c @@ -53,6 +53,20 @@ static const struct nla_policy psp_get_stats_nl_policy[PSP_A_STATS_DEV_ID + 1] = [PSP_A_STATS_DEV_ID] = NLA_POLICY_MIN(NLA_U32, 1), }; +/* PSP_CMD_DEV_ASSOC - do */ +static const struct nla_policy psp_dev_assoc_nl_policy[PSP_A_DEV_NSID + 1] = { + [PSP_A_DEV_ID] = NLA_POLICY_MIN(NLA_U32, 1), + [PSP_A_DEV_IFINDEX] = { .type = NLA_U32, }, + [PSP_A_DEV_NSID] = { .type = NLA_S32, }, +}; + +/* PSP_CMD_DEV_DISASSOC - do */ +static const struct nla_policy psp_dev_disassoc_nl_policy[PSP_A_DEV_NSID + 1] = { + [PSP_A_DEV_ID] = NLA_POLICY_MIN(NLA_U32, 1), + [PSP_A_DEV_IFINDEX] = { .type = NLA_U32, }, + [PSP_A_DEV_NSID] = { .type = NLA_S32, }, +}; + /* Ops table for psp */ static const struct genl_split_ops psp_nl_ops[] = { { @@ -71,7 +85,7 @@ static const struct genl_split_ops psp_nl_ops[] = { }, { .cmd = PSP_CMD_DEV_SET, - .pre_doit = psp_device_get_locked, + .pre_doit = psp_device_get_locked_admin, .doit = psp_nl_dev_set_doit, .post_doit = psp_device_unlock, .policy = psp_dev_set_nl_policy, @@ -80,7 +94,7 @@ static const struct genl_split_ops psp_nl_ops[] = { }, { .cmd = PSP_CMD_KEY_ROTATE, - .pre_doit = psp_device_get_locked, + .pre_doit = psp_device_get_locked_admin, .doit = psp_nl_key_rotate_doit, .post_doit = psp_device_unlock, .policy = psp_key_rotate_nl_policy, @@ -119,6 +133,24 @@ static const struct genl_split_ops psp_nl_ops[] = { .dumpit = psp_nl_get_stats_dumpit, .flags = GENL_CMD_CAP_DUMP, }, + { + .cmd = PSP_CMD_DEV_ASSOC, + .pre_doit = psp_device_get_locked_dev_assoc, + .doit = psp_nl_dev_assoc_doit, + .post_doit = psp_device_unlock, + .policy = psp_dev_assoc_nl_policy, + .maxattr = PSP_A_DEV_NSID, + .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO, + }, + { + .cmd = PSP_CMD_DEV_DISASSOC, + .pre_doit = psp_device_get_locked, + .doit = psp_nl_dev_disassoc_doit, + .post_doit = psp_device_unlock, + .policy = psp_dev_disassoc_nl_policy, + .maxattr = PSP_A_DEV_NSID, + .flags = GENL_ADMIN_PERM | GENL_CMD_CAP_DO, + }, }; static const struct genl_multicast_group psp_nl_mcgrps[] = { diff --git a/net/psp/psp-nl-gen.h b/net/psp/psp-nl-gen.h index 599c5f1c82f2..24d51bff997f 100644 --- a/net/psp/psp-nl-gen.h +++ b/net/psp/psp-nl-gen.h @@ -17,8 +17,13 @@ extern const struct nla_policy psp_keys_nl_policy[PSP_A_KEYS_SPI + 1]; int psp_device_get_locked(const struct genl_split_ops *ops, struct sk_buff *skb, struct genl_info *info); +int psp_device_get_locked_admin(const struct genl_split_ops *ops, + struct sk_buff *skb, struct genl_info *info); int psp_assoc_device_get_locked(const struct genl_split_ops *ops, struct sk_buff *skb, struct genl_info *info); +int psp_device_get_locked_dev_assoc(const struct genl_split_ops *ops, + struct sk_buff *skb, + struct genl_info *info); void psp_device_unlock(const struct genl_split_ops *ops, struct sk_buff *skb, struct genl_info *info); @@ -31,6 +36,8 @@ int psp_nl_rx_assoc_doit(struct sk_buff *skb, struct genl_info *info); int psp_nl_tx_assoc_doit(struct sk_buff *skb, struct genl_info *info); int psp_nl_get_stats_doit(struct sk_buff *skb, struct genl_info *info); int psp_nl_get_stats_dumpit(struct sk_buff *skb, struct netlink_callback *cb); +int psp_nl_dev_assoc_doit(struct sk_buff *skb, struct genl_info *info); +int psp_nl_dev_disassoc_doit(struct sk_buff *skb, struct genl_info *info); enum { PSP_NLGRP_MGMT, diff --git a/net/psp/psp.h b/net/psp/psp.h index 9f19137593a0..86eeba823ced 100644 --- a/net/psp/psp.h +++ b/net/psp/psp.h @@ -14,7 +14,9 @@ extern struct xarray psp_devs; extern struct mutex psp_devs_lock; void psp_dev_free(struct psp_dev *psd); -int psp_dev_check_access(struct psp_dev *psd, struct net *net); +int psp_dev_check_access(struct psp_dev *psd, struct net *net, bool admin); +bool psp_has_assoc_dev_in_ns(struct psp_dev *psd, struct net *net); +int psp_attach_netdev_notifier(void); void psp_nl_notify_dev(struct psp_dev *psd, u32 cmd); diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c index ccbbb2a5fa58..8b2f178e317c 100644 --- a/net/psp/psp_main.c +++ b/net/psp/psp_main.c @@ -27,13 +27,22 @@ struct mutex psp_devs_lock; * psp_dev_check_access() - check if user in a given net ns can access PSP dev * @psd: PSP device structure user is trying to access * @net: net namespace user is in + * @admin: If true, only allow access from @psd's main device's netns, + * for admin operations like config changes and key rotation. + * If false, also allow access from network namespaces that have + * an associated device with @psd, for read-only and association + * management operations. * * Return: 0 if PSP device should be visible in @net, errno otherwise. */ -int psp_dev_check_access(struct psp_dev *psd, struct net *net) +int psp_dev_check_access(struct psp_dev *psd, struct net *net, bool admin) { if (dev_net(psd->main_netdev) == net) return 0; + + if (!admin && psp_has_assoc_dev_in_ns(psd, net)) + return 0; + return -ENOENT; } @@ -69,6 +78,7 @@ psp_dev_create(struct net_device *netdev, return ERR_PTR(-ENOMEM); psd->main_netdev = netdev; + INIT_LIST_HEAD(&psd->assoc_dev_list); psd->ops = psd_ops; psd->caps = psd_caps; psd->drv_priv = priv_ptr; @@ -120,6 +130,7 @@ void psp_dev_free(struct psp_dev *psd) */ void psp_dev_unregister(struct psp_dev *psd) { + struct psp_assoc_dev *entry, *entry_tmp; struct psp_assoc *pas, *next; mutex_lock(&psp_devs_lock); @@ -139,6 +150,15 @@ void psp_dev_unregister(struct psp_dev *psd) list_for_each_entry_safe(pas, next, &psd->stale_assocs, assocs_list) psp_dev_tx_key_del(psd, pas); + list_for_each_entry_safe(entry, entry_tmp, &psd->assoc_dev_list, + dev_list) { + list_del(&entry->dev_list); + rcu_assign_pointer(entry->assoc_dev->psp_dev, NULL); + netdev_put(entry->assoc_dev, &entry->dev_tracker); + kfree(entry); + } + psd->assoc_dev_cnt = 0; + rcu_assign_pointer(psd->main_netdev->psp_dev, NULL); psd->ops = NULL; @@ -385,6 +405,81 @@ int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv) } EXPORT_SYMBOL(psp_dev_rcv); +static void psp_dev_disassoc_one(struct psp_dev *psd, struct net_device *dev) +{ + struct psp_assoc_dev *entry; + + list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) { + if (entry->assoc_dev == dev) { + list_del(&entry->dev_list); + psd->assoc_dev_cnt--; + rcu_assign_pointer(entry->assoc_dev->psp_dev, NULL); + netdev_put(entry->assoc_dev, &entry->dev_tracker); + kfree(entry); + return; + } + } +} + +static int psp_netdev_event(struct notifier_block *nb, unsigned long event, + void *ptr) +{ + struct net_device *dev = netdev_notifier_info_to_dev(ptr); + struct psp_dev *psd; + + if (event != NETDEV_UNREGISTER) + return NOTIFY_DONE; + + rcu_read_lock(); + psd = rcu_dereference(dev->psp_dev); + if (psd && psp_dev_tryget(psd)) { + rcu_read_unlock(); + mutex_lock(&psd->lock); + if (psp_dev_is_registered(psd)) + psp_nl_notify_dev(psd, PSP_CMD_DEV_CHANGE_NTF); + psp_dev_disassoc_one(psd, dev); + mutex_unlock(&psd->lock); + psp_dev_put(psd); + } else { + rcu_read_unlock(); + } + + return NOTIFY_DONE; +} + +static struct notifier_block psp_netdev_notifier = { + .notifier_call = psp_netdev_event, +}; + +static DEFINE_MUTEX(psp_notifier_lock); +static bool psp_notifier_registered; + +/* Register the netdevice notifier when the first device association + * is created. In many installations no associations will be created and + * the notifier won't be needed. + * + * Must be called without psd->lock held, due to lock ordering: + * rtnl_lock -> psd->lock (the notifier callback runs under rtnl_lock + * and takes psd->lock). + */ +int psp_attach_netdev_notifier(void) +{ + int err = 0; + + if (READ_ONCE(psp_notifier_registered)) + return 0; + + mutex_lock(&psp_notifier_lock); + if (!psp_notifier_registered) { + err = register_netdevice_notifier(&psp_netdev_notifier); + if (!err) + WRITE_ONCE(psp_notifier_registered, true); + } + mutex_unlock(&psp_notifier_lock); + + return err; +} + static int __init psp_init(void) { mutex_init(&psp_devs_lock); diff --git a/net/psp/psp_nl.c b/net/psp/psp_nl.c index 0cc744a6e1c9..9610d8c456ff 100644 --- a/net/psp/psp_nl.c +++ b/net/psp/psp_nl.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only #include <linux/ethtool.h> +#include <linux/net_namespace.h> #include <linux/skbuff.h> #include <linux/xarray.h> #include <net/genetlink.h> @@ -38,10 +39,79 @@ static int psp_nl_reply_send(struct sk_buff *rsp, struct genl_info *info) return genlmsg_reply(rsp, info); } +/** + * psp_nl_multicast_per_ns() - multicast a notification to each unique netns + * @psd: PSP device (must be locked) + * @group: multicast group + * @build_ntf: callback to build an skb for a given netns, or NULL on failure + * @ctx: opaque context passed to @build_ntf + * + * Iterates all unique network namespaces from the associated device list + * plus the main device's netns. For each unique netns, calls @build_ntf + * to construct a notification skb and multicasts it. + */ +static void +psp_nl_multicast_per_ns(struct psp_dev *psd, unsigned int group, + struct sk_buff *(*build_ntf)(struct psp_dev *, + struct net *, + void *), + void *ctx) +{ + struct psp_assoc_dev *entry; + struct xarray sent_nets; + struct net *main_net; + struct sk_buff *ntf; + + main_net = dev_net(psd->main_netdev); + xa_init(&sent_nets); + + list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) { + struct net *assoc_net = dev_net(entry->assoc_dev); + int ret; + + if (net_eq(assoc_net, main_net)) + continue; + + ret = xa_insert(&sent_nets, (unsigned long)assoc_net, assoc_net, + GFP_KERNEL); + if (ret == -EBUSY) + continue; + + ntf = build_ntf(psd, assoc_net, ctx); + if (!ntf) + continue; + + genlmsg_multicast_netns(&psp_nl_family, assoc_net, ntf, 0, + group, GFP_KERNEL); + } + xa_destroy(&sent_nets); + + /* Send to main device netns */ + ntf = build_ntf(psd, main_net, ctx); + if (!ntf) + return; + genlmsg_multicast_netns(&psp_nl_family, main_net, ntf, 0, group, + GFP_KERNEL); +} + +static struct sk_buff *psp_nl_clone_ntf(struct psp_dev *psd, struct net *net, + void *ctx) +{ + return skb_clone(ctx, GFP_KERNEL); +} + +static void psp_nl_multicast_all_ns(struct psp_dev *psd, struct sk_buff *ntf, + unsigned int group) +{ + psp_nl_multicast_per_ns(psd, group, psp_nl_clone_ntf, ntf); + nlmsg_consume(ntf); +} + /* Device stuff */ static struct psp_dev * -psp_device_get_and_lock(struct net *net, struct nlattr *dev_id) +psp_device_get_and_lock(struct net *net, struct nlattr *dev_id, + bool admin) { struct psp_dev *psd; int err; @@ -56,7 +126,7 @@ psp_device_get_and_lock(struct net *net, struct nlattr *dev_id) mutex_lock(&psd->lock); mutex_unlock(&psp_devs_lock); - err = psp_dev_check_access(psd, net); + err = psp_dev_check_access(psd, net, admin); if (err) { mutex_unlock(&psd->lock); return ERR_PTR(err); @@ -65,17 +135,79 @@ psp_device_get_and_lock(struct net *net, struct nlattr *dev_id) return psd; } -int psp_device_get_locked(const struct genl_split_ops *ops, - struct sk_buff *skb, struct genl_info *info) +static int __psp_device_get_locked(const struct genl_split_ops *ops, + struct sk_buff *skb, struct genl_info *info, + bool admin) { if (GENL_REQ_ATTR_CHECK(info, PSP_A_DEV_ID)) return -EINVAL; info->user_ptr[0] = psp_device_get_and_lock(genl_info_net(info), - info->attrs[PSP_A_DEV_ID]); + info->attrs[PSP_A_DEV_ID], + admin); return PTR_ERR_OR_ZERO(info->user_ptr[0]); } +int psp_device_get_locked_admin(const struct genl_split_ops *ops, + struct sk_buff *skb, struct genl_info *info) +{ + return __psp_device_get_locked(ops, skb, info, true); +} + +int psp_device_get_locked(const struct genl_split_ops *ops, + struct sk_buff *skb, struct genl_info *info) +{ + return __psp_device_get_locked(ops, skb, info, false); +} + +/* + * Non-admin version of psp_device_get_locked() + psp_attach_netdev_notifier() + * only used for dev-assoc. + */ +int psp_device_get_locked_dev_assoc(const struct genl_split_ops *ops, + struct sk_buff *skb, struct genl_info *info) +{ + int err; + + err = psp_attach_netdev_notifier(); + if (err) + return err; + + return __psp_device_get_locked(ops, skb, info, false); +} + +static struct net *psp_nl_resolve_assoc_dev_ns(struct psp_dev *psd, + struct genl_info *info) +{ + struct net *net; + int nsid; + + if (GENL_REQ_ATTR_CHECK(info, PSP_A_DEV_IFINDEX)) + return ERR_PTR(-EINVAL); + + if (info->attrs[PSP_A_DEV_NSID]) { + /* Only callers in the main netns may specify nsid */ + if (dev_net(psd->main_netdev) != genl_info_net(info)) { + NL_SET_BAD_ATTR(info->extack, + info->attrs[PSP_A_DEV_NSID]); + return ERR_PTR(-EPERM); + } + + nsid = nla_get_s32(info->attrs[PSP_A_DEV_NSID]); + + net = get_net_ns_by_id(genl_info_net(info), nsid); + if (!net) { + NL_SET_BAD_ATTR(info->extack, + info->attrs[PSP_A_DEV_NSID]); + return ERR_PTR(-EINVAL); + } + } else { + net = get_net(genl_info_net(info)); + } + + return net; +} + void psp_device_unlock(const struct genl_split_ops *ops, struct sk_buff *skb, struct genl_info *info) @@ -88,11 +220,67 @@ psp_device_unlock(const struct genl_split_ops *ops, struct sk_buff *skb, sockfd_put(socket); } +bool psp_has_assoc_dev_in_ns(struct psp_dev *psd, struct net *net) +{ + struct psp_assoc_dev *entry; + + list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) { + if (dev_net(entry->assoc_dev) == net) + return true; + } + + return false; +} + +static int psp_nl_fill_assoc_dev_list(struct psp_dev *psd, struct sk_buff *rsp, + struct net *cur_net, + struct net *filter_net) +{ + struct psp_assoc_dev *entry; + struct net *dev_net_ns; + struct nlattr *nest; + int nsid; + + list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) { + dev_net_ns = dev_net(entry->assoc_dev); + + if (filter_net && dev_net_ns != filter_net) + continue; + + /* When filtering by namespace, all devices are in the caller's + * namespace so nsid is always NETNSA_NSID_NOT_ASSIGNED (-1). + * Otherwise, calculate the nsid relative to cur_net. + */ + nsid = filter_net ? NETNSA_NSID_NOT_ASSIGNED : + peernet2id_alloc(cur_net, dev_net_ns, + GFP_KERNEL); + + nest = nla_nest_start(rsp, PSP_A_DEV_ASSOC_LIST); + if (!nest) + return -EMSGSIZE; + + if (nla_put_u32(rsp, PSP_A_ASSOC_DEV_INFO_IFINDEX, + entry->assoc_dev->ifindex) || + nla_put_s32(rsp, PSP_A_ASSOC_DEV_INFO_NSID, nsid)) { + nla_nest_cancel(rsp, nest); + return -EMSGSIZE; + } + + nla_nest_end(rsp, nest); + } + + return 0; +} + static int psp_nl_dev_fill(struct psp_dev *psd, struct sk_buff *rsp, const struct genl_info *info) { + struct net *cur_net; void *hdr; + int err; + + cur_net = genl_info_net(info); hdr = genlmsg_iput(rsp, info); if (!hdr) @@ -104,6 +292,22 @@ psp_nl_dev_fill(struct psp_dev *psd, struct sk_buff *rsp, nla_put_u32(rsp, PSP_A_DEV_PSP_VERSIONS_ENA, psd->config.versions)) goto err_cancel_msg; + if (cur_net == dev_net(psd->main_netdev)) { + /* Primary device - dump assoc list */ + err = psp_nl_fill_assoc_dev_list(psd, rsp, cur_net, NULL); + if (err) + goto err_cancel_msg; + } else { + /* In netns: set by-association flag and dump filtered + * assoc list containing only devices in cur_net + */ + if (nla_put_flag(rsp, PSP_A_DEV_BY_ASSOCIATION)) + goto err_cancel_msg; + err = psp_nl_fill_assoc_dev_list(psd, rsp, cur_net, cur_net); + if (err) + goto err_cancel_msg; + } + genlmsg_end(rsp, hdr); return 0; @@ -112,27 +316,34 @@ err_cancel_msg: return -EMSGSIZE; } -void psp_nl_notify_dev(struct psp_dev *psd, u32 cmd) +static struct sk_buff *psp_nl_build_dev_ntf(struct psp_dev *psd, + struct net *net, void *ctx) { + u32 cmd = *(u32 *)ctx; struct genl_info info; struct sk_buff *ntf; - if (!genl_has_listeners(&psp_nl_family, dev_net(psd->main_netdev), - PSP_NLGRP_MGMT)) - return; + if (!genl_has_listeners(&psp_nl_family, net, PSP_NLGRP_MGMT)) + return NULL; ntf = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL); if (!ntf) - return; + return NULL; genl_info_init_ntf(&info, &psp_nl_family, cmd); + genl_info_net_set(&info, net); if (psp_nl_dev_fill(psd, ntf, &info)) { nlmsg_free(ntf); - return; + return NULL; } - genlmsg_multicast_netns(&psp_nl_family, dev_net(psd->main_netdev), ntf, - 0, PSP_NLGRP_MGMT, GFP_KERNEL); + return ntf; +} + +void psp_nl_notify_dev(struct psp_dev *psd, u32 cmd) +{ + psp_nl_multicast_per_ns(psd, PSP_NLGRP_MGMT, + psp_nl_build_dev_ntf, &cmd); } int psp_nl_dev_get_doit(struct sk_buff *req, struct genl_info *info) @@ -160,7 +371,7 @@ static int psp_nl_dev_get_dumpit_one(struct sk_buff *rsp, struct netlink_callback *cb, struct psp_dev *psd) { - if (psp_dev_check_access(psd, sock_net(rsp->sk))) + if (psp_dev_check_access(psd, sock_net(rsp->sk), false)) return 0; return psp_nl_dev_fill(psd, rsp, genl_info_dump(cb)); @@ -266,8 +477,9 @@ int psp_nl_key_rotate_doit(struct sk_buff *skb, struct genl_info *info) psd->stats.rotations++; nlmsg_end(ntf, (struct nlmsghdr *)ntf->data); - genlmsg_multicast_netns(&psp_nl_family, dev_net(psd->main_netdev), ntf, - 0, PSP_NLGRP_USE, GFP_KERNEL); + + psp_nl_multicast_all_ns(psd, ntf, PSP_NLGRP_USE); + return psp_nl_reply_send(rsp, info); err_free_ntf: @@ -277,6 +489,143 @@ err_free_rsp: return err; } +int psp_nl_dev_assoc_doit(struct sk_buff *skb, struct genl_info *info) +{ + struct psp_dev *psd = info->user_ptr[0]; + struct psp_assoc_dev *psp_assoc_dev; + struct net_device *assoc_dev; + struct sk_buff *rsp; + u32 assoc_ifindex; + struct net *net; + int err; + + if (psd->assoc_dev_cnt >= PSP_ASSOC_DEV_MAX) { + NL_SET_ERR_MSG(info->extack, + "Maximum number of associated devices reached"); + return -ENOSPC; + } + + net = psp_nl_resolve_assoc_dev_ns(psd, info); + if (IS_ERR(net)) + return PTR_ERR(net); + + psp_assoc_dev = kzalloc_obj(*psp_assoc_dev); + if (!psp_assoc_dev) { + err = -ENOMEM; + goto err_put_net; + } + + assoc_ifindex = nla_get_u32(info->attrs[PSP_A_DEV_IFINDEX]); + assoc_dev = netdev_get_by_index(net, assoc_ifindex, + &psp_assoc_dev->dev_tracker, + GFP_KERNEL); + if (!assoc_dev) { + NL_SET_BAD_ATTR(info->extack, info->attrs[PSP_A_DEV_IFINDEX]); + err = -ENODEV; + goto err_free_assoc; + } + + /* Check if device is already associated with a PSP device */ + if (cmpxchg(&assoc_dev->psp_dev, NULL, RCU_INITIALIZER(psd))) { + NL_SET_ERR_MSG(info->extack, + "Device already associated with a PSP device"); + err = -EBUSY; + goto err_put_dev; + } + + psp_assoc_dev->assoc_dev = assoc_dev; + + /* Check for race with NETDEV_UNREGISTER. The cmpxchg above is a + * full barrier, and the unregister path has synchronize_net() + * between setting NETREG_UNREGISTERING and reading psp_dev in the + * notifier. So at least one side would do the clean-up if we are in + * the middle of unregitering assoc_dev. + * And the clean-up is serialized by psd->lock. + */ + if (READ_ONCE(assoc_dev->reg_state) != NETREG_REGISTERED) { + err = -ENODEV; + goto err_clean_ptr; + } + + rsp = psp_nl_reply_new(info); + if (!rsp) { + err = -ENOMEM; + goto err_clean_ptr; + } + + list_add_tail(&psp_assoc_dev->dev_list, &psd->assoc_dev_list); + psd->assoc_dev_cnt++; + + put_net(net); + + psp_nl_notify_dev(psd, PSP_CMD_DEV_CHANGE_NTF); + + return psp_nl_reply_send(rsp, info); + +err_clean_ptr: + rcu_assign_pointer(assoc_dev->psp_dev, NULL); +err_put_dev: + netdev_put(assoc_dev, &psp_assoc_dev->dev_tracker); +err_free_assoc: + kfree(psp_assoc_dev); +err_put_net: + put_net(net); + + return err; +} + +int psp_nl_dev_disassoc_doit(struct sk_buff *skb, struct genl_info *info) +{ + struct psp_assoc_dev *entry, *found = NULL; + struct psp_dev *psd = info->user_ptr[0]; + struct sk_buff *rsp; + u32 assoc_ifindex; + struct net *net; + + net = psp_nl_resolve_assoc_dev_ns(psd, info); + if (IS_ERR(net)) + return PTR_ERR(net); + + assoc_ifindex = nla_get_u32(info->attrs[PSP_A_DEV_IFINDEX]); + + /* Search the association list by ifindex and netns */ + list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) { + if (entry->assoc_dev->ifindex == assoc_ifindex && + dev_net(entry->assoc_dev) == net) { + found = entry; + break; + } + } + + if (!found) { + put_net(net); + NL_SET_BAD_ATTR(info->extack, info->attrs[PSP_A_DEV_IFINDEX]); + return -ENODEV; + } + + rsp = psp_nl_reply_new(info); + if (!rsp) { + put_net(net); + return -ENOMEM; + } + + put_net(net); + + /* Notify before removal so listeners in the disassociated namespace + * still receive the notification. + */ + psp_nl_notify_dev(psd, PSP_CMD_DEV_CHANGE_NTF); + + /* Remove from the association list */ + list_del(&found->dev_list); + psd->assoc_dev_cnt--; + rcu_assign_pointer(found->assoc_dev->psp_dev, NULL); + netdev_put(found->assoc_dev, &found->dev_tracker); + kfree(found); + + return psp_nl_reply_send(rsp, info); +} + /* Key etc. */ int psp_assoc_device_get_locked(const struct genl_split_ops *ops, @@ -310,7 +659,7 @@ int psp_assoc_device_get_locked(const struct genl_split_ops *ops, */ mutex_lock(&psd->lock); if (!psp_dev_is_registered(psd) || - psp_dev_check_access(psd, genl_info_net(info))) { + psp_dev_check_access(psd, genl_info_net(info), false)) { mutex_unlock(&psd->lock); psp_dev_put(psd); psd = NULL; @@ -334,7 +683,7 @@ int psp_assoc_device_get_locked(const struct genl_split_ops *ops, psp_dev_put(psd); } else { - psd = psp_device_get_and_lock(genl_info_net(info), id); + psd = psp_device_get_and_lock(genl_info_net(info), id, false); if (IS_ERR(psd)) { err = PTR_ERR(psd); goto err_sock_put; @@ -577,7 +926,7 @@ static int psp_nl_stats_get_dumpit_one(struct sk_buff *rsp, struct netlink_callback *cb, struct psp_dev *psd) { - if (psp_dev_check_access(psd, sock_net(rsp->sk))) + if (psp_dev_check_access(psd, sock_net(rsp->sk), false)) return 0; return psp_nl_stats_fill(psd, rsp, genl_info_dump(cb)); diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config index 617de8aaf551..91d4fd410914 100644 --- a/tools/testing/selftests/drivers/net/config +++ b/tools/testing/selftests/drivers/net/config @@ -8,6 +8,7 @@ CONFIG_NETCONSOLE=m CONFIG_NETCONSOLE_DYNAMIC=y CONFIG_NETCONSOLE_EXTENDED_LOG=y CONFIG_NETDEVSIM=m +CONFIG_NETKIT=y CONFIG_NET_SCH_ETF=m CONFIG_NET_SCH_FQ=m CONFIG_PPP=y diff --git a/tools/testing/selftests/drivers/net/hw/nk_qlease.py b/tools/testing/selftests/drivers/net/hw/nk_qlease.py index 139a91ebd229..f5fd64775989 100755 --- a/tools/testing/selftests/drivers/net/hw/nk_qlease.py +++ b/tools/testing/selftests/drivers/net/hw/nk_qlease.py @@ -193,9 +193,9 @@ def test_destroy(cfg) -> None: kill_timer = threading.Timer(1, rx_proc.proc.terminate) kill_timer.start() - ip(f"link del dev {cfg._nk_host_ifname}") + ip(f"link del dev {cfg.nk_host_ifname}") kill_timer.join() - cfg._nk_host_ifname = None + cfg.nk_host_ifname = None cfg.nk_guest_ifname = None queue_info = netdevnl.queue_get( diff --git a/tools/testing/selftests/drivers/net/lib/py/env.py b/tools/testing/selftests/drivers/net/lib/py/env.py index 2cc78b8a2152..b188ee55c76b 100644 --- a/tools/testing/selftests/drivers/net/lib/py/env.py +++ b/tools/testing/selftests/drivers/net/lib/py/env.py @@ -341,7 +341,7 @@ class NetDrvContEnv(NetDrvEpEnv): userns=False, **kwargs): self.netns = None self._userns = userns - self._nk_host_ifname = None + self.nk_host_ifname = None self.nk_guest_ifname = None self._tc_clsact_added = False self._tc_attached = False @@ -395,7 +395,7 @@ class NetDrvContEnv(NetDrvEpEnv): raise KsftSkipEx("Failed to create netkit pair") netkit_links.sort(key=lambda x: x['ifindex']) - self._nk_host_ifname = netkit_links[1]['ifname'] + self.nk_host_ifname = netkit_links[1]['ifname'] self.nk_guest_ifname = netkit_links[0]['ifname'] self.nk_host_ifindex = netkit_links[1]['ifindex'] self.nk_guest_ifindex = netkit_links[0]['ifindex'] @@ -407,11 +407,11 @@ class NetDrvContEnv(NetDrvEpEnv): def __del__(self): if self._primary_rx_redirect_attached: - cmd(f"tc filter del dev {self._nk_host_ifname} ingress", fail=False) + cmd(f"tc filter del dev {self.nk_host_ifname} ingress", fail=False) self._primary_rx_redirect_attached = False if self._primary_rx_redirect_clsact_added: - cmd(f"tc qdisc del dev {self._nk_host_ifname} clsact", fail=False) + cmd(f"tc qdisc del dev {self.nk_host_ifname} clsact", fail=False) self._primary_rx_redirect_clsact_added = False if self._tc_attached: @@ -427,9 +427,9 @@ class NetDrvContEnv(NetDrvEpEnv): host=self.remote, fail=False) self._remote_route_added = False - if self._nk_host_ifname: - cmd(f"ip link del dev {self._nk_host_ifname}") - self._nk_host_ifname = None + if self.nk_host_ifname: + cmd(f"ip link del dev {self.nk_host_ifname}") + self.nk_host_ifname = None self.nk_guest_ifname = None if self._init_ns_attached: @@ -470,9 +470,12 @@ class NetDrvContEnv(NetDrvEpEnv): self._init_ns_attached = True ip("netns set init 0", ns=self.netns) ip(f"link set dev {self.nk_guest_ifname} netns {self.netns.name}") - ip(f"link set dev {self._nk_host_ifname} up") - ip(f"-6 addr add fe80::1/64 dev {self._nk_host_ifname} nodad") - ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self._nk_host_ifname}") + nk_guest_dev = ip(f"link show dev {self.nk_guest_ifname}", + json=True, ns=self.netns)[0] + self.nk_guest_ifindex = nk_guest_dev['ifindex'] + ip(f"link set dev {self.nk_host_ifname} up") + ip(f"-6 addr add fe80::1/64 dev {self.nk_host_ifname} nodad") + ip(f"-6 route add {self.nk_guest_ipv6}/128 via fe80::2 dev {self.nk_host_ifname}") ip("link set lo up", ns=self.netns) ip(f"link set dev {self.nk_guest_ifname} up", ns=self.netns) @@ -512,10 +515,19 @@ class NetDrvContEnv(NetDrvEpEnv): return map_id raise Exception(f"Failed to find .bss map for prog {prog_id}") + def _find_bpf_obj(self, name): + bpf_obj = self.test_dir / name + if bpf_obj.exists(): + return bpf_obj + bpf_obj = self.test_dir / "hw" / name + if bpf_obj.exists(): + return bpf_obj + return None + def _attach_bpf(self): - bpf_obj = self.test_dir / "nk_forward.bpf.o" - if not bpf_obj.exists(): - raise KsftSkipEx("BPF prog not found") + bpf_obj = self._find_bpf_obj("nk_forward.bpf.o") + if not bpf_obj: + raise KsftSkipEx("BPF prog nk_forward.bpf.o not found") if self._tc_ensure_clsact(): self._tc_clsact_added = True @@ -535,13 +547,13 @@ class NetDrvContEnv(NetDrvEpEnv): def _attach_primary_rx_redirect_bpf(self): """Attach BPF redirect program on the primary netkit ingress.""" - bpf_obj = self.test_dir / "nk_primary_rx_redirect.bpf.o" - if not bpf_obj.exists(): - raise KsftSkipEx("Primary RX redirect BPF prog not found") + bpf_obj = self._find_bpf_obj("nk_primary_rx_redirect.bpf.o") + if not bpf_obj: + raise KsftSkipEx("nk_primary_rx_redirect.bpf.o not found") - if self._tc_ensure_clsact(self._nk_host_ifname): + if self._tc_ensure_clsact(self.nk_host_ifname): self._primary_rx_redirect_clsact_added = True - cmd(f"tc filter add dev {self._nk_host_ifname} ingress" + cmd(f"tc filter add dev {self.nk_host_ifname} ingress" f" bpf obj {bpf_obj} sec tc/ingress direct-action") self._primary_rx_redirect_attached = True @@ -550,7 +562,7 @@ class NetDrvContEnv(NetDrvEpEnv): self._remote_route_added = True filters = json.loads( - cmd(f"tc -j filter show dev {self._nk_host_ifname} ingress").stdout) + cmd(f"tc -j filter show dev {self.nk_host_ifname} ingress").stdout) redirect_prog_id = None for bpf in filters: if 'options' not in bpf: diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py index 864d9fce1094..315648a770d0 100755 --- a/tools/testing/selftests/drivers/net/psp.py +++ b/tools/testing/selftests/drivers/net/psp.py @@ -5,6 +5,7 @@ import errno import fcntl +import os import socket import struct import termios @@ -14,9 +15,13 @@ from lib.py import defer from lib.py import ksft_run, ksft_exit, ksft_pr from lib.py import ksft_true, ksft_eq, ksft_ne, ksft_gt, ksft_raises from lib.py import ksft_not_none -from lib.py import KsftSkipEx -from lib.py import NetDrvEpEnv, PSPFamily, NlError +from lib.py import ksft_variants, KsftNamedVariant +from lib.py import KsftSkipEx, KsftFailEx +from lib.py import NetDrvEpEnv, NetDrvContEnv +from lib.py import Netlink, NlError, PSPFamily, RtnlFamily +from lib.py import NetNSEnter from lib.py import bkg, rand_port, wait_port_listen +from lib.py import ip def _get_outq(s): @@ -117,11 +122,13 @@ def _get_stat(cfg, key): # Test case boiler plate # -def _init_psp_dev(cfg): +def _init_psp_dev(cfg, use_psp_ifindex=False): if not hasattr(cfg, 'psp_dev_id'): # Figure out which local device we are testing against + # For NetDrvContEnv: use psp_ifindex instead of ifindex + target_ifindex = cfg.psp_ifindex if use_psp_ifindex else cfg.ifindex for dev in cfg.pspnl.dev_get({}, dump=True): - if dev['ifindex'] == cfg.ifindex: + if dev['ifindex'] == target_ifindex: cfg.psp_info = dev cfg.psp_dev_id = cfg.psp_info['id'] break @@ -571,33 +578,388 @@ def removal_device_bi(cfg): _close_conn(cfg, s) -def psp_ip_ver_test_builder(name, test_func, psp_ver, ipver): - """Build test cases for each combo of PSP version and IP version""" - def test_case(cfg): - cfg.require_ipver(ipver) - test_func(cfg, psp_ver, ipver) +def _get_psp_ver_ip_variants(): + for ver in range(4): + for ipv in ("4", "6"): + yield KsftNamedVariant(f"v{ver}_ip{ipv}", ver, ipv) - test_case.__name__ = f"{name}_v{psp_ver}_ip{ipver}" - return test_case +def _get_ip_variants(): + for ipv in ("4", "6"): + yield KsftNamedVariant(f"ip{ipv}", ipv) -def ipver_test_builder(name, test_func, ipver): - """Build test cases for each IP version""" - def test_case(cfg): - cfg.require_ipver(ipver) - test_func(cfg, ipver) - test_case.__name__ = f"{name}_ip{ipver}" - return test_case +@ksft_variants(_get_psp_ver_ip_variants()) +def data_basic_send(cfg, version, ipver): + """Test basic PSP data send.""" + cfg.require_ipver(ipver) + _data_basic_send(cfg, version, ipver) + + +@ksft_variants(_get_ip_variants()) +def data_mss_adjust(cfg, ipver): + """Test MSS adjustment with PSP.""" + cfg.require_ipver(ipver) + _data_mss_adjust(cfg, ipver) + + +def _check_assoc_list(cfg, psp_dev_id, ifindex, nsid=None): + """Verify assoc-list contains device with given ifindex, no duplicates.""" + dev_info = cfg.pspnl.dev_get({'id': psp_dev_id}) + + ksft_true('assoc-list' in dev_info, + "No assoc-list in dev_get() response after association") + found = False + for assoc in dev_info['assoc-list']: + if assoc['ifindex'] != ifindex: + continue + if nsid is not None and assoc['nsid'] != nsid: + continue + ksft_eq(found, False, "Duplicate assoc entry found") + found = True + ksft_eq(found, True, + "Associated device not found in dev_get() response") + + +def _data_basic_send_netkit_psp_assoc(cfg, version, ipver): + """ + Test basic data send with netkit interface associated with PSP dev. + """ + _assoc_nk_guest(cfg) + + # Enter guest namespace (netns) to run PSP test + with NetNSEnter(cfg.netns.name): + cfg.pspnl = PSPFamily() + + sock = _make_psp_conn(cfg, version, ipver) + + rx_assoc = cfg.pspnl.rx_assoc({"version": version, + "dev-id": cfg.psp_dev_id, + "sock-fd": sock.fileno()}) + rx_key = rx_assoc['rx-key'] + tx_key = _spi_xchg(sock, rx_key) + + cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id, + "version": version, + "tx-key": tx_key, + "sock-fd": sock.fileno()}) + + data_len = _send_careful(cfg, sock, 100) + _check_data_rx(cfg, data_len) + _close_psp_conn(cfg, sock) + + +def _assoc_check_list(cfg): + """Test that assoc-list is correctly populated after dev-assoc.""" + _assoc_nk_guest(cfg) + _check_assoc_list(cfg, cfg.psp_dev_id, cfg.nk_guest_ifindex, + cfg.psp_dev_peer_nsid) + + +def _get_psp_ver_ip6_variants(): + for ver in range(4): + yield KsftNamedVariant(f"v{ver}_ip6", ver, "6") + + +@ksft_variants(_get_psp_ver_ip6_variants()) +def data_basic_send_netkit_psp_assoc(cfg, version, ipver): + """Test PSP data send via netkit with dev-assoc.""" + cfg.require_ipver(ipver) + _data_basic_send_netkit_psp_assoc(cfg, version, ipver) + + +def _key_rotation_notify_multi_ns_netkit(cfg): + """ Test key rotation notifications across multiple namespaces using netkit """ + _assoc_nk_guest(cfg) + + # Create listener in guest namespace; socket stays bound to that ns + with NetNSEnter(cfg.netns.name): + peer_pspnl = PSPFamily() + peer_pspnl.ntf_subscribe('use') + + # Create listener in main namespace + main_pspnl = PSPFamily() + main_pspnl.ntf_subscribe('use') + + # Trigger key rotation on the PSP device + cfg.pspnl.key_rotate({"id": cfg.psp_dev_id}) + + # Poll both sockets from main thread + for pspnl, label in [(main_pspnl, "main"), (peer_pspnl, "guest")]: + for ntf in pspnl.poll_ntf(duration=10): + if ntf['msg'].get('id') == cfg.psp_dev_id: + break + else: + raise KsftFailEx( + f"No key rotation notification received" + f" in {label} namespace") + + +def _dev_change_notify_multi_ns_netkit(cfg): + """ Test dev_change notifications across multiple namespaces using netkit """ + _assoc_nk_guest(cfg) + + # Create listener in guest namespace; socket stays bound to that ns + with NetNSEnter(cfg.netns.name): + peer_pspnl = PSPFamily() + peer_pspnl.ntf_subscribe('mgmt') + + # Create listener in main namespace + main_pspnl = PSPFamily() + main_pspnl.ntf_subscribe('mgmt') + + # Trigger dev_change by calling dev_set (notification is always sent) + cfg.pspnl.dev_set({'id': cfg.psp_dev_id, + 'psp-versions-ena': cfg.psp_info['psp-versions-cap']}) + + # Poll both sockets from main thread + for pspnl, label in [(main_pspnl, "main"), (peer_pspnl, "guest")]: + for ntf in pspnl.poll_ntf(duration=10): + if ntf['msg'].get('id') == cfg.psp_dev_id: + break + else: + raise KsftFailEx( + f"No dev_change notification received" + f" in {label} namespace") + + +def _psp_dev_get_check_netkit_psp_assoc(cfg): + """ Check psp dev-get output with netkit interface associated with PSP dev """ + _assoc_nk_guest(cfg) + + # Check 1: In default netns, verify dev-get has correct ifindex and assoc-list + dev_info = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) + ksft_eq(dev_info['ifindex'], cfg.psp_ifindex) + _check_assoc_list(cfg, cfg.psp_dev_id, cfg.nk_guest_ifindex, + cfg.psp_dev_peer_nsid) + + # Check 2: In guest netns, verify dev-get has assoc-list with nk_guest device + with NetNSEnter(cfg.netns.name): + peer_pspnl = PSPFamily() + + # Dump all devices in the guest namespace + peer_devices = peer_pspnl.dev_get({}, dump=True) + + # Find the device with by-association flag + peer_dev = None + for dev in peer_devices: + if dev.get('by-association'): + peer_dev = dev + break + + ksft_not_none(peer_dev, "No PSP device found with by-association flag in guest netns") + + # Verify assoc-list contains the nk_guest device + ksft_true('assoc-list' in peer_dev and len(peer_dev['assoc-list']) > 0, + "Guest device should have assoc-list with local devices") + + # Verify the assoc-list contains nk_guest ifindex with nsid=-1 (same namespace) + found = False + for assoc in peer_dev['assoc-list']: + if assoc['ifindex'] == cfg.nk_guest_ifindex: + ksft_eq(assoc['nsid'], -1, + "nsid should be -1 (NETNSA_NSID_NOT_ASSIGNED) for same-namespace device") + found = True + break + ksft_true(found, "nk_guest ifindex not found in assoc-list") + + +def _dev_assoc_no_nsid(cfg): + """ Test dev-assoc and dev-disassoc without nsid attribute """ + _init_psp_dev(cfg, True) + + # Associate without nsid - should look up ifindex in caller's netns + cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, + 'ifindex': cfg.nk_host_ifindex}) + defer(_try_disassoc, cfg, + cfg.psp_dev_id, cfg.nk_host_ifindex) + defer(delattr, cfg, 'psp_dev_id') + defer(delattr, cfg, 'psp_info') + + # Verify assoc-list contains the device (match by ifindex only) + _check_assoc_list(cfg, cfg.psp_dev_id, cfg.nk_host_ifindex) + + # Disassociate without nsid - should also use caller's netns + cfg.pspnl.dev_disassoc({'id': cfg.psp_dev_id, + 'ifindex': cfg.nk_host_ifindex}) + + # Verify assoc-list no longer contains the device + dev_info = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) + found = False + if 'assoc-list' in dev_info: + for assoc in dev_info['assoc-list']: + if assoc['ifindex'] == cfg.nk_host_ifindex: + found = True + break + ksft_true(not found, "Device should not be in assoc-list after disassociation") + + +def _psp_dev_assoc_cleanup_on_netkit_del(cfg): + """Test that assoc-list is cleared when associated netkit is deleted. + + Creates a disposable netkit pair for this test to avoid destroying + the shared environment. + """ + _init_psp_dev(cfg, True) + defer(delattr, cfg, 'psp_dev_id') + defer(delattr, cfg, 'psp_info') + + existing = {cfg.nk_host_ifindex, cfg.nk_guest_ifindex} + + # Create a temporary netkit pair + tmp_host_name = "tmp_nk_host" + tmp_guest_name = "tmp_nk_guest" + rtnl = RtnlFamily() + rtnl.newlink( + { + "ifname": tmp_host_name, + "linkinfo": { + "kind": "netkit", + "data": { + "mode": "l2", + "policy": "forward", + "peer-policy": "forward", + }, + }, + }, + flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL], + ) + cleanup_netkit = defer(ip, f"link del {tmp_host_name}") + + # Find the peer by diffing against existing netkit ifindexes + all_links = ip("-d link show", json=True) + tmp_peer = [link for link in all_links + if link.get('linkinfo', {}).get('info_kind') == 'netkit' + and link['ifindex'] not in existing + and link['ifname'] != tmp_host_name] + ksft_eq(len(tmp_peer), 1, + "Failed to find temporary netkit peer") + guest_name = tmp_peer[0]['ifname'] + + # Rename and move guest end into the test namespace + ip(f"link set dev {guest_name} name {tmp_guest_name}") + ip(f"link set dev {tmp_guest_name} netns {cfg.netns.name}") + tmp_guest_dev = ip(f"link show dev {tmp_guest_name}", + json=True, ns=cfg.netns)[0] + tmp_guest_ifindex = tmp_guest_dev['ifindex'] + ip(f"link set dev {tmp_guest_name} up", ns=cfg.netns) + + # Associate PSP device with the temporary guest interface + cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, + 'ifindex': tmp_guest_ifindex, + 'nsid': cfg.psp_dev_peer_nsid}) + + # Verify assoc-list contains the temporary device + _check_assoc_list(cfg, cfg.psp_dev_id, tmp_guest_ifindex, + cfg.psp_dev_peer_nsid) + + # Delete the temporary netkit pair (deleting one end removes both) + ip(f"link del {tmp_host_name}") + cleanup_netkit.cancel() + + # Verify assoc-list is cleared after netkit deletion + dev_info = cfg.pspnl.dev_get({'id': cfg.psp_dev_id}) + ksft_true('assoc-list' not in dev_info + or len(dev_info['assoc-list']) == 0, + "assoc-list should be empty after netkit deletion") + + +def _try_disassoc(cfg, psp_dev_id, ifindex, nsid=None): + """Best-effort disassociate, ignoring errors if already removed.""" + try: + params = {'id': psp_dev_id, 'ifindex': ifindex} + if nsid is not None: + params['nsid'] = nsid + cfg.pspnl.dev_disassoc(params) + except NlError: + pass + + +def _assoc_nk_guest(cfg): + """Associate nk_guest with PSP device and register cleanup via defer().""" + _init_psp_dev(cfg, True) + + cfg.pspnl.dev_assoc({'id': cfg.psp_dev_id, + 'ifindex': cfg.nk_guest_ifindex, + 'nsid': cfg.psp_dev_peer_nsid}) + defer(_disassoc_nk_guest, cfg, + cfg.psp_dev_id, cfg.nk_guest_ifindex) + + +def _disassoc_nk_guest(cfg, psp_dev_id, nk_guest_ifindex): + """Disassociate nk_guest and reset cfg PSP state.""" + pspnl = PSPFamily() + pspnl.dev_disassoc({'id': psp_dev_id, 'ifindex': nk_guest_ifindex, + 'nsid': cfg.psp_dev_peer_nsid}) + cfg.pspnl = pspnl + del cfg.psp_dev_id + del cfg.psp_info + + +def _get_nsid(ns_name): + """Get the nsid for a namespace.""" + for entry in ip("netns list-id", json=True): + if entry.get("name") == str(ns_name): + return entry["nsid"] + raise KsftSkipEx(f"nsid not found for namespace {ns_name}") + + +def _setup_psp_attributes(cfg): + # pylint: disable=protected-access + """ + Set up PSP-specific attributes on the environment. + + This sets attributes needed for PSP tests based on whether we're using + netdevsim or a real NIC. + """ + if cfg._ns is not None: + # netdevsim case: PSP device is the local dev (in host namespace) + cfg.psp_dev = cfg._ns.nsims[0].dev + cfg.psp_ifname = cfg.psp_dev['ifname'] + cfg.psp_ifindex = cfg.psp_dev['ifindex'] + + # PSP peer device is the remote dev (in _netns, where psp_responder runs) + cfg.psp_dev_peer = cfg._ns_peer.nsims[0].dev + cfg.psp_dev_peer_ifname = cfg.psp_dev_peer['ifname'] + cfg.psp_dev_peer_ifindex = cfg.psp_dev_peer['ifindex'] + else: + # Real NIC case: PSP device is the local interface + cfg.psp_dev = cfg.dev + cfg.psp_ifname = cfg.ifname + cfg.psp_ifindex = cfg.ifindex + + # PSP peer device is the remote interface + cfg.psp_dev_peer = cfg.remote_dev + cfg.psp_dev_peer_ifname = cfg.remote_ifname + cfg.psp_dev_peer_ifindex = cfg.remote_ifindex + + # Get nsid for the guest namespace (netns) where nk_guest is + cfg.psp_dev_peer_nsid = _get_nsid(cfg.netns.name) + def main() -> None: """ Ksft boiler plate main """ - with NetDrvEpEnv(__file__) as cfg: + # Make sure LOCAL_PREFIX_V6 is set + if "LOCAL_PREFIX_V6" not in os.environ: + os.environ["LOCAL_PREFIX_V6"] = "2001:db8:2::" + + try: + env = NetDrvContEnv(__file__, primary_rx_redirect=True) + has_cont = True + except KsftSkipEx: + env = NetDrvEpEnv(__file__) + has_cont = False + + with env as cfg: cfg.pspnl = PSPFamily() + if has_cont: + _setup_psp_attributes(cfg) + # Set up responder and communication sock + # psp_responder runs in _netns (remote namespace with psp_dev_peer) responder = cfg.remote.deploy("psp_responder") cfg.comm_port = rand_port() @@ -611,17 +973,18 @@ def main() -> None: cfg.comm_port), timeout=1) - cases = [ - psp_ip_ver_test_builder( - "data_basic_send", _data_basic_send, version, ipver - ) - for version in range(0, 4) - for ipver in ("4", "6") - ] - cases += [ - ipver_test_builder("data_mss_adjust", _data_mss_adjust, ipver) - for ipver in ("4", "6") - ] + cases = [data_basic_send, data_mss_adjust] + + if has_cont: + cases += [ + _assoc_check_list, + data_basic_send_netkit_psp_assoc, + _key_rotation_notify_multi_ns_netkit, + _dev_change_notify_multi_ns_netkit, + _psp_dev_get_check_netkit_psp_assoc, + _dev_assoc_no_nsid, + _psp_dev_assoc_cleanup_on_netkit_del, + ] ksft_run(cases=cases, globs=globals(), case_pfx={"dev_", "data_", "assoc_", "removal_"}, |
