bridge: Add vlan support to static neighbors
[deliverable/linux.git] / net / bridge / br_fdb.c
index d9576e6de2b85c232c1dcde0e54469bac3397212..4b75ad43aa85782a60f8a3bee190e60c0a55b1ed 100644 (file)
@@ -23,6 +23,7 @@
 #include <linux/slab.h>
 #include <linux/atomic.h>
 #include <asm/unaligned.h>
+#include <linux/if_vlan.h>
 #include "br_private.h"
 
 static struct kmem_cache *br_fdb_cache __read_mostly;
@@ -67,11 +68,11 @@ static inline int has_expired(const struct net_bridge *br,
                time_before_eq(fdb->updated + hold_time(br), jiffies);
 }
 
-static inline int br_mac_hash(const unsigned char *mac)
+static inline int br_mac_hash(const unsigned char *mac, __u16 vid)
 {
-       /* use 1 byte of OUI cnd 3 bytes of NIC */
+       /* use 1 byte of OUI and 3 bytes of NIC */
        u32 key = get_unaligned((u32 *)(mac + 2));
-       return jhash_1word(key, fdb_salt) & (BR_HASH_SIZE - 1);
+       return jhash_2words(key, vid, fdb_salt) & (BR_HASH_SIZE - 1);
 }
 
 static void fdb_rcu_free(struct rcu_head *head)
@@ -132,7 +133,7 @@ void br_fdb_change_mac_address(struct net_bridge *br, const u8 *newaddr)
        struct net_bridge_fdb_entry *f;
 
        /* If old entry was unassociated with any port, then delete it. */
-       f = __br_fdb_get(br, br->dev->dev_addr);
+       f = __br_fdb_get(br, br->dev->dev_addr, 0);
        if (f && f->is_local && !f->dst)
                fdb_delete(br, f);
 
