Attention is currently required from: flichtenheld, plaisthos, ralf_lici, stipa.

Hello plaisthos, ralf_lici,

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

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

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


Change subject: oob: Answer SERVER_PROBE on the server (P_CONTROL_OOB_V1)
......................................................................

oob: Answer SERVER_PROBE on the server (P_CONTROL_OOB_V1)

Make a --mode server UDP listener answer an out-of-band SERVER_PROBE without
creating a session, so probing costs the server no state:

  - tls_pre_decrypt_lite() accepts P_CONTROL_OOB_V1 and returns the new
    VERDICT_VALID_OOB_V1 verdict.
  - tls_wrap_oob_standalone() builds a session-less P_CONTROL_OOB_V1 packet:
    opcode + session id + the usual tls-auth/tls-crypt wrapping around a bare
    TLV payload (no reliability/ACK fields), mirroring tls_reset_standalone().
  - oob_client_reply_write() writes the PROBE_REPLY message (message-type
    header + probe_reply TLV).
  - do_pre_decrypt_check() handles the new verdict: it asks
    oob_server_probe_accept() whether to answer, and if so builds the
    PROBE_REPLY, echoing the peer's session id, and sends it via
    send_probe_reply() (synchronous, stateless, like the HMAC reset path)
    and returns false so no session is created. The verdict joins the reset
    verdicts in the reflect_filter rate-limit check, so a probe flood is capped
    like a reset flood.

The reply carries, as its own session id, the stateless SYN-cookie the three-way
handshake uses, so the server keeps nothing per probe and a client can later
reuse the reply as the server's reset.

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/oob.c
M src/openvpn/oob.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
10 files changed, 247 insertions(+), 4 deletions(-)


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

diff --git a/src/openvpn/mudp.c b/src/openvpn/mudp.c
index de3d467..d592d40 100644
--- a/src/openvpn/mudp.c
+++ b/src/openvpn/mudp.c
@@ -32,6 +32,7 @@

 #include "memdbg.h"
 #include "ssl_pkt.h"
+#include "oob.h"

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

+/* Send an out-of-band PROBE_REPLY back to the source of a SERVER_PROBE,
+ * synchronously and without keeping any state, mirroring the reset path. */
+static void
+send_probe_reply(struct multi_context *m, struct tls_pre_decrypt_state *state,
+                 struct tls_auth_standalone *tas, const struct oob_probe_reply 
*reply,
+                 struct session_id *own_sid, struct link_socket *sock)
+{
+    struct gc_arena gc = gc_new();
+
+    /* Build the payload of the reply (message-type header + probe_reply TLV) 
*/
+    struct buffer payload = alloc_buf_gc(128, &gc);
+    if (!oob_client_reply_write(&payload, reply))
+    {
+        gc_free(&gc);
+        return;
+    }
+
+    /* OOB replies use the same control-channel wrapping as the request */
+    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_oob_standalone(&state->tls_wrap_tmp, tas, 
own_sid, &payload);
+    send_standalone_reply(m, &buf, "Server Probe", "Server probe from client, 
sending probe reply",
+                          sock);
+
+    gc_free(&gc);
+}
+

 /* Returns true if this packet should create a new session */
 static bool
@@ -105,7 +134,8 @@
     const struct openvpn_sockaddr *from = &m->top.c2.from.dest;
     int handwindow = m->top.options.handshake_window;

-    if (verdict == VERDICT_VALID_RESET_V3 || verdict == VERDICT_VALID_RESET_V2)
+    if (verdict == VERDICT_VALID_RESET_V3 || verdict == VERDICT_VALID_RESET_V2
+        || verdict == VERDICT_VALID_OOB_V1)
     {
         /* Check if we are still below our limit for sending out
          * responses */
@@ -203,6 +233,40 @@

         return ret;
     }
