mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-03-21 23:16:50 +08:00
Whenever a TEQL devices has a lockless Qdisc as root, qdisc_reset should
be called using the seq_lock to avoid racing with the datapath. Failure
to do so may cause crashes like the following:
[ 238.028993][ T318] BUG: KASAN: double-free in skb_release_data (net/core/skbuff.c:1139)
[ 238.029328][ T318] Free of addr ffff88810c67ec00 by task poc_teql_uaf_ke/318
[ 238.029749][ T318]
[ 238.029900][ T318] CPU: 3 UID: 0 PID: 318 Comm: poc_teql_ke Not tainted 7.0.0-rc3-00149-ge5b31d988a41 #704 PREEMPT(full)
[ 238.029906][ T318] Hardware name: Bochs Bochs, BIOS Bochs 01/01/2011
[ 238.029910][ T318] Call Trace:
[ 238.029913][ T318] <TASK>
[ 238.029916][ T318] dump_stack_lvl (lib/dump_stack.c:122)
[ 238.029928][ T318] print_report (mm/kasan/report.c:379 mm/kasan/report.c:482)
[ 238.029940][ T318] ? skb_release_data (net/core/skbuff.c:1139)
[ 238.029944][ T318] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:221)
...
[ 238.029957][ T318] ? skb_release_data (net/core/skbuff.c:1139)
[ 238.029969][ T318] kasan_report_invalid_free (mm/kasan/report.c:221 mm/kasan/report.c:563)
[ 238.029979][ T318] ? skb_release_data (net/core/skbuff.c:1139)
[ 238.029989][ T318] check_slab_allocation (mm/kasan/common.c:231)
[ 238.029995][ T318] kmem_cache_free (mm/slub.c:2637 (discriminator 1) mm/slub.c:6168 (discriminator 1) mm/slub.c:6298 (discriminator 1))
[ 238.030004][ T318] skb_release_data (net/core/skbuff.c:1139)
...
[ 238.030025][ T318] sk_skb_reason_drop (net/core/skbuff.c:1256)
[ 238.030032][ T318] pfifo_fast_reset (./include/linux/ptr_ring.h:171 ./include/linux/ptr_ring.h:309 ./include/linux/skb_array.h:98 net/sched/sch_generic.c:827)
[ 238.030039][ T318] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:221)
...
[ 238.030054][ T318] qdisc_reset (net/sched/sch_generic.c:1034)
[ 238.030062][ T318] teql_destroy (./include/linux/spinlock.h:395 net/sched/sch_teql.c:157)
[ 238.030071][ T318] __qdisc_destroy (./include/net/pkt_sched.h:328 net/sched/sch_generic.c:1077)
[ 238.030077][ T318] qdisc_graft (net/sched/sch_api.c:1062 net/sched/sch_api.c:1053 net/sched/sch_api.c:1159)
[ 238.030089][ T318] ? __pfx_qdisc_graft (net/sched/sch_api.c:1091)
[ 238.030095][ T318] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:221)
[ 238.030102][ T318] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:221)
[ 238.030106][ T318] ? srso_alias_return_thunk (arch/x86/lib/retpoline.S:221)
[ 238.030114][ T318] tc_get_qdisc (net/sched/sch_api.c:1529 net/sched/sch_api.c:1556)
...
[ 238.072958][ T318] Allocated by task 303 on cpu 5 at 238.026275s:
[ 238.073392][ T318] kasan_save_stack (mm/kasan/common.c:58)
[ 238.073884][ T318] kasan_save_track (mm/kasan/common.c:64 (discriminator 5) mm/kasan/common.c:79 (discriminator 5))
[ 238.074230][ T318] __kasan_slab_alloc (mm/kasan/common.c:369)
[ 238.074578][ T318] kmem_cache_alloc_node_noprof (./include/linux/kasan.h:253 mm/slub.c:4542 mm/slub.c:4869 mm/slub.c:4921)
[ 238.076091][ T318] kmalloc_reserve (net/core/skbuff.c:616 (discriminator 107))
[ 238.076450][ T318] __alloc_skb (net/core/skbuff.c:713)
[ 238.076834][ T318] alloc_skb_with_frags (./include/linux/skbuff.h:1383 net/core/skbuff.c:6763)
[ 238.077178][ T318] sock_alloc_send_pskb (net/core/sock.c:2997)
[ 238.077520][ T318] packet_sendmsg (net/packet/af_packet.c:2926 net/packet/af_packet.c:3019 net/packet/af_packet.c:3108)
[ 238.081469][ T318]
[ 238.081870][ T318] Freed by task 299 on cpu 1 at 238.028496s:
[ 238.082761][ T318] kasan_save_stack (mm/kasan/common.c:58)
[ 238.083481][ T318] kasan_save_track (mm/kasan/common.c:64 (discriminator 5) mm/kasan/common.c:79 (discriminator 5))
[ 238.085348][ T318] kasan_save_free_info (mm/kasan/generic.c:587 (discriminator 1))
[ 238.085900][ T318] __kasan_slab_free (mm/kasan/common.c:287)
[ 238.086439][ T318] kmem_cache_free (mm/slub.c:6168 (discriminator 3) mm/slub.c:6298 (discriminator 3))
[ 238.087007][ T318] skb_release_data (net/core/skbuff.c:1139)
[ 238.087491][ T318] consume_skb (net/core/skbuff.c:1451)
[ 238.087757][ T318] teql_master_xmit (net/sched/sch_teql.c:358)
[ 238.088116][ T318] dev_hard_start_xmit (./include/linux/netdevice.h:5324 ./include/linux/netdevice.h:5333 net/core/dev.c:3871 net/core/dev.c:3887)
[ 238.088468][ T318] sch_direct_xmit (net/sched/sch_generic.c:347)
[ 238.088820][ T318] __qdisc_run (net/sched/sch_generic.c:420 (discriminator 1))
[ 238.089166][ T318] __dev_queue_xmit (./include/net/sch_generic.h:229 ./include/net/pkt_sched.h:121 ./include/net/pkt_sched.h:117 net/core/dev.c:4196 net/core/dev.c:4802)
Workflow to reproduce:
1. Initialize a TEQL topology (dummy0 and ifb0 as slaves, teql0 up).
2. Start multiple sender workers continuously transmitting packets
through teql0 to drive teql_master_xmit().
3. In parallel, repeatedly delete and re-add the root qdisc on
dummy0 and ifb0 via RTNETLINK, forcing frequent teardown and reset activity
(teql_destroy() / qdisc_reset()).
4. After running both workloads concurrently for several iterations,
KASAN reports slab-use-after-free or double-free in the skb free path.
Fix this by moving dev_reset_queue to sch_generic.h and calling it, instead
of qdisc_reset, in teql_destroy since it handles both the lock and lockless
cases correctly for root qdiscs.
Fixes: 96009c7d50 ("sched: replace __QDISC_STATE_RUNNING bit with a spin lock")
Reported-by: Xianrui Dong <keenanat2000@gmail.com>
Tested-by: Xianrui Dong <keenanat2000@gmail.com>
Co-developed-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
Link: https://patch.msgid.link/20260315155422.147256-1-jhs@mojatatu.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
530 lines
12 KiB
C
530 lines
12 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/* net/sched/sch_teql.c "True" (or "trivial") link equalizer.
|
|
*
|
|
* Authors: Alexey Kuznetsov, <kuznet@ms2.inr.ac.ru>
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/types.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/string.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/if_arp.h>
|
|
#include <linux/netdevice.h>
|
|
#include <linux/init.h>
|
|
#include <linux/skbuff.h>
|
|
#include <linux/moduleparam.h>
|
|
#include <net/dst.h>
|
|
#include <net/neighbour.h>
|
|
#include <net/pkt_sched.h>
|
|
|
|
/*
|
|
How to setup it.
|
|
----------------
|
|
|
|
After loading this module you will find a new device teqlN
|
|
and new qdisc with the same name. To join a slave to the equalizer
|
|
you should just set this qdisc on a device f.e.
|
|
|
|
# tc qdisc add dev eth0 root teql0
|
|
# tc qdisc add dev eth1 root teql0
|
|
|
|
That's all. Full PnP 8)
|
|
|
|
Applicability.
|
|
--------------
|
|
|
|
1. Slave devices MUST be active devices, i.e., they must raise the tbusy
|
|
signal and generate EOI events. If you want to equalize virtual devices
|
|
like tunnels, use a normal eql device.
|
|
2. This device puts no limitations on physical slave characteristics
|
|
f.e. it will equalize 9600baud line and 100Mb ethernet perfectly :-)
|
|
Certainly, large difference in link speeds will make the resulting
|
|
eqalized link unusable, because of huge packet reordering.
|
|
I estimate an upper useful difference as ~10 times.
|
|
3. If the slave requires address resolution, only protocols using
|
|
neighbour cache (IPv4/IPv6) will work over the equalized link.
|
|
Other protocols are still allowed to use the slave device directly,
|
|
which will not break load balancing, though native slave
|
|
traffic will have the highest priority. */
|
|
|
|
struct teql_master {
|
|
struct Qdisc_ops qops;
|
|
struct net_device *dev;
|
|
struct Qdisc *slaves;
|
|
struct list_head master_list;
|
|
unsigned long tx_bytes;
|
|
unsigned long tx_packets;
|
|
unsigned long tx_errors;
|
|
unsigned long tx_dropped;
|
|
};
|
|
|
|
struct teql_sched_data {
|
|
struct Qdisc *next;
|
|
struct teql_master *m;
|
|
struct sk_buff_head q;
|
|
};
|
|
|
|
#define NEXT_SLAVE(q) (((struct teql_sched_data *)qdisc_priv(q))->next)
|
|
|
|
#define FMASK (IFF_BROADCAST | IFF_POINTOPOINT)
|
|
|
|
/* "teql*" qdisc routines */
|
|
|
|
static int
|
|
teql_enqueue(struct sk_buff *skb, struct Qdisc *sch, struct sk_buff **to_free)
|
|
{
|
|
struct net_device *dev = qdisc_dev(sch);
|
|
struct teql_sched_data *q = qdisc_priv(sch);
|
|
|
|
if (q->q.qlen < READ_ONCE(dev->tx_queue_len)) {
|
|
__skb_queue_tail(&q->q, skb);
|
|
return NET_XMIT_SUCCESS;
|
|
}
|
|
|
|
return qdisc_drop(skb, sch, to_free);
|
|
}
|
|
|
|
static struct sk_buff *
|
|
teql_dequeue(struct Qdisc *sch)
|
|
{
|
|
struct teql_sched_data *dat = qdisc_priv(sch);
|
|
struct netdev_queue *dat_queue;
|
|
struct sk_buff *skb;
|
|
struct Qdisc *q;
|
|
|
|
skb = __skb_dequeue(&dat->q);
|
|
dat_queue = netdev_get_tx_queue(dat->m->dev, 0);
|
|
q = rcu_dereference_bh(dat_queue->qdisc);
|
|
|
|
if (skb == NULL) {
|
|
struct net_device *m = qdisc_dev(q);
|
|
if (m) {
|
|
dat->m->slaves = sch;
|
|
netif_wake_queue(m);
|
|
}
|
|
} else {
|
|
qdisc_bstats_update(sch, skb);
|
|
}
|
|
sch->q.qlen = dat->q.qlen + q->q.qlen;
|
|
return skb;
|
|
}
|
|
|
|
static struct sk_buff *
|
|
teql_peek(struct Qdisc *sch)
|
|
{
|
|
/* teql is meant to be used as root qdisc */
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
teql_reset(struct Qdisc *sch)
|
|
{
|
|
struct teql_sched_data *dat = qdisc_priv(sch);
|
|
|
|
skb_queue_purge(&dat->q);
|
|
}
|
|
|
|
static void
|
|
teql_destroy(struct Qdisc *sch)
|
|
{
|
|
struct Qdisc *q, *prev;
|
|
struct teql_sched_data *dat = qdisc_priv(sch);
|
|
struct teql_master *master = dat->m;
|
|
|
|
if (!master)
|
|
return;
|
|
|
|
prev = master->slaves;
|
|
if (prev) {
|
|
do {
|
|
q = NEXT_SLAVE(prev);
|
|
if (q == sch) {
|
|
NEXT_SLAVE(prev) = NEXT_SLAVE(q);
|
|
if (q == master->slaves) {
|
|
master->slaves = NEXT_SLAVE(q);
|
|
if (q == master->slaves) {
|
|
struct netdev_queue *txq;
|
|
|
|
txq = netdev_get_tx_queue(master->dev, 0);
|
|
master->slaves = NULL;
|
|
|
|
dev_reset_queue(master->dev,
|
|
txq, NULL);
|
|
}
|
|
}
|
|
skb_queue_purge(&dat->q);
|
|
break;
|
|
}
|
|
|
|
} while ((prev = q) != master->slaves);
|
|
}
|
|
}
|
|
|
|
static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
|
|
struct netlink_ext_ack *extack)
|
|
{
|
|
struct net_device *dev = qdisc_dev(sch);
|
|
struct teql_master *m = (struct teql_master *)sch->ops;
|
|
struct teql_sched_data *q = qdisc_priv(sch);
|
|
|
|
if (dev->hard_header_len > m->dev->hard_header_len)
|
|
return -EINVAL;
|
|
|
|
if (m->dev == dev)
|
|
return -ELOOP;
|
|
|
|
if (sch->parent != TC_H_ROOT) {
|
|
NL_SET_ERR_MSG_MOD(extack, "teql can only be used as root");
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
q->m = m;
|
|
|
|
skb_queue_head_init(&q->q);
|
|
|
|
if (m->slaves) {
|
|
if (m->dev->flags & IFF_UP) {
|
|
if ((m->dev->flags & IFF_POINTOPOINT &&
|
|
!(dev->flags & IFF_POINTOPOINT)) ||
|
|
(m->dev->flags & IFF_BROADCAST &&
|
|
!(dev->flags & IFF_BROADCAST)) ||
|
|
(m->dev->flags & IFF_MULTICAST &&
|
|
!(dev->flags & IFF_MULTICAST)) ||
|
|
dev->mtu < m->dev->mtu)
|
|
return -EINVAL;
|
|
} else {
|
|
if (!(dev->flags&IFF_POINTOPOINT))
|
|
m->dev->flags &= ~IFF_POINTOPOINT;
|
|
if (!(dev->flags&IFF_BROADCAST))
|
|
m->dev->flags &= ~IFF_BROADCAST;
|
|
if (!(dev->flags&IFF_MULTICAST))
|
|
m->dev->flags &= ~IFF_MULTICAST;
|
|
if (dev->mtu < m->dev->mtu)
|
|
m->dev->mtu = dev->mtu;
|
|
}
|
|
q->next = NEXT_SLAVE(m->slaves);
|
|
NEXT_SLAVE(m->slaves) = sch;
|
|
} else {
|
|
q->next = sch;
|
|
m->slaves = sch;
|
|
m->dev->mtu = dev->mtu;
|
|
m->dev->flags = (m->dev->flags&~FMASK)|(dev->flags&FMASK);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
__teql_resolve(struct sk_buff *skb, struct sk_buff *skb_res,
|
|
struct net_device *dev, struct netdev_queue *txq,
|
|
struct dst_entry *dst)
|
|
{
|
|
struct neighbour *n;
|
|
int err = 0;
|
|
|
|
n = dst_neigh_lookup_skb(dst, skb);
|
|
if (!n)
|
|
return -ENOENT;
|
|
|
|
if (dst->dev != dev) {
|
|
struct neighbour *mn;
|
|
|
|
mn = __neigh_lookup_errno(n->tbl, n->primary_key, dev);
|
|
neigh_release(n);
|
|
if (IS_ERR(mn))
|
|
return PTR_ERR(mn);
|
|
n = mn;
|
|
}
|
|
|
|
if (neigh_event_send(n, skb_res) == 0) {
|
|
int err;
|
|
char haddr[MAX_ADDR_LEN];
|
|
|
|
neigh_ha_snapshot(haddr, n, dev);
|
|
err = dev_hard_header(skb, dev, ntohs(skb_protocol(skb, false)),
|
|
haddr, NULL, skb->len);
|
|
|
|
if (err < 0)
|
|
err = -EINVAL;
|
|
} else {
|
|
err = (skb_res == NULL) ? -EAGAIN : 1;
|
|
}
|
|
neigh_release(n);
|
|
return err;
|
|
}
|
|
|
|
static inline int teql_resolve(struct sk_buff *skb,
|
|
struct sk_buff *skb_res,
|
|
struct net_device *dev,
|
|
struct netdev_queue *txq)
|
|
{
|
|
struct dst_entry *dst = skb_dst(skb);
|
|
int res;
|
|
|
|
if (rcu_access_pointer(txq->qdisc) == &noop_qdisc)
|
|
return -ENODEV;
|
|
|
|
if (!dev->header_ops || !dst)
|
|
return 0;
|
|
|
|
rcu_read_lock();
|
|
res = __teql_resolve(skb, skb_res, dev, txq, dst);
|
|
rcu_read_unlock();
|
|
|
|
return res;
|
|
}
|
|
|
|
static netdev_tx_t teql_master_xmit(struct sk_buff *skb, struct net_device *dev)
|
|
{
|
|
struct teql_master *master = netdev_priv(dev);
|
|
struct Qdisc *start, *q;
|
|
int busy;
|
|
int nores;
|
|
int subq = skb_get_queue_mapping(skb);
|
|
struct sk_buff *skb_res = NULL;
|
|
|
|
start = master->slaves;
|
|
|
|
restart:
|
|
nores = 0;
|
|
busy = 0;
|
|
|
|
q = start;
|
|
if (!q)
|
|
goto drop;
|
|
|
|
do {
|
|
struct net_device *slave = qdisc_dev(q);
|
|
struct netdev_queue *slave_txq = netdev_get_tx_queue(slave, 0);
|
|
|
|
if (rcu_access_pointer(slave_txq->qdisc_sleeping) != q)
|
|
continue;
|
|
if (netif_xmit_stopped(netdev_get_tx_queue(slave, subq)) ||
|
|
!netif_running(slave)) {
|
|
busy = 1;
|
|
continue;
|
|
}
|
|
|
|
switch (teql_resolve(skb, skb_res, slave, slave_txq)) {
|
|
case 0:
|
|
if (__netif_tx_trylock(slave_txq)) {
|
|
unsigned int length = qdisc_pkt_len(skb);
|
|
|
|
skb->dev = slave;
|
|
if (!netif_xmit_frozen_or_stopped(slave_txq) &&
|
|
netdev_start_xmit(skb, slave, slave_txq, false) ==
|
|
NETDEV_TX_OK) {
|
|
__netif_tx_unlock(slave_txq);
|
|
master->slaves = NEXT_SLAVE(q);
|
|
netif_wake_queue(dev);
|
|
master->tx_packets++;
|
|
master->tx_bytes += length;
|
|
return NETDEV_TX_OK;
|
|
}
|
|
__netif_tx_unlock(slave_txq);
|
|
}
|
|
if (netif_xmit_stopped(netdev_get_tx_queue(dev, 0)))
|
|
busy = 1;
|
|
break;
|
|
case 1:
|
|
master->slaves = NEXT_SLAVE(q);
|
|
return NETDEV_TX_OK;
|
|
default:
|
|
nores = 1;
|
|
break;
|
|
}
|
|
__skb_pull(skb, skb_network_offset(skb));
|
|
} while ((q = NEXT_SLAVE(q)) != start);
|
|
|
|
if (nores && skb_res == NULL) {
|
|
skb_res = skb;
|
|
goto restart;
|
|
}
|
|
|
|
if (busy) {
|
|
netif_stop_queue(dev);
|
|
return NETDEV_TX_BUSY;
|
|
}
|
|
master->tx_errors++;
|
|
|
|
drop:
|
|
master->tx_dropped++;
|
|
dev_kfree_skb(skb);
|
|
return NETDEV_TX_OK;
|
|
}
|
|
|
|
static int teql_master_open(struct net_device *dev)
|
|
{
|
|
struct Qdisc *q;
|
|
struct teql_master *m = netdev_priv(dev);
|
|
int mtu = 0xFFFE;
|
|
unsigned int flags = IFF_NOARP | IFF_MULTICAST;
|
|
|
|
if (m->slaves == NULL)
|
|
return -EUNATCH;
|
|
|
|
flags = FMASK;
|
|
|
|
q = m->slaves;
|
|
do {
|
|
struct net_device *slave = qdisc_dev(q);
|
|
|
|
if (slave == NULL)
|
|
return -EUNATCH;
|
|
|
|
if (slave->mtu < mtu)
|
|
mtu = slave->mtu;
|
|
if (slave->hard_header_len > LL_MAX_HEADER)
|
|
return -EINVAL;
|
|
|
|
/* If all the slaves are BROADCAST, master is BROADCAST
|
|
If all the slaves are PtP, master is PtP
|
|
Otherwise, master is NBMA.
|
|
*/
|
|
if (!(slave->flags&IFF_POINTOPOINT))
|
|
flags &= ~IFF_POINTOPOINT;
|
|
if (!(slave->flags&IFF_BROADCAST))
|
|
flags &= ~IFF_BROADCAST;
|
|
if (!(slave->flags&IFF_MULTICAST))
|
|
flags &= ~IFF_MULTICAST;
|
|
} while ((q = NEXT_SLAVE(q)) != m->slaves);
|
|
|
|
m->dev->mtu = mtu;
|
|
m->dev->flags = (m->dev->flags&~FMASK) | flags;
|
|
netif_start_queue(m->dev);
|
|
return 0;
|
|
}
|
|
|
|
static int teql_master_close(struct net_device *dev)
|
|
{
|
|
netif_stop_queue(dev);
|
|
return 0;
|
|
}
|
|
|
|
static void teql_master_stats64(struct net_device *dev,
|
|
struct rtnl_link_stats64 *stats)
|
|
{
|
|
struct teql_master *m = netdev_priv(dev);
|
|
|
|
stats->tx_packets = m->tx_packets;
|
|
stats->tx_bytes = m->tx_bytes;
|
|
stats->tx_errors = m->tx_errors;
|
|
stats->tx_dropped = m->tx_dropped;
|
|
}
|
|
|
|
static int teql_master_mtu(struct net_device *dev, int new_mtu)
|
|
{
|
|
struct teql_master *m = netdev_priv(dev);
|
|
struct Qdisc *q;
|
|
|
|
q = m->slaves;
|
|
if (q) {
|
|
do {
|
|
if (new_mtu > qdisc_dev(q)->mtu)
|
|
return -EINVAL;
|
|
} while ((q = NEXT_SLAVE(q)) != m->slaves);
|
|
}
|
|
|
|
WRITE_ONCE(dev->mtu, new_mtu);
|
|
return 0;
|
|
}
|
|
|
|
static const struct net_device_ops teql_netdev_ops = {
|
|
.ndo_open = teql_master_open,
|
|
.ndo_stop = teql_master_close,
|
|
.ndo_start_xmit = teql_master_xmit,
|
|
.ndo_get_stats64 = teql_master_stats64,
|
|
.ndo_change_mtu = teql_master_mtu,
|
|
};
|
|
|
|
static __init void teql_master_setup(struct net_device *dev)
|
|
{
|
|
struct teql_master *master = netdev_priv(dev);
|
|
struct Qdisc_ops *ops = &master->qops;
|
|
|
|
master->dev = dev;
|
|
ops->priv_size = sizeof(struct teql_sched_data);
|
|
|
|
ops->enqueue = teql_enqueue;
|
|
ops->dequeue = teql_dequeue;
|
|
ops->peek = teql_peek;
|
|
ops->init = teql_qdisc_init;
|
|
ops->reset = teql_reset;
|
|
ops->destroy = teql_destroy;
|
|
ops->owner = THIS_MODULE;
|
|
|
|
dev->netdev_ops = &teql_netdev_ops;
|
|
dev->type = ARPHRD_VOID;
|
|
dev->mtu = 1500;
|
|
dev->min_mtu = 68;
|
|
dev->max_mtu = 65535;
|
|
dev->tx_queue_len = 100;
|
|
dev->flags = IFF_NOARP;
|
|
dev->hard_header_len = LL_MAX_HEADER;
|
|
netif_keep_dst(dev);
|
|
}
|
|
|
|
static LIST_HEAD(master_dev_list);
|
|
static int max_equalizers = 1;
|
|
module_param(max_equalizers, int, 0);
|
|
MODULE_PARM_DESC(max_equalizers, "Max number of link equalizers");
|
|
|
|
static int __init teql_init(void)
|
|
{
|
|
int i;
|
|
int err = -ENODEV;
|
|
|
|
for (i = 0; i < max_equalizers; i++) {
|
|
struct net_device *dev;
|
|
struct teql_master *master;
|
|
|
|
dev = alloc_netdev(sizeof(struct teql_master), "teql%d",
|
|
NET_NAME_UNKNOWN, teql_master_setup);
|
|
if (!dev) {
|
|
err = -ENOMEM;
|
|
break;
|
|
}
|
|
|
|
if ((err = register_netdev(dev))) {
|
|
free_netdev(dev);
|
|
break;
|
|
}
|
|
|
|
master = netdev_priv(dev);
|
|
|
|
strscpy(master->qops.id, dev->name, IFNAMSIZ);
|
|
err = register_qdisc(&master->qops);
|
|
|
|
if (err) {
|
|
unregister_netdev(dev);
|
|
free_netdev(dev);
|
|
break;
|
|
}
|
|
|
|
list_add_tail(&master->master_list, &master_dev_list);
|
|
}
|
|
return i ? 0 : err;
|
|
}
|
|
|
|
static void __exit teql_exit(void)
|
|
{
|
|
struct teql_master *master, *nxt;
|
|
|
|
list_for_each_entry_safe(master, nxt, &master_dev_list, master_list) {
|
|
|
|
list_del(&master->master_list);
|
|
|
|
unregister_qdisc(&master->qops);
|
|
unregister_netdev(master->dev);
|
|
free_netdev(master->dev);
|
|
}
|
|
}
|
|
|
|
module_init(teql_init);
|
|
module_exit(teql_exit);
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_DESCRIPTION("True (or trivial) link equalizer qdisc");
|