@@ -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: