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

39
server/src/identity.cpp Normal file
View File

@@ -0,0 +1,39 @@
#include "identity.h"
#ifdef VOICECAT_HAS_NET
#include <filesystem>
#include <stdexcept>
namespace voicecat::server {
bool ServerIdentityManager::init(const std::filesystem::path& data_dir,
const std::string& server_name, std::string& error) {
try {
std::filesystem::create_directories(data_dir);
auto id_path = data_dir / "identity.key";
auto cert_path = data_dir / "server.crt";
auto key_path = data_dir / "server.key";
if (std::filesystem::exists(id_path) &&
std::filesystem::exists(cert_path) &&
std::filesystem::exists(key_path)) {
identity_ = crypto::ServerIdentity::load(id_path);
cert_ = crypto::ServerCert::load(cert_path, key_path);
} else {
identity_ = crypto::ServerIdentity::generate();
cert_ = crypto::ServerCert::generate(server_name);
identity_.save(id_path);
cert_.save(cert_path, key_path);
}
return true;
} catch (const std::exception& ex) {
error = ex.what();
return false;
}
}
} // namespace voicecat::server
#endif // VOICECAT_HAS_NET