Attention is currently required from: plaisthos, ralf_lici, stipa.

Hello plaisthos, ralf_lici,

I'd like you to reexamine a change. Please visit

    http://gerrit.openvpn.net/c/openvpn/+/1750?usp=email

to look at the new patch set (#29).


Change subject: oob: Wrap the client probe request with tls-auth/tls-crypt
......................................................................

oob: Wrap the client probe request with tls-auth/tls-crypt

The client --server-probe sent a plaintext probe request and parsed
replies by hand, so it only worked against a server with no
control-channel wrapping.

Build a standalone wrapping context for the probe, mirroring the
server's tls_auth_standalone, and send probes and unwrap replies through
the regular control-channel path (tls_wrap_oob_standalone() and
read_control_auth()). Replies are unwrapped on a per-packet copy of the
context, as tls_pre_decrypt_lite() does. Each transmission is wrapped on
its own, as each carries its own request_id; with tls-auth or tls-crypt
that also gives each its own replay packet id. Without either, the
probe goes out in plaintext as before.

tls-crypt-v2 is not supported yet: the server only learns the client
key from the WKc carried in the TLS handshake, which an out-of-band
probe cannot provide. Such configurations skip probing and keep the
configured remote order.

The probe is wrapped with the key of the first remote we can probe --
the same entry the other probed remotes are compared against -- so
probing is declined when another one of them is keyed differently. The
key material loaded for the probe is released afterwards; init loads it
again for the connection.

Change-Id: I1f9d7b5a5ec19bf77c0c212795f583c5ba4c03ac
Signed-off-by: Lev Stipakov <[email protected]>
---
M doc/man-sections/client-options.rst
M src/openvpn/oob_client.c
2 files changed, 141 insertions(+), 27 deletions(-)


  git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/50/1750/29

diff --git a/doc/man-sections/client-options.rst 
b/doc/man-sections/client-options.rst
index 9835403..f1f004f 100644
--- a/doc/man-sections/client-options.rst
+++ b/doc/man-sections/client-options.rst
@@ -620,20 +620,23 @@
   priority group a candidate, since replies only arrive within the
   one-second probe window; the group is then ordered by weight alone.

-  The probe is currently sent without control-channel wrapping, so it only
-  works against a server configured without ``--tls-auth``,
-  ``--tls-crypt`` or ``--tls-crypt-v2``.
+  The probe carries the same control-channel wrapping as a normal
+  connection (``--tls-auth`` or ``--tls-crypt``, when configured). With
+  ``--tls-crypt-v2`` the remotes are left in their configured order,
+  because the server cannot unwrap an out-of-band probe yet.

   Only UDP remotes are probed, and only when there are at least two
   remotes; remotes reached through a SOCKS proxy are not probed. The
   probes are sent from one socket per address family, set up like the
   connection socket of the first remote that can be probed; all probed
   remotes must therefore share its local bind settings (``--local``,
-  ``--lport``, ``--bind``), otherwise probing is skipped and the
-  configured order is used. Remotes that answered are tried first, in the
-  order described above; all other remotes, including TCP ones, follow in
-  their configured order. Probing runs once per process, before the first
-  connection attempt; a reconnect or a SIGUSR1 restart does not probe again.
+  ``--lport``, ``--bind``) and control-channel key, otherwise probing is
+  skipped and the configured order is used.
+
+  Remotes that answered are tried first, in the order described above;
+  all other remotes, including TCP ones, follow in their configured
+  order. Probing runs once per process, before the first connection
+  attempt; a reconnect or a SIGUSR1 restart does not probe again.

   Probing delays the first connection attempt by up to one second, plus
   the time needed to resolve each remote. A server answers only a few
diff --git a/src/openvpn/oob_client.c b/src/openvpn/oob_client.c
index 276e21a..2ac3faa 100644
--- a/src/openvpn/oob_client.c
+++ b/src/openvpn/oob_client.c
@@ -29,6 +29,8 @@
 #include "oob_client.h"
 #include "openvpn.h"
 #include "oob.h"
+#include "init.h"
+#include "ssl.h"
 #include "ssl_pkt.h"
 #include "session_id.h"
 #include "socket.h"
