/* * Copyright (c) 2019 AVM GmbH . * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * l2topo.c * * Gather information on the l2 bridging topology and prepare it for the * platform backends. */ #include "offdp.h" #include #include #include #include #include #include #include #include #include #include #include #define BACKEND_LINK_NAME "backend" static struct socket *rtnl_sk; static struct kobject *l2fwd_kobj; static struct kset *bridges_kset; static struct kset *ports_kset; static enum offdp_rv br_ifinfo_bool_attr(int attr_type, const char *dbg_name, const struct nlmsghdr *nlh, bool *value) { const struct ifinfomsg *ifi; struct nlattr *nla_protinfo; struct nlattr *nla_attr; u8 *attr_value; nla_protinfo = nlmsg_find_attr(nlh, sizeof(*ifi), IFLA_PROTINFO); nla_attr = nla_protinfo ? nla_find_nested(nla_protinfo, attr_type) : NULL; attr_value = nla_attr ? nla_data(nla_attr) : NULL; pr_debug("%s: %s\n", dbg_name, attr_value ? (*attr_value ? "yes" : "no") : "unknown"); if (!attr_value) return OFFDP_ERR_NOAVAIL; *value = !!*attr_value; return OFFDP_SUCCESS; } static enum offdp_rv br_ifinfo_is_isolated(const struct nlmsghdr *nlh, bool *is_isolated) { return br_ifinfo_bool_attr(IFLA_BRPORT_ISOLATED, "isolated", nlh, is_isolated); } static enum offdp_rv br_ifinfo_hairpin(const struct nlmsghdr *nlh, bool *hairpin) { return br_ifinfo_bool_attr(IFLA_BRPORT_MODE, "hairpin", nlh, hairpin); } static enum offdp_rv br_ifinfo_software_fwd(const struct nlmsghdr *nlh, bool *software_fwd) { return br_ifinfo_bool_attr(IFLA_BRPORT_SOFTWARE_FWD, "software_fwd", nlh, software_fwd); } /* caller needs to call dev_put on port and master */ static enum offdp_rv br_ifinfo_get_netdevs(const struct nlmsghdr *nlh, struct net_device **port, struct net_device **master) { const struct ifinfomsg *ifi; int port_idx, master_idx; struct nlattr *nla_master_idx; ifi = nlmsg_data(nlh); if (ifi->ifi_family != AF_BRIDGE) return OFFDP_ERR_DEV_RESOLVE; port_idx = ifi->ifi_index; // TODO find type-specific helper nla_master_idx = nlmsg_find_attr(nlh, sizeof(*ifi), IFLA_MASTER); if (!nla_master_idx) { pr_debug("no master for netif idx %d\n", port_idx); return OFFDP_ERR_DEV_RESOLVE; } memcpy(&master_idx, nla_data(nla_master_idx), sizeof(master_idx)); if (!port_idx || !master_idx) return OFFDP_ERR_DEV_RESOLVE; *port = dev_get_by_index(&init_net, port_idx); if (!*port) return OFFDP_ERR_DEV_RESOLVE; *master = dev_get_by_index(&init_net, master_idx); if (!*master) { dev_put(*port); return OFFDP_ERR_DEV_RESOLVE; } return OFFDP_SUCCESS; } static void br_ifinfo_put_netdevs(struct net_device *port, struct net_device *master) { dev_put(port); dev_put(master); } struct offdp_bridge_port; struct offdp_bridge { struct kobject kobj; struct net_device *dev; unsigned long backend_handle; struct kobject *backend_kobj; }; static void bridge_release(struct kobject *kobj) { struct offdp_bridge *br = container_of(kobj, struct offdp_bridge, kobj); struct net_device *master; master = br->dev; kobject_put(br->backend_kobj); kfree(br); if (master) { /* Drop the permanent reference. */ dev_put(master); } return; } static struct kobj_type bridge_kobj_type = { .release = bridge_release, .sysfs_ops = &kobj_sysfs_ops, }; struct offdp_bridge_port { struct kobject kobj; struct offdp_bridge *br; struct net_device *dev; struct kobject *backend_kobj; }; static void port_release(struct kobject *kobj) { struct offdp_bridge_port *p = container_of(kobj, struct offdp_bridge_port, kobj); struct net_device *port; port = p->dev; sysfs_remove_link(&p->br->kobj, port->name); kobject_put(p->backend_kobj); kobject_put(&p->br->kobj); kfree(p); dev_put(port); } static struct kobj_type port_kobj_type = { .release = port_release, .sysfs_ops = &kobj_sysfs_ops, }; static enum offdp_rv bridge_add(struct net_device *dev, struct offdp_bridge **br_out) { struct offdp_bridge *br; enum offdp_rv rv; /* bridge is unknown */ br = kzalloc(sizeof(*br), GFP_ATOMIC); if (!br) { return OFFDP_ERR_MEM; } /* Claim a reference to dev, which is also a prerequisite for calling * kobject_put. */ dev_hold(dev); br->dev = dev; kobject_init(&br->kobj, &bridge_kobj_type); br->kobj.kset = kset_get(bridges_kset); br->kobj.name = kstrdup(dev->name, GFP_ATOMIC); if (kobject_add(&br->kobj, NULL, NULL)) { kobject_put(&br->kobj); return OFFDP_ERR_KOBJ; } rv = offdp_backend_bridge_add(dev, &br->backend_kobj); if (rv != OFFDP_SUCCESS) { kobject_put(&br->kobj); return rv; } if (br->backend_kobj) sysfs_create_link(&br->kobj, br->backend_kobj, BACKEND_LINK_NAME); *br_out = br; return OFFDP_SUCCESS; } static enum offdp_rv port_add(struct net_device *dev, struct offdp_bridge *br, struct offdp_bridge_port **p_out) { struct offdp_bridge_port *p; enum offdp_rv rv; p = kzalloc(sizeof(*p), GFP_ATOMIC); if (!p) return OFFDP_ERR_MEM; /* Claim a reference to dev, which is also a prerequisite for calling * kobject_put. */ dev_hold(dev); p->dev = dev; p->br = br; kobject_init(&p->kobj, &port_kobj_type); p->kobj.name = kstrdup(dev->name, GFP_ATOMIC); p->kobj.kset = ports_kset; if (kobject_add(&p->kobj, NULL, NULL)) { kobject_put(&p->kobj); return OFFDP_ERR_KOBJ; } /* Link from port to bridge and get a reference to br. */ sysfs_create_link(kobject_get(&br->kobj), &p->kobj, kobject_name(&p->kobj)); rv = offdp_backend_bridge_port_add(dev, br->backend_kobj, &p->backend_kobj); if (rv != OFFDP_SUCCESS) { kobject_put(&p->kobj); return rv; } sysfs_create_link(&p->kobj, p->backend_kobj, BACKEND_LINK_NAME); *p_out = p; return OFFDP_SUCCESS; } #define br_from_kobj(p) container_of((p), struct offdp_bridge, kobj) #define port_from_kobj(p) container_of((p), struct offdp_bridge_port, kobj) static enum offdp_rv event_newlink(const struct nlmsghdr *nlh) { enum offdp_rv rv; struct net_device *port, *master; struct offdp_bridge *br; struct offdp_bridge_port *p; struct kobject *br_kobj, *p_kobj; bool needs_isolation, hairpin, software_fwd; rv = br_ifinfo_get_netdevs(nlh, &port, &master); if (rv != OFFDP_SUCCESS) /* We do not care about this message and ignore it. */ return OFFDP_SUCCESS; br_kobj = kset_find_obj(bridges_kset, master->name); if (br_kobj) { br = br_from_kobj(br_kobj); } else { rv = bridge_add(master, &br); if (rv != OFFDP_SUCCESS) goto br_err; } p_kobj = kset_find_obj(ports_kset, port->name); p = p_kobj ? port_from_kobj(p_kobj) : NULL; if (p) { if (p->br != br) { pr_err("Port %s already known at some other bridge\n", port->name); rv = OFFDP_ERR_CONFLICT; } else { pr_debug("Port %s already known at bridge %s\n", port->name, master->name); rv = OFFDP_SUCCESS; } /* Drop reference created by kset_find_obj() */ kobject_put(p_kobj); } else { rv = port_add(port, br, &p); } /* Update port isolation state if known */ if (rv == OFFDP_SUCCESS && br_ifinfo_is_isolated(nlh, &needs_isolation) == OFFDP_SUCCESS) offdp_backend_bridge_port_isolate(p->backend_kobj, needs_isolation); if (rv == OFFDP_SUCCESS && br_ifinfo_software_fwd(nlh, &software_fwd) == OFFDP_SUCCESS) rv = offdp_backend_bridge_port_software_fwd(p->backend_kobj, software_fwd); if (rv == OFFDP_SUCCESS && br_ifinfo_hairpin(nlh, &hairpin) == OFFDP_SUCCESS) rv = offdp_backend_bridge_port_hairpin(p->backend_kobj, hairpin); /* Drop the lookup/creation reference. */ kobject_put(&br->kobj); br_err: br_ifinfo_put_netdevs(port, master); return rv; } static enum offdp_rv event_dellink(const struct nlmsghdr *nlh) { enum offdp_rv rv; struct net_device *port, *master; struct kobject *p_kobj; rv = br_ifinfo_get_netdevs(nlh, &port, &master); if (rv != OFFDP_SUCCESS) return rv; p_kobj = kset_find_obj(ports_kset, port->name); if (!p_kobj) { goto out; } /* Remove port from ports_kset and remove the its sysfs dir * recursively. */ kobject_del(p_kobj); /* Drop reference create by kobject_add() */ kobject_put(p_kobj); /* Drop reference created by kset_find_obj() */ kobject_put(p_kobj); out: br_ifinfo_put_netdevs(port, master); return OFFDP_SUCCESS; } enum offdp_rv offdp_l2fwd_unregister(const struct net_device *dev) { struct kobject *p_kobj; p_kobj = kset_find_obj(ports_kset, dev->name); if (p_kobj) { kobject_del(p_kobj); kobject_put(p_kobj); kobject_put(p_kobj); } return OFFDP_SUCCESS; } static void netlink_data_process_async(void *skb_ptr, async_cookie_t cookie) { int type; struct sk_buff *skb, *skbc; struct nlmsghdr *nlh; int remaining; /* Make room in sock ASAP. */ skb = skb_ptr; skbc = skb_clone(skb, GFP_KERNEL); if (skbc) { skb_free_datagram(skb->sk, skb); skb = skbc; } remaining = skb->len; async_synchronize_cookie(cookie); for (nlh = (struct nlmsghdr *)skb->data; nlh && NLMSG_OK(nlh, remaining); nlh = NLMSG_NEXT(nlh, remaining)) { if (!NLMSG_OK(nlh, remaining)) { pr_warn("Invalid netlink header data."); break; } type = nlh->nlmsg_type; pr_debug("Received netlink type %x.\n", type); switch (type) { case RTM_NEWLINK: errstat_track(edom, event_newlink(nlh)); break; case RTM_DELLINK: errstat_track(edom, event_dellink(nlh)); break; default: break; } } if (skb_ptr == skb) skb_free_datagram(skb->sk, skb); else consume_skb(skb); } #if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 15, 0) static void netlink_data_ready(struct sock *sk) #else static void netlink_data_ready(struct sock *sk, int bytes) #endif { struct sk_buff *skb; int rc = 0; /* Receive the data packet (blocking) */ skb = skb_recv_datagram(sk, 0, 0, &rc); if (rc || !skb) { pr_err("Failed on skb_recv_datagram(). rc=%d.", -rc); return; } async_schedule(netlink_data_process_async, skb); } #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 2, 0) #define sock_create_kern(args...) sock_create_kern(&init_net, args) #endif static enum offdp_rv offdp_rtnl_bind(void) { struct sock *sock = NULL; struct sockaddr_nl addr = { 0 }; /* Create a netlink socket for rtnl traffic */ int rc = sock_create_kern(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE, &rtnl_sk); if (rc) return OFFDP_ERR_NETLINK; /* Setup socket callback */ sock = rtnl_sk->sk; sock->sk_data_ready = netlink_data_ready; sock->sk_allocation = GFP_KERNEL; addr.nl_family = AF_NETLINK; addr.nl_pid = 0; /* in netlink_autobind() we trust */ addr.nl_groups = RTM_NEWLINK | RTM_DELLINK; rc = kernel_bind(rtnl_sk, (struct sockaddr *)&addr, sizeof(addr)); if (rc) { sock_release(rtnl_sk); return OFFDP_ERR_NETLINK; } return OFFDP_SUCCESS; } static enum offdp_rv offdp_rtnl_getlink(void) { struct sk_buff *skb; struct nlmsghdr *nlh; struct rtgenmsg *rtgen; struct msghdr msg; struct sockaddr_nl addr; struct kvec vec; int addr_len; int rv; /* get netlink pid from socket */ rv = kernel_getsockname(rtnl_sk, (struct sockaddr *)&addr, &addr_len); if (rv) return OFFDP_ERR_NETLINK; BUG_ON(addr_len != sizeof(addr)); /* * This could be any buffer, but choosing a SKB allows us to use some of * the kernel helpers for building a netlink message. */ skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_ATOMIC); if (!skb) return OFFDP_ERR_MEM; nlh = nlmsg_put(skb, addr.nl_pid, 1, RTM_GETLINK, sizeof(*nlh), NLM_F_REQUEST | NLM_F_DUMP); if (!nlh) return OFFDP_ERR_NETLINK; /* target ndo_bridge_getlink() */ rtgen = nlmsg_data(nlh); memset(rtgen, 0, sizeof(*rtgen)); rtgen->rtgen_family = AF_BRIDGE; /* This gets copied before sendmsg returns. */ vec.iov_base = skb->data; vec.iov_len = skb->len; memset(&msg, 0, sizeof(struct msghdr)); /* * Socket is not connected, so specify "kernel" as destination in * the message header. */ memset(&addr, 0, sizeof(addr)); addr.nl_family = AF_NETLINK; msg.msg_name = &addr; msg.msg_namelen = sizeof(addr); rv = kernel_sendmsg(rtnl_sk, &msg, &vec, 1, vec.iov_len); kfree_skb(skb); return rv < 0 ? OFFDP_ERR_NETLINK : OFFDP_SUCCESS; } enum offdp_rv offdp_l2fwd_init(void) { enum offdp_rv rv; l2fwd_kobj = kobject_create_and_add("l2_topology", offdp_kobj); bridges_kset = kset_create_and_add("bridges", NULL, l2fwd_kobj); ports_kset = kset_create_and_add("ports", NULL, l2fwd_kobj); rv = offdp_rtnl_bind(); if (rv != OFFDP_SUCCESS) return rv; /* learn the current topology */ rv = offdp_rtnl_getlink(); return rv; } static void kobject_put_async(void *kobj, async_cookie_t cookie) { kobject_put(kobj); } void offdp_l2fwd_exit(void) { struct kobject *kobj; async_cookie_t cookie = 0; sock_release(rtnl_sk); /* Drop the reference created by port_add, but do so outside of the * critical section to avoid a deadlock. */ spin_lock(&ports_kset->list_lock); list_for_each_entry (kobj, &ports_kset->list, entry) { cookie = async_schedule(kobject_put_async, kobj); } spin_unlock(&ports_kset->list_lock); kset_unregister(ports_kset); /* Bridges get dropped implicitly when all ports are removed. Just get * rid of the kset. */ kset_unregister(bridges_kset); kobject_put(l2fwd_kobj); async_synchronize_cookie(cookie); }