[L] Change in openvpn[master]: oob: Add SERVER_PROBE parsing and the probe-reply decision

"stipa \(Code Review\) via Openvpn-devel" <[email protected]>
Newsgroups gmane.network.openvpn.devel
Message-ID <[email protected]>
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/+/1742?usp=email

to review the following change.


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

oob: Add SERVER_PROBE parsing and the probe-reply decision

Add the message-level helpers the server needs to answer a probe:

  - oob_server_probe_read() scans the TLV payload of a received OOB
    SERVER_PROBE for the probe_parameter TLV, skipping other/future TLV
    types and rejecting malformed or truncated input.
  - oob_timestamp_in_window() performs the cheap replay/skew check on the
    probe timestamp described in the wire protocol specification.
  - oob_build_probe_reply() composes the two: given a received SERVER_PROBE
    it scans for the probe_parameter, drops the probe if it is missing or
    its timestamp is outside the acceptable window, and otherwise populates
    the probe_reply (echoing the peer's session id) to send back.

These are pure functions exercised by unit tests; the server packet path
calls oob_build_probe_reply() in a follow-up, which performs the actual send.

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, 303 insertions(+), 0 deletions(-)



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

diff --git a/src/openvpn/oob.c b/src/openvpn/oob.c
index 9f123e2..58d47c0 100644
--- a/src/openvpn/oob.c
+++ b/src/openvpn/oob.c
@@ -152,3 +152,71 @@
     r->max_latency_diff = (uint16_t)max_latency_diff;
     return oob_skip_trailing(buf, value_len, OOB_PROBE_REPLY_LEN);
 }
+
+bool
+oob_server_probe_write(struct buffer *buf, const struct oob_probe_parameter *param)
+{
+    return oob_msg_write_header(buf, OOB_MSG_SERVER_PROBE)
+           && oob_probe_parameter_write(buf, param);
+}
+
+bool
+oob_server_probe_read(struct buffer *payload, struct oob_probe_parameter *param)
+{
+    if (!oob_msg_read_header(payload, OOB_MSG_SERVER_PROBE))
+    {
+        return false;
+    }
+
+    /* A TLV header is 4 bytes (type + length). Loop until we either find the
+     * probe_parameter or run out of well-formed TLVs. */
+    while (BLEN(payload) >= 4)
+    {
+        uint16_t type;
+        bool optional;
+        uint16_t value_len;
+        if (!oob_tlv_read_header(payload, &type, &optional, &value_len))
+        {
+            return false;
+        }
+        if (type == OOB_TLV_PROBE_PARAMETER)
+        {
+            return oob_probe_parameter_read(payload, param, value_len);
+        }
+        /* not the TLV we are looking for: skip its value and keep scanning */
+        if (!buf_advance(payload, value_len))
+        {
+            return false;
+        }
+    }
+    return false;
+}
+
+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;
+}
+
+bool
+oob_build_probe_reply(struct buffer *probe_payload, uint64_t now, uint64_t window_secs,
+                      const struct session_id *peer_sid, struct oob_probe_reply *reply)
+{
+    struct oob_probe_parameter param;
+    if (!oob_server_probe_read(probe_payload, &param))
+    {
+        return false;
+    }
+
+    /* Drop replayed or implausibly-timed probes before doing any more work. */
+    if (!oob_timestamp_in_window(param.timestamp, now, window_secs))
+    {
+        return false;
+    }
+
+    memset(reply, 0, sizeof(*reply));
+    reply->peer_session_id = *peer_sid;
+    /* priority/weight/connect_lifetime/flags left at 0 for now */
+    return true;
+}
diff --git a/src/openvpn/oob.h b/src/openvpn/oob.h
index c32a7db..48fcfa1 100644
--- a/src/openvpn/oob.h
+++ b/src/openvpn/oob.h
@@ -144,4 +144,56 @@
  */
 bool oob_probe_reply_read(struct buffer *buf, struct oob_probe_reply *r, uint16_t value_len);
 