@@ -69,9 +71,9 @@
     int round_start;  /* first transmission of the current round */
     uint32_t id_base; /* random, so request_ids cannot be guessed */
     /* What each probe is built from; only the request_id differs. */
+    struct tls_auth_standalone *tas;
     struct session_id *client_sid;
     struct oob_probe_request req;
-    uint8_t packet[64];                /* the probe being sent */
 #ifdef TARGET_ANDROID
     bool fd_protected[PROBE_AF_COUNT]; /* VPNService protect(), once per 
socket */
 #endif
@@ -85,20 +87,73 @@
     return (af == AF_INET6) ? PROBE_AF_V6 : PROBE_AF_V4;
 }

-/* Build a plaintext probe request packet with the given request_id:
- *   [opcode | key_id=0] [client session id] [probe request TLV]
- * This is the unauthenticated OOB wire format; adding tls-auth/tls-crypt
- * wrapping for the probe is a follow-up (it only works against a server with
- * no control-channel wrapping for now). The result points into pc->packet:
- * send it before building the next. */
+/* Build the standalone wrapping context the probe is sent with, mirroring the
+ * one the server answers it with, keyed like ce, the entry every other probed
+ * remote was checked against; without tls-auth/tls-crypt it stays in
+ * TLS_WRAP_NONE and the probe goes out in plaintext. Returns NULL (and logs)
+ * for tls-crypt-v2, which the probe cannot carry yet. */
+static struct tls_auth_standalone *
+oob_probe_init_tls_auth_standalone(struct context *c, const struct 
connection_entry *ce,
+                                   struct gc_arena *gc)
+{
+    /* tls-crypt-v2 wraps with a per-client key the server only learns from the
+     * wrapped client key (WKc) carried in the TLS handshake. An out-of-band
+     * probe carries no WKc, so the server cannot unwrap it; skip probing 
rather
+     * than send something unverifiable. */
+    if (ce->tls_crypt_v2_file)
+    {
+        msg(D_LOW, "server-probe: not supported with tls-crypt-v2; using 
configured order");
+        return NULL;
+    }
+
+    /* options.ce is not mapped yet; options_postprocess_mutate_ce() has 
already
+     * copied any global key into ce. Loaded again per-connection later. */
+    do_init_tls_wrap_key(c, ce);
+
+    struct tls_options to;
+    CLEAR(to);
+    init_tls_wrap_ctx(&to.tls_wrap, ce, c->options.tls_client, &c->c1.ks, 
&c->c1.pid_persist);
+    to.replay_window = c->options.replay_window;
+    to.replay_time = c->options.replay_time;
+
+    struct tls_auth_standalone *tas = tls_auth_standalone_init(&to, gc);
+
+    tls_init_control_channel_frame_parameters(&tas->frame, ce->tls_mtu);
+    tas->tls_wrap.work = alloc_buf_gc(BUF_SIZE(&tas->frame), gc);
+    tas->workbuf = alloc_buf_gc(BUF_SIZE(&tas->frame), gc);
+
+    return tas;
+}
+
+/* Release the probe's wrapping context and the key material
+ * do_init_tls_wrap_key() loaded for it: do_init_crypto_tls_c1() loads the same
+ * fields again later without freeing them first. */
+static void
+oob_probe_free_wrap(struct context *c, struct tls_auth_standalone *tas)
+{
+    tls_auth_standalone_free(tas);
+    free_key_ctx_bi(&c->c1.ks.tls_wrap_key);
+    CLEAR(c->c1.ks.tls_wrap_key);
+    /* the raw key bytes too: do_init_crypto_tls_c1() may never load a key over
+     * them, so they would otherwise stay resident for the process lifetime */
+    secure_memzero(&c->c1.ks.original_wrap_keydata, 
sizeof(c->c1.ks.original_wrap_keydata));
+}
+
+/* Build a wrapped probe request with the given request_id. The result points
+ * into tas's work buffers: send it before building the next. */
 static bool
 oob_probe_build(struct probe_ctx *pc, uint32_t request_id, struct buffer 
*probe)
 {
+    uint8_t data[64];
+    struct buffer payload;
+    buf_set_write(&payload, data, sizeof(data));
     pc->req.request_id = request_id;
-    buf_set_write(probe, pc->packet, sizeof(pc->packet));
-    uint8_t header = (uint8_t)(P_CONTROL_OOB_V1 << P_OPCODE_SHIFT);
-    return buf_write_u8(probe, header) && session_id_write(pc->client_sid, 
probe)
-           && oob_probe_request_write(probe, &pc->req);
+    if (!oob_probe_request_write(&payload, &pc->req))
+    {
+        return false;
+    }
+    *probe = tls_wrap_oob_standalone(&pc->tas->tls_wrap, pc->tas, 
pc->client_sid, &payload);
+    return BLEN(probe) > 0;
 }

 /* Was addr already probed in the current round? */
@@ -127,6 +182,7 @@
     struct buffer probe;
     if (!oob_probe_build(pc, pc->id_base + (uint32_t)pc->n_sends, &probe))
     {
+        msg(D_LOW, "server-probe: could not build probe packet");
         return false;
     }
     openvpn_gettimeofday(&s->sent_at, NULL);
@@ -201,6 +257,22 @@
            && str_equal_or_both_null(port_a, port_b);
 }