+    else if (verdict == VERDICT_VALID_OOB_V1)
+    {
+        /* Out-of-band server probe. state->newbuf points at the TLV payload
+         * (read_control_auth has stripped the opcode, session id and any
+         * tls-auth/tls-crypt wrapping). Answer it without creating a session. 
*/
+        enum oob_probe_verdict probe =
+            oob_server_probe_check(&state->newbuf, (uint64_t)now, 
(uint64_t)handwindow);
+        if (probe == OOB_PROBE_INVALID)
+        {
+            return false; /* malformed: silently drop */
+        }
+        /* A client whose clock is off by more than --hand-window still gets a
+         * few probes per period answered, which is also all a replayed probe
+         * can get. */
+        if (probe == OOB_PROBE_STALE && 
!reflect_filter_rate_limit_check(m->stale_probe_limiter))
+        {
+            return false;
+        }
+
+        /* the reply echoes the peer's session id */
+        struct oob_probe_reply reply = { .peer_session_id = 
state->peer_session_id };
+
+        /* Our session id is a stateless SYN cookie (the same HMAC the 
three-way
+         * handshake uses): we keep no per-probe state, and the reply can later
+         * also serve as the server's CONTROL_HARD_RESET_SERVER_V2, so a client
+         * can start the handshake from it (the connect_lifetime 
advertisement). */
+        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);
+
+        /* An OOB probe never creates a session */
+        return false;
+    }

     /* VERDICT_INVALID */
     return false;
diff --git a/src/openvpn/multi.c b/src/openvpn/multi.c
index 3e72b92..a438a34 100644
--- a/src/openvpn/multi.c
+++ b/src/openvpn/multi.c
@@ -326,6 +326,12 @@
     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);
+    /* stale server probes: a twentieth of the initial-packet rate, at least 1
+     * per period (5 per 10 s by default, the wire protocol's own example).
+     * Quiet, as a replayed probe hitting the limit is not worth a warning. */
+    m->stale_probe_limiter =
+        initial_rate_limit_init(max_int(1, t->options.cf_initial_max / 20), 
t->options.cf_initial_per);
+    m->stale_probe_limiter->quiet = true;

     /*
      * Allocate broadcast/multicast buffer list
@@ -689,6 +695,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 6cb86c7..d33bf85 100644
--- a/src/openvpn/multi.h
+++ b/src/openvpn/multi.h
@@ -178,6 +178,7 @@
     struct ifconfig_pool *ifconfig_pool;
     struct frequency_limit *new_connection_limiter;
     struct initial_packet_rate_limit *initial_rate_limiter;
+    struct initial_packet_rate_limit *stale_probe_limiter; /**< stale 
--server-probe answers */
     struct mroute_helper *route_helper;
     struct multi_reap *reaper;
     struct mroute_addr local;
diff --git a/src/openvpn/oob.c b/src/openvpn/oob.c
index 655c791..c3e4bc9 100644
--- a/src/openvpn/oob.c
+++ b/src/openvpn/oob.c
@@ -106,6 +106,12 @@
 }

 bool
