Pull request 682: Add an "easy" Windows service interface for TrustTunnel GUI clients

Squashed commit of the following:

commit e262ed1da2c2aef6a5c5a45ec5d41066ba44979f
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Sun Apr 26 18:13:02 2026 +0300

    Return more correct error code

commit 4a9b56b2b4e746b59e559ec6ce689f961ddd9e2b
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Sat Apr 25 00:18:15 2026 +0300

    Cleanup

commit 7884c91e0e869b9c5bc4b83548aa18cf487d7af2
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Sat Apr 25 00:06:10 2026 +0300

    Cleanup + run clang-format

commit 7973e7aa5cabbb2e02ee559ea3e7bf9f073d3430
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Fri Apr 24 23:28:24 2026 +0300

    Cleanup

commit 53e982d626ebe841a8d27bb6dd96cc2ace05d0c4
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Fri Apr 24 22:42:27 2026 +0300

    Vibe test

commit d4fab62e2838dbecb7d04f811b62d6c4efad6440
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Fri Apr 24 22:24:00 2026 +0300

    Service interface and test

commit 5a84e3b0216ab0ce4cedcd65cc620e82d5426cd5
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Fri Apr 24 20:29:07 2026 +0300

    Allow authenticated users to start/stop service

commit 85238d526d447547d42cfe689952315de76b2869
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Fri Apr 24 17:59:13 2026 +0300

    Fix vibe code

commit 79df07013043a16ca1fad3a9ea4b01af7af314e9
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Fri Apr 24 14:12:47 2026 +0300

    Fix vibe code

commit e8ced6e1c76f519983ac756ea091ecc04332e33f
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Thu Apr 23 22:10:38 2026 +0300

    Vibe code more

commit af1d789c08f6bf27bed7510b4644ad5e17d3e709
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Thu Apr 23 19:28:20 2026 +0300

    Vibe-update AGENTS.md

commit b34813d984ca927f8e427b551ed787f036386557
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Thu Apr 23 19:28:09 2026 +0300

    Remove non-vibe-coded

commit 496dc4057e5e7db21e12f4b5d9ef0f9c9e22df5f
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Thu Apr 23 18:52:33 2026 +0300

    Vibe test

commit a1877a026496dc5919c72d44c7e8d971cda18d8f
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Thu Apr 23 15:31:38 2026 +0300

    Vibe code

commit 9e706843376b71cbf4160fe9edcff62ee9652b8f
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Thu Apr 23 00:13:18 2026 +0300

    Vibe code

commit a641c864fde194f9cb7c7001487801842b1d56b9
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Wed Apr 22 22:58:12 2026 +0300

    Vibe code

commit 9edb4183a2173154502b00aa63b6f7af688b9624
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Wed Apr 22 22:37:20 2026 +0300

    Vibe code

commit f3a7b059e8e2bce823410d7c6efcc521a48027fd
Author: Nikita Gorskikh <n.gorskikh@adguard.com>
Date:   Tue Mar 31 20:54:15 2026 +0300

    WIP
