diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index dded6cc..2266448 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -39,6 +39,8 @@ jobs: - '3.2' os: - ubuntu-20.04 + - windows-2019 + - macos-11 - ubuntu-latest exclude: - { os: ubuntu-latest, ruby: '2.7' } @@ -52,7 +54,7 @@ jobs: name: ${{ matrix.os }} - Ruby ${{ matrix.ruby }} - ${{ matrix.test_cmd }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/lib/rex/socket.rb b/lib/rex/socket.rb index f58b28e..58e3e61 100644 --- a/lib/rex/socket.rb +++ b/lib/rex/socket.rb @@ -672,20 +672,27 @@ def self.ipv6_mac(intf) # is no concurrent use of the same locals and this is safe. def self.tcp_socket_pair lsock = nil + last_child_error = nil + accept_timeout = 10 rsock = nil laddr = '127.0.0.1' lport = 0 threads = [] mutex = ::Mutex.new - threads << Rex::ThreadFactory.spawn('TcpSocketPair', false) { + threads << Rex::ThreadFactory.spawn('TcpSocketPair', false) do server = nil - mutex.synchronize { - threads << Rex::ThreadFactory.spawn('TcpSocketPairClient', false) { - mutex.synchronize { - rsock = ::TCPSocket.new( laddr, lport ) - } - } + mutex.synchronize do + threads << Rex::ThreadFactory.spawn('TcpSocketPairClient', false) do + mutex.synchronize do + begin + rsock = ::TCPSocket.new( laddr, lport ) + rescue => e + last_child_error = "#{e.class} - #{e.message}" + raise + end + end + end server = ::TCPServer.new(laddr, 0) if (server.getsockname =~ /127\.0\.0\.1:/) # JRuby ridiculousness @@ -697,12 +704,23 @@ def self.tcp_socket_pair # sockaddr lport, caddr = ::Socket.unpack_sockaddr_in( server.getsockname ) end - } - lsock, _ = server.accept + end + + readable, _writable, _errors = ::IO.select([server], nil, nil, accept_timeout) + if readable && readable.any? + lsock, _ = server.accept + else + raise RuntimeError, "rsock didn't connect in #{accept_timeout} seconds" + end + server.close - } + end - threads.each { |t| t.join } + threads.each do |thread| + thread.join + rescue => e + raise "Error #{e} - last child error: #{last_child_error}" + end return [lsock, rsock] end diff --git a/spec/rex/socket_spec.rb b/spec/rex/socket_spec.rb index ac8db69..3d2d3af 100644 --- a/spec/rex/socket_spec.rb +++ b/spec/rex/socket_spec.rb @@ -4,6 +4,44 @@ RSpec.describe Rex::Socket do + describe '.tcp_socket_pair' do + let(:mock_thread_factory) do + double :mock_thread_factory + end + + before(:each) do + stub_const('Rex::ThreadFactory', mock_thread_factory) + # Fallback implementation from https://github.com/rapid7/metasploit-framework/blob/30e66c43a4932df922d7f1d986fb98387bd0ab1a/lib/rex/thread_factory.rb#L27-L37 + allow(mock_thread_factory).to receive(:spawn) do |name, crit, *args, &block| + if block + t = ::Thread.new(*args){ |*args_copy| block.call(*args_copy) } + else + t = ::Thread.new(*args) + end + t[:tm_name] = name + t[:tm_crit] = crit + t[:tm_time] = ::Time.now + t[:tm_call] = caller + t + end + end + + it 'creates two socket pairs' do + lsock, rsock = described_class.tcp_socket_pair + lsock.extend(Rex::IO::Stream) + rsock.extend(Rex::IO::Stream) + + expect(lsock.closed?).to be(false) + expect(rsock.closed?).to be(false) + + lsock.close + rsock.close + + expect(lsock.closed?).to be(true) + expect(rsock.closed?).to be(true) + end + end + describe '.addr_itoa' do context 'with explicit v6' do