@@ -231,13 +232,16 @@ void br_fdb_delete_by_port(struct net_bridge *br,
 
 /* No locking or refcounting, assumes caller has rcu_read_lock */
 struct net_bridge_fdb_entry *__br_fdb_get(struct net_bridge *br,
-                                         const unsigned char *addr)
+                                         const unsigned char *addr,
+                                         __u16 vid)
 {
        struct hlist_node *h;
        struct net_bridge_fdb_entry *fdb;
 
-       hlist_for_each_entry_rcu(fdb, h, &br->hash[br_mac_hash(addr)], hlist) {
-               if (ether_addr_equal(fdb->addr.addr, addr)) {
+       hlist_for_each_entry_rcu(fdb, h,
+                               &br->hash[br_mac_hash(addr, vid)], hlist) {
+               if (ether_addr_equal(fdb->addr.addr, addr) &&
+                   fdb->vlan_id == vid) {
                        if (unlikely(has_expired(br, fdb)))
                                break;
                        return fdb;
@@ -261,7 +265,7 @@ int br_fdb_test_addr(struct net_device *dev, unsigned char *addr)
        if (!port)
                ret = 0;
        else {
-               fdb = __br_fdb_get(port->br, addr);
+               fdb = __br_fdb_get(port->br, addr, 0);
                ret = fdb && fdb->dst && fdb->dst->dev != dev &&
                        fdb->dst->state == BR_STATE_FORWARDING;
        }
@@ -325,26 +329,30 @@ int br_fdb_fillbuf(struct net_bridge *br, void *buf,
 }
 
 static struct net_bridge_fdb_entry *fdb_find(struct hlist_head *head,
-                                            const unsigned char *addr)
+                                            const unsigned char *addr,
+                                            __u16 vid)
 {
        struct hlist_node *h;
        struct net_bridge_fdb_entry *fdb;
 
        hlist_for_each_entry(fdb, h, head, hlist) {
-               if (ether_addr_equal(fdb->addr.addr, addr))
+               if (ether_addr_equal(fdb->addr.addr, addr) &&
+                   fdb->vlan_id == vid)
                        return fdb;
        }
        return NULL;
 }
 
 static struct net_bridge_fdb_entry *fdb_find_rcu(struct hlist_head *head,
-                                                const unsigned char *addr)
+                                                const unsigned char *addr,
+                                                __u16 vid)
 {
        struct hlist_node *h;
        struct net_bridge_fdb_entry *fdb;
 
        hlist_for_each_entry_rcu(fdb, h, head, hlist) {
-               if (ether_addr_equal(fdb->addr.addr, addr))
+               if (ether_addr_equal(fdb->addr.addr, addr) &&
+                   fdb->vlan_id == vid)
                        return fdb;
        }
        return NULL;
@@ -352,7 +360,8 @@ static struct net_bridge_fdb_entry *fdb_find_rcu(struct hlist_head *head,
 
 static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head,
                                               struct net_bridge_port *source,
-                                              const unsigned char *addr)
+                                              const unsigned char *addr,
+                                              __u16 vid)
 {
        struct net_bridge_fdb_entry *fdb;
 
@@ -360,6 +369,7 @@ static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head,
        if (fdb) {
                memcpy(fdb->addr.addr, addr, ETH_ALEN);
                fdb->dst = source;
+               fdb->vlan_id = vid;
                fdb->is_local = 0;
                fdb->is_static = 0;
                fdb->updated = fdb->used = jiffies;
@@ -371,13 +381,13 @@ static struct net_bridge_fdb_entry *fdb_create(struct hlist_head *head,
 static int fdb_insert(struct net_bridge *br, struct net_bridge_port *source,
                  const unsigned char *addr)
 {
-       struct hlist_head *head = &br->hash[br_mac_hash(addr)];
+       struct hlist_head *head = &br->hash[br_mac_hash(addr, 0)];
        struct net_bridge_fdb_entry *fdb;
 
        if (!is_valid_ether_addr(addr))
                return -EINVAL;
 
-       fdb = fdb_find(head, addr);
+       fdb = fdb_find(head, addr, 0);
        if (fdb) {
                /* it is okay to have multiple ports with same
                 * address, just use the first one.
@@ -390,7 +400,7 @@ static int fdb_insert(struct net_bridge *br, struct net_bridge_port *source,
                fdb_delete(br, fdb);
        }
 
-       fdb = fdb_create(head, source, addr);
+       fdb = fdb_create(head, source, addr, 0);
        if (!fdb)
                return -ENOMEM;
 
@@ -412,9 +422,9 @@ int br_fdb_insert(struct net_bridge *br, struct net_bridge_port *source,
 }
 
 void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
-                  const unsigned char *addr)
+                  const unsigned char *addr, u16 vid)
 {
-       struct hlist_head *head = &br->hash[br_mac_hash(addr)];
+       struct hlist_head *head = &br->hash[br_mac_hash(addr, vid)];
        struct net_bridge_fdb_entry *fdb;
 
        /* some users want to always flood. */
@@ -426,7 +436,7 @@ void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
              source->state == BR_STATE_FORWARDING))
                return;
 
-       fdb = fdb_find_rcu(head, addr);
+       fdb = fdb_find_rcu(head, addr, vid);
        if (likely(fdb)) {
                /* attempt to update an entry for a local interface */
                if (unlikely(fdb->is_local)) {
@@ -441,8 +451,8 @@ void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
                }
        } else {
                spin_lock(&br->hash_lock);
-               if (likely(!fdb_find(head, addr))) {
-                       fdb = fdb_create(head, source, addr);
+               if (likely(!fdb_find(head, addr, vid))) {
+                       fdb = fdb_create(head, source, addr, vid);
                        if (fdb)
                                fdb_notify(br, fdb, RTM_NEWNEIGH);
                }
@@ -495,6 +505,10 @@ static int fdb_fill_info(struct sk_buff *skb, const struct net_bridge *br,
        ci.ndm_refcnt    = 0;
        if (nla_put(skb, NDA_CACHEINFO, sizeof(ci), &ci))
                goto nla_put_failure;
+
+       if (nla_put(skb, NDA_VLAN, sizeof(u16), &fdb->vlan_id))
+               goto nla_put_failure;
+
        return nlmsg_end(skb, nlh);
 
 nla_put_failure:
@@ -506,6 +520,7 @@ static inline size_t fdb_nlmsg_size(void)
 {
        return NLMSG_ALIGN(sizeof(struct ndmsg))
                + nla_total_size(ETH_ALEN) /* NDA_LLADDR */
+               + nla_total_size(sizeof(u16)) /* NDA_VLAN */
                + nla_total_size(sizeof(struct nda_cacheinfo));
 }
 
@@ -571,18 +586,18 @@ out:
 
 /* Update (create or replace) forwarding database entry */
 static int fdb_add_entry(struct net_bridge_port *source, const __u8 *addr,
-                        __u16 state, __u16 flags)
+                        __u16 state, __u16 flags, __u16 vid)
 {
        struct net_bridge *br = source->br;
-       struct hlist_head *head = &br->hash[br_mac_hash(addr)];
+       struct hlist_head *head = &br->hash[br_mac_hash(addr, vid)];
        struct net_bridge_fdb_entry *fdb;
 
-       fdb = fdb_find(head, addr);
+       fdb = fdb_find(head, addr, vid);
        if (fdb == NULL) {
                if (!(flags & NLM_F_CREATE))
                        return -ENOENT;
 
-               fdb = fdb_create(head, source, addr);
+               fdb = fdb_create(head, source, addr, vid);
                if (!fdb)
                        return -ENOMEM;
                fdb_notify(br, fdb, RTM_NEWNEIGH);
@@ -607,6 +622,25 @@ static int fdb_add_entry(struct net_bridge_port *source, const __u8 *addr,
        return 0;
 }
 
+static int __br_fdb_add(struct ndmsg *ndm, struct net_bridge_port *p,
+              const unsigned char *addr, u16 nlh_flags, u16 vid)
+{
+       int err = 0;
+
+       if (ndm->ndm_flags & NTF_USE) {
+               rcu_read_lock();
+               br_fdb_update(p->br, p, addr, vid);
+               rcu_read_unlock();
+       } else {
+               spin_lock_bh(&p->br->hash_lock);
+               err = fdb_add_entry(p, addr, ndm->ndm_state,
+                                   nlh_flags, vid);
+               spin_unlock_bh(&p->br->hash_lock);
+       }
+
+       return err;
+}
+
 /* Add new permanent fdb entry with RTM_NEWNEIGH */
 int br_fdb_add(struct ndmsg *ndm, struct nlattr *tb[],
               struct net_device *dev,
@@ -614,12 +648,29 @@ int br_fdb_add(struct ndmsg *ndm, struct nlattr *tb[],
 {
        struct net_bridge_port *p;
        int err = 0;
+       struct net_port_vlans *pv;
+       unsigned short vid = VLAN_N_VID;
 
        if (!(ndm->ndm_state & (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE))) {
                pr_info("bridge: RTM_NEWNEIGH with invalid state %#x\n", ndm->ndm_state);
                return -EINVAL;
        }
 
+       if (tb[NDA_VLAN]) {
+               if (nla_len(tb[NDA_VLAN]) != sizeof(unsigned short)) {
+                       pr_info("bridge: RTM_NEWNEIGH with invalid vlan\n");
+                       return -EINVAL;
+               }
+
+               vid = nla_get_u16(tb[NDA_VLAN]);
+
+               if (vid >= VLAN_N_VID) {
+                       pr_info("bridge: RTM_NEWNEIGH with invalid vlan id %d\n",
+                               vid);
+                       return -EINVAL;
+               }
+       }
+
        p = br_port_get_rtnl(dev);
        if (p == NULL) {
                pr_info("bridge: RTM_NEWNEIGH %s not a bridge port\n",
@@ -627,40 +678,90 @@ int br_fdb_add(struct ndmsg *ndm, struct nlattr *tb[],
                return -EINVAL;
        }
 
-       if (ndm->ndm_flags & NTF_USE) {
-               rcu_read_lock();
-               br_fdb_update(p->br, p, addr);
-               rcu_read_unlock();
+       pv = nbp_get_vlan_info(p);
+       if (vid != VLAN_N_VID) {
+               if (!pv || !test_bit(vid, pv->vlan_bitmap)) {
+                       pr_info("bridge: RTM_NEWNEIGH with unconfigured "
+                               "vlan %d on port %s\n", vid, dev->name);
+                       return -EINVAL;
+               }
+
+               /* VID was specified, so use it. */
+               err = __br_fdb_add(ndm, p, addr, nlh_flags, vid);
        } else {
-               spin_lock_bh(&p->br->hash_lock);
-               err = fdb_add_entry(p, addr, ndm->ndm_state, nlh_flags);
-               spin_unlock_bh(&p->br->hash_lock);
+               if (!pv || bitmap_empty(pv->vlan_bitmap, BR_VLAN_BITMAP_LEN)) {
+                       err = __br_fdb_add(ndm, p, addr, nlh_flags, 0);
+                       goto out;
+               }
+
+               /* We have vlans configured on this port and user didn't
+                * specify a VLAN.  To be nice, add/update entry for every
+                * vlan on this port.
+                */
+               vid = find_first_bit(pv->vlan_bitmap, BR_VLAN_BITMAP_LEN);
+               while (vid < BR_VLAN_BITMAP_LEN) {
+                       err = __br_fdb_add(ndm, p, addr, nlh_flags, vid);
+                       if (err)
+                               goto out;
+                       vid = find_next_bit(pv->vlan_bitmap,
+                                           BR_VLAN_BITMAP_LEN, vid+1);
+               }
        }
 
+out:
        return err;
 }
 
-static int fdb_delete_by_addr(struct net_bridge_port *p, const u8 *addr)
+static int fdb_delete_by_addr(struct net_bridge *br, const u8 *addr,
+                             u16 vlan)
 {
-       struct net_bridge *br = p->br;
-       struct hlist_head *head = &br->hash[br_mac_hash(addr)];
+       struct hlist_head *head = &br->hash[br_mac_hash(addr, vlan)];
        struct net_bridge_fdb_entry *fdb;
 
-       fdb = fdb_find(head, addr);
+       fdb = fdb_find(head, addr, vlan);
        if (!fdb)
                return -ENOENT;
 
-       fdb_delete(p->br, fdb);
+       fdb_delete(br, fdb);
        return 0;
 }
 
+static int __br_fdb_delete(struct net_bridge_port *p,
+                          const unsigned char *addr, u16 vid)
+{
+       int err;
+
+       spin_lock_bh(&p->br->hash_lock);
+       err = fdb_delete_by_addr(p->br, addr, vid);
+       spin_unlock_bh(&p->br->hash_lock);
+
+       return err;
+}
+
 /* Remove neighbor entry with RTM_DELNEIGH */
-int br_fdb_delete(struct ndmsg *ndm, struct net_device *dev,
+int br_fdb_delete(struct ndmsg *ndm, struct nlattr *tb[],
+                 struct net_device *dev,
                  const unsigned char *addr)
 {
        struct net_bridge_port *p;
        int err;
+       struct net_port_vlans *pv;
+       unsigned short vid = VLAN_N_VID;
 
+       if (tb[NDA_VLAN]) {
+               if (nla_len(tb[NDA_VLAN]) != sizeof(unsigned short)) {
+                       pr_info("bridge: RTM_NEWNEIGH with invalid vlan\n");
+                       return -EINVAL;
+               }
+
+               vid = nla_get_u16(tb[NDA_VLAN]);
+
+               if (vid >= VLAN_N_VID) {
+                       pr_info("bridge: RTM_NEWNEIGH with invalid vlan id %d\n",
+                               vid);
+                       return -EINVAL;
+               }
+       }
        p = br_port_get_rtnl(dev);
        if (p == NULL) {
                pr_info("bridge: RTM_DELNEIGH %s not a bridge port\n",
@@ -668,9 +769,33 @@ int br_fdb_delete(struct ndmsg *ndm, struct net_device *dev,
                return -EINVAL;
        }
 
-       spin_lock_bh(&p->br->hash_lock);
-       err = fdb_delete_by_addr(p, addr);
-       spin_unlock_bh(&p->br->hash_lock);
+       pv = nbp_get_vlan_info(p);
+       if (vid != VLAN_N_VID) {
+               if (!pv || !test_bit(vid, pv->vlan_bitmap)) {
+                       pr_info("bridge: RTM_DELNEIGH with unconfigured "
+                               "vlan %d on port %s\n", vid, dev->name);
+                       return -EINVAL;
+               }
 
+               err = __br_fdb_delete(p, addr, vid);
+       } else {
+               if (!pv || bitmap_empty(pv->vlan_bitmap, BR_VLAN_BITMAP_LEN)) {
+                       err = __br_fdb_delete(p, addr, 0);
+                       goto out;
+               }
+
+               /* We have vlans configured on this port and user didn't
+                * specify a VLAN.  To be nice, add/update entry for every
+                * vlan on this port.
+                */
+               err = -ENOENT;
+               vid = find_first_bit(pv->vlan_bitmap, BR_VLAN_BITMAP_LEN);
+               while (vid < BR_VLAN_BITMAP_LEN) {
+                       err &= __br_fdb_delete(p, addr, vid);
+                       vid = find_next_bit(pv->vlan_bitmap,
+                                           BR_VLAN_BITMAP_LEN, vid+1);
+               }
+       }
+out:
        return err;
 }
This page took 0.030068 seconds and 5 git commands to generate.