In case of UDP links, the local or remote endpoint used to communicate
with a given peer may change without a connection restart.

Add support for learning the new address in case of change.

Signed-off-by: Antonio Quartulli <anto...@openvpn.net>
---
 drivers/net/ovpn/io.c   |   8 ++
 drivers/net/ovpn/peer.c | 213 +++++++++++++++++++++++++++++++++++++++++++++---
 drivers/net/ovpn/peer.h |   2 +
 3 files changed, 210 insertions(+), 13 deletions(-)

diff --git a/drivers/net/ovpn/io.c b/drivers/net/ovpn/io.c
index 
fcba094ed412068af88cf74baa82ec2742e0b634..49bb58868053e2d1542a8a5849c4bbc256083efc
 100644
--- a/drivers/net/ovpn/io.c
+++ b/drivers/net/ovpn/io.c
@@ -96,6 +96,7 @@ void ovpn_decrypt_post(void *data, int ret)
        struct ovpn_crypto_key_slot *ks;
        unsigned int payload_offset = 0;
        struct sk_buff *skb = data;
+       struct ovpn_socket *sock;
        struct ovpn_peer *peer;
        __be16 proto;
        __be32 *pid;
@@ -137,6 +138,13 @@ void ovpn_decrypt_post(void *data, int ret)
        /* keep track of last received authenticated packet for keepalive */
        WRITE_ONCE(peer->last_recv, ktime_get_real_seconds());
 
+       rcu_read_lock();
+       sock = rcu_dereference(peer->sock);
+       if (sock && sock->sock->sk->sk_protocol == IPPROTO_UDP)
+               /* check if this peer changed local or remote endpoint */
+               ovpn_peer_endpoints_update(peer, skb);
+       rcu_read_unlock();
+
        /* point to encapsulated IP packet */
        __skb_pull(skb, payload_offset);
 
diff --git a/drivers/net/ovpn/peer.c b/drivers/net/ovpn/peer.c
index 
45e87ac155b554044388490a403f64c777d283a6..0d8b12fd5de4cd6fe15455b435c7d6807203a825
 100644
--- a/drivers/net/ovpn/peer.c
+++ b/drivers/net/ovpn/peer.c
@@ -127,6 +127,206 @@ struct ovpn_peer *ovpn_peer_new(struct ovpn_priv *ovpn, 
u32 id)
        return peer;
 }
 
