diff options
-rw-r--r-- | include/linux/nl80211.h | 34 | ||||
-rw-r--r-- | include/net/cfg80211.h | 44 | ||||
-rw-r--r-- | net/wireless/core.c | 3 | ||||
-rw-r--r-- | net/wireless/nl80211.c | 289 |
4 files changed, 370 insertions, 0 deletions
diff --git a/include/linux/nl80211.h b/include/linux/nl80211.h index 538ee1dd3d0a..8dc807d9c29a 100644 --- a/include/linux/nl80211.h +++ b/include/linux/nl80211.h | |||
@@ -37,6 +37,16 @@ | |||
37 | * userspace to request deletion of a virtual interface, then requires | 37 | * userspace to request deletion of a virtual interface, then requires |
38 | * attribute %NL80211_ATTR_IFINDEX. | 38 | * attribute %NL80211_ATTR_IFINDEX. |
39 | * | 39 | * |
40 | * @NL80211_CMD_GET_KEY: Get sequence counter information for a key specified | ||
41 | * by %NL80211_ATTR_KEY_IDX and/or %NL80211_ATTR_MAC. | ||
42 | * @NL80211_CMD_SET_KEY: Set key attributes %NL80211_ATTR_KEY_DEFAULT or | ||
43 | * %NL80211_ATTR_KEY_THRESHOLD. | ||
44 | * @NL80211_CMD_NEW_KEY: add a key with given %NL80211_ATTR_KEY_DATA, | ||
45 | * %NL80211_ATTR_KEY_IDX, %NL80211_ATTR_MAC and %NL80211_ATTR_KEY_CIPHER | ||
46 | * attributes. | ||
47 | * @NL80211_CMD_DEL_KEY: delete a key identified by %NL80211_ATTR_KEY_IDX | ||
48 | * or %NL80211_ATTR_MAC. | ||
49 | * | ||
40 | * @NL80211_CMD_MAX: highest used command number | 50 | * @NL80211_CMD_MAX: highest used command number |
41 | * @__NL80211_CMD_AFTER_LAST: internal use | 51 | * @__NL80211_CMD_AFTER_LAST: internal use |
42 | */ | 52 | */ |
@@ -54,6 +64,11 @@ enum nl80211_commands { | |||
54 | NL80211_CMD_NEW_INTERFACE, | 64 | NL80211_CMD_NEW_INTERFACE, |
55 | NL80211_CMD_DEL_INTERFACE, | 65 | NL80211_CMD_DEL_INTERFACE, |
56 | 66 | ||
67 | NL80211_CMD_GET_KEY, | ||
68 | NL80211_CMD_SET_KEY, | ||
69 | NL80211_CMD_NEW_KEY, | ||
70 | NL80211_CMD_DEL_KEY, | ||
71 | |||
57 | /* add commands here */ | 72 | /* add commands here */ |
58 | 73 | ||
59 | /* used to define NL80211_CMD_MAX below */ | 74 | /* used to define NL80211_CMD_MAX below */ |
@@ -75,6 +90,17 @@ enum nl80211_commands { | |||
75 | * @NL80211_ATTR_IFNAME: network interface name | 90 | * @NL80211_ATTR_IFNAME: network interface name |
76 | * @NL80211_ATTR_IFTYPE: type of virtual interface, see &enum nl80211_iftype | 91 | * @NL80211_ATTR_IFTYPE: type of virtual interface, see &enum nl80211_iftype |
77 | * | 92 | * |
93 | * @NL80211_ATTR_MAC: MAC address (various uses) | ||
94 | * | ||
95 | * @NL80211_ATTR_KEY_DATA: (temporal) key data; for TKIP this consists of | ||
96 | * 16 bytes encryption key followed by 8 bytes each for TX and RX MIC | ||
97 | * keys | ||
98 | * @NL80211_ATTR_KEY_IDX: key ID (u8, 0-3) | ||
99 | * @NL80211_ATTR_KEY_CIPHER: key cipher suite (u32, as defined by IEEE 802.11 | ||
100 | * section 7.3.2.25.1, e.g. 0x000FAC04) | ||
101 | * @NL80211_ATTR_KEY_SEQ: transmit key sequence number (IV/PN) for TKIP and | ||
102 | * CCMP keys, each six bytes in little endian | ||
103 | * | ||
78 | * @NL80211_ATTR_MAX: highest attribute number currently defined | 104 | * @NL80211_ATTR_MAX: highest attribute number currently defined |
79 | * @__NL80211_ATTR_AFTER_LAST: internal use | 105 | * @__NL80211_ATTR_AFTER_LAST: internal use |
80 | */ | 106 | */ |
@@ -89,6 +115,14 @@ enum nl80211_attrs { | |||
89 | NL80211_ATTR_IFNAME, | 115 | NL80211_ATTR_IFNAME, |
90 | NL80211_ATTR_IFTYPE, | 116 | NL80211_ATTR_IFTYPE, |
91 | 117 | ||
118 | NL80211_ATTR_MAC, | ||
119 | |||
120 | NL80211_ATTR_KEY_DATA, | ||
121 | NL80211_ATTR_KEY_IDX, | ||
122 | NL80211_ATTR_KEY_CIPHER, | ||
123 | NL80211_ATTR_KEY_SEQ, | ||
124 | NL80211_ATTR_KEY_DEFAULT, | ||
125 | |||
92 | /* add attributes here, update the policy in nl80211.c */ | 126 | /* add attributes here, update the policy in nl80211.c */ |
93 | 127 | ||
94 | __NL80211_ATTR_AFTER_LAST, | 128 | __NL80211_ATTR_AFTER_LAST, |
diff --git a/include/net/cfg80211.h b/include/net/cfg80211.h index d30960e1755c..3db7dfa53b6f 100644 --- a/include/net/cfg80211.h +++ b/include/net/cfg80211.h | |||
@@ -49,6 +49,26 @@ extern int ieee80211_radiotap_iterator_next( | |||
49 | struct ieee80211_radiotap_iterator *iterator); | 49 | struct ieee80211_radiotap_iterator *iterator); |
50 | 50 | ||
51 | 51 | ||
52 | /** | ||
53 | * struct key_params - key information | ||
54 | * | ||
55 | * Information about a key | ||
56 | * | ||
57 | * @key: key material | ||
58 | * @key_len: length of key material | ||
59 | * @cipher: cipher suite selector | ||
60 | * @seq: sequence counter (IV/PN) for TKIP and CCMP keys, only used | ||
61 | * with the get_key() callback, must be in little endian, | ||
62 | * length given by @seq_len. | ||
63 | */ | ||
64 | struct key_params { | ||
65 | u8 *key; | ||
66 | u8 *seq; | ||
67 | int key_len; | ||
68 | int seq_len; | ||
69 | u32 cipher; | ||
70 | }; | ||
71 | |||
52 | /* from net/wireless.h */ | 72 | /* from net/wireless.h */ |
53 | struct wiphy; | 73 | struct wiphy; |
54 | 74 | ||
@@ -71,6 +91,18 @@ struct wiphy; | |||
71 | * | 91 | * |
72 | * @change_virtual_intf: change type of virtual interface | 92 | * @change_virtual_intf: change type of virtual interface |
73 | * | 93 | * |
94 | * @add_key: add a key with the given parameters. @mac_addr will be %NULL | ||
95 | * when adding a group key. | ||
96 | * | ||
97 | * @get_key: get information about the key with the given parameters. | ||
98 | * @mac_addr will be %NULL when requesting information for a group | ||
99 | * key. All pointers given to the @callback function need not be valid | ||
100 | * after it returns. | ||
101 | * | ||
102 | * @del_key: remove a key given the @mac_addr (%NULL for a group key) | ||
103 | * and @key_index | ||
104 | * | ||
105 | * @set_default_key: set the default key on an interface | ||
74 | */ | 106 | */ |
75 | struct cfg80211_ops { | 107 | struct cfg80211_ops { |
76 | int (*add_virtual_intf)(struct wiphy *wiphy, char *name, | 108 | int (*add_virtual_intf)(struct wiphy *wiphy, char *name, |
@@ -78,6 +110,18 @@ struct cfg80211_ops { | |||
78 | int (*del_virtual_intf)(struct wiphy *wiphy, int ifindex); | 110 | int (*del_virtual_intf)(struct wiphy *wiphy, int ifindex); |
79 | int (*change_virtual_intf)(struct wiphy *wiphy, int ifindex, | 111 | int (*change_virtual_intf)(struct wiphy *wiphy, int ifindex, |
80 | enum nl80211_iftype type); | 112 | enum nl80211_iftype type); |
113 | |||
114 | int (*add_key)(struct wiphy *wiphy, struct net_device *netdev, | ||
115 | u8 key_index, u8 *mac_addr, | ||
116 | struct key_params *params); | ||
117 | int (*get_key)(struct wiphy *wiphy, struct net_device *netdev, | ||
118 | u8 key_index, u8 *mac_addr, void *cookie, | ||
119 | void (*callback)(void *cookie, struct key_params*)); | ||
120 | int (*del_key)(struct wiphy *wiphy, struct net_device *netdev, | ||
121 | u8 key_index, u8 *mac_addr); | ||
122 | int (*set_default_key)(struct wiphy *wiphy, | ||
123 | struct net_device *netdev, | ||
124 | u8 key_index); | ||
81 | }; | 125 | }; |
82 | 126 | ||
83 | #endif /* __NET_CFG80211_H */ | 127 | #endif /* __NET_CFG80211_H */ |
diff --git a/net/wireless/core.c b/net/wireless/core.c index febc33bc9c09..cfc5fc5f9e75 100644 --- a/net/wireless/core.c +++ b/net/wireless/core.c | |||
@@ -184,6 +184,9 @@ struct wiphy *wiphy_new(struct cfg80211_ops *ops, int sizeof_priv) | |||
184 | struct cfg80211_registered_device *drv; | 184 | struct cfg80211_registered_device *drv; |
185 | int alloc_size; | 185 | int alloc_size; |
186 | 186 | ||
187 | WARN_ON(!ops->add_key && ops->del_key); | ||
188 | WARN_ON(ops->add_key && !ops->del_key); | ||
189 | |||
187 | alloc_size = sizeof(*drv) + sizeof_priv; | 190 | alloc_size = sizeof(*drv) + sizeof_priv; |
188 | 191 | ||
189 | drv = kzalloc(alloc_size, GFP_KERNEL); | 192 | drv = kzalloc(alloc_size, GFP_KERNEL); |
diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c index 48b0d453e4e1..090936388528 100644 --- a/net/wireless/nl80211.c +++ b/net/wireless/nl80211.c | |||
@@ -61,6 +61,14 @@ static struct nla_policy nl80211_policy[NL80211_ATTR_MAX+1] __read_mostly = { | |||
61 | [NL80211_ATTR_IFTYPE] = { .type = NLA_U32 }, | 61 | [NL80211_ATTR_IFTYPE] = { .type = NLA_U32 }, |
62 | [NL80211_ATTR_IFINDEX] = { .type = NLA_U32 }, | 62 | [NL80211_ATTR_IFINDEX] = { .type = NLA_U32 }, |
63 | [NL80211_ATTR_IFNAME] = { .type = NLA_NUL_STRING, .len = IFNAMSIZ-1 }, | 63 | [NL80211_ATTR_IFNAME] = { .type = NLA_NUL_STRING, .len = IFNAMSIZ-1 }, |
64 | |||
65 | [NL80211_ATTR_MAC] = { .type = NLA_BINARY, .len = ETH_ALEN }, | ||
66 | |||
67 | [NL80211_ATTR_KEY_DATA] = { .type = NLA_BINARY, | ||
68 | .len = WLAN_MAX_KEY_LEN }, | ||
69 | [NL80211_ATTR_KEY_IDX] = { .type = NLA_U8 }, | ||
70 | [NL80211_ATTR_KEY_CIPHER] = { .type = NLA_U32 }, | ||
71 | [NL80211_ATTR_KEY_DEFAULT] = { .type = NLA_FLAG }, | ||
64 | }; | 72 | }; |
65 | 73 | ||
66 | /* message building helper */ | 74 | /* message building helper */ |
@@ -335,6 +343,263 @@ static int nl80211_del_interface(struct sk_buff *skb, struct genl_info *info) | |||
335 | return err; | 343 | return err; |
336 | } | 344 | } |
337 | 345 | ||
346 | struct get_key_cookie { | ||
347 | struct sk_buff *msg; | ||
348 | int error; | ||
349 | }; | ||
350 | |||
351 | static void get_key_callback(void *c, struct key_params *params) | ||
352 | { | ||
353 | struct get_key_cookie *cookie = c; | ||
354 | |||
355 | if (params->key) | ||
356 | NLA_PUT(cookie->msg, NL80211_ATTR_KEY_DATA, | ||
357 | params->key_len, params->key); | ||
358 | |||
359 | if (params->seq) | ||
360 | NLA_PUT(cookie->msg, NL80211_ATTR_KEY_SEQ, | ||
361 | params->seq_len, params->seq); | ||
362 | |||
363 | if (params->cipher) | ||
364 | NLA_PUT_U32(cookie->msg, NL80211_ATTR_KEY_CIPHER, | ||
365 | params->cipher); | ||
366 | |||
367 | return; | ||
368 | nla_put_failure: | ||
369 | cookie->error = 1; | ||
370 | } | ||
371 | |||
372 | static int nl80211_get_key(struct sk_buff *skb, struct genl_info *info) | ||
373 | { | ||
374 | struct cfg80211_registered_device *drv; | ||
375 | int err; | ||
376 | struct net_device *dev; | ||
377 | u8 key_idx = 0; | ||
378 | u8 *mac_addr = NULL; | ||
379 | struct get_key_cookie cookie = { | ||
380 | .error = 0, | ||
381 | }; | ||
382 | void *hdr; | ||
383 | struct sk_buff *msg; | ||
384 | |||
385 | if (info->attrs[NL80211_ATTR_KEY_IDX]) | ||
386 | key_idx = nla_get_u8(info->attrs[NL80211_ATTR_KEY_IDX]); | ||
387 | |||
388 | if (key_idx > 3) | ||
389 | return -EINVAL; | ||
390 | |||
391 | if (info->attrs[NL80211_ATTR_MAC]) | ||
392 | mac_addr = nla_data(info->attrs[NL80211_ATTR_MAC]); | ||
393 | |||
394 | err = get_drv_dev_by_info_ifindex(info, &drv, &dev); | ||
395 | if (err) | ||
396 | return err; | ||
397 | |||
398 | if (!drv->ops->get_key) { | ||
399 | err = -EOPNOTSUPP; | ||
400 | goto out; | ||
401 | } | ||
402 | |||
403 | msg = nlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL); | ||
404 | if (!msg) { | ||
405 | err = -ENOMEM; | ||
406 | goto out; | ||
407 | } | ||
408 | |||
409 | hdr = nl80211hdr_put(msg, info->snd_pid, info->snd_seq, 0, | ||
410 | NL80211_CMD_NEW_KEY); | ||
411 | |||
412 | if (IS_ERR(hdr)) { | ||
413 | err = PTR_ERR(hdr); | ||
414 | goto out; | ||
415 | } | ||
416 | |||
417 | cookie.msg = msg; | ||
418 | |||
419 | NLA_PUT_U32(msg, NL80211_ATTR_IFINDEX, dev->ifindex); | ||
420 | NLA_PUT_U8(msg, NL80211_ATTR_KEY_IDX, key_idx); | ||
421 | if (mac_addr) | ||
422 | NLA_PUT(msg, NL80211_ATTR_MAC, ETH_ALEN, mac_addr); | ||
423 | |||
424 | rtnl_lock(); | ||
425 | err = drv->ops->get_key(&drv->wiphy, dev, key_idx, mac_addr, | ||
426 | &cookie, get_key_callback); | ||
427 | rtnl_unlock(); | ||
428 | |||
429 | if (err) | ||
430 | goto out; | ||
431 | |||
432 | if (cookie.error) | ||
433 | goto nla_put_failure; | ||
434 | |||
435 | genlmsg_end(msg, hdr); | ||
436 | err = genlmsg_unicast(msg, info->snd_pid); | ||
437 | goto out; | ||
438 | |||
439 | nla_put_failure: | ||
440 | err = -ENOBUFS; | ||
441 | nlmsg_free(msg); | ||
442 | out: | ||
443 | cfg80211_put_dev(drv); | ||
444 | dev_put(dev); | ||
445 | return err; | ||
446 | } | ||
447 | |||
448 | static int nl80211_set_key(struct sk_buff *skb, struct genl_info *info) | ||
449 | { | ||
450 | struct cfg80211_registered_device *drv; | ||
451 | int err; | ||
452 | struct net_device *dev; | ||
453 | u8 key_idx; | ||
454 | |||
455 | if (!info->attrs[NL80211_ATTR_KEY_IDX]) | ||
456 | return -EINVAL; | ||
457 | |||
458 | key_idx = nla_get_u8(info->attrs[NL80211_ATTR_KEY_IDX]); | ||
459 | |||
460 | if (key_idx > 3) | ||
461 | return -EINVAL; | ||
462 | |||
463 | /* currently only support setting default key */ | ||
464 | if (!info->attrs[NL80211_ATTR_KEY_DEFAULT]) | ||
465 | return -EINVAL; | ||
466 | |||
467 | err = get_drv_dev_by_info_ifindex(info, &drv, &dev); | ||
468 | if (err) | ||
469 | return err; | ||
470 | |||
471 | if (!drv->ops->set_default_key) { | ||
472 | err = -EOPNOTSUPP; | ||
473 | goto out; | ||
474 | } | ||
475 | |||
476 | rtnl_lock(); | ||
477 | err = drv->ops->set_default_key(&drv->wiphy, dev, key_idx); | ||
478 | rtnl_unlock(); | ||
479 | |||
480 | out: | ||
481 | cfg80211_put_dev(drv); | ||
482 | dev_put(dev); | ||
483 | return err; | ||
484 | } | ||
485 | |||
486 | static int nl80211_new_key(struct sk_buff *skb, struct genl_info *info) | ||
487 | { | ||
488 | struct cfg80211_registered_device *drv; | ||
489 | int err; | ||
490 | struct net_device *dev; | ||
491 | struct key_params params; | ||
492 | u8 key_idx = 0; | ||
493 | u8 *mac_addr = NULL; | ||
494 | |||
495 | memset(¶ms, 0, sizeof(params)); | ||
496 | |||
497 | if (!info->attrs[NL80211_ATTR_KEY_CIPHER]) | ||
498 | return -EINVAL; | ||
499 | |||
500 | if (info->attrs[NL80211_ATTR_KEY_DATA]) { | ||
501 | params.key = nla_data(info->attrs[NL80211_ATTR_KEY_DATA]); | ||
502 | params.key_len = nla_len(info->attrs[NL80211_ATTR_KEY_DATA]); | ||
503 | } | ||
504 | |||
505 | if (info->attrs[NL80211_ATTR_KEY_IDX]) | ||
506 | key_idx = nla_get_u8(info->attrs[NL80211_ATTR_KEY_IDX]); | ||
507 | |||
508 | params.cipher = nla_get_u32(info->attrs[NL80211_ATTR_KEY_CIPHER]); | ||
509 | |||
510 | if (info->attrs[NL80211_ATTR_MAC]) | ||
511 | mac_addr = nla_data(info->attrs[NL80211_ATTR_MAC]); | ||
512 | |||
513 | if (key_idx > 3) | ||
514 | return -EINVAL; | ||
515 | |||
516 | /* | ||
517 | * Disallow pairwise keys with non-zero index unless it's WEP | ||
518 | * (because current deployments use pairwise WEP keys with | ||
519 | * non-zero indizes but 802.11i clearly specifies to use zero) | ||
520 | */ | ||
521 | if (mac_addr && key_idx && | ||
522 | params.cipher != WLAN_CIPHER_SUITE_WEP40 && | ||
523 | params.cipher != WLAN_CIPHER_SUITE_WEP104) | ||
524 | return -EINVAL; | ||
525 | |||
526 | /* TODO: add definitions for the lengths to linux/ieee80211.h */ | ||
527 | switch (params.cipher) { | ||
528 | case WLAN_CIPHER_SUITE_WEP40: | ||
529 | if (params.key_len != 5) | ||
530 | return -EINVAL; | ||
531 | break; | ||
532 | case WLAN_CIPHER_SUITE_TKIP: | ||
533 | if (params.key_len != 32) | ||
534 | return -EINVAL; | ||
535 | break; | ||
536 | case WLAN_CIPHER_SUITE_CCMP: | ||
537 | if (params.key_len != 16) | ||
538 | return -EINVAL; | ||
539 | break; | ||
540 | case WLAN_CIPHER_SUITE_WEP104: | ||
541 | if (params.key_len != 13) | ||
542 | return -EINVAL; | ||
543 | break; | ||
544 | default: | ||
545 | return -EINVAL; | ||
546 | } | ||
547 | |||
548 | err = get_drv_dev_by_info_ifindex(info, &drv, &dev); | ||
549 | if (err) | ||
550 | return err; | ||
551 | |||
552 | if (!drv->ops->add_key) { | ||
553 | err = -EOPNOTSUPP; | ||
554 | goto out; | ||
555 | } | ||
556 | |||
557 | rtnl_lock(); | ||
558 | err = drv->ops->add_key(&drv->wiphy, dev, key_idx, mac_addr, ¶ms); | ||
559 | rtnl_unlock(); | ||
560 | |||
561 | out: | ||
562 | cfg80211_put_dev(drv); | ||
563 | dev_put(dev); | ||
564 | return err; | ||
565 | } | ||
566 | |||
567 | static int nl80211_del_key(struct sk_buff *skb, struct genl_info *info) | ||
568 | { | ||
569 | struct cfg80211_registered_device *drv; | ||
570 | int err; | ||
571 | struct net_device *dev; | ||
572 | u8 key_idx = 0; | ||
573 | u8 *mac_addr = NULL; | ||
574 | |||
575 | if (info->attrs[NL80211_ATTR_KEY_IDX]) | ||
576 | key_idx = nla_get_u8(info->attrs[NL80211_ATTR_KEY_IDX]); | ||
577 | |||
578 | if (key_idx > 3) | ||
579 | return -EINVAL; | ||
580 | |||
581 | if (info->attrs[NL80211_ATTR_MAC]) | ||
582 | mac_addr = nla_data(info->attrs[NL80211_ATTR_MAC]); | ||
583 | |||
584 | err = get_drv_dev_by_info_ifindex(info, &drv, &dev); | ||
585 | if (err) | ||
586 | return err; | ||
587 | |||
588 | if (!drv->ops->del_key) { | ||
589 | err = -EOPNOTSUPP; | ||
590 | goto out; | ||
591 | } | ||
592 | |||
593 | rtnl_lock(); | ||
594 | err = drv->ops->del_key(&drv->wiphy, dev, key_idx, mac_addr); | ||
595 | rtnl_unlock(); | ||
596 | |||
597 | out: | ||
598 | cfg80211_put_dev(drv); | ||
599 | dev_put(dev); | ||
600 | return err; | ||
601 | } | ||
602 | |||
338 | static struct genl_ops nl80211_ops[] = { | 603 | static struct genl_ops nl80211_ops[] = { |
339 | { | 604 | { |
340 | .cmd = NL80211_CMD_GET_WIPHY, | 605 | .cmd = NL80211_CMD_GET_WIPHY, |
@@ -374,6 +639,30 @@ static struct genl_ops nl80211_ops[] = { | |||
374 | .policy = nl80211_policy, | 639 | .policy = nl80211_policy, |
375 | .flags = GENL_ADMIN_PERM, | 640 | .flags = GENL_ADMIN_PERM, |
376 | }, | 641 | }, |
642 | { | ||
643 | .cmd = NL80211_CMD_GET_KEY, | ||
644 | .doit = nl80211_get_key, | ||
645 | .policy = nl80211_policy, | ||
646 | .flags = GENL_ADMIN_PERM, | ||
647 | }, | ||
648 | { | ||
649 | .cmd = NL80211_CMD_SET_KEY, | ||
650 | .doit = nl80211_set_key, | ||
651 | .policy = nl80211_policy, | ||
652 | .flags = GENL_ADMIN_PERM, | ||
653 | }, | ||
654 | { | ||
655 | .cmd = NL80211_CMD_NEW_KEY, | ||
656 | .doit = nl80211_new_key, | ||
657 | .policy = nl80211_policy, | ||
658 | .flags = GENL_ADMIN_PERM, | ||
659 | }, | ||
660 | { | ||
661 | .cmd = NL80211_CMD_DEL_KEY, | ||
662 | .doit = nl80211_del_key, | ||
663 | .policy = nl80211_policy, | ||
664 | .flags = GENL_ADMIN_PERM, | ||
665 | }, | ||
377 | }; | 666 | }; |
378 | 667 | ||
379 | /* multicast groups */ | 668 | /* multicast groups */ |