* [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21
@ 2026-09-21 10:22 Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 01/11] ovpn: preserve IPv6 scope id for netlink peer endpoints Antonio Quartulli
` (10 more replies)
0 siblings, 11 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Antonio Quartulli, Sabrina Dubroca, Ralf Lici, Jakub Kicinski,
Paolo Abeni, Andrew Lunn, David S. Miller, Eric Dumazet
Hi all!
Here is a batch of eleven ovpn fixes for net, all from Ralf. They come
from two series that went through a few rounds of sashiko pre-review on
openvpn-devel....
There is also a patch for our selftest, which is just a companion change
for a fix introduced here, hence I attached it to this PR.
The first group hardens the UDP transmit path. struct ovpn_bind is
published with RCU and read locklessly on TX, but the local endpoint was
still being updated in place, which can result in torn IPv6 address
reads. The peer dst cache was also not invalidated when the socket mark
or UDP source port changed, and a route resolved from an already
replaced bind could end up being cached. On top of that,
netlink-configured IPv6 link-local remotes lost their scope id, which
breaks route lookup.
The second group tightens peer VPN address validation. In MP mode these
addresses are the lookup keys used to select the peer for an outgoing
tunnel packet, but duplicates, peers left with no usable address, and
addresses that can never identify a peer (multicast, broadcast,
loopback) were all accepted. Such configurations have never worked
reliably, so they are now rejected at configuration time rather than
misbehaving later.
NOTE: Sashiko points out that ovpn_route_key does not include sk_bound_dev_if:
that is a known gap, addressed by the bound-device work already in my
queue for net-next.
There are still larger fixes in our queue, so please ignore any
"previous issue" Sashiko may report on these (is it still reporting
pre-existing issues?).
Please pull or let me know of any issue!
Thanks a lot,
Antonio
The following changes since commit 1e24c4f2ee44be0eee94092b5d13cbdb4bdf0d60:
selftests: tc-testing: add a lateral-drift hfsc classify-walk test (2026-09-19 16:42:11 -0700)
are available in the Git repository at:
https://github.com/OpenVPN/ovpn-net-next.git ovpn-net-20260921
for you to fetch changes up to 006208026819d5e9e5ec07b3e73d95960a059327:
selftests: ovpn: validate peer VPN addresses (2026-09-21 11:39:13 +0200)
----------------------------------------------------------------
Included fixes:
* add selftest coverage for peer VPN address validation
* reject multicast, broadcast and loopback peer VPN addresses, which
can never identify a peer
* reject MP peers left with no usable VPN address, as they can never
be selected for TX
* reject duplicate peer VPN addresses, which made peer lookup return
an arbitrary peer
* fix stale entry left in the VPN address hashtable when an address
is cleared
* fix torn IPv6 address read on lockless TX when the unusable local
source is cleared in place
* fix torn IPv6 address read on lockless TX when a new local endpoint
is learned in place
* fix dst cache being populated with a route resolved from an already
replaced bind
* fix stale route being reused after the socket mark or UDP source
port changed
* fix bogus validation of an unspecified local source address, which
must instead be left to route source autoselection
* fix IPv6 link-local peer endpoints losing their scope id when
configured via netlink, breaking route lookup
----------------------------------------------------------------
Ralf Lici (11):
ovpn: preserve IPv6 scope id for netlink peer endpoints
ovpn: skip UDP source validation for unspecified addresses
ovpn: track UDP socket route key for peer dst cache
ovpn: validate peer state before caching UDP dst
ovpn: replace bind when learning local endpoint
ovpn: replace bind when clearing stale local source
ovpn: always unhash old VPN addresses before rehashing
ovpn: reject duplicate peer VPN addresses
ovpn: reject multipeer peers without VPN addresses
ovpn: reject invalid peer VPN addresses
selftests: ovpn: validate peer VPN addresses
drivers/net/ovpn/netlink.c | 108 ++++++++++++++--
drivers/net/ovpn/peer.c | 121 +++++++++++++----
drivers/net/ovpn/peer.h | 25 +++-
drivers/net/ovpn/udp.c | 193 +++++++++++++++++++++++-----
tools/testing/selftests/net/ovpn/common.sh | 13 ++
tools/testing/selftests/net/ovpn/ovpn-cli.c | 54 +++++---
tools/testing/selftests/net/ovpn/test.sh | 75 ++++++++++-
7 files changed, 493 insertions(+), 96 deletions(-)
^ permalink raw reply [flat|nested] 13+ messages in thread
* [PATCH net 01/11] ovpn: preserve IPv6 scope id for netlink peer endpoints
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-24 17:00 ` patchwork-bot+netdevbpf
2026-09-21 10:22 ` [PATCH net 02/11] ovpn: skip UDP source validation for unspecified addresses Antonio Quartulli
` (9 subsequent siblings)
10 siblings, 1 reply; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
ovpn accepts OVPN_A_PEER_REMOTE_IPV6_SCOPE_ID and reports
bind->remote.in6.sin6_scope_id in peer dumps, but the netlink endpoint
parser never copied the attribute into the sockaddr_in6 used to create or
update the peer bind.
As a result, an IPv6 link-local remote endpoint configured through
netlink loses its interface scope, unlike on the peer float path where
ipv6_iface_scope_id populates the field. The UDPv6 output path then
builds a flow with flowi6_oif set to zero and route lookup can fail or
select the wrong interface.
Copy the scope id when parsing non-v4-mapped IPv6 remote endpoints. The
existing precheck already rejects the scope-id attribute for IPv4 and
v4-mapped IPv6 remotes.
Fixes: 1d36a36f6d53 ("ovpn: implement peer add/get/dump/delete via netlink")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/netlink.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/drivers/net/ovpn/netlink.c b/drivers/net/ovpn/netlink.c
index 4dad85294198..2ba762082acc 100644
--- a/drivers/net/ovpn/netlink.c
+++ b/drivers/net/ovpn/netlink.c
@@ -100,6 +100,8 @@ static bool ovpn_nl_attr_sockaddr_remote(struct nlattr **attrs,
struct sockaddr_in6 *sin6;
struct sockaddr_in *sin;
struct in6_addr *in6;
+ struct nlattr *scope;
+ u32 scope_id = 0;
__be16 port = 0;
__be32 *in;
@@ -114,6 +116,9 @@ static bool ovpn_nl_attr_sockaddr_remote(struct nlattr **attrs,
} else if (attrs[OVPN_A_PEER_REMOTE_IPV6]) {
ss->ss_family = AF_INET6;
in6 = nla_data(attrs[OVPN_A_PEER_REMOTE_IPV6]);
+ scope = attrs[OVPN_A_PEER_REMOTE_IPV6_SCOPE_ID];
+ if (scope)
+ scope_id = nla_get_u32(scope);
} else {
return false;
}
@@ -126,6 +131,7 @@ static bool ovpn_nl_attr_sockaddr_remote(struct nlattr **attrs,
if (!ipv6_addr_v4mapped(in6)) {
sin6 = (struct sockaddr_in6 *)ss;
sin6->sin6_port = port;
+ sin6->sin6_scope_id = scope_id;
memcpy(&sin6->sin6_addr, in6, sizeof(*in6));
break;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 02/11] ovpn: skip UDP source validation for unspecified addresses
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 01/11] ovpn: preserve IPv6 scope id for netlink peer endpoints Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 03/11] ovpn: track UDP socket route key for peer dst cache Antonio Quartulli
` (8 subsequent siblings)
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
ovpn validates the cached local UDP source address before reusing or
refreshing a peer dst cache. This is only meaningful when a concrete
source address is selected.
For IPv6, calling ipv6_chk_addr with :: checks whether the unspecified
address itself is configured on the host. A peer may legitimately have
bind->local.ipv6 set to :: when no local endpoint was configured or
after a stale learned address was cleared. In that case the source
should be left unspecified and selected by ip6_dst_lookup_flow().
For IPv4, inet_confirm_addr(..., local = 0, ...) asks for local address
autoselection rather than validating a chosen source. Skip the precheck
there as well and let ip_route_output_flow select or reject the source.
Only validate non-zero/non-any source addresses.
Fixes: 08857b5ec5d9 ("ovpn: implement basic TX path (UDP)")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/udp.c | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/drivers/net/ovpn/udp.c b/drivers/net/ovpn/udp.c
index 7f69e8890b5b..df4750dabd1e 100644
--- a/drivers/net/ovpn/udp.c
+++ b/drivers/net/ovpn/udp.c
@@ -161,8 +161,8 @@ static int ovpn_udp4_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
if (rt)
goto transmit;
- if (unlikely(!inet_confirm_addr(sock_net(sk), NULL, 0, fl.saddr,
- RT_SCOPE_HOST))) {
+ if (fl.saddr && unlikely(!inet_confirm_addr(sock_net(sk), NULL, 0,
+ fl.saddr, RT_SCOPE_HOST))) {
/* we may end up here when the cached address is not usable
* anymore. In this case we reset address/cache and perform a
* new look up
@@ -238,7 +238,8 @@ static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
if (dst)
goto transmit;
- if (unlikely(!ipv6_chk_addr(sock_net(sk), &fl.saddr, NULL, 0))) {
+ if (!ipv6_addr_any(&fl.saddr) &&
+ unlikely(!ipv6_chk_addr(sock_net(sk), &fl.saddr, NULL, 0))) {
/* we may end up here when the cached address is not usable
* anymore. In this case we reset address/cache and perform a
* new look up
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 03/11] ovpn: track UDP socket route key for peer dst cache
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 01/11] ovpn: preserve IPv6 scope id for netlink peer endpoints Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 02/11] ovpn: skip UDP source validation for unspecified addresses Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 04/11] ovpn: validate peer state before caching UDP dst Antonio Quartulli
` (7 subsequent siblings)
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
ovpn stores the route used to transmit UDP packets in a per-peer dst
cache. A cached dst is only valid for the route lookup inputs used when
it was resolved.
Some of those inputs are mutable while userspace still owns the UDP
socket. In particular, changes to the socket mark or UDP source port do
not invalidate ovpn's peer dst cache, so ovpn can keep using a route
selected with an old socket route key.
Replace the cached mark with a route key containing the socket-owned
lookup inputs currently used by ovpn, and reset the peer dst cache when
the key changes. Before storing a newly looked-up dst, recheck the route
key under the peer lock so a dst resolved for stale socket state is not
published.
Fixes: 08857b5ec5d9 ("ovpn: implement basic TX path (UDP)")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/peer.c | 1 +
drivers/net/ovpn/peer.h | 19 ++++++++-
drivers/net/ovpn/udp.c | 90 +++++++++++++++++++++++++++++++++++------
3 files changed, 95 insertions(+), 15 deletions(-)
diff --git a/drivers/net/ovpn/peer.c b/drivers/net/ovpn/peer.c
index c95656ca7c35..b400783c2efa 100644
--- a/drivers/net/ovpn/peer.c
+++ b/drivers/net/ovpn/peer.c
@@ -113,6 +113,7 @@ struct ovpn_peer *ovpn_peer_new(struct ovpn_priv *ovpn, u32 id)
RCU_INIT_POINTER(peer->bind, NULL);
ovpn_crypto_state_init(&peer->crypto);
spin_lock_init(&peer->lock);
+ seqcount_spinlock_init(&peer->route_key_seq, &peer->lock);
kref_init(&peer->refcount);
ovpn_peer_stats_init(&peer->vpn_stats);
ovpn_peer_stats_init(&peer->link_stats);
diff --git a/drivers/net/ovpn/peer.h b/drivers/net/ovpn/peer.h
index dfa5c0037e02..063535699ecd 100644
--- a/drivers/net/ovpn/peer.h
+++ b/drivers/net/ovpn/peer.h
@@ -10,6 +10,7 @@
#ifndef _NET_OVPN_OVPNPEER_H_
#define _NET_OVPN_OVPNPEER_H_
+#include <linux/seqlock.h>
#include <net/dst_cache.h>
#include <net/strparser.h>
@@ -17,6 +18,16 @@
#include "socket.h"
#include "stats.h"
+/**
+ * struct ovpn_route_key - route key used for the peer dst cache
+ * @mark: fwmark used for route lookup
+ * @sport: UDP source port used for route lookup
+ */
+struct ovpn_route_key {
+ u32 mark;
+ __be16 sport;
+};
+
/**
* struct ovpn_peer - the main remote peer object
* @ovpn: main openvpn instance this peer belongs to
@@ -45,6 +56,8 @@
* @tcp.sk_cb.ops: pointer to the original prot_ops object (TCP only)
* @crypto: the crypto configuration (ciphers, keys, etc..)
* @dst_cache: cache for dst_entry used to send to peer
+ * @route_key: route key matching the current dst cache contents
+ * @route_key_seq: seqcount protecting lockless route_key reads
* @bind: remote peer binding
* @keepalive_interval: seconds after which a new keepalive should be sent
* @keepalive_xmit_exp: future timestamp when next keepalive should be sent
@@ -55,7 +68,7 @@
* @vpn_stats: per-peer in-VPN TX/RX stats
* @link_stats: per-peer link/transport TX/RX stats
* @delete_reason: why peer was deleted (i.e. timeout, transport error, ..)
- * @lock: protects binding to peer (bind) and keepalive* fields
+ * @lock: protects binding to peer (bind), route_key and keepalive* fields
* @refcount: reference counter
* @rcu: used to free peer in an RCU safe way
* @release_entry: entry for the socket release list
@@ -99,6 +112,8 @@ struct ovpn_peer {
} tcp;
struct ovpn_crypto_state crypto;
struct dst_cache dst_cache;
+ struct ovpn_route_key route_key;
+ seqcount_spinlock_t route_key_seq;
struct ovpn_bind __rcu *bind;
unsigned long keepalive_interval;
unsigned long keepalive_xmit_exp;
@@ -109,7 +124,7 @@ struct ovpn_peer {
struct ovpn_peer_stats vpn_stats;
struct ovpn_peer_stats link_stats;
enum ovpn_del_peer_reason delete_reason;
- spinlock_t lock; /* protects bind and keepalive* */
+ spinlock_t lock; /* protects bind, route_key and keepalive* */
struct kref refcount;
struct rcu_head rcu;
struct llist_node release_entry;
diff --git a/drivers/net/ovpn/udp.c b/drivers/net/ovpn/udp.c
index df4750dabd1e..c6d591cb7ff4 100644
--- a/drivers/net/ovpn/udp.c
+++ b/drivers/net/ovpn/udp.c
@@ -131,6 +131,48 @@ static int ovpn_udp_encap_recv(struct sock *sk, struct sk_buff *skb)
return 0;
}
+static bool ovpn_route_key_equal(const struct ovpn_route_key *a,
+ const struct ovpn_route_key *b)
+{
+ return a->mark == b->mark && a->sport == b->sport;
+}
+
+/**
+ * ovpn_dst_cache_check_key - reset peer dst cache after key changes
+ * @peer: the peer owning the dst cache
+ * @cache: the cache that might need to be reset
+ * @key: the route key for the packet being transmitted
+ *
+ * Reset the peer dst cache if it was populated for a different route key.
+ */
+static void ovpn_dst_cache_check_key(struct ovpn_peer *peer,
+ struct dst_cache *cache,
+ const struct ovpn_route_key *key)
+{
+ struct ovpn_route_key old_key;
+ unsigned int seq;
+
+ /* snapshot the saved key before deciding whether the cache matches */
+ do {
+ seq = read_seqcount_begin(&peer->route_key_seq);
+ old_key = peer->route_key;
+ } while (read_seqcount_retry(&peer->route_key_seq, seq));
+
+ /* nothing changed: the current cache can be reused */
+ if (likely(ovpn_route_key_equal(&old_key, key)))
+ return;
+
+ /* recheck under lock because another path may have updated the key */
+ spin_lock_bh(&peer->lock);
+ if (!ovpn_route_key_equal(&peer->route_key, key)) {
+ write_seqcount_begin(&peer->route_key_seq);
+ peer->route_key = *key;
+ dst_cache_reset(cache);
+ write_seqcount_end(&peer->route_key_seq);
+ }
+ spin_unlock_bh(&peer->lock);
+}
+
/**
* ovpn_udp4_output - send IPv4 packet over udp socket
* @peer: the destination peer
@@ -138,21 +180,23 @@ static int ovpn_udp_encap_recv(struct sock *sk, struct sk_buff *skb)
* @cache: dst cache
* @sk: the socket to send the packet over
* @skb: the packet to send
+ * @key: the route key snapshot used for cache validation and flow lookup
*
* Return: 0 on success or a negative error code otherwise
*/
static int ovpn_udp4_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
struct dst_cache *cache, struct sock *sk,
- struct sk_buff *skb)
+ struct sk_buff *skb,
+ const struct ovpn_route_key *key)
{
struct rtable *rt;
struct flowi4 fl = {
.saddr = bind->local.ipv4.s_addr,
.daddr = bind->remote.in4.sin_addr.s_addr,
- .fl4_sport = inet_sk(sk)->inet_sport,
+ .fl4_sport = key->sport,
.fl4_dport = bind->remote.in4.sin_port,
.flowi4_proto = sk->sk_protocol,
- .flowi4_mark = sk->sk_mark,
+ .flowi4_mark = key->mark,
};
int ret;
@@ -193,7 +237,12 @@ static int ovpn_udp4_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
ret);
goto err;
}
- dst_cache_set_ip4(cache, &rt->dst, fl.saddr);
+
+ /* avoid storing a stale cache */
+ spin_lock_bh(&peer->lock);
+ if (likely(ovpn_route_key_equal(key, &peer->route_key)))
+ dst_cache_set_ip4(cache, &rt->dst, fl.saddr);
+ spin_unlock_bh(&peer->lock);
transmit:
udp_tunnel_xmit_skb(rt, sk, skb, fl.saddr, fl.daddr, 0,
@@ -213,12 +262,14 @@ static int ovpn_udp4_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
* @cache: dst cache
* @sk: the socket to send the packet over
* @skb: the packet to send
+ * @key: the route key snapshot used for cache validation and flow lookup
*
* Return: 0 on success or a negative error code otherwise
*/
static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
struct dst_cache *cache, struct sock *sk,
- struct sk_buff *skb)
+ struct sk_buff *skb,
+ const struct ovpn_route_key *key)
{
struct dst_entry *dst;
int ret;
@@ -226,10 +277,10 @@ static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
struct flowi6 fl = {
.saddr = bind->local.ipv6,
.daddr = bind->remote.in6.sin6_addr,
- .fl6_sport = inet_sk(sk)->inet_sport,
+ .fl6_sport = key->sport,
.fl6_dport = bind->remote.in6.sin6_port,
.flowi6_proto = sk->sk_protocol,
- .flowi6_mark = sk->sk_mark,
+ .flowi6_mark = key->mark,
.flowi6_oif = bind->remote.in6.sin6_scope_id,
};
@@ -259,7 +310,12 @@ static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
&bind->remote.in6, ret);
goto err;
}
- dst_cache_set_ip6(cache, dst, &fl.saddr);
+
+ /* avoid storing a stale cache */
+ spin_lock_bh(&peer->lock);
+ if (likely(ovpn_route_key_equal(key, &peer->route_key)))
+ dst_cache_set_ip6(cache, dst, &fl.saddr);
+ spin_unlock_bh(&peer->lock);
transmit:
/* user IPv6 packets may be larger than the transport interface
@@ -288,6 +344,7 @@ static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
* @cache: dst cache
* @sk: the socket to send the packet over
* @skb: the packet to send
+ * @key: route key snapshot used for cache validation and flow lookup
*
* rcu_read_lock should be held on entry.
* On return, the skb is consumed.
@@ -295,7 +352,8 @@ static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
* Return: 0 on success or a negative error code otherwise
*/
static int ovpn_udp_output(struct ovpn_peer *peer, struct dst_cache *cache,
- struct sock *sk, struct sk_buff *skb)
+ struct sock *sk, struct sk_buff *skb,
+ struct ovpn_route_key *key)
{
struct ovpn_bind *bind;
int ret;
@@ -315,11 +373,11 @@ static int ovpn_udp_output(struct ovpn_peer *peer, struct dst_cache *cache,
switch (bind->remote.in4.sin_family) {
case AF_INET:
- ret = ovpn_udp4_output(peer, bind, cache, sk, skb);
+ ret = ovpn_udp4_output(peer, bind, cache, sk, skb, key);
break;
#if IS_ENABLED(CONFIG_IPV6)
case AF_INET6:
- ret = ovpn_udp6_output(peer, bind, cache, sk, skb);
+ ret = ovpn_udp6_output(peer, bind, cache, sk, skb, key);
break;
#endif
default:
@@ -341,15 +399,21 @@ static int ovpn_udp_output(struct ovpn_peer *peer, struct dst_cache *cache,
void ovpn_udp_send_skb(struct ovpn_peer *peer, struct sock *sk,
struct sk_buff *skb)
{
+ struct ovpn_route_key key = {
+ .mark = READ_ONCE(sk->sk_mark),
+ .sport = READ_ONCE(inet_sk(sk)->inet_sport),
+ };
int ret;
skb->dev = peer->ovpn->dev;
- skb->mark = READ_ONCE(sk->sk_mark);
+ skb->mark = key.mark;
/* no checksum performed at this layer */
skb->ip_summed = CHECKSUM_NONE;
+ ovpn_dst_cache_check_key(peer, &peer->dst_cache, &key);
+
/* crypto layer -> transport (UDP) */
- ret = ovpn_udp_output(peer, &peer->dst_cache, sk, skb);
+ ret = ovpn_udp_output(peer, &peer->dst_cache, sk, skb, &key);
if (unlikely(ret < 0))
kfree_skb(skb);
}
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 04/11] ovpn: validate peer state before caching UDP dst
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
` (2 preceding siblings ...)
2026-09-21 10:22 ` [PATCH net 03/11] ovpn: track UDP socket route key for peer dst cache Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 05/11] ovpn: replace bind when learning local endpoint Antonio Quartulli
` (6 subsequent siblings)
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
UDP route lookup runs without peer->lock while the bind is protected by
RCU. The route key is snapshotted separately. Either can change while
the lookup is in progress.
The TX path currently checks only the route key before publishing the
looked-up dst. If the bind changes but the route key does not, a dst
resolved from the old endpoint can be installed in the cache after the
bind replacement.
Compare both the bind pointer and the route key under peer->lock before
updating the cache. The RCU read-side critical section keeps the old
bind alive throughout the lookup, so pointer identity is sufficient to
detect a replacement.
Fixes: f0281c1d3732 ("ovpn: add support for updating local or remote UDP endpoint")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/udp.c | 33 +++++++++++++++++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)
diff --git a/drivers/net/ovpn/udp.c b/drivers/net/ovpn/udp.c
index c6d591cb7ff4..eeef4a7229f5 100644
--- a/drivers/net/ovpn/udp.c
+++ b/drivers/net/ovpn/udp.c
@@ -173,6 +173,35 @@ static void ovpn_dst_cache_check_key(struct ovpn_peer *peer,
spin_unlock_bh(&peer->lock);
}
+/**
+ * ovpn_dst_cache_current - check whether a route lookup matches peer state
+ * @peer: the peer owning the bind and dst cache
+ * @bind: the RCU bind used for the route lookup
+ * @key: the route key used for the route lookup
+ *
+ * Check that @bind is still the current peer bind and that @key still matches
+ * the peer route key. The caller must hold @peer->lock. The TX path keeps
+ * @bind inside an RCU read-side critical section, so pointer identity is enough
+ * to detect whether the bind was replaced while the route lookup was running.
+ *
+ * Return: true if the lookup result still matches the current peer state and
+ * may update the dst cache.
+ */
+static bool ovpn_dst_cache_current(const struct ovpn_peer *peer,
+ const struct ovpn_bind *bind,
+ const struct ovpn_route_key *key)
+{
+ const struct ovpn_bind *curr_bind;
+
+ lockdep_assert_held(&peer->lock);
+
+ curr_bind = rcu_dereference_protected(peer->bind,
+ lockdep_is_held(&peer->lock));
+
+ return curr_bind == bind &&
+ ovpn_route_key_equal(key, &peer->route_key);
+}
+
/**
* ovpn_udp4_output - send IPv4 packet over udp socket
* @peer: the destination peer
@@ -240,7 +269,7 @@ static int ovpn_udp4_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
/* avoid storing a stale cache */
spin_lock_bh(&peer->lock);
- if (likely(ovpn_route_key_equal(key, &peer->route_key)))
+ if (likely(ovpn_dst_cache_current(peer, bind, key)))
dst_cache_set_ip4(cache, &rt->dst, fl.saddr);
spin_unlock_bh(&peer->lock);
@@ -313,7 +342,7 @@ static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
/* avoid storing a stale cache */
spin_lock_bh(&peer->lock);
- if (likely(ovpn_route_key_equal(key, &peer->route_key)))
+ if (likely(ovpn_dst_cache_current(peer, bind, key)))
dst_cache_set_ip6(cache, dst, &fl.saddr);
spin_unlock_bh(&peer->lock);
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 05/11] ovpn: replace bind when learning local endpoint
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
` (3 preceding siblings ...)
2026-09-21 10:22 ` [PATCH net 04/11] ovpn: validate peer state before caching UDP dst Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 06/11] ovpn: replace bind when clearing stale local source Antonio Quartulli
` (5 subsequent siblings)
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
struct ovpn_bind is published through peer->bind with RCU, but local
endpoint learning updates bind->local in place under peer->lock. UDP TX
reads the field without that lock. In particular, a concurrent IPv6
update can therefore result in a torn address read.
Use ovpn_peer_reset_sockaddr to publish a replacement bind when learning
a new local endpoint, just as a remote endpoint change does. Preserve
the current remote address and reset the dst cache only after the new
bind has been published successfully.
Track remote endpoint changes separately so that float notification and
transport-address rehashing remain limited to actual peer floats.
Fixes: f0281c1d3732 ("ovpn: add support for updating local or remote UDP endpoint")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/peer.c | 43 ++++++++++++++++++++++-------------------
1 file changed, 23 insertions(+), 20 deletions(-)
diff --git a/drivers/net/ovpn/peer.c b/drivers/net/ovpn/peer.c
index b400783c2efa..430c6cd48db8 100644
--- a/drivers/net/ovpn/peer.c
+++ b/drivers/net/ovpn/peer.c
@@ -200,13 +200,12 @@ static void __ovpn_peer_hash_transp_addr(struct ovpn_peer *peer,
*/
void ovpn_peer_endpoints_update(struct ovpn_peer *peer, struct sk_buff *skb)
{
+ const void *local_ip = NULL;
struct sockaddr_storage ss;
struct sockaddr_in6 *sa6;
- bool reset_cache = false;
struct sockaddr_in *sa;
struct ovpn_bind *bind;
- const void *local_ip;
- size_t salen = 0;
+ bool floated = false;
spin_lock_bh(&peer->lock);
bind = rcu_dereference_protected(peer->bind,
@@ -233,8 +232,7 @@ void ovpn_peer_endpoints_update(struct ovpn_peer *peer, struct sk_buff *skb)
.sin_addr.s_addr = ip_hdr(skb)->saddr,
.sin_port = udp_hdr(skb)->source,
};
- salen = sizeof(*sa);
- reset_cache = true;
+ floated = true;
break;
}
@@ -246,10 +244,12 @@ void ovpn_peer_endpoints_update(struct ovpn_peer *peer, struct sk_buff *skb)
netdev_name(peer->ovpn->dev),
peer->id, &bind->local.ipv4.s_addr,
&ip_hdr(skb)->daddr);
- bind->local.ipv4.s_addr = ip_hdr(skb)->daddr;
- reset_cache = true;
+ local_ip = &ip_hdr(skb)->daddr;
+ memcpy(&ss, &bind->remote, sizeof(struct sockaddr_in));
+ break;
}
- break;
+ /* nothing changed */
+ goto unlock;
case htons(ETH_P_IPV6):
/* float check */
if (unlikely(!ovpn_bind_skb_src_match(bind, skb))) {
@@ -271,8 +271,7 @@ void ovpn_peer_endpoints_update(struct ovpn_peer *peer, struct sk_buff *skb)
ipv6_iface_scope_id(&ipv6_hdr(skb)->saddr,
skb->skb_iif),
};
- salen = sizeof(*sa6);
- reset_cache = true;
+ floated = true;
break;
}
@@ -285,26 +284,30 @@ void ovpn_peer_endpoints_update(struct ovpn_peer *peer, struct sk_buff *skb)
netdev_name(peer->ovpn->dev),
peer->id, &bind->local.ipv6,
&ipv6_hdr(skb)->daddr);
- bind->local.ipv6 = ipv6_hdr(skb)->daddr;
- reset_cache = true;
+ local_ip = &ipv6_hdr(skb)->daddr;
+ memcpy(&ss, &bind->remote, sizeof(struct sockaddr_in6));
+ break;
}
- break;
+ /* nothing changed */
+ goto unlock;
default:
goto unlock;
}
- if (unlikely(reset_cache))
- dst_cache_reset(&peer->dst_cache);
-
- /* if the peer did not float, we can bail out now */
- if (likely(!salen))
- goto unlock;
-
if (unlikely(ovpn_peer_reset_sockaddr(peer,
(struct sockaddr_storage *)&ss,
local_ip) < 0))
goto unlock;
+ /* reset the cache only after a successful bind update to avoid useless
+ * cache misses on concurrent TX
+ */
+ dst_cache_reset(&peer->dst_cache);
+
+ /* if only the local address changed, bail out now */
+ if (!floated)
+ goto unlock;
+
net_dbg_ratelimited("%s: peer %d floated to %pIScp",
netdev_name(peer->ovpn->dev), peer->id, &ss);
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 06/11] ovpn: replace bind when clearing stale local source
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
` (4 preceding siblings ...)
2026-09-21 10:22 ` [PATCH net 05/11] ovpn: replace bind when learning local endpoint Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 07/11] ovpn: always unhash old VPN addresses before rehashing Antonio Quartulli
` (4 subsequent siblings)
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
The UDP output fallback clears bind->local in place when the remembered
source address is no longer usable. The bind is RCU-published and read
locklessly by concurrent TX, so an IPv6 reader can observe a torn
address.
Retry the route lookup with source address autoselection without
modifying the bind. After a successful lookup, revalidate the bind and
route key under peer->lock, reset the dst cache, and best-effort publish
a replacement bind with a wildcard local address.
Do not cache the resolved dst when clearing the local source. Replacing
the source invalidates all per-CPU cache entries, while
dst_cache_set_ip4 and dst_cache_set_ip6 update only the current CPU
slot. The current packet can still use the resolved route; if bind
allocation fails, a later cache miss retries the repair.
Fixes: 08857b5ec5d9 ("ovpn: implement basic TX path (UDP)")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/udp.c | 81 +++++++++++++++++++++++++++++-------------
1 file changed, 56 insertions(+), 25 deletions(-)
diff --git a/drivers/net/ovpn/udp.c b/drivers/net/ovpn/udp.c
index eeef4a7229f5..055cdb1bee13 100644
--- a/drivers/net/ovpn/udp.c
+++ b/drivers/net/ovpn/udp.c
@@ -185,7 +185,7 @@ static void ovpn_dst_cache_check_key(struct ovpn_peer *peer,
* to detect whether the bind was replaced while the route lookup was running.
*
* Return: true if the lookup result still matches the current peer state and
- * may update the dst cache.
+ * may update the dst cache or replace the bind.
*/
static bool ovpn_dst_cache_current(const struct ovpn_peer *peer,
const struct ovpn_bind *bind,
@@ -218,6 +218,9 @@ static int ovpn_udp4_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
struct sk_buff *skb,
const struct ovpn_route_key *key)
{
+ struct sockaddr_storage remote;
+ struct in_addr local = {};
+ bool reset_local = false;
struct rtable *rt;
struct flowi4 fl = {
.saddr = bind->local.ipv4.s_addr,
@@ -236,24 +239,17 @@ static int ovpn_udp4_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
if (fl.saddr && unlikely(!inet_confirm_addr(sock_net(sk), NULL, 0,
fl.saddr, RT_SCOPE_HOST))) {
- /* we may end up here when the cached address is not usable
- * anymore. In this case we reset address/cache and perform a
- * new look up
+ /* The learned local address is not usable anymore.
+ * Retry with source address autoselection.
*/
fl.saddr = 0;
- spin_lock_bh(&peer->lock);
- bind->local.ipv4.s_addr = 0;
- spin_unlock_bh(&peer->lock);
- dst_cache_reset(cache);
+ reset_local = true;
}
rt = ip_route_output_flow(sock_net(sk), &fl, sk);
if (IS_ERR(rt) && PTR_ERR(rt) == -EINVAL) {
fl.saddr = 0;
- spin_lock_bh(&peer->lock);
- bind->local.ipv4.s_addr = 0;
- spin_unlock_bh(&peer->lock);
- dst_cache_reset(cache);
+ reset_local = true;
rt = ip_route_output_flow(sock_net(sk), &fl, sk);
}
@@ -267,10 +263,28 @@ static int ovpn_udp4_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
goto err;
}
- /* avoid storing a stale cache */
+ /* avoid storing a stale cache or local address */
spin_lock_bh(&peer->lock);
- if (likely(ovpn_dst_cache_current(peer, bind, key)))
- dst_cache_set_ip4(cache, &rt->dst, fl.saddr);
+ if (likely(ovpn_dst_cache_current(peer, bind, key))) {
+ if (!reset_local) {
+ dst_cache_set_ip4(cache, &rt->dst, fl.saddr);
+ spin_unlock_bh(&peer->lock);
+ goto transmit;
+ }
+
+ /* invalidate per-CPU dst entries that may still carry
+ * the stale source
+ */
+ dst_cache_reset(cache);
+
+ /* preserve the current remote */
+ memcpy(&remote, &bind->remote, sizeof(struct sockaddr_in));
+ /* The current packet already has a valid wildcard-source route.
+ * If replacing the bind fails, leave the stale local in place;
+ * a later cache miss will retry the repair.
+ */
+ ovpn_peer_reset_sockaddr(peer, &remote, &local);
+ }
spin_unlock_bh(&peer->lock);
transmit:
@@ -300,6 +314,9 @@ static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
struct sk_buff *skb,
const struct ovpn_route_key *key)
{
+ struct in6_addr local = in6addr_any;
+ struct sockaddr_storage remote;
+ bool reset_local = false;
struct dst_entry *dst;
int ret;
@@ -320,15 +337,11 @@ static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
if (!ipv6_addr_any(&fl.saddr) &&
unlikely(!ipv6_chk_addr(sock_net(sk), &fl.saddr, NULL, 0))) {
- /* we may end up here when the cached address is not usable
- * anymore. In this case we reset address/cache and perform a
- * new look up
+ /* The learned local address is not usable anymore.
+ * Retry with source address autoselection.
*/
fl.saddr = in6addr_any;
- spin_lock_bh(&peer->lock);
- bind->local.ipv6 = in6addr_any;
- spin_unlock_bh(&peer->lock);
- dst_cache_reset(cache);
+ reset_local = true;
}
dst = ip6_dst_lookup_flow(sock_net(sk), sk, &fl, NULL);
@@ -340,10 +353,28 @@ static int ovpn_udp6_output(struct ovpn_peer *peer, struct ovpn_bind *bind,
goto err;
}
- /* avoid storing a stale cache */
+ /* avoid storing a stale cache or local address */
spin_lock_bh(&peer->lock);
- if (likely(ovpn_dst_cache_current(peer, bind, key)))
- dst_cache_set_ip6(cache, dst, &fl.saddr);
+ if (likely(ovpn_dst_cache_current(peer, bind, key))) {
+ if (!reset_local) {
+ dst_cache_set_ip6(cache, dst, &fl.saddr);
+ spin_unlock_bh(&peer->lock);
+ goto transmit;
+ }
+
+ /* invalidate per-CPU dst entries that may still carry
+ * the stale source
+ */
+ dst_cache_reset(cache);
+
+ /* preserve the current remote */
+ memcpy(&remote, &bind->remote, sizeof(struct sockaddr_in6));
+ /* The current packet already has a valid wildcard-source route.
+ * If replacing the bind fails, leave the stale local in place;
+ * a later cache miss will retry the repair.
+ */
+ ovpn_peer_reset_sockaddr(peer, &remote, &local);
+ }
spin_unlock_bh(&peer->lock);
transmit:
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 07/11] ovpn: always unhash old VPN addresses before rehashing
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
` (5 preceding siblings ...)
2026-09-21 10:22 ` [PATCH net 06/11] ovpn: replace bind when clearing stale local source Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 08/11] ovpn: reject duplicate peer VPN addresses Antonio Quartulli
` (3 subsequent siblings)
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
ovpn_peer_hash_vpn_ip updates the per-peer VPN address hash entries
after userspace changes a peer VPN address. The current code removes an
old hash entry only when the new address for that family is not the
unspecified address.
When an address is cleared to 0.0.0.0 or ::, its hash node therefore
remains linked in the bucket selected by the old address. The address
comparison performed during lookup prevents the old address from
matching, but the table retains a stale entry until the peer is removed
or another address is configured for that family.
Always remove both old VPN address hash entries before conditionally
adding the currently configured addresses back. This ensures that a
cleared address leaves its hash node unhashed.
Fixes: 1d36a36f6d53 ("ovpn: implement peer add/get/dump/delete via netlink")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/peer.c | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/drivers/net/ovpn/peer.c b/drivers/net/ovpn/peer.c
index 430c6cd48db8..bbd9e17fb0bf 100644
--- a/drivers/net/ovpn/peer.c
+++ b/drivers/net/ovpn/peer.c
@@ -994,10 +994,11 @@ void ovpn_peer_hash_vpn_ip(struct ovpn_peer *peer)
if (hlist_unhashed(&peer->hash_entry_id))
return;
- if (peer->vpn_addrs.ipv4.s_addr != htonl(INADDR_ANY)) {
- /* remove potential old hashing */
- hlist_nulls_del_init_rcu(&peer->hash_entry_addr4);
+ /* remove potential old hashing */
+ hlist_nulls_del_init_rcu(&peer->hash_entry_addr4);
+ hlist_nulls_del_init_rcu(&peer->hash_entry_addr6);
+ if (peer->vpn_addrs.ipv4.s_addr != htonl(INADDR_ANY)) {
nhead = ovpn_get_hash_head(peer->ovpn->peers->by_vpn_addr4,
&peer->vpn_addrs.ipv4,
sizeof(peer->vpn_addrs.ipv4));
@@ -1005,9 +1006,6 @@ void ovpn_peer_hash_vpn_ip(struct ovpn_peer *peer)
}
if (!ipv6_addr_any(&peer->vpn_addrs.ipv6)) {
- /* remove potential old hashing */
- hlist_nulls_del_init_rcu(&peer->hash_entry_addr6);
-
nhead = ovpn_get_hash_head(peer->ovpn->peers->by_vpn_addr6,
&peer->vpn_addrs.ipv6,
sizeof(peer->vpn_addrs.ipv6));
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 08/11] ovpn: reject duplicate peer VPN addresses
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
` (6 preceding siblings ...)
2026-09-21 10:22 ` [PATCH net 07/11] ovpn: always unhash old VPN addresses before rehashing Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 09/11] ovpn: reject multipeer peers without " Antonio Quartulli
` (2 subsequent siblings)
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
In MP mode, ovpn uses the peer VPN addresses as lookup keys for
selecting the peer that should receive an outgoing tunnel packet.
However, the netlink peer configuration path does not currently reject
duplicate VPN addresses.
If two peers are configured with the same VPN address, both can be
inserted in the VPN address hash table and lookups return whichever peer
is found first. This makes peer selection ambiguous and dependent on
hash insertion order.
Reject peer creation or update when the resulting VPN address is already
assigned to another peer. Ignore unspecified addresses because those are
not inserted in the VPN address hash tables.
This changes such configurations from being accepted to being rejected,
but they have never worked reliably because peer selection is ambiguous.
Fixes: 1d36a36f6d53 ("ovpn: implement peer add/get/dump/delete via netlink")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/netlink.c | 37 ++++++++++++++++-----
drivers/net/ovpn/peer.c | 67 +++++++++++++++++++++++++++++++++++++-
drivers/net/ovpn/peer.h | 6 ++++
3 files changed, 101 insertions(+), 9 deletions(-)
diff --git a/drivers/net/ovpn/netlink.c b/drivers/net/ovpn/netlink.c
index 2ba762082acc..e23f7d1f49e0 100644
--- a/drivers/net/ovpn/netlink.c
+++ b/drivers/net/ovpn/netlink.c
@@ -480,8 +480,10 @@ int ovpn_nl_peer_new_doit(struct sk_buff *skb, struct genl_info *info)
int ovpn_nl_peer_set_doit(struct sk_buff *skb, struct genl_info *info)
{
- struct nlattr *attrs[OVPN_A_PEER_MAX + 1];
struct ovpn_priv *ovpn = info->user_ptr[0];
+ struct nlattr *attrs[OVPN_A_PEER_MAX + 1];
+ struct in6_addr vpn_addr6;
+ struct in_addr vpn_addr4;
struct ovpn_socket *sock;
struct ovpn_peer *peer;
u32 peer_id;
@@ -528,28 +530,47 @@ int ovpn_nl_peer_set_doit(struct sk_buff *skb, struct genl_info *info)
rcu_read_unlock();
spin_lock_bh(&ovpn->lock);
- ret = ovpn_nl_peer_modify(peer, info, attrs);
- if (ret < 0) {
- spin_unlock_bh(&ovpn->lock);
- ovpn_peer_put(peer);
- return ret;
+
+ /* reject peer with conflicting VPN address */
+ if (attrs[OVPN_A_PEER_VPN_IPV4]) {
+ vpn_addr4.s_addr = nla_get_in_addr(attrs[OVPN_A_PEER_VPN_IPV4]);
+ if (ovpn_peer_vpn_addr_conflict4(ovpn, peer, &vpn_addr4))
+ goto addr_conflict;
}
+ if (attrs[OVPN_A_PEER_VPN_IPV6]) {
+ vpn_addr6 = nla_get_in6_addr(attrs[OVPN_A_PEER_VPN_IPV6]);
+ if (ovpn_peer_vpn_addr_conflict6(ovpn, peer, &vpn_addr6))
+ goto addr_conflict;
+ }
+
+ ret = ovpn_nl_peer_modify(peer, info, attrs);
+ if (ret < 0)
+ goto unlock;
/* ret == 1 means that VPN IPv4/6 has been modified and rehashing
* is required
*/
- if (ret > 0)
+ if (ret > 0) {
ovpn_peer_hash_vpn_ip(peer);
+ ret = 0;
+ }
/* if the remote endpoint was updated, the by_transp_addr hash bucket
* also needs to be refreshed, otherwise incoming packets from the new
* remote address would fail the lockless lookup
*/
if (attrs[OVPN_A_PEER_REMOTE_IPV4] || attrs[OVPN_A_PEER_REMOTE_IPV6])
ovpn_peer_hash_transp_addr(peer);
+
+unlock:
spin_unlock_bh(&ovpn->lock);
ovpn_peer_put(peer);
- return 0;
+ return ret;
+addr_conflict:
+ NL_SET_ERR_MSG_FMT_MOD(info->extack,
+ "VPN IP is already assigned to another peer");
+ ret = -EADDRINUSE;
+ goto unlock;
}
static int ovpn_nl_send_peer(struct sk_buff *skb, const struct genl_info *info,
diff --git a/drivers/net/ovpn/peer.c b/drivers/net/ovpn/peer.c
index bbd9e17fb0bf..2067825bb5b6 100644
--- a/drivers/net/ovpn/peer.c
+++ b/drivers/net/ovpn/peer.c
@@ -488,7 +488,7 @@ static struct ovpn_peer *ovpn_peer_get_by_vpn_addr4(struct ovpn_priv *ovpn,
* Return: the peer if found or NULL otherwise
*/
static struct ovpn_peer *ovpn_peer_get_by_vpn_addr6(struct ovpn_priv *ovpn,
- struct in6_addr *addr)
+ const struct in6_addr *addr)
{
struct hlist_nulls_head *nhead;
struct hlist_nulls_node *ntmp;
@@ -513,6 +513,64 @@ static struct ovpn_peer *ovpn_peer_get_by_vpn_addr6(struct ovpn_priv *ovpn,
return NULL;
}
+/**
+ * ovpn_peer_vpn_addr_conflict4 - check if the VPN v4 address is already in use
+ * @ovpn: the openvpn instance to search
+ * @peer: peer being added or updated, or NULL
+ * @addr: VPN IPv4 address to check
+ *
+ * Check whether @addr is already assigned to another peer. @peer is ignored
+ * when found, allowing peer updates that keep an existing address.
+ * Unspecified addresses are ignored.
+ *
+ * Note: the caller must hold @ovpn->lock.
+ *
+ * Return: true on conflict, false otherwise.
+ */
+bool ovpn_peer_vpn_addr_conflict4(struct ovpn_priv *ovpn,
+ const struct ovpn_peer *peer,
+ const struct in_addr *addr)
+{
+ struct ovpn_peer *tmp = NULL;
+
+ lockdep_assert_held(&ovpn->lock);
+
+ /* we don't hash INADDR_ANY, no conflict in that case */
+ if (addr->s_addr != htonl(INADDR_ANY))
+ tmp = ovpn_peer_get_by_vpn_addr4(ovpn, addr->s_addr);
+
+ return tmp && tmp != peer;
+}
+
+/**
+ * ovpn_peer_vpn_addr_conflict6 - check if the VPN v6 address is already in use
+ * @ovpn: the openvpn instance to search
+ * @peer: peer being added or updated, or NULL
+ * @addr: VPN IPv6 address to check
+ *
+ * Check whether @addr is already assigned to another peer. @peer is ignored
+ * when found, allowing peer updates that keep an existing address.
+ * Unspecified addresses are ignored.
+ *
+ * Note: the caller must hold @ovpn->lock.
+ *
+ * Return: true on conflict, false otherwise.
+ */
+bool ovpn_peer_vpn_addr_conflict6(struct ovpn_priv *ovpn,
+ const struct ovpn_peer *peer,
+ const struct in6_addr *addr)
+{
+ struct ovpn_peer *tmp = NULL;
+
+ lockdep_assert_held(&ovpn->lock);
+
+ /* we don't hash ::, no conflict in that case */
+ if (!ipv6_addr_any(addr))
+ tmp = ovpn_peer_get_by_vpn_addr6(ovpn, addr);
+
+ return tmp && tmp != peer;
+}
+
/**
* ovpn_peer_transp_match - check if sockaddr and peer binding match
* @peer: the peer to get the binding from
@@ -1040,6 +1098,13 @@ static int ovpn_peer_add_mp(struct ovpn_priv *ovpn, struct ovpn_peer *peer)
goto out;
}
+ /* reject peer with conflicting VPN address */
+ if (ovpn_peer_vpn_addr_conflict4(ovpn, NULL, &peer->vpn_addrs.ipv4) ||
+ ovpn_peer_vpn_addr_conflict6(ovpn, NULL, &peer->vpn_addrs.ipv6)) {
+ ret = -EADDRINUSE;
+ goto out;
+ }
+
bind = rcu_dereference_protected(peer->bind, true);
/* peers connected via TCP have bind == NULL */
if (bind) {
diff --git a/drivers/net/ovpn/peer.h b/drivers/net/ovpn/peer.h
index 063535699ecd..1879bfb76992 100644
--- a/drivers/net/ovpn/peer.h
+++ b/drivers/net/ovpn/peer.h
@@ -164,6 +164,12 @@ struct ovpn_peer *ovpn_peer_get_by_transp_addr(struct ovpn_priv *ovpn,
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);
+bool ovpn_peer_vpn_addr_conflict4(struct ovpn_priv *ovpn,
+ const struct ovpn_peer *peer,
+ const struct in_addr *addr);
+bool ovpn_peer_vpn_addr_conflict6(struct ovpn_priv *ovpn,
+ const struct ovpn_peer *peer,
+ const struct in6_addr *addr);
void ovpn_peer_hash_vpn_ip(struct ovpn_peer *peer);
void ovpn_peer_hash_transp_addr(struct ovpn_peer *peer);
bool ovpn_peer_check_by_src(struct ovpn_priv *ovpn, struct sk_buff *skb,
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 09/11] ovpn: reject multipeer peers without VPN addresses
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
` (7 preceding siblings ...)
2026-09-21 10:22 ` [PATCH net 08/11] ovpn: reject duplicate peer VPN addresses Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 10/11] ovpn: reject invalid peer " Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 11/11] selftests: ovpn: validate " Antonio Quartulli
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
In MP mode, ovpn uses the peer VPN addresses to select the peer for
outgoing tunnel packets. Peer creation currently requires a VPN IPv4 or
IPv6 attribute, but it only checks for the presence of the attribute and
not for a usable address value.
This allows userspace to create an MP peer with only unspecified VPN
addresses, or to update an existing peer so that both VPN address
families become unspecified. Such a peer cannot be selected through the
VPN address hash tables.
Reject MP peer creation or update when the resulting peer would not have
at least one VPN address configured.
This changes such configurations from being accepted to being rejected,
but they have never been usable because the peer cannot be selected
through the VPN address hash tables.
Fixes: 1d36a36f6d53 ("ovpn: implement peer add/get/dump/delete via netlink")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/netlink.c | 36 ++++++++++++++++++++++++++++++------
1 file changed, 30 insertions(+), 6 deletions(-)
diff --git a/drivers/net/ovpn/netlink.c b/drivers/net/ovpn/netlink.c
index e23f7d1f49e0..e9e0f75e0443 100644
--- a/drivers/net/ovpn/netlink.c
+++ b/drivers/net/ovpn/netlink.c
@@ -352,8 +352,10 @@ static int ovpn_nl_peer_modify(struct ovpn_peer *peer, struct genl_info *info,
int ovpn_nl_peer_new_doit(struct sk_buff *skb, struct genl_info *info)
{
- struct nlattr *attrs[OVPN_A_PEER_MAX + 1];
+ struct in_addr vpn_addr4 = { .s_addr = htonl(INADDR_ANY) };
+ struct in6_addr vpn_addr6 = IN6ADDR_ANY_INIT;
struct ovpn_priv *ovpn = info->user_ptr[0];
+ struct nlattr *attrs[OVPN_A_PEER_MAX + 1];
struct ovpn_socket *ovpn_sock;
struct socket *sock = NULL;
struct ovpn_peer *peer;
@@ -377,11 +379,20 @@ int ovpn_nl_peer_new_doit(struct sk_buff *skb, struct genl_info *info)
return -EINVAL;
/* in MP mode VPN IPs are required for selecting the right peer */
- if (ovpn->mode == OVPN_MODE_MP && !attrs[OVPN_A_PEER_VPN_IPV4] &&
- !attrs[OVPN_A_PEER_VPN_IPV6]) {
- NL_SET_ERR_MSG_FMT_MOD(info->extack,
- "VPN IP must be provided in MP mode");
- return -EINVAL;
+ if (ovpn->mode == OVPN_MODE_MP) {
+ if (attrs[OVPN_A_PEER_VPN_IPV4])
+ vpn_addr4.s_addr =
+ nla_get_in_addr(attrs[OVPN_A_PEER_VPN_IPV4]);
+ if (attrs[OVPN_A_PEER_VPN_IPV6])
+ vpn_addr6 =
+ nla_get_in6_addr(attrs[OVPN_A_PEER_VPN_IPV6]);
+
+ if (vpn_addr4.s_addr == htonl(INADDR_ANY) &&
+ ipv6_addr_any(&vpn_addr6)) {
+ NL_SET_ERR_MSG_FMT_MOD(info->extack,
+ "at least one VPN IP must be configured in MP mode");
+ return -EINVAL;
+ }
}
peer_id = nla_get_u32(attrs[OVPN_A_PEER_ID]);
@@ -531,6 +542,9 @@ int ovpn_nl_peer_set_doit(struct sk_buff *skb, struct genl_info *info)
spin_lock_bh(&ovpn->lock);
+ vpn_addr4 = peer->vpn_addrs.ipv4;
+ vpn_addr6 = peer->vpn_addrs.ipv6;
+
/* reject peer with conflicting VPN address */
if (attrs[OVPN_A_PEER_VPN_IPV4]) {
vpn_addr4.s_addr = nla_get_in_addr(attrs[OVPN_A_PEER_VPN_IPV4]);
@@ -543,6 +557,16 @@ int ovpn_nl_peer_set_doit(struct sk_buff *skb, struct genl_info *info)
goto addr_conflict;
}
+ /* in MP mode VPN IPs are required for selecting the right peer */
+ if (ovpn->mode == OVPN_MODE_MP &&
+ vpn_addr4.s_addr == htonl(INADDR_ANY) &&
+ ipv6_addr_any(&vpn_addr6)) {
+ NL_SET_ERR_MSG_FMT_MOD(info->extack,
+ "at least one VPN IP must be configured in MP mode");
+ ret = -EINVAL;
+ goto unlock;
+ }
+
ret = ovpn_nl_peer_modify(peer, info, attrs);
if (ret < 0)
goto unlock;
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 10/11] ovpn: reject invalid peer VPN addresses
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
` (8 preceding siblings ...)
2026-09-21 10:22 ` [PATCH net 09/11] ovpn: reject multipeer peers without " Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 11/11] selftests: ovpn: validate " Antonio Quartulli
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
In MP mode, ovpn uses peer VPN addresses as lookup keys for selecting
the peer that should receive outgoing tunnel packets. The netlink
configuration path currently accepts address values that cannot sensibly
identify a VPN peer, such as multicast, broadcast or loopback addresses.
Reject invalid peer VPN addresses when creating or updating an MP peer.
Keep accepting the unspecified address as the internal unset value,
provided that at least one VPN address family remains configured.
Fixes: 1d36a36f6d53 ("ovpn: implement peer add/get/dump/delete via netlink")
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
drivers/net/ovpn/netlink.c | 55 +++++++++++++++++++++++++++++---------
1 file changed, 42 insertions(+), 13 deletions(-)
diff --git a/drivers/net/ovpn/netlink.c b/drivers/net/ovpn/netlink.c
index e9e0f75e0443..5432bc2eb8e8 100644
--- a/drivers/net/ovpn/netlink.c
+++ b/drivers/net/ovpn/netlink.c
@@ -185,6 +185,39 @@ static sa_family_t ovpn_nl_family_get(struct nlattr *addr4,
return AF_UNSPEC;
}
+static int ovpn_nl_peer_check_vpn_addrs(const struct in_addr *addr4,
+ const struct in6_addr *addr6,
+ struct genl_info *info)
+{
+ int addr6_type;
+
+ if (addr4->s_addr == htonl(INADDR_ANY) && ipv6_addr_any(addr6)) {
+ NL_SET_ERR_MSG_MOD(info->extack,
+ "at least one VPN IP must be configured in MP mode");
+ return -EINVAL;
+ }
+
+ if (ipv4_is_multicast(addr4->s_addr) || ipv4_is_lbcast(addr4->s_addr) ||
+ ipv4_is_loopback(addr4->s_addr)) {
+ NL_SET_ERR_MSG_MOD(info->extack,
+ "VPN IPv4 address must be valid unicast or any");
+ return -EADDRNOTAVAIL;
+ }
+
+ if (!ipv6_addr_any(addr6)) {
+ addr6_type = ipv6_addr_type(addr6);
+
+ if (!(addr6_type & IPV6_ADDR_UNICAST) ||
+ (addr6_type & (IPV6_ADDR_LOOPBACK | IPV6_ADDR_COMPATv4))) {
+ NL_SET_ERR_MSG_MOD(info->extack,
+ "VPN IPv6 address must be valid unicast or any");
+ return -EADDRNOTAVAIL;
+ }
+ }
+
+ return 0;
+}
+
static int ovpn_nl_peer_precheck(struct ovpn_priv *ovpn,
struct genl_info *info,
struct nlattr **attrs)
@@ -387,12 +420,10 @@ int ovpn_nl_peer_new_doit(struct sk_buff *skb, struct genl_info *info)
vpn_addr6 =
nla_get_in6_addr(attrs[OVPN_A_PEER_VPN_IPV6]);
- if (vpn_addr4.s_addr == htonl(INADDR_ANY) &&
- ipv6_addr_any(&vpn_addr6)) {
- NL_SET_ERR_MSG_FMT_MOD(info->extack,
- "at least one VPN IP must be configured in MP mode");
- return -EINVAL;
- }
+ ret = ovpn_nl_peer_check_vpn_addrs(&vpn_addr4, &vpn_addr6,
+ info);
+ if (ret < 0)
+ return ret;
}
peer_id = nla_get_u32(attrs[OVPN_A_PEER_ID]);
@@ -558,13 +589,11 @@ int ovpn_nl_peer_set_doit(struct sk_buff *skb, struct genl_info *info)
}
/* in MP mode VPN IPs are required for selecting the right peer */
- if (ovpn->mode == OVPN_MODE_MP &&
- vpn_addr4.s_addr == htonl(INADDR_ANY) &&
- ipv6_addr_any(&vpn_addr6)) {
- NL_SET_ERR_MSG_FMT_MOD(info->extack,
- "at least one VPN IP must be configured in MP mode");
- ret = -EINVAL;
- goto unlock;
+ if (ovpn->mode == OVPN_MODE_MP) {
+ ret = ovpn_nl_peer_check_vpn_addrs(&vpn_addr4, &vpn_addr6,
+ info);
+ if (ret < 0)
+ goto unlock;
}
ret = ovpn_nl_peer_modify(peer, info, attrs);
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH net 11/11] selftests: ovpn: validate peer VPN addresses
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
` (9 preceding siblings ...)
2026-09-21 10:22 ` [PATCH net 10/11] ovpn: reject invalid peer " Antonio Quartulli
@ 2026-09-21 10:22 ` Antonio Quartulli
10 siblings, 0 replies; 13+ messages in thread
From: Antonio Quartulli @ 2026-09-21 10:22 UTC (permalink / raw)
To: netdev
Cc: Ralf Lici, Sabrina Dubroca, Jakub Kicinski, Paolo Abeni,
Andrew Lunn, David S. Miller, Eric Dumazet, Antonio Quartulli
From: Ralf Lici <ralf@mandelbit.com>
Exercise peer VPN address validation through both peer creation and
update. Check missing, unspecified, duplicate, multicast, broadcast,
loopback, IPv4-compatible and IPv4-mapped addresses.
Temporarily configure a peer with both address families to verify that
either family can be cleared while the other remains configured, then
restore the original addresses before running the existing traffic
tests.
Extend ovpn-cli peer updates with an optional VPN address and preserve
peer creation errors so the negative tests can observe rejected netlink
requests.
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
---
tools/testing/selftests/net/ovpn/common.sh | 13 ++++
tools/testing/selftests/net/ovpn/ovpn-cli.c | 54 ++++++++++-----
tools/testing/selftests/net/ovpn/test.sh | 75 ++++++++++++++++++++-
3 files changed, 123 insertions(+), 19 deletions(-)
diff --git a/tools/testing/selftests/net/ovpn/common.sh b/tools/testing/selftests/net/ovpn/common.sh
index 2d844eb3aa6e..5e9c81e885e6 100644
--- a/tools/testing/selftests/net/ovpn/common.sh
+++ b/tools/testing/selftests/net/ovpn/common.sh
@@ -136,6 +136,19 @@ ovpn_create_ns() {
ip netns add "ovpn_peer${1}"
}
+ovpn_peer_vpn_addr() {
+ local peer="$1"
+ local file
+
+ if [ "${OVPN_PROTO}" == "UDP" ]; then
+ file="${OVPN_UDP_PEERS_FILE}"
+ else
+ file="${OVPN_TCP_PEERS_FILE}"
+ fi
+
+ awk -v peer="${peer}" '$1 == peer {print $NF; exit}' "${file}"
+}
+
ovpn_setup_ns() {
local peer="ovpn_peer${1}"
local server_ns="ovpn_peer0"
diff --git a/tools/testing/selftests/net/ovpn/ovpn-cli.c b/tools/testing/selftests/net/ovpn/ovpn-cli.c
index f4effa7580c0..3b612a8a18fe 100644
--- a/tools/testing/selftests/net/ovpn/ovpn-cli.c
+++ b/tools/testing/selftests/net/ovpn/ovpn-cli.c
@@ -650,6 +650,26 @@ static int ovpn_connect(struct ovpn_ctx *ovpn)
return ret;
}
+static int ovpn_nl_put_vpn_addr(struct nl_msg *msg,
+ const struct ovpn_ctx *ovpn)
+{
+ if (!ovpn->peer_ip_set)
+ return 0;
+
+ switch (ovpn->peer_ip.in4.sin_family) {
+ case AF_INET:
+ return nla_put_u32(msg, OVPN_A_PEER_VPN_IPV4,
+ ovpn->peer_ip.in4.sin_addr.s_addr);
+ case AF_INET6:
+ return nla_put(msg, OVPN_A_PEER_VPN_IPV6,
+ sizeof(struct in6_addr),
+ &ovpn->peer_ip.in6.sin6_addr);
+ default:
+ fprintf(stderr, "Invalid family for peer address\n");
+ return -EAFNOSUPPORT;
+ }
+}
+
static int ovpn_new_peer(struct ovpn_ctx *ovpn, bool is_tcp)
{
struct nlattr *attr;
@@ -691,22 +711,9 @@ static int ovpn_new_peer(struct ovpn_ctx *ovpn, bool is_tcp)
}
}
- if (ovpn->peer_ip_set) {
- switch (ovpn->peer_ip.in4.sin_family) {
- case AF_INET:
- NLA_PUT_U32(ctx->nl_msg, OVPN_A_PEER_VPN_IPV4,
- ovpn->peer_ip.in4.sin_addr.s_addr);
- break;
- case AF_INET6:
- NLA_PUT(ctx->nl_msg, OVPN_A_PEER_VPN_IPV6,
- sizeof(struct in6_addr),
- &ovpn->peer_ip.in6.sin6_addr);
- break;
- default:
- fprintf(stderr, "Invalid family for peer address\n");
- goto nla_put_failure;
- }
- }
+ ret = ovpn_nl_put_vpn_addr(ctx->nl_msg, ovpn);
+ if (ret)
+ goto nla_put_failure;
nla_nest_end(ctx->nl_msg, attr);
@@ -732,6 +739,10 @@ static int ovpn_set_peer(struct ovpn_ctx *ovpn)
ovpn->keepalive_interval);
NLA_PUT_U32(ctx->nl_msg, OVPN_A_PEER_KEEPALIVE_TIMEOUT,
ovpn->keepalive_timeout);
+
+ ret = ovpn_nl_put_vpn_addr(ctx->nl_msg, ovpn);
+ if (ret)
+ goto nla_put_failure;
nla_nest_end(ctx->nl_msg, attr);
ret = ovpn_nl_msg_send(ctx, NULL);
@@ -1730,13 +1741,14 @@ static void usage(const char *cmd)
fprintf(stderr, "\tmark: socket FW mark value\n");
fprintf(stderr,
- "* set_peer <iface> <peer_id> <keepalive_interval> <keepalive_timeout>: set peer attributes\n");
+ "* set_peer <iface> <peer_id> <keepalive_interval> <keepalive_timeout> [vpnaddr]: set peer attributes\n");
fprintf(stderr, "\tiface: ovpn interface name\n");
fprintf(stderr, "\tpeer_id: peer ID of the peer to modify\n");
fprintf(stderr,
"\tkeepalive_interval: interval for sending ping messages\n");
fprintf(stderr,
"\tkeepalive_timeout: time after which a peer is timed out\n");
+ fprintf(stderr, "\tvpnaddr: peer VPN IP\n");
fprintf(stderr, "* del_peer <iface> <peer_id>: delete peer\n");
fprintf(stderr, "\tiface: ovpn interface name\n");
@@ -2090,6 +2102,8 @@ static int ovpn_run_cmd(struct ovpn_ctx *ovpn)
return ret;
ret = ovpn_new_peer(ovpn, false);
+ if (ret < 0)
+ return ret;
ovpn_waitbg();
break;
case CMD_NEW_MULTI_PEER:
@@ -2331,6 +2345,12 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[])
"keepalive interval value out of range\n");
return -1;
}
+
+ if (argc > 6) {
+ ret = ovpn_parse_remote(ovpn, NULL, NULL, argv[6]);
+ if (ret < 0)
+ return -1;
+ }
break;
case CMD_DEL_PEER:
if (argc < 4)
diff --git a/tools/testing/selftests/net/ovpn/test.sh b/tools/testing/selftests/net/ovpn/test.sh
index 9b5610837032..392109d5e14e 100755
--- a/tools/testing/selftests/net/ovpn/test.sh
+++ b/tools/testing/selftests/net/ovpn/test.sh
@@ -56,6 +56,76 @@ ovpn_prepare_network() {
done
}
+ovpn_new_test_peer() {
+ local peer_id="$1"
+
+ shift
+ ip netns exec ovpn_peer0 "${OVPN_CLI}" new_peer tun0 \
+ "${peer_id}" none 65000 10.10.1.2 1 "$@"
+}
+
+ovpn_set_peer_vpn_addr() {
+ ip netns exec ovpn_peer0 "${OVPN_CLI}" set_peer tun0 \
+ "$1" 60 120 "$2"
+}
+
+ovpn_run_vpn_addr_validation() {
+ local addr
+ local peer1_addr4
+ local test_peer_id=$((OVPN_NUM_PEERS + 1))
+ local test_peer_addr6="2001:db8::2"
+ # Do not include 0.0.0.0 or :: here. They are invalid on creation, but
+ # clear one address family on update and are valid if the other remains.
+ local -a invalid_addrs=(
+ "127.0.0.1"
+ "224.0.0.1"
+ "255.255.255.255"
+ "::1"
+ "::192.0.2.1"
+ "::ffff:192.0.2.1"
+ "ff02::1"
+ )
+
+ peer1_addr4=$(ovpn_peer_vpn_addr 1)
+
+ ovpn_cmd_fail "reject peer without VPN address" \
+ ovpn_new_test_peer "${test_peer_id}"
+
+ for addr in "0.0.0.0" "::" "${invalid_addrs[@]}"; do
+ ovpn_cmd_fail "reject new peer VPN address ${addr}" \
+ ovpn_new_test_peer "${test_peer_id}" "${addr}"
+ done
+
+ ovpn_cmd_fail "reject duplicate IPv4 address on peer creation" \
+ ovpn_new_test_peer "${test_peer_id}" "${peer1_addr4}"
+ ovpn_cmd_fail "reject clearing the last peer VPN address" \
+ ovpn_set_peer_vpn_addr 1 0.0.0.0
+
+ for addr in "${invalid_addrs[@]}"; do
+ ovpn_cmd_fail "reject updated peer VPN address ${addr}" \
+ ovpn_set_peer_vpn_addr 1 "${addr}"
+ done
+
+ ovpn_cmd_fail "reject duplicate IPv4 address on peer update" \
+ ovpn_set_peer_vpn_addr 2 "${peer1_addr4}"
+
+ ovpn_cmd_ok "add peer IPv6 address" \
+ ovpn_set_peer_vpn_addr 1 "${test_peer_addr6}"
+ ovpn_cmd_fail "reject duplicate IPv6 address on peer creation" \
+ ovpn_new_test_peer "${test_peer_id}" "${test_peer_addr6}"
+ ovpn_cmd_fail "reject duplicate IPv6 address on peer update" \
+ ovpn_set_peer_vpn_addr 2 "${test_peer_addr6}"
+
+ ovpn_cmd_ok "clear peer IPv4 address" \
+ ovpn_set_peer_vpn_addr 1 0.0.0.0
+ ovpn_cmd_fail "reject clearing the remaining peer IPv6 address" \
+ ovpn_set_peer_vpn_addr 1 ::
+ ovpn_cmd_ok "restore peer IPv4 address" \
+ ovpn_set_peer_vpn_addr 1 "${peer1_addr4}"
+ ovpn_cmd_ok "clear peer IPv6 address" \
+ ovpn_set_peer_vpn_addr 1 ::
+}
+
ovpn_run_basic_traffic() {
local p
local header1
@@ -293,15 +363,16 @@ trap ovpn_stage_err ERR
ktap_print_header
if [ "${OVPN_FLOAT}" == "1" ]; then
- ktap_set_plan 13
+ ktap_set_plan 14
else
- ktap_set_plan 12
+ ktap_set_plan 13
fi
ovpn_cleanup
modprobe -q ovpn || true
ovpn_run_stage "setup network topology" ovpn_prepare_network
+ovpn_run_stage "validate peer VPN addresses" ovpn_run_vpn_addr_validation
ovpn_run_stage "run baseline data traffic" ovpn_run_basic_traffic
ovpn_run_stage "run LAN traffic behind peer1" ovpn_run_lan_traffic
[ "${OVPN_FLOAT}" == "1" ] && ovpn_run_stage "run floating peer checks" \
--
2.55.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* Re: [PATCH net 01/11] ovpn: preserve IPv6 scope id for netlink peer endpoints
2026-09-21 10:22 ` [PATCH net 01/11] ovpn: preserve IPv6 scope id for netlink peer endpoints Antonio Quartulli
@ 2026-09-24 17:00 ` patchwork-bot+netdevbpf
0 siblings, 0 replies; 13+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-09-24 17:00 UTC (permalink / raw)
To: Antonio Quartulli
Cc: netdev, ralf, sd, kuba, pabeni, andrew+netdev, davem, edumazet
Hello:
This series was applied to netdev/net.git (main)
by Antonio Quartulli <antonio@openvpn.net>:
On Mon, 21 Sep 2026 12:22:02 +0200 you wrote:
> From: Ralf Lici <ralf@mandelbit.com>
>
> ovpn accepts OVPN_A_PEER_REMOTE_IPV6_SCOPE_ID and reports
> bind->remote.in6.sin6_scope_id in peer dumps, but the netlink endpoint
> parser never copied the attribute into the sockaddr_in6 used to create or
> update the peer bind.
>
> [...]
Here is the summary with links:
- [net,01/11] ovpn: preserve IPv6 scope id for netlink peer endpoints
https://git.kernel.org/netdev/net/c/7a6d08ee0f0e
- [net,02/11] ovpn: skip UDP source validation for unspecified addresses
https://git.kernel.org/netdev/net/c/77393b4d72df
- [net,03/11] ovpn: track UDP socket route key for peer dst cache
https://git.kernel.org/netdev/net/c/7c66b7a4ae80
- [net,04/11] ovpn: validate peer state before caching UDP dst
https://git.kernel.org/netdev/net/c/fa603710bdb9
- [net,05/11] ovpn: replace bind when learning local endpoint
https://git.kernel.org/netdev/net/c/aea934a221ec
- [net,06/11] ovpn: replace bind when clearing stale local source
https://git.kernel.org/netdev/net/c/7d8104988f42
- [net,07/11] ovpn: always unhash old VPN addresses before rehashing
https://git.kernel.org/netdev/net/c/b43beccb3713
- [net,08/11] ovpn: reject duplicate peer VPN addresses
https://git.kernel.org/netdev/net/c/d25e885b31a0
- [net,09/11] ovpn: reject multipeer peers without VPN addresses
https://git.kernel.org/netdev/net/c/025af3a0a892
- [net,10/11] ovpn: reject invalid peer VPN addresses
https://git.kernel.org/netdev/net/c/5940f3407b78
- [net,11/11] selftests: ovpn: validate peer VPN addresses
https://git.kernel.org/netdev/net/c/006208026819
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 13+ messages in thread
end of thread, other threads:[~2026-09-24 17:01 UTC | newest]
Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-21 10:22 [PATCH net 00/11] pull request: fixes for ovpn 2026-09-21 Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 01/11] ovpn: preserve IPv6 scope id for netlink peer endpoints Antonio Quartulli
2026-09-24 17:00 ` patchwork-bot+netdevbpf
2026-09-21 10:22 ` [PATCH net 02/11] ovpn: skip UDP source validation for unspecified addresses Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 03/11] ovpn: track UDP socket route key for peer dst cache Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 04/11] ovpn: validate peer state before caching UDP dst Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 05/11] ovpn: replace bind when learning local endpoint Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 06/11] ovpn: replace bind when clearing stale local source Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 07/11] ovpn: always unhash old VPN addresses before rehashing Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 08/11] ovpn: reject duplicate peer VPN addresses Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 09/11] ovpn: reject multipeer peers without " Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 10/11] ovpn: reject invalid peer " Antonio Quartulli
2026-09-21 10:22 ` [PATCH net 11/11] selftests: ovpn: validate " Antonio Quartulli
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox