ovpn: implement keepalive mechanism

OpenVPN supports configuring a periodic keepalive packet.
message to allow the remote endpoint detect link failures.

This change implements the keepalive sending and timer expiring logic.

Signed-off-by: Antonio Quartulli <antonio@openvpn.net>
Link: https://patch.msgid.link/20250415-b4-ovpn-v26-16-577f6097b964@openvpn.net
Reviewed-by: Sabrina Dubroca <sd@queasysnail.net>
Tested-by: Oleksandr Natalenko <oleksandr@natalenko.name>
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
This commit is contained in:
Antonio Quartulli 2025-04-15 13:17:33 +02:00 committed by Paolo Abeni
parent a3aaef8cd1
commit 3ecfd9349f
6 changed files with 311 additions and 3 deletions

View File

@ -27,6 +27,33 @@
#include "skb.h"
#include "socket.h"
const unsigned char ovpn_keepalive_message[OVPN_KEEPALIVE_SIZE] = {
0x2a, 0x18, 0x7b, 0xf3, 0x64, 0x1e, 0xb4, 0xcb,
0x07, 0xed, 0x2d, 0x0a, 0x98, 0x1f, 0xc7, 0x48
};
/**
* ovpn_is_keepalive - check if skb contains a keepalive message
* @skb: packet to check
*
* Assumes that the first byte of skb->data is defined.
*
* Return: true if skb contains a keepalive or false otherwise
*/
static bool ovpn_is_keepalive(struct sk_buff *skb)
{
if (*skb->data != ovpn_keepalive_message[0])
return false;
if (skb->len != OVPN_KEEPALIVE_SIZE)
return false;
if (!pskb_may_pull(skb, OVPN_KEEPALIVE_SIZE))
return false;
return !memcmp(skb->data, ovpn_keepalive_message, OVPN_KEEPALIVE_SIZE);
}
/* Called after decrypt to write the IP packet to the device.
* This method is expected to manage/free the skb.
*/
@ -101,6 +128,9 @@ void ovpn_decrypt_post(void *data, int ret)
goto drop;
}
/* keep track of last received authenticated packet for keepalive */
WRITE_ONCE(peer->last_recv, ktime_get_real_seconds());
/* point to encapsulated IP packet */
__skb_pull(skb, payload_offset);
@ -118,6 +148,15 @@ void ovpn_decrypt_post(void *data, int ret)
goto drop;
}
if (ovpn_is_keepalive(skb)) {
net_dbg_ratelimited("%s: ping received from peer %u\n",
netdev_name(peer->ovpn->dev),
peer->id);
/* we drop the packet, but this is not a failure */
consume_skb(skb);
goto drop_nocount;
}
net_info_ratelimited("%s: unsupported protocol received from peer %u\n",
netdev_name(peer->ovpn->dev), peer->id);
goto drop;
@ -143,11 +182,12 @@ void ovpn_decrypt_post(void *data, int ret)
drop:
if (unlikely(skb))
dev_dstats_rx_dropped(peer->ovpn->dev);
kfree_skb(skb);
drop_nocount:
if (likely(peer))
ovpn_peer_put(peer);
if (likely(ks))
ovpn_crypto_key_slot_put(ks);
kfree_skb(skb);
}
/* RX path entry point: decrypt packet and forward it to the device */
@ -221,6 +261,8 @@ void ovpn_encrypt_post(void *data, int ret)
}
ovpn_peer_stats_increment_tx(&peer->link_stats, orig_len);
/* keep track of last sent packet for keepalive */
WRITE_ONCE(peer->last_sent, ktime_get_real_seconds());
/* skb passed down the stack - don't free it */
skb = NULL;
err_unlock:
@ -346,3 +388,37 @@ netdev_tx_t ovpn_net_xmit(struct sk_buff *skb, struct net_device *dev)
kfree_skb_list(skb);
return NET_XMIT_DROP;
}
/**
* ovpn_xmit_special - encrypt and transmit an out-of-band message to peer
* @peer: peer to send the message to
* @data: message content
* @len: message length
*
* Assumes that caller holds a reference to peer, which will be
* passed to ovpn_send()
*/
void ovpn_xmit_special(struct ovpn_peer *peer, const void *data,
const unsigned int len)
{
struct ovpn_priv *ovpn;
struct sk_buff *skb;
ovpn = peer->ovpn;
if (unlikely(!ovpn)) {
ovpn_peer_put(peer);
return;
}
skb = alloc_skb(256 + len, GFP_ATOMIC);
if (unlikely(!skb)) {
ovpn_peer_put(peer);
return;
}
skb_reserve(skb, 128);
skb->priority = TC_PRIO_BESTEFFORT;
__skb_put_data(skb, data, len);
ovpn_send(ovpn, skb, peer);
}

View File

@ -19,9 +19,14 @@
/* max padding required by encryption */
#define OVPN_MAX_PADDING 16
#define OVPN_KEEPALIVE_SIZE 16
extern const unsigned char ovpn_keepalive_message[OVPN_KEEPALIVE_SIZE];
netdev_tx_t ovpn_net_xmit(struct sk_buff *skb, struct net_device *dev);
void ovpn_recv(struct ovpn_peer *peer, struct sk_buff *skb);
void ovpn_xmit_special(struct ovpn_peer *peer, const void *data,
const unsigned int len);
void ovpn_encrypt_post(void *data, int ret);
void ovpn_decrypt_post(void *data, int ret);

View File

@ -170,6 +170,7 @@ static int ovpn_newlink(struct net_device *dev,
ovpn->dev = dev;
ovpn->mode = mode;
spin_lock_init(&ovpn->lock);
INIT_DELAYED_WORK(&ovpn->keepalive_work, ovpn_peer_keepalive_work);
/* Set carrier explicitly after registration, this way state is
* clearly defined.
@ -191,6 +192,7 @@ static void ovpn_dellink(struct net_device *dev, struct list_head *head)
{
struct ovpn_priv *ovpn = netdev_priv(dev);
cancel_delayed_work_sync(&ovpn->keepalive_work);
ovpn_peers_free(ovpn, NULL, OVPN_DEL_PEER_REASON_TEARDOWN);
unregister_netdevice_queue(dev, head);
}

View File

@ -40,6 +40,7 @@ struct ovpn_peer_collection {
* @peers: data structures holding multi-peer references
* @peer: in P2P mode, this is the only remote peer
* @gro_cells: pointer to the Generic Receive Offload cell
* @keepalive_work: struct used to schedule keepalive periodic job
*/
struct ovpn_priv {
struct net_device *dev;
@ -48,6 +49,7 @@ struct ovpn_priv {
struct ovpn_peer_collection *peers;
struct ovpn_peer __rcu *peer;
struct gro_cells gro_cells;
struct delayed_work keepalive_work;
};
#endif /* _NET_OVPN_OVPNSTRUCT_H_ */

View File

@ -36,6 +36,52 @@ static void unlock_ovpn(struct ovpn_priv *ovpn,
}
}
/**
* ovpn_peer_keepalive_set - configure keepalive values for peer
* @peer: the peer to configure
* @interval: outgoing keepalive interval
* @timeout: incoming keepalive timeout
*/
void ovpn_peer_keepalive_set(struct ovpn_peer *peer, u32 interval, u32 timeout)
{
time64_t now = ktime_get_real_seconds();
netdev_dbg(peer->ovpn->dev,
"scheduling keepalive for peer %u: interval=%u timeout=%u\n",
peer->id, interval, timeout);
peer->keepalive_interval = interval;
WRITE_ONCE(peer->last_sent, now);
peer->keepalive_xmit_exp = now + interval;
peer->keepalive_timeout = timeout;
WRITE_ONCE(peer->last_recv, now);
peer->keepalive_recv_exp = now + timeout;
/* now that interval and timeout have been changed, kick
* off the worker so that the next delay can be recomputed
*/
mod_delayed_work(system_wq, &peer->ovpn->keepalive_work, 0);
}
/**
* ovpn_peer_keepalive_send - periodic worker sending keepalive packets
* @work: pointer to the work member of the related peer object
*
* NOTE: the reference to peer is not dropped because it gets inherited
* by ovpn_xmit_special()
*/
static void ovpn_peer_keepalive_send(struct work_struct *work)
{
struct ovpn_peer *peer = container_of(work, struct ovpn_peer,
keepalive_work);
local_bh_disable();
ovpn_xmit_special(peer, ovpn_keepalive_message,
sizeof(ovpn_keepalive_message));
local_bh_enable();
}
/**
* ovpn_peer_new - allocate and initialize a new peer object
* @ovpn: the openvpn instance inside which the peer should be created
@ -65,6 +111,7 @@ struct ovpn_peer *ovpn_peer_new(struct ovpn_priv *ovpn, u32 id)
kref_init(&peer->refcount);
ovpn_peer_stats_init(&peer->vpn_stats);
ovpn_peer_stats_init(&peer->link_stats);
INIT_WORK(&peer->keepalive_work, ovpn_peer_keepalive_send);
ret = dst_cache_init(&peer->dst_cache, GFP_KERNEL);
if (ret < 0) {
@ -948,3 +995,162 @@ void ovpn_peers_free(struct ovpn_priv *ovpn, struct sock *sk,
break;
}
}
static time64_t ovpn_peer_keepalive_work_single(struct ovpn_peer *peer,
time64_t now,
struct llist_head *release_list)
{
time64_t last_recv, last_sent, next_run1, next_run2;
unsigned long timeout, interval;
bool expired;
spin_lock_bh(&peer->lock);
/* we expect both timers to be configured at the same time,
* therefore bail out if either is not set
*/
if (!peer->keepalive_timeout || !peer->keepalive_interval) {
spin_unlock_bh(&peer->lock);
return 0;
}
/* check for peer timeout */
expired = false;
timeout = peer->keepalive_timeout;
last_recv = READ_ONCE(peer->last_recv);
if (now < last_recv + timeout) {
peer->keepalive_recv_exp = last_recv + timeout;
next_run1 = peer->keepalive_recv_exp;
} else if (peer->keepalive_recv_exp > now) {
next_run1 = peer->keepalive_recv_exp;
} else {
expired = true;
}
if (expired) {
/* peer is dead -> kill it and move on */
spin_unlock_bh(&peer->lock);
netdev_dbg(peer->ovpn->dev, "peer %u expired\n",
peer->id);
ovpn_peer_remove(peer, OVPN_DEL_PEER_REASON_EXPIRED,
release_list);
return 0;
}
/* check for peer keepalive */
expired = false;
interval = peer->keepalive_interval;
last_sent = READ_ONCE(peer->last_sent);
if (now < last_sent + interval) {
peer->keepalive_xmit_exp = last_sent + interval;
next_run2 = peer->keepalive_xmit_exp;
} else if (peer->keepalive_xmit_exp > now) {
next_run2 = peer->keepalive_xmit_exp;
} else {
expired = true;
next_run2 = now + interval;
}
spin_unlock_bh(&peer->lock);
if (expired) {
/* a keepalive packet is required */
netdev_dbg(peer->ovpn->dev,
"sending keepalive to peer %u\n",
peer->id);
if (schedule_work(&peer->keepalive_work))
ovpn_peer_hold(peer);
}
if (next_run1 < next_run2)
return next_run1;
return next_run2;
}
static time64_t ovpn_peer_keepalive_work_mp(struct ovpn_priv *ovpn,
time64_t now,
struct llist_head *release_list)
{
time64_t tmp_next_run, next_run = 0;
struct hlist_node *tmp;
struct ovpn_peer *peer;
int bkt;
lockdep_assert_held(&ovpn->lock);
hash_for_each_safe(ovpn->peers->by_id, bkt, tmp, peer, hash_entry_id) {
tmp_next_run = ovpn_peer_keepalive_work_single(peer, now,
release_list);
if (!tmp_next_run)
continue;
/* the next worker run will be scheduled based on the shortest
* required interval across all peers
*/
if (!next_run || tmp_next_run < next_run)
next_run = tmp_next_run;
}
return next_run;
}
static time64_t ovpn_peer_keepalive_work_p2p(struct ovpn_priv *ovpn,
time64_t now,
struct llist_head *release_list)
{
struct ovpn_peer *peer;
time64_t next_run = 0;
lockdep_assert_held(&ovpn->lock);
peer = rcu_dereference_protected(ovpn->peer,
lockdep_is_held(&ovpn->lock));
if (peer)
next_run = ovpn_peer_keepalive_work_single(peer, now,
release_list);
return next_run;
}
/**
* ovpn_peer_keepalive_work - run keepalive logic on each known peer
* @work: pointer to the work member of the related ovpn object
*
* Each peer has two timers (if configured):
* 1. peer timeout: when no data is received for a certain interval,
* the peer is considered dead and it gets killed.
* 2. peer keepalive: when no data is sent to a certain peer for a
* certain interval, a special 'keepalive' packet is explicitly sent.
*
* This function iterates across the whole peer collection while
* checking the timers described above.
*/
void ovpn_peer_keepalive_work(struct work_struct *work)
{
struct ovpn_priv *ovpn = container_of(work, struct ovpn_priv,
keepalive_work.work);
time64_t next_run = 0, now = ktime_get_real_seconds();
LLIST_HEAD(release_list);
spin_lock_bh(&ovpn->lock);
switch (ovpn->mode) {
case OVPN_MODE_MP:
next_run = ovpn_peer_keepalive_work_mp(ovpn, now,
&release_list);
break;
case OVPN_MODE_P2P:
next_run = ovpn_peer_keepalive_work_p2p(ovpn, now,
&release_list);
break;
}
/* prevent rearming if the interface is being destroyed */
if (next_run > 0 &&
READ_ONCE(ovpn->dev->reg_state) == NETREG_REGISTERED) {
netdev_dbg(ovpn->dev,
"scheduling keepalive work: now=%llu next_run=%llu delta=%llu\n",
next_run, now, next_run - now);
schedule_delayed_work(&ovpn->keepalive_work,
(next_run - now) * HZ);
}
unlock_ovpn(ovpn, &release_list);
}

