Attention is currently required from: plaisthos.
Hello plaisthos,
I'd like you to reexamine a change. Please visit
http://gerrit.openvpn.net/c/openvpn/+/1770?usp=email
to look at the new patch set (#18).
Change subject: oob: start the client handshake from the server probe
......................................................................
oob: start the client handshake from the server probe
When the best remote answered a probe and advertised a connect_lifetime,
its reply also served as the server's HARD_RESET: its session id is a
valid stateless SYN-cookie. The client therefore starts the handshake
from that reply instead of running its own reset exchange.
Note this does not shorten the handshake itself -- the probe and its
reply take the place of the two reset packets rather than removing them.
The saving is one RTT compared with probing and then connecting; a
client that does not probe is unaffected.
- oob_client.c: when the winner advertised a connect_lifetime, hand
its probe socket, the captured server session id (cookie), our probe
session id, the pinned responder address, and the resend-wkc flag to
the connection via c2; relinquish that socket (do not close it);
gate off for dco-win, which has no user-to-kernel socket handoff
(probing still works, only starting from the reply is skipped).
- ssl.c session_skip_to_pre_start_client(): seed our session id to the
probe's (the cookie is an HMAC over it), the remote session id to
the cookie, ack the phantom server reset (id 0) so the third packet
carries the cookie, set CO_RESEND_WKC for tls-crypt-v2, and drop to
S_PRE_START so tls_process() promotes to S_START and sends the
ClientHello (no HARD_RESET sent).
- init.c: invoke the client skip after tls_multi_init_finalize when
adopting, and count the probe reply as the initial packet received
(n_sessions++, as the server does before its own
session_skip_to_pre_start) so check_server_poll_timeout() does not
restart the connected session.
Three things guard the shortcut. The reply's session id is rejected
unless it is defined: it becomes the remote session id, and the phantom
ACK means reliable_ack_write() would assert on an all-zero one, so a
server -- or, without tls-auth/tls-crypt, anyone who answers first --
could otherwise kill the client with a single packet.
The probe socket and the cookie belong to the remote that answered, so
both are given up unless next_connection_entry() selected that same
remote. The entry is mapped by value and the management interface may
rewrite the copy, so the check compares the resulting host, port and
proxy settings rather than which entry was picked.
Finally, the server honours its reply only for the advertised
connect_lifetime, which a passphrase or token prompt during init can
outlast; the shortcut is dropped when it has elapsed. Steps after that
point fall to the short fallback deadline instead.
Change-Id: I454d5040cbad4d373ee4f90b8d683200d2a4c0e4
Signed-off-by: Lev Stipakov <[email protected]>
---
M Changes.md
M doc/man-sections/client-options.rst
M src/openvpn/init.c
M src/openvpn/oob.h
M src/openvpn/oob_client.c
M src/openvpn/openvpn.h
M src/openvpn/ssl.c
M src/openvpn/ssl.h
8 files changed, 281 insertions(+), 12 deletions(-)
git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/70/1770/18
diff --git a/Changes.md b/Changes.md
index cb85456..fb92bef 100644
--- a/Changes.md
+++ b/Changes.md
@@ -10,6 +10,14 @@
weight with DNS-SRV-like semantics. Servers advertise these values with
`--server-probe-reply`.
+* **Probe exchange doubles as the start of the handshake**
+
+ When a probing client picks a server that advertises support for it, the
probe
+ and its reply take the place of the client's and server's reset packets, so
the
+ client does not need a separate reset exchange before the handshake. This
saves
+ one round trip compared with probing and then connecting, and needs no extra
+ configuration.
+
## User-visible Changes
* **Parsing Distinguished Names in certificates**
diff --git a/doc/man-sections/client-options.rst
b/doc/man-sections/client-options.rst
index 74ae59e..23684a5 100644
--- a/doc/man-sections/client-options.rst
+++ b/doc/man-sections/client-options.rst
@@ -637,8 +637,34 @@
whose timestamp is within its ``--hand-window`` of its own clock, so a
client whose clock is badly wrong receives no replies and keeps the
configured order. Ordering by probe replaces any order that
- ``--remote-random`` produced. See ``--server-probe-reply`` for the
- server side.
+ ``--remote-random`` produced.
+
+ When the winning server supports it, the probe exchange also starts the
+ handshake: its reply stands in for the server's reset packet, so the
+ client sends no reset of its own and the connection is established one
+ round trip sooner. This has consequences worth knowing:
+
+ - the connection reuses the probe's socket, and therefore its source
+ port, which matters where firewall or NAT rules pin a port;
+ - the connection is pinned to the address that answered, rather than
+ trying that remote's resolved addresses in turn;
+ - the server honours its reply only for a limited time (see
+ ``--server-probe-reply``), so an unusually slow start-up, such as a
+ private key passphrase or token prompt, can outlast it; the client
+ then falls back to a normal handshake;
+ - a handshake started this way is given only a few seconds to draw a
+ response, rather than ``--hand-window``. If none arrives the client
+ logs a key negotiation timeout, restarts the attempt and moves on to
+ the next address or remote, so that server is not retried during this
+ cycle. The server answered a probe moments earlier, so silence means
+ it did not accept the reply as a reset, which happens when a load
+ balancer sends the probe and the handshake to different instances,
+ when NAT changes the source port, or when the server rotated its
+ session id key.
+
+ The handshake shortcut is not used on Windows while DCO is active.
+
+ See ``--server-probe-reply`` for the server side.
--static-challenge args
Enable static challenge/response protocol
diff --git a/src/openvpn/init.c b/src/openvpn/init.c
index e506dfe..a683be4 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -3474,6 +3474,43 @@
}
}
+/* Give up the probe socket and the handshake shortcut that came with it. */
+static void
+oob_probe_drop_adoption(struct context *c)
+{
+ openvpn_close_socket(c->c2.oob_probe_sd);
+ c->c2.oob_probe_sd = SOCKET_UNDEFINED;
+ c->c2.oob_probe_adopt = false;
+}
+
+/* The probe socket and the handshake shortcut belong to the remote that
+ * answered: give them up if next_connection_entry() picked a different one. */
+static void
+oob_probe_confirm_adoption(struct context *c)
+{
+ if (!c->c2.oob_probe_adopt)
+ {
+ return;
+ }
+
+ /* next_connection_entry() maps the entry by value and the management
+ * interface may then rewrite that copy, so compare what we ended up with,
+ * not just which entry was picked. */
+ const struct connection_list *l = c->options.connection_list;
+ const struct connection_entry *won = c->c2.oob_probe_ce;
+ const struct connection_entry *ce = &c->options.ce;
+ if (l->array[l->current] == won && proto_is_udp(ce->proto) &&
!ce->socks_proxy_server
+ && !ce->http_proxy_options && ce->remote && won->remote
+ && streq(ce->remote, won->remote) && ce->remote_port &&
won->remote_port
+ && streq(ce->remote_port, won->remote_port))
+ {
+ return;
+ }
+
+ msg(D_LOW, "server-probe: a different remote was selected, not adopting
the probe socket");
+ oob_probe_drop_adoption(c);
+}
+
static void
do_init_frame_tls(struct context *c)
{
@@ -3486,6 +3523,32 @@
/* Keep the max mtu also in the frame of tls multi so it can access
* it in push_peer_info */
c->c2.tls_multi->opt.frame.tun_max_mtu = c->c2.frame.tun_max_mtu;
+
+ /* OOB server probe: the reply already served as the server's
HARD_RESET
+ * (it carried a valid SYN-cookie), so the handshake starts from it
and we
+ * send no reset of our own. Count it as the initial packet received,
as
+ * the server does, so check_server_poll_timeout() does not restart us.
+ * The reply only stands for as long as the server advertised, which a
+ * passphrase or token prompt can outlast. */
+ update_time();
+ if (c->c2.oob_probe_adopt
+ && now >= c->c2.oob_probe_reply_at +
c->c2.oob_probe_connect_lifetime)
+ {
+ msg(D_LOW,
+ "server-probe: the reply's connect-lifetime (%d s) elapsed
before the handshake"
+ " could start; connecting normally",
+ c->c2.oob_probe_connect_lifetime);
+ oob_probe_drop_adoption(c);
+ }
+
+ if (c->c2.oob_probe_adopt)
+ {
+ c->c2.tls_multi->n_sessions++;
+
session_skip_to_pre_start_client(&c->c2.tls_multi->session[TM_ACTIVE],
+ &c->c2.oob_probe_client_sid,
+ &c->c2.oob_probe_server_sid,
&c->c2.oob_probe_remote,
+ c->c2.oob_probe_resend_wkc);
+ }
}
if (c->c2.tls_auth_standalone)
{
@@ -4491,6 +4554,8 @@
/* map in current connection entry */
next_connection_entry(c);
+ oob_probe_confirm_adoption(c);
+
/* should we disable paging? */
if (c->first_time && options->mlock)
{
diff --git a/src/openvpn/oob.h b/src/openvpn/oob.h
index c12c5c4..a711451 100644
--- a/src/openvpn/oob.h
+++ b/src/openvpn/oob.h
@@ -192,8 +192,12 @@
{
int index;
bool responded;
- unsigned int rtt_ms; /* probe round-trip time in ms (responders
only) */
- struct oob_probe_reply reply; /* the values the server advertised */
+ unsigned int rtt_ms; /* probe round-trip time in ms (responders only) */
+ /* Captured from the packet and its reply TLV (responders only): */
+ struct session_id server_sid; /* the packet's own session id = server
SYN-cookie */
+ time_t received_at; /* when the reply arrived, for its
connect_lifetime */
+ struct openvpn_sockaddr responder; /* address that answered (pin the
connection to it) */
+ struct oob_probe_reply reply; /* the values the server advertised */
};
/* Where the client probed one connection entry: its resolved addresses. */
diff --git a/src/openvpn/oob_client.c b/src/openvpn/oob_client.c
index 3072d3c..f8b3d02 100644
--- a/src/openvpn/oob_client.c
+++ b/src/openvpn/oob_client.c
@@ -38,6 +38,7 @@
#include "otime.h"
#include "fdmisc.h"
#include "crypto.h"
+#include "dco.h"
#include "error.h"
#include "memdbg.h"
@@ -403,6 +404,20 @@
return;
}
+ /* The reply's own session id (the server's stateless SYN-cookie) follows
the
+ * opcode byte. Capture it before read_control_auth() strips it: a client
may
+ * reuse it to start the handshake from this reply (the connect_lifetime
+ * advertisement). */
+ struct session_id server_sid;
+ memcpy(server_sid.id, data + 1, SID_SIZE);
+ if (!session_id_defined(&server_sid))
+ {
+ /* No usable cookie, and starting a handshake from it would assert in
+ * reliable_ack_write(). tls_pre_decrypt() rejects an undefined session
+ * id the same way. */
+ return;
+ }
+
struct buffer buf;
buf_set_read(&buf, data, (size_t)len);
@@ -448,6 +463,9 @@
results[i].responded = true;
results[i].rtt_ms = (ms > 0) ? (unsigned int)ms : 0;
+ results[i].server_sid = server_sid;
+ results[i].received_at = rcv.tv_sec;
+ results[i].responder = *from; /* pin the connection to the address
that answered */
results[i].reply = reply;
i = oob_probe_next_target_at(from, targets, results, n, i + 1);
@@ -822,11 +840,6 @@
oob_probe_collect(&pc, &probe, &client_sid, &tas->tls_wrap, targets,
results, l->len,
c->sig);
}
- oob_probe_sockets_close(&pc);
- if (bind_local)
- {
- freeaddrinfo(bind_local);
- }
/* Log each remote's outcome while results[i] still maps to array[i]. */
int responded = 0;
@@ -842,10 +855,10 @@
int margin = oob_effective_margin(&results[i], client_margin);
const char *margin_src = client_margin >= 0 ? "client" :
"server-advertised";
msg(D_LOW,
- "server-probe: %s:%s answered (priority %d, weight %d, rtt %u
ms;"
- " latency margin %d ms [%s])",
+ "server-probe: %s:%s answered (priority %d, weight %d,
connect-lifetime %d s,"
+ " rtt %u ms; latency margin %d ms [%s])",
ce->remote, ce->remote_port, results[i].reply.priority,
results[i].reply.weight,
- results[i].rtt_ms, margin, margin_src);
+ results[i].reply.connect_lifetime, results[i].rtt_ms, margin,
margin_src);
}
else
{
@@ -867,6 +880,81 @@
msg(M_INFO, "server-probe: %d of %d probed remote(s) answered; connecting
best-first",
responded, sent_count);
+ /* If the winner advertised a connect_lifetime, its reply also served as
the
+ * server's reset: hand its probe socket and the captured cookie to the
+ * connection, which then starts the handshake from that reply (see
+ * session_skip_to_pre_start_client). Reusing that socket keeps the source
+ * IP+port the cookie is bound to. dco-win cannot hand a socket to the
+ * kernel, so it only gets the probe ordering. */
+
+ bool probe_start = results[0].responded &&
results[0].reply.connect_lifetime > 0;
+ bool wkc_gate = false;
+ /* Our first packet carries the WKc only if the server asked for it;
without
+ * that a stateless tls-crypt-v2 server cannot decrypt it. */
+ if (probe_start && tas->tls_wrap.tls_crypt_v2_wkc
+ && !(results[0].reply.flags & OOB_PROBE_REPLY_FLAG_RESEND_WKC))
+ {
+ probe_start = false;
+ wkc_gate = true;
+ }
+ bool dco_win_gate = false;
+#if defined(_WIN32)
+ if (dco_enabled(&c->options))
+ {
+ probe_start = false;
+ dco_win_gate = true;
+ }
+#endif
+ if (probe_start)
+ {
+ const int af_idx =
probe_af_index(results[0].responder.addr.sa.sa_family);
+ c->c2.oob_probe_sd = pc.sd[af_idx];
+ pc.sd[af_idx] = SOCKET_UNDEFINED; /* relinquish: the connection owns
it now */
+
+ c->c2.oob_probe_remote = results[0].responder;
+ c->c2.oob_probe_client_sid = client_sid;
+ c->c2.oob_probe_server_sid = results[0].server_sid;
+ c->c2.oob_probe_resend_wkc =
+ (results[0].reply.flags & OOB_PROBE_REPLY_FLAG_RESEND_WKC) != 0;
+ c->c2.oob_probe_adopt = true;
+ c->c2.oob_probe_ce = l->array[0];
+ c->c2.oob_probe_reply_at = results[0].received_at;
+ c->c2.oob_probe_connect_lifetime = results[0].reply.connect_lifetime;
+
+ msg(D_LOW, "server-probe: starting handshake from probe reply of %s:%s"
+ " (connect-lifetime %d s)",
+ l->array[0]->remote, l->array[0]->remote_port,
results[0].reply.connect_lifetime);
+ }
+ else if (results[0].responded)
+ {
+ /* A server answered but we won't start the handshake from it -- say
why. */
+ if (dco_win_gate)
+ {
+ msg(D_LOW, "server-probe: cannot start the handshake from a probe
reply"
+ " with dco-win;"
+ " using a full handshake");
+ }
+ else if (wkc_gate)
+ {
+ msg(D_LOW, "server-probe: %s:%s did not ask for the wrapped client
key;"
+ " using a full handshake",
+ l->array[0]->remote, l->array[0]->remote_port);
+ }
+ else if (results[0].reply.connect_lifetime == 0)
+ {
+ msg(D_LOW, "server-probe: %s:%s did not advertise a
connect-lifetime"
+ " (connect-lifetime 0); using a full handshake",
+ l->array[0]->remote, l->array[0]->remote_port);
+ }
+ }
+
+ /* Close any probe sockets we did not hand off to the connection. */
+ oob_probe_sockets_close(&pc);
+
+ if (bind_local)
+ {
+ freeaddrinfo(bind_local);
+ }
oob_probe_free_wrap(c, tas);
gc_free(&gc);
}
diff --git a/src/openvpn/openvpn.h b/src/openvpn/openvpn.h
index 09c6a3c..4278cea 100644
--- a/src/openvpn/openvpn.h
+++ b/src/openvpn/openvpn.h
@@ -248,8 +248,14 @@
* address probed) as the outgoing address, so the source IP+port and the
* destination match what the server's handshake cookie is bound to. */
bool oob_probe_adopt;
+ const struct connection_entry *oob_probe_ce; /* the entry the probe won on
*/
+ time_t oob_probe_reply_at; /* when its reply arrived */
+ int oob_probe_connect_lifetime; /* how long that reply stays
usable */
socket_descriptor_t oob_probe_sd;
struct openvpn_sockaddr oob_probe_remote;
+ struct session_id oob_probe_client_sid; /* our probe session id (cookie
was minted over it) */
+ struct session_id oob_probe_server_sid; /* the reply's session id =
server cookie to echo back */
+ bool oob_probe_resend_wkc; /* reply asked to complete with
CONTROL_WKC_V1 (v2) */
struct link_socket_actual *to_link_addr; /* IP address of remote */
struct link_socket_actual from; /* address of incoming datagram */
diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c
index 7184a2c..ec6c212 100644
--- a/src/openvpn/ssl.c
+++ b/src/openvpn/ssl.c
@@ -2587,6 +2587,61 @@
return session_move_pre_start(session, ks, true);
}
+bool
+session_skip_to_pre_start_client(struct tls_session *session, const struct
session_id *client_sid,
+ const struct session_id *server_sid,
+ const struct openvpn_sockaddr *remote, bool
resend_wkc)
+{
+ struct key_state *ks = &session->key[KS_PRIMARY];
+
+ /* Our session id must be the one used for the probe: the server's cookie
+ * (server_sid) is an HMAC over it, and the server re-derives and checks it
+ * when it validates our third packet. */
+ session->session_id = *client_sid;
+ ks->session_id_remote = *server_sid;
+
+ struct link_socket_actual act = { 0 };
+ act.dest = *remote;
+ ks->remote_addr = act;
+ session->untrusted_addr = act;
+ session->burst = true;
+
+ /* tls-crypt-v2: the stateless server discarded the WKc after the probe, so
+ * complete the handshake with P_CONTROL_WKC_V1 (drives
control_packet_needs_wkc).
+ * The flag comes from the reply, so only act on it when this client has a
+ * WKc to resend. */
+ if (resend_wkc && session->tls_wrap.tls_crypt_v2_wkc)
+ {
+ ks->crypto_options.flags |= CO_RESEND_WKC;
+ }
+
+ /* We never received the server's HARD_RESET (id 0) -- the probe reply
stood
+ * in for it. Acknowledge that phantom id 0 so our third packet carries an
+ * ACK together with the server session id (the cookie):
reliable_ack_write()
+ * only emits that session id when at least one ACK is present, and the
server
+ * needs it to validate the stateless cookie. */
+ reliable_ack_acknowledge_packet_id(ks->rec_ack, 0);
+
+ /* Skip one (RESET) packet in each direction, so ids start at 1 (see
+ * session_skip_to_pre_start). */
+ ks->rec_reliable->packet_id = 1;
+ session->tls_wrap.opt.packet_id.send.id = 1;
+
+ /* Do not send our own HARD_RESET. Unlike the server's skip, the client
must
+ * send first: the stateless server only replies once it sees our third
+ * packet. session_move_pre_start(skip=true) leaves us in S_PRE_START_SKIP,
+ * which only advances when a packet is *received*; instead drop to
+ * S_PRE_START with an empty send-reliable (the initial packet was
generated
+ * and immediately deleted), so tls_process() promotes us to S_START and
+ * sends the ClientHello without waiting for a server reset. */
+ if (!session_move_pre_start(session, ks, true))
+ {
+ return false;
+ }
+ ks->state = S_PRE_START;
+ return true;
+}
+
/**
* Parses the TLVs (type, length, value) in the early negotiation
*/
diff --git a/src/openvpn/ssl.h b/src/openvpn/ssl.h
index 7ddf965..a534007 100644
--- a/src/openvpn/ssl.h
+++ b/src/openvpn/ssl.h
@@ -576,4 +576,21 @@
bool session_skip_to_pre_start(struct tls_session *session, struct
tls_pre_decrypt_state *state,
struct link_socket_actual *from);
+/**
+ * Client counterpart of session_skip_to_pre_start(): start the handshake from
a
+ * server session id (SYN-cookie) learned earlier from an OOB server probe
+ * reply, which already served as the server's HARD_RESET, so we send none.
+ *
+ * @param session the (client) TLS session to fast-forward
+ * @param client_sid session id used for the probe (the cookie is an HMAC
over it)
+ * @param server_sid the server's cookie, echoed back to complete the
handshake
+ * @param remote the probed (pinned) server address to connect to
+ * @param resend_wkc true to complete with P_CONTROL_WKC_V1 (tls-crypt-v2)
+ * @return true if the session was fast-forwarded, false on failure
+ */
+bool session_skip_to_pre_start_client(struct tls_session *session,
+ const struct session_id *client_sid,
+ const struct session_id *server_sid,
+ const struct openvpn_sockaddr *remote,
bool resend_wkc);
+
#endif /* ifndef OPENVPN_SSL_H */
--
To view, visit http://gerrit.openvpn.net/c/openvpn/+/1770?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: I454d5040cbad4d373ee4f90b8d683200d2a4c0e4
Gerrit-Change-Number: 1770
Gerrit-PatchSet: 18
Gerrit-Owner: stipa <[email protected]>
Gerrit-Reviewer: plaisthos <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>
Gerrit-Attention: plaisthos <[email protected]>
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel