mirror of
https://github.com/openziti/ziti-sdk-c.git
synced 2026-09-10 16:55:38 +00:00
185 lines
7.3 KiB
C++
185 lines
7.3 KiB
C++
// Copyright (c) 2026. NetFoundry Inc
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// ctrl_next_ep() picks the endpoint ziti_ctrl_init() starts on. Its liveness test reads
|
|
// uv_now(loop), which on a loop that has not run yet is milliseconds since boot, so the selection
|
|
// has to hold for a small clock as well as a large one: a client started by systemd or as a Windows
|
|
// service runs within a minute of boot.
|
|
//
|
|
// Addresses come from the RFC 5737 documentation range so nothing resolves or connects.
|
|
|
|
#include "catch2_includes.hpp"
|
|
|
|
#include "ziti_ctrl.h"
|
|
#include <ziti/errors.h>
|
|
#include <catch2/generators/catch_generators.hpp>
|
|
#include <tlsuv/tlsuv.h>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
// One loop for every case here, never run and never closed. Creating and closing a loop per case
|
|
// aborted intermittently on libuv's `fd > STDERR_FILENO` assertion once the whole suite ran in the
|
|
// same process - 2 of 5 runs - while the cases on their own passed 20 of 20. Reusing one loop is
|
|
// stable and costs a single allocation for the life of the process.
|
|
uv_loop_t *test_loop() {
|
|
static uv_loop_t *loop = uv_loop_new();
|
|
return loop;
|
|
}
|
|
|
|
// libuv has no setter for the loop's cached time - uv_update_time() only refreshes it from the
|
|
// real clock - so it is assigned directly. Safe in either direction only because these cases never
|
|
// run the loop; uv_run() asserts that the clock never moves backwards.
|
|
void simulate_uptime(uv_loop_t *loop, uint64_t ms) {
|
|
loop->time = ms;
|
|
}
|
|
|
|
// 0 means "use the host's own uptime". The loop is shared, so every case sets the time it wants
|
|
// rather than inheriting whatever the previous one left. uv_update_time() is not an option: it
|
|
// asserts the clock never moves backwards, and a previous case may have simulated a larger value.
|
|
void init_and_check_selection(uint64_t uptime_ms, const std::vector<const char *> &endpoints) {
|
|
uv_loop_t *loop = test_loop();
|
|
simulate_uptime(loop, uptime_ms > 0 ? uptime_ms : uv_hrtime() / 1000000);
|
|
|
|
tls_context *tls = default_tls_context(nullptr, 0);
|
|
|
|
model_list urls = {};
|
|
for (const char *ep: endpoints) {
|
|
model_list_append(&urls, (void *) ep);
|
|
}
|
|
|
|
ziti_controller ctrl{};
|
|
REQUIRE(ziti_ctrl_init(loop, &ctrl, &urls, tls) == ZITI_OK);
|
|
|
|
const std::string chosen = cstr_str(&ctrl.url);
|
|
bool matched = false;
|
|
for (const char *ep: endpoints) {
|
|
matched = matched || chosen == ep;
|
|
}
|
|
CHECK(matched);
|
|
|
|
model_list_clear(&urls, nullptr);
|
|
ziti_ctrl_close(&ctrl);
|
|
tls->free_ctx(tls);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("controller-init-selects-endpoint-within-a-minute-of-boot", "[controller]") {
|
|
// 60s is the last value that fails without the fix and 61s the first that passes: the check is
|
|
// `offline_time + ONE_MINUTE < now`. A week of uptime covers the far side.
|
|
const uint64_t uptime = GENERATE(uint64_t{1}, uint64_t{5 * 1000}, uint64_t{60 * 1000},
|
|
uint64_t{61 * 1000}, uint64_t{7 * 24 * 60 * 60 * 1000});
|
|
|
|
init_and_check_selection(uptime, {"https://203.0.113.1:1280"});
|
|
}
|
|
|
|
TEST_CASE("controller-init-selects-endpoint-with-the-host-clock", "[controller]") {
|
|
// whatever the machine's real uptime is, rather than a simulated one
|
|
init_and_check_selection(0, {"https://203.0.113.1:1280"});
|
|
}
|
|
|
|
TEST_CASE("controller-init-selects-one-of-several-endpoints", "[controller]") {
|
|
const uint64_t uptime = GENERATE(uint64_t{5 * 1000}, uint64_t{61 * 1000});
|
|
|
|
init_and_check_selection(uptime, {"https://203.0.113.1:1280", "https://203.0.113.2:1280",
|
|
"https://203.0.113.3:1280"});
|
|
}
|
|
|
|
// The cases above only reach the "never tried" half of the candidate test, since every endpoint is
|
|
// freshly allocated when ziti_ctrl_init selects one. These drive the selection directly so the backoff
|
|
// arithmetic is under test too.
|
|
|
|
TEST_CASE("endpoint-backoff-lasts-a-minute-after-a-failure", "[controller]") {
|
|
const uint64_t failed_at = 200 * 1000;
|
|
|
|
uv_loop_t *loop = test_loop();
|
|
simulate_uptime(loop, failed_at);
|
|
|
|
tls_context *tls = default_tls_context(nullptr, 0);
|
|
|
|
model_list urls = {};
|
|
model_list_append(&urls, (void *) "https://203.0.113.1:1280");
|
|
|
|
ziti_controller ctrl{};
|
|
REQUIRE(ziti_ctrl_init(loop, &ctrl, &urls, tls) == ZITI_OK);
|
|
|
|
// reporting the only endpoint as failed leaves nothing to fall back to
|
|
CHECK(ziti_ctrl_next_ep(&ctrl, "https://203.0.113.1:1280") == nullptr);
|
|
|
|
// still cooling down: the test is `offline_time + ONE_MINUTE < now`, so the minute mark is out
|
|
simulate_uptime(loop, failed_at + 60 * 1000);
|
|
CHECK(ziti_ctrl_next_ep(&ctrl, nullptr) == nullptr);
|
|
|
|
// a millisecond past the minute it is a candidate again
|
|
simulate_uptime(loop, failed_at + 60 * 1000 + 1);
|
|
const char *revived = ziti_ctrl_next_ep(&ctrl, nullptr);
|
|
REQUIRE(revived != nullptr);
|
|
CHECK(std::string(revived) == "https://203.0.113.1:1280");
|
|
|
|
model_list_clear(&urls, nullptr);
|
|
ziti_ctrl_close(&ctrl);
|
|
tls->free_ctx(tls);
|
|
}
|
|
|
|
TEST_CASE("endpoint-selection-skips-the-one-still-cooling-down", "[controller]") {
|
|
// 100001ms is picked so that now % (number of endpoints) lands past the end of the candidate list,
|
|
// which is what the selection used to index by.
|
|
const uint64_t failed_at = 100 * 1000;
|
|
const uint64_t now = 100001;
|
|
|
|
uv_loop_t *loop = test_loop();
|
|
simulate_uptime(loop, failed_at);
|
|
|
|
tls_context *tls = default_tls_context(nullptr, 0);
|
|
|
|
model_list urls = {};
|
|
model_list_append(&urls, (void *) "https://203.0.113.1:1280");
|
|
model_list_append(&urls, (void *) "https://203.0.113.2:1280");
|
|
model_list_append(&urls, (void *) "https://203.0.113.3:1280");
|
|
|
|
ziti_controller ctrl{};
|
|
REQUIRE(ziti_ctrl_init(loop, &ctrl, &urls, tls) == ZITI_OK);
|
|
|
|
// one endpoint fails, so two of the three remain candidates
|
|
ziti_ctrl_next_ep(&ctrl, "https://203.0.113.1:1280");
|
|
REQUIRE(now % model_map_size(&ctrl.endpoints) == 2);
|
|
|
|
simulate_uptime(loop, now);
|
|
const char *chosen = ziti_ctrl_next_ep(&ctrl, nullptr);
|
|
REQUIRE(chosen != nullptr);
|
|
CHECK(std::string(chosen) != "https://203.0.113.1:1280");
|
|
|
|
model_list_clear(&urls, nullptr);
|
|
ziti_ctrl_close(&ctrl);
|
|
tls->free_ctx(tls);
|
|
}
|
|
|
|
TEST_CASE("controller-init-rejects-empty-endpoint-list", "[controller]") {
|
|
uv_loop_t *loop = test_loop();
|
|
simulate_uptime(loop, 5 * 1000);
|
|
|
|
tls_context *tls = default_tls_context(nullptr, 0);
|
|
|
|
model_list urls = {};
|
|
ziti_controller ctrl{};
|
|
|
|
// no endpoint to select leaves the url unset, so init has to fail instead of handing that on
|
|
CHECK(ziti_ctrl_init(loop, &ctrl, &urls, tls) != ZITI_OK);
|
|
|
|
ziti_ctrl_close(&ctrl);
|
|
tls->free_ctx(tls);
|
|
}
|