@@ -12,6 +12,7 @@ ovpn-y += crypto.o
ovpn-y += crypto_aead.o
ovpn-y += main.o
ovpn-y += io.o
+ovpn-y += mcast.o
ovpn-y += netlink.o
ovpn-y += netlink-gen.o
ovpn-y += peer.o
@@ -17,6 +17,7 @@
#include "ovpnpriv.h"
#include "peer.h"
#include "io.h"
+#include "mcast.h"
#include "bind.h"
#include "crypto.h"
#include "crypto_aead.h"
@@ -183,8 +184,12 @@ void ovpn_decrypt_post(void *data, int ret)
}
skb->protocol = proto;
- /* perform Reverse Path Filtering (RPF) */
- if (unlikely(!ovpn_peer_check_by_src(peer->ovpn, skb, peer))) {
+ /* perform Reverse Path Filtering (RPF).
+ * snoop IGMP/MLD before RPF: these control protocols may use source
+ * addresses that differ from the peer's VPN address
+ */
+ if (likely(!ovpn_mcast_snoop_skb(peer, skb)) &&
+ unlikely(!ovpn_peer_check_by_src(peer->ovpn, skb, peer))) {
if (skb->protocol == htons(ETH_P_IPV6))
net_dbg_ratelimited("%s: RPF dropped packet from peer %u, src: %pI6c\n",
netdev_name(peer->ovpn->dev),
@@ -19,6 +19,7 @@
#include "ovpnpriv.h"
#include "main.h"
+#include "mcast.h"
#include "netlink.h"
#include "io.h"
#include "peer.h"
@@ -30,6 +31,7 @@ static void ovpn_priv_free(struct net_device *net)
{
struct ovpn_priv *ovpn = netdev_priv(net);
+ ovpn_mcast_cleanup(ovpn);
kfree(ovpn->peers);
}
@@ -190,6 +192,7 @@ static int ovpn_newlink(struct net_device *dev,
ovpn->dev = dev;
ovpn->mode = mode;
spin_lock_init(&ovpn->lock);
+ hash_init(ovpn->mcast_table);
INIT_DELAYED_WORK(&ovpn->keepalive_work, ovpn_peer_keepalive_work);
/* Set carrier explicitly after registration, this way state is
new file mode 100644
@@ -0,0 +1,369 @@
+// SPDX-License-Identifier: GPL-2.0
+/* OpenVPN data channel offload
+ *
+ * Copyright (C) 2020-2026 OpenVPN, Inc.
+ */
+
+#include <linux/igmp.h>
+#include <net/mld.h>
+
+#include "ovpnpriv.h"
+#include "peer.h"
+#include "mcast.h"
+
+struct ovpn_mcast_group {
+ struct hlist_node hash_entry;
+ struct in6_addr addr;
+ struct list_head subs;
+};
+
+struct ovpn_mcast_sub {
+ struct list_head list;
+ struct ovpn_peer *peer;
+};
+
+static inline u32 ovpn_mcast_hash(const struct in6_addr *group_addr)
+{
+ return jhash(group_addr, sizeof(*group_addr), 0);
+}
+
+static bool ovpn_mcast_addr_valid(const struct in6_addr *group_addr)
+{
+ if (ipv6_addr_v4mapped(group_addr))
+ return ipv4_is_multicast(group_addr->s6_addr32[3]);
+ return ipv6_addr_is_multicast(group_addr);
+}
+
+static struct ovpn_mcast_group *
+ovpn_mcast_group_find(const struct ovpn_priv *ovpn, const struct in6_addr *group_addr)
+{
+ struct ovpn_mcast_group *group;
+ u32 hash = ovpn_mcast_hash(group_addr);
+
+ hash_for_each_possible(ovpn->mcast_table, group, hash_entry, hash) {
+ if (ipv6_addr_equal(&group->addr, group_addr))
+ return group;
+ }
+ return NULL;
+}
+
+static struct ovpn_peer *ovpn_mcast_sub_del(struct ovpn_mcast_sub *sub)
+{
+ struct ovpn_peer *peer = sub->peer;
+
+ list_del(&sub->list);
+ kfree(sub);
+ return peer;
+}
+
+static void ovpn_mcast_group_try_del(struct ovpn_mcast_group *group)
+{
+ if (!list_empty(&group->subs))
+ return;
+ hash_del(&group->hash_entry);
+ kfree(group);
+}
+
+/**
+ * ovpn_mcast_cleanup - tear down all multicast state
+ * @ovpn: the ovpn instance
+ *
+ * Walks the multicast hash table and frees every group and subscription.
+ * Called at instance destruction time.
+ */
+void ovpn_mcast_cleanup(struct ovpn_priv *ovpn)
+{
+ struct ovpn_mcast_group *group;
+ struct hlist_node *tmp;
+ struct ovpn_mcast_sub *sub, *next;
+ unsigned int bkt;
+
+ hash_for_each_safe(ovpn->mcast_table, bkt, tmp, group, hash_entry) {
+ list_for_each_entry_safe(sub, next, &group->subs, list)
+ ovpn_peer_put(ovpn_mcast_sub_del(sub));
+ ovpn_mcast_group_try_del(group);
+ }
+}
+
+/**
+ * ovpn_mcast_join - add a peer to a multicast group
+ * @ovpn: the ovpn instance
+ * @peer: the peer joining the group
+ * @group_addr: the multicast group address (IPv4-mapped IPv6 for IPv4 groups)
+ *
+ * Creates the group if it does not exist and adds a subscription for @peer.
+ * If the peer is already subscribed, returns success without doing anything.
+ */
+void ovpn_mcast_join(struct ovpn_priv *ovpn, struct ovpn_peer *peer,
+ const struct in6_addr *group_addr)
+{
+ struct ovpn_mcast_group *group;
+ struct ovpn_mcast_sub *sub;
+
+ if (!ovpn_mcast_addr_valid(group_addr))
+ return;
+
+ spin_lock_bh(&ovpn->lock);
+
+ group = ovpn_mcast_group_find(ovpn, group_addr);
+ if (!group) {
+ group = kzalloc_obj(*group, GFP_ATOMIC);
+ if (unlikely(!group))
+ goto end;
+ group->addr = *group_addr;
+ INIT_LIST_HEAD(&group->subs);
+ hash_add(ovpn->mcast_table, &group->hash_entry,
+ ovpn_mcast_hash(group_addr));
+ }
+
+ list_for_each_entry(sub, &group->subs, list) {
+ if (sub->peer == peer)
+ goto end;
+ }
+
+ sub = kzalloc_obj(*sub, GFP_ATOMIC);
+ if (unlikely(!sub))
+ goto end;
+
+ sub->peer = peer;
+ ovpn_peer_hold(peer);
+ list_add_tail(&sub->list, &group->subs);
+end:
+ spin_unlock_bh(&ovpn->lock);
+}
+
+/**
+ * ovpn_mcast_leave - remove a peer from a multicast group
+ * @ovpn: the ovpn instance
+ * @peer: the peer leaving the group
+ * @group_addr: the multicast group address
+ *
+ * Removes @peer's subscription for @group_addr. If the group has no remaining
+ * subscribers it is destroyed.
+ */
+void ovpn_mcast_leave(struct ovpn_priv *ovpn, struct ovpn_peer *peer,
+ const struct in6_addr *group_addr)
+{
+ struct ovpn_mcast_group *group;
+ struct ovpn_mcast_sub *sub, *next;
+ struct ovpn_peer *peer_to_put = NULL;
+
+ spin_lock_bh(&ovpn->lock);
+
+ group = ovpn_mcast_group_find(ovpn, group_addr);
+ if (!group)
+ goto end;
+
+ list_for_each_entry_safe(sub, next, &group->subs, list) {
+ if (sub->peer != peer)
+ continue;
+ peer_to_put = ovpn_mcast_sub_del(sub);
+ ovpn_mcast_group_try_del(group);
+ goto end;
+ }
+end:
+ spin_unlock_bh(&ovpn->lock);
+
+ if (peer_to_put)
+ ovpn_peer_put(peer_to_put);
+}
+
+/**
+ * ovpn_mcast_leave_all - remove a peer from all multicast groups
+ * @peer: the peer to remove
+ *
+ * Called when a peer disconnects. Removes the peer from every group it
+ * was subscribed to and destroys any groups that become empty.
+ */
+void ovpn_mcast_leave_all(struct ovpn_peer *peer)
+{
+ struct ovpn_priv *ovpn = peer->ovpn;
+ struct ovpn_mcast_group *group;
+ struct hlist_node *tmp;
+ struct ovpn_mcast_sub *sub, *next;
+ unsigned int bkt, nput = 0;
+
+ spin_lock_bh(&ovpn->lock);
+
+ hash_for_each_safe(ovpn->mcast_table, bkt, tmp, group, hash_entry) {
+ list_for_each_entry_safe(sub, next, &group->subs, list) {
+ if (sub->peer != peer)
+ continue;
+ ovpn_mcast_sub_del(sub);
+ nput++;
+ ovpn_mcast_group_try_del(group);
+ break;
+ }
+ }
+
+ spin_unlock_bh(&ovpn->lock);
+
+ while (nput--)
+ ovpn_peer_put(peer);
+}
+
+/**
+ * ovpn_peer_list_get_by_mcast_group - retrieve peers subscribed to a multicast group
+ * @ovpn: the ovpn instance to search
+ * @group_addr: the multicast group address to look up
+ * @list: the lockless list to append matching peers to
+ *
+ * Searches for the multicast group identified by @group_addr and appends all
+ * subscribed peers to @list, acquiring a reference on each one.
+ *
+ * Return: false if no peer was found, true otherwise
+ */
+bool ovpn_peer_list_get_by_mcast_group(struct ovpn_priv *ovpn,
+ const struct in6_addr *group_addr,
+ struct llist_head *list)
+{
+ struct ovpn_mcast_group *group;
+ struct ovpn_mcast_sub *sub;
+
+ spin_lock_bh(&ovpn->lock);
+
+ group = ovpn_mcast_group_find(ovpn, group_addr);
+ if (group) {
+ list_for_each_entry(sub, &group->subs, list) {
+ if (ovpn_peer_hold(sub->peer))
+ llist_add(&sub->peer->mcast_entry, list);
+ }
+ }
+
+ spin_unlock_bh(&ovpn->lock);
+ return !(llist_empty(list));
+}
+
+/**
+ * ovpn_mcast_mld_offset - compute the offset to the MLD payload in an IPv6 packet
+ * @skb: the packet to inspect
+ * @offsetp: pointer to store the computed offset
+ *
+ * MLD packets may be preceded by a Hop-by-Hop options header containing
+ * the Router Alert option. Calculate the actual payload offset and
+ * verify that the next header is ICMPv6.
+ *
+ * Return: true if the offset was computed successfully, false otherwise
+ */
+static bool ovpn_mcast_mld_offset(struct sk_buff *skb, unsigned int *offsetp)
+{
+ unsigned int offset = sizeof(struct ipv6hdr);
+ u8 nexthdr = ipv6_hdr(skb)->nexthdr;
+
+ if (nexthdr == IPPROTO_HOPOPTS) {
+ struct ipv6_opt_hdr *hopopt;
+
+ if (!pskb_may_pull(skb, offset + sizeof(*hopopt)))
+ return false;
+
+ hopopt = (struct ipv6_opt_hdr *)(skb_network_header(skb) + offset);
+ nexthdr = hopopt->nexthdr;
+ offset += ipv6_optlen(hopopt);
+ }
+
+ if (nexthdr != IPPROTO_ICMPV6)
+ return false;
+
+ *offsetp = offset;
+ return true;
+}
+
+/**
+ * ovpn_mcast_snoop_mld - inspect an IPv6 packet for MLD join/leave messages
+ * @peer: the peer this packet was received from
+ * @skb: the packet to inspect
+ *
+ * Parse the MLD header and update the multicast subscription table on
+ * MLDv1 reports and done messages.
+ *
+ * Return: true if the packet was a recognized MLD join/leave and was
+ * consumed, false otherwise
+ */
+static bool ovpn_mcast_snoop_mld(struct ovpn_peer *peer, struct sk_buff *skb)
+{
+ struct mld_msg *mld;
+ unsigned int offset;
+
+ if (!ovpn_mcast_mld_offset(skb, &offset))
+ return false;
+
+ if (!pskb_may_pull(skb, offset + sizeof(*mld)))
+ return false;
+
+ mld = (struct mld_msg *)(skb_network_header(skb) + offset);
+
+ switch (mld->mld_type) {
+ case ICMPV6_MGM_REPORT:
+ ovpn_mcast_join(peer->ovpn, peer, &mld->mld_mca);
+ return true;
+ case ICMPV6_MGM_REDUCTION:
+ ovpn_mcast_leave(peer->ovpn, peer, &mld->mld_mca);
+ return true;
+ case ICMPV6_MGM_QUERY:
+ return true;
+ }
+ return false;
+}
+
+/**
+ * ovpn_mcast_snoop_igmp - inspect an IPv4 packet for IGMP join/leave messages
+ * @peer: the peer this packet was received from
+ * @skb: the packet to inspect
+ *
+ * Parse the IGMP header and update the multicast subscription table on
+ * IGMPv2 membership reports and leave messages.
+ *
+ * Return: true if the packet was a recognized IGMP join/leave and was
+ * consumed, false otherwise
+ */
+static bool ovpn_mcast_snoop_igmp(struct ovpn_peer *peer, struct sk_buff *skb)
+{
+ struct igmphdr *ih;
+ struct in6_addr addr6;
+ unsigned int ihl;
+
+ ihl = ip_hdr(skb)->ihl * 4;
+ if (!pskb_may_pull(skb, ihl + sizeof(struct igmphdr)))
+ return false;
+
+ ih = (struct igmphdr *)(skb_network_header(skb) + ihl);
+
+ switch (ih->type) {
+ case IGMPV2_HOST_MEMBERSHIP_REPORT:
+ ipv6_addr_set_v4mapped(ih->group, &addr6);
+ ovpn_mcast_join(peer->ovpn, peer, &addr6);
+ return true;
+ case IGMP_HOST_LEAVE_MESSAGE:
+ ipv6_addr_set_v4mapped(ih->group, &addr6);
+ ovpn_mcast_leave(peer->ovpn, peer, &addr6);
+ return true;
+ }
+ return true;
+}
+
+/**
+ * ovpn_mcast_snoop_skb - snoop IGMP/MLD control packets from a peer
+ * @peer: the peer this packet was received from
+ * @skb: the packet to inspect
+ *
+ * Check whether @skb contains an IGMP or MLD membership report/leave message.
+ * If so, update the multicast forwarding table and report that the packet was
+ * consumed. Snooping is only performed in multi-peer (server) mode; in P2P
+ * mode the function returns false immediately since there is only one peer.
+ *
+ * Return: true if the packet was a recognized IGMP/MLD join/leave and was
+ * consumed, false otherwise
+ */
+bool ovpn_mcast_snoop_skb(struct ovpn_peer *peer, struct sk_buff *skb)
+{
+ if (peer->ovpn->mode != OVPN_MODE_MP)
+ return false;
+ if (skb->protocol == htons(ETH_P_IP)) {
+ if (ip_hdr(skb)->protocol == IPPROTO_IGMP)
+ return ovpn_mcast_snoop_igmp(peer, skb);
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ if (ipv6_hdr(skb)->nexthdr == IPPROTO_ICMPV6)
+ return ovpn_mcast_snoop_mld(peer, skb);
+ }
+ return false;
+}
new file mode 100644
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* OpenVPN data channel offload
+ *
+ * Copyright (C) 2020-2026 OpenVPN, Inc.
+ */
+
+#ifndef _NET_OVPN_MCAST_H_
+#define _NET_OVPN_MCAST_H_
+
+struct ovpn_priv;
+struct ovpn_peer;
+struct in6_addr;
+struct llist_head;
+struct sk_buff;
+
+void ovpn_mcast_cleanup(struct ovpn_priv *ovpn);
+void ovpn_mcast_join(struct ovpn_priv *ovpn, struct ovpn_peer *peer,
+ const struct in6_addr *group_addr);
+void ovpn_mcast_leave(struct ovpn_priv *ovpn, struct ovpn_peer *peer,
+ const struct in6_addr *group_addr);
+void ovpn_mcast_leave_all(struct ovpn_peer *peer);
+bool ovpn_peer_list_get_by_mcast_group(struct ovpn_priv *ovpn,
+ const struct in6_addr *group_addr,
+ struct llist_head *list);
+bool ovpn_mcast_snoop_skb(struct ovpn_peer *peer, struct sk_buff *skb);
+
+#endif /* _NET_OVPN_MCAST_H_ */
@@ -50,6 +50,7 @@ struct ovpn_priv {
struct ovpn_peer __rcu *peer;
struct gro_cells gro_cells;
struct delayed_work keepalive_work;
+ DECLARE_HASHTABLE(mcast_table, 8);
};
#endif /* _NET_OVPN_OVPNSTRUCT_H_ */
@@ -18,6 +18,7 @@
#include "crypto.h"
#include "io.h"
#include "main.h"
+#include "mcast.h"
#include "netlink.h"
#include "peer.h"
#include "socket.h"
@@ -732,17 +733,6 @@ static void ovpn_peer_list_get_all(struct ovpn_priv *ovpn,
rcu_read_unlock();
}
-/**
- * TO DO: At the moment the list contain all the peers,
- * after IGMP snooping is implemented we want to select only the peers
- * subscribed to a specific multicast group.
- */
-static void ovpn_peer_list_get_by_mcast_group(struct ovpn_priv *ovpn,
- struct llist_head *list)
-{
- ovpn_peer_list_get_all(ovpn, list);
-}
-
/**
* ovpn_peer_list_get_by_dst - Lookup peers to send skb to
* @ovpn: the private data representing the current VPN session
@@ -787,10 +777,13 @@ void ovpn_peer_list_get_by_dst(struct ovpn_priv *ovpn, struct sk_buff *skb,
rcu_read_unlock();
addr_type = inet_dev_addr_type(dev_net(ovpn->dev), ovpn->dev, addr4);
- if (addr_type == RTN_MULTICAST)
- ovpn_peer_list_get_by_mcast_group(ovpn, list);
- else if (addr_type == RTN_BROADCAST)
+ if (addr_type == RTN_MULTICAST) {
+ ipv6_addr_set_v4mapped(addr4, &addr6);
+ if (!ovpn_peer_list_get_by_mcast_group(ovpn, &addr6, list))
+ ovpn_peer_list_get_all(ovpn, list);
+ } else if (addr_type == RTN_BROADCAST) {
ovpn_peer_list_get_all(ovpn, list);
+ }
return;
case htons(ETH_P_IPV6):
addr6 = ovpn_nexthop_from_skb6(skb);
@@ -801,8 +794,10 @@ void ovpn_peer_list_get_by_dst(struct ovpn_priv *ovpn, struct sk_buff *skb,
break;
rcu_read_unlock();
- if (ipv6_addr_is_multicast(&addr6))
- ovpn_peer_list_get_by_mcast_group(ovpn, list);
+ if (ipv6_addr_is_multicast(&addr6) &&
+ !ovpn_peer_list_get_by_mcast_group(ovpn, &addr6, list)) {
+ ovpn_peer_list_get_all(ovpn, list);
+ }
return;
}
@@ -1153,6 +1148,8 @@ int ovpn_peer_del(struct ovpn_peer *peer, enum ovpn_del_peer_reason reason)
LLIST_HEAD(release_list);
int ret = -EOPNOTSUPP;
+ ovpn_mcast_leave_all(peer);
+
spin_lock_bh(&peer->ovpn->lock);
switch (peer->ovpn->mode) {
case OVPN_MODE_MP:
Add multicast snooping for IGMPv2 and MLDv1 control traffic received from tunnel peers. IGMPv2 membership reports/leaves and MLDv1 reports/done messages are parsed to build a per-peer multicast group subscription table. On the TX path, multicast packets are forwarded only to peers subscribed to the destination group instead of flooding all peers. Because IGMP and MLD control packets may use source addresses that do not match the peer's VPN-assigned address (e.g., MLD requires link-local sources per RFC 2710 and RFC 3810), snooping is performed before Reverse Path Filtering. Recognized join/leave messages bypass the RPF check via short-circuit evaluation so they are not dropped. Multicast subscriptions are cleaned up automatically when a peer is deleted or the interface is destroyed. Signed-off-by: Marco Baffo <marco@mandelbit.com> --- drivers/net/ovpn/Makefile | 1 + drivers/net/ovpn/io.c | 9 +- drivers/net/ovpn/main.c | 3 + drivers/net/ovpn/mcast.c | 369 ++++++++++++++++++++++++++++++++++++ drivers/net/ovpn/mcast.h | 27 +++ drivers/net/ovpn/ovpnpriv.h | 1 + drivers/net/ovpn/peer.c | 29 ++- 7 files changed, 421 insertions(+), 18 deletions(-) create mode 100644 drivers/net/ovpn/mcast.c create mode 100644 drivers/net/ovpn/mcast.h