Attention is currently required from: plaisthos.
Hello plaisthos,
I'd like you to do a code review.
Please visit
http://gerrit.openvpn.net/c/openvpn/+/1770?usp=email
to review the following change.
Change subject: oob: client handshake shortcut via a server probe
......................................................................
oob: client handshake shortcut via a server probe
When the best remote answered a probe and advertised a connect_lifetime, the
client now skips its own three-way handshake and starts at the third packet,
reusing the probe reply as the server's HARD_RESET (its session id is a valid
stateless SYN-cookie). Saves one RTT on connect.
- oob_client.c: on a shortcut-capable winner, 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 the shortcut 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.
Change-Id: I454d5040cbad4d373ee4f90b8d683200d2a4c0e4
Signed-off-by: Lev Stipakov <[email protected]>
---
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
6 files changed, 172 insertions(+), 4 deletions(-)
git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/70/1770/1
diff --git a/src/openvpn/init.c b/src/openvpn/init.c
index 8eca6f7..fc44f41 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -3505,6 +3505,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 handshake shortcut: the probe reply already served
as
+ * the server's HARD_RESET (it carried a valid SYN-cookie), so skip our
+ * own reset exchange and start the handshake at the third packet.
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 44a51ec..1bfa601 100644
--- a/src/openvpn/oob.h
+++ b/src/openvpn/oob.h
@@ -245,6 +245,11 @@
uint16_t weight;
uint16_t max_latency_diff; /* margin advertised by this server (ms); 0 =
use client default */
unsigned int rtt_ms; /* probe round-trip time in ms (responders
only) */
+ /* Captured from the reply for the handshake shortcut (responders only): */
+ struct session_id server_sid; /* the reply's own session id = server
SYN-cookie */
+ uint16_t connect_lifetime; /* seconds the reply is valid as a
shortcut; 0 = none */
+ uint32_t reply_flags; /* OOB_PROBE_REPLY_FLAG_* advertised by
the server */
+ struct sockaddr_storage responder; /* address that answered (pin the
connection to it) */
};
/**
diff --git a/src/openvpn/oob_client.c b/src/openvpn/oob_client.c
index b257da3..6d47413 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"
@@ -215,6 +216,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 skip to the third handshake packet (the connect_lifetime
+ * shortcut). */
+ struct session_id server_sid;
+ memcpy(server_sid.id, data + 1, SID_SIZE);
+
struct buffer buf;
buf_set_read(&buf, data, (size_t)len);
@@ -273,6 +281,10 @@
results[i].weight = reply.weight;
results[i].max_latency_diff = reply.max_latency_diff;
results[i].rtt_ms = (ms > 0) ? (unsigned int)ms : 0;
+ results[i].server_sid = server_sid;
+ results[i].connect_lifetime = reply.connect_lifetime;
+ results[i].reply_flags = reply.flags;
+ results[i].responder = *from; /* pin the connection to the address
that answered */
break;
}
}
@@ -588,7 +600,6 @@
{
oob_probe_collect(&pc, &probe, &client_sid, &tas->tls_wrap, targets,
results, l->len);
}
- oob_probe_sockets_close(&pc);
/* Log each remote's outcome while results[i] still maps to array[i]. */
int responded = 0;
@@ -607,10 +618,10 @@
: results[i].max_latency_diff > 0 ?
"server-advertised"
:
"default";
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].priority,
results[i].weight,
- results[i].rtt_ms, margin, margin_src);
+ results[i].connect_lifetime, results[i].rtt_ms, margin,
margin_src);
}
else
{
@@ -632,6 +643,70 @@
msg(M_INFO, "server-probe: %d of %d remote(s) answered; connecting
best-first", responded,
l->len);
+ /* Handshake shortcut: if the best remote (now l->array[0] / results[0])
+ * answered and advertised a connect_lifetime, hand its probe socket and
the
+ * captured cookie/details to the connection so it starts at the third
packet
+ * (see session_skip_to_pre_start_client). The winner's AF socket becomes
the
+ * connection socket, preserving the source IP+port the server's cookie is
+ * bound to. Not possible with dco-win (no user->kernel socket handoff), so
+ * skip the shortcut there; probing/ordering still applied above. */
+ /* The RFC also requires the client to fall back to a full handshake once
+ * more than connect_lifetime seconds have elapsed since the probe. We
probe
+ * immediately before connecting (~0 s gap), so that check is a no-op
today;
+ * add an elapsed-time guard here if probe results are ever cached and
reused
+ * across reconnects. */
+ bool shortcut = results[0].responded && results[0].connect_lifetime > 0;
+ bool dco_win_gate = false;
+#if defined(_WIN32)
+ if (dco_enabled(&c->options))
+ {
+ shortcut = false;
+ dco_win_gate = true;
+ }
+#endif
+ if (shortcut)
+ {
+ const int af_idx = probe_af_index(results[0].responder.ss_family);
+ c->c2.oob_probe_sd = pc.sd[af_idx];
+ pc.sd[af_idx] = SOCKET_UNDEFINED; /* relinquish: the connection owns
it now */
+
+ CLEAR(c->c2.oob_probe_remote);
+ if (results[0].responder.ss_family == AF_INET)
+ {
+ c->c2.oob_probe_remote.addr.in4 = *(struct sockaddr_in *)(void
*)&results[0].responder;
+ }
+ else
+ {
+ c->c2.oob_probe_remote.addr.in6 = *(struct sockaddr_in6 *)(void
*)&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: using handshake shortcut to %s:%s
(connect-lifetime %d s)",
+ l->array[0]->remote, l->array[0]->remote_port,
results[0].connect_lifetime);
+ }
+ else if (results[0].responded)
+ {
+ /* A server answered but we won't take the shortcut -- say why. */
+ if (dco_win_gate)
+ {
+ msg(D_LOW, "server-probe: handshake shortcut unavailable with
dco-win;"
+ " using a full handshake");
+ }
+ else if (results[0].connect_lifetime == 0)
+ {
+ msg(D_LOW, "server-probe: %s:%s did not offer a handshake shortcut"
+ " (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);
+
tls_auth_standalone_free(tas);
gc_free(&gc);
}
diff --git a/src/openvpn/openvpn.h b/src/openvpn/openvpn.h
index e5a1d76..69eec10 100644
--- a/src/openvpn/openvpn.h
+++ b/src/openvpn/openvpn.h
@@ -249,6 +249,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 0998bd6..4503a05 100644
--- a/src/openvpn/ssl.c
+++ b/src/openvpn/ssl.c
@@ -2516,6 +2516,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 (shortcut) 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 5822336..6ae9678 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 at
the
+ * third packet using a server session id (SYN-cookie) learned earlier from an
+ * OOB server probe reply, instead of sending our own HARD_RESET. Saves one
RTT.
+ *
+ * @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: newchange
Gerrit-Project: openvpn
Gerrit-Branch: master
Gerrit-Change-Id: I454d5040cbad4d373ee4f90b8d683200d2a4c0e4
Gerrit-Change-Number: 1770
Gerrit-PatchSet: 1
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