Files
shadowsocks-libev/tests/reference/cache.cpp
T
Max Lv 002b4093fd C++20 port 14/14: remove the C shims, report, fixes (#3077)
* Remove ss_event and the C resolver shim

Each program's loop, signals, plugin watcher and stats timer now go
through ss::event::Program (src/event/program.hpp), held on main()'s
stack; ss-manager uses it too. ss-server resolves the manager address
with the relay resolver. With no C caller left, ss_event.cpp/.h,
resolv.cpp/.h and their C tests go; test_resolver_cpp.cpp replaces the
resolver tests against the same delayed local DNS server, and ss_ev is
now an interface target over libuv.

* Reinitialize the event loop after daemonize()

The loop is created before the programs fork into the background, and
kqueue/epoll descriptors do not survive fork(), so ss-server -f (which
ss-manager always uses) died as soon as it ran. Call uv_loop_fork() in
the child.

* Move the C codecs to tests/reference and drop uthash from ss-manager

The programs now use only the C++ codecs. The C stream/AEAD/AEAD-2022
implementations stay as the reference the differential tests and the C
unit tests compare against. Key derivation and the cipher tables used by
the doc snippets move to src/crypto/keys.{hpp,cpp}.

ss-manager keeps its servers in an ss_list (at most MAX_PORT_NUM,
looked up only on control messages) instead of a uthash table, and
tests/test_manager.py covers add/ping/list/remove end to end.

* Replace uthash and ss_list with the C++ containers

The C cache (now used only by the reference codecs) moves to
tests/reference and src/uthash.h is deleted. ACL rules and ss-manager's
servers are ListNodes on IntrusiveLists; destroying an element unlinks
it, so the free paths no longer have to remember to. core.h keeps only
ss_ip. test_rule is ported to C++ and also covers literal rules.

* Describe the C++20/libuv build in the Debian and snap metadata

* Keep clang-tidy clean in the new C++ tests

* Add --max-dns-queries / max_dns_queries

Sizes the resolver's query pool, the last of the capacity settings the
no-heap-after-startup design calls for. Defaults are unchanged: the
connection limit (plus 64 on ss-server, which also resolves for its
statistics).

* Report the C++20 rewrite: allocation, size, throughput and memory

docs/performance.md compares master (C) with the C++ engine on macOS
arm64: zero heap allocations per connection or datagram (from 27-89),
TCP throughput within noise, and about 20 MiB more RSS at 1,000 open
connections from the inline per-connection buffers. docs/cpp-core.md
and CLAUDE.md now describe the finished layout.

* Fix the MinGW build of ss-server: stat_resolver is POSIX-only

* Fix the fully static image and the build image for the C++ sources

docker/static: the C++ unit tests link the static libstdc++, whose error
paths need an unwinder that --rtlib=compiler-rt does not provide; link
llvm-libunwind. The programs themselves still need no C++ runtime.

docker/build: build in the default bundled mode with CMake instead of
against libev/udns (Alpine's Mbed TLS 4 is not supported by system
mode).

* Fall back to the system heap when a library realloc cannot be served

heap_realloc returned NULL when a block outgrew the largest size class or
the budget, and libuv aborts when growing its watcher array fails: a poll
on fd 8190 or above (about 4,000 connections) killed the process.
Move the block to the system heap instead, as malloc and calloc already
do. test_heap_guard_cpp polls fd 16380 with the heaps installed.

* Leave pool room for UDP sessions that are still closing

An evicted session keeps its pool slot until its socket's close callback
runs, so with pool capacity equal to the table's the acquire after an
eviction failed: a full table dropped new-session packets after evicting
a live session for nothing. The AEAD-2022 shared-session pool had the
same problem with entries still referenced by relay sessions.

* Time client-side TCP connections when idle, not only while connecting

ss-local, ss-tunnel and ss-redir only timed the upstream connect (as the
C relays did), so a client that never finished its SOCKS handshake, or a
stream whose peer vanished without a FIN, held a pool slot forever. With
the pool now a hard cap that pauses accepting when full, enough of them
stopped the listener for good. Every connection now starts with the idle
deadline, connected() re-arms it, and traffic refreshes it for all kinds
(the idle timeout keeps its 24 h floor, MIN_TCP_IDLE_TIMEOUT).

* Include <algorithm> for std::min in crypto/keys.cpp (CLANG64)

* test.py: accept host:port for --dns

The tunnel test hard-coded port 53, so it could only use a public
resolver or root. With a port it runs against a local DNS stand-in.

* Use the Mbed TLS compat wrappers for MD5 key derivation

* Fix the documentation build after moving the codecs

utils.cpp's \snippet aead.cpp and stream.cpp now live in src/crypto/,
which was not on Doxygen's EXAMPLE_PATH, so the man1 and HTML builds
failed. List the documentation sources explicitly instead of guessing
their extension.
2026-10-15 10:11:56 +08:00

399 lines
12 KiB
C++

/*
* cache.cpp - Fixed-capacity LRU cache for the UDP relay and AEAD-2022
*
* Copyright (C) 2013 - 2019, Max Lv <max.c.lv@gmail.com>
*
* This file is part of the shadowsocks-libev.
*
* shadowsocks-libev is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* shadowsocks-libev is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with shadowsocks-libev; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*/
// Layout: one slab of `capacity` nodes, each holding its key inline, linked
// in least-recently-used order, plus an open-addressing index of node ids
// (linear probing, backward-shift deletion, at most half full). Keys are
// stored once, in the slab; the index hashes through it. Both blocks are
// reserved by cache_create() and only touched as entries are used: nodes
// are handed out in order before recycled ones, and the index stores
// node id + 1 so that zero-filled memory reads as empty.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <new>
#include <sodium.h>
#include "cache.h"
#include "core/fixed_map.hpp"
#include "core/region.hpp"
#include "platform.h"
namespace {
constexpr std::uint32_t kNone = UINT32_MAX;
struct Node {
std::uint32_t prev = kNone;
std::uint32_t next = kNone;
double ts = 0;
void *data = nullptr;
std::uint16_t key_len = 0;
bool used = false;
// key bytes (max_key_len + 1, NUL-terminated for free_cb) follow
};
std::size_t align_up(std::size_t n, std::size_t a)
{
return (n + a - 1) & ~(a - 1);
}
} // namespace
struct cache {
std::size_t capacity = 0; // as requested; eviction threshold
std::size_t max_key_len = 0;
std::size_t stride = 0; // bytes per node, key included
void (*free_cb)(void *key, void *element) = nullptr;
ss::Region nodes;
ss::Region index; // node id + 1 per slot, 0 when empty
std::uint32_t node_count = 0; // slab size (capacity, at least 1)
std::uint32_t fresh = 0; // nodes [fresh, node_count) were never used
std::uint32_t mask = 0; // index slots - 1
std::uint32_t size = 0;
std::uint32_t free_head = kNone;
std::uint32_t oldest = kNone;
std::uint32_t newest = kNone;
std::uint64_t seed = 0;
Node *node(std::uint32_t id) const
{
return reinterpret_cast<Node *>(nodes.data() + static_cast<std::size_t>(id) * stride);
}
char *key(std::uint32_t id) const { return reinterpret_cast<char *>(node(id)) + sizeof(Node); }
std::uint32_t *slots() const { return reinterpret_cast<std::uint32_t *>(index.data()); }
bool empty_slot(std::uint32_t i) const { return slots()[i] == 0; }
std::uint32_t at(std::uint32_t i) const { return slots()[i] - 1; }
void put(std::uint32_t i, std::uint32_t id) const { slots()[i] = id + 1; }
std::uint32_t home(const char *k, std::size_t len) const
{
return static_cast<std::uint32_t>(ss::hash_bytes(k, len, seed)) & mask;
}
bool matches(std::uint32_t id, const char *k, std::size_t len) const
{
const Node *n = node(id);
return n->key_len == len && std::memcmp(key(id), k, len) == 0;
}
// Index slot holding the key, or the empty slot that ends its probe run.
std::uint32_t probe(const char *k, std::size_t len, bool &found) const
{
std::uint32_t i = home(k, len);
while (!empty_slot(i)) {
if (matches(at(i), k, len)) {
found = true;
return i;
}
i = (i + 1) & mask;
}
found = false;
return i;
}
void unindex(std::uint32_t hole)
{
slots()[hole] = 0;
for (std::uint32_t j = (hole + 1) & mask; !empty_slot(j); j = (j + 1) & mask) {
std::uint32_t id = at(j);
std::uint32_t k = home(key(id), node(id)->key_len);
bool movable = (hole <= j) ? (k <= hole || k > j) : (k <= hole && k > j);
if (movable) {
put(hole, id);
slots()[j] = 0;
hole = j;
}
}
}
void unlink(std::uint32_t id)
{
Node *n = node(id);
if (n->prev != kNone) {
node(n->prev)->next = n->next;
} else {
oldest = n->next;
}
if (n->next != kNone) {
node(n->next)->prev = n->prev;
} else {
newest = n->prev;
}
n->prev = n->next = kNone;
}
void link_newest(std::uint32_t id)
{
Node *n = node(id);
n->prev = newest;
n->next = kNone;
if (newest != kNone) {
node(newest)->next = id;
} else {
oldest = id;
}
newest = id;
}
void touch(std::uint32_t id)
{
node(id)->ts = ss_monotonic_time();
if (id != newest) {
unlink(id);
link_newest(id);
}
}
// Unlink the entry in index slot `slot` and release its data. The node
// is back on the free list before free_cb runs, so free_cb may use the
// cache.
void drop(std::uint32_t slot, bool release_data)
{
std::uint32_t id = at(slot);
Node *n = node(id);
char key_copy[256];
std::size_t len = n->key_len;
std::memcpy(key_copy, key(id), len);
key_copy[len] = '\0';
void *data = n->data;
unindex(slot);
unlink(id);
n->used = false;
n->data = nullptr;
n->next = free_head;
free_head = id;
--size;
if (release_data && data != nullptr) {
if (free_cb != nullptr) {
free_cb(key_copy, data);
} else {
std::free(data); // heap-ok: caller-owned data, per the C API contract
}
}
}
void drop_id(std::uint32_t id, bool release_data)
{
bool found = false;
std::uint32_t slot = probe(key(id), node(id)->key_len, found);
if (!found) {
std::abort(); // index and slab disagree
}
drop(slot, release_data);
}
};
extern "C" {
int
cache_create_keyed(struct cache **dst, size_t capacity, size_t max_key_len,
void (*free_cb)(void *key, void *element))
{
if (dst == nullptr || max_key_len == 0 || max_key_len > 255 || capacity > (UINT32_MAX >> 2)) {
return EINVAL;
}
auto *c = static_cast<struct cache *>(std::malloc(sizeof(struct cache))); // heap-ok: created at startup
if (c == nullptr) {
return ENOMEM;
}
new (c) struct cache();
c->capacity = capacity;
c->max_key_len = max_key_len;
c->free_cb = free_cb;
c->node_count = capacity == 0 ? 1 : static_cast<std::uint32_t>(capacity);
c->stride = align_up(sizeof(Node) + max_key_len + 1, alignof(Node));
std::uint32_t slots = 2;
while (slots < c->node_count * 2U) {
slots <<= 1;
}
c->mask = slots - 1;
c->seed = (static_cast<std::uint64_t>(randombytes_random()) << 32) | randombytes_random();
auto nodes = ss::Region::reserve(static_cast<std::size_t>(c->node_count) * c->stride, alignof(Node));
auto index = ss::Region::reserve(static_cast<std::size_t>(slots) * sizeof(std::uint32_t), alignof(std::uint32_t));
if (!nodes || !index) {
c->~cache();
std::free(c); // heap-ok: failed startup reservation
return ENOMEM;
}
c->nodes = std::move(*nodes);
c->index = std::move(*index);
*dst = c;
return 0;
}
int
cache_create(struct cache **dst, const size_t capacity,
void (*free_cb)(void *key, void *element))
{
return cache_create_keyed(dst, capacity, CACHE_DEFAULT_MAX_KEY, free_cb);
}
int
cache_delete(struct cache *cache, int keep_data)
{
if (cache == nullptr) {
return EINVAL;
}
while (cache->oldest != kNone) {
cache->drop_id(cache->oldest, !keep_data);
}
cache->~cache();
std::free(cache); // heap-ok: created at startup
return 0;
}
int
cache_clear(struct cache *cache, double age)
{
if (cache == nullptr) {
return EINVAL;
}
double now = ss_monotonic_time();
// Entries are in last-use order, so the stale ones are at the front.
while (cache->oldest != kNone && now - cache->node(cache->oldest)->ts > age) {
cache->drop_id(cache->oldest, true);
}
return 0;
}
int
cache_remove(struct cache *cache, char *key, size_t key_len)
{
if (cache == nullptr || key == nullptr) {
return EINVAL;
}
bool found = false;
std::uint32_t slot = cache->probe(key, key_len, found);
if (found) {
cache->drop(slot, true);
}
return 0;
}
int
cache_lookup(struct cache *cache, char *key, size_t key_len, void *result)
{
if (cache == nullptr || key == nullptr || result == nullptr) {
return EINVAL;
}
void **out = static_cast<void **>(result);
bool found = false;
std::uint32_t slot = cache->probe(key, key_len, found);
if (!found) {
*out = nullptr;
return 0;
}
std::uint32_t id = cache->at(slot);
cache->touch(id);
*out = cache->node(id)->data;
return 0;
}
int
cache_key_exist(struct cache *cache, char *key, size_t key_len)
{
if (cache == nullptr || key == nullptr) {
return 0;
}
bool found = false;
std::uint32_t slot = cache->probe(key, key_len, found);
if (!found) {
return 0;
}
cache->touch(cache->at(slot));
return 1;
}
int
cache_insert(struct cache *cache, char *key, size_t key_len, void *data)
{
if (cache == nullptr || key == nullptr || key_len > cache->max_key_len) {
return EINVAL;
}
bool found = false;
std::uint32_t slot = cache->probe(key, key_len, found);
if (found) {
// Re-inserting a key replaces its data; the old data is released
// as if the entry had been evicted.
std::uint32_t id = cache->at(slot);
Node *n = cache->node(id);
void *old = n->data;
n->data = data;
cache->touch(id);
if (old != nullptr && old != data) {
char key_copy[256];
std::memcpy(key_copy, key, key_len);
key_copy[key_len] = '\0';
if (cache->free_cb != nullptr) {
cache->free_cb(key_copy, old);
} else {
std::free(old); // heap-ok: caller-owned data, per the C API contract
}
}
return 0;
}
std::uint32_t id;
if (cache->free_head != kNone) {
id = cache->free_head;
cache->free_head = cache->node(id)->next;
} else if (cache->fresh < cache->node_count) {
id = cache->fresh++;
new (cache->node(id)) Node();
} else {
std::abort(); // eviction below keeps at least one node free
}
Node *n = cache->node(id);
n->used = true;
n->data = data;
n->ts = ss_monotonic_time();
n->key_len = static_cast<std::uint16_t>(key_len);
std::memcpy(cache->key(id), key, key_len);
cache->key(id)[key_len] = '\0';
cache->put(slot, id);
cache->link_newest(id);
++cache->size;
if (cache->size >= cache->capacity) {
cache->drop_id(cache->oldest, true);
}
return 0;
}
size_t
cache_size(const struct cache *cache)
{
return cache == nullptr ? 0 : cache->size;
}
} // extern "C"