feat(M1): TCP/TLS control plane -- auth, channels, ephemeral text

Implements the full M1 milestone. Two clients authenticate over TLS 1.3
(guest + Argon2id password) and exchange channel + private text messages
through a real server. All five ctest --preset m1-dev tests pass in ~1 s.

Key components added:
- vcpkg baseline + m1-dev preset (protobuf/mbedTLS/libsodium/asio/sqlite3)
- FrameCodec feed+emit, encode/decode_envelope, protobuf codegen
- TcpServerConn with blocking TLS handshake thread + tls_read_loop
- TlsContext (mbedTLS 1.3, ECDSA-P256 self-signed cert, TOFU on client)
- WorkerPool (3 threads, used for Argon2id)
- Database: SQLite + libsodium Argon2id, account lifecycle, bootstrap admin
- ServerIdentityManager: Ed25519 key + cert generate/persist/fingerprint
- ConnSession state machine: WaitingHello -> WaitingAuth -> Authenticated
- SessionRegistry: channel tree, user map, text routing, broadcast
- vc_client full M1 C ABI: connect/TLS/handshake/auth/text/disconnect
- voicecat-admin CLI: account add/reset/del/list
- test_m1_integration: M1 exit criterion, verified green

Bug fixed: double-framing in ConnSession::send_envelope -- encode_envelope
was adding the [4-byte len] prefix, then TcpServerConn::send_frame added
a second one, causing the client to parse [len][proto] as protobuf (silent
failure). Fixed by serializing raw protobuf bytes in send_envelope and
letting send_frame apply the single length prefix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 23:48:44 +02:00
parent b332b0972b
commit 63f457fc54
42 changed files with 4180 additions and 190 deletions

View File

@@ -1,6 +1,33 @@
file(GLOB_RECURSE VOICECAT_SERVER_SOURCES CONFIGURE_DEPENDS
# voicecat-server-lib — all server implementation except main.cpp.
# Linked by the server binary AND by tests (test_m1_integration).
file(GLOB_RECURSE VOICECAT_SERVER_LIB_SOURCES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
list(FILTER VOICECAT_SERVER_LIB_SOURCES EXCLUDE REGEX ".*[/\\\\]main\\.cpp$")
add_executable(voicecat-server ${VOICECAT_SERVER_SOURCES})
target_link_libraries(voicecat-server PRIVATE voicecat::voicecat)
add_library(voicecat-server-lib STATIC ${VOICECAT_SERVER_LIB_SOURCES})
target_compile_features(voicecat-server-lib PRIVATE cxx_std_20)
target_link_libraries(voicecat-server-lib PUBLIC voicecat::voicecat)
target_include_directories(voicecat-server-lib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/core/src)
add_library(voicecat::server ALIAS voicecat-server-lib)
if(VOICECAT_USE_VCPKG_DEPS)
find_package(unofficial-sqlite3 CONFIG REQUIRED)
find_package(unofficial-sodium CONFIG REQUIRED)
find_package(MbedTLS CONFIG REQUIRED)
find_package(asio CONFIG REQUIRED)
target_link_libraries(voicecat-server-lib PUBLIC
unofficial::sqlite3::sqlite3
unofficial-sodium::sodium
MbedTLS::mbedtls MbedTLS::mbedcrypto MbedTLS::mbedx509
asio::asio)
if(WIN32)
target_link_libraries(voicecat-server-lib PUBLIC ws2_32 mswsock)
endif()
endif()
# The server binary is just main.cpp calling Server::run().
add_executable(voicecat-server ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp)
target_link_libraries(voicecat-server PRIVATE voicecat::server)
target_compile_features(voicecat-server PRIVATE cxx_std_20)