+oob_client_reply_write(struct buffer *buf, const struct oob_probe_reply *reply)
+{
+    return buf_write_u16(buf, OOB_MSG_PROBE_REPLY) && 
oob_probe_reply_write(buf, reply);
+}
+
+bool
 oob_timestamp_in_window(uint64_t probe_ts, uint64_t now, uint64_t window_secs)
 {
     uint64_t diff = (now > probe_ts) ? (now - probe_ts) : (probe_ts - now);
diff --git a/src/openvpn/oob.h b/src/openvpn/oob.h
index 6d83eca..d8eca60 100644
--- a/src/openvpn/oob.h
+++ b/src/openvpn/oob.h
@@ -127,6 +127,12 @@
 bool oob_server_probe_read(struct buffer *payload, struct oob_probe_parameter 
*param);

 /**
+ * Write a complete PROBE_REPLY message (message-type header + probe_reply TLV)
+ * to buf. Sent by the server.
+ */
+bool oob_client_reply_write(struct buffer *buf, const struct oob_probe_reply 
*reply);
+
+/**
  * Check whether a probe timestamp is within an acceptable window around the
  * current time. Used to cheaply drop replayed or implausibly-timed probes
  * before doing any further work (see the probe_parameter timestamp rationale
diff --git a/src/openvpn/reflect_filter.c b/src/openvpn/reflect_filter.c
index e5226e4..b50f72e 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 "
diff --git a/src/openvpn/reflect_filter.h b/src/openvpn/reflect_filter.h
index 84915e8c6..e996d07 100644
--- a/src/openvpn/reflect_filter.h
+++ b/src/openvpn/reflect_filter.h
@@ -44,6 +44,9 @@
     /* we want to warn once per period that packets are being started to
      * be dropped */
     bool warning_displayed;
+
+    /* drop silently: no per-period warning or summary in the log */
+    bool quiet;
 };


diff --git a/src/openvpn/ssl_pkt.c b/src/openvpn/ssl_pkt.c
index 4e6b98b..8e78444 100644
--- a/src/openvpn/ssl_pkt.c
+++ b/src/openvpn/ssl_pkt.c
@@ -315,7 +315,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
+        && !opcode_is_oob(op))
     {
         /*
          * This can occur due to bogus data or DoS packets.
@@ -390,6 +391,10 @@
     {
         return VERDICT_VALID_WKC_V1;
     }
+    else if (opcode_is_oob(op))
+    {
+        return VERDICT_VALID_OOB_V1;
+    }
     else
     {
         return VERDICT_VALID_RESET_V2;
@@ -444,6 +449,29 @@
     return buf;
 }

+struct buffer
+tls_wrap_oob_standalone(struct tls_wrap_ctx *ctx, struct tls_auth_standalone 
*tas,
+                        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));
+
+    /* Out-of-band messages carry the payload directly, with no reliability
+     * or ACK fields. */
+    ASSERT(buf_copy(&buf, payload));
+
+    uint8_t header = (uint8_t)(P_CONTROL_OOB_V1 << 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 d8d4b5e..a7dc6d9 100644
--- a/src/openvpn/ssl_pkt.h
+++ b/src/openvpn/ssl_pkt.h
@@ -106,6 +106,9 @@
     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 out-of-band control message (e.g. a server
+     * probe). It does not belong to a session and must not create one. */
+    VERDICT_VALID_OOB_V1,
     /** the packet failed on of the various checks */
     VERDICT_INVALID
 };
@@ -226,6 +229,21 @@
                                    struct session_id *own_sid, struct 
session_id *remote_sid,
                                    uint8_t header, bool request_resend_wkc);

