diff --git a/CLAUDE.md b/CLAUDE.md index 05e6d4cb..8347cb7a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,9 +103,9 @@ A library `libshadowsocks-c` (legacy names `libshadowsocks-libev.*` are kept) is **Shared by all binaries:** - `utils.c` - logging, system utilities, `ss_malloc`/`ss_free` -- `jconf.c` / `json.c` - JSON config file parsing +- `jconf.c` / `json.cpp` - JSON config file parsing - `netutils.c` - network address utilities -- `cache.c` - uthash-based connection cache (UDP relay, AEAD-2022 tables) +- `cache.cpp` - uthash-based connection cache (UDP relay, AEAD-2022 tables) - `udprelay.c` - UDP relay implementation (shared, but uses `#ifdef MODULE_*` for per-binary behavior) - `ss_event.c` / `ss_event.h` - event loop API over libuv - `resolv.c` - asynchronous DNS via c-ares @@ -115,11 +115,11 @@ A library `libshadowsocks-c` (legacy names `libshadowsocks-libev.*` are kept) is - `stream.c` - legacy stream ciphers - `aead.c` - AEAD ciphers (AES-GCM via Mbed TLS/libsodium, ChaCha20-Poly1305 via libsodium) - `aead2022.c` / `aead2022_udp.c` - Shadowsocks 2022 (SIP022), BLAKE3 from `src/blake3` -- `ppbloom.c` - ping-pong bloom filter for salt replay detection (`third_party/bloom`) +- `ppbloom.cpp` - ping-pong bloom filter for salt replay detection (`third_party/bloom`) -**ACL:** `acl.c` / `rule.c` / `ipset.c` - IP prefix sets and PCRE2/literal domain rules. +**ACL:** `acl.c` / `rule.cpp` / `ipset.cpp` - IP prefix sets and PCRE2/literal domain rules. -**Plugins:** `plugin.c` / `process.c` - SIP003 plugin subprocess management. +**Plugins:** `plugin.c` / `process.cpp` - SIP003 plugin subprocess management. **C++ core:** `src/core/*.hpp` - header-only ownership and allocation primitives (pools, handles, fixed buffers, arena, fixed hash map/LRU, intrusive list, bounded heap). See `docs/cpp-core.md`. diff --git a/cmake/ProjectOptions.cmake b/cmake/ProjectOptions.cmake index f73366a8..82726baa 100644 --- a/cmake/ProjectOptions.cmake +++ b/cmake/ProjectOptions.cmake @@ -26,9 +26,11 @@ function(ss_project_options target) else() target_compile_options(${target} PRIVATE -Wall -Wno-deprecated-declarations -fno-strict-aliasing) # C++ sources: no exceptions or RTTI, and no locks around function - # statics (the event loop is single-threaded). + # statics (the event loop is single-threaded). New C++ code also opts + # into -Wextra through ss_strict_warnings(); modules ported from C get + # it when they are rewritten. target_compile_options(${target} PRIVATE - $<$:-Wextra -fno-exceptions -fno-rtti -fno-threadsafe-statics>) + $<$:-fno-exceptions -fno-rtti -fno-threadsafe-statics>) if(SS_WARNINGS_AS_ERRORS) target_compile_options(${target} PRIVATE -Werror) endif() @@ -54,3 +56,11 @@ function(ss_project_options target) target_link_options(${target} PRIVATE --coverage) endif() endfunction() + +# Extra warnings for code written as C++ from the start (src/core and its +# tests). +function(ss_strict_warnings target) + if(NOT MSVC) + target_compile_options(${target} PRIVATE $<$:-Wextra>) + endif() +endfunction() diff --git a/debian/copyright b/debian/copyright index aa8e2484..148d692f 100644 --- a/debian/copyright +++ b/debian/copyright @@ -41,11 +41,11 @@ Files: scripts/build_deb.sh Copyright: 2018 Roger Shimizu License: GPL-3+ -Files: src/base64.c src/base64.h +Files: src/base64.cpp src/base64.h Copyright: 2006 Ryan Martell License: LGPL-2.1+ -Files: src/json.c src/json.h +Files: src/json.cpp src/json.h Copyright: 2012-2014, James McLaughlin et al. License: BSD-2-clause @@ -53,7 +53,7 @@ Files: src/resolv.c src/resolv.h Copyright: 2011-2014, Dustin Lundquist License: BSD-2-clause -Files: src/rule.c src/rule.h +Files: src/rule.cpp src/rule.h Copyright: 2011-2012, Dustin Lundquist 2011, Manuel Kasper License: BSD-2-clause diff --git a/docs/cpp-core.md b/docs/cpp-core.md index 24ecf11e..ab21d5cb 100644 --- a/docs/cpp-core.md +++ b/docs/cpp-core.md @@ -43,7 +43,7 @@ All binaries are still built from the C sources. | `ref.hpp` | `RefCounted`/`RefPtr`, intrusive shared ownership for the few genuinely shared objects (AEAD-2022 UDP sessions). Used instead of `std::shared_ptr` because its control block cannot be pooled portably. | | `buffer.hpp` | `FixedBuffer`: inline bytes with `std::span` views; replaces `buffer_t`. | | `arena.hpp` | `MonotonicArena` for configuration data that lives as long as the process. | -| `fixed_map.hpp` | `FixedHashMap` (open addressing, backward-shift deletion) and `FixedLru`; replace uthash and `cache.c`. | +| `fixed_map.hpp` | `FixedHashMap` (open addressing, backward-shift deletion) and `FixedLru`; replace uthash and `cache.cpp`. | | `intrusive_list.hpp` | `IntrusiveList`/`ListNode`; replaces `ss_list`. Nodes unlink themselves when destroyed. | | `fixed_heap.hpp` | Bounded size-class allocator to hand to libuv, c-ares and Mbed TLS through their allocator hooks. | diff --git a/scripts/check_cxx_runtime.py b/scripts/check_cxx_runtime.py new file mode 100644 index 00000000..a1276d8b --- /dev/null +++ b/scripts/check_cxx_runtime.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-or-later +"""Fail if programs or libraries need the C++ runtime library. + +The C++ core is built without exceptions or RTTI and uses only header-only +parts of the standard library, so the programs and libshadowsocks-c stay +consumable from plain C: a C program linking libshadowsocks-c.a must not need +libstdc++ or libc++. This script lists the undefined symbols of each file with +nm and rejects anything served by the C++ runtime (operator new/delete, +exception and RTTI support, std:: functions). + +Usage: check_cxx_runtime.py [--nm NM] FILE... +""" + +import argparse +import re +import shutil +import subprocess +import sys + +# Itanium-ABI names (with or without the Mach-O leading underscore) that only +# the C++ runtime defines. +RUNTIME = re.compile( + r"^_?(?:" + r"_Zn[wa]|_Zd[la]|" # operator new / delete + r"_ZSt|_ZNSt|_ZNKSt|" # std:: functions and members + r"_ZTI|_ZTS|_ZTV|" # typeinfo / vtables (RTTI, classes from the runtime) + r"__cxa_(?!atexit$|finalize$|thread_atexit_impl$)|" # exception/guard support; libc owns these three + r"__gxx_personality|_Unwind_|__dynamic_cast" + r")" +) + + +def undefined_symbols(nm, path): + result = subprocess.run([nm, "-u", path], capture_output=True, text=True) + if result.returncode != 0: + raise SystemExit(f"{nm} failed on {path}: {result.stderr.strip()}") + symbols = set() + for line in result.stdout.splitlines(): + line = line.strip() + if not line or line.endswith(":"): + continue # archive member headers + symbols.add(line.split()[-1]) + return symbols + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--nm", default=shutil.which("nm") or "nm") + parser.add_argument("files", nargs="+") + args = parser.parse_args() + failed = False + for path in args.files: + bad = sorted(s for s in undefined_symbols(args.nm, path) if RUNTIME.match(s)) + if bad: + failed = True + print(f"{path}: needs the C++ runtime:") + for symbol in bad: + print(f" {symbol}") + else: + print(f"{path}: ok") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_no_heap.py b/scripts/check_no_heap.py index 39fa63b6..7675efbb 100644 --- a/scripts/check_no_heap.py +++ b/scripts/check_no_heap.py @@ -14,7 +14,12 @@ A line that must allocate (a startup reservation) carries a justification: void *base = std::calloc(1, size); // heap-ok: startup reservation -Usage: check_no_heap.py [--root DIR] [--self-test] +Modules converted mechanically from C still allocate the way the C code did +until they are rewritten. scripts/no_heap_baseline.txt lists them with their +current violation counts, and works as a ratchet: a file may not gain +violations, and when it loses some the baseline must be lowered to match. + +Usage: check_no_heap.py [--root DIR] [--baseline FILE] [--self-test] """ import argparse @@ -89,16 +94,42 @@ def check_text(text): return problems -def check_tree(root): +def read_baseline(path): + baseline = {} + if path is None or not path.exists(): + return baseline + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.split("#", 1)[0].strip() + if line: + name, count = line.split() + baseline[name] = int(count) + return baseline + + +def check_tree(root, baseline): src = root / "src" files = sorted(p for p in src.rglob("*") if p.suffix in (".cpp", ".hpp") and "blake3" not in p.parts) - failures = 0 + ok = True + total = 0 for path in files: - for number, why, line in check_text(path.read_text(encoding="utf-8")): - print(f"{path.relative_to(root)}:{number}: {why}: {line}") - failures += 1 - print(f"check_no_heap: {len(files)} C++ files, {failures} violation(s)") - return failures == 0 + name = path.relative_to(root).as_posix() + problems = check_text(path.read_text(encoding="utf-8")) + allowed = baseline.pop(name, 0) + total += len(problems) + if len(problems) > allowed: + for number, why, line in problems: + print(f"{name}:{number}: {why}: {line}") + print(f"{name}: {len(problems)} violation(s), baseline allows {allowed}") + ok = False + elif len(problems) < allowed: + print(f"{name}: {len(problems)} violation(s), below the baseline of {allowed}; " + "lower it in scripts/no_heap_baseline.txt") + ok = False + for name in sorted(baseline): + print(f"{name}: listed in the baseline but not a C++ source; remove the entry") + ok = False + print(f"check_no_heap: {len(files)} C++ files, {total} baselined or new violation(s)") + return ok def self_test(): @@ -144,11 +175,15 @@ def self_test(): def main(): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path(__file__).resolve().parent.parent) + parser.add_argument("--baseline", type=pathlib.Path, + help="ratchet file (default: scripts/no_heap_baseline.txt under --root)") parser.add_argument("--self-test", action="store_true", help="run the pattern self-test first") args = parser.parse_args() if args.self_test and not self_test(): return 1 - return 0 if check_tree(args.root.resolve()) else 1 + root = args.root.resolve() + baseline = read_baseline(args.baseline or root / "scripts" / "no_heap_baseline.txt") + return 0 if check_tree(root, baseline) else 1 if __name__ == "__main__": diff --git a/scripts/no_heap_baseline.txt b/scripts/no_heap_baseline.txt new file mode 100644 index 00000000..6cedf8e0 --- /dev/null +++ b/scripts/no_heap_baseline.txt @@ -0,0 +1,10 @@ +# Heap-allocation ratchet for C++ sources converted mechanically from C. +# Format: . check_no_heap.py fails if a file gains +# violations, and also when it loses some, so the number here always goes +# down. Remove a file once it is rewritten on the src/core primitives. +src/cache.cpp 3 +src/ipset.cpp 3 +src/json.cpp 2 +src/process.cpp 26 +src/rule.cpp 2 +src/ssurl.cpp 3 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 255bd791..fe150898 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,8 +10,8 @@ endif () set(SS_ACL_SOURCE acl.c - ipset.c - rule.c + ipset.cpp + rule.cpp ) set(SS_CRYPTO_SOURCE @@ -19,7 +19,7 @@ set(SS_CRYPTO_SOURCE aead.c aead2022.c aead2022_udp.c - base64.c + base64.cpp ) # BLAKE3 (vendored) provides the key derivation for the 2022 ciphers. Only @@ -43,16 +43,16 @@ if(SS_ENABLE_LEGACY) list(APPEND SS_CRYPTO_SOURCE stream.c) endif() if(SS_ENABLE_PLUGINS) - set(SS_PLUGIN_SOURCE plugin.c process.c) + set(SS_PLUGIN_SOURCE plugin.c process.cpp) else() set(SS_PLUGIN_SOURCE plugin_disabled.c) endif() set(SS_SHARED_SOURCES - ppbloom.c + ppbloom.cpp utils.c jconf.c - json.c + json.cpp netutils.c ) if (MINGW) @@ -62,10 +62,10 @@ endif () set(LIBSHADOWSOCKS_LIBEV_SOURCE ${SS_SHARED_SOURCES} udprelay.c - cache.c + cache.cpp local.c resolv.c - ssurl.c + ssurl.cpp ${SS_CRYPTO_SOURCE} ${SS_PLUGIN_SOURCE} ${SS_ACL_SOURCE} @@ -79,7 +79,7 @@ set(SS_LOCAL_SOURCE set(SS_TUNNEL_SOURCE ${SS_SHARED_SOURCES} udprelay.c - cache.c + cache.cpp tunnel.c ${SS_CRYPTO_SOURCE} ${SS_PLUGIN_SOURCE} @@ -88,7 +88,7 @@ set(SS_TUNNEL_SOURCE set(SS_SERVER_SOURCE ${SS_SHARED_SOURCES} udprelay.c - cache.c + cache.cpp resolv.c server.c ${SS_CRYPTO_SOURCE} @@ -104,7 +104,7 @@ set(SS_MANAGER_SOURCE set(SS_REDIR_SOURCE ${SS_SHARED_SOURCES} udprelay.c - cache.c + cache.cpp redir.c ${SS_CRYPTO_SOURCE} ${SS_PLUGIN_SOURCE} diff --git a/src/base64.c b/src/base64.cpp similarity index 100% rename from src/base64.c rename to src/base64.cpp diff --git a/src/cache.c b/src/cache.cpp similarity index 100% rename from src/cache.c rename to src/cache.cpp diff --git a/src/core/fixed_map.hpp b/src/core/fixed_map.hpp index 0385f021..a821e313 100644 --- a/src/core/fixed_map.hpp +++ b/src/core/fixed_map.hpp @@ -2,7 +2,7 @@ #ifndef SS_CORE_FIXED_MAP_HPP #define SS_CORE_FIXED_MAP_HPP -// Fixed-capacity associative containers. Replace uthash (cache.c, +// Fixed-capacity associative containers. Replace uthash (cache.cpp, // ss_event.c, manager.c) without any per-entry allocation. // // FixedHashMap @@ -15,7 +15,7 @@ // FixedLru // Map plus recency order. Values live in a slab and never move, so the // pointer returned by insert() or find() stays valid until that entry -// is erased or evicted. Replaces the pseudo-LRU in cache.c. +// is erased or evicted. Replaces the pseudo-LRU in cache.cpp. // // Keys from the network (UDP peer addresses, session IDs) are hashed with a // per-table seed. Seed it from a CSPRNG in production code so a peer cannot diff --git a/src/ipset.c b/src/ipset.cpp similarity index 100% rename from src/ipset.c rename to src/ipset.cpp diff --git a/src/json.c b/src/json.cpp similarity index 100% rename from src/json.c rename to src/json.cpp diff --git a/src/ppbloom.c b/src/ppbloom.cpp similarity index 100% rename from src/ppbloom.c rename to src/ppbloom.cpp diff --git a/src/process.c b/src/process.cpp similarity index 100% rename from src/process.c rename to src/process.cpp diff --git a/src/rule.c b/src/rule.cpp similarity index 100% rename from src/rule.c rename to src/rule.cpp diff --git a/src/ssurl.c b/src/ssurl.cpp similarity index 100% rename from src/ssurl.c rename to src/ssurl.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c950432c..49145de9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -60,6 +60,7 @@ endfunction() # C++ core (src/core): header-only ownership and allocation primitives. foreach(core_test result pool buffer arena fixed_map ownership) ss_add_test(test_core_${core_test} "test_core_${core_test}.cpp" "") + ss_strict_warnings(test_core_${core_test}) endforeach() # C++ sources under src/ must not allocate on the heap (see the script). @@ -68,11 +69,30 @@ if(Python3_Interpreter_FOUND) add_test(NAME check_no_heap COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/check_no_heap.py --self-test) set_tests_properties(check_no_heap PROPERTIES LABELS "unit") + + # The programs and the static library must stay usable without a C++ + # runtime library (see docs/cpp-core.md). Sanitizer runtimes bring their + # own C++ symbols, so skip the check there. + set(SS_RUNTIME_CHECKED) + foreach(target ss-local ss-server ss-tunnel ss-redir ss-manager shadowsocks-c-static) + if(TARGET ${target}) + get_target_property(excluded ${target} EXCLUDE_FROM_ALL) + if(NOT excluded) + list(APPEND SS_RUNTIME_CHECKED $) + endif() + endif() + endforeach() + if(SS_RUNTIME_CHECKED AND CMAKE_NM AND NOT ENABLE_SANITIZERS) + add_test(NAME check_cxx_runtime + COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/check_cxx_runtime.py + --nm ${CMAKE_NM} ${SS_RUNTIME_CHECKED}) + set_tests_properties(check_cxx_runtime PROPERTIES LABELS "unit") + endif() endif() # test_base64 - no external deps ss_add_test(test_base64 - "test_base64.c;${PROJECT_SOURCE_DIR}/src/base64.c" + "test_base64.c;${PROJECT_SOURCE_DIR}/src/base64.cpp" "") # test_utils - needs sodium (for ss_malloc -> uses sodium in utils.c) @@ -82,12 +102,12 @@ ss_add_test(test_utils # test_json - standalone json parser ss_add_test(test_json - "test_json.c;${PROJECT_SOURCE_DIR}/src/json.c" + "test_json.c;${PROJECT_SOURCE_DIR}/src/json.cpp" "${SS_MATH_LIBRARY}") # test_ssurl - ss:// URI parsing (SIP002 and legacy) ss_add_test(test_ssurl - "test_ssurl.c;${PROJECT_SOURCE_DIR}/src/ssurl.c;${PROJECT_SOURCE_DIR}/src/base64.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "test_ssurl.c;${PROJECT_SOURCE_DIR}/src/ssurl.cpp;${PROJECT_SOURCE_DIR}/src/base64.cpp;${PROJECT_SOURCE_DIR}/src/utils.c" "ss_sodium") # test_netutils - address parsing and resolver integration @@ -102,14 +122,14 @@ ss_add_test(test_netutils # test_cache - expiration uses a platform monotonic clock ss_add_test(test_cache - "test_cache.c;${PROJECT_SOURCE_DIR}/src/cache.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "test_cache.c;${PROJECT_SOURCE_DIR}/src/cache.cpp;${PROJECT_SOURCE_DIR}/src/utils.c" "ss_sodium") # test_ppbloom - needs bloom set(TEST_PPBLOOM_LIBS "") list(APPEND TEST_PPBLOOM_LIBS bloom) ss_add_test(test_ppbloom - "test_ppbloom.c;${PROJECT_SOURCE_DIR}/src/ppbloom.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "test_ppbloom.c;${PROJECT_SOURCE_DIR}/src/ppbloom.cpp;${PROJECT_SOURCE_DIR}/src/utils.c" "${TEST_PPBLOOM_LIBS};ss_sodium;${SS_MATH_LIBRARY}") if(SS_ENABLE_REGEX) @@ -119,13 +139,13 @@ set(TEST_RULE_LIBS ss_sodium ) ss_add_test(test_rule - "test_rule.c;${PROJECT_SOURCE_DIR}/src/rule.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "test_rule.c;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.c" "${TEST_RULE_LIBS}") # test_acl - ACL precedence and prefix matching set(TEST_ACL_LIBS ${TEST_RULE_LIBS}) ss_add_test(test_acl - "test_acl.c;${PROJECT_SOURCE_DIR}/src/acl.c;${PROJECT_SOURCE_DIR}/src/ipset.c;${PROJECT_SOURCE_DIR}/src/rule.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "test_acl.c;${PROJECT_SOURCE_DIR}/src/acl.c;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.c" "${TEST_ACL_LIBS}") endif() @@ -137,7 +157,7 @@ set(TEST_JCONF_LIBS ss_cares ) ss_add_test(test_jconf - "test_jconf.c;${PROJECT_SOURCE_DIR}/src/jconf.c;${PROJECT_SOURCE_DIR}/src/json.c;${PROJECT_SOURCE_DIR}/src/utils.c;${PROJECT_SOURCE_DIR}/src/netutils.c" + "test_jconf.c;${PROJECT_SOURCE_DIR}/src/jconf.c;${PROJECT_SOURCE_DIR}/src/json.cpp;${PROJECT_SOURCE_DIR}/src/utils.c;${PROJECT_SOURCE_DIR}/src/netutils.c" "${TEST_JCONF_LIBS};${SS_MATH_LIBRARY}") # Crypto sources shared by the tests that link the crypto layer. The AEAD-2022 @@ -148,9 +168,9 @@ set(SS_CRYPTO_TEST_SOURCES ${PROJECT_SOURCE_DIR}/src/aead.c ${PROJECT_SOURCE_DIR}/src/aead2022.c ${PROJECT_SOURCE_DIR}/src/aead2022_udp.c - ${PROJECT_SOURCE_DIR}/src/ppbloom.c - ${PROJECT_SOURCE_DIR}/src/cache.c - ${PROJECT_SOURCE_DIR}/src/base64.c + ${PROJECT_SOURCE_DIR}/src/ppbloom.cpp + ${PROJECT_SOURCE_DIR}/src/cache.cpp + ${PROJECT_SOURCE_DIR}/src/base64.cpp ${PROJECT_SOURCE_DIR}/src/utils.c ) @@ -200,11 +220,11 @@ add_test(NAME test_ss_setup set_tests_properties(test_ss_setup PROPERTIES LABELS "integration") ss_add_test(test_ipset - "test_ipset.c;${PROJECT_SOURCE_DIR}/src/ipset.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "test_ipset.c;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/utils.c" "ss_sodium") ss_add_test(test_process - "test_process.c;${PROJECT_SOURCE_DIR}/src/process.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "test_process.c;${PROJECT_SOURCE_DIR}/src/process.cpp;${PROJECT_SOURCE_DIR}/src/utils.c" "ss_sodium") set_tests_properties(test_process PROPERTIES TIMEOUT 20) @@ -218,7 +238,7 @@ if(SS_ENABLE_REGEX) list(APPEND TEST_LITERAL_LIBS ss_pcre2) endif() ss_add_test(test_acl_literal - "test_acl_literal.c;${PROJECT_SOURCE_DIR}/src/acl.c;${PROJECT_SOURCE_DIR}/src/ipset.c;${PROJECT_SOURCE_DIR}/src/rule.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "test_acl_literal.c;${PROJECT_SOURCE_DIR}/src/acl.c;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.c" "${TEST_LITERAL_LIBS}") ss_add_test(test_event "test_event.c" "ss_ev")