mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-20 13:03:50 +00:00
Add an embedding API: ss_local_start/stop/wait and a C++ wrapper (#3076)
<shadowsocks/control.h> starts ss-local on a library-owned thread, stops it from any thread through a libuv async handle and waits for it; <shadowsocks/shadowsocks.hpp> wraps that in a move-only, header-only shadowsocks::LocalServer whose destructor stops and waits. The library exports only C symbols, shadowsocks.h is unchanged, and the shared library version becomes 2.1.0 (SOVERSION 2). test_local_api_cpp covers the lifecycle; the installed-package consumer gains a C++ program.
This commit is contained in:
+3
-1
@@ -177,7 +177,7 @@ foreach(kind STATIC SHARED)
|
||||
target_link_libraries(${target} PRIVATE ${SS_CORE_DEPS} ${SS_ACL_DEPS})
|
||||
set_target_properties(${target} PROPERTIES OUTPUT_NAME shadowsocks-c)
|
||||
if(kind STREQUAL "SHARED")
|
||||
set_target_properties(${target} PROPERTIES VERSION 2.0.0 SOVERSION 2)
|
||||
set_target_properties(${target} PROPERTIES VERSION 2.1.0 SOVERSION 2)
|
||||
endif()
|
||||
ss_project_options(${target})
|
||||
install(TARGETS ${target}
|
||||
@@ -188,4 +188,6 @@ foreach(kind STATIC SHARED)
|
||||
endforeach()
|
||||
if(SS_BUILD_STATIC_LIBRARY OR SS_BUILD_SHARED_LIBRARY)
|
||||
install(FILES shadowsocks.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(FILES shadowsocks/control.h shadowsocks/shadowsocks.hpp
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/shadowsocks)
|
||||
endif()
|
||||
|
||||
+155
-1
@@ -41,6 +41,7 @@
|
||||
#endif
|
||||
#ifdef LIB_ONLY
|
||||
#include "shadowsocks.h"
|
||||
#include "shadowsocks/control.h"
|
||||
#endif
|
||||
|
||||
#if defined(HAVE_SYS_IOCTL_H) && defined(HAVE_NET_IF_H) && defined(__linux__)
|
||||
@@ -1332,6 +1333,144 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
|
||||
|
||||
#else
|
||||
|
||||
// ------------------------------------------------------------ library API
|
||||
|
||||
int _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udata);
|
||||
|
||||
struct ss_local_handle {
|
||||
profile_t profile;
|
||||
ss_local_callback callback;
|
||||
void *udata;
|
||||
uv_thread_t thread;
|
||||
int result;
|
||||
bool waited;
|
||||
bool stop_requested; // guarded by control_lock
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
// The running server's stop signal. ss_local_stop() and the server thread
|
||||
// take the lock around it, so a stop never wakes a loop that is gone.
|
||||
uv_mutex_t control_lock;
|
||||
uv_once_t control_once = UV_ONCE_INIT;
|
||||
uv_async_t *stop_signal = NULL;
|
||||
ss_local_handle *running_handle = NULL;
|
||||
bool server_busy = false;
|
||||
|
||||
void control_init(void) { uv_mutex_init(&control_lock); }
|
||||
|
||||
void on_stop_signal(uv_async_t *) { ss_unloop(SS_DEFAULT, SS_UNLOOP_ALL); }
|
||||
|
||||
char *copy_string(const char *s)
|
||||
{
|
||||
return s == NULL ? NULL : strdup(s); // heap-ok: startup, library only
|
||||
}
|
||||
|
||||
void free_profile(profile_t *p)
|
||||
{
|
||||
free(p->remote_host); // heap-ok: library API, startup
|
||||
free(p->local_addr); // heap-ok: library API, startup
|
||||
free(p->method); // heap-ok: library API, startup
|
||||
free(p->password); // heap-ok: library API, startup
|
||||
free(p->acl); // heap-ok: library API, startup
|
||||
free(p->log); // heap-ok: library API, startup
|
||||
}
|
||||
|
||||
void server_thread(void *arg)
|
||||
{
|
||||
ss_local_handle *h = (ss_local_handle *)arg;
|
||||
h->result = _start_ss_local_server(h->profile, h->callback, h->udata);
|
||||
uv_mutex_lock(&control_lock);
|
||||
running_handle = NULL;
|
||||
server_busy = false;
|
||||
uv_mutex_unlock(&control_lock);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int
|
||||
ss_local_start(const profile_t *profile, ss_local_callback callback, void *udata, ss_local_handle **handle)
|
||||
{
|
||||
if (profile == NULL || handle == NULL || profile->remote_host == NULL || profile->method == NULL
|
||||
|| profile->password == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
uv_once(&control_once, control_init);
|
||||
uv_mutex_lock(&control_lock);
|
||||
if (server_busy) {
|
||||
uv_mutex_unlock(&control_lock);
|
||||
return EBUSY;
|
||||
}
|
||||
ss_local_handle *h = (ss_local_handle *)calloc(1, sizeof(*h)); // heap-ok: startup, library only
|
||||
if (h == NULL) {
|
||||
uv_mutex_unlock(&control_lock);
|
||||
return ENOMEM;
|
||||
}
|
||||
h->profile = *profile;
|
||||
h->profile.remote_host = copy_string(profile->remote_host);
|
||||
h->profile.local_addr = copy_string(profile->local_addr);
|
||||
h->profile.method = copy_string(profile->method);
|
||||
h->profile.password = copy_string(profile->password);
|
||||
h->profile.acl = copy_string(profile->acl);
|
||||
h->profile.log = copy_string(profile->log);
|
||||
h->callback = callback;
|
||||
h->udata = udata;
|
||||
server_busy = true;
|
||||
running_handle = h;
|
||||
uv_mutex_unlock(&control_lock);
|
||||
if (uv_thread_create(&h->thread, server_thread, h) != 0) {
|
||||
uv_mutex_lock(&control_lock);
|
||||
server_busy = false;
|
||||
running_handle = NULL;
|
||||
uv_mutex_unlock(&control_lock);
|
||||
free_profile(&h->profile);
|
||||
free(h); // heap-ok: library API, startup
|
||||
return ENOMEM;
|
||||
}
|
||||
*handle = h;
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" void
|
||||
ss_local_stop(ss_local_handle *handle)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
return;
|
||||
}
|
||||
uv_once(&control_once, control_init);
|
||||
uv_mutex_lock(&control_lock);
|
||||
handle->stop_requested = true;
|
||||
if (running_handle == handle && stop_signal != NULL) {
|
||||
uv_async_send(stop_signal);
|
||||
}
|
||||
uv_mutex_unlock(&control_lock);
|
||||
}
|
||||
|
||||
extern "C" int
|
||||
ss_local_wait(ss_local_handle *handle)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
if (!handle->waited) {
|
||||
uv_thread_join(&handle->thread);
|
||||
handle->waited = true;
|
||||
}
|
||||
return handle->result;
|
||||
}
|
||||
|
||||
extern "C" void
|
||||
ss_local_destroy(ss_local_handle *handle)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
return;
|
||||
}
|
||||
ss_local_stop(handle);
|
||||
(void)ss_local_wait(handle);
|
||||
free_profile(&handle->profile);
|
||||
free(handle); // heap-ok: library API, startup
|
||||
}
|
||||
|
||||
int
|
||||
_start_ss_local_server(profile_t profile, ss_local_callback callback, void *udata)
|
||||
{
|
||||
@@ -1441,8 +1580,23 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat
|
||||
callback(listen_fd, udp_fd, udata);
|
||||
}
|
||||
|
||||
// ss_local_stop() wakes the loop through this handle.
|
||||
uv_async_t stop_async;
|
||||
uv_async_init(ss_native_loop(loop), &stop_async, on_stop_signal);
|
||||
uv_once(&control_once, control_init);
|
||||
uv_mutex_lock(&control_lock);
|
||||
bool stop_now = running_handle != NULL && running_handle->stop_requested;
|
||||
stop_signal = &stop_async;
|
||||
uv_mutex_unlock(&control_lock);
|
||||
|
||||
// Enter the loop
|
||||
ss_run(loop, 0);
|
||||
if (!stop_now)
|
||||
ss_run(loop, 0);
|
||||
|
||||
uv_mutex_lock(&control_lock);
|
||||
stop_signal = NULL;
|
||||
uv_mutex_unlock(&control_lock);
|
||||
uv_close((uv_handle_t *)&stop_async, NULL);
|
||||
|
||||
if (verbose) {
|
||||
LOGI("closed gracefully");
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SHADOWSOCKS_CONTROL_H
|
||||
#define SHADOWSOCKS_CONTROL_H
|
||||
|
||||
/*
|
||||
* Run ss-local inside an application: start it on a thread owned by the
|
||||
* library, stop it from anywhere, and wait for it to finish.
|
||||
*
|
||||
* The server state is process-wide, so one instance runs at a time; a
|
||||
* second ss_local_start() while one is running fails with EBUSY.
|
||||
*/
|
||||
|
||||
#include <shadowsocks.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct ss_local_handle ss_local_handle;
|
||||
|
||||
/*
|
||||
* Copy the profile and start serving on a new thread. `callback` (may be
|
||||
* NULL) runs on that thread once the sockets are listening, as with
|
||||
* start_ss_local_server_with_callback(). Returns 0 and sets *handle, or an
|
||||
* errno value: EBUSY, EINVAL or ENOMEM.
|
||||
*/
|
||||
int ss_local_start(const profile_t *profile, ss_local_callback callback, void *udata, ss_local_handle **handle);
|
||||
|
||||
/* Ask the server to stop. Safe from any thread (not from a signal
|
||||
* handler), and harmless once it has stopped. */
|
||||
void ss_local_stop(ss_local_handle *handle);
|
||||
|
||||
/* Wait for the server thread and return what the server returned (0 after
|
||||
* a requested stop). Call it once. */
|
||||
int ss_local_wait(ss_local_handle *handle);
|
||||
|
||||
/* Release the handle, stopping and waiting for the server first if needed. */
|
||||
void ss_local_destroy(ss_local_handle *handle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SHADOWSOCKS_CONTROL_H */
|
||||
@@ -0,0 +1,84 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SHADOWSOCKS_SHADOWSOCKS_HPP
|
||||
#define SHADOWSOCKS_SHADOWSOCKS_HPP
|
||||
|
||||
// C++ wrapper for embedding ss-local (header-only; C++17 or later).
|
||||
//
|
||||
// shadowsocks::LocalServer server;
|
||||
// if (int err = server.start(profile)) { ... }
|
||||
// ...
|
||||
// server.stop(); // from any thread
|
||||
// int result = server.wait();
|
||||
//
|
||||
// The library exports only the C entry points in <shadowsocks/control.h>,
|
||||
// so this header adds no C++ ABI to libshadowsocks-c. The destructor stops
|
||||
// and waits for a running server.
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <shadowsocks/control.h>
|
||||
|
||||
namespace shadowsocks {
|
||||
|
||||
class LocalServer {
|
||||
public:
|
||||
LocalServer() noexcept = default;
|
||||
LocalServer(const LocalServer &) = delete;
|
||||
LocalServer &operator=(const LocalServer &) = delete;
|
||||
LocalServer(LocalServer &&other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {}
|
||||
LocalServer &operator=(LocalServer &&other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
reset();
|
||||
handle_ = std::exchange(other.handle_, nullptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
~LocalServer() { reset(); }
|
||||
|
||||
// 0 on success, otherwise an errno value (EBUSY when a server already
|
||||
// runs in this process).
|
||||
int start(const profile_t &profile, ss_local_callback callback = nullptr, void *udata = nullptr) noexcept
|
||||
{
|
||||
if (handle_ != nullptr) {
|
||||
return EBUSY;
|
||||
}
|
||||
return ss_local_start(&profile, callback, udata, &handle_);
|
||||
}
|
||||
|
||||
void stop() noexcept
|
||||
{
|
||||
if (handle_ != nullptr) {
|
||||
ss_local_stop(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
// The server's return value; the object can start again afterwards.
|
||||
int wait() noexcept
|
||||
{
|
||||
if (handle_ == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
int result = ss_local_wait(handle_);
|
||||
ss_local_destroy(handle_);
|
||||
handle_ = nullptr;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool running() const noexcept { return handle_ != nullptr; }
|
||||
|
||||
private:
|
||||
void reset() noexcept
|
||||
{
|
||||
stop();
|
||||
(void)wait();
|
||||
}
|
||||
|
||||
ss_local_handle *handle_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace shadowsocks
|
||||
|
||||
#endif
|
||||
@@ -63,6 +63,18 @@ foreach(core_test result pool buffer arena fixed_map ownership)
|
||||
ss_strict_warnings(test_core_${core_test}.cpp)
|
||||
endforeach()
|
||||
|
||||
# The embedding API on the static library (POSIX: the test uses BSD sockets).
|
||||
if(TARGET shadowsocks-c-static AND UNIX)
|
||||
add_executable(test_local_api_cpp test_local_api_cpp.cpp)
|
||||
target_link_libraries(test_local_api_cpp PRIVATE shadowsocks-c-static Threads::Threads)
|
||||
target_compile_options(test_local_api_cpp PRIVATE -UNDEBUG)
|
||||
target_include_directories(test_local_api_cpp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
ss_project_options(test_local_api_cpp)
|
||||
ss_strict_warnings(test_local_api_cpp.cpp)
|
||||
add_test(NAME test_local_api_cpp COMMAND test_local_api_cpp)
|
||||
set_tests_properties(test_local_api_cpp PROPERTIES LABELS "unit" TIMEOUT 60)
|
||||
endif()
|
||||
|
||||
# C++ event layer (src/event) over libuv.
|
||||
ss_add_test(test_event_cpp "test_event_cpp.cpp" "ss_uv")
|
||||
ss_strict_warnings(test_event_cpp.cpp)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(shadowsocks_consumer LANGUAGES C)
|
||||
project(shadowsocks_consumer LANGUAGES C CXX)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(SS_PACKAGE_NAME shadowsocks-c CACHE STRING "Installed package to test")
|
||||
find_package(${SS_PACKAGE_NAME} CONFIG REQUIRED)
|
||||
foreach(kind shared static)
|
||||
@@ -18,5 +19,7 @@ foreach(kind shared static)
|
||||
endif()
|
||||
add_executable(consumer_${kind} main.c)
|
||||
target_link_libraries(consumer_${kind} PRIVATE shadowsocks::${kind})
|
||||
add_executable(consumer_cxx_${kind} main.cpp)
|
||||
target_link_libraries(consumer_cxx_${kind} PRIVATE shadowsocks::${kind})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// C++ consumer of the installed headers: the RAII wrapper over the C
|
||||
// control API, without starting a server.
|
||||
#include <shadowsocks/shadowsocks.hpp>
|
||||
|
||||
int main()
|
||||
{
|
||||
shadowsocks::LocalServer server;
|
||||
static int (*volatile start)(const profile_t *, ss_local_callback, void *, ss_local_handle **) = ss_local_start;
|
||||
return server.running() || start == nullptr;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
// The embedding API (<shadowsocks/control.h>, <shadowsocks/shadowsocks.hpp>):
|
||||
// start ss-local on the library's thread, talk SOCKS5 to it, refuse a second
|
||||
// instance, stop it from this thread, and start it again.
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <shadowsocks/shadowsocks.hpp>
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<int> ready_fd{ -1 };
|
||||
|
||||
void on_ready(int socks_fd, int, void *) { ready_fd = socks_fd; }
|
||||
|
||||
int free_port()
|
||||
{
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
sockaddr_in a{};
|
||||
a.sin_family = AF_INET;
|
||||
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
SS_CHECK(bind(fd, reinterpret_cast<sockaddr *>(&a), sizeof(a)) == 0);
|
||||
socklen_t len = sizeof(a);
|
||||
SS_CHECK(getsockname(fd, reinterpret_cast<sockaddr *>(&a), &len) == 0);
|
||||
close(fd);
|
||||
return ntohs(a.sin_port);
|
||||
}
|
||||
|
||||
bool socks_greeting(int port)
|
||||
{
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
sockaddr_in a{};
|
||||
a.sin_family = AF_INET;
|
||||
a.sin_port = htons(static_cast<uint16_t>(port));
|
||||
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
bool ok = false;
|
||||
if (connect(fd, reinterpret_cast<sockaddr *>(&a), sizeof(a)) == 0) {
|
||||
const unsigned char hello[3] = { 5, 1, 0 };
|
||||
unsigned char reply[2] = {};
|
||||
ok = send(fd, hello, sizeof(hello), 0) == 3 && recv(fd, reply, 2, MSG_WAITALL) == 2 && reply[0] == 5
|
||||
&& reply[1] == 0;
|
||||
}
|
||||
close(fd);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void wait_ready()
|
||||
{
|
||||
for (int i = 0; i < 500 && ready_fd < 0; ++i) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
SS_CHECK(ready_fd >= 0);
|
||||
}
|
||||
|
||||
void test_lifecycle()
|
||||
{
|
||||
int port = free_port();
|
||||
char host[] = "127.0.0.1", method[] = "aes-256-gcm", password[] = "secret";
|
||||
profile_t profile{};
|
||||
profile.remote_host = host;
|
||||
profile.local_addr = host;
|
||||
profile.method = method;
|
||||
profile.password = password;
|
||||
profile.remote_port = 1; // nothing listens there; only the SOCKS side is exercised
|
||||
profile.local_port = port;
|
||||
profile.timeout = 10;
|
||||
|
||||
for (int round = 0; round < 2; ++round) {
|
||||
ready_fd = -1;
|
||||
shadowsocks::LocalServer server;
|
||||
SS_CHECK(server.start(profile, &on_ready, nullptr) == 0);
|
||||
wait_ready();
|
||||
SS_CHECK(socks_greeting(port));
|
||||
|
||||
shadowsocks::LocalServer second;
|
||||
SS_CHECK(second.start(profile) == EBUSY);
|
||||
|
||||
server.stop();
|
||||
SS_CHECK(server.wait() == 0);
|
||||
SS_CHECK(!server.running());
|
||||
}
|
||||
|
||||
// A stop before the loop runs is honoured too.
|
||||
ss_local_handle *h = nullptr;
|
||||
ready_fd = -1;
|
||||
SS_CHECK(ss_local_start(&profile, &on_ready, nullptr, &h) == 0);
|
||||
ss_local_stop(h);
|
||||
SS_CHECK(ss_local_wait(h) == 0);
|
||||
ss_local_destroy(h);
|
||||
SS_CHECK(ss_local_start(nullptr, nullptr, nullptr, &h) == EINVAL);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_lifecycle);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user