Browse code

dco: Add support for float notifications

When a peer changes its UDP endpoint, the DCO module emits a
notification to userpace. The message is parsed and the relevant
information are extracted in order to process the floating operation.

Note that we preserve IPv4-mapped IPv6 addresses in userspace when
receiving a pure IPv4 address from the module, otherwise openvpn
wouldn't be able to retrieve the multi_instance using the transport
address hash table lookup.

It may happen that a netlink notification gets lost, causing us to skip
a float step. If the peer then floats back to its previous address,
userspace closes the only valid instance while trying to process the
float, leading to a segfault. To prevent this, we ignore float attempts
to an address already taken by a peer with the same peer ID.

Change-Id: I33e9272b4196c7634db2fb33a75ae4261660867f
Signed-off-by: Ralf Lici <ralf@mandelbit.com>
Acked-by: Gert Doering <gert@greenie.muc.de>
Acked-by: Antonio Quartulli <antonio@mandelbit.com>
Acked-by: Lev Stipakov <lstipakov@gmail.com>
Message-Id: <20250718122230.14008-1-gert@greenie.muc.de>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg32210.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>

Ralf Lici authored on 2025/07/18 21:22:24
Showing 10 changed files
... ...
@@ -768,6 +768,44 @@ nla_put_failure:
768 768
     return ret;
769 769
 }
770 770
 
771
+static bool
772
+ovpn_parse_float_addr(struct nlattr **attrs, struct sockaddr *out)
773
+{
774
+    if (!attrs[OVPN_A_PEER_REMOTE_PORT])
775
+    {
776
+        msg(D_DCO, "ovpn-dco: no remote port in PEER_FLOAT_NTF message");
777
+        return false;
778
+    }
779
+
780
+    if (attrs[OVPN_A_PEER_REMOTE_IPV4])
781
+    {
782
+        struct sockaddr_in *addr4 = (struct sockaddr_in *)out;
783
+        CLEAR(*addr4);
784
+        addr4->sin_family = AF_INET;
785
+        addr4->sin_port = nla_get_u16(attrs[OVPN_A_PEER_REMOTE_PORT]);
786
+        addr4->sin_addr.s_addr = nla_get_u32(attrs[OVPN_A_PEER_REMOTE_IPV4]);
787
+        return true;
788
+    }
789
+    else if (attrs[OVPN_A_PEER_REMOTE_IPV6]
790
+             && nla_len(attrs[OVPN_A_PEER_REMOTE_IPV6]) == sizeof(struct in6_addr))
791
+    {
792
+        struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)out;
793
+        CLEAR(*addr6);
794
+        addr6->sin6_family = AF_INET6;
795
+        addr6->sin6_port = nla_get_u16(attrs[OVPN_A_PEER_REMOTE_PORT]);
796
+        memcpy(&addr6->sin6_addr, nla_data(attrs[OVPN_A_PEER_REMOTE_IPV6]),
797
+               sizeof(addr6->sin6_addr));
798
+        if (attrs[OVPN_A_PEER_REMOTE_IPV6_SCOPE_ID])
799
+        {
800
+            addr6->sin6_scope_id = nla_get_u32(attrs[OVPN_A_PEER_REMOTE_IPV6_SCOPE_ID]);
801
+        }
802
+        return true;
803
+    }
804
+
805
+    msg(D_DCO, "ovpn-dco: no valid remote IP address in PEER_FLOAT_NTF message");
806
+    return false;
807
+}
808
+
771 809
 /* This function parses any netlink message sent by ovpn-dco to userspace */
772 810
 static int
773 811
 ovpn_handle_msg(struct nl_msg *msg, void *arg)
... ...
@@ -856,6 +894,45 @@ ovpn_handle_msg(struct nl_msg *msg, void *arg)
856 856
             break;
857 857
         }
858 858
 
859
+        case OVPN_CMD_PEER_FLOAT_NTF:
860
+        {
861
+            if (!attrs[OVPN_A_PEER])
862
+            {
863
+                msg(D_DCO, "ovpn-dco: no peer in PEER_FLOAT_NTF message");
864
+                return NL_STOP;
865
+            }
866
+
867
+            struct nlattr *fp_attrs[OVPN_A_PEER_MAX + 1];
868
+            if (nla_parse_nested(fp_attrs, OVPN_A_PEER_MAX, attrs[OVPN_A_PEER],
869
+                                 NULL))
870
+            {
871
+                msg(D_DCO, "ovpn-dco: can't parse peer in PEER_FLOAT_NTF messsage");
872
+                return NL_STOP;
873
+            }
874
+
875
+            if (!fp_attrs[OVPN_A_PEER_ID])
876
+            {
877
+                msg(D_DCO, "ovpn-dco: no peer-id in PEER_FLOAT_NTF message");
878
+                return NL_STOP;
879
+            }
880
+            uint32_t peerid = nla_get_u32(fp_attrs[OVPN_A_PEER_ID]);
881
+
882
+            if (!ovpn_parse_float_addr(fp_attrs, (struct sockaddr *)&dco->dco_float_peer_ss))
883
+            {
884
+                return NL_STOP;
885
+            }
886
+
887
+            struct gc_arena gc = gc_new();
888
+            msg(D_DCO_DEBUG,
889
+                "ovpn-dco: received CMD_PEER_FLOAT_NTF, ifindex: %u, peer-id %u, address: %s",
890
+                ifindex, peerid, print_sockaddr((struct sockaddr *)&dco->dco_float_peer_ss, &gc));
891
+            dco->dco_message_peer_id = (int)peerid;
892
+            dco->dco_message_type = OVPN_CMD_PEER_FLOAT_NTF;
893
+
894
+            gc_free(&gc);
895
+            break;
896
+        }
897
+
859 898
         case OVPN_CMD_KEY_SWAP_NTF:
860 899
         {
861 900
             if (!attrs[OVPN_A_KEYCONF])
... ...
@@ -34,6 +34,7 @@
34 34
 /* Defines to avoid mismatching with other platforms */
35 35
 #define OVPN_CMD_DEL_PEER OVPN_CMD_PEER_DEL_NTF
36 36
 #define OVPN_CMD_SWAP_KEYS OVPN_CMD_KEY_SWAP_NTF
37
+#define OVPN_CMD_FLOAT_PEER OVPN_CMD_PEER_FLOAT_NTF
37 38
 
38 39
 typedef enum ovpn_key_slot dco_key_slot_t;
39 40
 typedef enum ovpn_cipher_alg dco_cipher_t;
... ...
@@ -75,6 +76,7 @@ typedef struct
75 75
     int dco_message_peer_id;
76 76
     int dco_message_key_id;
77 77
     int dco_del_peer_reason;
78
+    struct sockaddr_storage dco_float_peer_ss;
78 79
     uint64_t dco_read_bytes;
79 80
     uint64_t dco_write_bytes;
80 81
 } dco_context_t;
... ...
@@ -663,6 +663,7 @@ dco_handle_overlapped_success(dco_context_t *dco, bool queued)
663 663
         dco->dco_message_peer_id = dco->notif_buf.PeerId;
664 664
         dco->dco_message_type = dco->notif_buf.Cmd;
665 665
         dco->dco_del_peer_reason = dco->notif_buf.DelPeerReason;
666
+        dco->dco_float_peer_ss = dco->notif_buf.FloatAddress;
666 667
     }
667 668
     else
