Attention is currently required from: Arne Schwabe, Frank Lichtenheld, Lev 
Stipakov, Ralf Lici.

Hello Arne Schwabe, Ralf Lici,

I'd like you to reexamine a change. Please visit

    https://gerrit.openvpn.net/c/openvpn/+/1744?usp=email

to look at the new patch set (#23).


Change subject: probe: Answer probe requests on the server (P_CONTROL_V2)
......................................................................

probe: Answer probe requests on the server (P_CONTROL_V2)

Let a --mode server UDP listener answer a server probe without creating
a session, so that probing costs the server no state at all.

tls_pre_decrypt_lite() now accepts P_CONTROL_V2 and reports it as
VERDICT_VALID_CONTROL_V2, and tls_wrap_control_v2_standalone() wraps a
bare TLV payload the way tls_reset_standalone() wraps a reset, minus
the reliability layer's fields, which P_CONTROL_V2 does not carry.
do_pre_decrypt_check() classifies what arrived with
probe_request_check() and answers with a probe reply naming the request
it answers.

A reply is charged against the same budget as the reset replies, and
only once one is going to be sent, so that a flood of probes is capped
exactly like a flood of resets. A stale request, one whose timestamp is
outside the window, is still answered, but out of a budget of its own,
a twentieth of --connect-freq-initial per period and at least one: a
client with an unsynchronised clock can still probe, and a replayed
request gets no more than that. That budget's limiter is quiet, since
saying in the log that a replayed probe hit it would be the flood
writing the log.

The wire protocol leaves the width of that window to the server and
only gives 10 seconds as an example. We pass --hand-window, 60 seconds
by default, because that is what the cookie below is already tied to:
calculate_session_id_hmac() quantises time into slots half that wide
and check_session_hmac_and_pkt_id() accepts the current slot and the
two before it, so answering an older request would hand out a cookie
the handshake would no longer take.

The reply carries as its own session id that stateless cookie, so that
the server keeps nothing per probe and the reply can later stand in for
its reset, which is what lets a client carry on into a handshake from
it. That part comes later; for now the client never sends a probe.

P_CONTROL_WKC_V2, a probe from a tls-crypt-v2 client that appends its
wrapped key, is not accepted yet and is rejected like any other opcode
that does not belong here. It needs the key unwrapped before the
message can be read, and that follows separately.

Change-Id: I930d3789e0313aa0c3bc51ee5fd1d108343d59f0
Signed-off-by: Lev Stipakov <[email protected]>
---
M src/openvpn/mudp.c
M src/openvpn/multi.c
M src/openvpn/multi.h
M src/openvpn/reflect_filter.c
M src/openvpn/reflect_filter.h
M src/openvpn/ssl_pkt.c
M src/openvpn/ssl_pkt.h
M tests/unit_tests/openvpn/test_pkt.c
8 files changed, 258 insertions(+), 3 deletions(-)


  git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/44/1744/23

diff --git a/src/openvpn/mudp.c b/src/openvpn/mudp.c
index 217b299..0037f19 100644
--- a/src/openvpn/mudp.c
+++ b/src/openvpn/mudp.c
@@ -34,6 +34,7 @@
 #include "memdbg.h"
 #include "ssl_pkt.h"
 #include "sid_hash.h"
+#include "probe.h"

 #ifdef HAVE_SYS_INOTIFY_H
 #include <sys/inotify.h>
@@ -90,6 +91,34 @@
                           "Reset packet from client, sending HMAC based reset 
challenge", sock);
 }

