From c6ba0c7c9590aed7d24b1fdb952d433a0fe923d3 Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Sat, 14 Feb 2026 22:45:15 +0000 Subject: [PATCH] test: add failing probe for idle TLS session flood --- tools/security/check_idle_tls_flood.rb | 108 +++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100755 tools/security/check_idle_tls_flood.rb diff --git a/tools/security/check_idle_tls_flood.rb b/tools/security/check_idle_tls_flood.rb new file mode 100755 index 0000000..f01b10c --- /dev/null +++ b/tools/security/check_idle_tls_flood.rb @@ -0,0 +1,108 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "fileutils" +require "openssl" +require "socket" +require "timeout" +require "tmpdir" + +def find_free_port(start_port = 26250) + port = start_port + loop do + begin + server = TCPServer.new("127.0.0.1", port) + server.close + return port + rescue Errno::EADDRINUSE + port += 1 + end + end +end + +def wait_for_start(log_path, timeout_secs = 5) + Timeout.timeout(timeout_secs) do + loop do + if File.exist?(log_path) && File.read(log_path).include?("Started listener") + return + end + + sleep 0.05 + end + end +end + +repo_root = File.expand_path("../..", __dir__) +agate_bin = ENV.fetch("AGATE_BIN", File.join(repo_root, "target/debug/agate")) +conn_count = Integer(ENV.fetch("PROBE_IDLE_CONN_COUNT", "160")) + +unless File.executable?(agate_bin) + Dir.chdir(repo_root) { system("cargo", "build", out: File::NULL) } || abort("cargo build failed") +end + +Dir.mktmpdir("agate-idle-flood-") do |tmp_dir| + content = File.join(tmp_dir, "content") + certs = File.join(tmp_dir, "certs") + log_path = File.join(tmp_dir, "server.log") + FileUtils.mkdir_p(content) + File.write(File.join(content, "index.gmi"), "home\n") + + port = find_free_port + pid = spawn( + agate_bin, + "--addr", "127.0.0.1:#{port}", + "--content", content, + "--certs", certs, + "--hostname", "localhost", + out: File::NULL, + err: log_path + ) + + sockets = [] + successful = 0 + + begin + wait_for_start(log_path) + + ctx = OpenSSL::SSL::SSLContext.new + ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE + + conn_count.times do + begin + tcp = TCPSocket.new("127.0.0.1", port) + tls = OpenSSL::SSL::SSLSocket.new(tcp, ctx) + tls.hostname = "localhost" if tls.respond_to?(:hostname=) + tls.connect + sockets << [tls, tcp] + successful += 1 + rescue StandardError + break + end + end + + sleep 2 + + if successful >= conn_count + warn "VULNERABLE: accepted #{successful} idle TLS sessions without visible cap." + exit 1 + end + + puts "PASS: idle TLS sessions were capped before #{conn_count}." + exit 0 + ensure + sockets.each do |tls, tcp| + begin + tls.close + rescue StandardError + nil + end + begin + tcp.close + rescue StandardError + nil + end + end + Process.kill("TERM", pid) rescue nil + Process.wait(pid) rescue nil + end +end