随着网络安全的日益重要,传输层安全性(TLS)协议在保护数据传输中扮演着关键角色。TLS握手作为该协议的核心部分,确保了客户端和服务器之间的安全通信。鉴于其重要性,对TLS握手的性能进行精确评估变得至关重要。该工具专注于TLS握手的性能测试,而不涉及数据传输或重协商。
快速重置TCP连接
理解快速重置TCP连接对于优化TLS握手性能具有重要意义。
在数据传输过程中,TCP连接的建立和关闭是必要的操作。特别是在TLS握手中,传统的TLS测试工具在每次握手后不会立即关闭TCP连接,而是保持连接状态,这会导致不必要的资源占用,增加服务器的负担。新工具采用的快速TCP连接重置方法能有效缓解这一问题。
首先,快速重置TCP连接有助于减少所谓的TIME_WAIT状态。在标准的TCP连接关闭过程中,当一端(通常是客户端)完成数据发送并发送FIN请求关闭连接时,另一端在确认数据接收无误后也会发送FIN包,最终关闭连接。然而,在这个关闭过程中,连接会处于TIME_WAIT状态,通常持续4分钟(两个MSL,即两倍的最大报文生存时间),以确保网络中仍然在传输中的报文得以妥善处理。
快速重置TCP连接的方法可以显著减少这些资源的占用。通过在TLS握手完成后立即发送RESET信号而不是标准的FIN信号,连接可以被立即关闭,无需等待TIME_WAIT状态的结束。这样,系统资源如端口号能更快被释放并重新利用,极大提高了服务器的性能和并发处理能力。
快速重置TCP连接还有助于防止网络拥塞。在高并发的应用场景下,如数千个连接请求同时到达服务器,如果不及时关闭不再需要的连接,就会造成大量的端点资源占用,进而导致网络拥塞,影响正常的网络通信]。通过实施快速重置,可以避免这种情况,确保网络资源的高效利用。
从安全性角度来看,快速重置TCP连接同样具有优势。长时间保持的TCP连接可能会成为恶意攻击的目标,例如拒绝服务攻击(DoS)。攻击者可能通过大量伪造的SYN包尝试消耗服务器资源,而快速重置可以迅速释放这些无效连接,避免攻击行为对服务器造成实质性的影响。
结合TLS握手的工作流程看,快速重置TCP连接的优势更为明显。在TLS握手过程中,客户端与服务器需要进行多次信息交换以协商加密算法和验证身份等,一旦握手完成,敏感信息交换即告结束,此时即可执行快速重置,从而既保证了安全又提升了效率。
此外,考虑到不同应用和环境可能对快速重置TCP连接的适应性,开发者应当提供灵活的配置选项,允许用户根据实际需求选择是否启用快速重置功能。同时,在进行网络安全设计和性能优化时,也需要考虑这一特性对现有网络架构和业务流程的潜在影响。
快速重置TCP连接不仅提升了TLS握手的效率,减少了服务器的资源占用,同时也增强了网络的安全性。这种技术的应用,特别是在需要高并发、高安全等级的网络环境中,将会成为提升网络性能的重要手段。
下面该工具通过优化TCP连接的重置机制,实现了在完成TLS握手后立即重置TCP连接,从而大幅提高了测试的效率和并发性。这种快速重置机制避免了长时间的连接保持,使得测试工具能够在短时间内发起更多的TLS握手。
多线程
多线程 是指在同一个进程中可以同时运行多个线程执行不同的任务。每个线程可以看作是程序执行的独立流,拥有自己的寄存器和堆栈,但共享进程的内存空间(如代码段、数据段等)。在网络应用中,多线程可以带来以下优势:
- 并行处理:多个线程可以同时处理多个网络连接或任务,提高了程序的并发能力。
- 资源利用率:通过合理分配线程,可以更高效地利用CPU资源,特别是在多核处理器上。
- 响应速度:线程间的上下文切换比进程间的上下文切换要快,这可以提高程序的响应速度。
epoll() IO优化
epoll 是Linux提供的一种高效的I/O多路复用接口,它在处理大量并发socket连接时,相较于传统的select和poll机制,具有显著的性能优势。epoll的工作原理如下:
- 事件驱动:epoll通过监听文件描述符(通常是socket)上的事件(如读、写事件),当事件发生时,epoll会通知程序进行处理。
- 高效的事件通知:与传统的select和poll不同,epoll不需要在每次调用时都传递和检查整个文件描述符集合,而是通过维护一个内部的数据结构来记录哪些文件描述符有待处理的事件。这大大减少了资源消耗和响应时间。
- 边缘触发(Edge Triggered, ET)模式:epoll支持边缘触发模式,在此模式下,只有在状态发生变化时(如从无数据到有数据可读),才会通知程序。这减少了不必要的通知,提高了效率。
多线程与epoll()的结合
将多线程与epoll结合使用,可以进一步提升网络应用的性能:
- 线程池管理:可以创建一个线程池,每个线程负责监听一部分epoll事件,或者在新连接到来时动态分配线程。
- 负载均衡:通过合理分配线程和epoll监听的文件描述符,可以平衡每个线程的负载,避免某些线程过载而其他线程空闲。
- 资源利用最大化:结合多线程和epoll的优势,可以更高效地利用CPU和IO资源,特别是在高并发场景下。
在实际的网络服务器或客户端应用中,开发者可以根据实际需求选择合适的线程数量和epoll的使用方式。例如,可以使用少量线程处理大量的网络连接,每个线程负责一部分连接的读写操作,同时利用epoll高效地管理这些连接的事件。
总之,多线程与epoll()的结合为高性能网络编程提供了强大的工具,使得开发者能够构建出既快速又高效的网络应用。
TLS握手性能测试工具:快速重置、多线程与高级统计分析(C/C++代码实现)
工具的实现基于C++和OpenSSL库,使用了以下关键技术:
- epoll():用于高效的I/O多路复用,处理大量并发连接。
- 多线程:通过std::thread库实现,每个线程可以独立处理TLS握手。
- 原子操作:使用std::atomic确保多线程环境下的数据一致性。
- 定时器:使用std::chrono库实现定时任务,如每秒更新统计数据。
...
class IO {
private:
static const size_t N_EVENTS = 128;
static const size_t TO_MSEC = 5;
private:
int ed_;
int ev_count_;
SSL_CTX *tls_ctx_;
struct epoll_event events_[N_EVENTS];
std::list<SocketHandler *> reconnect_q_;
std::list<SocketHandler *> backlog_;
public:
IO()
: ed_(-1), ev_count_(0), tls_ctx_(NULL)
{
tls_ctx_ = SSL_CTX_new(TLS_client_method());
// 只允许TLS 1.2和1.3
// requested.
if (g_opt.tls_vers != TLS_ANY_VERSION) {
SSL_CTX_set_min_proto_version(tls_ctx_, g_opt.tls_vers);
SSL_CTX_set_max_proto_version(tls_ctx_, g_opt.tls_vers);
} else {
SSL_CTX_set_min_proto_version(tls_ctx_, TLS1_2_VERSION);
SSL_CTX_set_max_proto_version(tls_ctx_, TLS1_3_VERSION);
}
// 会话恢复
if (!g_opt.use_tickets) {
unsigned int mode = SSL_SESS_CACHE_OFF
| SSL_SESS_CACHE_NO_INTERNAL;
if (!g_opt.adv_tickets)
SSL_CTX_set_options(tls_ctx_, SSL_OP_NO_TICKET);
SSL_CTX_set_session_cache_mode(tls_ctx_, mode);
}
else {
unsigned int mode = SSL_SESS_CACHE_CLIENT
| SSL_SESS_CACHE_NO_AUTO_CLEAR;
SSL_CTX_set_session_cache_mode(tls_ctx_, mode);
}
if (g_opt.cipher) {
if (g_opt.tls_vers == TLS1_3_VERSION
|| g_opt.tls_vers == TLS_ANY_VERSION)
if (!SSL_CTX_set_ciphersuites(tls_ctx_,
g_opt.cipher))
throw Except("cannot set cipher");
if (g_opt.tls_vers == TLS1_2_VERSION
|| g_opt.tls_vers == TLS_ANY_VERSION)
if (!SSL_CTX_set_cipher_list(tls_ctx_,
g_opt.cipher))
throw Except("cannot set cipher");
}
if (g_opt.curve)
if (!SSL_CTX_set1_groups_list(tls_ctx_, g_opt.curve))
throw Except("cannot set elliptic curve");
SSL_CTX_set_verify(tls_ctx_, SSL_VERIFY_NONE, NULL);
if (g_opt.keylogfile)
SSL_CTX_set_keylog_callback(tls_ctx_, keylog);
if ((ed_ = epoll_create(1)) < 0)
throw Except("can't create epoll");
memset(events_, 0, sizeof(events_));
}
~IO()
{
if (ed_ > -1)
close(ed_);
reconnect_q_.clear();
SSL_CTX_set_keylog_callback(tls_ctx_, nullptr);
if (tls_ctx_)
SSL_CTX_free(tls_ctx_);
}
void
add(SocketHandler *sh, int events)
{
struct epoll_event ev = {
.events = events | EPOLLONESHOT,
.data = { .ptr = sh }
};
if (epoll_ctl(ed_, EPOLL_CTL_MOD, sh->sd, &ev) < 0) {
if (errno == ENOENT &&
epoll_ctl(ed_, EPOLL_CTL_ADD, sh->sd, &ev) < 0)
{
throw Except("can't add socket to poller");
}
}
}
void
del(SocketHandler *sh)
{
if (epoll_ctl(ed_, EPOLL_CTL_DEL, sh->sd, NULL) < 0)
throw Except("can't delete socket from poller");
}
void
queue_reconnect(SocketHandler *sh) noexcept
{
reconnect_q_.push_back(sh);
}
void
wait()
{
retry:
ev_count_ = epoll_wait(ed_, events_, N_EVENTS, TO_MSEC);
if (ev_count_ < 0) {
if (errno == EINTR)
goto retry;
throw Except("poller wait error");
}
}
SocketHandler *
next_sk() noexcept
{
if (ev_count_)
return (SocketHandler *)events_[--ev_count_].data.ptr;
return NULL;
}
void
backlog() noexcept
{
backlog_.swap(reconnect_q_);
}
SocketHandler *
next_backlog() noexcept
{
if (backlog_.empty())
return NULL;
SocketHandler *sh = backlog_.front();
backlog_.pop_front();
return sh;
}
SSL *
new_tls_ctx(SocketHandler *sh)
{
SSL *ctx = SSL_new(tls_ctx_);
if (!ctx)
throw Except("cannot clone TLS context");
SSL_set_fd(ctx, sh->sd);
BIO_set_tcp_ndelay(sh->sd, true);
if (g_opt.use_tickets) {
auto sess = sh->get_session();
if (sess)
SSL_set_session(ctx, sess);
}
if(g_opt.sni)
SSL_set_tlsext_host_name(ctx, g_opt.sni);
return ctx;
}
};
class Peer : public SocketHandler {
private:
enum _states {
STATE_TCP_CONNECT,
STATE_TCP_CONNECTING,
STATE_TLS_HANDSHAKING,
};
private:
IO &io_;
int id_;
SSL *tls_;
SSL_SESSION *sess_;
std::chrono::time_point<std::chrono::steady_clock> ts_;
enum _states state_;
public:
Peer(IO &io, int id) noexcept
: io_(io), id_(id), tls_(NULL), sess_(NULL)
, state_(STATE_TCP_CONNECT)
{
sd = -1;
dbg_status("created");
}
virtual ~Peer()
{
disconnect();
if (sess_)
SSL_SESSION_free(sess_);
}
bool
next_state() final override
{
switch (state_) {
case STATE_TCP_CONNECT:
return tcp_connect();
case STATE_TCP_CONNECTING:
return tcp_connect_try_finish();
case STATE_TLS_HANDSHAKING:
return tls_handshake();
default:
throw Except("bad next state %d", state_);
}
return false;
}
SSL_SESSION*
get_session()
{
return sess_;
}
private:
void
poll_for_read()
{
io_.add(this, EPOLLIN | EPOLLERR);
}
void
poll_for_write()
{
io_.add(this, EPOLLOUT | EPOLLERR);
}
void
del_from_poll()
{
io_.del(this);
}
void
dbg_status(const char *msg) noexcept
{
if (g_opt.debug)
dbg << "peer " << id_ << " " << msg << std::endl;
}
bool
tls_handshake()
{
using namespace std::chrono;
state_ = STATE_TLS_HANDSHAKING;
if (!tls_) {
tls_ = io_.new_tls_ctx(this);
stat.tls_handshakes++;
ts_ = steady_clock::now();
}
int r = SSL_connect(tls_);
if (r == 1) {
auto t1(steady_clock::now());
const duration<double, std::milli> lat = t1 - ts_;
lat_stat.update(lat.count());
dbg_status("has completed TLS handshake");
stat.tls_handshakes--;
stat.tls_connections++;
stat.tot_tls_handshakes++;
disconnect();
stat.tcp_connections--;
io_.queue_reconnect(this);
return true;
}
switch (SSL_get_error(tls_, r)) {
case SSL_ERROR_WANT_READ:
poll_for_read();
break;
case SSL_ERROR_WANT_WRITE:
poll_for_write();
break;
default:
if (!stat.tot_tls_handshakes)
throw Except("cannot establish even one TLS"
" connection");
stat.tls_handshakes--;
stat.error_count++;
disconnect();
stat.tcp_connections--;
}
return false;
}
bool
handle_established_tcp_conn()
{
// del_from_poll();
dbg_status("has established TCP connection");
stat.tcp_handshakes--;
stat.tcp_connections++;
return tls_handshake();
}
void
handle_connect_error(int err)
{
if (err == EINPROGRESS || err == EAGAIN) {
errno = 0;
// 继续等待TCP握手
//add_to_poll();
poll_for_write();
return;
}
if (!stat.tcp_connections)
throw Except("cannot establish even one TCP connection");
errno = 0;
stat.tcp_handshakes--;
disconnect();
}
bool
tcp_connect_try_finish()
{
int ret = 0;
socklen_t len = 4;
if (getsockopt(sd, SOL_SOCKET, SO_ERROR, &ret, &len))
throw Except("cannot get a socket connect() status");
if (!ret)
return handle_established_tcp_conn();
handle_connect_error(ret);
return false;
}
bool
tcp_connect()
{
sd = socket(g_opt.ip.sin6_family, SOCK_STREAM, IPPROTO_TCP);
if (sd < 0)
throw Except("cannot create a socket");
fcntl(sd, F_SETFL, fcntl(sd, F_GETFL, 0) | O_NONBLOCK);
int sz = (g_opt.ip.sin6_family == AF_INET) ? sizeof(sockaddr_in)
: sizeof(sockaddr_in6);
int r = connect(sd, (struct sockaddr *)&g_opt.ip, sz);
stat.tcp_handshakes++;
state_ = STATE_TCP_CONNECTING;
if (!r)
return handle_established_tcp_conn();
handle_connect_error(errno);
return false;
}
void
disconnect() noexcept
{
...
}
state_ = STATE_TCP_CONNECT;
}
};
...
static int do_getopt(int argc, char *argv[]) noexcept
{
...
static struct option long_opts[] = {
{"help", no_argument, NULL, 'h'},
{"debug", no_argument, NULL, 'd'},
{"quiet", no_argument, NULL, 'q'},
{"to", no_argument, NULL, 'T'},
{"tls", required_argument, NULL, 'V'},
{"tickets", required_argument, NULL, 'K'},
{"keylogfile", required_argument, NULL, 'F'},
{"sni", required_argument, NULL, 's'},
{0, 0, 0, 0}
};
while ((c = getopt_long(argc, argv, "hl:c:C:dqt:n:T:", long_opts, &o))
!= -1)
{
switch (c) {
case 0:
break;
case 'c':
g_opt.cipher = optarg;
break;
case 'C':
g_opt.curve = optarg;
break;
case 'd':
g_opt.debug = true;
break;
case 'q':
g_opt.quiet = true;
break;
case 'l':
g_opt.n_peers = atoi(optarg);
break;
case 't':
g_opt.n_threads = atoi(optarg);
if (g_opt.n_threads > 512) {
std::cerr << "ERROR: too many threads requested"
<< std::endl;
exit(2);
}
break;
case 'n':
g_opt.n_hs = atoi(optarg);
break;
case 'T':
g_opt.timeout = atoi(optarg);
break;
case 'V':
if (!strncmp(optarg, "1.2", 4)) {
g_opt.tls_vers = TLS1_2_VERSION;
} else if (!strncmp(optarg, "1.3", 4)) {
g_opt.tls_vers = TLS1_3_VERSION;
} else if (!strncmp(optarg, "any", 4)) {
g_opt.tls_vers = TLS_ANY_VERSION;
}else {
std::cout << "Unknown TLS version, fallback to"
" 1.2\n" << std::endl;
g_opt.tls_vers = TLS1_2_VERSION;
}
break;
case 'K':
if (!strncmp(optarg, "on", 3)) {
g_opt.use_tickets = true;
} else if (!strncmp(optarg, "off", 4)) {
g_opt.use_tickets = false;
} else if (!strncmp(optarg, "advertise", 10)) {
g_opt.use_tickets = false;
g_opt.adv_tickets = true;
}else {
std::cout << "Unknown TLS version, fallback to"
" 1.2\n" << std::endl;
g_opt.tls_vers = TLS1_2_VERSION;
}
break;
case 'F':
g_opt.keylogfile = optarg;
break;
case 's':
g_opt.sni = optarg;
break;
case 'h':
default:
usage();
return 1;
}
}
if (g_opt.keylogfile) {
// 不要丢弃以前保存的密钥
bio_keylog = BIO_new_file(g_opt.keylogfile, "a");
if (!bio_keylog) {
std::cerr << "Error writing keylog file '"
<< g_opt.keylogfile << "'" << std::endl;
return -ENOENT;
}
}
if (optind != argc && optind + 2 != argc) {
std::cerr << "\nERROR: either 0 or 2 arguments are allowed: "
<< "none for defaults or address and port."
<< std::endl;
usage();
return -EINVAL;
}
if (optind >= argc) {
parse_ipv4("127.0.0.1", "443");
return 0;
}
const char *addr_str = argv[optind];
const char *port_str = argv[++optind];
if (parse_ipv4(addr_str, port_str) && parse_ipv6(addr_str, port_str)) {
std::cerr << "ERROR: can't parse ip address from string '"
<< addr_str << "'" << std::endl;
return -EINVAL;
}
return 0;
}
int main(int argc, char *argv[])
{
...
if ((r = do_getopt(argc, argv))) {
BIO_free_all(bio_keylog);
return r;
}
if (!g_opt.quiet)
print_settings();
update_limits();
signal(SIGTERM, sig_handler);
signal(SIGINT, sig_handler);
SSL_library_init();
SSL_load_error_strings();
std::vector<std::thread> thr(g_opt.n_threads);
for (auto i = 0; i < g_opt.n_threads; ++i) {
dbg << "spawn thread " << (i + 1) << std::endl;
thr[i] = std::thread([]() {
try {
io_loop();
}
catch (Except &e) {
std::cerr << "ERROR: " << e.what() << std::endl;
exit(1);
}
lat_stat.dump();
});
}
auto start_t(steady_clock::now());
stat.start_count();
while (!end_of_work()) {
std::this_thread::sleep_for(std::chrono::seconds(1));
statistics_update();
auto now(steady_clock::now());
auto dt = duration_cast<seconds>(now - start_t).count();
if (g_opt.timeout && g_opt.timeout <= dt)
finish = true;
}
for (auto &t : thr)
t.join();
statistics_dump();
BIO_free_all(bio_keylog);
return 0;
}
If you need the complete source code, please add the WeChat number (c17865354792)
主要功能:
- 仅执行TLS握手:专注于TLS握手过程,快速重置TCP连接,不进行数据传输或重协商。
- 多线程与高效I/O:采用基于epoll()的I/O处理,实现了多线程,提高了效率。这对于需要在客户端进行昂贵加密计算的ECC握手尤为重要。
- 丰富的统计信息:提供了详尽的统计数据,帮助用户深入了解测试结果。
参数说明
-h, --help
:显示帮助信息并退出。-d, --debug
:开启调试模式。-q, --quiet
:减少运行时的统计信息显示。-l <N>
:每个线程的并行连接数限制(默认:1)。-n <N>
:建立的总握手次数。-t <N>
:线程数(默认:1)。-T, --to
:测试持续时间(秒)。-c <cipher>
:强制选择密码套件。-C <curve>
:强制选择椭圆曲线算法的特定曲线。-V, --tls <version>
:设置握手的TLS版本。-K, --tickets <mode>
:处理TLS会话票证和会话恢复。-F, --keylogfile <f>
:将密钥转储到文件,供流量分析器使用。-s, --sni <servernameindicator>
:指定要使用的SNI。
示例
以下是一些使用示例:
-
运行时减少统计信息的显示
./perf -q 192.168.1.1 443
-
强制选择特定的密码套件
./perf -c "ECDHE-RSA-AES128-GCM-SHA256" 192.168.1.1 443
-
通过8个线程,每个线程100个并发连接,测试10秒内的TLS v1.3握手:
./perf -T 10 -l 100 -t 8 --tls 1.3 192.168.76.7 8081
-
测试100次握手,让OpenSSL自行决定TLS版本和密码套件:
./perf -n 100 --tls any ::1 8081
-
设置握手的TLS版本(‘1.2’, ‘1.3’ 或 ‘any’)
./perf -V 1.3 192.168.1.1 443
-
将密钥转储到文件,供流量分析器使用
./perf -F keys.log 192.168.1.1 443
- 指定要使用的服务器名称指示(SNI)
./tperf -s "baidu.com" 192.168.1.1 443
结论
TLS握手性能测试工具,它可以帮助我们评估服务器在处理TLS握手时的性能。通过模拟大量客户端连接,它可以揭示服务器在高负载下的行为,从而为性能优化提供依据。
We also undertake the development of program requirements here. If necessary, please follow the WeChat official account 【程序猿编码】and contact me