+/**
+ * Wrap an already-built out-of-band payload (e.g. probe-reply TLVs) into a
+ * standalone, session-less P_CONTROL_OOB_V1 packet: it prepends the opcode and
+ * own_sid and applies the same tls-auth/tls-crypt wrapping as a regular
+ * control packet, but carries no reliability/ACK fields.
+ *
+ * @param ctx       tls wrapping context (from the pre-decrypt state)
+ * @param tas       standalone auth context providing the work buffer
+ * @param own_sid   session id to use as our session id in the header
+ * @param payload   the OOB message payload (TLV stream) to wrap
+ * @return          the wrapped packet buffer, ready to send
+ */
+struct buffer tls_wrap_oob_standalone(struct tls_wrap_ctx *ctx, struct 
tls_auth_standalone *tas,
+                                      struct session_id *own_sid, const struct 
buffer *payload);
+

 /**
  * Extracts a control channel message from buf and adjusts the size of
diff --git a/tests/unit_tests/openvpn/test_pkt.c 
b/tests/unit_tests/openvpn/test_pkt.c
index a732c2b..ca3045a 100644
--- a/tests/unit_tests/openvpn/test_pkt.c
+++ b/tests/unit_tests/openvpn/test_pkt.c
@@ -695,6 +695,113 @@
     free_tas(&tas_server);
 }

+/* Payload every OOB round-trip below wraps: a message header and one TLV. */
+static const uint8_t oob_payload[] = { 0x01, 0x00, 0x00, 0x01, 0x00, 0x04, 
0xde, 0xad, 0xbe, 0xef };
+
+/* Wrap oob_payload as a standalone P_CONTROL_OOB_V1 with the client side of a
+ * tls-auth/tls-crypt pair (or none), run it through the server's stateless
+ * first-packet path, and check verdict, recovered session id and payload. */
+static void
+oob_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 } };
+
+    struct buffer payload = alloc_buf(64);
+    buf_write(&payload, oob_payload, sizeof(oob_payload));
+
+    /* Client side: opcode + session id + tls-auth/tls-crypt wrapping around 
the
+     * bare payload, with none of the reliability/ACK fields a control packet
+     * carries. */
+    struct buffer buf =
+        tls_wrap_oob_standalone(&tas_client->tls_wrap, tas_client, &sid, 
&payload);
+    assert_int_equal((BPTR(&buf))[0] >> P_OPCODE_SHIFT, P_CONTROL_OOB_V1);
+
+    /* Server side: the stateless first-packet path must recognise the opcode,
+     * verify the wrapping and leave exactly the payload in newbuf -- that is
+     * what mudp.c hands to the probe parser. */
+    enum first_packet_verdict verdict = tls_pre_decrypt_lite(tas_server, 
&state, &from, &buf);
+    assert_int_equal(verdict, VERDICT_VALID_OOB_V1);
+    assert_memory_equal(state.peer_session_id.id, sid.id, SID_SIZE);
+    assert_int_equal(BLEN(&state.newbuf), (int)sizeof(oob_payload));
+    assert_memory_equal(BPTR(&state.newbuf), oob_payload, sizeof(oob_payload));
+    free_tls_pre_decrypt_state(&state);
+
+    /* Tampering with any single byte must fail authentication: the flip runs
+     * over opcode, session id, packet id, HMAC/tag and payload alike. Skipped
+     * for TLS_WRAP_NONE, where a changed 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_oob_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);
+
+    oob_standalone_roundtrip(&tas, &tas);
+
+    free_tas(&tas);
+}
+
+static void
+test_oob_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 consumes the tls-auth packet id only 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;
+    oob_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_oob_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;
+    oob_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)
 {
@@ -745,6 +852,9 @@
         cmocka_unit_test(test_verify_hmac_none_out_of_range_ack),
         cmocka_unit_test(test_generate_reset_packet_plain),
         cmocka_unit_test(test_generate_reset_packet_tls_auth),
+        cmocka_unit_test(test_oob_standalone_plain),
+        cmocka_unit_test(test_oob_standalone_tls_auth),
+        cmocka_unit_test(test_oob_standalone_tls_crypt),
         cmocka_unit_test(test_extract_control_message)
     };


--
To view, visit http://gerrit.openvpn.net/c/openvpn/+/1744?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: I930d3789e0313aa0c3bc51ee5fd1d108343d59f0
Gerrit-Change-Number: 1744
Gerrit-PatchSet: 16
Gerrit-Owner: stipa <[email protected]>
Gerrit-Reviewer: plaisthos <[email protected]>
Gerrit-Reviewer: ralf_lici <[email protected]>
Gerrit-CC: flichtenheld <[email protected]>
Gerrit-CC: openvpn-devel <[email protected]>
Gerrit-Attention: plaisthos <[email protected]>
Gerrit-Attention: flichtenheld <[email protected]>
Gerrit-Attention: ralf_lici <[email protected]>
Gerrit-Attention: stipa <[email protected]>
_______________________________________________
Openvpn-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/openvpn-devel

Reply via email to