| /* |
| ************************************************************************** |
| * Copyright (c) 2019-2021, The Linux Foundation. All rights reserved. |
| * Permission to use, copy, modify, and/or distribute this software for |
| * any purpose with or without fee is hereby granted, provided that the |
| * above copyright notice and this permission notice appear in all copies. |
| * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT |
| * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| ************************************************************************** |
| */ |
| |
| #include <linux/if_ether.h> |
| #include <linux/in.h> |
| #include <linux/ip.h> |
| #include <linux/netdevice.h> |
| #include <linux/skbuff.h> |
| #include <linux/version.h> |
| #include <net/addrconf.h> |
| #include <net/dst.h> |
| #include <net/flow.h> |
| #include <net/ipv6.h> |
| #include <net/route.h> |
| #include <net/vxlan.h> |
| #include <nss_api_if.h> |
| #include "nss_vxlanmgr.h" |
| #include "nss_vxlanmgr_tun_stats.h" |
| |
| /* |
| * VxLAN context |
| */ |
| extern struct nss_vxlanmgr_ctx vxlan_ctx; |
| |
| /* |
| * nss_vxlanmgr_tunnel_ctx_dev_get() |
| * Find VxLAN tunnel context using netdev. |
| * Context lock must be held before calling this API. |
| */ |
| struct nss_vxlanmgr_tun_ctx *nss_vxlanmgr_tunnel_ctx_dev_get(struct net_device *dev) |
| { |
| struct nss_vxlanmgr_tun_ctx *tun_ctx; |
| |
| list_for_each_entry(tun_ctx, &vxlan_ctx.list, head) { |
| if (tun_ctx->dev == dev) { |
| return tun_ctx; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_tx_msg() |
| * Transmit VxLAN tunnel operation messages asynchronously. |
| */ |
| static nss_tx_status_t nss_vxlanmgr_tunnel_tx_msg(struct nss_ctx_instance *ctx, |
| struct nss_vxlan_msg *msg, |
| uint32_t if_num, |
| enum nss_vxlan_msg_type type, |
| uint32_t len) |
| { |
| nss_vxlan_msg_init(msg, if_num, type, len, NULL, NULL); |
| return nss_vxlan_tx_msg(ctx, msg); |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_tx_msg_sync() |
| * Transmit VxLAN tunnel operation messages. |
| */ |
| static nss_tx_status_t nss_vxlanmgr_tunnel_tx_msg_sync(struct nss_ctx_instance *ctx, |
| struct nss_vxlan_msg *msg, |
| uint32_t if_num, |
| enum nss_vxlan_msg_type type, |
| uint32_t len) |
| { |
| nss_vxlan_msg_init(msg, if_num, type, len, NULL, NULL); |
| return nss_vxlan_tx_msg_sync(ctx, msg); |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_flags_parse() |
| * Function to parse vxlan flags. |
| */ |
| #if (LINUX_VERSION_CODE <= KERNEL_VERSION(4, 5, 7)) |
| static uint16_t nss_vxlanmgr_tunnel_flags_parse(struct vxlan_dev *priv) |
| { |
| uint16_t flags = 0; |
| uint32_t priv_flags = priv->flags; |
| |
| if (priv_flags & VXLAN_F_RSC) |
| return flags; |
| if (priv_flags & VXLAN_F_GBP) |
| flags |= NSS_VXLAN_RULE_FLAG_GBP_ENABLED; |
| |
| if (priv_flags & VXLAN_F_IPV6) { |
| flags |= NSS_VXLAN_RULE_FLAG_IPV6; |
| if (!(priv_flags & VXLAN_F_UDP_ZERO_CSUM6_TX)) |
| flags |= NSS_VXLAN_RULE_FLAG_ENCAP_L4_CSUM_REQUIRED; |
| } else { |
| flags |= NSS_VXLAN_RULE_FLAG_IPV4; |
| if (priv_flags & VXLAN_F_UDP_CSUM) |
| flags |= NSS_VXLAN_RULE_FLAG_ENCAP_L4_CSUM_REQUIRED; |
| } |
| |
| if (priv->cfg.tos == 1) |
| flags |= NSS_VXLAN_RULE_FLAG_INHERIT_TOS; |
| |
| return (flags | NSS_VXLAN_RULE_FLAG_UDP); |
| } |
| #else |
| static uint16_t nss_vxlanmgr_tunnel_flags_parse(struct vxlan_dev *priv) |
| { |
| uint16_t flags = 0; |
| struct vxlan_config *cfg = &priv->cfg; |
| uint32_t priv_flags = cfg->flags; |
| |
| if (priv_flags & VXLAN_F_RSC) |
| return flags; |
| if (priv_flags & VXLAN_F_GPE) |
| return flags; |
| if (priv_flags & VXLAN_F_GBP) |
| flags |= NSS_VXLAN_RULE_FLAG_GBP_ENABLED; |
| |
| if (priv_flags & VXLAN_F_IPV6) { |
| flags |= NSS_VXLAN_RULE_FLAG_IPV6; |
| if (!(priv_flags & VXLAN_F_UDP_ZERO_CSUM6_TX)) |
| flags |= NSS_VXLAN_RULE_FLAG_ENCAP_L4_CSUM_REQUIRED; |
| } else { |
| flags |= NSS_VXLAN_RULE_FLAG_IPV4; |
| if (!(priv_flags & VXLAN_F_UDP_ZERO_CSUM_TX)) |
| flags |= NSS_VXLAN_RULE_FLAG_ENCAP_L4_CSUM_REQUIRED; |
| } |
| |
| if (cfg->tos == 1) |
| flags |= NSS_VXLAN_RULE_FLAG_INHERIT_TOS; |
| |
| return (flags | NSS_VXLAN_RULE_FLAG_UDP); |
| } |
| #endif |
| |
| /* |
| * nss_vxlanmgr_tunnel_fill_src_ip() |
| * Return src_ip using route lookup. |
| */ |
| static bool nss_vxlanmgr_tunnel_fill_src_ip(struct vxlan_dev *vxlan, |
| union vxlan_addr *src_ip, |
| union vxlan_addr *rem_ip, |
| sa_family_t sa_family, |
| uint32_t *new_src_ip) |
| { |
| struct flowi4 fl4; |
| struct flowi6 fl6; |
| struct rtable *rt = NULL; |
| #if (LINUX_VERSION_CODE <= KERNEL_VERSION(4, 5, 7)) |
| struct dst_entry *dst = NULL; |
| int err; |
| #else |
| const struct in6_addr *final_dst = NULL; |
| struct dst_entry *dentry; |
| #endif |
| |
| /* |
| * IPv4 |
| */ |
| if (sa_family == AF_INET) { |
| if (src_ip->sin.sin_addr.s_addr == htonl(INADDR_ANY)) { |
| /* |
| * Lookup |
| */ |
| memset(&fl4, 0, sizeof(fl4)); |
| fl4.flowi4_proto = IPPROTO_UDP; |
| fl4.daddr = rem_ip->sin.sin_addr.s_addr; |
| fl4.saddr = src_ip->sin.sin_addr.s_addr; |
| |
| rt = ip_route_output_key(vxlan->net, &fl4); |
| if (IS_ERR(rt)) { |
| nss_vxlanmgr_warn("No route available.\n"); |
| return false; |
| } |
| new_src_ip[0] = fl4.saddr; |
| return true; |
| } |
| new_src_ip[0] = src_ip->sin.sin_addr.s_addr; |
| return true; |
| } |
| |
| /* |
| * IPv6 |
| */ |
| if (ipv6_addr_any(&src_ip->sin6.sin6_addr)) { |
| /* |
| * Lookup |
| */ |
| memset(&fl6, 0, sizeof(fl6)); |
| fl6.flowi6_proto = IPPROTO_UDP; |
| fl6.daddr = rem_ip->sin6.sin6_addr; |
| fl6.saddr = src_ip->sin6.sin6_addr; |
| |
| #if (LINUX_VERSION_CODE <= KERNEL_VERSION(4, 5, 7)) |
| err = ipv6_stub->ipv6_dst_lookup(vxlan->net, |
| vxlan->vn6_sock->sock->sk, &dst, &fl6); |
| if (err < 0) { |
| #else |
| dentry = ipv6_stub->ipv6_dst_lookup_flow(vxlan->net, |
| vxlan->vn6_sock->sock->sk, &fl6, final_dst); |
| if (!dentry) { |
| #endif |
| nss_vxlanmgr_warn("No route, drop packet.\n"); |
| return false; |
| } |
| memcpy(new_src_ip, &fl6.saddr, sizeof(struct in6_addr)); |
| return true; |
| } |
| memcpy(new_src_ip, &src_ip->sin6.sin6_addr, sizeof(struct in6_addr)); |
| return true; |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_mac_del() |
| * VxLAN tunnel mac delete messages. |
| */ |
| static nss_tx_status_t nss_vxlanmgr_tunnel_mac_del(struct nss_vxlanmgr_tun_ctx *tun_ctx, |
| struct vxlan_fdb_event *vfe) |
| { |
| struct net_device *dev; |
| struct nss_vxlan_mac_msg *mac_del_msg; |
| struct nss_vxlan_msg vxlanmsg; |
| struct vxlan_config *cfg; |
| struct vxlan_dev *priv; |
| union vxlan_addr *remote_ip, *src_ip; |
| uint32_t i, inner_ifnum; |
| nss_tx_status_t status = NSS_TX_FAILURE; |
| |
| dev = vfe->dev; |
| dev_hold(dev); |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| inner_ifnum = tun_ctx->inner_ifnum; |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| |
| /* |
| * Only non-zero mac entries should be sent to NSS. |
| */ |
| if (is_zero_ether_addr(vfe->eth_addr)) { |
| nss_vxlanmgr_trace("Only non-zero mac entries should be sent to NSS.\n"); |
| goto done; |
| } |
| |
| memset(&vxlanmsg, 0, sizeof(struct nss_vxlan_msg)); |
| priv = netdev_priv(dev); |
| |
| /* |
| * Set MAC rule message |
| */ |
| mac_del_msg = &vxlanmsg.msg.mac_del; |
| mac_del_msg->vni = vxlan_get_vni(priv); |
| ether_addr_copy((uint8_t *)mac_del_msg->mac_addr, (uint8_t *)vfe->eth_addr); |
| |
| cfg = &priv->cfg; |
| src_ip = &cfg->saddr; |
| remote_ip = &vfe->rdst->remote_ip; |
| |
| if (remote_ip->sa.sa_family == AF_INET){ |
| if (remote_ip->sin.sin_addr.s_addr == htonl(INADDR_ANY)) { |
| nss_vxlanmgr_warn("%px: MAC deletion failed for unknown remote\n", dev); |
| goto done; |
| } |
| memcpy(&mac_del_msg->encap.dest_ip, &remote_ip->sin.sin_addr, sizeof(struct in_addr)); |
| memcpy(&mac_del_msg->encap.src_ip, &src_ip->sin.sin_addr, sizeof(struct in_addr)); |
| } else { |
| if (ipv6_addr_any(&remote_ip->sin6.sin6_addr)) { |
| nss_vxlanmgr_warn("%px: MAC deletion failed for unknown remote\n", dev); |
| goto done; |
| } |
| memcpy(&mac_del_msg->encap.dest_ip, &remote_ip->sin6.sin6_addr, sizeof(struct in6_addr)); |
| memcpy(&mac_del_msg->encap.src_ip, &src_ip->sin6.sin6_addr, sizeof(struct in6_addr)); |
| } |
| |
| /* |
| * Send MAC del message asynchronously as it is called by chain |
| * notifier in atomic context from the vxlan driver. |
| */ |
| status = nss_vxlanmgr_tunnel_tx_msg(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| inner_ifnum, |
| NSS_VXLAN_MSG_TYPE_MAC_DEL, |
| sizeof(struct nss_vxlan_mac_msg)); |
| if (status != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: MAC deletion failed %d\n", dev, status); |
| } |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| for (i = 0; i < NSS_VXLAN_MACDB_ENTRIES_MAX; i++) { |
| if (ether_addr_equal((uint8_t *)&tun_ctx->stats->mac_stats[i][0], (uint8_t *)vfe->eth_addr)) { |
| tun_ctx->stats->mac_stats[i][0] = 0; |
| tun_ctx->stats->mac_stats[i][1] = 0; |
| break; |
| } |
| } |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| |
| done: |
| dev_put(dev); |
| return status; |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_mac_add() |
| * VxLAN tunnel mac add messages. |
| */ |
| static nss_tx_status_t nss_vxlanmgr_tunnel_mac_add(struct nss_vxlanmgr_tun_ctx *tun_ctx, |
| struct vxlan_fdb_event *vfe) |
| { |
| struct net_device *dev; |
| struct nss_vxlan_mac_msg *mac_add_msg; |
| struct nss_vxlan_msg vxlanmsg; |
| struct vxlan_config *cfg; |
| struct vxlan_dev *priv; |
| union vxlan_addr *remote_ip, *src_ip; |
| uint32_t i, inner_ifnum; |
| uint32_t new_src_ip[4] = {0}; |
| nss_tx_status_t status = NSS_TX_FAILURE; |
| |
| dev = vfe->dev; |
| dev_hold(dev); |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| inner_ifnum = tun_ctx->inner_ifnum; |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| |
| /* |
| * Only non-zero mac entries should be sent to NSS. |
| */ |
| if (is_zero_ether_addr(vfe->eth_addr)) { |
| nss_vxlanmgr_trace("Only non-zero mac entries should be sent to NSS.\n"); |
| goto done; |
| } |
| |
| memset(&vxlanmsg, 0, sizeof(struct nss_vxlan_msg)); |
| priv = netdev_priv(dev); |
| |
| /* |
| * Set MAC rule message |
| */ |
| mac_add_msg = &vxlanmsg.msg.mac_add; |
| mac_add_msg->vni = vxlan_get_vni(priv); |
| ether_addr_copy((uint8_t *)mac_add_msg->mac_addr, (uint8_t *)vfe->eth_addr); |
| |
| cfg = &priv->cfg; |
| src_ip = &cfg->saddr; |
| remote_ip = &vfe->rdst->remote_ip; |
| |
| if (remote_ip->sa.sa_family == AF_INET){ |
| if (remote_ip->sin.sin_addr.s_addr == htonl(INADDR_ANY)) { |
| nss_vxlanmgr_warn("%px: MAC addition failed for unknown remote\n", dev); |
| goto done; |
| } |
| memcpy(&mac_add_msg->encap.dest_ip[0], &remote_ip->sin.sin_addr, sizeof(struct in_addr)); |
| if (!nss_vxlanmgr_tunnel_fill_src_ip(priv, src_ip, remote_ip, AF_INET, new_src_ip)) { |
| nss_vxlanmgr_warn("%px: MAC addition failed for unknown source\n", dev); |
| goto done; |
| } |
| mac_add_msg->encap.src_ip[0] = new_src_ip[0]; |
| } else { |
| if (ipv6_addr_any(&remote_ip->sin6.sin6_addr)) { |
| nss_vxlanmgr_warn("%px: MAC addition failed for unknown remote\n", dev); |
| goto done; |
| } |
| memcpy(mac_add_msg->encap.dest_ip, &remote_ip->sin6.sin6_addr, sizeof(struct in6_addr)); |
| if (!nss_vxlanmgr_tunnel_fill_src_ip(priv, src_ip, remote_ip, AF_INET6, new_src_ip)) { |
| nss_vxlanmgr_warn("%px: MAC addition failed for unknown source\n", dev); |
| goto done; |
| } |
| memcpy(mac_add_msg->encap.src_ip, new_src_ip, sizeof(struct in6_addr)); |
| } |
| |
| /* |
| * Send MAC add message asynchronously as it is called by chain |
| * notifier in atomic context from the vxlan driver. |
| */ |
| status = nss_vxlanmgr_tunnel_tx_msg(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| inner_ifnum, |
| NSS_VXLAN_MSG_TYPE_MAC_ADD, |
| sizeof(struct nss_vxlan_mac_msg)); |
| if (status != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: MAC addition failed %d\n", dev, status); |
| goto done; |
| } |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| for (i = 0; i < NSS_VXLAN_MACDB_ENTRIES_MAX; i++) { |
| if (!tun_ctx->stats->mac_stats[i][0]) { |
| ether_addr_copy((uint8_t *)&tun_ctx->stats->mac_stats[i][0], |
| (uint8_t *)vfe->eth_addr); |
| break; |
| } |
| } |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| |
| done: |
| dev_put(dev); |
| return status; |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_fdb_event() |
| * Event handler for VxLAN fdb updates |
| */ |
| static int nss_vxlanmgr_tunnel_fdb_event(struct notifier_block *nb, unsigned long event, void *data) |
| { |
| struct vxlan_fdb_event *vfe; |
| struct nss_vxlanmgr_tun_ctx *tun_ctx; |
| |
| vfe = (struct vxlan_fdb_event *)data; |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| tun_ctx = nss_vxlanmgr_tunnel_ctx_dev_get(vfe->dev); |
| if (!tun_ctx) { |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_warn("%px: Invalid tunnel context\n", vfe->dev); |
| return NOTIFY_DONE; |
| } |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| |
| switch(event) { |
| case RTM_DELNEIGH: |
| nss_vxlanmgr_tunnel_mac_del(tun_ctx, vfe); |
| break; |
| case RTM_NEWNEIGH: |
| nss_vxlanmgr_tunnel_mac_add(tun_ctx, vfe); |
| break; |
| default: |
| nss_vxlanmgr_warn("%lu: Unknown FDB event received.\n", event); |
| } |
| return NOTIFY_DONE; |
| } |
| |
| /* |
| * Notifier to receive fdb events from VxLAN |
| */ |
| static struct notifier_block nss_vxlanmgr_tunnel_fdb_notifier = { |
| .notifier_call = nss_vxlanmgr_tunnel_fdb_event, |
| }; |
| |
| /* |
| * nss_vxlanmgr_tunnel_inner_stats() |
| * Update vxlan netdev stats with inner node stats |
| */ |
| static void nss_vxlanmgr_tunnel_inner_stats(struct nss_vxlanmgr_tun_ctx *tun_ctx, struct nss_vxlan_msg *nvm) |
| { |
| struct nss_vxlan_stats_msg *stats; |
| struct pcpu_sw_netstats *tstats; |
| struct net_device *dev; |
| struct net_device_stats *netdev_stats; |
| uint32_t i; |
| uint64_t dropped = 0; |
| |
| stats = &nvm->msg.stats; |
| dev = tun_ctx->dev; |
| |
| dev_hold(dev); |
| netdev_stats = (struct net_device_stats *)&dev->stats; |
| |
| /* |
| * Only look at the tx_packets/tx_bytes for both host_inner/outer interfaces. |
| * rx_bytes/rx_packets are increased when the packet is received by the node. |
| * Therefore, it includes both transmitted/dropped packets. tx_bytes/tx_packets |
| * reflect successfully transmitted packets. |
| */ |
| for (i = 0; i < NSS_MAX_NUM_PRI; i++) { |
| dropped += stats->node_stats.rx_dropped[i]; |
| } |
| |
| tstats = this_cpu_ptr(dev->tstats); |
| u64_stats_update_begin(&tstats->syncp); |
| tstats->tx_packets += stats->node_stats.tx_packets; |
| tstats->tx_bytes += stats->node_stats.tx_bytes; |
| u64_stats_update_end(&tstats->syncp); |
| netdev_stats->tx_dropped += dropped; |
| dev_put(dev); |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_outer_stats() |
| * Update vxlan netdev stats with outer node stats |
| */ |
| static void nss_vxlanmgr_tunnel_outer_stats(struct nss_vxlanmgr_tun_ctx *tun_ctx, struct nss_vxlan_msg *nvm) |
| { |
| struct nss_vxlan_stats_msg *stats; |
| struct pcpu_sw_netstats *tstats; |
| struct net_device *dev; |
| struct net_device_stats *netdev_stats; |
| uint32_t i; |
| uint64_t dropped = 0; |
| |
| stats = &nvm->msg.stats; |
| dev = tun_ctx->dev; |
| |
| dev_hold(dev); |
| netdev_stats = (struct net_device_stats *)&dev->stats; |
| |
| /* |
| * Only look at the tx_packets/tx_bytes for both host_inner/outer interfaces. |
| * rx_bytes/rx_packets are increased when the packet is received by the node. |
| * Therefore, it includes both transmitted/dropped packets. tx_bytes/tx_packets |
| * reflect successfully transmitted packets. |
| */ |
| for (i = 0; i < NSS_MAX_NUM_PRI; i++) { |
| dropped += stats->node_stats.rx_dropped[i]; |
| } |
| |
| tstats = this_cpu_ptr(dev->tstats); |
| u64_stats_update_begin(&tstats->syncp); |
| tstats->rx_packets += stats->node_stats.tx_packets; |
| tstats->rx_bytes += stats->node_stats.tx_bytes; |
| u64_stats_update_end(&tstats->syncp); |
| netdev_stats->rx_dropped += dropped; |
| dev_put(dev); |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_fdb_update() |
| * Update vxlan fdb entries |
| */ |
| static void nss_vxlanmgr_tunnel_fdb_update(struct nss_vxlanmgr_tun_ctx *tun_ctx, struct nss_vxlan_msg *nvm) |
| { |
| uint8_t *mac; |
| uint16_t i, nentries; |
| struct vxlan_dev *priv; |
| struct nss_vxlan_macdb_stats_msg *db_stats; |
| |
| db_stats = &nvm->msg.db_stats; |
| nentries = db_stats->cnt; |
| priv = netdev_priv(tun_ctx->dev); |
| |
| dev_hold(tun_ctx->dev); |
| |
| if (nentries > NSS_VXLAN_MACDB_ENTRIES_PER_MSG) { |
| nss_vxlanmgr_warn("%px: No more than 20 entries allowed per message.\n", tun_ctx->dev); |
| dev_put(tun_ctx->dev); |
| return; |
| } |
| |
| for (i = 0; i < nentries; i++) { |
| if (likely(db_stats->entry[i].hits)) { |
| mac = (uint8_t *)db_stats->entry[i].mac; |
| #if (LINUX_VERSION_CODE <= KERNEL_VERSION(4, 5, 7)) |
| vxlan_fdb_update_mac(priv, mac); |
| #else |
| vxlan_fdb_update_mac(priv, mac, tun_ctx->vni); |
| #endif |
| } |
| } |
| dev_put(tun_ctx->dev); |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_inner_notifier() |
| * Notifier for vxlan tunnel encap node |
| */ |
| static void nss_vxlanmgr_tunnel_inner_notifier(void *app_data, struct nss_cmn_msg *ncm) |
| { |
| struct net_device *dev = (struct net_device *)app_data; |
| struct nss_vxlanmgr_tun_ctx *tun_ctx; |
| struct nss_vxlan_msg *nvm; |
| |
| if (!ncm) { |
| nss_vxlanmgr_info("%px: NULL msg received.\n", dev); |
| return; |
| } |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| tun_ctx = nss_vxlanmgr_tunnel_ctx_dev_get(dev); |
| if (!tun_ctx) { |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_warn("%px: Invalid tunnel context\n", dev); |
| return; |
| } |
| |
| nvm = (struct nss_vxlan_msg *)ncm; |
| switch (nvm->cm.type) { |
| case NSS_VXLAN_MSG_TYPE_STATS_SYNC: |
| nss_vxlanmgr_tunnel_inner_stats(tun_ctx, nvm); |
| nss_vxlanmgr_tun_stats_sync(tun_ctx, nvm); |
| break; |
| case NSS_VXLAN_MSG_TYPE_MACDB_STATS: |
| nss_vxlanmgr_tunnel_fdb_update(tun_ctx, nvm); |
| nss_vxlanmgr_tun_macdb_stats_sync(tun_ctx, nvm); |
| break; |
| default: |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_info("%px: Unknown Event from NSS", dev); |
| return; |
| } |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_outer_notifier() |
| * Notifier for vxlan tunnel decap node |
| */ |
| static void nss_vxlanmgr_tunnel_outer_notifier(void *app_data, struct nss_cmn_msg *ncm) |
| { |
| struct net_device *dev = (struct net_device *)app_data; |
| struct nss_vxlanmgr_tun_ctx *tun_ctx; |
| struct nss_vxlan_msg *nvm; |
| |
| if (!ncm) { |
| nss_vxlanmgr_info("%px: NULL msg received.\n", dev); |
| return; |
| } |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| tun_ctx = nss_vxlanmgr_tunnel_ctx_dev_get(dev); |
| if (!tun_ctx) { |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_warn("%px: Invalid tunnel context\n", dev); |
| return; |
| } |
| |
| nvm = (struct nss_vxlan_msg *)ncm; |
| switch (nvm->cm.type) { |
| case NSS_VXLAN_MSG_TYPE_STATS_SYNC: |
| nss_vxlanmgr_tunnel_outer_stats(tun_ctx, nvm); |
| nss_vxlanmgr_tun_stats_sync(tun_ctx, nvm); |
| break; |
| default: |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_info("%px: Unknown Event from NSS", dev); |
| return; |
| } |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_inner_recv() |
| * Receives a pkt from NSS |
| */ |
| static void nss_vxlanmgr_tunnel_inner_recv(struct net_device *dev, struct sk_buff *skb, |
| __attribute__((unused)) struct napi_struct *napi) |
| { |
| dev_hold(dev); |
| nss_vxlanmgr_info("%px: (vxlan packet) Exception packet received.\n", dev); |
| |
| /* |
| * These are decapped and exceptioned packets. |
| */ |
| skb->protocol = eth_type_trans(skb, dev); |
| netif_receive_skb(skb); |
| dev_put(dev); |
| return; |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_outer_recv() |
| * Receives a pkt from NSS |
| */ |
| static void nss_vxlanmgr_tunnel_outer_recv(struct net_device *dev, struct sk_buff *skb, |
| __attribute__((unused)) struct napi_struct *napi) |
| { |
| struct iphdr *iph; |
| size_t l3_hdr_size; |
| struct nss_vxlanmgr_tun_ctx *tun_ctx; |
| |
| nss_vxlanmgr_info("%px: (vxlan packet) Exception packet received.\n", dev); |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| tun_ctx = nss_vxlanmgr_tunnel_ctx_dev_get(dev); |
| if (!tun_ctx) { |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_warn("%px: Invalid tunnel context\n", dev); |
| dev_kfree_skb_any(skb); |
| return; |
| } |
| |
| iph = (struct iphdr *)skb->data; |
| switch (iph->version) { |
| case 4: |
| l3_hdr_size = sizeof(struct iphdr); |
| skb->protocol = htons(ETH_P_IP); |
| break; |
| case 6: |
| l3_hdr_size = sizeof(struct ipv6hdr); |
| skb->protocol = htons(ETH_P_IPV6); |
| break; |
| default: |
| tun_ctx->stats->host_packet_drop++; |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_trace("%px: Skb received with unknown IP version: %d.\n", dev, iph->version); |
| dev_kfree_skb_any(skb); |
| return; |
| } |
| |
| /* |
| * VxLAN encapsulated packet exceptioned, remove the encapsulation |
| * and transmit on VxLAN interface. |
| */ |
| if (unlikely(!pskb_may_pull(skb, (l3_hdr_size + sizeof(struct udphdr) |
| + sizeof(struct vxlanhdr))))) { |
| tun_ctx->stats->host_packet_drop++; |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_trace("%px: pskb_may_pull failed for skb:%px\n", dev, skb); |
| dev_kfree_skb_any(skb); |
| return; |
| } |
| |
| skb_pull(skb, (l3_hdr_size + sizeof(struct udphdr) + sizeof(struct vxlanhdr))); |
| |
| /* |
| * Inner ethernet payload. |
| */ |
| if (unlikely(!pskb_may_pull(skb, sizeof(struct ethhdr)))) { |
| tun_ctx->stats->host_packet_drop++; |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_trace("%px: pskb_may_pull failed for skb:%px\n", dev, skb); |
| dev_kfree_skb_any(skb); |
| return; |
| } |
| |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| skb->dev = dev; |
| skb_reset_mac_header(skb); |
| skb_reset_network_header(skb); |
| skb_reset_transport_header(skb); |
| skb_reset_mac_len(skb); |
| dev_queue_xmit(skb); |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_deconfig() |
| * Function to send dynamic interface disable message |
| */ |
| int nss_vxlanmgr_tunnel_deconfig(struct net_device *dev) |
| { |
| struct nss_vxlanmgr_tun_ctx *tun_ctx; |
| uint32_t inner_ifnum, outer_ifnum; |
| struct nss_vxlan_msg vxlanmsg; |
| nss_tx_status_t ret; |
| |
| dev_hold(dev); |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| tun_ctx = nss_vxlanmgr_tunnel_ctx_dev_get(dev); |
| if (!tun_ctx) { |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_warn("%px: Invalid tunnel context\n", dev); |
| goto done; |
| } |
| |
| inner_ifnum = tun_ctx->inner_ifnum; |
| outer_ifnum = tun_ctx->outer_ifnum; |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| |
| memset(&vxlanmsg, 0, sizeof(struct nss_vxlan_msg)); |
| |
| ret = nss_vxlanmgr_tunnel_tx_msg_sync(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| inner_ifnum, |
| NSS_VXLAN_MSG_TYPE_TUN_DISABLE, 0); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Sending configuration to inner interface failed: %d\n", dev, ret); |
| goto done; |
| } |
| |
| ret = nss_vxlanmgr_tunnel_tx_msg_sync(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| outer_ifnum, |
| NSS_VXLAN_MSG_TYPE_TUN_DISABLE, 0); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Sending configuration to outer interface failed: %d\n", dev, ret); |
| } |
| |
| done: |
| dev_put(dev); |
| return NOTIFY_DONE; |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_config() |
| * Function to send dynamic interface enable message |
| */ |
| int nss_vxlanmgr_tunnel_config(struct net_device *dev) |
| { |
| uint32_t inner_ifnum, outer_ifnum; |
| struct nss_vxlanmgr_tun_ctx *tun_ctx; |
| struct nss_vxlan_msg vxlanmsg; |
| nss_tx_status_t ret; |
| |
| dev_hold(dev); |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| tun_ctx = nss_vxlanmgr_tunnel_ctx_dev_get(dev); |
| if (!tun_ctx) { |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_warn("%px: Invalid tunnel context\n", dev); |
| goto done; |
| } |
| |
| inner_ifnum = tun_ctx->inner_ifnum; |
| outer_ifnum = tun_ctx->outer_ifnum; |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| |
| memset(&vxlanmsg, 0, sizeof(struct nss_vxlan_msg)); |
| |
| ret = nss_vxlanmgr_tunnel_tx_msg_sync(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| inner_ifnum, |
| NSS_VXLAN_MSG_TYPE_TUN_ENABLE, 0); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Sending configuration to inner interface failed: %d\n", dev, ret); |
| goto done; |
| } |
| |
| ret = nss_vxlanmgr_tunnel_tx_msg_sync(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| outer_ifnum, |
| NSS_VXLAN_MSG_TYPE_TUN_ENABLE, 0); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Sending configuration to outer interface failed: %d\n", dev, ret); |
| /* |
| * Disable inner node. |
| */ |
| nss_vxlanmgr_tunnel_tx_msg_sync(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| inner_ifnum, |
| NSS_VXLAN_MSG_TYPE_TUN_DISABLE, 0); |
| } |
| |
| done: |
| dev_put(dev); |
| return NOTIFY_DONE; |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_destroy() |
| * Function to unregister and destroy dynamic interfaces. |
| */ |
| int nss_vxlanmgr_tunnel_destroy(struct net_device *dev) |
| { |
| uint32_t inner_ifnum, outer_ifnum; |
| struct nss_vxlanmgr_tun_ctx *tun_ctx; |
| struct nss_vxlan_msg vxlanmsg; |
| nss_tx_status_t ret; |
| |
| dev_hold(dev); |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| if (!vxlan_ctx.tun_count) { |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_warn("%px: No more tunnels to destroy.\n", dev); |
| goto done; |
| } |
| |
| tun_ctx = nss_vxlanmgr_tunnel_ctx_dev_get(dev); |
| if (!tun_ctx) { |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_warn("%px: Invalid tunnel context\n", dev); |
| goto done; |
| } |
| |
| inner_ifnum = tun_ctx->inner_ifnum; |
| outer_ifnum = tun_ctx->outer_ifnum; |
| |
| /* |
| * Remove tunnel from global list. |
| */ |
| list_del(&tun_ctx->head); |
| |
| /* |
| * Decrement interface count. |
| */ |
| vxlan_ctx.tun_count--; |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| |
| nss_vxlanmgr_tun_stats_deinit(tun_ctx); |
| nss_vxlanmgr_tun_stats_dentry_remove(tun_ctx); |
| kfree(tun_ctx); |
| |
| if (!vxlan_ctx.tun_count) { |
| /* |
| * Unregister fdb notifier chain if |
| * all vxlan tunnels are destroyed. |
| */ |
| vxlan_fdb_unregister_notify(&nss_vxlanmgr_tunnel_fdb_notifier); |
| } |
| nss_vxlanmgr_info("%px: VxLAN interface count is #%d\n", dev, vxlan_ctx.tun_count); |
| |
| memset(&vxlanmsg, 0, sizeof(struct nss_vxlan_msg)); |
| ret = nss_vxlanmgr_tunnel_tx_msg_sync(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| inner_ifnum, |
| NSS_VXLAN_MSG_TYPE_TUN_UNCONFIGURE, 0); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Sending configuration to inner interface failed: %d\n", dev, ret); |
| } |
| |
| if (!nss_vxlan_unregister_if(inner_ifnum)) { |
| nss_vxlanmgr_warn("%px: Inner interface not found\n", dev); |
| } |
| ret = nss_dynamic_interface_dealloc_node(inner_ifnum, |
| NSS_DYNAMIC_INTERFACE_TYPE_VXLAN_INNER); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Failed to dealloc inner: %d\n", dev, ret); |
| } |
| |
| ret = nss_vxlanmgr_tunnel_tx_msg_sync(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| outer_ifnum, |
| NSS_VXLAN_MSG_TYPE_TUN_UNCONFIGURE, 0); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Sending configuration to outer interface failed: %d\n", dev, ret); |
| } |
| |
| if (!nss_vxlan_unregister_if(outer_ifnum)) { |
| nss_vxlanmgr_warn("%px: Outer interface not found\n", dev); |
| } |
| ret = nss_dynamic_interface_dealloc_node(outer_ifnum, |
| NSS_DYNAMIC_INTERFACE_TYPE_VXLAN_OUTER); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Failed to dealloc outer: %d\n", dev, ret); |
| } |
| |
| done: |
| dev_put(dev); |
| return NOTIFY_DONE; |
| } |
| |
| /* |
| * nss_vxlanmgr_tunnel_create() |
| * Function to create and register dynamic interfaces. |
| */ |
| int nss_vxlanmgr_tunnel_create(struct net_device *dev) |
| { |
| struct vxlan_dev *priv; |
| struct nss_vxlan_msg vxlanmsg; |
| struct nss_vxlanmgr_tun_ctx *tun_ctx; |
| struct nss_vxlan_rule_msg *vxlan_cfg; |
| struct nss_ctx_instance *nss_ctx; |
| uint32_t inner_ifnum, outer_ifnum; |
| uint16_t parse_flags; |
| nss_tx_status_t ret; |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| if (vxlan_ctx.tun_count == NSS_VXLAN_MAX_TUNNELS) { |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_warn("%px: Max number of vxlan interfaces supported is %d\n", dev, NSS_VXLAN_MAX_TUNNELS); |
| return NOTIFY_DONE; |
| } |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| |
| dev_hold(dev); |
| priv = netdev_priv(dev); |
| parse_flags = nss_vxlanmgr_tunnel_flags_parse(priv); |
| |
| /* |
| * Check if the tunnel is supported. |
| */ |
| if (!parse_flags) { |
| nss_vxlanmgr_warn("%px: Tunnel offload not supported\n", dev); |
| goto ctx_alloc_fail; |
| } |
| |
| tun_ctx = kzalloc(sizeof(struct nss_vxlanmgr_tun_ctx), GFP_ATOMIC); |
| if (!tun_ctx) { |
| nss_vxlanmgr_warn("Failed to allocate memory for tun_ctx\n"); |
| goto ctx_alloc_fail; |
| } |
| tun_ctx->dev = dev; |
| tun_ctx->vxlan_ctx = &vxlan_ctx; |
| INIT_LIST_HEAD(&tun_ctx->head); |
| |
| inner_ifnum = nss_dynamic_interface_alloc_node(NSS_DYNAMIC_INTERFACE_TYPE_VXLAN_INNER); |
| if (inner_ifnum < 0) { |
| nss_vxlanmgr_warn("%px: Inner interface allocation failed.\n", dev); |
| goto inner_alloc_fail; |
| } |
| tun_ctx->inner_ifnum = inner_ifnum; |
| |
| outer_ifnum = nss_dynamic_interface_alloc_node(NSS_DYNAMIC_INTERFACE_TYPE_VXLAN_OUTER); |
| if (outer_ifnum < 0) { |
| nss_vxlanmgr_warn("%px: Outer interface allocation failed.\n", dev); |
| goto outer_alloc_fail; |
| } |
| tun_ctx->outer_ifnum = outer_ifnum; |
| |
| /* |
| * Register vxlan tunnel with NSS |
| */ |
| nss_ctx = nss_vxlan_register_if(inner_ifnum, NSS_DYNAMIC_INTERFACE_TYPE_VXLAN_INNER, |
| nss_vxlanmgr_tunnel_inner_recv, |
| nss_vxlanmgr_tunnel_inner_notifier, dev, 0); |
| if (!nss_ctx) { |
| nss_vxlanmgr_warn("%px: Failed to register inner iface\n", dev); |
| goto inner_reg_fail; |
| } |
| |
| nss_ctx = nss_vxlan_register_if(outer_ifnum, NSS_DYNAMIC_INTERFACE_TYPE_VXLAN_OUTER, |
| nss_vxlanmgr_tunnel_outer_recv, |
| nss_vxlanmgr_tunnel_outer_notifier, dev, 0); |
| if (!nss_ctx) { |
| nss_vxlanmgr_warn("%px: Failed to register outer iface\n", dev); |
| goto outer_reg_fail; |
| } |
| |
| nss_vxlanmgr_trace("%px: Successfully registered inner and outer iface for VxLAN\n", dev); |
| |
| memset(&vxlanmsg, 0, sizeof(struct nss_vxlan_msg)); |
| vxlan_cfg = &vxlanmsg.msg.vxlan_create; |
| |
| vxlan_cfg->vni = vxlan_get_vni(priv); |
| vxlan_cfg->tunnel_flags = parse_flags; |
| vxlan_cfg->src_port_min = priv->cfg.port_min; |
| vxlan_cfg->src_port_max = priv->cfg.port_max; |
| vxlan_cfg->dest_port = priv->cfg.dst_port; |
| vxlan_cfg->tos = priv->cfg.tos; |
| vxlan_cfg->ttl = (priv->cfg.ttl ? priv->cfg.ttl : IPDEFTTL); |
| |
| vxlan_cfg->sibling_if_num = outer_ifnum; |
| ret = nss_vxlanmgr_tunnel_tx_msg_sync(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| inner_ifnum, |
| NSS_VXLAN_MSG_TYPE_TUN_CONFIGURE, |
| sizeof(struct nss_vxlan_rule_msg)); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Sending configuration to inner interface failed: %d\n", dev, ret); |
| goto config_fail; |
| } |
| |
| vxlan_cfg->sibling_if_num = inner_ifnum; |
| ret = nss_vxlanmgr_tunnel_tx_msg_sync(vxlan_ctx.nss_ctx, |
| &vxlanmsg, |
| outer_ifnum, |
| NSS_VXLAN_MSG_TYPE_TUN_CONFIGURE, |
| sizeof(struct nss_vxlan_rule_msg)); |
| if (ret != NSS_TX_SUCCESS) { |
| nss_vxlanmgr_warn("%px: Sending configuration to outer interface failed: %d\n", dev, ret); |
| goto config_fail; |
| } |
| |
| if (!nss_vxlanmgr_tun_stats_dentry_create(tun_ctx)) { |
| nss_vxlanmgr_warn("%px: Tun stats dentry init failed\n", vxlan_ctx.nss_ctx); |
| goto config_fail; |
| } |
| |
| if (!nss_vxlanmgr_tun_stats_init(tun_ctx)) { |
| nss_vxlanmgr_warn("%px: Tun stats init failed\n", vxlan_ctx.nss_ctx); |
| goto config_fail; |
| } |
| |
| tun_ctx->vni = vxlan_cfg->vni; |
| tun_ctx->tunnel_flags = vxlan_cfg->tunnel_flags; |
| tun_ctx->flow_label = vxlan_cfg->flow_label; |
| tun_ctx->src_port_min = vxlan_cfg->src_port_min; |
| tun_ctx->src_port_max = vxlan_cfg->src_port_max; |
| tun_ctx->dest_port = vxlan_cfg->dest_port; |
| tun_ctx->tos = vxlan_cfg->tos; |
| tun_ctx->ttl = vxlan_cfg->ttl; |
| |
| spin_lock_bh(&vxlan_ctx.tun_lock); |
| /* |
| * Add tunnel to global list. |
| */ |
| list_add(&tun_ctx->head, &vxlan_ctx.list); |
| |
| if (!vxlan_ctx.tun_count) { |
| /* |
| * Register with fdb notifier chain |
| * when first tunnel is created. |
| */ |
| vxlan_fdb_register_notify(&nss_vxlanmgr_tunnel_fdb_notifier); |
| } |
| |
| /* |
| * Increment vxlan tunnel interface count |
| */ |
| vxlan_ctx.tun_count++; |
| spin_unlock_bh(&vxlan_ctx.tun_lock); |
| nss_vxlanmgr_info("%px: VxLAN interface count is #%d\n", dev, vxlan_ctx.tun_count); |
| |
| dev_put(dev); |
| return NOTIFY_DONE; |
| |
| config_fail: |
| nss_vxlan_unregister_if(outer_ifnum); |
| outer_reg_fail: |
| nss_vxlan_unregister_if(inner_ifnum); |
| inner_reg_fail: |
| ret = nss_dynamic_interface_dealloc_node(outer_ifnum, NSS_DYNAMIC_INTERFACE_TYPE_VXLAN_OUTER); |
| if (ret != NSS_TX_SUCCESS) |
| nss_vxlanmgr_warn("%px: Outer interface dealloc failed: %d\n", dev, ret); |
| outer_alloc_fail: |
| ret = nss_dynamic_interface_dealloc_node(inner_ifnum, NSS_DYNAMIC_INTERFACE_TYPE_VXLAN_INNER); |
| if (ret != NSS_TX_SUCCESS) |
| nss_vxlanmgr_warn("%px: Inner interface dealloc failed: %d\n", dev, ret); |
| inner_alloc_fail: |
| kfree(tun_ctx); |
| ctx_alloc_fail: |
| dev_put(dev); |
| return NOTIFY_DONE; |
| } |