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

Hello flichtenheld, plaisthos, ralf_lici,

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

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

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


Change subject: oob: Add probe request parsing and the probe-reply decision
......................................................................

oob: Add probe request parsing and the probe-reply decision

Add the message-level helpers the server needs to answer a probe:
oob_probe_request_find() for the probe request TLV,
oob_timestamp_in_window() for the replay/skew check on its timestamp,
and oob_probe_request_check(), which combines the two and classifies a
received probe as invalid, stale or OK.

The reader returns the probe request, whose request_id the reply
echoes, so a client can tell which of its probes a reply answers.

A stale probe, one whose timestamp is outside the window, is reported
separately rather than dropped: the timestamp lets a server drop
replayed requests, but a client with a skewed clock sends the same
thing, so the server path decides (a follow-up answers a few per
period). The window itself is the caller's choice; the server path
passes --hand-window.

These are pure functions exercised by unit tests; the server packet
path uses them in a follow-up and builds and sends the reply itself.

Change-Id: Iafa9efc1c156974ad9f5752e9de810a8c2f68c30
Signed-off-by: Lev Stipakov <[email protected]>
---
M src/openvpn/oob.c
M src/openvpn/oob.h
M tests/unit_tests/openvpn/test_oob.c
3 files changed, 271 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/42/1742/19

diff --git a/src/openvpn/oob.c b/src/openvpn/oob.c
index 5f624be..954694a 100644
--- a/src/openvpn/oob.c
+++ b/src/openvpn/oob.c
@@ -83,3 +83,30 @@
     r->flags = (uint16_t)buf_read_u16(buf);
     return true;
 }
+
+bool
+oob_probe_request_find(struct buffer *payload, struct oob_probe_request *req)
+{
+    struct buffer value;
+    return ctrl_msg_find_tlv(payload, TLV_TYPE_PROBE_REQUEST, &value)
+           && oob_probe_request_read(&value, req);
+}
+
+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);
+    return diff <= window_secs;
+}
+
+enum oob_probe_verdict
+oob_probe_request_check(struct buffer *probe_payload, uint64_t now, uint64_t 
window_secs,
+                        struct oob_probe_request *req)
+{
+    if (!oob_probe_request_find(probe_payload, req))
+    {
+        return OOB_PROBE_INVALID;
+    }
+    return oob_timestamp_in_window(req->timestamp, now, window_secs) ? 
OOB_PROBE_OK
+                                                                     : 
OOB_PROBE_STALE;
+}
diff --git a/src/openvpn/oob.h b/src/openvpn/oob.h
index e3e4144..95b3afc 100644
--- a/src/openvpn/oob.h
+++ b/src/openvpn/oob.h
@@ -99,4 +99,49 @@
  */
 bool oob_probe_reply_read(struct buffer *buf, struct oob_probe_reply *r);

+/**
+ * Scan the payload of a received OOB message for the probe request TLV.
+ * Unknown TLV types marked optional are skipped; an unknown mandatory one
+ * rejects the message. payload is consumed as it is read.
+ *
+ * @param payload  buffer positioned at the start of the OOB message payload
+ * @param req      filled with the parsed probe request on success
+ * @return true if a well-formed probe request was found, false otherwise
+ */
+bool oob_probe_request_find(struct buffer *payload, struct oob_probe_request 
*req);
+
+/**
+ * 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 request timestamp rationale
+ * in the wire protocol specification).
+ *
+ * @param probe_ts     timestamp from the probe request (UNIX seconds)
+ * @param now          current time (UNIX seconds)
+ * @param window_secs  maximum allowed difference, in either direction
+ * @return true if |now - probe_ts| <= window_secs
+ */
+bool oob_timestamp_in_window(uint64_t probe_ts, uint64_t now, uint64_t 
window_secs);
+
+enum oob_probe_verdict
+{
+    OOB_PROBE_INVALID, /**< no valid probe request: drop */
+    OOB_PROBE_STALE,   /**< well-formed, timestamp outside the window */
+    OOB_PROBE_OK,      /**< well-formed, timestamp within the window */
+};
+
+/**
+ * Classify a received probe request. Combines oob_probe_request_find() and
+ * oob_timestamp_in_window(). This is the transport-agnostic decision step;
+ * the caller decides what to do with a stale probe and builds the reply.
+ *
+ * @param probe_payload  payload of the received OOB message, consumed
+ * @param now            current time (UNIX seconds)
+ * @param window_secs    acceptable timestamp skew, in either direction
+ * @param req            filled with the probe request unless the verdict is
+ *                       OOB_PROBE_INVALID, for the reply to echo its 
request_id
+ */
+enum oob_probe_verdict oob_probe_request_check(struct buffer *probe_payload, 
uint64_t now,
+                                               uint64_t window_secs, struct 
oob_probe_request *req);
+
 #endif /* OOB_H */
