mirror of
https://github.com/ClusterM/open-bamboo-networking.git
synced 2026-10-22 11:02:26 -05:00
344 lines
10 KiB
C++
344 lines
10 KiB
C++
#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 <openssl/err.h>
|
|
#include <openssl/ssl.h>
|
|
#include <openssl/x509v3.h>
|
|
|
|
#if defined(_WIN32)
|
|
# ifndef WIN32_LEAN_AND_MEAN
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# endif
|
|
# ifndef NOMINMAX
|
|
# define NOMINMAX
|
|
# endif
|
|
# include <winsock2.h>
|
|
# include <ws2tcpip.h>
|
|
# include <windows.h>
|
|
#else
|
|
# include <netdb.h>
|
|
# include <netinet/in.h>
|
|
# include <netinet/tcp.h>
|
|
# include <sys/socket.h>
|
|
# include <sys/types.h>
|
|
# include <unistd.h>
|
|
#endif
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
|
|
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<obn::os::socket_t>(raw);
|
|
if (!obn::os::socket_valid(fd)) { fd = obn::os::kInvalidSocket; continue; }
|
|
|
|
#if defined(_WIN32)
|
|
DWORD tv_ms = static_cast<DWORD>(timeout_ms);
|
|
::setsockopt(static_cast<SOCKET>(fd), SOL_SOCKET, SO_SNDTIMEO,
|
|
reinterpret_cast<const char*>(&tv_ms), sizeof(tv_ms));
|
|
::setsockopt(static_cast<SOCKET>(fd), SOL_SOCKET, SO_RCVTIMEO,
|
|
reinterpret_cast<const char*>(&tv_ms), sizeof(tv_ms));
|
|
BOOL one_b = TRUE;
|
|
::setsockopt(static_cast<SOCKET>(fd), IPPROTO_TCP, TCP_NODELAY,
|
|
reinterpret_cast<const char*>(&one_b), sizeof(one_b));
|
|
if (::connect(static_cast<SOCKET>(fd), ai->ai_addr,
|
|
static_cast<int>(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<int>(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<const std::uint8_t*>(buf);
|
|
std::size_t sent = 0;
|
|
while (sent < len) {
|
|
int n = SSL_write(ssl, p + sent, static_cast<int>(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<std::size_t>(n);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int ssl_read_full(SSL* ssl, void* buf, std::size_t len)
|
|
{
|
|
auto* p = static_cast<std::uint8_t*>(buf);
|
|
std::size_t got = 0;
|
|
while (got < len) {
|
|
int n = SSL_read(ssl, p + got, static_cast<int>(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<std::size_t>(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
|