+/**
+ * Write a complete SERVER_PROBE message (message-type header + probe_parameter
+ * TLV) to @p buf. Sent by the client.
+ */
+bool oob_server_probe_write(struct buffer *buf, const struct oob_probe_parameter *param);
+
+/**
+ * Read a received OOB SERVER_PROBE: verify its message-type header, then scan
+ * for the probe_parameter TLV. TLV types other than probe_parameter are
+ * skipped, so the scan tolerates additional/future TLVs. @p payload is consumed
+ * as it is read.
+ *
+ * @param payload  buffer positioned at the start of the OOB message payload
+ * @param param    filled with the parsed probe_parameter on success
+ * @return true if the header matched and a well-formed probe_parameter was
+ *         found, false otherwise
+ */
+bool oob_server_probe_read(struct buffer *payload, struct oob_probe_parameter *param);
+
+/**
+ * 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
+ * in the wire protocol specification).
+ *
+ * @param probe_ts     timestamp from the probe_parameter (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);
+
+/**
+ * Process the TLV payload of a received SERVER_PROBE and decide whether to
+ * answer it. Combines oob_server_probe_read() and oob_timestamp_in_window():
+ * the probe is dropped (false returned) if it has no valid probe_parameter or
+ * its timestamp is outside the acceptable window. On success @p reply is
+ * populated with the peer's session id echoed back and the remaining fields
+ * zeroed, ready to be wrapped and sent.
+ *
+ * This is the transport-agnostic decision step; the caller performs the send.
+ *
+ * @param probe_payload  TLV payload of the received OOB SERVER_PROBE
+ * @param now            current time (UNIX seconds)
+ * @param window_secs    acceptable timestamp skew, in either direction
+ * @param peer_sid       session id of the requesting peer (echoed in the reply)
+ * @param reply          filled with the reply to send on success
+ * @return true if a reply should be sent, false to silently drop the probe
+ */
+bool oob_build_probe_reply(struct buffer *probe_payload, uint64_t now, uint64_t window_secs,
+                           const struct session_id *peer_sid, struct oob_probe_reply *reply);
+
 #endif /* OOB_H */
diff --git a/tests/unit_tests/openvpn/test_oob.c b/tests/unit_tests/openvpn/test_oob.c
index 40dcaf9..a9998d2 100644
--- a/tests/unit_tests/openvpn/test_oob.c
+++ b/tests/unit_tests/openvpn/test_oob.c
@@ -172,6 +172,180 @@
     gc_free(&gc);
 }
 