diff --git a/tests/unit_tests/openvpn/test_oob.c 
b/tests/unit_tests/openvpn/test_oob.c
index 4e06cfd..8af595d 100644
--- a/tests/unit_tests/openvpn/test_oob.c
+++ b/tests/unit_tests/openvpn/test_oob.c
@@ -338,6 +338,195 @@
     gc_free(&gc);
 }

+/* A payload of just a probe request is found by the scan, along with its
+ * request_id. */
+static void
+test_probe_request_find(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    const struct oob_probe_request in = {
+        .request_id = 42,
+        .timestamp = 0x1122334455667788ULL,
+        .flags = 0,
+    };
+    assert_true(oob_probe_request_write(&buf, &in));
+
+    struct oob_probe_request out = { 0 };
+    assert_true(oob_probe_request_find(&buf, &out));
+    assert_int_equal(out.request_id, 42);
+    assert_true(in.timestamp == out.timestamp);
+    assert_int_equal(in.flags, out.flags);
+
+    gc_free(&gc);
+}
+
+/* TLVs other than probe request are skipped, so the scan finds the
+ * probe request even when preceded by an unknown TLV. */
+static void
+test_probe_request_find_skips_unknown(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    /* An unknown but optional TLV (type 0x7ff) ... */
+    assert_true(ctrl_msg_tlv_write_header(&buf, 0x7ff, true, 4));
+    assert_true(buf_write_u32(&buf, 0xcafef00d));
+    /* ... followed by the real probe request */
+    const struct oob_probe_request in = { .timestamp = 42, .flags = 0 };
+    assert_true(oob_probe_request_write(&buf, &in));
+
+    struct oob_probe_request out = { 0 };
+    assert_true(oob_probe_request_find(&buf, &out));
+    assert_true(out.timestamp == 42);
+
+    gc_free(&gc);
+}
+
+/* An unknown TLV that is NOT marked optional carries something the sender
+ * requires us to act on: the whole message is rejected, whether that TLV comes
+ * before or after the one we were looking for. */
+static void
+test_probe_request_find_rejects_unknown_mandatory(void **state)
+{
+    struct gc_arena gc = gc_new();
+    const struct oob_probe_request in = { .timestamp = 42, .flags = 0 };
+
+    struct buffer before = alloc_buf_gc(128, &gc);
+    assert_true(ctrl_msg_tlv_write_header(&before, 0x7ff, false, 4));
+    assert_true(buf_write_u32(&before, 0xcafef00d));
+    assert_true(oob_probe_request_write(&before, &in));
+
+    struct buffer after = alloc_buf_gc(128, &gc);
+    assert_true(oob_probe_request_write(&after, &in));
+    assert_true(ctrl_msg_tlv_write_header(&after, 0x7ff, false, 4));
+    assert_true(buf_write_u32(&after, 0xcafef00d));
+
+    struct oob_probe_request out = { 0 };
+    assert_false(oob_probe_request_find(&before, &out));
+    assert_false(oob_probe_request_find(&after, &out));
+
+    gc_free(&gc);
+}
+
+/* A payload with no probe request must be rejected. */
+static void
+test_probe_request_find_missing(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    assert_true(ctrl_msg_tlv_write_header(&buf, 0x7ff, true, 4));
+    assert_true(buf_write_u32(&buf, 0));
+
+    struct oob_probe_request out = { 0 };
+    assert_false(oob_probe_request_find(&buf, &out));
+
+    gc_free(&gc);
+}
+
+/* A TLV whose declared length runs past the buffer must be rejected, not
+ * read out of bounds. */
+static void
+test_probe_request_find_truncated(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    /* TLV header claims a 16-byte value but no value bytes follow */
+    assert_true(ctrl_msg_tlv_write_header(&buf, 0x7ff, false, 16));
+
+    struct oob_probe_request out = { 0 };
+    assert_false(oob_probe_request_find(&buf, &out));
+
+    gc_free(&gc);
+}
+
+/* A payload carrying a probe reply instead holds no probe request. */
+static void
+test_probe_request_find_reply_tlv(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    const struct oob_probe_reply in = { .request_id = 42 };
+    assert_true(oob_probe_reply_write(&buf, &in));
+
+    struct oob_probe_request out = { 0 };
+    assert_false(oob_probe_request_find(&buf, &out));
+
+    gc_free(&gc);
+}
+
+/* Timestamp window check accepts values within the window (either direction)
+ * and rejects values outside it. */
+static void
+test_timestamp_in_window(void **state)
+{
+    const uint64_t now = 1000000;
+    const uint64_t window = 30;
+
+    assert_true(oob_timestamp_in_window(now, now, window));
+    assert_true(oob_timestamp_in_window(now - window, now, window));      /* 
boundary, past */
+    assert_true(oob_timestamp_in_window(now + window, now, window));      /* 
boundary, future */
+    assert_false(oob_timestamp_in_window(now - window - 1, now, window)); /* 
too old */
+    assert_false(oob_timestamp_in_window(now + window + 1, now, window)); /* 
too far ahead */
+}
+
+/* A well-formed probe with an in-window timestamp is answered. */
+static void
+test_probe_request_check_valid(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    const uint64_t now = 1000000;
+    const struct oob_probe_request probe = { .request_id = 1, .timestamp = 
now, .flags = 0 };
+    assert_true(oob_probe_request_write(&buf, &probe));
+
+    struct oob_probe_request req = { 0 };
+    assert_int_equal(oob_probe_request_check(&buf, now, 30, &req), 
OOB_PROBE_OK);
+    assert_int_equal(req.request_id, 1);
+
+    gc_free(&gc);
+}
+
+/* A well-formed probe whose timestamp is outside the window is stale; the
+ * caller decides whether it still gets an answer. */
+static void
+test_probe_request_check_stale(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    const uint64_t now = 1000000;
+    const struct oob_probe_request probe = { .request_id = 7, .timestamp = now 
- 1000, .flags = 0 };
+    assert_true(oob_probe_request_write(&buf, &probe));
+
+    struct oob_probe_request req = { 0 };
+    assert_int_equal(oob_probe_request_check(&buf, now, 30, &req), 
OOB_PROBE_STALE);
+    assert_int_equal(req.request_id, 7); /* a stale probe may still be 
answered */
+
+    gc_free(&gc);
+}
+
+/* A payload without a probe request is invalid (no reply). */
+static void
+test_probe_request_check_no_request(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    assert_true(ctrl_msg_tlv_write_header(&buf, 0x7ff, true, 4));
+    assert_true(buf_write_u32(&buf, 0));
+
+    struct oob_probe_request req;
+    assert_int_equal(oob_probe_request_check(&buf, 1000000, 30, &req), 
OOB_PROBE_INVALID);
+
+    gc_free(&gc);
+}
+
 /* The OOB tests run as a second group of pkt_testdriver; see test_pkt.c. */
 int
 run_oob_tests(void)
@@ -353,6 +542,16 @@
         cmocka_unit_test(test_find_tlv_trailing_bytes),
         cmocka_unit_test(test_find_tlv_skips_optional_after_wanted),
         cmocka_unit_test(test_find_tlv_empty_payload),
+        cmocka_unit_test(test_probe_request_find),
+        cmocka_unit_test(test_probe_request_find_skips_unknown),
+        cmocka_unit_test(test_probe_request_find_rejects_unknown_mandatory),
+        cmocka_unit_test(test_probe_request_find_missing),
+        cmocka_unit_test(test_probe_request_find_truncated),
+        cmocka_unit_test(test_probe_request_find_reply_tlv),
+        cmocka_unit_test(test_timestamp_in_window),
+        cmocka_unit_test(test_probe_request_check_valid),
+        cmocka_unit_test(test_probe_request_check_stale),
+        cmocka_unit_test(test_probe_request_check_no_request),
     };

     return cmocka_run_group_tests_name("oob tests", tests, NULL, NULL);

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

Reply via email to