Back to Repositories

Testing TLS Socket Helper Implementation in Fluentd

This test suite validates the socket helper functionality in Fluentd, focusing on TLS/SSL socket creation and certificate handling. It ensures secure communication capabilities with various certificate configurations and validation scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of socket helper functionality in Fluentd, particularly focusing on TLS/SSL connections.

  • Tests self-signed certificate/key pair handling
  • Validates certificates signed by self-signed CA
  • Tests client-server certificate chain verification
  • Handles error cases with invalid certificates

Implementation Analysis

The testing approach utilizes a custom EchoTLSServer implementation to simulate secure socket connections.

  • Implements setup/teardown for port management
  • Uses OpenSSL for certificate and context creation
  • Employs non-blocking IO operations
  • Implements proper resource cleanup

Technical Details

  • Uses Test::Unit framework
  • Integrates OpenSSL for SSL/TLS functionality
  • Implements custom socket server class
  • Uses fixture certificates in test/data/cert directories
  • Handles TCP socket creation and management

Best Practices Demonstrated

The test suite exemplifies several testing best practices for secure socket implementations.

  • Proper resource cleanup in teardown
  • Comprehensive error case handling
  • Isolation of test scenarios
  • Use of fixture data for certificates
  • Thread-safe server implementation

fluent/fluentd

test/plugin_helper/test_socket.rb

            
require_relative '../helper'
require 'fluent/plugin_helper/socket'
require 'fluent/plugin/base'

require 'socket'
require 'openssl'

class SocketHelperTest < Test::Unit::TestCase
  CERT_DIR = File.expand_path(File.dirname(__FILE__) + '/data/cert/without_ca')
  CA_CERT_DIR = File.expand_path(File.dirname(__FILE__) + '/data/cert/with_ca')
  CERT_CHAINS_DIR = File.expand_path(File.dirname(__FILE__) + '/data/cert/cert_chains')

  def setup
    @port = unused_port(protocol: :tcp)
  end

  def teardown
    @port = nil
  end

  class SocketHelperTestPlugin < Fluent::Plugin::TestBase
    helpers :socket
  end

  class EchoTLSServer
    def initialize(port, host: '127.0.0.1', cert_path: nil, private_key_path: nil, ca_path: nil)
      server = TCPServer.open(host, port)
      ctx = OpenSSL::SSL::SSLContext.new
      ctx.cert = OpenSSL::X509::Certificate.new(File.open(cert_path)) if cert_path

      cert_store = OpenSSL::X509::Store.new
      cert_store.set_default_paths
      cert_store.add_file(ca_path) if ca_path
      ctx.cert_store = cert_store

      ctx.key = OpenSSL::PKey::RSA.new(File.open(private_key_path)) if private_key_path
      ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
      ctx.verify_hostname = false

      @server = OpenSSL::SSL::SSLServer.new(server, ctx)
      @thread = nil
      @r, @w = IO.pipe
    end

    def start
      do_start

      if block_given?
        begin
          yield
          @thread.join(5)
        ensure
          stop
        end
      end
    end

    def stop
      unless @w.closed?
        @w.write('stop')
      end

      [@server, @w, @r].each do |s|
        next if s.closed?
        s.close
      end

      @thread.join(5)
    end

    private

    def do_start
      @thread = Thread.new(@server) do |s|
        socks, _, _ = IO.select([s.accept, @r], nil, nil)

        if socks.include?(@r)
          break
        end

        sock = socks.first
        buf = +''
        loop do
          b = sock.read_nonblock(1024, nil, exception: false)
          if b == :wait_readable || b.nil?
            break
          end
          buf << b
        end

        sock.write(buf)
        sock.close
      end
    end
  end

  test 'with self-signed cert/key pair' do
    cert_path = File.join(CERT_DIR, 'cert.pem')
    private_key_path = File.join(CERT_DIR, 'cert-key.pem')

    EchoTLSServer.new(@port, cert_path: cert_path, private_key_path: private_key_path).start do
      client = SocketHelperTestPlugin.new.socket_create_tls('127.0.0.1', @port, verify_fqdn: false, cert_paths: [cert_path])
      client.write('hello')
      assert_equal 'hello', client.readpartial(100)
      client.close
    end
  end

  test 'with cert/key signed by self-signed CA' do
    cert_path = File.join(CA_CERT_DIR, 'cert.pem')
    private_key_path = File.join(CA_CERT_DIR, 'cert-key.pem')

    ca_cert_path = File.join(CA_CERT_DIR, 'ca-cert.pem')

    EchoTLSServer.new(@port, cert_path: cert_path, private_key_path: private_key_path).start do
      client = SocketHelperTestPlugin.new.socket_create_tls('127.0.0.1', @port, verify_fqdn: false, cert_paths: [ca_cert_path])
      client.write('hello')
      assert_equal 'hello', client.readpartial(100)
      client.close
    end
  end

  test 'with cert/key signed by self-signed CA in server and client cert chain' do
    cert_path = File.join(CERT_DIR, 'cert.pem')
    private_key_path = File.join(CERT_DIR, 'cert-key.pem')

    client_ca_cert_path = File.join(CERT_CHAINS_DIR, 'ca-cert.pem')
    client_cert_path = File.join(CERT_CHAINS_DIR, 'cert.pem')
    client_private_key_path = File.join(CERT_CHAINS_DIR, 'cert-key.pem')

    EchoTLSServer.new(@port, cert_path: cert_path, private_key_path: private_key_path, ca_path: client_ca_cert_path).start do
      client = SocketHelperTestPlugin.new.socket_create_tls('127.0.0.1', @port, verify_fqdn: false, cert_path: client_cert_path, private_key_path: client_private_key_path, cert_paths: [cert_path])
      client.write('hello')
      assert_equal 'hello', client.readpartial(100)
      client.close
    end
  end

  test 'with empty cert file' do
    cert_path = File.expand_path(File.dirname(__FILE__) + '/data/cert/empty.pem')

    assert_raise Fluent::ConfigError do
      SocketHelperTestPlugin.new.socket_create_tls('127.0.0.1', @port, cert_path: cert_path)
    end
  end
end