+/* Send a probe reply back to the peer that sent us a probe request,
+ * synchronously and without keeping any state, as the reset path does. */
+static void
+send_probe_reply(struct multi_context *m, struct tls_pre_decrypt_state *state,
+                 const struct tls_auth_standalone *tas, const struct 
probe_reply *reply,
+                 const struct session_id *own_sid, struct link_socket *sock)
+{
+    struct gc_arena gc = gc_new();
+
+    /* the message is the reply TLV and nothing else */
+    struct buffer payload = alloc_buf_gc(128, &gc);
+    if (!probe_reply_write(&payload, reply))
+    {
+        gc_free(&gc);
+        return;
+    }
+
+    reset_packet_id_send(&state->tls_wrap_tmp.opt.packet_id.send);
+    state->tls_wrap_tmp.opt.packet_id.rec.initialized = true;
+
+    struct buffer buf =
+        tls_wrap_control_v2_standalone(&state->tls_wrap_tmp, tas, own_sid, 
&payload);
+    send_standalone_reply(m, &buf, "Server Probe", "Probe request from client, 
sending probe reply",
+                          sock);
+
+    gc_free(&gc);
+}
+
 /**
  * Verdict if this packet should create a new session. If a packet is invalid
  * or we send out an HMAC based challenge, we do not want to create a new
@@ -227,6 +256,47 @@

         return action;
     }
+    else if (verdict == VERDICT_VALID_CONTROL_V2)
+    {
+        /* read_control_auth() has taken off the opcode, the session id and
+         * any wrapping, so newbuf is positioned at the TLVs. The protocol
+         * leaves the window's width to us; --hand-window is about how long
+         * the cookie we reply with stays valid, so an older request would be
+         * answered with one that has already expired. */
+        struct probe_request req = { 0 };
+        enum probe_verdict probe =
+            probe_request_check(&state->newbuf, (uint64_t)now, 
(uint64_t)handwindow, &req);
+        if (probe == PROBE_INVALID)
+        {
+            return PRE_DECRYPT_NO_ACTION;
+        }
+
+        /* a budget of its own for stale requests, so that an unsynchronised
+         * clock is not locked out and a replay gets no more than that */
+        if (probe == PROBE_STALE && 
!reflect_filter_rate_limit_check(m->stale_probe_limiter))
+        {
+            return PRE_DECRYPT_NO_ACTION;
+        }
+
+        /* charged against the reset replies' budget, and only now that we
+         * know a reply is going out, so probes are capped like resets */
+        if (!reflect_filter_rate_limit_check(m->initial_rate_limiter))
+        {
+            return PRE_DECRYPT_NO_ACTION;
+        }
+
+        struct probe_reply reply = { .request_id = req.request_id };
+
+        /* We answer with the handshake's own stateless cookie, so we keep
+         * nothing per probe and the reply can later stand in for our reset. */
+        struct session_id sid =
+            calculate_session_id_hmac(state->peer_session_id, from, hmac_key, 
handwindow, 0);
+
+        send_probe_reply(m, state, tas, &reply, &sid, sock);
+
+        /* a probe never creates a session */
+        return PRE_DECRYPT_NO_ACTION;
+    }

     /* VERDICT_INVALID */
     return PRE_DECRYPT_NO_ACTION;
diff --git a/src/openvpn/multi.c b/src/openvpn/multi.c
index 3f514d6..862a957 100644
--- a/src/openvpn/multi.c
+++ b/src/openvpn/multi.c
@@ -334,6 +334,14 @@
     m->new_connection_limiter = frequency_limit_init(t->options.cf_max, 
t->options.cf_per);
     m->initial_rate_limiter =
         initial_rate_limit_init(t->options.cf_initial_max, 
t->options.cf_initial_per);
+    /* Answers to stale probe requests get a budget of their own, a twentieth
+     * of the initial packet rate and at least one per period, so that they
+     * cannot crowd out the handshakes the main budget is for. */
+    m->stale_probe_limiter = initial_rate_limit_init(max_int(1, 
t->options.cf_initial_max / 20),
+                                                     
t->options.cf_initial_per);
+    /* A replayed probe request is what normally runs this budget out, and
+     * saying so in the log on every period would be the flood doing it. */
+    m->stale_probe_limiter->quiet = true;

     /*
      * Allocate broadcast/multicast buffer list
@@ -713,6 +721,7 @@
         ifconfig_pool_free(m->ifconfig_pool);
         frequency_limit_free(m->new_connection_limiter);
         initial_rate_limit_free(m->initial_rate_limiter);
+        initial_rate_limit_free(m->stale_probe_limiter);
         multi_reap_free(m->reaper);
         mroute_helper_free(m->route_helper);
         multi_io_free(m->multi_io);
diff --git a/src/openvpn/multi.h b/src/openvpn/multi.h
index 3d2e932..ecc7158 100644
--- a/src/openvpn/multi.h
+++ b/src/openvpn/multi.h
@@ -194,6 +194,8 @@
     struct ifconfig_pool *ifconfig_pool;
     struct frequency_limit *new_connection_limiter;
     struct initial_packet_rate_limit *initial_rate_limiter;
+    /** budget for answering probe requests whose timestamp is stale */
+    struct initial_packet_rate_limit *stale_probe_limiter;
     struct mroute_helper *route_helper;
     struct multi_reap *reaper;
     struct mroute_addr local;