668 669
     {
... ...
@@ -52,6 +52,7 @@ struct dco_context {
52 52
     int dco_message_peer_id;
53 53
     int dco_message_type;
54 54
     int dco_del_peer_reason;
55
+    struct sockaddr_storage dco_float_peer_ss;
55 56
 
56 57
     uint64_t dco_read_bytes;
57 58
     uint64_t dco_write_bytes;
... ...
@@ -1243,6 +1243,41 @@ process_incoming_link(struct context *c, struct link_socket *sock)
1243 1243
     perf_pop();
1244 1244
 }
1245 1245
 
1246
+void
1247
+extract_dco_float_peer_addr(const sa_family_t socket_family,
1248
+                            struct openvpn_sockaddr *out_osaddr,
1249
+                            const struct sockaddr *float_sa)
1250
+{
1251
+    if (float_sa->sa_family == AF_INET)
1252
+    {
1253
+        struct sockaddr_in *float4 = (struct sockaddr_in *)float_sa;
1254
+        /* DCO treats IPv4-mapped IPv6 addresses as pure IPv4. However, on a
1255
+         * dual-stack socket, we need to preserve the mapping otherwise openvpn
1256
+         * will not be able to find the peer by its transport address.
1257
+         */
1258
+        if (socket_family == AF_INET6)
1259
+        {
1260
+            out_osaddr->addr.in6.sin6_family = AF_INET6;
1261
+            out_osaddr->addr.in6.sin6_port = float4->sin_port;
1262
+
1263
+            memset(&out_osaddr->addr.in6.sin6_addr.s6_addr, 0, 10);
1264
+            out_osaddr->addr.in6.sin6_addr.s6_addr[10] = 0xff;
1265
+            out_osaddr->addr.in6.sin6_addr.s6_addr[11] = 0xff;
1266
+            memcpy(&out_osaddr->addr.in6.sin6_addr.s6_addr[12],
1267
+                   &float4->sin_addr.s_addr, sizeof(in_addr_t));
1268
+        }
1269
+        else
1270
+        {
1271
+            memcpy(&out_osaddr->addr.in4, float4, sizeof(struct sockaddr_in));
1272
+        }
1273
+    }
1274
+    else
1275
+    {
1276
+        struct sockaddr_in6 *float6 = (struct sockaddr_in6 *)float_sa;
1277
+        memcpy(&out_osaddr->addr.in6, float6, sizeof(struct sockaddr_in6));
1278
+    }
1279
+}
1280
+
1246 1281
 static void
1247 1282
 process_incoming_dco(struct context *c)
1248 1283
 {
... ...
@@ -196,6 +196,21 @@ bool process_incoming_link_part1(struct context *c, struct link_socket_info *lsi
196 196
 void process_incoming_link_part2(struct context *c, struct link_socket_info *lsi, const uint8_t *orig_buf);
197 197
 
198 198
 /**
199
+ * Transfers \c float_sa data extracted from an incoming DCO
200
+ * PEER_FLOAT_NTF to \c out_osaddr for later processing.
201
+ *
202
+ * @param socket_family - The address family of the socket
203
+ * @param out_osaddr - openvpn_sockaddr struct that will be filled the new
204
+ *      address data
205
+ * @param float_sa - The sockaddr struct containing the data received from the
206
+ *      DCO notification
207
+ */
208
+void
209
+extract_dco_float_peer_addr(sa_family_t socket_family,
210
+                            struct openvpn_sockaddr *out_osaddr,
211
+                            const struct sockaddr *float_sa);
212
+
213
+/**
199 214
  * Write a packet to the external network interface.
200 215
  * @ingroup external_multiplexer
201 216
  *
... ...
@@ -3251,6 +3251,18 @@ multi_process_float(struct multi_context *m, struct multi_instance *mi,
3251 3251
             goto done;
3252 3252
         }
3253 3253
 
3254
+        /* It doesn't make sense to let a peer float to the address it already
3255
+         * has, so we disallow it. This can happen if a DCO netlink notification
3256
+         * gets lost and we miss a floating step.
3257
+         */
3258
+        if (m1->peer_id == m2->peer_id)
3259
+        {
3260
+            msg(M_WARN, "disallowing peer %" PRIu32 " (%s) from floating to "
3261
+                "its own address (%s)",
3262
+                m1->peer_id, tls_common_name(mi->context.c2.tls_multi, false),
3263
+                mroute_addr_print(&mi->real, &gc));
3264
+            goto done;
3265
+        }
3254 3266
         msg(D_MULTI_MEDIUM, "closing instance %s", multi_instance_string(ex_mi, false, &gc));
3255 3267
         multi_close_instance(m, ex_mi, false);
3256 3268
     }
... ...
@@ -3384,6 +3396,17 @@ multi_process_incoming_dco(struct multi_context *m)
3384 3384
         {
3385 3385
             process_incoming_del_peer(m, mi, dco);
3386 3386
         }
3387
+#if defined(TARGET_LINUX) || defined(TARGET_WIN32)
3388
+        else if (dco->dco_message_type == OVPN_CMD_FLOAT_PEER)
3389
+        {
3390
+            ASSERT(mi->context.c2.link_sockets[0]);
3391
+            extract_dco_float_peer_addr(mi->context.c2.link_sockets[0]->info.af,
3392
+                                        &m->top.c2.from.dest,
3393
+                                        (struct sockaddr *)&dco->dco_float_peer_ss);
3394
+            multi_process_float(m, mi, mi->context.c2.link_sockets[0]);
3395
+            CLEAR(dco->dco_float_peer_ss);
3396
+        }
3397
+#endif /* if defined(TARGET_LINUX) || defined(TARGET_WIN32) */
3387 3398
         else if (dco->dco_message_type == OVPN_CMD_SWAP_KEYS)
3388 3399
         {
3389 3400
             tls_session_soft_reset(mi->context.c2.tls_multi);
... ...
@@ -322,7 +322,7 @@ bool multi_process_post(struct multi_context *m, struct multi_instance *mi, cons
322 322
 /**
323 323
  * Process an incoming DCO message (from kernel space).
324 324
  *
325
- * @param m            - The single \c multi_context structur.e
325
+ * @param m            - The single \c multi_context structure.
326 326
  *
327 327
  * @return
328 328
  *  - True, if the message was received correctly.
... ...
@@ -99,6 +99,7 @@ enum {
99 99
 	OVPN_CMD_KEY_SWAP,
100 100
 	OVPN_CMD_KEY_SWAP_NTF,
101 101
 	OVPN_CMD_KEY_DEL,
102
+	OVPN_CMD_PEER_FLOAT_NTF,
102 103
 
103 104
 	__OVPN_CMD_MAX,
104 105
 	OVPN_CMD_MAX = (__OVPN_CMD_MAX - 1)
... ...
@@ -149,7 +149,8 @@ typedef struct _OVPN_MP_START_VPN {
149 149
 
150 150
 typedef enum {
151 151
     OVPN_CMD_DEL_PEER,
152
-    OVPN_CMD_SWAP_KEYS
152
+    OVPN_CMD_SWAP_KEYS,
153
+    OVPN_CMD_FLOAT_PEER
153 154
 } OVPN_NOTIFY_CMD;
154 155
 
155 156
 typedef enum {
... ...
@@ -164,6 +165,7 @@ typedef struct _OVPN_NOTIFY_EVENT {
164 164
     OVPN_NOTIFY_CMD Cmd;
165 165
     int PeerId;
166 166
     OVPN_DEL_PEER_REASON DelPeerReason;
167
+    struct sockaddr_storage FloatAddress;
167 168
 } OVPN_NOTIFY_EVENT, * POVPN_NOTIFY_EVENT;
168 169
 
169 170
 typedef struct _OVPN_MP_DEL_PEER {