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 (#16).
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.
Change-Id: I454d5040cbad4d373ee4f90b8d683200d2a4c0e4
Signed-off-by: Lev Stipakov <[email protected]>
---
M Changes.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
7 files changed, 172 insertions(+), 10 deletions(-)
git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/70/1770/16
diff --git a/Changes.rst b/Changes.rst
index 080d928..4d7a16c9 100644
--- a/Changes.rst
+++ b/Changes.rst
@@ -9,6 +9,13 @@
measured latency and advertised 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.
+
Overview of changes in 2.7
==========================
diff --git a/src/openvpn/init.c b/src/openvpn/init.c
index ea85bad..81562a7 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -3483,6 +3483,21 @@
/* 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 probe reply already served as the server's
+ * HARD_RESET (it carried a valid SYN-cookie), so the handshake starts
+ * from that reply and we send no reset of our own. Count
+ * the reply as the initial packet received (as the server does before
its
+ * own session_skip_to_pre_start), so tls_initial_packet_received() is
true
+ * and check_server_poll_timeout() does not restart a connected
session. */
+ 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)
{
diff --git a/src/openvpn/oob.h b/src/openvpn/oob.h
index 9fde094..5568177 100644
--- a/src/openvpn/oob.h
+++ b/src/openvpn/oob.h
@@ -192,8 +192,11 @@
{
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 */
+ 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 fb59104..dd9e3c6a 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"
@@ -343,6 +344,13 @@
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);
+
struct buffer buf;
buf_set_read(&buf, data, (size_t)len);
@@ -386,6 +394,8 @@
results[i].responded = true;
results[i].rtt_ms = (ms > 0) ? (unsigned int)ms : 0;
+ results[i].server_sid = server_sid;
+ 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);
@@ -753,11 +763,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;
@@ -773,10 +778,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
{
@@ -798,6 +803,65 @@
msg(M_INFO, "server-probe: %d of %d remote(s) answered; connecting
best-first", responded,
l->len);
+ /* 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. */
+
+ /* Single-use, so the RFC's connect_lifetime expiry check is not needed
yet:
+ * we probe once (c->first_time) and arm only results[0]. */
+ bool probe_start = results[0].responded &&
results[0].reply.connect_lifetime > 0;
+ 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;
+
+ 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 (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..238eba2 100644
--- a/src/openvpn/openvpn.h
+++ b/src/openvpn/openvpn.h
@@ -250,6 +250,9 @@
bool oob_probe_adopt;
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 c8fc465..ccd5fda 100644
--- a/src/openvpn/ssl.c
+++ b/src/openvpn/ssl.c
@@ -2569,6 +2569,59 @@
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). */
+ if (resend_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: 16
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