diff --git a/src/openvpn/reflect_filter.c b/src/openvpn/reflect_filter.c
index e5226e4..5bb1cf4 100644
--- a/src/openvpn/reflect_filter.c
+++ b/src/openvpn/reflect_filter.c
@@ -44,7 +44,7 @@
     if (now > irl->last_period_reset + irl->period_length)
     {
         int64_t dropped = irl->curr_period_counter - irl->max_per_period;
-        if (dropped > 0)
+        if (dropped > 0 && !irl->quiet)
         {
             msg(D_TLS_DEBUG_LOW,
                 "Dropped %" PRId64 " initial handshake packets"
@@ -60,7 +60,7 @@

     bool over_limit = irl->curr_period_counter > irl->max_per_period;

-    if (over_limit && !irl->warning_displayed)
+    if (over_limit && !irl->warning_displayed && !irl->quiet)
     {
         msg(M_WARN,
             "Note: --connect-freq-initial %" PRId64 " %d rate limit "
@@ -95,6 +95,7 @@
     irl->period_length = period_length;
     irl->curr_period_counter = 0;
     irl->last_period_reset = 0;
+    irl->quiet = false;

     return irl;
 }
diff --git a/src/openvpn/reflect_filter.h b/src/openvpn/reflect_filter.h
index 84915e8c6..fce36b5 100644
--- a/src/openvpn/reflect_filter.h
+++ b/src/openvpn/reflect_filter.h
@@ -44,6 +44,11 @@
     /* we want to warn once per period that packets are being started to
      * be dropped */
     bool warning_displayed;
+
+    /* drop silently: no warning and no per-period summary in the log. For a
+     * limiter that caps something an attacker can trigger at will, where the
+     * log messages would only turn the flood into a second flood */
+    bool quiet;
 };


diff --git a/src/openvpn/ssl_pkt.c b/src/openvpn/ssl_pkt.c
index ccb841e..7f0e170 100644
--- a/src/openvpn/ssl_pkt.c
+++ b/src/openvpn/ssl_pkt.c
@@ -317,7 +317,8 @@

     /* Allow only the reset packet or the first packet of the actual 
handshake. */
     if (op != P_CONTROL_HARD_RESET_CLIENT_V2 && op != 
P_CONTROL_HARD_RESET_CLIENT_V3
-        && op != P_CONTROL_V1 && op != P_CONTROL_WKC_V1 && op != P_ACK_V1)
+        && op != P_CONTROL_V1 && op != P_CONTROL_WKC_V1 && op != P_ACK_V1
+        && op != P_CONTROL_V2)
     {
         /*
          * This can occur due to bogus data or DoS packets.
@@ -392,6 +393,10 @@
     {
         return VERDICT_VALID_WKC_V1;
     }
+    else if (op == P_CONTROL_V2)
+    {
+        return VERDICT_VALID_CONTROL_V2;
+    }
     else
     {
         return VERDICT_VALID_RESET_V2;
@@ -445,6 +450,29 @@
     return buf;
 }

+struct buffer
+tls_wrap_control_v2_standalone(struct tls_wrap_ctx *ctx, const struct 
tls_auth_standalone *tas,
+                               const struct session_id *own_sid, const struct 
buffer *payload)
+{
+    /* Copy buffer here to point at the same data but allow tls_wrap_control
+     * to potentially change buf to point to another buffer without
+     * modifying the buffer in tas */
+    struct buffer buf = tas->workbuf;
+    ASSERT(buf_init(&buf, tas->frame.buf.headroom));
+
+    /* A P_CONTROL_V2 message is the session id and then the TLVs: no ACK
+     * array and no message id, so the payload goes in as it is. */
+    ASSERT(buf_copy(&buf, payload));
+
+    uint8_t header = (uint8_t)(P_CONTROL_V2 << P_OPCODE_SHIFT);
+
+    /* Add tls-auth/tls-crypt wrapping, this might replace buf with
+     * ctx->work */
+    tls_wrap_control(ctx, header, &buf, own_sid);
+
+    return buf;
+}
+
 struct session_id
 calculate_session_id_hmac(struct session_id client_sid, const struct 
openvpn_sockaddr *from,
                           const uint8_t *key, int handwindow, int offset)
diff --git a/src/openvpn/ssl_pkt.h b/src/openvpn/ssl_pkt.h
index 3b45492..4d24437 100644
--- a/src/openvpn/ssl_pkt.h
+++ b/src/openvpn/ssl_pkt.h
@@ -133,6 +133,10 @@
     VERDICT_VALID_ACK_V1,
     /** The packet is a valid control packet with appended wrapped client key 
*/
     VERDICT_VALID_WKC_V1,
+    /** This packet is a valid P_CONTROL_V2 message, e.g. a server probe. It
+     * belongs to no session and must not create one; what it carries is for
+     * the caller to work out from its TLVs. */
+    VERDICT_VALID_CONTROL_V2,
     /** the packet failed on of the various checks */
     VERDICT_INVALID
 };
@@ -256,6 +260,25 @@


 /**
+ * Wrap an already-built TLV payload into a standalone P_CONTROL_V2 packet, to
+ * be sent without a session: it prepends the opcode and own_sid and applies
+ * the same tls-auth/tls-crypt wrapping as any other control channel packet,
+ * but writes none of the reliability layer's fields, which P_CONTROL_V2 does
+ * not have. It is to a probe reply what tls_reset_standalone() is to a reset.
+ *
+ * @param ctx       tls wrapping context, from the pre-decrypt state
+ * @param tas       standalone auth context, providing the work buffer
+ * @param own_sid   the session id to send as ours
+ * @param payload   the TLVs to wrap
+ * @return          the wrapped packet, ready to send
+ */
+struct buffer tls_wrap_control_v2_standalone(struct tls_wrap_ctx *ctx,
+                                             const struct tls_auth_standalone 
*tas,
+                                             const struct session_id *own_sid,
+                                             const struct buffer *payload);
+
+
+/**
  * Extracts a control channel message from buf and adjusts the size of
  * buf after the message has been extracted
  * @param buf   The buffer the message should be extracted from
diff --git a/tests/unit_tests/openvpn/test_pkt.c 
b/tests/unit_tests/openvpn/test_pkt.c
index 0cb2ea5..c2a4754 100644
--- a/tests/unit_tests/openvpn/test_pkt.c
+++ b/tests/unit_tests/openvpn/test_pkt.c
@@ -38,6 +38,7 @@
 #include "options.h"
 #include "ssl_backend.h"
 #include "control_msg.h"
+#include "probe.h"
 #include "ssl_pkt.h"
 #include "tls_crypt.h"

@@ -785,6 +786,119 @@
     free_tas(&tas);
 }

+/* Wrap a probe request as a standalone P_CONTROL_V2 with the client side of a
+ * tls-auth/tls-crypt pair (or with neither), run it through the server's
+ * stateless first-packet path, and check the verdict, the recovered session id
+ * and the payload. This is the chain mudp.c walks for a probe. */
+static void
+control_v2_standalone_roundtrip(struct tls_auth_standalone *tas_client,
+                                struct tls_auth_standalone *tas_server)
+{
+    struct link_socket_actual from = { 0 };
+    struct tls_pre_decrypt_state state = { 0 };
+    struct session_id sid = { { 0x0b, 1, 2, 3, 4, 5, 6, 0x0b } };
+
+    const struct probe_request req = {
+        .request_id = 0xdeadbeefcafef00dULL,
+        .timestamp = 0x22446688,
+        .flags = PROBE_REQUEST_FLAG_PROBE_ONLY,
+    };
+    struct buffer payload = alloc_buf(64);
+    assert_true(probe_request_write(&payload, &req));
+
+    /* Client side: opcode, session id and the wrapping around the bare TLVs,
+     * with none of the reliability layer's fields. */
+    struct buffer buf =
+        tls_wrap_control_v2_standalone(&tas_client->tls_wrap, tas_client, 
&sid, &payload);
+    assert_int_equal((BPTR(&buf))[0] >> P_OPCODE_SHIFT, P_CONTROL_V2);
+
+    /* Server side: the stateless path must take the opcode, verify the
+     * wrapping and leave exactly the TLVs in newbuf, which is what the probe
+     * parser is then handed. */
+    enum first_packet_verdict verdict = tls_pre_decrypt_lite(tas_server, 
&state, &from, &buf);
+    assert_int_equal(verdict, VERDICT_VALID_CONTROL_V2);
+    assert_memory_equal(state.peer_session_id.id, sid.id, SID_SIZE);
+    assert_int_equal(BLEN(&state.newbuf), BLEN(&payload));
+    assert_memory_equal(BPTR(&state.newbuf), BPTR(&payload), BLEN(&payload));
+
+    struct probe_request got = { 0 };
+    assert_int_equal(probe_request_check(&state.newbuf, req.timestamp, 30, 
&got), PROBE_OK);
+    assert_true(got.request_id == req.request_id);
+    free_tls_pre_decrypt_state(&state);
+
+    /* Flipping any single byte must fail authentication: the flip runs over
+     * the opcode, session id, packet id, HMAC or tag and payload alike. Not
+     * for TLS_WRAP_NONE, where an altered payload is legitimately accepted. */
+    if (tas_server->tls_wrap.mode != TLS_WRAP_NONE)
+    {
+        struct buffer copy = alloc_buf(BLEN(&buf));
+        for (int i = 0; i < BLEN(&buf); i++)
+        {
+            buf_reset_len(&copy);
+            buf_write(&copy, BPTR(&buf), BLEN(&buf));
+            (BPTR(&copy))[i] ^= 0xff;
+            struct tls_pre_decrypt_state tstate = { 0 };
+            verdict = tls_pre_decrypt_lite(tas_server, &tstate, &from, &copy);
+            assert_int_equal(verdict, VERDICT_INVALID);
+            free_tls_pre_decrypt_state(&tstate);
+        }
+        free_buf(&copy);
+    }
+
+    free_buf(&payload);
+}
+
+static void
+test_control_v2_standalone_plain(void **ut_state)
+{
+    struct tls_auth_standalone tas = { 0 };
+    struct frame frame = { .buf = { .headroom = 200, .payload_size = 1400 }, 0 
};
+    tas.frame = frame;
+    tas.tls_wrap.mode = TLS_WRAP_NONE;
+    tas.workbuf = alloc_buf(1600);
+
+    control_v2_standalone_roundtrip(&tas, &tas);
+
+    free_tas(&tas);
+}
+
+static void
+test_control_v2_standalone_tls_auth(void **ut_state)
+{
+    struct tls_auth_standalone tas_server = 
init_tas_auth(KEY_DIRECTION_NORMAL);
+    struct tls_auth_standalone tas_client = 
init_tas_auth(KEY_DIRECTION_INVERSE);
+    packet_id_init(&tas_client.tls_wrap.opt.packet_id, 5, 5, "UNITTEST", 0);
+    /* the server only consumes the tls-auth packet id with packet-id state,
+     * which tls_auth_standalone_init() sets up for the real one */
+    packet_id_init(&tas_server.tls_wrap.opt.packet_id, 5, 5, "UNITTEST", 0);
+
+    now = 0x22446688;
+    control_v2_standalone_roundtrip(&tas_client, &tas_server);
+
+    packet_id_free(&tas_client.tls_wrap.opt.packet_id);
+    packet_id_free(&tas_server.tls_wrap.opt.packet_id);
+    free_tas(&tas_client);
+    free_tas(&tas_server);
+}
+
+static void
+test_control_v2_standalone_tls_crypt(void **ut_state)
+{
+    struct frame frame = { .buf = { .headroom = 200, .payload_size = 1400 }, 0 
};
+    struct tls_auth_standalone tas_server = init_tas_crypt(true);
+    struct tls_auth_standalone tas_client = init_tas_crypt(false);
+    tas_server.frame = frame;
+    tas_client.frame = frame;
+    packet_id_init(&tas_client.tls_wrap.opt.packet_id, 5, 5, "UNITTEST", 0);
+
+    now = 0x22446688;
+    control_v2_standalone_roundtrip(&tas_client, &tas_server);
+
+    packet_id_free(&tas_client.tls_wrap.opt.packet_id);
+    free_tas(&tas_client);
+    free_tas(&tas_server);
+}
+
 static void
 test_extract_control_message(void **ut_state)
 {
@@ -837,6 +951,9 @@
         cmocka_unit_test(test_generate_reset_packet_resend_wkc),
         cmocka_unit_test(test_generate_reset_packet_tls_auth),
         cmocka_unit_test(test_wkc_not_appended_without_key),
+        cmocka_unit_test(test_control_v2_standalone_plain),
+        cmocka_unit_test(test_control_v2_standalone_tls_auth),
+        cmocka_unit_test(test_control_v2_standalone_tls_crypt),
         cmocka_unit_test(test_extract_control_message),
         cmocka_unit_test(test_tlv_write_u16_wire_format),
         cmocka_unit_test(test_tlv_header_roundtrip),

--
To view, visit https://gerrit.openvpn.net/c/openvpn/+/1744?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.openvpn.net/settings?usp=email

Gerrit-MessageType: newpatchset
Gerrit-Project: openvpn
Gerrit-Branch: master
Gerrit-Change-Id: I930d3789e0313aa0c3bc51ee5fd1d108343d59f0
Gerrit-Change-Number: 1744
Gerrit-PatchSet: 23
Gerrit-Owner: Lev Stipakov <[email protected]>
Gerrit-Reviewer: Arne Schwabe <[email protected]>
Gerrit-Reviewer: Ralf Lici <[email protected]>
Gerrit-CC: Frank Lichtenheld <[email protected]>
Gerrit-CC: Răzvan Cojocaru <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>
Gerrit-Attention: Ralf Lici <[email protected]>
Gerrit-Attention: Lev Stipakov <[email protected]>
Gerrit-Attention: Arne Schwabe <[email protected]>
Gerrit-Attention: Frank Lichtenheld <[email protected]>
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel

Reply via email to