Files
voice-cat/server/src/server.cpp

241 lines
10 KiB
C++
Raw Normal View History

#include "server.h"
#include <cstdio>
feat(macos): validate dev + apple-dev presets on macOS, fix 3 cross-platform bugs macOS port groundwork — core, server, tools, and tests now build and run on macOS 26.5 / Apple Silicon. ctest --preset dev green 21/21 (2 consecutive runs). apple-dev produces valid arm64 libvoicecat.a + XCFramework for the Swift Package. Three real cross-platform bugs found and fixed (all latent on Windows/Linux): 1. test_m2_voice.cpp POSIX branch missing <netdb.h> — Linux glibc transitively includes it, macOS doesn't. Would fail on any strict POSIX system. 2. SIGPIPE killing processes on macOS — writing to a closed TCP socket raises SIGPIPE by default (doesn't exist on Windows, benign on Linux). Fixed by ignoring SIGPIPE in both core client init and server startup (POSIX-only, #ifndef _WIN32). Production fix, not just tests. 3. Use-after-free of Asio's kqueue reactor on server shutdown — the deterministic test_tofu_flow segfault. TcpServerConn's tls_read_loop runs on a blocking-I/O thread; when Server::run() returned, io_context was destroyed while those threads were still running. On macOS kqueue the reactor pointer is null'd immediately -> segfault in socket.close(). Latent on Windows IOCP and Linux epoll. Fix: TcpAcceptor now tracks connections; new shutdown() closes all + joins threads before io is destroyed; Server::stop() now closes acceptor + media_relay too (was just io.stop()). Verified: dev + apple-dev presets build green, 21/21 tests pass, server starts + two vccli text chat over TLS (M1 on Mac), vccli --voice starts MIC stream via CoreAudio (M2 protocol-level), vccli --list-devices enumerates CoreAudio devices, xcodebuild -create-xcframework produces valid VoiceCatCore.xcframework. No ABI or proto changes. Docs updated: building.md, clients/apple/README.md, PROGRESS.md, CLAUDE.md status line.
2026-06-18 13:24:42 +02:00
#include <csignal>
2026-06-15 23:48:44 +02:00
#ifdef VOICECAT_HAS_NET
#define ASIO_STANDALONE 1
#include <asio.hpp>
#include <asio/signal_set.hpp>
fix(net): broadcast LEFT on disconnect, add keepalive/reaper, cap PLC hiss Three reported bugs traced to one root cause plus two missing designed features: 1. Stale users + eternal PLC hiss (root cause): ConnSession::close() silently erased dropped users without broadcasting UserEvent::LEFT, so peers never learned the user left and their audio engines never called remove_stream — Opus PLC synthesized comfort noise forever. Fix: broadcast_left() helper + close() broadcasts LEFT before erasing. 2. PLC cap (defense-in-depth): on_playback now caps pure PLC at ~2s, then emits digital silence so a stale stream can never hiss forever even if remove_stream is skipped. Resets automatically on fresh packets. 3. No timeout / no ping: client never sent Ping, server had no last_seen / reaper, so half-open connections (NAT timeout, wifi loss, sleep) left ghost users forever. Fix: client Ping every 15s with RTT measurement, ConnSession::last_seen bumped on every inbound TCP/UDP frame, steady_timer reaper sweeps every 15s and drops sessions older than 45s (configurable via server::Config). 4. UDP KEEPALIVE: client sends plaintext kFrameKeepalive every 5s; server bumps last_seen + echoes back. Keeps NAT bindings alive and lets media activity defer the reaper independently of TCP. 5. Graceful client disconnect: vc_disconnect() sends Disconnect{code=0} via a flag-based io-thread exit (no double-close race); server handles client-sent Disconnect with immediate close() + LEFT broadcast. 3 new tests: disconnect_left, plc_cap, reaper_timeout. 21/21 ctest green. Docs: protocol.md §6/§7, voice.md §6, architecture.md §5, PROGRESS.md.
2026-06-18 01:18:33 +02:00
#include <chrono>
#include <functional>
2026-06-15 23:48:44 +02:00
#include <memory>
fix(net): broadcast LEFT on disconnect, add keepalive/reaper, cap PLC hiss Three reported bugs traced to one root cause plus two missing designed features: 1. Stale users + eternal PLC hiss (root cause): ConnSession::close() silently erased dropped users without broadcasting UserEvent::LEFT, so peers never learned the user left and their audio engines never called remove_stream — Opus PLC synthesized comfort noise forever. Fix: broadcast_left() helper + close() broadcasts LEFT before erasing. 2. PLC cap (defense-in-depth): on_playback now caps pure PLC at ~2s, then emits digital silence so a stale stream can never hiss forever even if remove_stream is skipped. Resets automatically on fresh packets. 3. No timeout / no ping: client never sent Ping, server had no last_seen / reaper, so half-open connections (NAT timeout, wifi loss, sleep) left ghost users forever. Fix: client Ping every 15s with RTT measurement, ConnSession::last_seen bumped on every inbound TCP/UDP frame, steady_timer reaper sweeps every 15s and drops sessions older than 45s (configurable via server::Config). 4. UDP KEEPALIVE: client sends plaintext kFrameKeepalive every 5s; server bumps last_seen + echoes back. Keeps NAT bindings alive and lets media activity defer the reaper independently of TCP. 5. Graceful client disconnect: vc_disconnect() sends Disconnect{code=0} via a flag-based io-thread exit (no double-close race); server handles client-sent Disconnect with immediate close() + LEFT broadcast. 3 new tests: disconnect_left, plc_cap, reaper_timeout. 21/21 ctest green. Docs: protocol.md §6/§7, voice.md §6, architecture.md §5, PROGRESS.md.
2026-06-18 01:18:33 +02:00
#include <vector>
2026-06-15 23:48:44 +02:00
#include "conn_session.h"
#include "core/worker_pool.h"
#include "crypto/crypto.h"
#include "db.h"
#include "identity.h"
#include "media_relay.h"
2026-06-15 23:48:44 +02:00
#include "net/transport.h"
#include "session_registry.h"
namespace voicecat::server {
int Server::run() {
2026-06-15 23:48:44 +02:00
// ── Identity + cert ──────────────────────────────────────────────────────
ServerIdentityManager id_mgr;
std::string error;
if (!id_mgr.init(cfg_.data_dir, cfg_.server_name, error)) {
std::fprintf(stderr, "[server] identity init failed: %s\n", error.c_str());
return 1;
}
// ── Database + bootstrap admin ───────────────────────────────────────────
auto db = std::make_shared<Database>(cfg_.data_dir + "/voicecat.db");
if (!db->open(error)) {
std::fprintf(stderr, "[server] db open failed: %s\n", error.c_str());
return 1;
}
if (db->is_empty()) {
std::string pw = Database::generate_password(20);
auto acc = db->create_account("admin", pw, true, error);
if (!acc) {
std::fprintf(stderr, "[server] failed to create admin account: %s\n", error.c_str());
return 1;
}
std::printf("\n");
std::printf("┌─────────────────────────────────────────────────────────┐\n");
std::printf("│ First run — admin account created │\n");
std::printf("│ username : %-44s│\n", "admin");
std::printf("│ password : %-44s│\n", pw.c_str());
std::printf("│ Change with: voicecat-admin account reset admin │\n");
std::printf("└─────────────────────────────────────────────────────────┘\n");
std::printf("\n");
}
// ── Session registry ─────────────────────────────────────────────────────
auto registry = std::make_shared<SessionRegistry>(db);
registry->load_channels();
2026-06-15 23:48:44 +02:00
// ── Worker pool ──────────────────────────────────────────────────────────
auto workers = std::make_shared<WorkerPool>(3);
// ── Asio io_context ──────────────────────────────────────────────────────
asio::io_context io;
// ── UDP media relay (M2) ─────────────────────────────────────────────────
auto media_relay = std::make_shared<MediaRelay>(io, registry);
if (!media_relay->bind(cfg_.media_port)) {
std::fprintf(stderr, "[server] failed to bind UDP media port %u\n", cfg_.media_port);
return 1;
}
media_relay->start();
uint16_t media_bound = media_relay->media_port();
if (cfg_.on_media_ready) cfg_.on_media_ready(media_bound);
2026-06-15 23:48:44 +02:00
// Capture all locals by reference for the factory lambda (io lifetime is > factory)
voicecat::net::TcpAcceptor acceptor(
io, cfg_.bind_port,
[&](asio::ip::tcp::socket sock) -> std::shared_ptr<voicecat::net::TcpServerConn> {
auto session = std::make_shared<ConnSession>(
db, registry, workers,
id_mgr.identity().fingerprint,
cfg_.allow_guests,
media_bound);
2026-06-15 23:48:44 +02:00
// Use shared_ptr (not weak_ptr) so TcpServerConn keeps ConnSession alive.
// cycle is broken by weak_tcp in the send/close fns below.
voicecat::net::TcpChannelCallbacks cbs;
cbs.on_frame = [session](std::vector<uint8_t> f) {
session->on_frame(std::move(f));
};
cbs.on_disconnected = [session] {
session->on_disconnect();
};
cbs.on_error = [session](std::error_code) {
session->on_disconnect();
};
// Derive media keys right after TLS handshake (server is not the client).
cbs.on_tls_ready = [session](voicecat::crypto::TlsContext& tls) {
auto send = voicecat::crypto::SodiumMediaCrypto::derive_send(tls, false);
auto recv = voicecat::crypto::SodiumMediaCrypto::derive_recv(tls, false);
if (send && recv) session->set_media_crypto(std::move(send), std::move(recv));
};
2026-06-15 23:48:44 +02:00
// Create a TLS context for this connection (server role).
auto tls = std::make_unique<voicecat::crypto::TlsContext>(
voicecat::crypto::TlsContext::Role::Server,
&id_mgr.cert());
auto tcp = std::make_shared<voicecat::net::TcpServerConn>(
std::move(sock), std::move(cbs), std::move(tls));
// Give session its send/close capability (weak_ptr avoids cycle)
std::weak_ptr<voicecat::net::TcpServerConn> weak_tcp = tcp;
session->set_io(
[weak_tcp](std::vector<uint8_t> frame) {
if (auto t = weak_tcp.lock()) t->send_frame(std::move(frame));
},
[weak_tcp] {
if (auto t = weak_tcp.lock()) t->close();
});
uint64_t sid = registry->register_session(session);
session->set_session_id(sid);
session->begin();
// NOTE: do NOT call tcp->start() here — TcpAcceptor::do_accept() calls it.
return tcp;
});
acceptor.start();
// Arm programmatic stop (for tests and embedders).
{
std::lock_guard lk(stop_mutex_);
feat(macos): validate dev + apple-dev presets on macOS, fix 3 cross-platform bugs macOS port groundwork — core, server, tools, and tests now build and run on macOS 26.5 / Apple Silicon. ctest --preset dev green 21/21 (2 consecutive runs). apple-dev produces valid arm64 libvoicecat.a + XCFramework for the Swift Package. Three real cross-platform bugs found and fixed (all latent on Windows/Linux): 1. test_m2_voice.cpp POSIX branch missing <netdb.h> — Linux glibc transitively includes it, macOS doesn't. Would fail on any strict POSIX system. 2. SIGPIPE killing processes on macOS — writing to a closed TCP socket raises SIGPIPE by default (doesn't exist on Windows, benign on Linux). Fixed by ignoring SIGPIPE in both core client init and server startup (POSIX-only, #ifndef _WIN32). Production fix, not just tests. 3. Use-after-free of Asio's kqueue reactor on server shutdown — the deterministic test_tofu_flow segfault. TcpServerConn's tls_read_loop runs on a blocking-I/O thread; when Server::run() returned, io_context was destroyed while those threads were still running. On macOS kqueue the reactor pointer is null'd immediately -> segfault in socket.close(). Latent on Windows IOCP and Linux epoll. Fix: TcpAcceptor now tracks connections; new shutdown() closes all + joins threads before io is destroyed; Server::stop() now closes acceptor + media_relay too (was just io.stop()). Verified: dev + apple-dev presets build green, 21/21 tests pass, server starts + two vccli text chat over TLS (M1 on Mac), vccli --voice starts MIC stream via CoreAudio (M2 protocol-level), vccli --list-devices enumerates CoreAudio devices, xcodebuild -create-xcframework produces valid VoiceCatCore.xcframework. No ABI or proto changes. Docs updated: building.md, clients/apple/README.md, PROGRESS.md, CLAUDE.md status line.
2026-06-18 13:24:42 +02:00
stop_fn_ = [&] {
acceptor.stop();
media_relay->stop();
io.stop();
};
2026-06-15 23:48:44 +02:00
}
// Notify caller of the actual bound port (matters when bind_port==0).
uint16_t bound = acceptor.local_port();
if (cfg_.on_ready) cfg_.on_ready(bound);
feat(macos): validate dev + apple-dev presets on macOS, fix 3 cross-platform bugs macOS port groundwork — core, server, tools, and tests now build and run on macOS 26.5 / Apple Silicon. ctest --preset dev green 21/21 (2 consecutive runs). apple-dev produces valid arm64 libvoicecat.a + XCFramework for the Swift Package. Three real cross-platform bugs found and fixed (all latent on Windows/Linux): 1. test_m2_voice.cpp POSIX branch missing <netdb.h> — Linux glibc transitively includes it, macOS doesn't. Would fail on any strict POSIX system. 2. SIGPIPE killing processes on macOS — writing to a closed TCP socket raises SIGPIPE by default (doesn't exist on Windows, benign on Linux). Fixed by ignoring SIGPIPE in both core client init and server startup (POSIX-only, #ifndef _WIN32). Production fix, not just tests. 3. Use-after-free of Asio's kqueue reactor on server shutdown — the deterministic test_tofu_flow segfault. TcpServerConn's tls_read_loop runs on a blocking-I/O thread; when Server::run() returned, io_context was destroyed while those threads were still running. On macOS kqueue the reactor pointer is null'd immediately -> segfault in socket.close(). Latent on Windows IOCP and Linux epoll. Fix: TcpAcceptor now tracks connections; new shutdown() closes all + joins threads before io is destroyed; Server::stop() now closes acceptor + media_relay too (was just io.stop()). Verified: dev + apple-dev presets build green, 21/21 tests pass, server starts + two vccli text chat over TLS (M1 on Mac), vccli --voice starts MIC stream via CoreAudio (M2 protocol-level), vccli --list-devices enumerates CoreAudio devices, xcodebuild -create-xcframework produces valid VoiceCatCore.xcframework. No ABI or proto changes. Docs updated: building.md, clients/apple/README.md, PROGRESS.md, CLAUDE.md status line.
2026-06-18 13:24:42 +02:00
#ifndef _WIN32
// POSIX/macOS: ignore SIGPIPE — writing to a disconnected client's TCP socket returns
// EPIPE instead of killing the server process. On macOS SIGPIPE is delivered by
// default (unlike Windows where the signal doesn't exist). Must be set before any
// async writes are posted. Process-global; safe alongside the asio::signal_set below
// (which only catches SIGINT/SIGTERM).
std::signal(SIGPIPE, SIG_IGN);
#endif
2026-06-15 23:48:44 +02:00
// Graceful shutdown on SIGINT/SIGTERM
asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](std::error_code, int sig) {
std::printf("\n[server] signal %d — shutting down\n", sig);
acceptor.stop();
media_relay->stop();
2026-06-15 23:48:44 +02:00
io.stop();
});
fix(net): broadcast LEFT on disconnect, add keepalive/reaper, cap PLC hiss Three reported bugs traced to one root cause plus two missing designed features: 1. Stale users + eternal PLC hiss (root cause): ConnSession::close() silently erased dropped users without broadcasting UserEvent::LEFT, so peers never learned the user left and their audio engines never called remove_stream — Opus PLC synthesized comfort noise forever. Fix: broadcast_left() helper + close() broadcasts LEFT before erasing. 2. PLC cap (defense-in-depth): on_playback now caps pure PLC at ~2s, then emits digital silence so a stale stream can never hiss forever even if remove_stream is skipped. Resets automatically on fresh packets. 3. No timeout / no ping: client never sent Ping, server had no last_seen / reaper, so half-open connections (NAT timeout, wifi loss, sleep) left ghost users forever. Fix: client Ping every 15s with RTT measurement, ConnSession::last_seen bumped on every inbound TCP/UDP frame, steady_timer reaper sweeps every 15s and drops sessions older than 45s (configurable via server::Config). 4. UDP KEEPALIVE: client sends plaintext kFrameKeepalive every 5s; server bumps last_seen + echoes back. Keeps NAT bindings alive and lets media activity defer the reaper independently of TCP. 5. Graceful client disconnect: vc_disconnect() sends Disconnect{code=0} via a flag-based io-thread exit (no double-close race); server handles client-sent Disconnect with immediate close() + LEFT broadcast. 3 new tests: disconnect_left, plc_cap, reaper_timeout. 21/21 ctest green. Docs: protocol.md §6/§7, voice.md §6, architecture.md §5, PROGRESS.md.
2026-06-18 01:18:33 +02:00
// ── Keepalive reaper (docs/protocol.md §7) ─────────────────────────────────
// Sweeps every reaper_sweep_ms and drops any session whose last_seen is older than
// reaper_timeout_ms. Each close() broadcasts UserEvent::LEFT via the Tier 1 fix, so
// peers learn about the timeout exactly like a normal disconnect — their audio engines
// call remove_stream and stop PLC. This catches half-open connections (NAT timeout,
// wifi loss without RST, laptop sleep) that never produce a TCP EOF and would otherwise
// leave ghost users forever. Disabled when reaper_timeout_ms <= 0.
asio::steady_timer reaper_timer(io);
std::function<void()> arm_reaper;
if (cfg_.reaper_timeout_ms > 0) {
arm_reaper = [&] {
reaper_timer.expires_after(std::chrono::milliseconds(cfg_.reaper_sweep_ms));
reaper_timer.async_wait([&](std::error_code ec) {
if (ec || io.stopped()) return;
for (auto& sess : registry->find_stale_sessions(cfg_.reaper_timeout_ms)) {
std::printf("[server] reaper: dropping stale session %llu (user %u)\n",
static_cast<unsigned long long>(sess->session_id()),
sess->user_id());
sess->close();
}
arm_reaper();
});
};
arm_reaper();
}
std::printf("[voicecat-server] %s — TCP :%u UDP :%u\n",
cfg_.server_name.c_str(), bound, media_bound);
2026-06-15 23:48:44 +02:00
std::printf("[voicecat-server] fingerprint: %s\n",
id_mgr.fingerprint_display().c_str());
io.run();
feat(macos): validate dev + apple-dev presets on macOS, fix 3 cross-platform bugs macOS port groundwork — core, server, tools, and tests now build and run on macOS 26.5 / Apple Silicon. ctest --preset dev green 21/21 (2 consecutive runs). apple-dev produces valid arm64 libvoicecat.a + XCFramework for the Swift Package. Three real cross-platform bugs found and fixed (all latent on Windows/Linux): 1. test_m2_voice.cpp POSIX branch missing <netdb.h> — Linux glibc transitively includes it, macOS doesn't. Would fail on any strict POSIX system. 2. SIGPIPE killing processes on macOS — writing to a closed TCP socket raises SIGPIPE by default (doesn't exist on Windows, benign on Linux). Fixed by ignoring SIGPIPE in both core client init and server startup (POSIX-only, #ifndef _WIN32). Production fix, not just tests. 3. Use-after-free of Asio's kqueue reactor on server shutdown — the deterministic test_tofu_flow segfault. TcpServerConn's tls_read_loop runs on a blocking-I/O thread; when Server::run() returned, io_context was destroyed while those threads were still running. On macOS kqueue the reactor pointer is null'd immediately -> segfault in socket.close(). Latent on Windows IOCP and Linux epoll. Fix: TcpAcceptor now tracks connections; new shutdown() closes all + joins threads before io is destroyed; Server::stop() now closes acceptor + media_relay too (was just io.stop()). Verified: dev + apple-dev presets build green, 21/21 tests pass, server starts + two vccli text chat over TLS (M1 on Mac), vccli --voice starts MIC stream via CoreAudio (M2 protocol-level), vccli --list-devices enumerates CoreAudio devices, xcodebuild -create-xcframework produces valid VoiceCatCore.xcframework. No ABI or proto changes. Docs updated: building.md, clients/apple/README.md, PROGRESS.md, CLAUDE.md status line.
2026-06-18 13:24:42 +02:00
// Close all connections and block until their TLS I/O threads have finished.
// MUST happen before io (and its reactor) is destroyed — the TLS read threads do
// blocking I/O (not async on io_context) and touch the reactor on socket close.
// On macOS kqueue the reactor pointer is null'd immediately on io_context destruction,
// causing a segfault; latent on Windows IOCP / Linux epoll where timing is more forgiving.
acceptor.shutdown();
2026-06-15 23:48:44 +02:00
{
std::lock_guard lk(stop_mutex_);
stop_fn_ = nullptr;
}
workers->join();
return 0;
}
void Server::stop() {
std::lock_guard lk(stop_mutex_);
if (stop_fn_) stop_fn_();
}
} // namespace voicecat::server
#else // !VOICECAT_HAS_NET
namespace voicecat::server {
int Server::run() {
std::fprintf(stderr, "[server] stub: VOICECAT_HAS_NET not defined (build with dev preset)\n");
std::printf(" server_name : %s\n", cfg_.server_name.c_str());
std::printf(" data_dir : %s\n", cfg_.data_dir.c_str());
2026-06-15 23:48:44 +02:00
std::printf(" bind_port : %u\n", cfg_.bind_port);
return 0;
}
2026-06-15 23:48:44 +02:00
void Server::stop() {}
} // namespace voicecat::server
2026-06-15 23:48:44 +02:00
#endif // VOICECAT_HAS_NET