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/+/1746?usp=email
to review the following change.
Change subject: oob: Add probe-result ranking for server selection
......................................................................
oob: Add probe-result ranking for server selection
Add oob_rank_probe_results(), which orders remotes best-first from their probe
results, following the DNS-SRV (RFC 2782) semantics of the probe_reply TLV:
- remotes that answered rank before those that did not (non-responders keep
their original relative order, last);
- responders are grouped by priority, lowest value first (an absolute
ordering, never overridden by latency or weight);
- within a priority group the "candidates" are the responders whose RTT is
within a margin of the fastest (see oob_effective_margin()); candidates
rank ahead of non-candidates;
- candidates are ordered by RFC-2782 weighted-random selection by weight, so
a server is chosen first with probability proportional to its weight (load
distribution); non-candidates follow, ordered by RTT.
The RNG is injected as a function pointer so this module stays free of the
crypto layer and the weighted ordering is deterministically unit-testable
(production passes get_random; the tests pass stubs).
RTT is only populated once the client measures it (a later commit); until then
every responder sits in the band and selection is purely weighted-random.
Change-Id: I55da68cc341bcfc707fd34ca2f84ff9b6a55501f
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, 343 insertions(+), 0 deletions(-)
git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/46/1746/1
diff --git a/src/openvpn/oob.c b/src/openvpn/oob.c
index 9cc7c42..9b359dd 100644
--- a/src/openvpn/oob.c
+++ b/src/openvpn/oob.c
@@ -241,3 +241,151 @@
/* priority/weight/connect_lifetime/flags left at 0 for now */
return true;
}
+
+/* Base ordering: responders before non-responders, then by priority (lower
+ * first), then by RTT (lower first), then by original index for determinism.
+ * This groups responders into priority runs pre-sorted by RTT, which the
+ * candidate-band step below relies on (run[0] is the fastest in its group). */
+static int
+oob_probe_result_compare(const void *a, const void *b)
+{
+ const struct oob_probe_result *ra = a;
+ const struct oob_probe_result *rb = b;
+
+ if (ra->responded != rb->responded)
+ {
+ return ra->responded ? -1 : 1;
+ }
+ if (ra->responded)
+ {
+ if (ra->priority != rb->priority)
+ {
+ return ra->priority < rb->priority ? -1 : 1;
+ }
+ if (ra->rtt_ms != rb->rtt_ms)
+ {
+ return ra->rtt_ms < rb->rtt_ms ? -1 : 1;
+ }
+ }
+ return ra->index - rb->index;
+}
+
+int
+oob_effective_margin(const struct oob_probe_result *r, int client_margin)
+{
+ if (client_margin >= 0)
+ {
+ return client_margin; /* the client's own setting is authoritative */
+ }
+ if (r->max_latency_diff > 0)
+ {
+ return (int)r->max_latency_diff; /* else the server's advertised value
*/
+ }
+ return OOB_DEFAULT_LATENCY_MARGIN_MS;
+}
+
+/* Reorder the index list @p idx[0..m) into DNS-SRV (RFC 2782) weighted-random
+ * order by results[idx[k]].weight: each position is filled by a remaining
entry
+ * chosen with probability proportional to its weight. When all remaining
+ * weights are 0 the current (RTT-sorted) order is kept. */
+static void
+oob_weighted_order(const struct oob_probe_result *results, int *idx, int m,
long (*rng)(void))
+{
+ for (int pos = 0; pos < m; pos++)
+ {
+ long sum = 0;
+ for (int k = pos; k < m; k++)
+ {
+ sum += results[idx[k]].weight;
+ }
+ int chosen = pos;
+ if (sum > 0)
+ {
+ long r = rng() % sum; /* uniform in [0, sum) */
+ long acc = 0;
+ for (int k = pos; k < m; k++)
+ {
+ acc += results[idx[k]].weight;
+ if (acc > r)
+ {
+ chosen = k;
+ break;
+ }
+ }
+ }
+ int t = idx[pos];
+ idx[pos] = idx[chosen];
+ idx[chosen] = t;
+ }
+}
+
+/* Reorder one priority run (@p run[0..m), already RTT-sorted) in place:
+ * candidates (RTT within the band of the fastest) first, ordered by weighted
+ * random; then non-candidates in RTT order. */
+static void
+oob_order_priority_run(struct oob_probe_result *run, int m, int client_margin,
long (*rng)(void),
+ struct gc_arena *gc)
+{
+ if (m <= 1)
+ {
+ return;
+ }
+
+ unsigned int best_rtt = run[0].rtt_ms; /* run is RTT-sorted: [0] is
fastest */
+
+ int *cand = gc_malloc(sizeof(int) * m, false, gc);
+ int *non = gc_malloc(sizeof(int) * m, false, gc);
+ int nc = 0;
+ int nn = 0;
+ for (int k = 0; k < m; k++)
+ {
+ if (run[k].rtt_ms - best_rtt < (unsigned
int)oob_effective_margin(&run[k], client_margin))
+ {
+ cand[nc++] = k;
+ }
+ else
+ {
+ non[nn++] = k;
+ }
+ }
+
+ oob_weighted_order(run, cand, nc, rng);
+
+ struct oob_probe_result *tmp = gc_malloc(sizeof(*tmp) * m, false, gc);
+ int t = 0;
+ for (int k = 0; k < nc; k++)
+ {
+ tmp[t++] = run[cand[k]];
+ }
+ for (int k = 0; k < nn; k++)
+ {
+ tmp[t++] = run[non[k]];
+ }
+ memcpy(run, tmp, sizeof(*run) * m);
+}
+
+void
+oob_rank_probe_results(struct oob_probe_result *results, int n, int
client_margin,
+ long (*rng)(void), struct gc_arena *gc)
+{
+ if (n <= 1)
+ {
+ return;
+ }
+
+ /* Base order: responders first, grouped by priority, RTT-sorted within. */
+ qsort(results, (size_t)n, sizeof(*results), oob_probe_result_compare);
+
+ /* Reorder each priority run of responders by candidate-band + weight. */
+ int i = 0;
+ while (i < n && results[i].responded)
+ {
+ int j = i;
+ while (j < n && results[j].responded && results[j].priority ==
results[i].priority)
+ {
+ j++;
+ }
+ oob_order_priority_run(results + i, j - i, client_margin, rng, gc);
+ i = j;
+ }
+}
diff --git a/src/openvpn/oob.h b/src/openvpn/oob.h
index 9040416..65e84bd 100644
--- a/src/openvpn/oob.h
+++ b/src/openvpn/oob.h
@@ -215,4 +215,55 @@
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);
+/* Candidate-band margin (ms) used when neither the client nor the server
+ * specifies one. */
+#define OOB_DEFAULT_LATENCY_MARGIN_MS 10
+
+/* Outcome of probing one remote, used to order remotes best-first. @index is
+ * the caller's identifier for the remote (e.g. its position in the connection
+ * list); priority/weight are only meaningful when @responded is true. */
+struct oob_probe_result
+{
+ int index;
+ bool responded;
+ uint16_t priority;
+ uint16_t weight;
+ uint16_t max_latency_diff; /* margin advertised by this server (ms); 0 =
use client default */
+ unsigned int rtt_ms; /* probe round-trip time in ms (responders
only) */
+};
+
+/**
+ * Order @p results best-first, in place, per the server-probe selection
policy:
+ * - remotes that responded rank before those that did not (non-responders
keep
+ * their original relative order, last);
+ * - responders are grouped by priority, lowest priority value first (an
+ * absolute ordering, never overridden by latency or weight);
+ * - within a priority group, the "candidates" are the responders whose RTT
is
+ * within a margin of the fastest in the group (see
oob_effective_margin()).
+ * Candidates are ordered ahead of non-candidates;
+ * - candidates are ordered by DNS-SRV (RFC 2782) weighted-random selection
by
+ * weight, so a server is chosen first with probability proportional to its
+ * weight (load distribution). Non-candidates follow, ordered by RTT.
+ *
+ * @param results results to reorder in place
+ * @param n number of results
+ * @param client_margin client's candidate-band margin in ms, or < 0 if the
+ * client did not set one (see oob_effective_margin())
+ * @param rng returns a non-negative random value (e.g. get_random);
+ * injected so this module stays free of the crypto layer
+ * and the weighted ordering is deterministically
testable
+ * @param gc arena for scratch allocation
+ */
+void oob_rank_probe_results(struct oob_probe_result *results, int n, int
client_margin,
+ long (*rng)(void), struct gc_arena *gc);
+
+/**
+ * The candidate-band margin (ms) that applies to one probed remote. The
client's
+ * own setting is authoritative: when @p client_margin >= 0 (the client set
+ * --server-probe with a value) it is used for every remote. Otherwise the
+ * server's advertised @c max_latency_diff is used when non-zero, falling back
to
+ * OOB_DEFAULT_LATENCY_MARGIN_MS.
+ */
+int oob_effective_margin(const struct oob_probe_result *r, int client_margin);
+
#endif /* OOB_H */
diff --git a/tests/unit_tests/openvpn/test_oob.c
b/tests/unit_tests/openvpn/test_oob.c
index 1029399..b66ade2 100644
--- a/tests/unit_tests/openvpn/test_oob.c
+++ b/tests/unit_tests/openvpn/test_oob.c
@@ -429,6 +429,144 @@
gc_free(&gc);
}
+/* Deterministic RNG stubs for the weighted-selection ordering. rank_rng_zero
+ * makes the weighted draw always pick the first remaining candidate,
preserving
+ * order; rank_rng_fixed returns a value we set to land in a chosen weight
slice. */
+static long
+rank_rng_zero(void)
+{
+ return 0;
+}
+
+static long rank_rng_value;
+static long
+rank_rng_fixed(void)
+{
+ return rank_rng_value;
+}
+
+/* Responders rank ahead of non-responders regardless of index order. */
+static void
+test_rank_responder_before_nonresponder(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct oob_probe_result r[] = {
+ { .index = 0, .responded = false },
+ { .index = 1, .responded = true, .priority = 100, .weight = 50 },
+ };
+ oob_rank_probe_results(r, 2, 10, rank_rng_zero, &gc);
+ assert_int_equal(r[0].index, 1);
+ assert_int_equal(r[1].index, 0);
+ gc_free(&gc);
+}
+
+/* Among responders, the lowest priority value wins (an absolute ordering). */
+static void
+test_rank_by_priority(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct oob_probe_result r[] = {
+ { .index = 0, .responded = true, .priority = 20, .weight = 50 },
+ { .index = 1, .responded = true, .priority = 5, .weight = 50 },
+ { .index = 2, .responded = true, .priority = 10, .weight = 50 },
+ };
+ oob_rank_probe_results(r, 3, 10, rank_rng_zero, &gc);
+ assert_int_equal(r[0].index, 1); /* priority 5 */
+ assert_int_equal(r[1].index, 2); /* priority 10 */
+ assert_int_equal(r[2].index, 0); /* priority 20 */
+ gc_free(&gc);
+}
+
+/* Within a priority, only servers within the latency margin of the fastest are
+ * candidates; a slower (out-of-band) server ranks behind a faster one no
matter
+ * how large its weight. */
+static void
+test_rank_candidate_band(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct oob_probe_result r[] = {
+ { .index = 0, .responded = true, .priority = 10, .weight = 1000,
.rtt_ms = 100 },
+ { .index = 1, .responded = true, .priority = 10, .weight = 1, .rtt_ms
= 20 },
+ };
+ /* margin 10ms: 20ms is fastest; 100ms is 80ms slower -> out of band */
+ oob_rank_probe_results(r, 2, 10, rank_rng_zero, &gc);
+ assert_int_equal(r[0].index, 1); /* fast, in-band, despite tiny weight */
+ assert_int_equal(r[1].index, 0); /* slow, out-of-band, despite huge weight
*/
+ gc_free(&gc);
+}
+
+/* A server widens its own band via the advertised max_latency_diff, joining
the
+ * candidate set even when it is well behind the fastest; it then participates
in
+ * the weighted selection. */
+static void
+test_rank_advertised_margin(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct oob_probe_result r[] = {
+ { .index = 0, .responded = true, .priority = 10, .weight = 1, .rtt_ms
= 20 },
+ { .index = 1,
+ .responded = true,
+ .priority = 10,
+ .weight = 1000,
+ .rtt_ms = 100,
+ .max_latency_diff = 200 },
+ };
+ /* Client did not set a margin (-1), so each server's advertised value
+ * applies: the 100ms server advertises 200 -> it is a candidate (the
+ * default 10 would have excluded it); with weight 1000 (slice [1,1001)) a
+ * draw of 500 selects it first. */
+ rank_rng_value = 500;
+ oob_rank_probe_results(r, 2, -1, rank_rng_fixed, &gc);
+ assert_int_equal(r[0].index, 1);
+ gc_free(&gc);
+}
+
+/* Among candidates, weight drives RFC-2782 proportional selection: a draw is
+ * mapped to the server whose cumulative weight slice it falls in. */
+static void
+test_rank_weighted_selection(void **state)
+{
+ struct gc_arena gc = gc_new();
+ /* equal priority and RTT -> both in band; weights 30 and 70, sum 100:
+ * index 0 owns [0,30), index 1 owns [30,100). */
+ const struct oob_probe_result base[] = {
+ { .index = 0, .responded = true, .priority = 10, .weight = 30, .rtt_ms
= 20 },
+ { .index = 1, .responded = true, .priority = 10, .weight = 70, .rtt_ms
= 20 },
+ };
+ struct oob_probe_result r[2];
+
+ memcpy(r, base, sizeof(base));
+ rank_rng_value = 10; /* falls in index 0's slice */
+ oob_rank_probe_results(r, 2, 50, rank_rng_fixed, &gc);
+ assert_int_equal(r[0].index, 0);
+
+ memcpy(r, base, sizeof(base));
+ rank_rng_value = 50; /* falls in index 1's slice */
+ oob_rank_probe_results(r, 2, 50, rank_rng_fixed, &gc);
+ assert_int_equal(r[0].index, 1);
+
+ gc_free(&gc);
+}
+
+/* Non-responders are placed last, keeping their original relative order. */
+static void
+test_rank_nonresponders_last(void **state)
+{
+ struct gc_arena gc = gc_new();
+ struct oob_probe_result r[] = {
+ { .index = 0, .responded = false },
+ { .index = 1, .responded = true, .priority = 10, .weight = 50, .rtt_ms
= 20 },
+ { .index = 2, .responded = false },
+ { .index = 3, .responded = true, .priority = 10, .weight = 50, .rtt_ms
= 20 },
+ };
+ oob_rank_probe_results(r, 4, 10, rank_rng_zero, &gc);
+ assert_int_equal(r[0].index, 1); /* responder (rng_zero keeps order) */
+ assert_int_equal(r[1].index, 3); /* responder */
+ assert_int_equal(r[2].index, 0); /* non-responder, original order kept */
+ assert_int_equal(r[3].index, 2);
+ gc_free(&gc);
+}
+
int
main(void)
{
@@ -452,6 +590,12 @@
cmocka_unit_test(test_client_reply_read_skips_unknown),
cmocka_unit_test(test_client_reply_read_missing),
cmocka_unit_test(test_client_reply_read_wrong_msg_type),
+ cmocka_unit_test(test_rank_responder_before_nonresponder),
+ cmocka_unit_test(test_rank_by_priority),
+ cmocka_unit_test(test_rank_candidate_band),
+ cmocka_unit_test(test_rank_advertised_margin),
+ cmocka_unit_test(test_rank_weighted_selection),
+ cmocka_unit_test(test_rank_nonresponders_last),
};
return cmocka_run_group_tests_name("oob tests", tests, NULL, NULL);
--
To view, visit http://gerrit.openvpn.net/c/openvpn/+/1746?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: I55da68cc341bcfc707fd34ca2f84ff9b6a55501f
Gerrit-Change-Number: 1746
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