+/**
+ * ovpn_peer_reset_sockaddr - recreate binding for peer
+ * @peer: peer to recreate the binding for
+ * @ss: sockaddr to use as remote endpoint for the binding
+ * @local_ip: local IP for the binding
+ *
+ * Return: 0 on success or a negative error code otherwise
+ */
+static int ovpn_peer_reset_sockaddr(struct ovpn_peer *peer,
+                                   const struct sockaddr_storage *ss,
+                                   const void *local_ip)
+{
+       struct ovpn_bind *bind;
+       size_t ip_len;
+
+       lockdep_assert_held(&peer->lock);
+
+       /* create new ovpn_bind object */
+       bind = ovpn_bind_from_sockaddr(ss);
+       if (IS_ERR(bind))
+               return PTR_ERR(bind);
+
+       if (ss->ss_family == AF_INET) {
+               ip_len = sizeof(struct in_addr);
+       } else if (ss->ss_family == AF_INET6) {
+               ip_len = sizeof(struct in6_addr);
+       } else {
+               net_dbg_ratelimited("%s: invalid family %u for remote endpoint 
for peer %u\n",
+                                   netdev_name(peer->ovpn->dev),
+                                   ss->ss_family, peer->id);
+               kfree(bind);
+               return -EINVAL;
+       }
+
+       memcpy(&bind->local, local_ip, ip_len);
+
+       /* set binding */
+       ovpn_bind_reset(peer, bind);
+
+       return 0;
+}
+
+/* variable name __tbl2 needs to be different from __tbl1
+ * in the macro below to avoid confusing clang
+ */
+#define ovpn_get_hash_slot(_tbl, _key, _key_len) ({    \
+       typeof(_tbl) *__tbl2 = &(_tbl);                 \
+       jhash(_key, _key_len, 0) % HASH_SIZE(*__tbl2);  \
+})
+
+#define ovpn_get_hash_head(_tbl, _key, _key_len) ({            \
+       typeof(_tbl) *__tbl1 = &(_tbl);                         \
+       &(*__tbl1)[ovpn_get_hash_slot(*__tbl1, _key, _key_len)];\
+})
+
+/**
+ * ovpn_peer_endpoints_update - update remote or local endpoint for peer
+ * @peer: peer to update the remote endpoint for
+ * @skb: incoming packet to retrieve the source/destination address from
+ */
+void ovpn_peer_endpoints_update(struct ovpn_peer *peer, struct sk_buff *skb)
+{
+       struct hlist_nulls_head *nhead;
+       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;
+
+       spin_lock_bh(&peer->lock);
+       bind = rcu_dereference_protected(peer->bind,
+                                        lockdep_is_held(&peer->lock));
+       if (unlikely(!bind))
+               goto unlock;
+
+       switch (skb->protocol) {
+       case htons(ETH_P_IP):
+               /* float check */
+               if (unlikely(!ovpn_bind_skb_src_match(bind, skb))) {
+                       /* unconditionally save local endpoint in case
+                        * of float, as it may have changed as well
+                        */
+                       local_ip = &ip_hdr(skb)->daddr;
+                       sa = (struct sockaddr_in *)&ss;
+                       sa->sin_family = AF_INET;
+                       sa->sin_addr.s_addr = ip_hdr(skb)->saddr;
+                       sa->sin_port = udp_hdr(skb)->source;
+                       salen = sizeof(*sa);
+                       reset_cache = true;
+                       break;
+               }
+
+               /* if no float happened, let's double check if the local 
endpoint
+                * has changed
+                */
+               if (unlikely(bind->local.ipv4.s_addr != ip_hdr(skb)->daddr)) {
+                       net_dbg_ratelimited("%s: learning local IPv4 for peer 
%d (%pI4 -> %pI4)\n",
+                                           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;
+               }
+               break;
+       case htons(ETH_P_IPV6):
+               /* float check */
+               if (unlikely(!ovpn_bind_skb_src_match(bind, skb))) {
+                       /* unconditionally save local endpoint in case
+                        * of float, as it may have changed as well
+                        */
+                       local_ip = &ipv6_hdr(skb)->daddr;
+                       sa6 = (struct sockaddr_in6 *)&ss;
+                       sa6->sin6_family = AF_INET6;
+                       sa6->sin6_addr = ipv6_hdr(skb)->saddr;
+                       sa6->sin6_port = udp_hdr(skb)->source;
+                       sa6->sin6_scope_id = 
ipv6_iface_scope_id(&ipv6_hdr(skb)->saddr,
+                                                                skb->skb_iif);
+                       salen = sizeof(*sa6);
+                       reset_cache = true;
+                       break;
+               }
+
+               /* if no float happened, let's double check if the local 
endpoint
+                * has changed
+                */
+               if (unlikely(!ipv6_addr_equal(&bind->local.ipv6,
+                                             &ipv6_hdr(skb)->daddr))) {
+                       net_dbg_ratelimited("%s: learning local IPv6 for peer 
%d (%pI6c -> %pI6c\n",
+                                           netdev_name(peer->ovpn->dev),
+                                           peer->id, &bind->local.ipv6,
+                                           &ipv6_hdr(skb)->daddr);
+                       bind->local.ipv6 = ipv6_hdr(skb)->daddr;
+                       reset_cache = true;
+               }
+               break;
+       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;
+
+       net_dbg_ratelimited("%s: peer %d floated to %pIScp",
+                           netdev_name(peer->ovpn->dev), peer->id, &ss);
+
+       spin_unlock_bh(&peer->lock);
+
+       /* rehashing is required only in MP mode as P2P has one peer
+        * only and thus there is no hashtable
+        */
+       if (peer->ovpn->mode == OVPN_MODE_MP) {
+               spin_lock_bh(&peer->ovpn->lock);
+               spin_lock_bh(&peer->lock);
+               bind = rcu_dereference_protected(peer->bind,
+                                                lockdep_is_held(&peer->lock));
+               if (unlikely(!bind)) {
+                       spin_unlock_bh(&peer->lock);
+                       spin_unlock_bh(&peer->ovpn->lock);
+                       return;
+               }
+
+               /* This function may be invoked concurrently, therefore another
+                * float may have happened in parallel: perform rehashing
+                * using the peer->bind->remote directly as key
+                */
+
+               switch (bind->remote.in4.sin_family) {
+               case AF_INET:
+                       salen = sizeof(*sa);
+                       break;
+               case AF_INET6:
+                       salen = sizeof(*sa6);
+                       break;
+               }
+
+               /* remove old hashing */
+               hlist_nulls_del_init_rcu(&peer->hash_entry_transp_addr);
+               /* re-add with new transport address */
+               nhead = ovpn_get_hash_head(peer->ovpn->peers->by_transp_addr,
+                                          &bind->remote, salen);
+               hlist_nulls_add_head_rcu(&peer->hash_entry_transp_addr, nhead);
+               spin_unlock_bh(&peer->lock);
+               spin_unlock_bh(&peer->ovpn->lock);
+       }
+       return;
+unlock:
+       spin_unlock_bh(&peer->lock);
+}
+
 /**
  * ovpn_peer_release_rcu - RCU callback performing last peer release steps
  * @head: RCU member of the ovpn_peer
@@ -230,19 +430,6 @@ static struct in6_addr ovpn_nexthop_from_skb6(struct 
sk_buff *skb)
        return rt->rt6i_gateway;
 }
 
-/* variable name __tbl2 needs to be different from __tbl1
- * in the macro below to avoid confusing clang
- */
-#define ovpn_get_hash_slot(_tbl, _key, _key_len) ({    \
-       typeof(_tbl) *__tbl2 = &(_tbl);                 \
-       jhash(_key, _key_len, 0) % HASH_SIZE(*__tbl2);  \
-})
-
-#define ovpn_get_hash_head(_tbl, _key, _key_len) ({            \
-       typeof(_tbl) *__tbl1 = &(_tbl);                         \
-       &(*__tbl1)[ovpn_get_hash_slot(*__tbl1, _key, _key_len)];\
-})
-
 /**
  * ovpn_peer_get_by_vpn_addr4 - retrieve peer by its VPN IPv4 address
  * @ovpn: the openvpn instance to search
diff --git a/drivers/net/ovpn/peer.h b/drivers/net/ovpn/peer.h
index 
e747c4b210642db990222986a80bb37c9a0413fe..f1288734ff100ee76b0c41ebb6dc71725ea33261
 100644
--- a/drivers/net/ovpn/peer.h
+++ b/drivers/net/ovpn/peer.h
@@ -153,4 +153,6 @@ bool ovpn_peer_check_by_src(struct ovpn_priv *ovpn, struct 
sk_buff *skb,
 void ovpn_peer_keepalive_set(struct ovpn_peer *peer, u32 interval, u32 
timeout);
 void ovpn_peer_keepalive_work(struct work_struct *work);
 
+void ovpn_peer_endpoints_update(struct ovpn_peer *peer, struct sk_buff *skb);
+
 #endif /* _NET_OVPN_OVPNPEER_H_ */

-- 
2.48.1


Reply via email to