Skip to content
Snippets Groups Projects
mcast.c 62.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • Linus Torvalds's avatar
    Linus Torvalds committed
    /*
     *	Multicast support for IPv6
    
     *	Linux INET6 implementation
    
    Linus Torvalds's avatar
    Linus Torvalds committed
     *
     *	Authors:
    
     *	Pedro Roque		<roque@di.fc.ul.pt>
    
    Linus Torvalds's avatar
    Linus Torvalds committed
     *
     *	$Id: mcast.c,v 1.40 2002/02/08 03:57:19 davem Exp $
     *
    
     *	Based on linux/ipv4/igmp.c and linux/ipv4/ip_sockglue.c
    
    Linus Torvalds's avatar
    Linus Torvalds committed
     *
     *	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.
     */
    
    /* Changes:
     *
     *	yoshfuji	: fix format of router-alert option
     *	YOSHIFUJI Hideaki @USAGI:
     *		Fixed source address for MLD message based on
     *		<draft-ietf-magma-mld-source-05.txt>.
     *	YOSHIFUJI Hideaki @USAGI:
     *		- Ignore Queries for invalid addresses.
     *		- MLD for link-local addresses.
     *	David L Stevens <dlstevens@us.ibm.com>:
     *		- MLDv2 support
     */
    
    #include <linux/module.h>
    #include <linux/errno.h>
    #include <linux/types.h>
    #include <linux/string.h>
    #include <linux/socket.h>
    #include <linux/sockios.h>
    #include <linux/jiffies.h>
    #include <linux/times.h>
    #include <linux/net.h>
    #include <linux/in.h>
    #include <linux/in6.h>
    #include <linux/netdevice.h>
    #include <linux/if_arp.h>
    #include <linux/route.h>
    #include <linux/init.h>
    #include <linux/proc_fs.h>
    #include <linux/seq_file.h>
    
    #include <linux/netfilter.h>
    #include <linux/netfilter_ipv6.h>
    
    
    #include <net/net_namespace.h>
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    #include <net/sock.h>
    #include <net/snmp.h>
    
    #include <net/ipv6.h>
    #include <net/protocol.h>
    #include <net/if_inet6.h>
    #include <net/ndisc.h>
    #include <net/addrconf.h>
    #include <net/ip6_route.h>
    
    #include <net/ip6_checksum.h>
    
    /* Set to 3 to get tracing... */
    #define MCAST_DEBUG 2
    
    #if MCAST_DEBUG >= 3
    #define MDBG(x) printk x
    #else
    #define MDBG(x)
    #endif
    
    /*
     *  These header formats should be in a separate include file, but icmpv6.h
     *  doesn't have in6_addr defined in all cases, there is no __u128, and no
     *  other files reference these.
     *
     *  			+-DLS 4/14/03
     */
    
    /* Multicast Listener Discovery version 2 headers */
    
    struct mld2_grec {
    	__u8		grec_type;
    	__u8		grec_auxwords;
    
    Al Viro's avatar
    Al Viro committed
    	__be16		grec_nsrcs;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	struct in6_addr	grec_mca;
    	struct in6_addr	grec_src[0];
    };
    
    struct mld2_report {
    	__u8	type;
    	__u8	resv1;
    
    Al Viro's avatar
    Al Viro committed
    	__be16	resv2;
    	__be16	ngrec;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	struct mld2_grec grec[0];
    };
    
    struct mld2_query {
    	__u8 type;
    	__u8 code;
    
    Al Viro's avatar
    Al Viro committed
    	__be16 mrc;
    	__be16 resv1;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	struct in6_addr mca;
    #if defined(__LITTLE_ENDIAN_BITFIELD)
    	__u8 qrv:3,
    	     suppress:1,
    	     resv2:4;
    #elif defined(__BIG_ENDIAN_BITFIELD)
    	__u8 resv2:4,
    	     suppress:1,
    	     qrv:3;
    #else
    #error "Please fix <asm/byteorder.h>"
    #endif
    	__u8 qqic;
    
    Al Viro's avatar
    Al Viro committed
    	__be16 nsrcs;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	struct in6_addr srcs[0];
    };
    
    static struct in6_addr mld2_all_mcr = MLD2_ALL_MCR_INIT;
    
    /* Big mc list lock for all the sockets */
    static DEFINE_RWLOCK(ipv6_sk_mc_lock);
    
    static struct socket *igmp6_socket;
    
    int __ipv6_dev_mc_dec(struct inet6_dev *idev, struct in6_addr *addr);
    
    static void igmp6_join_group(struct ifmcaddr6 *ma);
    static void igmp6_leave_group(struct ifmcaddr6 *ma);
    static void igmp6_timer_handler(unsigned long data);
    
    static void mld_gq_timer_expire(unsigned long data);
    static void mld_ifc_timer_expire(unsigned long data);
    static void mld_ifc_event(struct inet6_dev *idev);
    static void mld_add_delrec(struct inet6_dev *idev, struct ifmcaddr6 *pmc);
    static void mld_del_delrec(struct inet6_dev *idev, struct in6_addr *addr);
    static void mld_clear_delrec(struct inet6_dev *idev);
    static int sf_setstate(struct ifmcaddr6 *pmc);
    static void sf_markstate(struct ifmcaddr6 *pmc);
    static void ip6_mc_clear_src(struct ifmcaddr6 *pmc);
    static int ip6_mc_del_src(struct inet6_dev *idev, struct in6_addr *pmca,
    			  int sfmode, int sfcount, struct in6_addr *psfsrc,
    			  int delta);
    static int ip6_mc_add_src(struct inet6_dev *idev, struct in6_addr *pmca,
    			  int sfmode, int sfcount, struct in6_addr *psfsrc,
    			  int delta);
    static int ip6_mc_leave_src(struct sock *sk, struct ipv6_mc_socklist *iml,
    			    struct inet6_dev *idev);
    
    
    #define IGMP6_UNSOLICITED_IVAL	(10*HZ)
    #define MLD_QRV_DEFAULT		2
    
    #define MLD_V1_SEEN(idev) (ipv6_devconf.force_mld_version == 1 || \
    		(idev)->cnf.force_mld_version == 1 || \
    		((idev)->mc_v1_seen && \
    		time_before(jiffies, (idev)->mc_v1_seen)))
    
    #define MLDV2_MASK(value, nb) ((nb)>=32 ? (value) : ((1<<(nb))-1) & (value))
    #define MLDV2_EXP(thresh, nbmant, nbexp, value) \
    	((value) < (thresh) ? (value) : \
    
    	((MLDV2_MASK(value, nbmant) | (1<<(nbmant))) << \
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	(MLDV2_MASK((value) >> (nbmant), nbexp) + (nbexp))))
    
    #define MLDV2_QQIC(value) MLDV2_EXP(0x80, 4, 3, value)
    #define MLDV2_MRC(value) MLDV2_EXP(0x8000, 12, 3, value)
    
    
    #define IPV6_MLD_MAX_MSF	64
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    
    int sysctl_mld_max_msf __read_mostly = IPV6_MLD_MAX_MSF;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    /*
     *	socket join on multicast group
     */
    
    int ipv6_sock_mc_join(struct sock *sk, int ifindex, struct in6_addr *addr)
    {
    	struct net_device *dev = NULL;
    	struct ipv6_mc_socklist *mc_lst;
    	struct ipv6_pinfo *np = inet6_sk(sk);
    	int err;
    
    	if (!ipv6_addr_is_multicast(addr))
    		return -EINVAL;
    
    
    	read_lock_bh(&ipv6_sk_mc_lock);
    	for (mc_lst=np->ipv6_mc_list; mc_lst; mc_lst=mc_lst->next) {
    		if ((ifindex == 0 || mc_lst->ifindex == ifindex) &&
    		    ipv6_addr_equal(&mc_lst->addr, addr)) {
    			read_unlock_bh(&ipv6_sk_mc_lock);
    			return -EADDRINUSE;
    		}
    	}
    	read_unlock_bh(&ipv6_sk_mc_lock);
    
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	mc_lst = sock_kmalloc(sk, sizeof(struct ipv6_mc_socklist), GFP_KERNEL);
    
    	if (mc_lst == NULL)
    		return -ENOMEM;
    
    	mc_lst->next = NULL;
    	ipv6_addr_copy(&mc_lst->addr, addr);
    
    	if (ifindex == 0) {
    		struct rt6_info *rt;
    
    		rt = rt6_lookup(&init_net, addr, NULL, 0, 0);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		if (rt) {
    			dev = rt->rt6i_dev;
    			dev_hold(dev);
    			dst_release(&rt->u.dst);
    		}
    	} else
    
    		dev = dev_get_by_index(&init_net, ifindex);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    	if (dev == NULL) {
    		sock_kfree_s(sk, mc_lst, sizeof(*mc_lst));
    		return -ENODEV;
    	}
    
    	mc_lst->ifindex = dev->ifindex;
    	mc_lst->sfmode = MCAST_EXCLUDE;
    
    	rwlock_init(&mc_lst->sflock);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	mc_lst->sflist = NULL;
    
    	/*
    	 *	now add/increase the group membership on the device
    	 */
    
    	err = ipv6_dev_mc_inc(dev, addr);
    
    	if (err) {
    		sock_kfree_s(sk, mc_lst, sizeof(*mc_lst));
    		dev_put(dev);
    		return err;
    	}
    
    	write_lock_bh(&ipv6_sk_mc_lock);
    	mc_lst->next = np->ipv6_mc_list;
    	np->ipv6_mc_list = mc_lst;
    	write_unlock_bh(&ipv6_sk_mc_lock);
    
    	dev_put(dev);
    
    	return 0;
    }
    
    /*
     *	socket leave on multicast group
     */
    int ipv6_sock_mc_drop(struct sock *sk, int ifindex, struct in6_addr *addr)
    {
    	struct ipv6_pinfo *np = inet6_sk(sk);
    	struct ipv6_mc_socklist *mc_lst, **lnk;
    
    	write_lock_bh(&ipv6_sk_mc_lock);
    	for (lnk = &np->ipv6_mc_list; (mc_lst = *lnk) !=NULL ; lnk = &mc_lst->next) {
    		if ((ifindex == 0 || mc_lst->ifindex == ifindex) &&
    		    ipv6_addr_equal(&mc_lst->addr, addr)) {
    			struct net_device *dev;
    
    			*lnk = mc_lst->next;
    			write_unlock_bh(&ipv6_sk_mc_lock);
    
    
    			if ((dev = dev_get_by_index(&init_net, mc_lst->ifindex)) != NULL) {
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    				struct inet6_dev *idev = in6_dev_get(dev);
    
    
    				(void) ip6_mc_leave_src(sk, mc_lst, idev);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    				if (idev) {
    					__ipv6_dev_mc_dec(idev, &mc_lst->addr);
    					in6_dev_put(idev);
    				}
    				dev_put(dev);
    
    			} else
    				(void) ip6_mc_leave_src(sk, mc_lst, NULL);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			sock_kfree_s(sk, mc_lst, sizeof(*mc_lst));
    			return 0;
    		}
    	}
    	write_unlock_bh(&ipv6_sk_mc_lock);
    
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    }
    
    static struct inet6_dev *ip6_mc_find_dev(struct in6_addr *group, int ifindex)
    {
    	struct net_device *dev = NULL;
    	struct inet6_dev *idev = NULL;
    
    	if (ifindex == 0) {
    		struct rt6_info *rt;
    
    
    		rt = rt6_lookup(&init_net, group, NULL, 0, 0);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		if (rt) {
    			dev = rt->rt6i_dev;
    			dev_hold(dev);
    			dst_release(&rt->u.dst);
    		}
    	} else
    
    		dev = dev_get_by_index(&init_net, ifindex);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    	if (!dev)
    		return NULL;
    	idev = in6_dev_get(dev);
    	if (!idev) {
    		dev_put(dev);
    		return NULL;
    	}
    	read_lock_bh(&idev->lock);
    	if (idev->dead) {
    		read_unlock_bh(&idev->lock);
    		in6_dev_put(idev);
    		dev_put(dev);
    		return NULL;
    	}
    	return idev;
    }
    
    void ipv6_sock_mc_close(struct sock *sk)
    {
    	struct ipv6_pinfo *np = inet6_sk(sk);
    	struct ipv6_mc_socklist *mc_lst;
    
    	write_lock_bh(&ipv6_sk_mc_lock);
    	while ((mc_lst = np->ipv6_mc_list) != NULL) {
    		struct net_device *dev;
    
    		np->ipv6_mc_list = mc_lst->next;
    		write_unlock_bh(&ipv6_sk_mc_lock);
    
    
    		dev = dev_get_by_index(&init_net, mc_lst->ifindex);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		if (dev) {
    			struct inet6_dev *idev = in6_dev_get(dev);
    
    
    			(void) ip6_mc_leave_src(sk, mc_lst, idev);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			if (idev) {
    				__ipv6_dev_mc_dec(idev, &mc_lst->addr);
    				in6_dev_put(idev);
    			}
    			dev_put(dev);
    
    		} else
    			(void) ip6_mc_leave_src(sk, mc_lst, NULL);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    		sock_kfree_s(sk, mc_lst, sizeof(*mc_lst));
    
    		write_lock_bh(&ipv6_sk_mc_lock);
    	}
    	write_unlock_bh(&ipv6_sk_mc_lock);
    }
    
    int ip6_mc_source(int add, int omode, struct sock *sk,
    	struct group_source_req *pgsr)
    {
    	struct in6_addr *source, *group;
    	struct ipv6_mc_socklist *pmc;
    	struct net_device *dev;
    	struct inet6_dev *idev;
    	struct ipv6_pinfo *inet6 = inet6_sk(sk);
    	struct ip6_sf_socklist *psl;
    	int i, j, rv;
    
    	int leavegroup = 0;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	int err;
    
    	if (pgsr->gsr_group.ss_family != AF_INET6 ||
    	    pgsr->gsr_source.ss_family != AF_INET6)
    		return -EINVAL;
    
    	source = &((struct sockaddr_in6 *)&pgsr->gsr_source)->sin6_addr;
    	group = &((struct sockaddr_in6 *)&pgsr->gsr_group)->sin6_addr;
    
    	if (!ipv6_addr_is_multicast(group))
    		return -EINVAL;
    
    	idev = ip6_mc_find_dev(group, pgsr->gsr_interface);
    	if (!idev)
    		return -ENODEV;
    	dev = idev->dev;
    
    	err = -EADDRNOTAVAIL;
    
    
    	read_lock_bh(&ipv6_sk_mc_lock);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	for (pmc=inet6->ipv6_mc_list; pmc; pmc=pmc->next) {
    		if (pgsr->gsr_interface && pmc->ifindex != pgsr->gsr_interface)
    			continue;
    		if (ipv6_addr_equal(&pmc->addr, group))
    			break;
    	}
    
    	if (!pmc) {		/* must have a prior join */
    		err = -EINVAL;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		goto done;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	/* if a source filter was set, must be the same mode as before */
    	if (pmc->sflist) {
    
    		if (pmc->sfmode != omode) {
    			err = -EINVAL;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			goto done;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	} else if (pmc->sfmode != omode) {
    		/* allow mode switches for empty-set filters */
    		ip6_mc_add_src(idev, group, omode, 0, NULL, 0);
    		ip6_mc_del_src(idev, group, pmc->sfmode, 0, NULL, 0);
    		pmc->sfmode = omode;
    	}
    
    
    	write_lock_bh(&pmc->sflock);
    	pmclocked = 1;
    
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	psl = pmc->sflist;
    	if (!add) {
    		if (!psl)
    
    			goto done;	/* err = -EADDRNOTAVAIL */
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		rv = !0;
    		for (i=0; i<psl->sl_count; i++) {
    			rv = memcmp(&psl->sl_addr[i], source,
    				sizeof(struct in6_addr));
    			if (rv == 0)
    				break;
    		}
    		if (rv)		/* source not found */
    
    			goto done;	/* err = -EADDRNOTAVAIL */
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    
    		/* special case - (INCLUDE, empty) == LEAVE_GROUP */
    		if (psl->sl_count == 1 && omode == MCAST_INCLUDE) {
    			leavegroup = 1;
    			goto done;
    		}
    
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		/* update the interface filter */
    		ip6_mc_del_src(idev, group, omode, 1, source, 1);
    
    		for (j=i+1; j<psl->sl_count; j++)
    			psl->sl_addr[j-1] = psl->sl_addr[j];
    		psl->sl_count--;
    		err = 0;
    		goto done;
    	}
    	/* else, add a new source to the filter */
    
    	if (psl && psl->sl_count >= sysctl_mld_max_msf) {
    		err = -ENOBUFS;
    		goto done;
    	}
    	if (!psl || psl->sl_count == psl->sl_max) {
    		struct ip6_sf_socklist *newpsl;
    		int count = IP6_SFBLOCK;
    
    		if (psl)
    			count += psl->sl_max;
    
    		newpsl = sock_kmalloc(sk, IP6_SFLSIZE(count), GFP_ATOMIC);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		if (!newpsl) {
    			err = -ENOBUFS;
    			goto done;
    		}
    		newpsl->sl_max = count;
    		newpsl->sl_count = count - IP6_SFBLOCK;
    		if (psl) {
    			for (i=0; i<psl->sl_count; i++)
    				newpsl->sl_addr[i] = psl->sl_addr[i];
    			sock_kfree_s(sk, psl, IP6_SFLSIZE(psl->sl_max));
    		}
    		pmc->sflist = psl = newpsl;
    	}
    	rv = 1;	/* > 0 for insert logic below if sl_count is 0 */
    	for (i=0; i<psl->sl_count; i++) {
    		rv = memcmp(&psl->sl_addr[i], source, sizeof(struct in6_addr));
    		if (rv == 0)
    			break;
    	}
    	if (rv == 0)		/* address already there is an error */
    		goto done;
    	for (j=psl->sl_count-1; j>=i; j--)
    		psl->sl_addr[j+1] = psl->sl_addr[j];
    	psl->sl_addr[i] = *source;
    	psl->sl_count++;
    	err = 0;
    	/* update the interface list */
    	ip6_mc_add_src(idev, group, omode, 1, source, 1);
    done:
    
    	if (pmclocked)
    		write_unlock_bh(&pmc->sflock);
    
    	read_unlock_bh(&ipv6_sk_mc_lock);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	read_unlock_bh(&idev->lock);
    	in6_dev_put(idev);
    	dev_put(dev);
    
    	if (leavegroup)
    		return ipv6_sock_mc_drop(sk, pgsr->gsr_interface, group);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	return err;
    }
    
    int ip6_mc_msfilter(struct sock *sk, struct group_filter *gsf)
    {
    	struct in6_addr *group;
    	struct ipv6_mc_socklist *pmc;
    	struct net_device *dev;
    	struct inet6_dev *idev;
    	struct ipv6_pinfo *inet6 = inet6_sk(sk);
    	struct ip6_sf_socklist *newpsl, *psl;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	int i, err;
    
    	group = &((struct sockaddr_in6 *)&gsf->gf_group)->sin6_addr;
    
    	if (!ipv6_addr_is_multicast(group))
    		return -EINVAL;
    	if (gsf->gf_fmode != MCAST_INCLUDE &&
    	    gsf->gf_fmode != MCAST_EXCLUDE)
    		return -EINVAL;
    
    	idev = ip6_mc_find_dev(group, gsf->gf_interface);
    
    	if (!idev)
    		return -ENODEV;
    	dev = idev->dev;
    
    
    	read_lock_bh(&ipv6_sk_mc_lock);
    
    
    	if (gsf->gf_fmode == MCAST_INCLUDE && gsf->gf_numsrc == 0) {
    		leavegroup = 1;
    		goto done;
    	}
    
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	for (pmc=inet6->ipv6_mc_list; pmc; pmc=pmc->next) {
    		if (pmc->ifindex != gsf->gf_interface)
    			continue;
    		if (ipv6_addr_equal(&pmc->addr, group))
    			break;
    	}
    
    	if (!pmc) {		/* must have a prior join */
    		err = -EINVAL;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		goto done;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	if (gsf->gf_numsrc) {
    
    		newpsl = sock_kmalloc(sk, IP6_SFLSIZE(gsf->gf_numsrc),
    							  GFP_ATOMIC);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		if (!newpsl) {
    			err = -ENOBUFS;
    			goto done;
    		}
    		newpsl->sl_max = newpsl->sl_count = gsf->gf_numsrc;
    		for (i=0; i<newpsl->sl_count; ++i) {
    			struct sockaddr_in6 *psin6;
    
    			psin6 = (struct sockaddr_in6 *)&gsf->gf_slist[i];
    			newpsl->sl_addr[i] = psin6->sin6_addr;
    		}
    		err = ip6_mc_add_src(idev, group, gsf->gf_fmode,
    			newpsl->sl_count, newpsl->sl_addr, 0);
    		if (err) {
    			sock_kfree_s(sk, newpsl, IP6_SFLSIZE(newpsl->sl_max));
    			goto done;
    		}
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    		newpsl = NULL;
    
    		(void) ip6_mc_add_src(idev, group, gsf->gf_fmode, 0, NULL, 0);
    	}
    
    
    	write_lock_bh(&pmc->sflock);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	psl = pmc->sflist;
    	if (psl) {
    		(void) ip6_mc_del_src(idev, group, pmc->sfmode,
    			psl->sl_count, psl->sl_addr, 0);
    		sock_kfree_s(sk, psl, IP6_SFLSIZE(psl->sl_max));
    	} else
    		(void) ip6_mc_del_src(idev, group, pmc->sfmode, 0, NULL, 0);
    	pmc->sflist = newpsl;
    	pmc->sfmode = gsf->gf_fmode;
    
    	write_unlock_bh(&pmc->sflock);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    done:
    
    	read_unlock_bh(&ipv6_sk_mc_lock);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	read_unlock_bh(&idev->lock);
    	in6_dev_put(idev);
    	dev_put(dev);
    
    	if (leavegroup)
    		err = ipv6_sock_mc_drop(sk, gsf->gf_interface, group);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	return err;
    }
    
    int ip6_mc_msfget(struct sock *sk, struct group_filter *gsf,
    	struct group_filter __user *optval, int __user *optlen)
    {
    	int err, i, count, copycount;
    	struct in6_addr *group;
    	struct ipv6_mc_socklist *pmc;
    	struct inet6_dev *idev;
    	struct net_device *dev;
    	struct ipv6_pinfo *inet6 = inet6_sk(sk);
    	struct ip6_sf_socklist *psl;
    
    	group = &((struct sockaddr_in6 *)&gsf->gf_group)->sin6_addr;
    
    	if (!ipv6_addr_is_multicast(group))
    		return -EINVAL;
    
    	idev = ip6_mc_find_dev(group, gsf->gf_interface);
    
    	if (!idev)
    		return -ENODEV;
    
    	dev = idev->dev;
    
    	err = -EADDRNOTAVAIL;
    
    	/*
    	 * changes to the ipv6_mc_list require the socket lock and
    	 * a read lock on ip6_sk_mc_lock. We have the socket lock,
    	 * so reading the list is safe.
    	 */
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    	for (pmc=inet6->ipv6_mc_list; pmc; pmc=pmc->next) {
    		if (pmc->ifindex != gsf->gf_interface)
    			continue;
    		if (ipv6_addr_equal(group, &pmc->addr))
    			break;
    	}
    	if (!pmc)		/* must have a prior join */
    		goto done;
    	gsf->gf_fmode = pmc->sfmode;
    	psl = pmc->sflist;
    	count = psl ? psl->sl_count : 0;
    	read_unlock_bh(&idev->lock);
    	in6_dev_put(idev);
    	dev_put(dev);
    
    	copycount = count < gsf->gf_numsrc ? count : gsf->gf_numsrc;
    	gsf->gf_numsrc = count;
    	if (put_user(GROUP_FILTER_SIZE(copycount), optlen) ||
    	    copy_to_user(optval, gsf, GROUP_FILTER_SIZE(0))) {
    		return -EFAULT;
    	}
    
    	/* changes to psl require the socket lock, a read lock on
    	 * on ipv6_sk_mc_lock and a write lock on pmc->sflock. We
    	 * have the socket lock, so reading here is safe.
    	 */
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	for (i=0; i<copycount; i++) {
    		struct sockaddr_in6 *psin6;
    		struct sockaddr_storage ss;
    
    		psin6 = (struct sockaddr_in6 *)&ss;
    		memset(&ss, 0, sizeof(ss));
    		psin6->sin6_family = AF_INET6;
    		psin6->sin6_addr = psl->sl_addr[i];
    
    		if (copy_to_user(&optval->gf_slist[i], &ss, sizeof(ss)))
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    			return -EFAULT;
    	}
    	return 0;
    done:
    	read_unlock_bh(&idev->lock);
    	in6_dev_put(idev);
    	dev_put(dev);
    	return err;
    }
    
    int inet6_mc_check(struct sock *sk, struct in6_addr *mc_addr,
    	struct in6_addr *src_addr)
    {
    	struct ipv6_pinfo *np = inet6_sk(sk);
    	struct ipv6_mc_socklist *mc;
    	struct ip6_sf_socklist *psl;
    	int rv = 1;
    
    	read_lock(&ipv6_sk_mc_lock);
    	for (mc = np->ipv6_mc_list; mc; mc = mc->next) {
    		if (ipv6_addr_equal(&mc->addr, mc_addr))
    			break;
    	}
    	if (!mc) {
    		read_unlock(&ipv6_sk_mc_lock);
    		return 1;
    	}
    
    	read_lock(&mc->sflock);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	psl = mc->sflist;
    	if (!psl) {
    		rv = mc->sfmode == MCAST_EXCLUDE;
    	} else {
    		int i;
    
    		for (i=0; i<psl->sl_count; i++) {
    			if (ipv6_addr_equal(&psl->sl_addr[i], src_addr))
    				break;
    		}
    		if (mc->sfmode == MCAST_INCLUDE && i >= psl->sl_count)
    			rv = 0;
    		if (mc->sfmode == MCAST_EXCLUDE && i < psl->sl_count)
    			rv = 0;
    	}
    
    	read_unlock(&mc->sflock);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	read_unlock(&ipv6_sk_mc_lock);
    
    	return rv;
    }
    
    static void ma_put(struct ifmcaddr6 *mc)
    {
    	if (atomic_dec_and_test(&mc->mca_refcnt)) {
    		in6_dev_put(mc->idev);
    		kfree(mc);
    	}
    }
    
    static void igmp6_group_added(struct ifmcaddr6 *mc)
    {
    	struct net_device *dev = mc->idev->dev;
    	char buf[MAX_ADDR_LEN];
    
    	spin_lock_bh(&mc->mca_lock);
    	if (!(mc->mca_flags&MAF_LOADED)) {
    		mc->mca_flags |= MAF_LOADED;
    		if (ndisc_mc_map(&mc->mca_addr, buf, dev, 0) == 0)
    			dev_mc_add(dev, buf, dev->addr_len, 0);
    	}
    	spin_unlock_bh(&mc->mca_lock);
    
    	if (!(dev->flags & IFF_UP) || (mc->mca_flags & MAF_NOREPORT))
    		return;
    
    	if (MLD_V1_SEEN(mc->idev)) {
    		igmp6_join_group(mc);
    		return;
    	}
    	/* else v2 */
    
    	mc->mca_crcount = mc->idev->mc_qrv;
    	mld_ifc_event(mc->idev);
    }
    
    static void igmp6_group_dropped(struct ifmcaddr6 *mc)
    {
    	struct net_device *dev = mc->idev->dev;
    	char buf[MAX_ADDR_LEN];
    
    	spin_lock_bh(&mc->mca_lock);
    	if (mc->mca_flags&MAF_LOADED) {
    		mc->mca_flags &= ~MAF_LOADED;
    		if (ndisc_mc_map(&mc->mca_addr, buf, dev, 0) == 0)
    			dev_mc_delete(dev, buf, dev->addr_len, 0);
    	}
    
    	if (mc->mca_flags & MAF_NOREPORT)
    		goto done;
    	spin_unlock_bh(&mc->mca_lock);
    
    	if (!mc->idev->dead)
    		igmp6_leave_group(mc);
    
    	spin_lock_bh(&mc->mca_lock);
    	if (del_timer(&mc->mca_timer))
    		atomic_dec(&mc->mca_refcnt);
    done:
    	ip6_mc_clear_src(mc);
    	spin_unlock_bh(&mc->mca_lock);
    }
    
    /*
     * deleted ifmcaddr6 manipulation
     */
    static void mld_add_delrec(struct inet6_dev *idev, struct ifmcaddr6 *im)
    {
    	struct ifmcaddr6 *pmc;
    
    	/* this is an "ifmcaddr6" for convenience; only the fields below
    	 * are actually used. In particular, the refcnt and users are not
    	 * used for management of the delete list. Using the same structure
    	 * for deleted items allows change reports to use common code with
    	 * non-deleted or query-response MCA's.
    	 */
    
    	pmc = kzalloc(sizeof(*pmc), GFP_ATOMIC);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	if (!pmc)
    		return;
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    	spin_lock_bh(&im->mca_lock);
    	spin_lock_init(&pmc->mca_lock);
    	pmc->idev = im->idev;
    	in6_dev_hold(idev);
    	pmc->mca_addr = im->mca_addr;
    	pmc->mca_crcount = idev->mc_qrv;
    	pmc->mca_sfmode = im->mca_sfmode;
    	if (pmc->mca_sfmode == MCAST_INCLUDE) {
    		struct ip6_sf_list *psf;
    
    		pmc->mca_tomb = im->mca_tomb;
    		pmc->mca_sources = im->mca_sources;
    		im->mca_tomb = im->mca_sources = NULL;
    		for (psf=pmc->mca_sources; psf; psf=psf->sf_next)
    			psf->sf_crcount = pmc->mca_crcount;
    	}
    	spin_unlock_bh(&im->mca_lock);
    
    	write_lock_bh(&idev->mc_lock);
    	pmc->next = idev->mc_tomb;
    	idev->mc_tomb = pmc;
    	write_unlock_bh(&idev->mc_lock);
    }
    
    static void mld_del_delrec(struct inet6_dev *idev, struct in6_addr *pmca)
    {
    	struct ifmcaddr6 *pmc, *pmc_prev;
    	struct ip6_sf_list *psf, *psf_next;
    
    	write_lock_bh(&idev->mc_lock);
    	pmc_prev = NULL;
    	for (pmc=idev->mc_tomb; pmc; pmc=pmc->next) {
    		if (ipv6_addr_equal(&pmc->mca_addr, pmca))
    			break;
    		pmc_prev = pmc;
    	}
    	if (pmc) {
    		if (pmc_prev)
    			pmc_prev->next = pmc->next;
    		else
    			idev->mc_tomb = pmc->next;
    	}
    	write_unlock_bh(&idev->mc_lock);
    	if (pmc) {
    		for (psf=pmc->mca_tomb; psf; psf=psf_next) {
    			psf_next = psf->sf_next;
    			kfree(psf);
    		}
    		in6_dev_put(pmc->idev);
    		kfree(pmc);
    	}
    }
    
    static void mld_clear_delrec(struct inet6_dev *idev)
    {
    	struct ifmcaddr6 *pmc, *nextpmc;
    
    	write_lock_bh(&idev->mc_lock);
    	pmc = idev->mc_tomb;
    	idev->mc_tomb = NULL;
    	write_unlock_bh(&idev->mc_lock);
    
    	for (; pmc; pmc = nextpmc) {
    		nextpmc = pmc->next;
    		ip6_mc_clear_src(pmc);
    		in6_dev_put(pmc->idev);
    		kfree(pmc);
    	}
    
    	/* clear dead sources, too */
    	read_lock_bh(&idev->lock);
    	for (pmc=idev->mc_list; pmc; pmc=pmc->next) {
    		struct ip6_sf_list *psf, *psf_next;
    
    		spin_lock_bh(&pmc->mca_lock);
    		psf = pmc->mca_tomb;
    		pmc->mca_tomb = NULL;
    		spin_unlock_bh(&pmc->mca_lock);
    		for (; psf; psf=psf_next) {
    			psf_next = psf->sf_next;
    			kfree(psf);
    		}
    	}
    	read_unlock_bh(&idev->lock);
    }
    
    
    /*
     *	device multicast group inc (add if not found)
     */
    int ipv6_dev_mc_inc(struct net_device *dev, struct in6_addr *addr)
    {
    	struct ifmcaddr6 *mc;
    	struct inet6_dev *idev;
    
    	idev = in6_dev_get(dev);
    
    	if (idev == NULL)
    		return -EINVAL;
    
    	write_lock_bh(&idev->lock);
    	if (idev->dead) {
    		write_unlock_bh(&idev->lock);
    		in6_dev_put(idev);
    		return -ENODEV;
    	}
    
    	for (mc = idev->mc_list; mc; mc = mc->next) {
    		if (ipv6_addr_equal(&mc->mca_addr, addr)) {
    			mc->mca_users++;
    			write_unlock_bh(&idev->lock);
    			ip6_mc_add_src(idev, &mc->mca_addr, MCAST_EXCLUDE, 0,
    				NULL, 0);
    			in6_dev_put(idev);
    			return 0;
    		}
    	}
    
    	/*
    	 *	not found: create a new one.
    	 */
    
    
    	mc = kzalloc(sizeof(struct ifmcaddr6), GFP_ATOMIC);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    	if (mc == NULL) {
    		write_unlock_bh(&idev->lock);
    		in6_dev_put(idev);
    		return -ENOMEM;
    	}
    
    
    	setup_timer(&mc->mca_timer, igmp6_timer_handler, (unsigned long)mc);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    	ipv6_addr_copy(&mc->mca_addr, addr);
    	mc->idev = idev;
    	mc->mca_users = 1;
    	/* mca_stamp should be updated upon changes */
    	mc->mca_cstamp = mc->mca_tstamp = jiffies;
    	atomic_set(&mc->mca_refcnt, 2);
    	spin_lock_init(&mc->mca_lock);
    
    	/* initial mode is (EX, empty) */
    	mc->mca_sfmode = MCAST_EXCLUDE;
    	mc->mca_sfcount[MCAST_EXCLUDE] = 1;
    
    	if (ipv6_addr_is_ll_all_nodes(&mc->mca_addr) ||
    	    IPV6_ADDR_MC_SCOPE(&mc->mca_addr) < IPV6_ADDR_SCOPE_LINKLOCAL)
    		mc->mca_flags |= MAF_NOREPORT;
    
    	mc->next = idev->mc_list;
    	idev->mc_list = mc;
    	write_unlock_bh(&idev->lock);
    
    	mld_del_delrec(idev, &mc->mca_addr);
    	igmp6_group_added(mc);
    	ma_put(mc);
    	return 0;
    }
    
    /*
     *	device multicast group del
     */
    int __ipv6_dev_mc_dec(struct inet6_dev *idev, struct in6_addr *addr)
    {
    	struct ifmcaddr6 *ma, **map;
    
    	write_lock_bh(&idev->lock);
    	for (map = &idev->mc_list; (ma=*map) != NULL; map = &ma->next) {
    		if (ipv6_addr_equal(&ma->mca_addr, addr)) {
    			if (--ma->mca_users == 0) {
    				*map = ma->next;
    				write_unlock_bh(&idev->lock);
    
    				igmp6_group_dropped(ma);
    
    				ma_put(ma);
    				return 0;
    			}
    			write_unlock_bh(&idev->lock);
    			return 0;
    		}
    	}
    	write_unlock_bh(&idev->lock);
    
    	return -ENOENT;
    }
    
    int ipv6_dev_mc_dec(struct net_device *dev, struct in6_addr *addr)
    {
    	struct inet6_dev *idev = in6_dev_get(dev);
    	int err;
    
    	if (!idev)
    		return -ENODEV;
    
    	err = __ipv6_dev_mc_dec(idev, addr);
    
    	in6_dev_put(idev);
    
    	return err;
    }
    
    /*
     * identify MLD packets for MLD filter exceptions
     */
    int ipv6_is_mld(struct sk_buff *skb, int nexthdr)
    {
    	struct icmp6hdr *pic;
    
    	if (nexthdr != IPPROTO_ICMPV6)
    		return 0;
    
    	if (!pskb_may_pull(skb, sizeof(struct icmp6hdr)))
    		return 0;
    
    
    	pic = icmp6_hdr(skb);
    
    Linus Torvalds's avatar
    Linus Torvalds committed
    
    	switch (pic->icmp6_type) {
    	case ICMPV6_MGM_QUERY:
    	case ICMPV6_MGM_REPORT:
    	case ICMPV6_MGM_REDUCTION:
    	case ICMPV6_MLD2_REPORT:
    		return 1;
    	default:
    		break;
    	}