Skip to content

Commit 117a5e0

Browse files
committed
fix(core): avoid socket timeouts
1 parent 8d328ae commit 117a5e0

7 files changed

Lines changed: 232 additions & 54 deletions

File tree

src/cmd/test_routing.cpp

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -450,39 +450,29 @@ std::optional<bool> test_rule_ipset_membership(const KernelSetTester& set_tester
450450
return false;
451451
}
452452

453-
std::string find_actual_outbound(const KernelSetTester& set_tester,
454-
const std::vector<RuleState>& rule_states,
455-
const std::string& ip,
456-
bool is_v4) {
453+
std::string find_actual_outbound(
454+
const std::vector<RuleState>& rule_states,
455+
const std::vector<RuleIpDiagnostic>& rule_ip_diagnostics) {
457456
bool any_answer = false;
458457

459-
for (const auto& rs : rule_states) {
458+
const size_t count = std::min(rule_states.size(), rule_ip_diagnostics.size());
459+
for (size_t idx = 0; idx < count; ++idx) {
460+
const auto& rs = rule_states[idx];
460461
if (rs.action_type == RuleActionType::Skip) continue;
461462

462-
for (const auto& set_name : rs.set_names) {
463-
bool v4_set = has_prefix(set_name, "kpbr4_") || has_prefix(set_name, "kpbr4s_") ||
464-
has_prefix(set_name, "kpbr4S_") || has_prefix(set_name, "kpbr4d_");
465-
bool v6_set = has_prefix(set_name, "kpbr6_") || has_prefix(set_name, "kpbr6s_") ||
466-
has_prefix(set_name, "kpbr6S_") || has_prefix(set_name, "kpbr6d_");
467-
468-
if (is_v4 && !v4_set) continue;
469-
if (!is_v4 && !v6_set) continue;
470-
471-
auto result = set_tester.contains(set_name, ip);
472-
if (result.has_value()) {
473-
any_answer = true;
474-
if (*result) return rs.outbound_tag;
475-
}
463+
const auto& membership = rule_ip_diagnostics[idx].in_ipset;
464+
if (membership.has_value()) {
465+
any_answer = true;
466+
if (*membership) return rs.outbound_tag;
476467
}
477468
}
478469

479470
return any_answer ? "(default)" : "(unknown)";
480471
}
481472

482-
// Kernel membership checks invoke nft/ipset subprocesses. Keep a modest bound
483-
// so a domain with a very large DNS response cannot overwhelm the router, while
484-
// still checking different IPs concurrently.
485-
constexpr std::size_t kTestRoutingMaxConcurrentIps = 8;
473+
// Kernel membership checks invoke nft/ipset subprocesses. Keep the per-request
474+
// bound low because the daemon may run multiple routing tests concurrently.
475+
constexpr std::size_t kTestRoutingMaxConcurrentIps = 2;
486476

487477
struct PerIpRoutingResult {
488478
TestRoutingEntry entry;
@@ -566,10 +556,6 @@ TestRoutingResult compute_test_routing(const Config& config,
566556
auto [expected, match] = find_expected_outbound(config, lookups, ip, domain_cands);
567557
per_ip.entry.expected_outbound = expected;
568558
per_ip.entry.list_match = std::move(match);
569-
per_ip.entry.actual_outbound = set_tester.has_value()
570-
? find_actual_outbound(*set_tester, rule_states, ip, is_ipv4_address(ip))
571-
: "(unknown)";
572-
per_ip.entry.ok = (per_ip.entry.expected_outbound == per_ip.entry.actual_outbound);
573559

574560
per_ip.rule_ip_diagnostics.reserve(result.rule_diagnostics.size());
575561
for (size_t idx = 0; idx < result.rule_diagnostics.size(); ++idx) {
@@ -581,6 +567,11 @@ TestRoutingResult compute_test_routing(const Config& config,
581567
}
582568
per_ip.rule_ip_diagnostics.push_back(std::move(ip_diag));
583569
}
570+
571+
per_ip.entry.actual_outbound = set_tester.has_value()
572+
? find_actual_outbound(rule_states, per_ip.rule_ip_diagnostics)
573+
: "(unknown)";
574+
per_ip.entry.ok = (per_ip.entry.expected_outbound == per_ip.entry.actual_outbound);
584575
};
585576

586577
const size_t worker_count = std::min(kTestRoutingMaxConcurrentIps, ips.size());

src/daemon/daemon.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ class Daemon {
160160
void handle_control_commands();
161161
void setup_ipc_control_socket();
162162
void handle_ipc_control_socket();
163+
bool try_begin_routing_test();
164+
void finish_routing_test();
163165
void remove_ipc_control_socket() noexcept;
164166
void wake_control_loop();
165167
bool is_event_loop_thread() const;
@@ -380,12 +382,16 @@ IcmpTester icmp_tester_;
380382
BlockingExecutor resolver_stream_executor_{1, 16};
381383
BlockingExecutor resolver_io_executor_{1, 32};
382384
BlockingExecutor lifecycle_executor_{1, 16};
385+
// Routing diagnostics are CPU/process-heavy. Two workers allow API and CLI
386+
// tests to overlap while the small queue keeps resource use bounded.
387+
BlockingExecutor routing_test_executor_{2, 2};
383388
std::atomic<std::uint64_t> runtime_generation_{1};
384389
std::atomic<bool> remote_list_refresh_inflight_{false};
385390
std::atomic<bool> ipc_mutation_inflight_{false};
386391
std::atomic<bool> ipc_resolver_hook_inflight_{false};
387392
std::atomic<bool> resolver_hash_refresh_inflight_{false};
388393
std::atomic<std::uint64_t> resolver_stream_completed_{0};
394+
std::atomic<std::size_t> routing_tests_inflight_{0};
389395
TracedMutex system_resolver_hook_mutex_;
390396

391397
#ifdef WITH_API

src/daemon/daemon_api.cpp

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -738,11 +738,31 @@ void Daemon::setup_api() {
738738
[this](const std::string& target) {
739739
const Config active_config = config_store_.active_config();
740740
const auto runtime_snapshot = runtime_state_store_.snapshot();
741-
return compute_test_routing(
742-
active_config,
743-
list_service_.cache_manager(),
744-
target,
745-
&runtime_snapshot.firewall_state.get_rules());
741+
const auto realized_rules = runtime_snapshot.firewall_state.get_rules();
742+
if (!try_begin_routing_test()) {
743+
throw ApiError("Too many routing tests are already running", 503);
744+
}
745+
auto pending = routing_test_executor_.try_submit(
746+
"api-test-routing",
747+
[this, active_config, realized_rules, target] {
748+
try {
749+
auto result = compute_test_routing(
750+
active_config,
751+
list_service_.cache_manager(),
752+
target,
753+
&realized_rules);
754+
finish_routing_test();
755+
return result;
756+
} catch (...) {
757+
finish_routing_test();
758+
throw;
759+
}
760+
});
761+
if (!pending.has_value()) {
762+
finish_routing_test();
763+
throw ApiError("Routing test executor queue is full", 503);
764+
}
765+
return pending->get();
746766
},
747767
[this]() {
748768
begin_config_operation_or_throw(ConfigOperationState::Saving,

src/daemon/daemon_core.cpp

Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ constexpr auto INTERFACE_MONITOR_RECONNECT_RETRY_DELAY =
5656
std::chrono::seconds{5};
5757
constexpr std::size_t kResolverStreamChunkBytes =
5858
static_cast<std::size_t>(16) * 1024U;
59+
constexpr std::size_t kMaxConcurrentRoutingTests = 2;
5960

6061
void send_all(int fd, const char *data, std::size_t size) {
6162
std::size_t written = 0;
@@ -220,6 +221,7 @@ Daemon::~Daemon() {
220221
resolver_hook_executor_.shutdown();
221222
resolver_stream_executor_.shutdown();
222223
resolver_io_executor_.shutdown();
224+
routing_test_executor_.shutdown();
223225
blocking_executor_.shutdown();
224226

225227
if (control_fd_ >= 0) {
@@ -324,6 +326,22 @@ void Daemon::remove_ipc_control_socket() noexcept {
324326
}
325327
}
326328

329+
bool Daemon::try_begin_routing_test() {
330+
std::size_t current = routing_tests_inflight_.load(std::memory_order_acquire);
331+
while (current < kMaxConcurrentRoutingTests) {
332+
if (routing_tests_inflight_.compare_exchange_weak(
333+
current, current + 1, std::memory_order_acq_rel,
334+
std::memory_order_acquire)) {
335+
return true;
336+
}
337+
}
338+
return false;
339+
}
340+
341+
void Daemon::finish_routing_test() {
342+
routing_tests_inflight_.fetch_sub(1, std::memory_order_acq_rel);
343+
}
344+
327345
void Daemon::handle_ipc_control_socket() {
328346
while (true) {
329347
const int client = accept4(ipc_control_fd_, nullptr, nullptr, SOCK_CLOEXEC);
@@ -340,7 +358,7 @@ void Daemon::handle_ipc_control_socket() {
340358
sizeof(timeout));
341359
nlohmann::json request = nlohmann::json::object();
342360
nlohmann::json response;
343-
bool resolver_stream_dispatched = false;
361+
bool client_dispatched = false;
344362
try {
345363
ucred peer{};
346364
socklen_t peer_length = sizeof(peer);
@@ -400,34 +418,78 @@ void Daemon::handle_ipc_control_socket() {
400418
const std::string target = request.value("target", "");
401419
if (target.empty())
402420
throw ipc::ControlProtocolError("test-routing requires a target");
421+
const Config active_config = config_store_.active_config();
403422
const auto runtime_snapshot = runtime_state_store_.snapshot();
404-
const auto result =
405-
compute_test_routing(config_store_.active_config(),
406-
list_service_.cache_manager(), target,
407-
&runtime_snapshot.firewall_state.get_rules());
408-
nlohmann::json entries = nlohmann::json::array();
409-
for (const auto &entry : result.entries) {
410-
nlohmann::json entry_json = {
411-
{"ip", entry.ip},
412-
{"expected_outbound", entry.expected_outbound},
413-
{"actual_outbound", entry.actual_outbound},
414-
{"ok", entry.ok}};
415-
if (entry.list_match.has_value()) {
416-
entry_json["list_match"] = {
417-
{"list_name", entry.list_match->list_name},
418-
{"via", entry.list_match->via}};
419-
}
420-
entries.push_back(std::move(entry_json));
423+
const auto realized_rules = runtime_snapshot.firewall_state.get_rules();
424+
const auto request_snapshot = request;
425+
if (!try_begin_routing_test()) {
426+
response = ipc::make_error_response(
427+
request, "busy", "too many routing tests are already running");
428+
const std::string frame = ipc::encode_message(response);
429+
(void)send(client, frame.data(), frame.size(), MSG_NOSIGNAL);
430+
close(client);
431+
continue;
421432
}
422-
response = {{"protocol_version", ipc::kControlProtocolVersion},
423-
{"request_id", request.at("request_id")},
433+
const bool queued = routing_test_executor_.try_post(
434+
"ipc-test-routing",
435+
[this, client, active_config, realized_rules, request_snapshot,
436+
target] {
437+
nlohmann::json worker_response;
438+
try {
439+
const auto result = compute_test_routing(
440+
active_config, list_service_.cache_manager(), target,
441+
&realized_rules);
442+
nlohmann::json entries = nlohmann::json::array();
443+
for (const auto &entry : result.entries) {
444+
nlohmann::json entry_json = {
445+
{"ip", entry.ip},
446+
{"expected_outbound", entry.expected_outbound},
447+
{"actual_outbound", entry.actual_outbound},
448+
{"ok", entry.ok}};
449+
if (entry.list_match.has_value()) {
450+
entry_json["list_match"] = {
451+
{"list_name", entry.list_match->list_name},
452+
{"via", entry.list_match->via}};
453+
}
454+
entries.push_back(std::move(entry_json));
455+
}
456+
worker_response = {
457+
{"protocol_version", ipc::kControlProtocolVersion},
458+
{"request_id", request_snapshot.at("request_id")},
424459
{"ok", !result.dns_error.has_value()},
425460
{"result",
426461
{{"target", result.target},
427462
{"resolved_ips", result.resolved_ips},
428463
{"entries", std::move(entries)},
429464
{"warnings", result.warnings},
430465
{"dns_error", result.dns_error}}}};
466+
} catch (const std::exception &error) {
467+
worker_response = ipc::make_error_response(
468+
request_snapshot, "daemon_error", error.what());
469+
} catch (...) {
470+
worker_response = ipc::make_error_response(
471+
request_snapshot, "daemon_error",
472+
"routing test failed with an unknown error");
473+
}
474+
475+
finish_routing_test();
476+
477+
try {
478+
const std::string frame = ipc::encode_message(worker_response);
479+
send_all(client, frame.data(), frame.size());
480+
} catch (const std::exception &error) {
481+
Logger::instance().warn(
482+
"test-routing control response failed: {}", error.what());
483+
}
484+
close(client);
485+
});
486+
if (queued) {
487+
client_dispatched = true;
488+
continue;
489+
}
490+
finish_routing_test();
491+
response = ipc::make_error_response(
492+
request, "busy", "routing test executor queue is full");
431493
} else if (operation == "generate-resolver-config") {
432494
const RuntimeState runtime_state = runtime_state_machine_.state();
433495
// The DNS configuration is a daemon-owned desired-state
@@ -527,7 +589,7 @@ void Daemon::handle_ipc_control_socket() {
527589
}
528590
});
529591
if (queued) {
530-
resolver_stream_dispatched = true;
592+
client_dispatched = true;
531593
continue;
532594
}
533595
response = ipc::make_error_response(
@@ -629,7 +691,7 @@ void Daemon::handle_ipc_control_socket() {
629691
response =
630692
ipc::make_error_response(request, "protocol_error", error.what());
631693
}
632-
if (!resolver_stream_dispatched) {
694+
if (!client_dispatched) {
633695
const std::string frame = ipc::encode_message(response);
634696
(void)send(client, frame.data(), frame.size(), MSG_NOSIGNAL);
635697
close(client);
@@ -1376,6 +1438,7 @@ void Daemon::run() {
13761438
resolver_hook_executor_.shutdown();
13771439
resolver_stream_executor_.shutdown();
13781440
resolver_io_executor_.shutdown();
1441+
routing_test_executor_.shutdown();
13791442
blocking_executor_.shutdown();
13801443

13811444
#ifdef WITH_API

src/util/blocking_executor.hpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <functional>
99
#include <memory>
1010
#include <mutex>
11+
#include <optional>
1112
#include <pthread.h>
1213
#include <queue>
1314
#include <stdexcept>
@@ -31,6 +32,37 @@ class BlockingExecutor {
3132
std::function<void()> task,
3233
TraceId trace_id = current_trace_id());
3334

35+
template<typename Fn>
36+
auto try_submit(std::string label,
37+
Fn&& fn,
38+
TraceId trace_id = current_trace_id())
39+
-> std::optional<std::future<typename std::invoke_result_t<Fn>>> {
40+
using Result = typename std::invoke_result_t<Fn>;
41+
42+
auto promise = std::make_shared<std::promise<Result>>();
43+
auto future = promise->get_future();
44+
const bool enqueued = enqueue(
45+
std::move(label),
46+
[promise, fn = std::forward<Fn>(fn)]() mutable {
47+
try {
48+
if constexpr (std::is_void_v<Result>) {
49+
fn();
50+
promise->set_value();
51+
} else {
52+
promise->set_value(fn());
53+
}
54+
} catch (...) {
55+
promise->set_exception(std::current_exception());
56+
}
57+
},
58+
trace_id,
59+
/*block_until_room=*/false);
60+
if (!enqueued) {
61+
return std::nullopt;
62+
}
63+
return std::optional<std::future<Result>>(std::move(future));
64+
}
65+
3466
template<typename Fn>
3567
auto submit(std::string label,
3668
Fn&& fn,

tests/test_test_routing.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,14 @@ TEST_CASE("compute_test_routing includes route rule conditions in diagnostics")
353353
TEST_CASE("compute_test_routing uses realized iptables generation set names") {
354354
const auto temp_dir = make_temp_dir();
355355
const auto bin_dir = temp_dir / "bin";
356+
const auto invocation_log = temp_dir / "ipset-invocations.txt";
356357
std::filesystem::create_directories(bin_dir);
357358

358359
write_executable(bin_dir / "iptables", "#!/bin/sh\nexit 0\n");
359360
write_executable(
360361
bin_dir / "ipset",
361362
"#!/bin/sh\n"
363+
"echo test >> " + invocation_log.string() + "\n"
362364
"if [ \"$1\" = test ] && [ \"$2\" = kpbr4S_remote ] && "
363365
"[ \"$3\" = 203.0.113.10 ]; then\n"
364366
" exit 0\n"
@@ -417,6 +419,12 @@ TEST_CASE("compute_test_routing uses realized iptables generation set names") {
417419
REQUIRE(result.rule_diagnostics.front().ip_rows.front().in_ipset.has_value());
418420
CHECK(*result.rule_diagnostics.front().ip_rows.front().in_ipset);
419421

422+
std::ifstream invocations(invocation_log);
423+
const std::string invocation_contents{
424+
std::istreambuf_iterator<char>(invocations),
425+
std::istreambuf_iterator<char>()};
426+
CHECK(invocation_contents == "test\n");
427+
420428
std::filesystem::remove_all(temp_dir);
421429
}
422430

0 commit comments

Comments
 (0)