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 (#14).


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/14

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 3f805f3..ede0259 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 37a75ff..e50147b 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"
@@ -303,6 +304,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);

@@ -346,6 +354,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);
@@ -704,11 +714,6 @@
     {
         oob_probe_collect(&pc, &probe, &client_sid, &tas->tls_wrap, targets, 
results, l->len);
     }
-    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;
@@ -724,10 +729,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
         {
@@ -749,6 +754,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);
+    }
     tls_auth_standalone_free(tas);
     gc_free(&gc);
 }
diff --git a/src/openvpn/openvpn.h b/src/openvpn/openvpn.h
index 436495e..c0f9c62 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..66efd39 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: 14
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

Reply via email to