+/* A SERVER_PROBE carrying just a probe_parameter is found by the scan. */
+static void
+test_server_probe_read_finds_parameter(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    const struct oob_probe_parameter in = {
+        .timestamp = 0x1122334455667788ULL,
+        .flags = 0,
+    };
+    assert_true(oob_server_probe_write(&buf, &in));
+
+    struct oob_probe_parameter out = { 0 };
+    assert_true(oob_server_probe_read(&buf, &out));
+    assert_true(in.timestamp == out.timestamp);
+    assert_int_equal(in.flags, out.flags);
+
+    gc_free(&gc);
+}
+
+/* TLVs other than probe_parameter are skipped, so the scan finds the
+ * probe_parameter even when preceded by an unknown TLV. */
+static void
+test_server_probe_read_skips_unknown(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    /* SERVER_PROBE message header, then an unknown TLV (type 0x7ff) ... */
+    assert_true(oob_msg_write_header(&buf, OOB_MSG_SERVER_PROBE));
+    assert_true(oob_tlv_write_header(&buf, 0x7ff, false, 4));
+    assert_true(buf_write_u32(&buf, 0xcafef00d));
+    /* ... followed by the real probe_parameter */
+    const struct oob_probe_parameter in = { .timestamp = 42, .flags = 0 };
+    assert_true(oob_probe_parameter_write(&buf, &in));
+
+    struct oob_probe_parameter out = { 0 };
+    assert_true(oob_server_probe_read(&buf, &out));
+    assert_true(out.timestamp == 42);
+
+    gc_free(&gc);
+}
+
+/* A payload with no probe_parameter must be rejected. */
+static void
+test_server_probe_read_missing(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    assert_true(oob_msg_write_header(&buf, OOB_MSG_SERVER_PROBE));
+    assert_true(oob_tlv_write_header(&buf, 0x7ff, false, 4));
+    assert_true(buf_write_u32(&buf, 0));
+
+    struct oob_probe_parameter out = { 0 };
+    assert_false(oob_server_probe_read(&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_server_probe_read_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(oob_msg_write_header(&buf, OOB_MSG_SERVER_PROBE));
+    assert_true(oob_tlv_write_header(&buf, 0x7ff, false, 16));
+
+    struct oob_probe_parameter out = { 0 };
+    assert_false(oob_server_probe_read(&buf, &out));
+
+    gc_free(&gc);
+}
+
+/* A SERVER_PROBE reader rejects a payload carrying a different message type
+ * (here a PROBE_REPLY's), even if it contains a valid probe_parameter TLV. */
+static void
+test_server_probe_read_wrong_msg_type(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    assert_true(oob_msg_write_header(&buf, OOB_MSG_PROBE_REPLY));
+    const struct oob_probe_parameter in = { .timestamp = 42, .flags = 0 };
+    assert_true(oob_probe_parameter_write(&buf, &in));
+
+    struct oob_probe_parameter out = { 0 };
+    assert_false(oob_server_probe_read(&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 valid, in-window SERVER_PROBE yields a reply that echoes the peer's
+ * session id and zeroes the remaining fields. */
+static void
+test_build_probe_reply_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_parameter probe = { .timestamp = now, .flags = 0 };
+    assert_true(oob_server_probe_write(&buf, &probe));
+
+    struct session_id peer;
+    memcpy(peer.id, "PEER1234", SID_SIZE);
+
+    struct oob_probe_reply reply;
+    assert_true(oob_build_probe_reply(&buf, now, 30, &peer, &reply));
+    assert_memory_equal(reply.peer_session_id.id, peer.id, SID_SIZE);
+    assert_int_equal(reply.priority, 0);
+    assert_int_equal(reply.weight, 0);
+    assert_int_equal(reply.connect_lifetime, 0);
+    assert_int_equal(reply.flags, 0);
+
+    gc_free(&gc);
+}
+
+/* A probe whose timestamp is outside the window is dropped (no reply). */
+static void
+test_build_probe_reply_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_parameter probe = { .timestamp = now - 1000, .flags = 0 };
+    assert_true(oob_server_probe_write(&buf, &probe));
+
+    struct session_id peer = { 0 };
+    struct oob_probe_reply reply;
+    assert_false(oob_build_probe_reply(&buf, now, 30, &peer, &reply));
+
+    gc_free(&gc);
+}
+
+/* A payload without a probe_parameter is dropped (no reply). */
+static void
+test_build_probe_reply_no_parameter(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct buffer buf = alloc_buf_gc(128, &gc);
+
+    assert_true(oob_msg_write_header(&buf, OOB_MSG_SERVER_PROBE));
+    assert_true(oob_tlv_write_header(&buf, 0x7ff, false, 4));
+    assert_true(buf_write_u32(&buf, 0));
+
+    struct session_id peer = { 0 };
+    struct oob_probe_reply reply;
+    assert_false(oob_build_probe_reply(&buf, 1000000, 30, &peer, &reply));
+
+    gc_free(&gc);
+}
+
 int
 main(void)
 {
@@ -182,6 +356,15 @@
         cmocka_unit_test(test_probe_parameter_forward_compat),
         cmocka_unit_test(test_probe_parameter_too_short),
         cmocka_unit_test(test_tlv_header_truncated),
+        cmocka_unit_test(test_server_probe_read_finds_parameter),
+        cmocka_unit_test(test_server_probe_read_skips_unknown),
+        cmocka_unit_test(test_server_probe_read_missing),
+        cmocka_unit_test(test_server_probe_read_truncated),
+        cmocka_unit_test(test_server_probe_read_wrong_msg_type),
+        cmocka_unit_test(test_timestamp_in_window),
+        cmocka_unit_test(test_build_probe_reply_valid),
+        cmocka_unit_test(test_build_probe_reply_stale),
+        cmocka_unit_test(test_build_probe_reply_no_parameter),
     };
 
     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: newchange
Gerrit-Project: openvpn
Gerrit-Branch: master
Gerrit-Change-Id: Iafa9efc1c156974ad9f5752e9de810a8c2f68c30
Gerrit-Change-Number: 1742
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.