+/* Do a and b wrap the control channel with the same key (--tls-auth,
+ * --tls-crypt, --tls-crypt-v2 and key direction)? Probes are wrapped with the
+ * first probeable entry's key, so a server keyed differently could never 
answer
+ * them. */
+static bool
+oob_probe_same_wrap(const struct connection_entry *a, const struct 
connection_entry *b)
+{
+    return str_equal_or_both_null(a->tls_auth_file, b->tls_auth_file)
+           && a->tls_auth_file_inline == b->tls_auth_file_inline
+           && a->key_direction == b->key_direction
+           && str_equal_or_both_null(a->tls_crypt_file, b->tls_crypt_file)
+           && a->tls_crypt_file_inline == b->tls_crypt_file_inline
+           && str_equal_or_both_null(a->tls_crypt_v2_file, 
b->tls_crypt_v2_file)
+           && a->tls_crypt_v2_file_inline == b->tls_crypt_v2_file_inline;
+}
+
 /* Resolve the local bind address of ce's connection socket; NULL with 
--nobind.
  * Fatal on failure, like the link socket. */
 static struct addrinfo *
@@ -295,7 +367,7 @@
 /* Parse one received datagram as a probe reply and, if valid and answering one
  * of the probes we sent, record the reply in results. */
 static void
-oob_probe_handle_reply(const struct probe_ctx *pc, const uint8_t *data, int 
len,
+oob_probe_handle_reply(const struct probe_ctx *pc, uint8_t *data, int len,
                        const struct openvpn_sockaddr *from, const struct 
oob_probe_target *targets,
                        struct oob_probe_result *results, int n)
 {
@@ -307,7 +379,19 @@

     struct buffer buf;
     buf_set_read(&buf, data, (size_t)len);
-    buf_advance(&buf, 1 + SID_SIZE); /* skip opcode + server session id */
+
+    /* Unwrap the reply with the same control-channel path the server used to
+     * wrap it: this verifies the tls-auth HMAC / decrypts tls-crypt, and (in 
all
+     * modes) strips the opcode + server session id, leaving buf at the OOB
+     * message. read_control_auth() mutates the wrapping context, so we work 
on a
+     * per-packet copy (as tls_pre_decrypt_lite() does on the server). The peer
+     * address is only used for log messages, and tls_options only for
+     * tls-crypt-v2 metadata checks, so both are passed as NULL. */
+    struct tls_wrap_ctx wrap = pc->tas->tls_wrap;
+    if (!read_control_auth(&buf, &wrap, NULL, NULL))
+    {
+        return; /* not for us, or failed authentication */
+    }

     struct oob_probe_reply reply;
     if (!oob_probe_reply_find(&buf, &reply))
@@ -537,11 +621,20 @@
             tmpl = ce;
             continue;
         }
+        const char *differs = NULL;
         if (!oob_probe_same_bind(tmpl, ce, &c->options))
         {
-            msg(D_LOW, "server-probe: %s:%s binds differently from %s:%s;"
+            differs = "binds";
+        }
+        else if (!oob_probe_same_wrap(tmpl, ce))
+        {
+            differs = "wraps the control channel";
+        }
+        if (differs)
+        {
+            msg(D_LOW, "server-probe: %s:%s %s differently from %s:%s;"
                        " not probing, using configured order",
-                ce->remote, ce->remote_port, tmpl->remote, tmpl->remote_port);
+                ce->remote, ce->remote_port, differs, tmpl->remote, 
tmpl->remote_port);
             gc_free(&gc);
             return;
         }
@@ -553,12 +646,25 @@
         return;
     }