This commit is contained in:
Nikita Gorskikh
2026-05-04 14:12:30 +00:00
parent 10e2666ae1
commit 3c93077f4c
13 changed files with 2965 additions and 18 deletions
+2
View File
@@ -8,6 +8,8 @@ pyvenv.cfg
.clangd/
/.cache/
target/
# Conan and conan-cmake create some files in-tree by design
conan.lock
conanbuildinfo.*
+33 -3
View File
@@ -19,6 +19,12 @@ See [README.md](README.md) for full product details and
- **Conan 2.0.5+** — C++ package manager
- **Ninja** — build backend
- **Clang / LLVM 17+** (LLVM 19 on macOS) — compiler and tooling
(Windows builds use MSVC `cl.exe`, not clang)
- **clang-format 21+** — required by `make lint-cpp` / `make clang-format`
- **Python 3** — `scripts/bootstrap_conan_deps.py`, Conan wrappers
(`requirements.txt`)
- **Ruby / Fastlane** — iOS/Android release automation (`Gemfile`,
`fastlane/`)
## Directory Structure
@@ -38,6 +44,10 @@ See [README.md](README.md) for full product details and
| `cmake/` | CMake modules: unit test helper, Conan bootstrapping/provider |
| `bamboo-specs/` | CI/CD pipeline definitions (Bamboo) |
| `integration-tests/` | Docker-based integration test harness |
| `conan/` | Conan user settings and build profiles |
| `fastlane/` | iOS/Android release automation (Fastfile, Matchfile) |
| `.devcontainer/` | Docker-based remote debugging environment (see below) |
| `.github/` | GitHub Actions workflows and PR/issue templates |
### Module Dependency Flow
@@ -52,22 +62,31 @@ common ← tcpip ← core
## Build Commands
Run `make init` once after cloning to set up git hooks.
Running `make` with no arguments runs the `init` target — use `make all` (or
`make build_libs`) to actually build.
| Command | What It Does |
| --- | --- |
| `make init` | Configure git hooks path to `./scripts/hooks` |
| `make bootstrap_deps` | Export AdGuard Conan recipes to local cache (prerequisite of most build targets) |
| `make setup_cmake` | CMake configure only (accepts `SKIP_BOOTSTRAP=1`) |
| `make build_libs` | Bootstrap Conan deps → CMake configure → build `vpnlibs_core` |
| `make build_trusttunnel_client` | Build the CLI client binary (depends on `build_libs`) |
| `make build_wizard` | Build the setup wizard binary |
| `make build_and_export_bin` | Build binaries and copy to `$(EXPORT_DIR)` (default `bin/`) |
| `make all` | Build all binaries (client + wizard) |
| `make test` | Run all tests (`test-cpp` + `test-rust`) |
| `make test-cpp` | Build libs → build test targets → run `ctest` |
| `make test-rust` | `cargo test` on the setup_wizard workspace |
| `make lint` | Run all linters (`lint-md` + `lint-rust` + `lint-cpp`) |
| `make lint-cpp` | `clang-format` check + `clangd-tidy` |
| `make clang-format` | Explicit `clang-format` check only |
| `make clang-tidy` / `make clangd-tidy` | Run C++ static analysis only |
| `make lint-rust` | `cargo clippy` + `cargo fmt --check` |
| `make lint-md` | `markdownlint .` |
| `make lint-fix` | Auto-fix all fixable linter issues |
| `make lint-fix-cpp` / `lint-fix-rust` / `lint-fix-md` | Granular auto-fix targets |
| `make list-deps-dirs` | List Conan package directories (for finding dep headers) |
| `make compile_commands` | Generate `compile_commands.json` for IDE integration |
| `make clean` | Clean build artifacts |
@@ -92,6 +111,10 @@ Set `BUILD_TYPE=debug` for debug builds (default is `release` →
- `UPPER_CASE`: constants, `constexpr` locals, static constants
- Private/protected members prefixed with `m_`, globals with `g_`
- Use `libc++` (not `libstdc++`)
- Use static storage duration instead of anonymous namespaces for internal linkage where possible
- (e.g. `static const int VALUE = 42;` instead of putting it in an anonymous namespace)
- Function descriptions are written in imperative language
- e.g. "Calculate the sum of two numbers" instead of "Calculates the sum of two numbers"
### Rust
@@ -119,6 +142,12 @@ Set `BUILD_TYPE=debug` for debug builds (default is `release` →
- Prefer existing patterns over inventing new ones
- Keep changes minimal and focused
- Tests live in `test/` subdirectories alongside the module they cover
- Logging guidelines:
- Use `DEBUG` level for verbose debug info, `INFO` for high-level events,
`WARN` for recoverable issues, and `ERROR` for critical (unrecoverable) errors
- Very frequent events (e.g. every packet) should be logged at `DEBUG` level, while important state changes (e.g. connection established, error occurred) should be at `INFO` or higher
- Include relevant context in log messages (e.g. connection ID, error code)
- Avoid logging sensitive information (e.g. IP addresses, payload data)
## Docker Debug Environment
@@ -141,10 +170,11 @@ Managed via Conan. Key libraries:
- **libevent** — async event loop
- **nghttp2** — HTTP/2
- **quiche** — HTTP/3 / QUIC (disabled on MIPS)
- **openssl** (BoringSSL) — TLS
- **openssl** (BoringSSL; MIPS falls back to `openssl/3.1.5-quic1@adguard/oss`) — TLS
- **nlohmann_json**, **tomlplusplus** — config parsing
- **cxxopts** — CLI argument parsing
- **magic_enum** — enum reflection
- **gtest** — unit testing
Local conan cache is populated by `make bootstrap_deps` which is dependency for many other make commands.
@@ -156,10 +186,10 @@ directories, then look in each directory's `include/` subdirectory.
You MUST follow the following rules for EVERY task that you perform:
- You MUST verify it with linter, formatter, and TypeScript compiler.
- You MUST verify it with linter, formatter, and C++/Rust compilers.
Use the following commands:
- `make` to check if code builds
- `make all` to check if code builds (bare `make` only runs `init`)
- `make test` to build and run unit tests
- `make lint` to run the linters
- `make lint-fix` to fix linting issues that can be fixed automatically
+17 -2
View File
@@ -28,12 +28,27 @@ if (NOT TARGET vpnlibs_trusttunnel)
add_subdirectory(${ROOT_DIR} ${CMAKE_BINARY_DIR}/trusttunnel)
endif ()
add_library(vpn_easy_a STATIC EXCLUDE_FROM_ALL src/vpn_easy.cpp)
add_library(vpn_easy_a STATIC src/vpn_easy.cpp src/vpn_easy_pipe.cpp)
target_include_directories(vpn_easy_a PUBLIC include)
target_link_libraries(vpn_easy_a vpnlibs_trusttunnel)
add_library(vpn_easy SHARED src/vpn_easy.cpp)
target_link_libraries(vpn_easy vpn_easy_a)
add_executable(vpn_easy_test EXCLUDE_FROM_ALL test/vpn_easy_test.cpp)
add_executable(vpn_easy_test test/vpn_easy_test.cpp)
target_link_libraries(vpn_easy_test vpn_easy_a)
add_executable(vpn_easy_service src/vpn_easy_service.cpp)
target_link_libraries(vpn_easy_service vpn_easy_a)
add_executable(vpn_easy_service_test test/vpn_easy_service_test.cpp)
target_link_libraries(vpn_easy_service_test vpn_easy_a)
add_dependencies(vpn_easy_service_test vpn_easy_service)
enable_testing()
include(${ROOT_DIR}/cmake/add_unit_test.cmake)
set(TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/test)
link_libraries(vpn_easy_a)
add_unit_test(vpn_easy_pipe_test "${TEST_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/src" TRUE TRUE)
+1 -1
View File
@@ -1,4 +1,4 @@
# Easy wrapper for AdGuard VPI API
# Easy wrapper for AdGuard VPN API
- Basically, the `trusttunnel_client` command line application in the form of a library.
- Only two buttons: `start` and `stop`. The first one accepts the configuration in TOML format.
+22 -1
View File
@@ -10,7 +10,12 @@ extern "C" {
#endif
typedef struct vpn_easy_s vpn_easy_t;
typedef void (*on_state_changed_t)(void *arg, int new_state_description);
/** See `ag::VpnSessionState`. */
typedef void (*on_state_changed_t)(void *arg, int state);
/** See `ag::VpnConnectionInfoEvent`. */
typedef void (*on_connection_info_t)(void *arg, void *connection_info);
/**
* Start (connect) a VPN client.
@@ -29,6 +34,22 @@ WIN_EXPORT void vpn_easy_start(
*/
WIN_EXPORT void vpn_easy_stop();
/**
* Start (connect) a VPN client. The callbacks and their arguments passed to this function
* must remain valid throughout the lifetime of the VPN client.
* @param toml_config VPN client parameters in TOML format.
* @param state_changed_cb A function which will be called each time the VPN client's state changes.
* @param state_changed_cb_arg An argument passed to each invocation of the state change function.
* @param connection_info_cb A function called each time a connection is made through the VPN.
* @param connection_info_cb_arg An argument passed to each invocation of the connection info function.
* @return On success, a pointer to the started VPN client instance. On error, a null pointer.
*/
vpn_easy_t *vpn_easy_start_ex(const char *toml_config, on_state_changed_t state_changed_cb, void *state_changed_cb_arg,
on_connection_info_t connection_info_cb, void *connection_info_cb_arg);
/** Stop (disconnect) a VPN client and free all associated resources. */
void vpn_easy_stop_ex(vpn_easy_t *vpn);
#ifdef __cplusplus
}; // extern "C"
#endif
@@ -0,0 +1,137 @@
#pragma once
#include <stdint.h>
#include "vpn/platform.h"
#include "vpn/vpn_easy.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* Communication with the service is done by sending messages of the form:
* ```
* struct Message {
* uint32_t type;
* uint32_t length; // The length of the `data` field.
* uint8_t data[0]; // `length` bytes of data.
* };
* ```
* over the named pipe configured at service creation time (see `vpn_easy_service_install()`).
* The format of the `data` field is given by the message type. Integers are in network byte order.
*/
typedef enum {
/**
* A request to start (connect) the VPN client. The data field must contain the VPN client configuration
* in TOML format (encoded in UTF-8 as per TOML specification).
*
* If the client is already connecting or connected, it is requested to stop
* first and then start with the new configuration.
*
* If the client fails to start for whatever reason, the service will send a state change
* message with the state `VPN_SS_DISCONNECTED`.
*/
VPN_EASY_SVC_MSG_START = 0,
/**
* A request to stop (disconnect) the VPN client. The length field must be zero, the data field empty.
* If the client is already stopped, this message is ignored.
*/
VPN_EASY_SVC_MSG_STOP,
/**
* Sent by the service when the VPN client state changes. `length` is always `4` in network byte order, `data`
* is an `int32_t` in network byte order, one of the `ag::VpnSessionState` values.
*
* The service client should wait for this message after sending a start/stop request.
*/
VPN_EASY_SVC_MSG_STATE_CHANGED,
/**
* Sent by the service to notify the client of a new connection through the VPN tunnel. The data field
* is a JSON document describing the connection, as returned by `ag::ConnectionInfo::to_json()`.
*/
VPN_EASY_SVC_MSG_CONNECTION_INFO,
} VpnEasyServiceMessageType;
typedef enum {
/** Access denied. Check if the calling process is running as administrator. */
VPN_EASY_SVC_ERR_ACCESS = 1,
/** Service already exists. Uninstall it with `vpn_easy_service_uninstall()` first. */
VPN_EASY_SVC_ERR_SERVICE_EXISTS,
/** No service with the given name exists. */
VPN_EASY_SVC_ERR_NO_SUCH_SERVICE,
/** An operation on the service took too long. */
VPN_EASY_SVC_ERR_TIMED_OUT,
/** Encountered an unexpected error. Probably as a result of API misusage. The log may contain more details. */
VPN_EASY_SVC_ERR_OTHER,
} VpnEasyServiceError;
/**
* Create and start a VPN service. This function requires administrator privileges. The service is configured
* to start automatically at system startup. After startup, the service is listening on a named pipe `pipe_name`,
* and can be controlled by connecting and sending messages on that pipe. The protocol details are given by the
* description of `VpnEasyServiceMessageType` enumeration. Anyone can read/write from/to the pipe.
* @param image_path The absolute path to the `vpn_easy_service` executable.
* @param logfile_path The absolute path to the service's log file. Will be created if doesn't exist.
* @param name The service name. At most 256 characters.
* @param pipe_name The name for the named pipe used to communicate with the service.
* A string of at most 256 characters of the form: "\\.\pipe\<pipename>", where "<pipename>"
* can include any character except the backslash.
* @param display_name The display name to be used by user interface programs to identify the service.
* At most 256 characters.
* @param description A comment that explains the purpose of the service.
* @return Zero on success, one of `VpnEasyServiceError` constants on failure.
*/
WIN_EXPORT int32_t vpn_easy_service_install(const wchar_t *image_path, const wchar_t *logfile_path,
const wchar_t *pipe_name, const wchar_t *name, const wchar_t *display_name, const wchar_t *description);
/**
* Stop and delete the VPN service named `name`. This function requires administrator privileges. The service is
* requested to stop and marked for deletion. It will be deleted when it has stopped and all handles to it have
* been closed. If it doesn't stop for some reason, it will be deleted when the system is restarted.
* @param name The service name that was passed to `vpn_easy_service_install()`.
* @return Zero on success, one of `VpnEasyServiceError` constants on failure.
*/
WIN_EXPORT int32_t vpn_easy_service_uninstall(const wchar_t *name);
/**
* Start the VPN service named `service_name`.
*
* This will start the Windows service if it's not already running, connect to the running service
* through the named pipe and instruct it to start the VPN client with the provided configuration.
*
* It shall then pass the service state change messages to `state_changed_cb`, which must remain
* valid (along with `state_changed_cb_arg`) until the service is stopped with `vpn_easy_service_stop()`.
* The callback is invoked on an unspecified thread, and may be called concurrently with `vpn_easy_service_start()`.
*
* @param service_name The service name that was passed to `vpn_easy_service_install()`.
* @param pipe_name The name of the pipe that was passed to `vpn_easy_service_install()`.
* @param toml_config The VPN client configuration in TOML format (encoded in UTF-8 as per TOML specification).
* @param state_changed_cb A function which will be called each time the VPN client's state changes.
* @param state_changed_cb_arg An argument passed to each invocation of the state change function.
* @return Zero on success, one of `VpnEasyServiceError` constants on failure.
*/
WIN_EXPORT int32_t vpn_easy_service_start(const wchar_t *service_name, const wchar_t *pipe_name,
const char *toml_config, on_state_changed_t state_changed_cb, void *state_changed_cb_arg);
/**
* Stop the VPN service named `service_name`.
*
* This will stop both the VPN client and the Windows service.
* After this function returns, the state change callback will not be called anymore.
*
* @param service_name The service name that was passed to `vpn_easy_service_install()`.
* @param pipe_name The name of the pipe that was passed to `vpn_easy_service_install()`.
* @return Zero on success, one of `VpnEasyServiceError` constants on failure.
*/
WIN_EXPORT int32_t vpn_easy_service_stop(const wchar_t *service_name, const wchar_t *pipe_name);
#ifdef __cplusplus
}; // extern "C"
#endif
+407 -7
View File
@@ -1,20 +1,35 @@
#include "vpn/vpn_easy.h"
#include "vpn/vpn_easy_service.h"
#include <chrono>
#include <filesystem>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <variant>
#include <vector>
#include <aclapi.h>
#include <magic_enum/magic_enum.hpp>
#include <toml++/toml.h>
#include <fmt/format.h>
#include <fmt/xchar.h>
#include "common/logger.h"
#include "common/net_utils.h"
#include "common/utils.h"
#include "net/tls.h"
#include "vpn/event_loop.h"
#include "vpn/platform.h"
#include "vpn/trusttunnel/auto_network_monitor.h"
#include "vpn/trusttunnel/client.h"
#include "vpn/trusttunnel/config.h"
#include "vpn/vpn.h"
#include "vpn_easy_pipe.h"
static ag::Logger g_logger{"VPN_SIMPLE"};
@@ -84,8 +99,8 @@ struct vpn_easy_s {
std::unique_ptr<ag::AutoNetworkMonitor> network_monitor;
};
static vpn_easy_t *vpn_easy_start_internal(
const char *toml_config, on_state_changed_t state_changed_cb, void *state_changed_cb_arg) {
vpn_easy_t *vpn_easy_start_ex(const char *toml_config, on_state_changed_t state_changed_cb, void *state_changed_cb_arg,
on_connection_info_t connection_info_cb, void *connection_info_cb_arg) {
toml::parse_result parsed_config = toml::parse(toml_config);
if (!parsed_config) {
warnlog(g_logger, "Failed to parse the TOML config: {}", parsed_config.error().description());
@@ -119,11 +134,17 @@ static vpn_easy_t *vpn_easy_start_internal(
state_changed_cb(state_changed_cb_arg, event->state);
}
};
if (connection_info_cb) {
callbacks.connection_info_handler = [connection_info_cb, connection_info_cb_arg](
ag::VpnConnectionInfoEvent *event) {
connection_info_cb(connection_info_cb_arg, event);
};
}
auto vpn = std::make_unique<vpn_easy_t>();
std::string bound_if;
if (const auto *tun = std::get_if<TrustTunnelConfig::TunListener>(&trusttunnel_config->listener)) {
if (const auto *tun = std::get_if<ag::TrustTunnelConfig::TunListener>(&trusttunnel_config->listener)) {
bound_if = tun->bound_if;
}
@@ -141,7 +162,7 @@ static vpn_easy_t *vpn_easy_start_internal(
return vpn.release();
}
static void vpn_easy_stop_internal(vpn_easy_t *vpn) {
void vpn_easy_stop_ex(vpn_easy_t *vpn) {
if (!vpn) {
return;
}
@@ -175,7 +196,7 @@ public:
warnlog(g_logger, "VPN has been already started");
return;
}
m_vpn = vpn_easy_start_internal(config.data(), callback, arg); // blocking
m_vpn = vpn_easy_start_ex(config.data(), callback, arg, nullptr, nullptr); // blocking
if (!m_vpn) {
errlog(g_logger, "Failed to start VPN!");
return;
@@ -193,7 +214,7 @@ public:
return;
}
auto *vpn = std::exchange(m_vpn, nullptr);
vpn_easy_stop_internal(vpn);
vpn_easy_stop_ex(vpn);
});
}
@@ -215,4 +236,383 @@ void vpn_easy_start(const char *toml_config, on_state_changed_t state_changed_cb
void vpn_easy_stop() {
VpnEasyManager::instance().stop_async();
}
}
static std::wstring escape(const wchar_t *str, const wchar_t *chars_to_escape, wchar_t escape_char) {
std::wstring ret;
ret.reserve(wcslen(str) * 2);
while (*str != L'\0') {
if (wcschr(chars_to_escape, *str)) {
ret += escape_char;
}
ret += *str;
++str;
}
return ret;
}
using AutoScHandle = ag::UniquePtr<std::remove_pointer_t<SC_HANDLE>, &CloseServiceHandle>;
// Grant SERVICE_START and SERVICE_STOP to authenticated users on the given service handle.
// Return true on success, false on any error (logged at DEBUG level).
static bool grant_authenticated_users_start_stop(SC_HANDLE svc) {
// Query the current security descriptor size
DWORD bytes_needed = 0;
if (!QueryServiceObjectSecurity(svc, DACL_SECURITY_INFORMATION, nullptr, 0, &bytes_needed)
&& GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
dbglog(g_logger, "QueryServiceObjectSecurity (size): {} ({})", GetLastError(),
ag::sys::strerror(GetLastError()));
return false;
}
std::vector<uint8_t> sd_buf;
sd_buf.resize(bytes_needed);
auto *sd = reinterpret_cast<PSECURITY_DESCRIPTOR>(sd_buf.data());
if (!QueryServiceObjectSecurity(svc, DACL_SECURITY_INFORMATION, sd, bytes_needed, &bytes_needed)) {
dbglog(g_logger, "QueryServiceObjectSecurity: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return false;
}
// Retrieve the existing DACL from the security descriptor
BOOL dacl_present = FALSE;
PACL old_dacl = nullptr;
BOOL dacl_defaulted = FALSE;
if (!GetSecurityDescriptorDacl(sd, &dacl_present, &old_dacl, &dacl_defaulted)) {
dbglog(g_logger, "GetSecurityDescriptorDacl: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return false;
}
// Build the SID for Authenticated Users (S-1-5-11)
SID_IDENTIFIER_AUTHORITY nt_authority = SECURITY_NT_AUTHORITY;
PSID authenticated_users_sid = nullptr;
if (!AllocateAndInitializeSid(
&nt_authority, 1, SECURITY_AUTHENTICATED_USER_RID, 0, 0, 0, 0, 0, 0, 0, &authenticated_users_sid)) {
dbglog(g_logger, "AllocateAndInitializeSid: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return false;
}
PACL new_dacl = nullptr;
ag::utils::ScopeExit cleanup{[&] {
LocalFree(new_dacl);
FreeSid(authenticated_users_sid);
}};
// Build an EXPLICIT_ACCESS entry granting SERVICE_START | SERVICE_STOP
EXPLICIT_ACCESS_W ea{};
ea.grfAccessPermissions = SERVICE_START | SERVICE_STOP;
ea.grfAccessMode = SET_ACCESS;
ea.grfInheritance = NO_INHERITANCE;
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ea.Trustee.ptstrName = reinterpret_cast<LPWSTR>(authenticated_users_sid);
DWORD result = SetEntriesInAclW(1, &ea, old_dacl, &new_dacl);
if (result != ERROR_SUCCESS) {
dbglog(g_logger, "SetEntriesInAclW: {} ({})", result, ag::sys::strerror(result));
return false;
}
// Build a new security descriptor with the updated DACL
SECURITY_DESCRIPTOR new_sd{};
if (!InitializeSecurityDescriptor(&new_sd, SECURITY_DESCRIPTOR_REVISION)) {
dbglog(g_logger, "InitializeSecurityDescriptor: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return false;
}
if (!SetSecurityDescriptorDacl(&new_sd, TRUE, new_dacl, FALSE)) {
dbglog(g_logger, "SetSecurityDescriptorDacl: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return false;
}
// Apply the updated security descriptor to the service
if (!SetServiceObjectSecurity(svc, DACL_SECURITY_INFORMATION, &new_sd)) {
dbglog(g_logger, "SetServiceObjectSecurity: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return false;
}
return true;
}
int32_t vpn_easy_service_install(const wchar_t *image_path_, const wchar_t *logfile_path_, const wchar_t *pipe_name_,
const wchar_t *name, const wchar_t *display_name, const wchar_t *description) {
std::wstring image_path = escape(image_path_, L"\"", L'\\');
std::wstring logfile_path = escape(logfile_path_, L"\"", L'\\');
std::wstring pipe_name = escape(pipe_name_, L"\"", L'\\');
std::wstring cmd = fmt::format(L"\"{}\" \"{}\" \"{}\"", image_path, logfile_path, pipe_name);
AutoScHandle scm{OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CREATE_SERVICE)};
if (!scm) {
if (ERROR_ACCESS_DENIED == GetLastError()) {
return VPN_EASY_SVC_ERR_ACCESS;
}
dbglog(g_logger, "OpenSCManagerW: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return VPN_EASY_SVC_ERR_OTHER;
}
AutoScHandle svc{CreateServiceW(scm.get(), name, display_name, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, cmd.c_str(), nullptr, nullptr, nullptr, nullptr, nullptr)};
if (!svc) {
if (ERROR_SERVICE_EXISTS == GetLastError()) {
return VPN_EASY_SVC_ERR_SERVICE_EXISTS;
}
if (ERROR_ACCESS_DENIED == GetLastError()) {
return VPN_EASY_SVC_ERR_ACCESS;
}
dbglog(g_logger, "CreateServiceW: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return VPN_EASY_SVC_ERR_OTHER;
}
SERVICE_DESCRIPTIONW desc{.lpDescription = const_cast<wchar_t *>(description)};
ChangeServiceConfig2W(svc.get(), SERVICE_CONFIG_DESCRIPTION, &desc);
if (!grant_authenticated_users_start_stop(svc.get())) {
dbglog(g_logger, "Failed to grant start/stop permissions to authenticated users");
return VPN_EASY_SVC_ERR_OTHER;
}
if (!StartServiceW(svc.get(), 0, nullptr)) {
dbglog(g_logger, "StartServiceW: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return VPN_EASY_SVC_ERR_OTHER;
}
return 0;
}
int32_t vpn_easy_service_uninstall(const wchar_t *name) {
AutoScHandle scm{OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT)};
if (!scm) {
if (ERROR_ACCESS_DENIED == GetLastError()) {
return VPN_EASY_SVC_ERR_ACCESS;
}
dbglog(g_logger, "OpenSCManagerW: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return VPN_EASY_SVC_ERR_OTHER;
}
AutoScHandle svc{OpenServiceW(scm.get(), name, STANDARD_RIGHTS_DELETE | SERVICE_STOP)};
if (!svc) {
if (ERROR_ACCESS_DENIED == GetLastError()) {
return VPN_EASY_SVC_ERR_ACCESS;
}
if (ERROR_SERVICE_DOES_NOT_EXIST == GetLastError()) {
return VPN_EASY_SVC_ERR_NO_SUCH_SERVICE;
}
dbglog(g_logger, "OpenServiceW: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return VPN_EASY_SVC_ERR_OTHER;
}
SERVICE_STATUS status{};
if (!ControlService(svc.get(), SERVICE_CONTROL_STOP, &status) && ERROR_SERVICE_NOT_ACTIVE != GetLastError()) {
dbglog(g_logger, "ControlService(STOP): {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
}
if (!DeleteService(svc.get())) {
dbglog(g_logger, "DeleteService: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return VPN_EASY_SVC_ERR_OTHER;
}
return 0;
}
static constexpr auto SERVICE_OPERATION_TIMEOUT = std::chrono::seconds{30};
static constexpr auto SERVICE_POLL_INTERVAL = std::chrono::milliseconds{250};
static struct ServiceControllerState {
std::mutex mutex;
HANDLE stop_event = nullptr;
std::unique_ptr<ag::vpn_easy::PipeClient> pipe_client;
std::thread io_thread;
on_state_changed_t state_changed_cb = nullptr;
void *state_changed_cb_arg = nullptr;
/// Tear down the pipe session and clear all state. Caller must hold `mutex`.
void reset() {
if (stop_event) {
SetEvent(stop_event);
}
if (io_thread.joinable()) {
io_thread.join();
}
pipe_client.reset();
if (stop_event) {
CloseHandle(stop_event);
stop_event = nullptr;
}
state_changed_cb = nullptr;
state_changed_cb_arg = nullptr;
}
} g_svc_state;
/// Poll a service until it reaches the desired state, or timeout.
/// Return true if the desired state was reached, false on timeout.
static bool wait_for_service_state(SC_HANDLE svc, DWORD desired_state, std::chrono::milliseconds timeout) {
auto deadline = std::chrono::steady_clock::now() + timeout;
for (;;) {
SERVICE_STATUS status{};
if (!QueryServiceStatus(svc, &status)) {
dbglog(g_logger, "QueryServiceStatus: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return false;
}
if (status.dwCurrentState == desired_state) {
return true;
}
if (std::chrono::steady_clock::now() >= deadline) {
return false;
}
std::this_thread::sleep_for(SERVICE_POLL_INTERVAL);
}
}
/// Map a Windows error code from an SCM operation to a VpnEasyServiceError.
static int32_t map_scm_error(const char *func_name) {
DWORD err = GetLastError();
if (err == ERROR_ACCESS_DENIED) {
return VPN_EASY_SVC_ERR_ACCESS;
}
if (err == ERROR_SERVICE_DOES_NOT_EXIST) {
return VPN_EASY_SVC_ERR_NO_SUCH_SERVICE;
}
dbglog(g_logger, "{}: {} ({})", func_name, err, ag::sys::strerror(err));
return VPN_EASY_SVC_ERR_OTHER;
}
int32_t vpn_easy_service_start(const wchar_t *service_name, const wchar_t *pipe_name, const char *toml_config,
on_state_changed_t state_changed_cb, void *state_changed_cb_arg) {
std::scoped_lock lock{g_svc_state.mutex};
if (g_svc_state.pipe_client) {
warnlog(g_logger, "Service client is already active");
return VPN_EASY_SVC_ERR_OTHER;
}
// Save callbacks early so the handler lambda can reference them.
g_svc_state.state_changed_cb = state_changed_cb;
g_svc_state.state_changed_cb_arg = state_changed_cb_arg;
// ScopeExit: on any error return, clean up everything and clear callbacks.
bool success = false;
ag::utils::ScopeExit cleanup{[&] {
if (success) {
return;
}
g_svc_state.reset();
}};
AutoScHandle scm{OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT)};
if (!scm) {
return map_scm_error("OpenSCManagerW");
}
AutoScHandle svc{OpenServiceW(scm.get(), service_name, SERVICE_START | SERVICE_QUERY_STATUS)};
if (!svc) {
return map_scm_error("OpenServiceW");
}
SERVICE_STATUS status{};
if (!QueryServiceStatus(svc.get(), &status)) {
dbglog(g_logger, "QueryServiceStatus: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return VPN_EASY_SVC_ERR_OTHER;
}
if (status.dwCurrentState != SERVICE_RUNNING) {
if (!StartServiceW(svc.get(), 0, nullptr)) {
if (GetLastError() != ERROR_SERVICE_ALREADY_RUNNING) {
dbglog(g_logger, "StartServiceW: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return VPN_EASY_SVC_ERR_OTHER;
}
}
if (!wait_for_service_state(svc.get(), SERVICE_RUNNING, SERVICE_OPERATION_TIMEOUT)) {
errlog(g_logger, "Service did not reach RUNNING state within timeout");
return VPN_EASY_SVC_ERR_TIMED_OUT;
}
}
g_svc_state.stop_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!g_svc_state.stop_event) {
dbglog(g_logger, "CreateEventW: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return VPN_EASY_SVC_ERR_OTHER;
}
g_svc_state.pipe_client = std::make_unique<ag::vpn_easy::PipeClient>(
pipe_name, g_svc_state.stop_event,
[](VpnEasyServiceMessageType what, ag::Uint8View data) {
switch (what) {
case VPN_EASY_SVC_MSG_STATE_CHANGED: {
if (data.size() < sizeof(uint32_t)) {
dbglog(g_logger, "STATE_CHANGED message too short: {} bytes", data.size());
break;
}
uint32_t net_state = 0;
memcpy(&net_state, data.data(), sizeof(net_state));
auto state = static_cast<int32_t>(ntohl(net_state));
if (g_svc_state.state_changed_cb) {
g_svc_state.state_changed_cb(g_svc_state.state_changed_cb_arg, state);
}
break;
}
case VPN_EASY_SVC_MSG_CONNECTION_INFO:
dbglog(g_logger, "Received CONNECTION_INFO (ignored)");
break;
default:
dbglog(g_logger, "Ignoring unexpected message type: {}", static_cast<int>(what));
break;
}
},
std::chrono::duration_cast<std::chrono::milliseconds>(SERVICE_OPERATION_TIMEOUT));
g_svc_state.io_thread = std::thread([] {
if (!g_svc_state.pipe_client->loop()) {
// Deliver VPN_SS_DISCONNECTED on unexpected exit if callback is still set.
if (g_svc_state.state_changed_cb) {
g_svc_state.state_changed_cb(g_svc_state.state_changed_cb_arg, ag::VPN_SS_DISCONNECTED);
}
}
});
if (!g_svc_state.pipe_client->wait_connected()) {
errlog(g_logger, "PipeClient failed to connect within timeout");
return VPN_EASY_SVC_ERR_TIMED_OUT;
}
size_t config_len = strlen(toml_config);
g_svc_state.pipe_client->send(VPN_EASY_SVC_MSG_START, {reinterpret_cast<const uint8_t *>(toml_config), config_len});
success = true;
return 0;
}
int32_t vpn_easy_service_stop(const wchar_t *service_name, const wchar_t *pipe_name) {
std::scoped_lock lock{g_svc_state.mutex};
if (!g_svc_state.pipe_client) {
return 0;
}
g_svc_state.pipe_client->send(VPN_EASY_SVC_MSG_STOP, {});
g_svc_state.reset();
AutoScHandle scm{OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT)};
if (!scm) {
return map_scm_error("OpenSCManagerW");
}
AutoScHandle svc{OpenServiceW(scm.get(), service_name, SERVICE_STOP | SERVICE_QUERY_STATUS)};
if (!svc) {
return map_scm_error("OpenServiceW");
}
SERVICE_STATUS status{};
if (!ControlService(svc.get(), SERVICE_CONTROL_STOP, &status)) {
if (GetLastError() != ERROR_SERVICE_NOT_ACTIVE) {
dbglog(g_logger, "ControlService(STOP): {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
}
}
if (!wait_for_service_state(svc.get(), SERVICE_STOPPED, SERVICE_OPERATION_TIMEOUT)) {
errlog(g_logger, "Service did not stop within timeout");
return VPN_EASY_SVC_ERR_TIMED_OUT;
}
return 0;
}
+589
View File
@@ -0,0 +1,589 @@
#include "vpn_easy_pipe.h"
#include <sddl.h>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstring>
#include <utility>
#include "common/logger.h"
#include "common/system_error.h"
#include "vpn/internal/wire_utils.h"
namespace ag::vpn_easy {
static ag::Logger g_server_logger{"PIPE_SERVER"};
static ag::Logger g_client_logger{"PIPE_CLIENT"};
namespace detail {
void free_security_descriptor(SECURITY_DESCRIPTOR *sd) {
LocalFree(sd);
}
} // namespace detail
// ---------------------------------------------------------------------------
// PipeEndpoint
// ---------------------------------------------------------------------------
PipeEndpoint::PipeEndpoint(HANDLE stop_event, Handler handler, ag::Logger &logger)
: m_io_event{CreateEventW(nullptr, TRUE, FALSE, nullptr)}
, m_write_event{CreateEventW(nullptr, TRUE, FALSE, nullptr)}
, m_wake_event{CreateEventW(nullptr, FALSE, FALSE, nullptr)}
, m_logger{logger}
, m_stop_event{stop_event}
, m_handler{std::move(handler)} {
assert(m_handler);
m_olr.hEvent = m_io_event;
m_olw.hEvent = m_write_event;
m_input_buf.resize(INPUT_BUF_SIZE);
}
PipeEndpoint::~PipeEndpoint() {
// Subclass destructor must have already torn down `m_pipe` (so that any virtual
// `teardown_pipe()` would be unreachable from here, where it would not dispatch to the
// derived implementation).
if (m_io_event != nullptr) {
CloseHandle(m_io_event);
}
if (m_write_event != nullptr) {
CloseHandle(m_write_event);
}
if (m_wake_event != nullptr) {
CloseHandle(m_wake_event);
}
}
void PipeEndpoint::cancel_pending_io() {
CancelIoEx(m_pipe, nullptr);
if (m_write_pending) {
DWORD ignored = 0;
GetOverlappedResult(m_pipe, &m_olw, &ignored, TRUE);
}
if (m_read_pending || !m_connected.load(std::memory_order_relaxed)) {
DWORD ignored = 0;
GetOverlappedResult(m_pipe, &m_olr, &ignored, TRUE);
}
}
void PipeEndpoint::prepare_for_connect() {
m_connected.store(false, std::memory_order_relaxed);
m_read_pending = false;
m_write_pending = false;
m_input_buf_used = 0;
ResetEvent(m_io_event);
ResetEvent(m_write_event);
}
std::vector<uint8_t> PipeEndpoint::compose_message(VpnEasyServiceMessageType what, ag::Uint8View data) {
assert(data.size() < size_t(UINT32_MAX));
std::vector<uint8_t> ret;
ret.resize(sizeof(uint32_t) + sizeof(uint32_t) + data.size());
ag::wire_utils::Writer w{{ret.data(), ret.size()}};
w.put_u32(static_cast<uint32_t>(what));
w.put_u32(static_cast<uint32_t>(data.size()));
w.put_data(data);
return ret;
}
void PipeEndpoint::send(VpnEasyServiceMessageType what, ag::Uint8View data) {
{
std::scoped_lock l{m_pending_writes_lock};
// disconnect_and_reset() stores `false` BEFORE taking this lock, so any push that
// happens-before disconnect's lock acquisition will be observed (and cleared) by
// disconnect, and any push that happens-after will see `false` here and bail out.
if (!m_connected.load(std::memory_order_relaxed)) {
return;
}
if (m_pending_writes.size() == MAX_PENDING_WRITES) {
static_assert(MAX_PENDING_WRITES > 0);
m_pending_writes.pop_front();
}
m_pending_writes.push_back(PendingWrite{compose_message(what, data), 0});
}
SetEvent(m_wake_event);
}
bool PipeEndpoint::loop() {
if (m_io_event == nullptr || m_write_event == nullptr || m_wake_event == nullptr) {
return false;
}
if (!start_connect()) {
return false;
}
HANDLE events[] = {m_stop_event, m_wake_event, m_io_event, m_write_event};
constexpr DWORD EVENT_COUNT = static_cast<DWORD>(std::size(events));
for (;;) {
DWORD wait = WaitForMultipleObjects(EVENT_COUNT, events, FALSE, INFINITE);
if (wait >= WAIT_OBJECT_0 + EVENT_COUNT) {
errlog(m_logger, "WaitForMultipleObjects: {:#x}, GetLastError: {} ({})", wait, GetLastError(),
ag::sys::strerror(GetLastError()));
return false;
}
DWORD idx = wait - WAIT_OBJECT_0;
if (idx == 0) {
// Stop event.
return true;
}
if (idx == 2) {
// m_io_event: overlapped connect or ReadFile completed.
if (!m_connected.load(std::memory_order_relaxed)) {
if (!finalize_connect()) {
if (auto r = handle_disconnect()) {
return *r;
}
continue;
}
} else if (m_read_pending) {
if (!complete_read()) {
if (auto r = handle_disconnect()) {
return *r;
}
continue;
}
}
}
if (idx == 3 && m_write_pending) {
// m_write_event: WriteFile completed.
if (!complete_write()) {
if (auto r = handle_disconnect()) {
return *r;
}
continue;
}
}
// After any wake-up, try to issue a fresh read (if connected and not already pending) and
// pump as many writes as possible.
if (m_connected.load(std::memory_order_relaxed) && !m_read_pending) {
if (!start_read()) {
if (auto r = handle_disconnect()) {
return *r;
}
continue;
}
}
if (m_connected.load(std::memory_order_relaxed) && !pump_writes()) {
if (auto r = handle_disconnect()) {
return *r;
}
continue;
}
}
}
std::optional<bool> PipeEndpoint::handle_disconnect() {
disconnect_and_reset();
if (!should_reconnect_on_disconnect()) {
return true; // Graceful peer-initiated close.
}
if (!start_connect()) {
return false;
}
return std::nullopt;
}
bool PipeEndpoint::start_read() {
if (m_input_buf_used >= m_input_buf.size()) {
// Buffer is full but no complete message could be parsed -- impossible if MAX_MESSAGE_SIZE
// is honored, so this indicates a protocol violation. Drop the connection.
warnlog(m_logger, "input buffer full ({} bytes) with no parsable message; dropping connection",
m_input_buf_used);
return false;
}
DWORD read_size = 0;
BOOL ok = ReadFile(m_pipe, m_input_buf.data() + m_input_buf_used,
static_cast<DWORD>(m_input_buf.size() - m_input_buf_used), &read_size, &m_olr);
if (ok) {
// Synchronous completion. The kernel may also have signaled m_io_event on its own; if so,
// the next WFMO will wake on it but find `!m_read_pending` and just fall through to
// re-entering start_read(). Either way we must wake the loop ourselves so
// that start_read() runs again to drain any further data.
m_input_buf_used += read_size;
if (!handle_input()) {
return false;
}
SetEvent(m_wake_event);
return true;
}
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING) {
m_read_pending = true;
return true;
}
if (err == ERROR_BROKEN_PIPE || err == ERROR_PIPE_NOT_CONNECTED || err == ERROR_NO_DATA) {
infolog(m_logger, "ReadFile: peer disconnected ({}: {})", err, ag::sys::strerror(err));
return false;
}
warnlog(m_logger, "ReadFile: {} ({})", err, ag::sys::strerror(err));
return false;
}
bool PipeEndpoint::complete_read() {
DWORD read_size = 0;
if (!GetOverlappedResult(m_pipe, &m_olr, &read_size, FALSE)) {
DWORD err = GetLastError();
if (err == ERROR_BROKEN_PIPE || err == ERROR_PIPE_NOT_CONNECTED || err == ERROR_OPERATION_ABORTED) {
infolog(m_logger, "GetOverlappedResult(read): peer disconnected ({}: {})", err, ag::sys::strerror(err));
return false;
}
warnlog(m_logger, "GetOverlappedResult(read): {} ({})", err, ag::sys::strerror(err));
return false;
}
ResetEvent(m_io_event);
m_read_pending = false;
if (read_size == 0) {
infolog(m_logger, "ReadFile: EOF, peer disconnected");
return false;
}
m_input_buf_used += read_size;
return handle_input();
}
bool PipeEndpoint::handle_input() {
for (;;) {
ag::wire_utils::Reader r{{m_input_buf.data(), m_input_buf_used}};
auto what = r.get_u32();
auto size = r.get_u32();
if (!what.has_value() || !size.has_value()) {
return true; // Need more bytes for the header.
}
if (*size > MAX_MESSAGE_SIZE) {
warnlog(m_logger, "incoming message size {} exceeds MAX_MESSAGE_SIZE ({}); dropping connection", *size,
MAX_MESSAGE_SIZE);
return false;
}
auto data = r.get_bytes(*size);
if (!data.has_value()) {
return true; // Need more bytes for the payload.
}
m_handler(static_cast<VpnEasyServiceMessageType>(*what), *data);
ag::Uint8View remaining = r.get_buffer();
std::memmove(m_input_buf.data(), remaining.data(), remaining.size());
m_input_buf_used = remaining.size();
}
}
bool PipeEndpoint::pump_writes() {
while (!m_write_pending) {
if (!m_inflight_write.has_value()) {
std::scoped_lock l{m_pending_writes_lock};
if (m_pending_writes.empty()) {
return true;
}
m_inflight_write.emplace(std::move(m_pending_writes.front()));
m_pending_writes.pop_front();
}
PendingWrite &w = *m_inflight_write;
DWORD written = 0;
BOOL ok = WriteFile(
m_pipe, w.data.data() + w.written, static_cast<DWORD>(w.data.size() - w.written), &written, &m_olw);
if (ok) {
// Synchronous completion. The kernel may have signaled m_write_event (the docs are
// inconsistent), so reset it here to avoid a spurious wake on the next WFMO.
// For async (ERROR_IO_PENDING), the kernel resets the event itself when queueing.
ResetEvent(m_write_event);
w.written += written;
if (w.written == w.data.size()) {
m_inflight_write.reset();
}
continue;
}
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING) {
m_write_pending = true;
return true;
}
if (err == ERROR_BROKEN_PIPE || err == ERROR_PIPE_NOT_CONNECTED || err == ERROR_NO_DATA) {
infolog(m_logger, "WriteFile: peer disconnected ({}: {})", err, ag::sys::strerror(err));
return false;
}
warnlog(m_logger, "WriteFile: {} ({})", err, ag::sys::strerror(err));
return false;
}
return true;
}
bool PipeEndpoint::complete_write() {
DWORD written = 0;
if (!GetOverlappedResult(m_pipe, &m_olw, &written, FALSE)) {
DWORD err = GetLastError();
if (err == ERROR_BROKEN_PIPE || err == ERROR_PIPE_NOT_CONNECTED || err == ERROR_OPERATION_ABORTED) {
infolog(m_logger, "GetOverlappedResult(write): peer disconnected ({}: {})", err, ag::sys::strerror(err));
return false;
}
warnlog(m_logger, "GetOverlappedResult(write): {} ({})", err, ag::sys::strerror(err));
return false;
}
// Consume the kernel-set completion signal so WFMO doesn't keep firing on m_write_event.
ResetEvent(m_write_event);
m_write_pending = false;
if (m_inflight_write.has_value()) {
PendingWrite &w = *m_inflight_write;
w.written += written;
if (w.written == w.data.size()) {
m_inflight_write.reset();
}
}
return true;
}
void PipeEndpoint::disconnect_and_reset() {
// Mark disconnected BEFORE taking the lock, so that any send() that subsequently acquires the
// lock observes `false` and bails out. Any send() already holding (or already past) the lock
// happens-before our lock acquisition below, so its push will be cleared by the clear() call.
m_connected.store(false, std::memory_order_relaxed);
{
std::scoped_lock l{m_pending_writes_lock};
m_pending_writes.clear();
}
if (m_pipe != INVALID_HANDLE_VALUE) {
CancelIoEx(m_pipe, nullptr);
if (m_read_pending) {
DWORD ignored = 0;
GetOverlappedResult(m_pipe, &m_olr, &ignored, TRUE);
}
if (m_write_pending) {
DWORD ignored = 0;
GetOverlappedResult(m_pipe, &m_olw, &ignored, TRUE);
}
}
// Safe to drop the in-flight buffer now: the kernel has acknowledged the cancel.
m_inflight_write.reset();
teardown_pipe();
// Both event handles may have been left signaled by GetOverlappedResult(TRUE) above.
ResetEvent(m_io_event);
ResetEvent(m_write_event);
m_read_pending = false;
m_write_pending = false;
m_input_buf_used = 0;
}
// ---------------------------------------------------------------------------
// PipeServer
// ---------------------------------------------------------------------------
SecurityDescriptorPtr PipeServer::for_authenticated_users() {
// SDDL DACL:
// (A;;GA;;;SY) - SYSTEM full
// (A;;GA;;;BA) - BUILTIN\Administrators full
// (A;;GRGW;;;AU) - NT AUTHORITY\Authenticated Users read+write
static constexpr wchar_t SDDL[] = L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)";
PSECURITY_DESCRIPTOR sd = nullptr;
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(SDDL, SDDL_REVISION_1, &sd, nullptr)) {
DWORD err = GetLastError();
errlog(g_server_logger, "ConvertStringSecurityDescriptorToSecurityDescriptorW: {} ({})", err,
ag::sys::strerror(err));
return {};
}
return SecurityDescriptorPtr{static_cast<SECURITY_DESCRIPTOR *>(sd)};
}
PipeServer::PipeServer(
const wchar_t *pipe_name, HANDLE stop_event, Handler handler, SECURITY_DESCRIPTOR *security_descriptor)
: PipeEndpoint{stop_event, std::move(handler), g_server_logger} {
m_pipe = create_pipe(pipe_name, security_descriptor);
}
PipeServer::~PipeServer() {
if (m_pipe != INVALID_HANDLE_VALUE) {
cancel_pending_io();
DisconnectNamedPipe(m_pipe);
CloseHandle(m_pipe);
m_pipe = INVALID_HANDLE_VALUE;
}
}
HANDLE PipeServer::create_pipe(const wchar_t *pipe_name, SECURITY_DESCRIPTOR *security_descriptor) {
SECURITY_ATTRIBUTES sa{};
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = security_descriptor;
sa.bInheritHandle = FALSE;
HANDLE h = CreateNamedPipeW(pipe_name, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
1, // single instance
PIPE_BUFFER_SIZE, PIPE_BUFFER_SIZE, 0, security_descriptor != nullptr ? &sa : nullptr);
if (h == INVALID_HANDLE_VALUE) {
errlog(g_server_logger, "CreateNamedPipeW: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
}
return h;
}
bool PipeServer::start_connect() {
prepare_for_connect();
if (m_pipe == INVALID_HANDLE_VALUE) {
// create_pipe() failed in the constructor.
return false;
}
if (ConnectNamedPipe(m_pipe, &m_olr)) {
// Synchronous success (very rare for overlapped pipes). The OVERLAPPED was not really
// used by the kernel in this case, so do not call finalize_connect (which would call
// GetOverlappedResult on it). Mark connected directly and kick the loop.
ResetEvent(m_io_event); // Defensive: kernel may have signaled on sync completion.
m_connected.store(true, std::memory_order_relaxed);
SetEvent(m_wake_event);
infolog(m_logger, "client connected (sync)");
return true;
}
DWORD err = GetLastError();
if (err == ERROR_PIPE_CONNECTED) {
// A client connected between CreateNamedPipe and ConnectNamedPipe. No overlapped op was
// submitted; mark connected directly.
m_connected.store(true, std::memory_order_relaxed);
SetEvent(m_wake_event);
infolog(m_logger, "client connected (already connected)");
return true;
}
if (err == ERROR_IO_PENDING) {
return true;
}
errlog(m_logger, "ConnectNamedPipe: {} ({})", err, ag::sys::strerror(err));
return false;
}
bool PipeServer::finalize_connect() {
DWORD transferred = 0;
if (!GetOverlappedResult(m_pipe, &m_olr, &transferred, FALSE)) {
DWORD err = GetLastError();
warnlog(m_logger, "GetOverlappedResult(connect): {} ({})", err, ag::sys::strerror(err));
return false;
}
ResetEvent(m_io_event);
m_connected.store(true, std::memory_order_relaxed);
infolog(m_logger, "client connected");
return true;
}
void PipeServer::teardown_pipe() {
// Server reuses the same pipe instance across reconnects: just disconnect the current client.
if (m_pipe != INVALID_HANDLE_VALUE) {
DisconnectNamedPipe(m_pipe);
}
}
// ---------------------------------------------------------------------------
// PipeClient
// ---------------------------------------------------------------------------
PipeClient::PipeClient(
const wchar_t *pipe_name, HANDLE stop_event, Handler handler, std::chrono::milliseconds connect_timeout)
: PipeEndpoint{stop_event, std::move(handler), g_client_logger}
, m_pipe_name{pipe_name}
, m_connect_timeout{connect_timeout.count() <= 0 ? DEFAULT_CONNECT_TIMEOUT : connect_timeout}
, m_connected_or_failed_event{CreateEventW(nullptr, TRUE, FALSE, nullptr)} {
}
PipeClient::~PipeClient() {
if (m_pipe != INVALID_HANDLE_VALUE) {
cancel_pending_io();
CloseHandle(m_pipe);
m_pipe = INVALID_HANDLE_VALUE;
}
if (m_connected_or_failed_event != nullptr) {
CloseHandle(m_connected_or_failed_event);
}
}
bool PipeClient::wait_connected() {
if (m_connected_or_failed_event == nullptr) {
return false;
}
HANDLE events[] = {m_connected_or_failed_event, stop_event()};
intmax_t timeout_ms = INFINITE;
if (m_connect_timeout.count() < 0) {
timeout_ms = 0;
} else if (m_connect_timeout.count() < INFINITE) {
timeout_ms = m_connect_timeout.count();
}
DWORD r = WaitForMultipleObjects(
static_cast<DWORD>(std::size(events)), events, FALSE, static_cast<DWORD>(timeout_ms));
if (r != WAIT_OBJECT_0) {
// Stop event won, or timed out, or wait failed.
return false;
}
// The event is signaled both on successful connect and on fatal start failure; the atomic
// disambiguates.
return m_connected.load(std::memory_order_relaxed);
}
bool PipeClient::start_connect() {
prepare_for_connect();
ResetEvent(m_connected_or_failed_event);
// The server is single-instance: when the previous client disconnects there is a brief
// window between the kernel observing the broken pipe and the server having both
// DisconnectNamedPipe()'d the old client AND posted a fresh ConnectNamedPipe() for the new
// one. During that window CreateFileW returns ERROR_PIPE_BUSY (instance still bound to the
// previous client) or ERROR_FILE_NOT_FOUND (no listening instance yet). Retry with a bounded
// total deadline, in short stop-event-interruptible slices.
constexpr auto DEFAULT_SLICE = ag::Millis{10};
// Clamp the slice so it never overshoots the configured connect timeout.
auto slice = std::max(ag::Millis{1}, std::min(DEFAULT_SLICE, m_connect_timeout));
DWORD slice_ms = static_cast<DWORD>(slice.count());
auto deadline = std::chrono::steady_clock::now() + m_connect_timeout;
for (;;) {
// CreateFileW is synchronous; FILE_FLAG_OVERLAPPED affects only subsequent IO on the handle.
m_pipe = CreateFileW(m_pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, nullptr);
if (m_pipe != INVALID_HANDLE_VALUE) {
break;
}
DWORD err = GetLastError();
if (err != ERROR_PIPE_BUSY && err != ERROR_FILE_NOT_FOUND) {
errlog(m_logger, "CreateFileW: {} ({})", err, ag::sys::strerror(err));
SetEvent(m_connected_or_failed_event);
return false;
}
if (std::chrono::steady_clock::now() >= deadline) {
errlog(m_logger, "CreateFileW: timed out waiting for server (last err {}: {})", err,
ag::sys::strerror(err));
SetEvent(m_connected_or_failed_event);
return false;
}
// Interruptible nap before retry.
if (WaitForSingleObject(stop_event(), slice_ms) == WAIT_OBJECT_0) {
SetEvent(m_connected_or_failed_event);
return false;
}
if (err == ERROR_PIPE_BUSY) {
// Wait for an instance to become available; ignore the result and just retry CreateFileW.
// WaitNamedPipeW is uninterruptible.
WaitNamedPipeW(m_pipe_name.c_str(), slice_ms);
}
}
// Defensive: ensure byte-mode read semantics regardless of the server's configuration.
DWORD mode = PIPE_READMODE_BYTE;
if (!SetNamedPipeHandleState(m_pipe, &mode, nullptr, nullptr)) {
DWORD err = GetLastError();
warnlog(m_logger, "SetNamedPipeHandleState: {} ({})", err, ag::sys::strerror(err));
}
m_connected.store(true, std::memory_order_relaxed);
SetEvent(m_connected_or_failed_event);
SetEvent(m_wake_event);
infolog(m_logger, "connected to server");
return true;
}
void PipeClient::teardown_pipe() {
// Client uses a single-shot handle: close it and let the loop exit
// (should_reconnect_on_disconnect() returns false).
if (m_pipe != INVALID_HANDLE_VALUE) {
CloseHandle(m_pipe);
m_pipe = INVALID_HANDLE_VALUE;
}
}
} // namespace ag::vpn_easy
+291
View File
@@ -0,0 +1,291 @@
#pragma once
#include <atomic>
#include <chrono>
#include <functional>
#include <list>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "common/defs.h"
#include "common/logger.h"
#include "vpn/vpn_easy_service.h"
namespace ag::vpn_easy {
namespace detail {
/** Free a security descriptor returned by an SDDL helper. Used as the deleter for `SecurityDescriptorPtr`. */
void free_security_descriptor(SECURITY_DESCRIPTOR *sd);
} // namespace detail
/** Owning pointer for a security descriptor allocated via `LocalAlloc` (e.g. by SDDL helpers). */
using SecurityDescriptorPtr = ag::UniquePtr<SECURITY_DESCRIPTOR, &detail::free_security_descriptor>;
/**
* Asynchronous named-pipe endpoint base class for the VPN easy-service control protocol.
*
* Holds all framing, queueing, overlapped-IO and event-loop machinery shared by both ends of the
* pipe. Subclasses implement only:
* - how a pipe handle is acquired (`start_connect()`),
* - how a posted overlapped connect completion is reaped (`finalize_connect()`),
* - how the pipe handle is torn down on disconnect (`teardown_pipe()`),
* - and the disconnect policy (`should_reconnect_on_disconnect()`).
*
* After construction, the caller drives the IO loop by calling `loop()` on a worker thread; the
* loop returns once the externally-provided `stop_event` becomes signaled, the peer disconnects
* (only for endpoints whose `should_reconnect_on_disconnect()` returns `false`), or a fatal
* error occurs.
*
* `send()` is thread-safe and may be called from any thread, including from inside the receive
* callback. If the endpoint is not currently connected the message is dropped. If the queue
* overflows, the oldest pending messages are dropped.
*/
class PipeEndpoint {
public:
/**
* Callback invoked from `loop()`'s thread for every fully-received message.
* The `data` view is valid only for the duration of the call.
*/
using Handler = std::function<void(VpnEasyServiceMessageType what, ag::Uint8View data)>;
virtual ~PipeEndpoint();
PipeEndpoint(const PipeEndpoint &) = delete;
PipeEndpoint &operator=(const PipeEndpoint &) = delete;
PipeEndpoint(PipeEndpoint &&) = delete;
PipeEndpoint &operator=(PipeEndpoint &&) = delete;
/**
* Run the asynchronous IO loop until `stop_event` is signaled, the peer disconnects (for
* non-reconnecting endpoints), or a fatal error occurs.
* @return `true` if stopped via the stop event or a graceful peer disconnect, `false` on
* fatal error.
*/
bool loop();
/**
* Enqueue a message to be sent to the currently-connected peer. Thread-safe.
* Drop the message if no peer is connected. If the internal queue is full, drop the oldest
* pending messages.
*/
void send(VpnEasyServiceMessageType what, ag::Uint8View data);
protected:
/**
* @param stop_event Externally-owned manual-reset event. When signaled, `loop()` returns `true`.
* Ownership is NOT transferred.
* @param handler Message receive callback. Must be non-null.
* @param logger Logger to use for diagnostic messages.
*/
PipeEndpoint(HANDLE stop_event, Handler handler, ag::Logger &logger);
/**
* Subclass hook: acquire/initiate the pipe connection. On success, either:
* - mark `m_connected` true synchronously and `SetEvent(m_wake_event)`, or
* - leave `m_connected` false and post an overlapped op on `m_olr`/`m_io_event`; the loop
* will then call `finalize_connect()` when `m_io_event` fires.
* Implementations MUST call `prepare_for_connect()` first to clear per-connection state.
* @return `true` on success (sync or pending), `false` on fatal error (`loop()` returns false).
*/
virtual bool start_connect() = 0;
/**
* Subclass hook: reap the completion of an overlapped connect posted by `start_connect()`,
* called when `m_io_event` fires while not yet connected. Default returns `false` (no
* overlapped connect was posted; an `m_io_event` wake here is unexpected).
*/
virtual bool finalize_connect() {
return false;
}
/**
* Subclass hook: tear down the pipe handle after pending IO has been cancelled and drained.
* Called from `disconnect_and_reset()`. Implementations typically call `DisconnectNamedPipe`
* (server -- handle is reused) or `CloseHandle` and reset `m_pipe` to `INVALID_HANDLE_VALUE`
* (client -- handle is single-use).
*/
virtual void teardown_pipe() = 0;
/**
* Subclass hook: report the disconnect policy. Returning `true` (the default) causes the
* loop to invoke `start_connect()` again after each disconnect; returning `false` causes
* `loop()` to return `true` after the first disconnect. `PipeClient` overrides to return
* `false`.
*/
virtual bool should_reconnect_on_disconnect() const {
return true;
}
/**
* Reset per-connection state and event handles to their initial values. Subclasses MUST call
* this at the top of `start_connect()`.
*/
void prepare_for_connect();
/**
* Cancel any pending overlapped IO on `m_pipe` and synchronously wait for the cancellations
* to land. Used by subclass destructors to safely tear down a still-active endpoint.
* Caller must ensure `m_pipe != INVALID_HANDLE_VALUE`.
*/
void cancel_pending_io();
// Pipe handle. Owned by the subclass: the server creates it once in its constructor and
// re-uses it across `DisconnectNamedPipe`; the client creates it in `start_connect()` and
// destroys it in `teardown_pipe()`.
HANDLE m_pipe = INVALID_HANDLE_VALUE;
// Overlapped state, used by both subclass connect logic (`m_olr`) and the shared read/write
// pipeline. Subclasses must not touch these except as documented above.
OVERLAPPED m_olr{}; ///< For overlapped connect (subclass) / `ReadFile` (base).
OVERLAPPED m_olw{}; ///< For `WriteFile` (base only).
HANDLE m_io_event = nullptr; ///< Signaled on overlapped connect or read completion.
HANDLE m_write_event = nullptr; ///< Signaled on write completion.
HANDLE m_wake_event = nullptr; ///< Set by `send()` (and by sync-connect paths) to wake the loop.
// Connection state. Written by the loop thread; read by `send()` (any thread).
std::atomic<bool> m_connected{false};
// Logger. Bound to a static ag::Logger owned by the subclass's translation unit.
ag::Logger &m_logger;
// Externally-owned stop event. Exposed to subclasses so that long-running connect retries
// (e.g. PipeClient::start_connect) can be interrupted promptly when the loop is asked to stop.
HANDLE stop_event() const {
return m_stop_event;
}
private:
static constexpr size_t MAX_PENDING_WRITES = 100;
// Maximum payload size of a single message. Messages larger than this are rejected and the
// connection is dropped (protocol violation / DoS protection).
static constexpr size_t MAX_MESSAGE_SIZE = 16 * 1024;
// Receive buffer size: large enough to hold one full message plus its 8-byte header.
static constexpr size_t INPUT_BUF_SIZE = MAX_MESSAGE_SIZE + 2 * sizeof(uint32_t);
struct PendingWrite {
std::vector<uint8_t> data;
size_t written;
};
HANDLE m_stop_event = nullptr;
Handler m_handler;
bool m_read_pending = false;
bool m_write_pending = false;
std::vector<uint8_t> m_input_buf;
size_t m_input_buf_used = 0;
std::mutex m_pending_writes_lock;
std::list<PendingWrite> m_pending_writes; // Guarded by m_pending_writes_lock.
// Owned exclusively by the loop thread: the message currently being written (possibly with an
// overlapped WriteFile in flight). Moved here from m_pending_writes under the lock and kept
// alive until the write fully completes, so that send() can never free the in-flight buffer.
std::optional<PendingWrite> m_inflight_write;
static std::vector<uint8_t> compose_message(VpnEasyServiceMessageType what, ag::Uint8View data);
// Returns nullopt to continue the loop; otherwise the value `loop()` should return.
std::optional<bool> handle_disconnect();
bool start_read();
bool complete_read();
bool handle_input();
bool pump_writes();
bool complete_write();
void disconnect_and_reset();
};
/**
* Server endpoint: owns a single byte-stream named pipe instance, accepts one client at a time,
* and transparently reconnects (waits for a new client) when the current client disconnects.
*/
class PipeServer : public PipeEndpoint {
public:
/**
* Create a security descriptor that grants GENERIC_READ | GENERIC_WRITE to
* NT AUTHORITY\Authenticated Users, and full control to SYSTEM and BUILTIN\Administrators.
* Suitable for a service-side IPC named pipe that must be reachable from any locally
* authenticated user session. Returns null on failure.
*/
static SecurityDescriptorPtr for_authenticated_users();
/**
* @param pipe_name Full named-pipe name (e.g. `\\.\pipe\my_pipe`).
* @param stop_event See `PipeEndpoint`.
* @param handler See `PipeEndpoint`.
* @param security_descriptor Optional security descriptor for the pipe. If null (the default),
* the system default DACL is used. The pointer is consumed
* synchronously by the constructor; the caller may destroy the
* descriptor immediately after construction returns.
*/
PipeServer(const wchar_t *pipe_name, HANDLE stop_event, Handler handler,
SECURITY_DESCRIPTOR *security_descriptor = nullptr);
~PipeServer() override;
protected:
bool start_connect() override;
bool finalize_connect() override;
void teardown_pipe() override;
// Default `Reconnect` policy is exactly what the server wants.
private:
static constexpr DWORD PIPE_BUFFER_SIZE = 64 * 1024;
static HANDLE create_pipe(const wchar_t *pipe_name, SECURITY_DESCRIPTOR *security_descriptor);
};
/**
* Client endpoint: opens a connection to an existing named-pipe server. The IO loop exits on
* peer disconnect (returning `true` from `loop()`); the caller should construct a new `PipeClient` to reconnect.
*/
class PipeClient : public PipeEndpoint {
public:
/** Default total timeout used by `start_connect()` when the constructor is passed `0`. */
static constexpr std::chrono::milliseconds DEFAULT_CONNECT_TIMEOUT{500};
/**
* @param pipe_name Full named-pipe name (e.g. `\\.\pipe\my_pipe`).
* @param stop_event See `PipeEndpoint`.
* @param handler See `PipeEndpoint`.
* @param connect_timeout Maximum total time `start_connect()` will spend retrying
* `CreateFileW` while the server is briefly unavailable
* (e.g. mid-reconnect of a previous client). A value of `0` selects
* `DEFAULT_CONNECT_TIMEOUT`. Negative values are treated as `0`.
*/
PipeClient(const wchar_t *pipe_name, HANDLE stop_event, Handler handler,
std::chrono::milliseconds connect_timeout = std::chrono::milliseconds{0});
~PipeClient() override;
/**
* Block until the client has successfully connected to the server (i.e. `start_connect()`,
* driven by `loop()` on another thread, has succeeded), or the externally-supplied stop event
* is signaled -- whichever happens first. Thread-safe; may be called
* from any thread, including before `loop()` has started.
* @return `true` if the connection is established within the timeout, `false` otherwise
* (timeout, stop event signaled, or fatal connect failure).
*/
bool wait_connected();
protected:
bool start_connect() override;
void teardown_pipe() override;
bool should_reconnect_on_disconnect() const override {
return false;
}
private:
std::wstring m_pipe_name;
std::chrono::milliseconds m_connect_timeout;
// Manual-reset event signaled by start_connect() on success and by loop() on fatal start
// failure. Used by wait_connected() so callers can synchronize without polling. Reset at the
// top of every start_connect() attempt so that a fresh PipeClient instance starts clean.
HANDLE m_connected_or_failed_event = nullptr;
};
} // namespace ag::vpn_easy
+159
View File
@@ -0,0 +1,159 @@
#include "vpn/vpn_easy_service.h"
#include "vpn/vpn_easy.h"
#include <cstdio>
#include <functional>
#include <optional>
#include <string>
#include "common/defs.h"
#include "common/logger.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "common/system_error.h"
#include "vpn/trusttunnel/connection_info.h"
#include "vpn/vpn.h"
#include "vpn_easy_pipe.h"
using ag::vpn_easy::PipeServer;
static ag::Logger g_logger{"VPN_EASY_SERVICE"};
static std::wstring g_pipe_name;
static SERVICE_STATUS_HANDLE g_status_handle;
static HANDLE g_shutdown_event;
static vpn_easy_t *g_vpn;
/// Send a `VPN_EASY_SVC_MSG_STATE_CHANGED` message with the given state value.
static void send_state(PipeServer &server, int32_t state) {
uint32_t net_state = htonl(static_cast<uint32_t>(state));
server.send(VPN_EASY_SVC_MSG_STATE_CHANGED, {reinterpret_cast<const uint8_t *>(&net_state), sizeof(net_state)});
}
/// Handle an incoming pipe message from a client.
static void pipe_handler(PipeServer &server, VpnEasyServiceMessageType what, ag::Uint8View data) {
switch (what) {
case VPN_EASY_SVC_MSG_START: {
if (g_vpn != nullptr) {
infolog(g_logger, "VPN already running, stopping before restart");
vpn_easy_stop_ex(g_vpn);
g_vpn = nullptr;
}
std::string toml_config(reinterpret_cast<const char *>(data.data()), data.size());
infolog(g_logger, "Starting VPN client");
g_vpn = vpn_easy_start_ex(
toml_config.c_str(),
[](void *arg, int state) {
send_state(*static_cast<PipeServer *>(arg), state);
},
&server,
[](void *arg, void *connection_info) {
std::string json =
ag::ConnectionInfo::to_json(static_cast<ag::VpnConnectionInfoEvent *>(connection_info));
static_cast<PipeServer *>(arg)->send(VPN_EASY_SVC_MSG_CONNECTION_INFO,
{reinterpret_cast<const uint8_t *>(json.data()), json.size()});
},
&server);
if (g_vpn == nullptr) {
warnlog(g_logger, "vpn_easy_start_ex failed");
send_state(server, ag::VPN_SS_DISCONNECTED);
}
break;
}
case VPN_EASY_SVC_MSG_STOP: {
if (g_vpn == nullptr) {
infolog(g_logger, "VPN already stopped, ignoring STOP");
return;
}
infolog(g_logger, "Stopping VPN client");
vpn_easy_stop_ex(g_vpn);
g_vpn = nullptr;
break;
}
case VPN_EASY_SVC_MSG_STATE_CHANGED:
case VPN_EASY_SVC_MSG_CONNECTION_INFO:
warnlog(g_logger, "Ignoring server-to-client message type: {}", static_cast<int>(what));
break;
default:
warnlog(g_logger, "Unknown message type: {}", static_cast<int>(what));
break;
}
}
static void service_set_status(DWORD current_state) {
SERVICE_STATUS status{
.dwServiceType = SERVICE_WIN32_OWN_PROCESS,
.dwCurrentState = current_state,
.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN,
};
SetServiceStatus(g_status_handle, &status);
}
static void WINAPI service_ctrl_handler(DWORD control) {
switch (control) {
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
SetEvent(g_shutdown_event);
break;
default:
break;
}
}
static void WINAPI service_main(DWORD /*argc*/, LPWSTR * /*argv*/) {
g_status_handle = RegisterServiceCtrlHandlerW(L"", service_ctrl_handler);
g_shutdown_event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
service_set_status(SERVICE_START_PENDING);
PipeServer server{g_pipe_name.c_str(), g_shutdown_event,
[&server](VpnEasyServiceMessageType what, ag::Uint8View data) {
pipe_handler(server, what, data);
},
PipeServer::for_authenticated_users().get()};
service_set_status(SERVICE_RUNNING);
server.loop();
if (g_vpn != nullptr) {
infolog(g_logger, "Shutting down: stopping VPN client");
vpn_easy_stop_ex(g_vpn);
g_vpn = nullptr;
}
service_set_status(SERVICE_STOPPED);
}
int wmain(int argc, wchar_t **argv) {
if (argc != 3) {
return 1;
}
ag::UniquePtr<FILE, &fclose> logfile{_wfsopen(argv[1], L"w", _SH_DENYWR)};
if (logfile) {
setvbuf(logfile.get(), nullptr, _IONBF, 0);
ag::Logger::set_callback(ag::Logger::LogToFile{logfile.get()});
}
ag::Logger::set_log_level(ag::LOG_LEVEL_INFO);
g_pipe_name = argv[2];
wchar_t svc_name[] = L"";
SERVICE_TABLE_ENTRYW start_table[] = {
{svc_name, service_main},
{nullptr, nullptr},
};
#ifndef AG_DEBUGGING_VPN_EASY_SERVICE
if (!StartServiceCtrlDispatcherW(start_table)) {
errlog(g_logger, "StartServiceCtrlDispatcherW: {} ({})", GetLastError(), ag::sys::strerror(GetLastError()));
return 3;
}
#else
service_main(0, nullptr);
#endif
return 0;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,166 @@
#include "vpn/vpn.h"
#include "vpn/vpn_easy.h"
#include "vpn/vpn_easy_service.h"
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <fmt/format.h>
#include <magic_enum/magic_enum.hpp>
#include "common/logger.h"
static constexpr const wchar_t *SERVICE_NAME = L"vpn_easy_service";
static constexpr const wchar_t *PIPE_NAME = L"\\\\.\\pipe\\TestPipeName";
static void state_changed_cb(void *, int state) {
fmt::println(stderr, "VPN state changed: ({}) {}", state,
magic_enum::enum_name(static_cast<ag::VpnSessionState>(state)));
}
/// Read config.toml into a string. Return empty string on failure.
static std::string read_config() {
std::ifstream in("config.toml");
std::stringstream buf;
buf << in.rdbuf();
if (in.fail()) {
fmt::println(stderr, "Failed to read config.toml");
return {};
}
return buf.str();
}
/// Install the service. If it already exists, uninstall first and retry.
static int32_t install_service() {
auto image = absolute(std::filesystem::path(".") / "vpn_easy_service.exe").wstring();
auto logfile = absolute(std::filesystem::path(".") / "vpn_easy_service.log").wstring();
int32_t ret = vpn_easy_service_install(
image.c_str(), logfile.c_str(), PIPE_NAME, SERVICE_NAME, L"VPN easy service", L"Test description");
if (ret == VPN_EASY_SVC_ERR_SERVICE_EXISTS) {
fmt::println(stderr, "Service already exists, uninstalling first...");
vpn_easy_service_uninstall(SERVICE_NAME);
ret = vpn_easy_service_install(
image.c_str(), logfile.c_str(), PIPE_NAME, SERVICE_NAME, L"VPN easy service", L"Test description");
}
return ret;
}
/// Test install and uninstall only.
static int test_install_uninstall() {
fmt::println(stderr, "=== test_install_uninstall ===");
fmt::println(stderr, "Installing service...");
int32_t ret = install_service();
if (ret) {
fmt::println(stderr, "vpn_easy_service_install: {}", ret);
return -1;
}
fmt::println(stderr, "Type 's' to stop");
while (getchar() != 's') {
}
ret = vpn_easy_service_uninstall(SERVICE_NAME);
if (ret) {
fmt::println(stderr, "vpn_easy_service_uninstall: {}", ret);
return -1;
}
return 0;
}
/// Test start and stop via the pipe client (requires service to be installed already).
static int test_start_stop() {
fmt::println(stderr, "=== test_start_stop ===");
std::string config = read_config();
if (config.empty()) {
return -1;
}
fmt::println(stderr, "Starting service...");
int32_t ret = vpn_easy_service_start(SERVICE_NAME, PIPE_NAME, config.c_str(), state_changed_cb, nullptr);
if (ret) {
fmt::println(stderr, "vpn_easy_service_start: {}", ret);
return -1;
}
fmt::println(stderr, "Service started. Type 's' to stop");
while (getchar() != 's') {
}
fmt::println(stderr, "Stopping service...");
ret = vpn_easy_service_stop(SERVICE_NAME, PIPE_NAME);
if (ret) {
fmt::println(stderr, "vpn_easy_service_stop: {}", ret);
return -1;
}
fmt::println(stderr, "Service stopped.");
return 0;
}
/// Test full lifecycle: install, start, stop, uninstall.
static int test_full_lifecycle() {
fmt::println(stderr, "=== test_full_lifecycle ===");
std::string config = read_config();
if (config.empty()) {
return -1;
}
fmt::println(stderr, "Installing service...");
int32_t ret = install_service();
if (ret) {
fmt::println(stderr, "vpn_easy_service_install: {}", ret);
return -1;
}
fmt::println(stderr, "Starting VPN via service...");
ret = vpn_easy_service_start(SERVICE_NAME, PIPE_NAME, config.c_str(), state_changed_cb, nullptr);
if (ret) {
fmt::println(stderr, "vpn_easy_service_start: {}", ret);
vpn_easy_service_uninstall(SERVICE_NAME);
return -1;
}
fmt::println(stderr, "VPN started. Type 's' to stop");
while (getchar() != 's') {
}
fmt::println(stderr, "Stopping VPN via service...");
ret = vpn_easy_service_stop(SERVICE_NAME, PIPE_NAME);
if (ret) {
fmt::println(stderr, "vpn_easy_service_stop: {}", ret);
}
fmt::println(stderr, "Uninstalling service...");
ret = vpn_easy_service_uninstall(SERVICE_NAME);
if (ret) {
fmt::println(stderr, "vpn_easy_service_uninstall: {}", ret);
return -1;
}
fmt::println(stderr, "Done.");
return 0;
}
int main(int argc, char **argv) {
ag::Logger::set_log_level(ag::LOG_LEVEL_DEBUG);
const char *test = (argc > 1) ? argv[1] : "full";
if (strcmp(test, "install") == 0) {
return test_install_uninstall();
}
if (strcmp(test, "startstop") == 0) {
return test_start_stop();
}
if (strcmp(test, "full") == 0) {
return test_full_lifecycle();
}
fmt::println(stderr, "Usage: {} [install|startstop|full]", argv[0]);
return 1;
}
+7 -4
View File
@@ -1,11 +1,14 @@
#include "vpn/vpn.h"
#include "vpn/vpn_easy.h"
#include <cstdio>
#include <fstream>
#include <sstream>
static void state_changed_cb(void *, const char *new_state_description) {
fprintf(stderr, "VPN state changed: %s\n", new_state_description);
#include <magic_enum/magic_enum.hpp>
static void state_changed_cb(void *, int state) {
fprintf(stderr, "VPN state changed: (%d) %s\n", state, magic_enum::enum_name((ag::VpnSessionState) state).data());
}
int main() {
@@ -18,12 +21,12 @@ int main() {
}
in.close();
vpn_easy_t *vpn = vpn_easy_start(config.str().c_str(), state_changed_cb, nullptr);
vpn_easy_t *vpn = vpn_easy_start_ex(config.str().c_str(), state_changed_cb, nullptr, nullptr, nullptr);
fprintf(stderr, "Type 's' to stop");
while (getchar() != 's') {
}
vpn_easy_stop(vpn);
vpn_easy_stop_ex(vpn);
return 0;
}