View File

@ -45,13 +45,20 @@
* @crypto: the crypto configuration (ciphers, keys, etc..)
* @dst_cache: cache for dst_entry used to send to peer
* @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
* @last_sent: timestamp of the last successfully sent packet
* @keepalive_timeout: seconds after which an inactive peer is considered dead
* @keepalive_recv_exp: future timestamp when the peer should expire
* @last_recv: timestamp of the last authenticated received packet
* @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)
* @lock: protects binding to peer (bind) and keepalive* fields
* @refcount: reference counter
* @rcu: used to free peer in an RCU safe way
* @release_entry: entry for the socket release list
* @keepalive_work: used to schedule keepalive sending
*/
struct ovpn_peer {
struct ovpn_priv *ovpn;
@ -91,13 +98,20 @@ struct ovpn_peer {
struct ovpn_crypto_state crypto;
struct dst_cache dst_cache;
struct ovpn_bind __rcu *bind;
unsigned long keepalive_interval;
unsigned long keepalive_xmit_exp;
time64_t last_sent;
unsigned long keepalive_timeout;
unsigned long keepalive_recv_exp;
time64_t last_recv;
struct ovpn_peer_stats vpn_stats;
struct ovpn_peer_stats link_stats;
enum ovpn_del_peer_reason delete_reason;
spinlock_t lock; /* protects bind */
spinlock_t lock; /* protects bind and keepalive* */
struct kref refcount;
struct rcu_head rcu;
struct llist_node release_entry;
struct work_struct keepalive_work;
};
/**
@ -136,4 +150,7 @@ struct ovpn_peer *ovpn_peer_get_by_dst(struct ovpn_priv *ovpn,
bool ovpn_peer_check_by_src(struct ovpn_priv *ovpn, struct sk_buff *skb,
struct ovpn_peer *peer);
void ovpn_peer_keepalive_set(struct ovpn_peer *peer, u32 interval, u32 timeout);
void ovpn_peer_keepalive_work(struct work_struct *work);
#endif /* _NET_OVPN_OVPNPEER_H_ */