/* Copyright (c) 2014 Mahesh Bandewar <maheshb@google.com>
*
* 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.
*
*/
#include "ipvlan.h"
static unsigned int ipvlan_netid __read_mostly;
struct ipvlan_netns {
unsigned int ipvl_nf_hook_refcnt;
};
static const struct nf_hook_ops ipvl_nfops[] = {
{
.hook = ipvlan_nf_input,
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN,
.priority = INT_MAX,
},
#if IS_ENABLED(CONFIG_IPV6)
{
.hook = ipvlan_nf_input,
.pf = NFPROTO_IPV6,
.hooknum = NF_INET_LOCAL_IN,
.priority = INT_MAX,
},
#endif
};
static const struct l3mdev_ops ipvl_l3mdev_ops = {
.l3mdev_l3_rcv = ipvlan_l3_rcv,
};
static void ipvlan_adjust_mtu(struct ipvl_dev *ipvlan, struct net_device *dev)
{
ipvlan->dev->mtu = dev->mtu;
}
static int ipvlan_register_nf_hook(struct net *net)
{
struct ipvlan_netns *vnet = net_generic(net, ipvlan_netid);
int err = 0;
if (!vnet->ipvl_nf_hook_refcnt) {
err = nf_register_net_hooks(net, ipvl_nfops,
ARRAY_SIZE(ipvl_nfops));
if (!err)
vnet->ipvl_nf_hook_refcnt = 1;
} else {
vnet->ipvl_nf_hook_refcnt++;
}
return err;
}
static void ipvlan_unregister_nf_hook(struct net *net)
{
struct ipvlan_netns *vnet = net_generic(net, ipvlan_netid);
if (WARN_ON(!vnet->ipvl_nf_hook_refcnt))
return;
vnet->ipvl_nf_hook_refcnt--;
if (!vnet->ipvl_nf_hook_refcnt)
nf_unregister_net_hooks(net, ipvl_nfops,
ARRAY_SIZE(ipvl_nfops));
}
static int ipvlan_set_port_mode(struct ipvl_port *port, u16 nval,
struct netlink_ext_ack *extack)
{
struct ipvl_dev *ipvlan;
struct net_device *mdev = port->dev;
unsigned int flags;
int err;
ASSERT_RTNL();
if (port->mode != nval) {
list_for_each_entry(ipvlan, &port->ipvlans, pnode) {
flags = ipvlan->dev->flags;
if (nval == IPVLAN_MODE_L3 || nval == IPVLAN_MODE_L3S) {
err = dev_change_flags(ipvlan->dev,
flags | IFF_NOARP);
} else {
err = dev_change_flags(ipvlan->dev,
flags & ~IFF_NOARP);
}
if (unlikely(err))
goto fail;
}
if (nval == IPVLAN_MODE_L3S) {
/* New mode is L3S */
err = ipvlan_register_nf_hook(read_pnet(&port->pnet));
if (!err) {
mdev->l3mdev_ops = &ipvl_l3mdev_ops;
mdev->priv_flags |= IFF_L3MDEV_MASTER;
} else
goto fail;
} else if (port->mode == IPVLAN_MODE_L3S) {
/* Old mode was L3S */
mdev->priv_flags &= ~IFF_L3MDEV_MASTER;
ipvlan_unregister_nf_hook(read_pnet(&port->pnet));
mdev->l3mdev_ops = NULL;
}
port->mode = nval;
}
return 0;
fail:
/* Undo the flags changes that have been done so far. */
list_for_each_entry_continue_reverse(ipvlan, &port->ipvlans, pnode) {
flags = ipvlan->dev->flags;
if (port->mode == IPVLAN_MODE_L3 ||
port->mode == IPVLAN_MODE_L3S)
dev_change_flags(ipvlan->dev, flags | IFF_NOARP);
else
dev_change_flags(ipvlan->dev, flags & ~IFF_NOARP);
}
return err;
}
static int ipvlan_port_create(struct net_device *dev)
{
struct ipvl_port *port;
int err, idx;
port = kzalloc(sizeof(struct ipvl_port), GFP_KERNEL);
if (!port)
return -ENOMEM;
write_pnet(&port->pnet, dev_net(dev));
port->dev = dev;
port->mode = IPVLAN_MODE_L3;
INIT_LIST_HEAD(&port->ipvlans);
for (idx = 0; idx < IPVLAN_HASH_SIZE; idx++)
INIT_HLIST_HEAD(&port->hlhead[idx]);
skb_queue_head_init(&port->backlog