+    /* Wrapping context for the probe (tls-auth/tls-crypt, or plaintext if
+     * neither). NULL means this configuration cannot be probed; keep the
+     * configured order. */
+    struct tls_auth_standalone *tas = oob_probe_init_tls_auth_standalone(c, 
tmpl, &gc);
+    if (!tas)
+    {
+        gc_free(&gc);
+        return;
+    }
+
     struct probe_ctx pc = { .sd = { SOCKET_UNDEFINED, SOCKET_UNDEFINED } };
     struct oob_probe_target *targets = gc_malloc(sizeof(*targets) * l->len, 
true, &gc);
     struct oob_probe_result *results = gc_malloc(sizeof(*results) * l->len, 
true, &gc);

-    /* Every probe carries the same session id and timestamp; each
-     * transmission gets a request_id of its own. */
+    /* Every probe is a single probe request TLV, wrapped (or sent in
+     * plaintext) like any other control packet, with the client session id as
+     * the sender session id. Each transmission is built on its own, as each
+     * carries its own request_id. */
+    pc.tas = tas;
     pc.client_sid = &client_sid;
     pc.id_base = (uint32_t)get_random();
     pc.req = (struct oob_probe_request){
@@ -566,8 +672,11 @@
         .flags = 0,
     };
 
-    msg(D_LOW, "server-probe: probing %d remote(s) with a %d ms window", 
l->len,
-        OOB_PROBE_WINDOW_MS);
+    const char *wrap_name = (tas->tls_wrap.mode == TLS_WRAP_CRYPT)  ? 
"tls-crypt"
+                            : (tas->tls_wrap.mode == TLS_WRAP_AUTH) ? 
"tls-auth"
+                                                                    : "none 
(plaintext)";
+    msg(D_LOW, "server-probe: probing %d remote(s) with a %d ms window, 
control-channel wrapping: %s",
+        l->len, OOB_PROBE_WINDOW_MS, wrap_name);

     /* Resolve every remote first, so only the address families actually in use
      * get a probe socket. */
@@ -614,6 +723,7 @@
         {
             freeaddrinfo(bind_local);
         }
+        oob_probe_free_wrap(c, tas);
         gc_free(&gc);
         return;
     }
@@ -707,5 +817,6 @@
     msg(M_INFO, "server-probe: %d of %d probed remote(s) answered; connecting 
best-first",
         responded, sent_count);

+    oob_probe_free_wrap(c, tas);
     gc_free(&gc);
 }

--
To view, visit http://gerrit.openvpn.net/c/openvpn/+/1750?usp=email
To unsubscribe, or for help writing mail filters, visit 
http://gerrit.openvpn.net/settings?usp=email

Gerrit-MessageType: newpatchset
Gerrit-Project: openvpn
Gerrit-Branch: master
Gerrit-Change-Id: I1f9d7b5a5ec19bf77c0c212795f583c5ba4c03ac
Gerrit-Change-Number: 1750
Gerrit-PatchSet: 29
Gerrit-Owner: stipa <[email protected]>
Gerrit-Reviewer: plaisthos <[email protected]>
Gerrit-Reviewer: ralf_lici <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>
Gerrit-Attention: ralf_lici <[email protected]>
Gerrit-Attention: stipa <[email protected]>
Gerrit-Attention: plaisthos <[email protected]>
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel

Reply via email to