[Openvpn-devel,RFC,ovpn,net-next,1/5] ovpn: add multicast/braodcast packet transmission support

Message ID 20260514095210.288979-1-marco@mandelbit.com
State New
Headers
Series [Openvpn-devel,RFC,ovpn,net-next,1/5] ovpn: add multicast/braodcast packet transmission support |

Commit Message

Marco Baffo May 14, 2026, 9:52 a.m. UTC
  The ovpn DCO driver currently drops all multicast/broadcast packets
because it does not set IFF_MULTICAST and IFF_BROADCAST on the
netdevice and always performs a unicast peer lookup in ovpn_net_xmit().
This prevents multicast routing daemons such as smcroute from using an
ovpn interface as a multicast VIF and makes it impossible to
forward multicastand broadcast traffic to VPN clients.

Add the minimal infrastructure needed to get multicast/broadcast working:

- Set IFF_MULTICAST and IFF_BROADCAST in ovpn_setup().

- Detect multicast and broadcast destinations in
  ovpn_peer_list_get_by_dst() and create a list with the target peers.

- Introduce ovpn_skb_list_clone() to clone GSO segment lists and
  replicate the packet to every connected peer in
  ovpn_net_xmit().

For now multicast traffic is flooded to all peers. A future
enhancement will replace the flood with a subscription table driven by
IGMP snooping.

Signed-off-by: Marco Baffo <marco@mandelbit.com>
---
 drivers/net/ovpn/io.c   | 57 ++++++++++++++++++++++++++-----
 drivers/net/ovpn/main.c |  2 +-
 drivers/net/ovpn/peer.c | 76 ++++++++++++++++++++++++++++++++---------
 drivers/net/ovpn/peer.h |  5 +--
 4 files changed, 112 insertions(+), 28 deletions(-)
  

Comments

Antonio Quartulli May 14, 2026, 11:27 p.m. UTC | #1
Hi,

On 14/05/2026 11:52, Marco Baffo wrote:
> The ovpn DCO driver currently drops all multicast/broadcast packets
> because it does not set IFF_MULTICAST and IFF_BROADCAST on the
> netdevice and always performs a unicast peer lookup in ovpn_net_xmit().
> This prevents multicast routing daemons such as smcroute from using an
> ovpn interface as a multicast VIF and makes it impossible to
> forward multicastand broadcast traffic to VPN clients.
> 
> Add the minimal infrastructure needed to get multicast/broadcast working:
> 
> - Set IFF_MULTICAST and IFF_BROADCAST in ovpn_setup().
> 
> - Detect multicast and broadcast destinations in
>    ovpn_peer_list_get_by_dst() and create a list with the target peers.
> 
> - Introduce ovpn_skb_list_clone() to clone GSO segment lists and
>    replicate the packet to every connected peer in
>    ovpn_net_xmit().
> 
> For now multicast traffic is flooded to all peers. A future
> enhancement will replace the flood with a subscription table driven by
> IGMP snooping.
> 
> Signed-off-by: Marco Baffo <marco@mandelbit.com>

As discussed on IRC, we will move forward only with the first patch of 
this batch.

The rest is not going to be further developed as we concluded there is 
not much traction for such feature nowadays.

Regards,
  

Patch

diff --git a/drivers/net/ovpn/io.c b/drivers/net/ovpn/io.c
index 22c555dd962e..acf0907dd445 100644
--- a/drivers/net/ovpn/io.c
+++ b/drivers/net/ovpn/io.c
@@ -351,15 +351,39 @@  static void ovpn_send(struct ovpn_priv *ovpn, struct sk_buff *skb,
 	ovpn_peer_put(peer);
 }
 
