// SPDX-License-Identifier: GPL-2.0-or-later/* Copyright (c) 2021 Taehee Yoo <ap420073@gmail.com> */ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include <linux/module.h>#include <linux/skbuff.h>#include <linux/udp.h>#include <linux/jhash.h>#include <linux/if_tunnel.h>#include <linux/net.h>#include <linux/igmp.h>#include <linux/workqueue.h>#include <net/flow.h>#include <net/pkt_sched.h>#include <net/net_namespace.h>#include <net/ip.h>#include <net/udp.h>#include <net/udp_tunnel.h>#include <net/icmp.h>#include <net/mld.h>#include <net/amt.h>#include <uapi/linux/amt.h>#include <linux/security.h>#include <net/gro_cells.h>#include <net/ipv6.h>#include <net/if_inet6.h>#include <net/ndisc.h>#include <net/addrconf.h>#include <net/ip6_route.h>#include <net/inet_common.h>#include <net/inet_dscp.h>#include <net/ip6_checksum.h> static struct workqueue_struct *amt_wq; static HLIST_HEAD(source_gc_list);/* Lock for source_gc_list */static spinlock_t source_gc_lock;static struct delayed_work source_gc_wq;static char *status_str[] = { "AMT_STATUS_INIT", "AMT_STATUS_SENT_DISCOVERY", "AMT_STATUS_RECEIVED_DISCOVERY", "AMT_STATUS_SENT_ADVERTISEMENT", "AMT_STATUS_RECEIVED_ADVERTISEMENT", "AMT_STATUS_SENT_REQUEST", "AMT_STATUS_RECEIVED_REQUEST", "AMT_STATUS_SENT_QUERY", "AMT_STATUS_RECEIVED_QUERY", "AMT_STATUS_SENT_UPDATE", "AMT_STATUS_RECEIVED_UPDATE",}; static char *type_str[] = { "", /* Type 0 is not defined */ "AMT_MSG_DISCOVERY", "AMT_MSG_ADVERTISEMENT", "AMT_MSG_REQUEST", "AMT_MSG_MEMBERSHIP_QUERY", "AMT_MSG_MEMBERSHIP_UPDATE", "AMT_MSG_MULTICAST_DATA", "AMT_MSG_TEARDOWN",}; static char *action_str[] = { "AMT_ACT_GMI", "AMT_ACT_GMI_ZERO", "AMT_ACT_GT", "AMT_ACT_STATUS_FWD_NEW", "AMT_ACT_STATUS_D_FWD_NEW", "AMT_ACT_STATUS_NONE_NEW",}; static struct igmpv3_grec igmpv3_zero_grec; #if IS_ENABLED(CONFIG_IPV6)#define MLD2_ALL_NODE_INIT { { { 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01 } } }static struct in6_addr mld2_all_node = MLD2_ALL_NODE_INIT;static struct mld2_grec mldv2_zero_grec;#endif static struct amt_skb_cb *amt_skb_cb(struct sk_buff *skb){ BUILD_BUG_ON(sizeof(struct amt_skb_cb) + sizeof(struct tc_skb_cb) > sizeof_field(struct sk_buff, cb)); return (struct amt_skb_cb *)((void *)skb->cb + sizeof(struct tc_skb_cb));} static void __amt_source_gc_work(void){ struct amt_source_node *snode; struct hlist_head gc_list; struct hlist_node *t; spin_lock_bh(&source_gc_lock); hlist_move_list(&source_gc_list, &gc_list); spin_unlock_bh(&source_gc_lock); hlist_for_each_entry_safe(snode, t, &gc_list, node) { hlist_del_rcu(&snode->node); kfree_rcu(snode, rcu); }} static void amt_source_gc_work(struct work_struct *work){ __amt_source_gc_work(); spin_lock_bh(&source_gc_lock); mod_delayed_work(amt_wq, &source_gc_wq, msecs_to_jiffies(AMT_GC_INTERVAL)); spin_unlock_bh(&source_gc_lock);} static bool amt_addr_equal(union amt_addr *a, union amt_addr *b){ return !memcmp(a, b, sizeof(union amt_addr));} static u32 amt_source_hash(struct amt_tunnel_list *tunnel, union amt_addr *src){ u32 hash = jhash(src, sizeof(*src), tunnel->amt->hash_seed); return reciprocal_scale(hash, tunnel->amt->hash_buckets);} static bool amt_status_filter(struct amt_source_node *snode, enum amt_filter filter){ bool rc = false; switch (filter) { case AMT_FILTER_FWD: if (snode->status == AMT_SOURCE_STATUS_FWD && snode->flags == AMT_SOURCE_OLD) rc = true; break; case AMT_FILTER_D_FWD: if (snode->status == AMT_SOURCE_STATUS_D_FWD && snode->flags == AMT_SOURCE_OLD) rc = true; break; case AMT_FILTER_FWD_NEW: if (snode->status == AMT_SOURCE_STATUS_FWD && snode->flags == AMT_SOURCE_NEW) rc = true; break; case AMT_FILTER_D_FWD_NEW: if (snode->status == AMT_SOURCE_STATUS_D_FWD && snode->flags == AMT_SOURCE_NEW) rc = true; break; case AMT_FILTER_ALL: rc = true; break; case AMT_FILTER_NONE_NEW: if (snode->status == AMT_SOURCE_STATUS_NONE && snode->flags == AMT_SOURCE_NEW) rc = true; break; case AMT_FILTER_BOTH: if ((snode->status == AMT_SOURCE_STATUS_D_FWD || snode->status == AMT_SOURCE_STATUS_FWD) && snode->flags == AMT_SOURCE_OLD) rc = true; break; case AMT_FILTER_BOTH_NEW: if ((snode->status == AMT_SOURCE_STATUS_D_FWD || snode->status == AMT_SOURCE_STATUS_FWD) && snode->flags == AMT_SOURCE_NEW) rc = true; break; default: WARN_ON_ONCE(1); break; } return rc;} static struct amt_source_node *amt_lookup_src(struct amt_tunnel_list *tunnel, struct amt_group_node *gnode, enum amt_filter filter, union amt_addr *src){ u32 hash = amt_source_hash(tunnel, src); struct amt_source_node *snode; hlist_for_each_entry_rcu(snode, &gnode->sources[hash], node) if (amt_status_filter(snode, filter) && amt_addr_equal(&snode->source_addr, src)) return snode; return NULL;} static u32 amt_group_hash(struct amt_tunnel_list *tunnel, union amt_addr *group){ u32 hash = jhash(group, sizeof(*group), tunnel->amt->hash_seed); return reciprocal_scale(hash, tunnel->amt->hash_buckets);} static struct amt_group_node *amt_lookup_group(struct amt_tunnel_list *tunnel, union amt_addr *group, union amt_addr *host, bool v6){ u32 hash = amt_group_hash(tunnel, group); struct amt_group_node *gnode; hlist_for_each_entry_rcu(gnode, &tunnel->groups[hash], node) { if (amt_addr_equal(&gnode->group_addr, group) && amt_addr_equal(&gnode->host_addr, host) && gnode->v6 == v6) return gnode; } return NULL;} static void amt_destroy_source(struct amt_source_node *snode){ struct amt_group_node *gnode = snode->gnode; struct amt_tunnel_list *tunnel; tunnel = gnode->tunnel_list; if (!gnode->v6) { netdev_dbg(snode->gnode->amt->dev, "Delete source %pI4 from %pI4\n", &snode->source_addr.ip4, &gnode->group_addr.ip4);#if IS_ENABLED(CONFIG_IPV6) } else { netdev_dbg(snode->gnode->amt->dev, "Delete source %pI6 from %pI6\n", &snode->source_addr.ip6, &gnode->group_addr.ip6);#endif } cancel_delayed_work(&snode->source_timer); hlist_del_init_rcu(&snode->node); tunnel->nr_sources--; gnode->nr_sources--; spin_lock_bh(&source_gc_lock); hlist_add_head_rcu(&snode->node, &source_gc_list); spin_unlock_bh(&source_gc_lock);} static void amt_del_group(struct amt_dev *amt, struct amt_group_node *gnode){ struct amt_source_node *snode; struct hlist_node *t; int i; if (cancel_delayed_work(&gnode->group_timer)) dev_put(amt->dev); hlist_del_rcu(&gnode->node); gnode->tunnel_list->nr_groups--; if (!gnode->v6) netdev_dbg(amt->dev, "Leave group %pI4\n", &gnode->group_addr.ip4);#if IS_ENABLED(CONFIG_IPV6) else netdev_dbg(amt->dev, "Leave group %pI6\n", &gnode->group_addr.ip6);#endif for (i = 0; i < amt->hash_buckets; i++) hlist_for_each_entry_safe(snode, t, &gnode->sources[i], node) amt_destroy_source(snode); /* tunnel->lock was acquired outside of amt_del_group() * But rcu_read_lock() was acquired too so It's safe. */ kfree_rcu(gnode, rcu);} /* If a source timer expires with a router filter-mode for the group of * INCLUDE, the router concludes that traffic from this particular * source is no longer desired on the attached network, and deletes the * associated source record. */static void amt_source_work(struct work_struct *work){ struct amt_source_node *snode = container_of(to_delayed_work(work), struct amt_source_node, source_timer); struct amt_group_node *gnode = snode->gnode; struct amt_dev *amt = gnode->amt; struct amt_tunnel_list *tunnel; tunnel = gnode->tunnel_list; spin_lock_bh(&tunnel->lock); rcu_read_lock(); if (gnode->filter_mode == MCAST_INCLUDE) { amt_destroy_source(snode); if (!gnode->nr_sources) amt_del_group(amt, gnode); } else { /* When a router filter-mode for a group is EXCLUDE, * source records are only deleted when the group timer expires */ snode->status = AMT_SOURCE_STATUS_D_FWD; } rcu_read_unlock(); spin_unlock_bh(&tunnel->lock);} static void amt_act_src(struct amt_tunnel_list *tunnel, struct amt_group_node *gnode, struct amt_source_node *snode, enum amt_act act){ struct amt_dev *amt = tunnel->amt; switch (act) { case AMT_ACT_GMI: mod_delayed_work(amt_wq, &snode->source_timer, msecs_to_jiffies(amt_gmi(amt))); break; case AMT_ACT_GMI_ZERO: cancel_delayed_work(&snode->source_timer); break; case AMT_ACT_GT: mod_delayed_work(amt_wq, &snode->source_timer, gnode->group_timer.timer.expires); break; case AMT_ACT_STATUS_FWD_NEW: snode->status = AMT_SOURCE_STATUS_FWD; snode->flags = AMT_SOURCE_NEW; break; case AMT_ACT_STATUS_D_FWD_NEW: snode->status = AMT_SOURCE_STATUS_D_FWD; snode->flags = AMT_SOURCE_NEW; break; case AMT_ACT_STATUS_NONE_NEW: cancel_delayed_work(&snode->source_timer); snode->status = AMT_SOURCE_STATUS_NONE; snode->flags = AMT_SOURCE_NEW; break; default: WARN_ON_ONCE(1); return; } if (!gnode->v6) netdev_dbg(amt->dev, "Source %pI4 from %pI4 Acted %s\n", &snode->source_addr.ip4, &gnode->group_addr.ip4, action_str[act]);#if IS_ENABLED(CONFIG_IPV6) else netdev_dbg(amt->dev, "Source %pI6 from %pI6 Acted %s\n", &snode->source_addr.ip6, &gnode->group_addr.ip6, action_str[act]);#endif} static struct amt_source_node *amt_alloc_snode(struct amt_group_node *gnode, union amt_addr *src){ struct amt_source_node *snode; snode = kzalloc_obj(*snode, GFP_ATOMIC); if (!snode) return NULL; memcpy(&snode->source_addr, src, sizeof(union amt_addr)); snode->gnode = gnode; snode->status = AMT_SOURCE_STATUS_NONE; snode->flags = AMT_SOURCE_NEW; INIT_HLIST_NODE(&snode->node); INIT_DELAYED_WORK(&snode->source_timer, amt_source_work); return snode;} /* RFC 3810 - 7.2.2. Definition of Filter Timers * * Router Mode Filter Timer Actions/Comments * ----------- ----------------- ---------------- * * INCLUDE Not Used All listeners in * INCLUDE mode. * * EXCLUDE Timer > 0 At least one listener * in EXCLUDE mode. * * EXCLUDE Timer == 0 No more listeners in * EXCLUDE mode for the * multicast address. * If the Requested List * is empty, delete * Multicast Address * Record. If not, switch * to INCLUDE filter mode; * the sources in the * Requested List are * moved to the Include * List, and the Exclude * List is deleted. */static void amt_group_work(struct work_struct *work){ struct amt_group_node *gnode = container_of(to_delayed_work(work), struct amt_group_node, group_timer); struct amt_tunnel_list *tunnel = gnode->tunnel_list; struct amt_dev *amt = gnode->amt; struct amt_source_node *snode; bool delete_group = true; struct hlist_node *t; int i, buckets; buckets = amt->hash_buckets; spin_lock_bh(&tunnel->lock); if (gnode->filter_mode == MCAST_INCLUDE) { /* Not Used */ spin_unlock_bh(&tunnel->lock); goto out; } rcu_read_lock(); for (i = 0; i < buckets; i++) { hlist_for_each_entry_safe(snode, t, &gnode->sources[i], node) { if (!delayed_work_pending(&snode->source_timer) || snode->status == AMT_SOURCE_STATUS_D_FWD) { amt_destroy_source(snode); } else { delete_group = false; snode->status = AMT_SOURCE_STATUS_FWD; } } } if (delete_group) amt_del_group(amt, gnode); else gnode->filter_mode = MCAST_INCLUDE; rcu_read_unlock(); spin_unlock_bh(&tunnel->lock);out: dev_put(amt->dev);} /* Non-existent group is created as INCLUDE {empty}: * * RFC 3376 - 5.1. Action on Change of Interface State * * If no interface state existed for that multicast address before * the change (i.e., the change consisted of creating a new * per-interface record), or if no state exists after the change * (i.e., the change consisted of deleting a per-interface record), * then the "non-existent" state is considered to have a filter mode * of INCLUDE and an empty source list. */static struct amt_group_node *amt_add_group(struct amt_dev *amt, struct amt_tunnel_list *tunnel, union amt_addr *group, union amt_addr *host, bool v6){ struct amt_group_node *gnode; u32 hash; int i; if (tunnel->nr_groups >= amt->max_groups) return ERR_PTR(-ENOSPC); gnode = kzalloc(sizeof(*gnode) + (sizeof(struct hlist_head) * amt->hash_buckets), GFP_ATOMIC); if (unlikely(!gnode)) return ERR_PTR(-ENOMEM); gnode->amt = amt; gnode->group_addr = *group; gnode->host_addr = *host; gnode->v6 = v6; gnode->tunnel_list = tunnel; gnode->filter_mode = MCAST_INCLUDE; INIT_HLIST_NODE(&gnode->node); INIT_DELAYED_WORK(&gnode->group_timer, amt_group_work); for (i = 0; i < amt->hash_buckets; i++) INIT_HLIST_HEAD(&gnode->sources[i]); hash = amt_group_hash(tunnel, group); hlist_add_head_rcu(&gnode->node, &tunnel->groups[hash]); tunnel->nr_groups++; if (!gnode->v6) netdev_dbg(amt->dev, "Join group %pI4\n",Lines 1–500 of 3513. Load more
// SPDX-License-Identifier: GPL-2.0-or-later /* Copyright (c) 2021 Taehee Yoo <ap420073@gmail.com> */ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include <linux/module.h> #include <linux/skbuff.h> #include <linux/udp.h> #include <linux/jhash.h> #include <linux/if_tunnel.h> #include <linux/net.h> #include <linux/igmp.h> #include <linux/workqueue.h> #include <net/flow.h> #include <net/pkt_sched.h> #include <net/net_namespace.h> #include <net/ip.h> #include <net/udp.h> #include <net/udp_tunnel.h> #include <net/icmp.h> #include <net/mld.h> #include <net/amt.h> #include <uapi/linux/amt.h> #include <linux/security.h> #include <net/gro_cells.h> #include <net/ipv6.h> #include <net/if_inet6.h> #include <net/ndisc.h> #include <net/addrconf.h> #include <net/ip6_route.h> #include <net/inet_common.h> #include <net/inet_dscp.h> #include <net/ip6_checksum.h> static struct workqueue_struct *amt_wq; static HLIST_HEAD(source_gc_list); /* Lock for source_gc_list */ static spinlock_t source_gc_lock; static struct delayed_work source_gc_wq; static char *status_str[] = { "AMT_STATUS_INIT", "AMT_STATUS_SENT_DISCOVERY", "AMT_STATUS_RECEIVED_DISCOVERY", "AMT_STATUS_SENT_ADVERTISEMENT", "AMT_STATUS_RECEIVED_ADVERTISEMENT", "AMT_STATUS_SENT_REQUEST", "AMT_STATUS_RECEIVED_REQUEST", "AMT_STATUS_SENT_QUERY", "AMT_STATUS_RECEIVED_QUERY", "AMT_STATUS_SENT_UPDATE", "AMT_STATUS_RECEIVED_UPDATE", }; static char *type_str[] = { "", /* Type 0 is not defined */ "AMT_MSG_DISCOVERY", "AMT_MSG_ADVERTISEMENT", "AMT_MSG_REQUEST", "AMT_MSG_MEMBERSHIP_QUERY", "AMT_MSG_MEMBERSHIP_UPDATE", "AMT_MSG_MULTICAST_DATA", "AMT_MSG_TEARDOWN", }; static char *action_str[] = { "AMT_ACT_GMI", "AMT_ACT_GMI_ZERO", "AMT_ACT_GT", "AMT_ACT_STATUS_FWD_NEW", "AMT_ACT_STATUS_D_FWD_NEW", "AMT_ACT_STATUS_NONE_NEW", }; static struct igmpv3_grec igmpv3_zero_grec; #if IS_ENABLED(CONFIG_IPV6) #define MLD2_ALL_NODE_INIT { { { 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01 } } } static struct in6_addr mld2_all_node = MLD2_ALL_NODE_INIT; static struct mld2_grec mldv2_zero_grec; #endif static struct amt_skb_cb *amt_skb_cb(struct sk_buff *skb) { BUILD_BUG_ON(sizeof(struct amt_skb_cb) + sizeof(struct tc_skb_cb) > sizeof_field(struct sk_buff, cb)); return (struct amt_skb_cb *)((void *)skb->cb + sizeof(struct tc_skb_cb)); } static void __amt_source_gc_work(void) { struct amt_source_node *snode; struct hlist_head gc_list; struct hlist_node *t; spin_lock_bh(&source_gc_lock); hlist_move_list(&source_gc_list, &gc_list); spin_unlock_bh(&source_gc_lock); hlist_for_each_entry_safe(snode, t, &gc_list, node) { hlist_del_rcu(&snode->node); kfree_rcu(snode, rcu); } } static void amt_source_gc_work(struct work_struct *work) { __amt_source_gc_work(); spin_lock_bh(&source_gc_lock); mod_delayed_work(amt_wq, &source_gc_wq, msecs_to_jiffies(AMT_GC_INTERVAL)); spin_unlock_bh(&source_gc_lock); } static bool amt_addr_equal(union amt_addr *a, union amt_addr *b) { return !memcmp(a, b, sizeof(union amt_addr)); } static u32 amt_source_hash(struct amt_tunnel_list *tunnel, union amt_addr *src) { u32 hash = jhash(src, sizeof(*src), tunnel->amt->hash_seed); return reciprocal_scale(hash, tunnel->amt->hash_buckets); } static bool amt_status_filter(struct amt_source_node *snode, enum amt_filter filter) { bool rc = false; switch (filter) { case AMT_FILTER_FWD: if (snode->status == AMT_SOURCE_STATUS_FWD && snode->flags == AMT_SOURCE_OLD) rc = true; break; case AMT_FILTER_D_FWD: if (snode->status == AMT_SOURCE_STATUS_D_FWD && snode->flags == AMT_SOURCE_OLD) rc = true; break; case AMT_FILTER_FWD_NEW: if (snode->status == AMT_SOURCE_STATUS_FWD && snode->flags == AMT_SOURCE_NEW) rc = true; break; case AMT_FILTER_D_FWD_NEW: if (snode->status == AMT_SOURCE_STATUS_D_FWD && snode->flags == AMT_SOURCE_NEW) rc = true; break; case AMT_FILTER_ALL: rc = true; break; case AMT_FILTER_NONE_NEW: if (snode->status == AMT_SOURCE_STATUS_NONE && snode->flags == AMT_SOURCE_NEW) rc = true; break; case AMT_FILTER_BOTH: if ((snode->status == AMT_SOURCE_STATUS_D_FWD || snode->status == AMT_SOURCE_STATUS_FWD) && snode->flags == AMT_SOURCE_OLD) rc = true; break; case AMT_FILTER_BOTH_NEW: if ((snode->status == AMT_SOURCE_STATUS_D_FWD || snode->status == AMT_SOURCE_STATUS_FWD) && snode->flags == AMT_SOURCE_NEW) rc = true; break; default: WARN_ON_ONCE(1); break; } return rc; } static struct amt_source_node *amt_lookup_src(struct amt_tunnel_list *tunnel, struct amt_group_node *gnode, enum amt_filter filter, union amt_addr *src) { u32 hash = amt_source_hash(tunnel, src); struct amt_source_node *snode; hlist_for_each_entry_rcu(snode, &gnode->sources[hash], node) if (amt_status_filter(snode, filter) && amt_addr_equal(&snode->source_addr, src)) return snode; return NULL; } static u32 amt_group_hash(struct amt_tunnel_list *tunnel, union amt_addr *group) { u32 hash = jhash(group, sizeof(*group), tunnel->amt->hash_seed); return reciprocal_scale(hash, tunnel->amt->hash_buckets); } static struct amt_group_node *amt_lookup_group(struct amt_tunnel_list *tunnel, union amt_addr *group, union amt_addr *host, bool v6) { u32 hash = amt_group_hash(tunnel, group); struct amt_group_node *gnode; hlist_for_each_entry_rcu(gnode, &tunnel->groups[hash], node) { if (amt_addr_equal(&gnode->group_addr, group) && amt_addr_equal(&gnode->host_addr, host) && gnode->v6 == v6) return gnode; } return NULL; } static void amt_destroy_source(struct amt_source_node *snode) { struct amt_group_node *gnode = snode->gnode; struct amt_tunnel_list *tunnel; tunnel = gnode->tunnel_list; if (!gnode->v6) { netdev_dbg(snode->gnode->amt->dev, "Delete source %pI4 from %pI4\n", &snode->source_addr.ip4, &gnode->group_addr.ip4); #if IS_ENABLED(CONFIG_IPV6) } else { netdev_dbg(snode->gnode->amt->dev, "Delete source %pI6 from %pI6\n", &snode->source_addr.ip6, &gnode->group_addr.ip6); #endif } cancel_delayed_work(&snode->source_timer); hlist_del_init_rcu(&snode->node); tunnel->nr_sources--; gnode->nr_sources--; spin_lock_bh(&source_gc_lock); hlist_add_head_rcu(&snode->node, &source_gc_list); spin_unlock_bh(&source_gc_lock); } static void amt_del_group(struct amt_dev *amt, struct amt_group_node *gnode) { struct amt_source_node *snode; struct hlist_node *t; int i; if (cancel_delayed_work(&gnode->group_timer)) dev_put(amt->dev); hlist_del_rcu(&gnode->node); gnode->tunnel_list->nr_groups--; if (!gnode->v6) netdev_dbg(amt->dev, "Leave group %pI4\n", &gnode->group_addr.ip4); #if IS_ENABLED(CONFIG_IPV6) else netdev_dbg(amt->dev, "Leave group %pI6\n", &gnode->group_addr.ip6); #endif for (i = 0; i < amt->hash_buckets; i++) hlist_for_each_entry_safe(snode, t, &gnode->sources[i], node) amt_destroy_source(snode); /* tunnel->lock was acquired outside of amt_del_group() * But rcu_read_lock() was acquired too so It's safe. */ kfree_rcu(gnode, rcu); } /* If a source timer expires with a router filter-mode for the group of * INCLUDE, the router concludes that traffic from this particular * source is no longer desired on the attached network, and deletes the * associated source record. */ static void amt_source_work(struct work_struct *work) { struct amt_source_node *snode = container_of(to_delayed_work(work), struct amt_source_node, source_timer); struct amt_group_node *gnode = snode->gnode; struct amt_dev *amt = gnode->amt; struct amt_tunnel_list *tunnel; tunnel = gnode->tunnel_list; spin_lock_bh(&tunnel->lock); rcu_read_lock(); if (gnode->filter_mode == MCAST_INCLUDE) { amt_destroy_source(snode); if (!gnode->nr_sources) amt_del_group(amt, gnode); } else { /* When a router filter-mode for a group is EXCLUDE, * source records are only deleted when the group timer expires */ snode->status = AMT_SOURCE_STATUS_D_FWD; } rcu_read_unlock(); spin_unlock_bh(&tunnel->lock); } static void amt_act_src(struct amt_tunnel_list *tunnel, struct amt_group_node *gnode, struct amt_source_node *snode, enum amt_act act) { struct amt_dev *amt = tunnel->amt; switch (act) { case AMT_ACT_GMI: mod_delayed_work(amt_wq, &snode->source_timer, msecs_to_jiffies(amt_gmi(amt))); break; case AMT_ACT_GMI_ZERO: cancel_delayed_work(&snode->source_timer); break; case AMT_ACT_GT: mod_delayed_work(amt_wq, &snode->source_timer, gnode->group_timer.timer.expires); break; case AMT_ACT_STATUS_FWD_NEW: snode->status = AMT_SOURCE_STATUS_FWD; snode->flags = AMT_SOURCE_NEW; break; case AMT_ACT_STATUS_D_FWD_NEW: snode->status = AMT_SOURCE_STATUS_D_FWD; snode->flags = AMT_SOURCE_NEW; break; case AMT_ACT_STATUS_NONE_NEW: cancel_delayed_work(&snode->source_timer); snode->status = AMT_SOURCE_STATUS_NONE; snode->flags = AMT_SOURCE_NEW; break; default: WARN_ON_ONCE(1); return; } if (!gnode->v6) netdev_dbg(amt->dev, "Source %pI4 from %pI4 Acted %s\n", &snode->source_addr.ip4, &gnode->group_addr.ip4, action_str[act]); #if IS_ENABLED(CONFIG_IPV6) else netdev_dbg(amt->dev, "Source %pI6 from %pI6 Acted %s\n", &snode->source_addr.ip6, &gnode->group_addr.ip6, action_str[act]); #endif } static struct amt_source_node *amt_alloc_snode(struct amt_group_node *gnode, union amt_addr *src) { struct amt_source_node *snode; snode = kzalloc_obj(*snode, GFP_ATOMIC); if (!snode) return NULL; memcpy(&snode->source_addr, src, sizeof(union amt_addr)); snode->gnode = gnode; snode->status = AMT_SOURCE_STATUS_NONE; snode->flags = AMT_SOURCE_NEW; INIT_HLIST_NODE(&snode->node); INIT_DELAYED_WORK(&snode->source_timer, amt_source_work); return snode; } /* RFC 3810 - 7.2.2. Definition of Filter Timers * * Router Mode Filter Timer Actions/Comments * ----------- ----------------- ---------------- * * INCLUDE Not Used All listeners in * INCLUDE mode. * * EXCLUDE Timer > 0 At least one listener * in EXCLUDE mode. * * EXCLUDE Timer == 0 No more listeners in * EXCLUDE mode for the * multicast address. * If the Requested List * is empty, delete * Multicast Address * Record. If not, switch * to INCLUDE filter mode; * the sources in the * Requested List are * moved to the Include * List, and the Exclude * List is deleted. */ static void amt_group_work(struct work_struct *work) { struct amt_group_node *gnode = container_of(to_delayed_work(work), struct amt_group_node, group_timer); struct amt_tunnel_list *tunnel = gnode->tunnel_list; struct amt_dev *amt = gnode->amt; struct amt_source_node *snode; bool delete_group = true; struct hlist_node *t; int i, buckets; buckets = amt->hash_buckets; spin_lock_bh(&tunnel->lock); if (gnode->filter_mode == MCAST_INCLUDE) { /* Not Used */ spin_unlock_bh(&tunnel->lock); goto out; } rcu_read_lock(); for (i = 0; i < buckets; i++) { hlist_for_each_entry_safe(snode, t, &gnode->sources[i], node) { if (!delayed_work_pending(&snode->source_timer) || snode->status == AMT_SOURCE_STATUS_D_FWD) { amt_destroy_source(snode); } else { delete_group = false; snode->status = AMT_SOURCE_STATUS_FWD; } } } if (delete_group) amt_del_group(amt, gnode); else gnode->filter_mode = MCAST_INCLUDE; rcu_read_unlock(); spin_unlock_bh(&tunnel->lock); out: dev_put(amt->dev); } /* Non-existent group is created as INCLUDE {empty}: * * RFC 3376 - 5.1. Action on Change of Interface State * * If no interface state existed for that multicast address before * the change (i.e., the change consisted of creating a new * per-interface record), or if no state exists after the change * (i.e., the change consisted of deleting a per-interface record), * then the "non-existent" state is considered to have a filter mode * of INCLUDE and an empty source list. */ static struct amt_group_node *amt_add_group(struct amt_dev *amt, struct amt_tunnel_list *tunnel, union amt_addr *group, union amt_addr *host, bool v6) { struct amt_group_node *gnode; u32 hash; int i; if (tunnel->nr_groups >= amt->max_groups) return ERR_PTR(-ENOSPC); gnode = kzalloc(sizeof(*gnode) + (sizeof(struct hlist_head) * amt->hash_buckets), GFP_ATOMIC); if (unlikely(!gnode)) return ERR_PTR(-ENOMEM); gnode->amt = amt; gnode->group_addr = *group; gnode->host_addr = *host; gnode->v6 = v6; gnode->tunnel_list = tunnel; gnode->filter_mode = MCAST_INCLUDE; INIT_HLIST_NODE(&gnode->node); INIT_DELAYED_WORK(&gnode->group_timer, amt_group_work); for (i = 0; i < amt->hash_buckets; i++) INIT_HLIST_HEAD(&gnode->sources[i]); hash = amt_group_hash(tunnel, group); hlist_add_head_rcu(&gnode->node, &tunnel->groups[hash]); tunnel->nr_groups++; if (!gnode->v6) netdev_dbg(amt->dev, "Join group %pI4\n",