#include "tls_socket.hpp" #include "source_log.hpp" #include "obn/lan_tls.hpp" #include "obn/lan_tls_env.hpp" #include "obn/os_compat.hpp" #include #include #include #if defined(_WIN32) # ifndef WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN # endif # ifndef NOMINMAX # define NOMINMAX # endif # include # include # include #else # include # include # include # include # include # include #endif #include #include #include #include namespace obn::tls { namespace { std::once_flag g_init_flag; SSL_CTX* g_ctx_insecure = nullptr; void init_once() { std::call_once(g_init_flag, []() { obn::os::winsock_init_once(); SSL_library_init(); OpenSSL_add_all_algorithms(); SSL_load_error_strings(); g_ctx_insecure = SSL_CTX_new(TLS_client_method()); if (!g_ctx_insecure) { obn::source::set_last_error("SSL_CTX_new failed"); return; } SSL_CTX_set_min_proto_version(g_ctx_insecure, TLS1_VERSION); SSL_CTX_set_verify(g_ctx_insecure, SSL_VERIFY_NONE, nullptr); }); } void store_openssl_error(const char* prefix) { char errbuf[256]; ERR_error_string_n(ERR_get_error(), errbuf, sizeof(errbuf)); char composed[320]; std::snprintf(composed, sizeof(composed), "%s: %s", prefix, errbuf); obn::source::set_last_error(composed); } #if defined(_WIN32) const char* gai_strerror_portable(int rc) { return ::gai_strerrorA(rc); } #else const char* gai_strerror_portable(int rc) { return ::gai_strerror(rc); } #endif SSL_CTX* make_verify_ctx(const char* ca_file, const char* peer_cert, std::string& err) { SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); if (!ctx) { err = "SSL_CTX_new failed"; return nullptr; } SSL_CTX_set_min_proto_version(ctx, TLS1_VERSION); std::string peer_path = peer_cert ? peer_cert : ""; if (!obn::lan_tls::configure_lan_ssl_verify(ctx, ca_file, peer_path, &err)) { SSL_CTX_free(ctx); return nullptr; } return ctx; } bool verify_peer_cn(SSL* ssl, const char* expected_cn) { if (!expected_cn || !*expected_cn) return false; X509* cert = SSL_get1_peer_certificate(ssl); if (!cert) return false; const int ok = X509_check_host(cert, expected_cn, std::strlen(expected_cn), 0, nullptr); X509_free(cert); return ok == 1; } } // namespace SSL_CTX* shared_ctx() { init_once(); return g_ctx_insecure; } obn::os::socket_t dial(const std::string& host, int port, int timeout_ms) { init_once(); addrinfo hints{}; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; addrinfo* res = nullptr; char port_s[16]; std::snprintf(port_s, sizeof(port_s), "%d", port); int gai = ::getaddrinfo(host.c_str(), port_s, &hints, &res); if (gai != 0 || !res) { obn::source::set_last_error(gai_strerror_portable(gai)); return obn::os::kInvalidSocket; } obn::os::socket_t fd = obn::os::kInvalidSocket; auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms); for (auto* ai = res; ai; ai = ai->ai_next) { auto raw = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); fd = static_cast(raw); if (!obn::os::socket_valid(fd)) { fd = obn::os::kInvalidSocket; continue; } #if defined(_WIN32) DWORD tv_ms = static_cast(timeout_ms); ::setsockopt(static_cast(fd), SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&tv_ms), sizeof(tv_ms)); ::setsockopt(static_cast(fd), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&tv_ms), sizeof(tv_ms)); BOOL one_b = TRUE; ::setsockopt(static_cast(fd), IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&one_b), sizeof(one_b)); if (::connect(static_cast(fd), ai->ai_addr, static_cast(ai->ai_addrlen)) == 0) break; #else timeval tv{timeout_ms / 1000, (timeout_ms % 1000) * 1000}; ::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); ::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); int one = 1; ::setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); if (::connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break; #endif obn::os::close_socket(fd); fd = obn::os::kInvalidSocket; if (std::chrono::steady_clock::now() > deadline) break; } freeaddrinfo(res); if (!obn::os::socket_valid(fd)) obn::source::set_last_error("connect failed"); return fd; } int dial_tls(const std::string& host, int port, int timeout_ms, obn::os::socket_t* out_fd, SSL** out_ssl, const char* expected_serial) { *out_fd = obn::os::kInvalidSocket; *out_ssl = nullptr; init_once(); const bool want_verify = !obn::lan_tls::skip_verify_from_env(); std::string ca_path; std::string serial_str; if (want_verify) { if (const char* ca = obn::lan_tls::resolve_lan_ca_file()) { ca_path = ca; } if (expected_serial && *expected_serial) { serial_str = expected_serial; } else if (const char* s = obn::lan_tls::wait_env_serial( host.c_str(), obn::lan_tls::serial_env_wait_ms())) { serial_str = s; } if (ca_path.empty()) { obn::source::set_last_error( "OBN_LAN_TLS_CA_FILE not set (check obn.lan_tls.env in config dir)"); return -1; } if (serial_str.empty()) { char msg[256]; std::snprintf(msg, sizeof(msg), "no serial for %s after %dms (key=%s)", host.c_str(), obn::lan_tls::serial_env_wait_ms(), obn::lan_tls::env_key_for_ip(host).c_str()); obn::source::set_last_error(msg); return -1; } } std::string peer_path; if (want_verify) { if (const char* peer = obn::lan_tls::resolve_lan_peer_cert( host.c_str(), serial_str.c_str())) { peer_path = peer; } } SSL_CTX* ctx = nullptr; bool owned_ctx = false; if (want_verify) { std::string err; ctx = make_verify_ctx(ca_path.c_str(), peer_path.empty() ? nullptr : peer_path.c_str(), err); if (!ctx) { obn::source::set_last_error(err.c_str()); return -1; } owned_ctx = true; } else { ctx = g_ctx_insecure; if (!ctx) { obn::source::set_last_error("SSL_CTX not initialized"); return -1; } } obn::os::socket_t fd = dial(host, port, timeout_ms); if (!obn::os::socket_valid(fd)) { if (owned_ctx) SSL_CTX_free(ctx); return -1; } SSL* ssl = SSL_new(ctx); if (!ssl) { store_openssl_error("SSL_new"); obn::os::close_socket(fd); if (owned_ctx) SSL_CTX_free(ctx); return -1; } const char* sni = want_verify ? serial_str.c_str() : host.c_str(); SSL_set_fd(ssl, static_cast(fd)); SSL_set_tlsext_host_name(ssl, sni); if (SSL_connect(ssl) != 1) { store_openssl_error("SSL_connect"); SSL_free(ssl); obn::os::close_socket(fd); if (owned_ctx) SSL_CTX_free(ctx); return -1; } if (want_verify && !verify_peer_cn(ssl, serial_str.c_str())) { obn::source::set_last_error("TLS hostname verify failed"); SSL_free(ssl); obn::os::close_socket(fd); if (owned_ctx) SSL_CTX_free(ctx); return -1; } if (owned_ctx) SSL_CTX_free(ctx); *out_fd = fd; *out_ssl = ssl; return 0; } void close_tls(obn::os::socket_t* fd, SSL** ssl) { if (ssl && *ssl) { SSL_shutdown(*ssl); SSL_free(*ssl); *ssl = nullptr; } if (fd && obn::os::socket_valid(*fd)) { obn::os::close_socket(*fd); *fd = obn::os::kInvalidSocket; } } int ssl_write_all(SSL* ssl, const void* buf, std::size_t len) { const auto* p = static_cast(buf); std::size_t sent = 0; while (sent < len) { int n = SSL_write(ssl, p + sent, static_cast(len - sent)); if (n <= 0) { int err = SSL_get_error(ssl, n); if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) continue; return -1; } sent += static_cast(n); } return 0; } int ssl_read_full(SSL* ssl, void* buf, std::size_t len) { auto* p = static_cast(buf); std::size_t got = 0; while (got < len) { int n = SSL_read(ssl, p + got, static_cast(len - got)); if (n <= 0) { int err = SSL_get_error(ssl, n); if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) continue; if (err == SSL_ERROR_ZERO_RETURN) return 1; return -1; } got += static_cast(n); } return 0; } int ssl_read_line(SSL* ssl, std::string* out, std::size_t max_len) { out->clear(); out->reserve(128); char prev = '\0'; while (out->size() < max_len) { char c = '\0'; int n = SSL_read(ssl, &c, 1); if (n <= 0) { int err = SSL_get_error(ssl, n); if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) continue; if (err == SSL_ERROR_ZERO_RETURN) return 1; return -1; } if (prev == '\r' && c == '\n') { if (!out->empty()) out->pop_back(); return 0; } out->push_back(c); prev = c; } obn::source::set_last_error("ssl_read_line: line exceeded max_len"); return -1; } } // namespace obn::tls