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