+static struct sk_buff *ovpn_skb_list_clone(struct sk_buff *skb)
+{
+	struct sk_buff *copy, *curr, *next, *head = NULL, *prev = NULL;
+
+	skb_list_walk_safe(skb, curr, next) {
+		copy = skb_clone(curr, GFP_ATOMIC);
+		if (unlikely(!copy)) {
+			kfree_skb_list(head);
+			return NULL;
+		}
+
+		if (unlikely(!head))
+			head = copy;
+		else
+			prev->next = copy;
+
+		prev = copy;
+	}
+
+	return head;
+}
+
 /* Send user data to the network
  */
 netdev_tx_t ovpn_net_xmit(struct sk_buff *skb, struct net_device *dev)
 {
 	struct ovpn_priv *ovpn = netdev_priv(dev);
-	struct sk_buff *segments, *curr, *next;
+	struct sk_buff *segments, *curr, *next, *to_send;
 	struct sk_buff_head skb_list;
-	unsigned int tx_bytes = 0;
+	struct llist_head mcast_list;
+	struct llist_node *node, *n;
 	struct ovpn_peer *peer;
+	unsigned int tx_bytes = 0;
 	__be16 proto;
 	int ret;
 
@@ -372,8 +396,9 @@  netdev_tx_t ovpn_net_xmit(struct sk_buff *skb, struct net_device *dev)
 		goto drop_no_peer;
 
 	/* retrieve peer serving the destination IP of this packet */
-	peer = ovpn_peer_get_by_dst(ovpn, skb);
-	if (unlikely(!peer)) {
+	init_llist_head(&mcast_list);
+	ovpn_peer_list_get_by_dst(ovpn, skb, &mcast_list);
+	if (unlikely(llist_empty(&mcast_list))) {
 		switch (skb->protocol) {
 		case htons(ETH_P_IP):
 			net_dbg_ratelimited("%s: no peer to send data to dst=%pI4\n",
@@ -427,18 +452,34 @@  netdev_tx_t ovpn_net_xmit(struct sk_buff *skb, struct net_device *dev)
 	 * incremented the counter for each failure in the loop
 	 */
 	if (unlikely(skb_queue_empty(&skb_list))) {
-		ovpn_peer_put(peer);
+		llist_for_each_safe(node, n, mcast_list.first) {
+			peer = llist_entry(node, struct ovpn_peer, mcast_entry);
+			ovpn_peer_put(peer);
+		}
 		return NETDEV_TX_OK;
 	}
 	skb_list.prev->next = NULL;
 
-	ovpn_peer_stats_increment_tx(&peer->vpn_stats, tx_bytes);
-	ovpn_send(ovpn, skb_list.next, peer);
+	llist_for_each_safe(node, n, mcast_list.first) {
+		peer = llist_entry(node, struct ovpn_peer, mcast_entry);
+
+		to_send = n ? ovpn_skb_list_clone(skb_list.next) : skb_list.next;
+		if (likely(to_send)) {
+			ovpn_peer_stats_increment_tx(&peer->vpn_stats, tx_bytes);
+			ovpn_send(ovpn, to_send, peer);
+		} else {
+			dev_dstats_tx_dropped(ovpn->dev);
+			ovpn_peer_put(peer);
+		}
+	}
 
 	return NETDEV_TX_OK;
 
 drop:
-	ovpn_peer_put(peer);
+	llist_for_each_safe(node, n, mcast_list.first) {
+		peer = llist_entry(node, struct ovpn_peer, mcast_entry);
+		ovpn_peer_put(peer);
+	}
 drop_no_peer:
 	dev_dstats_tx_dropped(ovpn->dev);
 	skb_tx_error(skb);
diff --git a/drivers/net/ovpn/main.c b/drivers/net/ovpn/main.c
index 2e0420febda0..ee9cb61a090f 100644
--- a/drivers/net/ovpn/main.c
+++ b/drivers/net/ovpn/main.c
@@ -155,7 +155,7 @@  static void ovpn_setup(struct net_device *dev)
 	dev->max_mtu = IP_MAX_MTU - OVPN_HEAD_ROOM;
 
 	dev->type = ARPHRD_NONE;
-	dev->flags = IFF_POINTOPOINT | IFF_NOARP;
+	dev->flags = IFF_POINTOPOINT | IFF_NOARP | IFF_MULTICAST | IFF_BROADCAST;
 	dev->priv_flags |= IFF_NO_QUEUE;
 	/* when routing packets to a LAN behind a client, we rely on the
 	 * route entry that originally brought the packet into ovpn, so
diff --git a/drivers/net/ovpn/peer.c b/drivers/net/ovpn/peer.c
index c02dfab51a6e..06d47c468956 100644
--- a/drivers/net/ovpn/peer.c
+++ b/drivers/net/ovpn/peer.c
@@ -718,23 +718,49 @@  static void ovpn_peer_remove(struct ovpn_peer *peer,
 	llist_add(&peer->release_entry, release_list);
 }
 
+static void ovpn_peer_list_get_all(struct ovpn_priv *ovpn,
+				   struct llist_head *list)
+{
+	struct ovpn_peer *peer;
+	int bkt;
+
+	rcu_read_lock();
+	hash_for_each_rcu(ovpn->peers->by_id, bkt, peer, hash_entry_id) {
+		if (ovpn_peer_hold(peer))
+			llist_add(&peer->mcast_entry, list);
+	}
+	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_get_by_dst - Lookup peer to send skb to
+ * ovpn_peer_list_get_by_dst - Lookup peers to send skb to
  * @ovpn: the private data representing the current VPN session
  * @skb: the skb to extract the destination address from
+ * @list: the head of the list to fill with the target peers
  *
- * This function takes a tunnel packet and looks up the peer to send it to
- * after encapsulation. The skb is expected to be the in-tunnel packet, without
- * any OpenVPN related header.
+ * This function takes a tunnel packet and looks up the peers to send it to
+ * after encapsulation and add them to `list'. The skb is expected to be the
+ * in-tunnel packet, without any OpenVPN related header.
  *
  * Assume that the IP header is accessible in the skb data.
  *
- * Return: the peer if found or NULL otherwise.
  */
-struct ovpn_peer *ovpn_peer_get_by_dst(struct ovpn_priv *ovpn,
-				       struct sk_buff *skb)
+void ovpn_peer_list_get_by_dst(struct ovpn_priv *ovpn, struct sk_buff *skb,
+			       struct llist_head *list)
 {
 	struct ovpn_peer *peer = NULL;
+	unsigned int addr_type;
 	struct in6_addr addr6;
 	__be32 addr4;
 
@@ -744,29 +770,45 @@  struct ovpn_peer *ovpn_peer_get_by_dst(struct ovpn_priv *ovpn,
 	if (ovpn->mode == OVPN_MODE_P2P) {
 		rcu_read_lock();
 		peer = rcu_dereference(ovpn->peer);
-		if (unlikely(peer && !ovpn_peer_hold(peer)))
-			peer = NULL;
+		if (likely(peer && ovpn_peer_hold(peer)))
+			llist_add(&peer->mcast_entry, list);
 		rcu_read_unlock();
-		return peer;
+		return;
 	}
 
-	rcu_read_lock();
 	switch (skb->protocol) {
 	case htons(ETH_P_IP):
 		addr4 = ovpn_nexthop_from_skb4(skb);
+		rcu_read_lock();
 		peer = ovpn_peer_get_by_vpn_addr4(ovpn, addr4);
-		break;
+
+		if (peer)
+			break;
+
+		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)
+			ovpn_peer_list_get_all(ovpn, list);
+		return;
 	case htons(ETH_P_IPV6):
 		addr6 = ovpn_nexthop_from_skb6(skb);
+		rcu_read_lock();
 		peer = ovpn_peer_get_by_vpn_addr6(ovpn, &addr6);
-		break;
+
+		if (peer)
+			break;
+
+		rcu_read_unlock();
+		if (ipv6_addr_is_multicast(&addr6))
+			ovpn_peer_list_get_by_mcast_group(ovpn, list);
+		return;
 	}
 
-	if (unlikely(peer && !ovpn_peer_hold(peer)))
-		peer = NULL;
+	if (likely(peer && ovpn_peer_hold(peer)))
+		llist_add(&peer->mcast_entry, list);
 	rcu_read_unlock();
-
-	return peer;
 }
 
 /**
diff --git a/drivers/net/ovpn/peer.h b/drivers/net/ovpn/peer.h
index 328401570cba..08a471deb187 100644
--- a/drivers/net/ovpn/peer.h
+++ b/drivers/net/ovpn/peer.h
@@ -113,6 +113,7 @@  struct ovpn_peer {
 	struct kref refcount;
 	struct rcu_head rcu;
 	struct llist_node release_entry;
+	struct llist_node mcast_entry;
 	struct work_struct keepalive_work;
 };
 
@@ -148,8 +149,8 @@  void ovpn_peers_free(struct ovpn_priv *ovpn, struct sock *sock,
 struct ovpn_peer *ovpn_peer_get_by_transp_addr(struct ovpn_priv *ovpn,
 					       struct sk_buff *skb);
 struct ovpn_peer *ovpn_peer_get_by_id(struct ovpn_priv *ovpn, u32 peer_id);
-struct ovpn_peer *ovpn_peer_get_by_dst(struct ovpn_priv *ovpn,
-				       struct sk_buff *skb);
+void ovpn_peer_list_get_by_dst(struct ovpn_priv *ovpn, struct sk_buff *skb,
+			       struct llist_head *list);
 void ovpn_peer_hash_vpn_ip(struct ovpn_peer *peer);
 bool ovpn_peer_check_by_src(struct ovpn_priv *ovpn, struct sk_buff *skb,
 			    struct ovpn_peer *peer);