From 082d99f4789c3619dde92d225bae78f774cf2f3e Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Fri, 3 Jul 2026 11:25:39 +0100 Subject: [PATCH] Resolve test failures for c2 branch --- .../workflows/command_shell_acceptance.yml | 21 +++--- .../shared_meterpreter_acceptance.yml | 29 ++++---- Gemfile.lock | 8 +- lib/msf/base/sessions/mettle_config.rb | 2 +- lib/msf/core/handler/reverse_http.rb | 10 +-- .../core/payload/java/meterpreter_loader.rb | 4 +- lib/msf/core/payload/malleable_c2.rb | 4 +- lib/rex/proto/http/request.rb | 2 +- lib/rex/proto/http/server.rb | 2 +- metasploit-framework.gemspec | 4 +- .../singles/php/meterpreter_reverse_http.rb | 2 +- .../singles/php/meterpreter_reverse_https.rb | 2 +- spec/modules/payloads_spec.rb | 74 +++++++++++++++++++ spec/support/acceptance/session/php.rb | 4 +- test/modules/post/test/socket_channels.rb | 74 ++++++++++++++----- 15 files changed, 180 insertions(+), 62 deletions(-) diff --git a/.github/workflows/command_shell_acceptance.yml b/.github/workflows/command_shell_acceptance.yml index 0907f22e2b071..a4c26df5ed08a 100644 --- a/.github/workflows/command_shell_acceptance.yml +++ b/.github/workflows/command_shell_acceptance.yml @@ -109,19 +109,22 @@ jobs: tools: none - name: Install system dependencies (Windows) - shell: cmd + shell: pwsh if: runner.os == 'Windows' run: | - REM pcap dependencies - powershell -Command "[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} ; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip', 'C:\Windows\Temp\WpdPack_4_1_2.zip')" + $ErrorActionPreference = 'Stop' + # pcap dependencies + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri 'https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip' -OutFile 'C:\Windows\Temp\WpdPack_4_1_2.zip' -SkipCertificateCheck + $expected = 'ea799cf2f26e4afb1892938070fd2b1ca37ce5cf75fec4349247df12b784edbd' + $actual = (Get-FileHash 'C:\Windows\Temp\WpdPack_4_1_2.zip' -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $expected) { throw "WpdPack_4_1_2.zip SHA256 mismatch: expected $expected, got $actual" } - choco install 7zip.installServerCertificateValidationCallback - 7z x "C:\Windows\Temp\WpdPack_4_1_2.zip" -o"C:\" - - dir C:\\ + choco install 7zip -y --no-progress + if ($LASTEXITCODE -ne 0) { throw "choco install 7zip failed with exit code $LASTEXITCODE" } - dir %WINDIR% - type %WINDIR%\\system32\\drivers\\etc\\hosts + 7z x "C:\Windows\Temp\WpdPack_4_1_2.zip" -o"C:\" + if ($LASTEXITCODE -ne 0) { throw "7z extraction failed with exit code $LASTEXITCODE" } # The job checkout structure is: # . diff --git a/.github/workflows/shared_meterpreter_acceptance.yml b/.github/workflows/shared_meterpreter_acceptance.yml index 6cfdab19875b7..a5ead47544831 100644 --- a/.github/workflows/shared_meterpreter_acceptance.yml +++ b/.github/workflows/shared_meterpreter_acceptance.yml @@ -30,11 +30,11 @@ on: type: boolean jobs: - # Compile the Meterpreter payloads via docker if required, we can't always do this on the - # host environment (i.e. for macos). So it instead gets compiled first on a linux - # host, then the artifacts are copied back to the host later + # Compile Java Meterpreter and prepare PHP/Python payloads via docker if required, we can't + # always do this on the host environment (i.e. for macos). So it instead gets compiled first + # on a linux host, then the artifacts are copied back to the host later meterpreter_compilation: - name: Compile Meterpreter + name: Build Meterpreter Gem (optional) runs-on: ubuntu-latest if: ${{ inputs.build_metasploit_payloads }} @@ -141,19 +141,22 @@ jobs: java-version: ${{ matrix.meterpreter.runtime_version }} - name: Install system dependencies (Windows) - shell: cmd + shell: pwsh if: runner.os == 'Windows' run: | - REM pcap dependencies - powershell -Command "[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} ; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip', 'C:\Windows\Temp\WpdPack_4_1_2.zip')" + $ErrorActionPreference = 'Stop' + # pcap dependencies + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri 'https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip' -OutFile 'C:\Windows\Temp\WpdPack_4_1_2.zip' -SkipCertificateCheck + $expected = 'ea799cf2f26e4afb1892938070fd2b1ca37ce5cf75fec4349247df12b784edbd' + $actual = (Get-FileHash 'C:\Windows\Temp\WpdPack_4_1_2.zip' -Algorithm SHA256).Hash.ToLower() + if ($actual -ne $expected) { throw "WpdPack_4_1_2.zip SHA256 mismatch: expected $expected, got $actual" } - choco install 7zip.installServerCertificateValidationCallback - 7z x "C:\Windows\Temp\WpdPack_4_1_2.zip" -o"C:\" + choco install 7zip -y --no-progress + if ($LASTEXITCODE -ne 0) { throw "choco install 7zip failed with exit code $LASTEXITCODE" } - dir C:\\ - - dir %WINDIR% - type %WINDIR%\\system32\\drivers\\etc\\hosts + 7z x "C:\Windows\Temp\WpdPack_4_1_2.zip" -o"C:\" + if ($LASTEXITCODE -ne 0) { throw "7z extraction failed with exit code $LASTEXITCODE" } # The job checkout structure is: # . diff --git a/Gemfile.lock b/Gemfile.lock index 35074de0b00cb..7c45ad9595080 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,9 +61,9 @@ PATH metasploit-concern metasploit-credential (>= 6.0.21) metasploit-model - metasploit-payloads (= 2.0.245) + metasploit-payloads (= 2.0.246.pre.7) metasploit_data_models (>= 6.0.15) - metasploit_payloads-mettle (= 1.0.46) + metasploit_payloads-mettle (= 1.0.48.pre.3) mqtt msgpack (~> 1.6.0) mutex_m @@ -369,7 +369,7 @@ GEM drb mutex_m railties (>= 7.0, < 8.1) - metasploit-payloads (2.0.245) + metasploit-payloads (2.0.246.pre.7) metasploit_data_models (6.0.18) activerecord (>= 7.0, < 8.1) activesupport (>= 7.0, < 8.1) @@ -383,7 +383,7 @@ GEM railties (>= 7.0, < 8.1) recog webrick - metasploit_payloads-mettle (1.0.46) + metasploit_payloads-mettle (1.0.48.pre.3) method_source (1.1.0) mime-types (3.7.0) logger diff --git a/lib/msf/base/sessions/mettle_config.rb b/lib/msf/base/sessions/mettle_config.rb index 50fb6c252c4ea..cff585cc9fca4 100644 --- a/lib/msf/base/sessions/mettle_config.rb +++ b/lib/msf/base/sessions/mettle_config.rb @@ -135,7 +135,7 @@ def generate_config(opts = {}) # Mettle reserves a fixed 8 KB slot in the binary for the config # block. Catch the overflow here with a useful message instead of # letting the gem's `to_binary` raise a generic "config block too - # large" — baked-in EXTENSIONS= is the usual culprit. + # large" -- baked-in EXTENSIONS= is the usual culprit. if opts[:config_block].length > MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX raise ArgumentError, "Mettle config block (#{opts[:config_block].length} bytes) exceeds the #{MetasploitPayloads::Mettle::CONFIG_BLOCK_MAX}-byte embedded slot. " \ "Drop EXTENSIONS= and `load ` once the session is up." diff --git a/lib/msf/core/handler/reverse_http.rb b/lib/msf/core/handler/reverse_http.rb index bbbf25c1b993d..b64311139ed19 100644 --- a/lib/msf/core/handler/reverse_http.rb +++ b/lib/msf/core/handler/reverse_http.rb @@ -287,14 +287,14 @@ def setup_handler # # * query parameter (profile: `parameter "name";`) # * request header (profile: `header "name";`) - # * trailing path seg (default — no placement directive) + # * trailing path seg (default -- no placement directive) # # For the path case, the URI looks like `/`, where `` # is the profile's per-verb `set uri` if defined, otherwise `LURI` # (each is registered as a separate mount point in `all_uris`). # Taking the last `/`-separated segment skips the base regardless of # which one was used. Profile authors should not put `/` in - # prepend/append directives — that would split the id across segments + # prepend/append directives -- that would split the id across segments # and defeat this scheme. # # If the profile applied `prepend` / `append` / `base64` / `base64url` @@ -325,7 +325,7 @@ def find_resource_id(cli, request) # Reverse the prepend/append + base64 transforms the profile applied # to the id on the payload side. If a declared wrapper is missing from - # the candidate, leave the candidate alone — this is not a payload + # the candidate, leave the candidate alone -- this is not a payload # request, and `process_uri_resource` will return nil for it. def unwrap_profile_uuid(candidate, placement) prefix = placement.prepend.map{|d| d.args[0]}.join('') @@ -357,8 +357,8 @@ def add_response_headers(req, resp) # Return the live meterpreter session whose passive dispatcher is # registered for this bare conn_id, or nil if none matches. Used so - # MC2 traffic — which Rex routes to on_request because the profile - # URI prefix outranks the session's / mount — can still be + # MC2 traffic -- which Rex routes to on_request because the profile + # URI prefix outranks the session's / mount -- can still be # delivered to the right session instead of triggering a fresh # "orphaned attach" on every poll. def session_for_conn_id(bare_conn_id) diff --git a/lib/msf/core/payload/java/meterpreter_loader.rb b/lib/msf/core/payload/java/meterpreter_loader.rb index 7ec6087e6ce25..969e0bd59b64f 100644 --- a/lib/msf/core/payload/java/meterpreter_loader.rb +++ b/lib/msf/core/payload/java/meterpreter_loader.rb @@ -18,7 +18,7 @@ module Payload::Java::MeterpreterLoader include Msf::Sessions::MeterpreterOptions::Java # Resource path the stageless StagelessMain bootstrap reads. Deliberately - # innocuous — no meterpreter/metasploit markers in the name. + # innocuous -- no meterpreter/metasploit markers in the name. STAGELESS_CONFIG_RESOURCE = 'META-INF/data'.freeze STAGELESS_MAIN_CLASS = 'com.metasploit.meterpreter.StagelessMain'.freeze @@ -44,7 +44,7 @@ def stage_payload(opts={}) # # When opts[:stageless] is set, returns a self-contained jar with the # TLV config embedded as a resource and Main-Class pinned to - # StagelessMain — ready to run under `java -jar`. + # StagelessMain -- ready to run under `java -jar`. # def stage_meterpreter(opts={}) met = MetasploitPayloads.read('meterpreter', 'meterpreter.jar') diff --git a/lib/msf/core/payload/malleable_c2.rb b/lib/msf/core/payload/malleable_c2.rb index 33494c3be5113..e905c65f4d093 100644 --- a/lib/msf/core/payload/malleable_c2.rb +++ b/lib/msf/core/payload/malleable_c2.rb @@ -115,7 +115,7 @@ def tokenize(text) # blank line next elsif scanner.scan(/#[^\n]*/) - # comment — anchorless so it matches indented comments too + # comment -- anchorless so it matches indented comments too # (the prior `\s+` rule has already eaten any leading # whitespace by the time we get here) next @@ -450,7 +450,7 @@ def parse(file) profile.sets << parse_set elsif block_ahead? # Any `name { ... }` is a block. We keep the ones we understand - # (http-get/http-post/etc.) and harmlessly retain the rest — + # (http-get/http-post/etc.) and harmlessly retain the rest -- # unsupported CS blocks like dns-beacon/process-inject/post-ex # are parsed for structure and simply ignored by to_tlv. profile.sections << parse_section diff --git a/lib/rex/proto/http/request.rb b/lib/rex/proto/http/request.rb index bf21fdf42d656..388ac20b9be6f 100644 --- a/lib/rex/proto/http/request.rb +++ b/lib/rex/proto/http/request.rb @@ -219,7 +219,7 @@ def to_s # # Returns a hijacked version of the body that shoves the request's query string in as a - # replacement in cases where there is no body. YOLO! ¯\_(ツ)_/¯ + # replacement in cases where there is no body. YOLO! (shrug) # def body str = super || '' diff --git a/lib/rex/proto/http/server.rb b/lib/rex/proto/http/server.rb index bc4d71fa30476..2ebf34a80292d 100644 --- a/lib/rex/proto/http/server.rb +++ b/lib/rex/proto/http/server.rb @@ -294,7 +294,7 @@ def dispatch_request(cli, request) p = resources[request.resource] # This branch is reached when resource_id is nil (e.g. fetch-payload # adapters have no find_resource_id), so the matched resource is the - # request resource itself — not resource_id, which would nil-deref. + # request resource itself -- not resource_id, which would nil-deref. len = request.resource.length root = request.resource else diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index ee0a73ed8b027..efd66fd6aafc8 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -74,9 +74,9 @@ Gem::Specification.new do |spec| # are needed when there's no database spec.add_runtime_dependency 'metasploit-model' # Needed for Meterpreter - spec.add_runtime_dependency 'metasploit-payloads', '2.0.245' + spec.add_runtime_dependency 'metasploit-payloads', '2.0.246.pre.7' # Needed for the next-generation POSIX Meterpreter - spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.46' + spec.add_runtime_dependency 'metasploit_payloads-mettle', '1.0.48.pre.3' # Needed by msfgui and other rpc components # Locked until build env can handle newer version. See: https://github.com/msgpack/msgpack-ruby/issues/334 spec.add_runtime_dependency 'msgpack', '~> 1.6.0' diff --git a/modules/payloads/singles/php/meterpreter_reverse_http.rb b/modules/payloads/singles/php/meterpreter_reverse_http.rb index 9fabbf97a740a..86337a446ff33 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_http.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_http.rb @@ -10,7 +10,7 @@ module MetasploitModule include Msf::Payload::Php::ReverseHttp include Msf::Payload::TransportConfig include Msf::Payload::UUID::Options - include Msf::Sessions::MeterpreterOptions + include Msf::Sessions::MeterpreterOptions::Php def initialize(info = {}) super( diff --git a/modules/payloads/singles/php/meterpreter_reverse_https.rb b/modules/payloads/singles/php/meterpreter_reverse_https.rb index 8729897636d24..c6d08b4a3152d 100644 --- a/modules/payloads/singles/php/meterpreter_reverse_https.rb +++ b/modules/payloads/singles/php/meterpreter_reverse_https.rb @@ -10,7 +10,7 @@ module MetasploitModule include Msf::Payload::Php::ReverseHttp include Msf::Payload::TransportConfig include Msf::Payload::UUID::Options - include Msf::Sessions::MeterpreterOptions + include Msf::Sessions::MeterpreterOptions::Php def initialize(info = {}) super( diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 5e9c52b309d22..74a95ea79fbf3 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -1712,6 +1712,42 @@ reference_name: 'java/shell_reverse_tcp' end + context 'java/meterpreter_bind_tcp' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_bind_tcp' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_bind_tcp' + end + + context 'java/meterpreter_reverse_http' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_reverse_http' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_reverse_http' + end + + context 'java/meterpreter_reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_reverse_https' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_reverse_https' + end + + context 'java/meterpreter_reverse_tcp' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/java/meterpreter_reverse_tcp' + ], + modules_pathname: modules_pathname, + reference_name: 'java/meterpreter_reverse_tcp' + end + context 'linux/aarch64/chmod' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -2987,6 +3023,44 @@ reference_name: 'php/meterpreter_reverse_tcp' end + context 'php/meterpreter_reverse_http' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/php/meterpreter_reverse_http' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter_reverse_http' + end + + context 'php/meterpreter_reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/php/meterpreter_reverse_https' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter_reverse_https' + end + + context 'php/meterpreter/reverse_http' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/php/reverse_http', + 'stages/php/meterpreter' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter/reverse_http' + end + + context 'php/meterpreter/reverse_https' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/php/reverse_https', + 'stages/php/meterpreter' + ], + modules_pathname: modules_pathname, + reference_name: 'php/meterpreter/reverse_https' + end + context 'php/reverse_php' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ diff --git a/spec/support/acceptance/session/php.rb b/spec/support/acceptance/session/php.rb index cdb04ba9566e3..f36e791ede2e4 100644 --- a/spec/support/acceptance/session/php.rb +++ b/spec/support/acceptance/session/php.rb @@ -144,9 +144,7 @@ module Acceptance::Session::Php known_failures: [] }, osx: { - known_failures: [ - "[-] FAILED: should return a list of processes" - ] + known_failures: [] }, windows: { known_failures: [] diff --git a/test/modules/post/test/socket_channels.rb b/test/modules/post/test/socket_channels.rb index 1f7e8387758fb..b46d662269db0 100644 --- a/test/modules/post/test/socket_channels.rb +++ b/test/modules/post/test/socket_channels.rb @@ -27,6 +27,7 @@ def initialize(info = {}) [ OptAddressLocal.new('LHOST', [true, 'The local IP address to use for binding.', '127.0.0.1']), OptAddress.new('RHOST', [true, 'The remote IP address to use for binding.', '127.0.0.1']), + OptInt.new('TIMEOUT', [true, 'The timeout in seconds to use when waiting for socket operations.', ENV['CI'] ? 30 : 5]), ], self.class ) end @@ -41,7 +42,7 @@ def run super end - def tcp_client_socket_pair(params={}, timeout: 5) + def tcp_client_socket_pair(params={}, timeout: datastore['TIMEOUT']) params = Rex::Socket::Parameters.new('Proto' => 'tcp', 'PeerHost' => datastore['LHOST'], **params) server = TCPSocketServer.new(host: params.peerhost, port: params.peerport) @@ -52,7 +53,7 @@ def tcp_client_socket_pair(params={}, timeout: 5) [client, server_client] end - def tcp_server_socket_trio(params={}, timeout: 5) + def tcp_server_socket_trio(params={}, timeout: datastore['TIMEOUT']) params = Rex::Socket::Parameters.new('Proto' => 'tcp', 'LocalHost' => datastore['RHOST'], 'Server' => true, **params) server = session.create(params) @@ -64,7 +65,7 @@ def tcp_server_socket_trio(params={}, timeout: 5) [client, server_client, server] end - def tcp_server_socket_pair(params={}, timeout: 5) + def tcp_server_socket_pair(params={}, timeout: datastore['TIMEOUT']) client, server_client, server = tcp_server_socket_trio(params, timeout: timeout) server.close [client, server_client] @@ -125,7 +126,10 @@ def test_tcp_client_channel it '[TCP-Client] Propagates close events to the peer' do client, server_client = tcp_client_socket_pair client.close - ret = retry_until_truthy(timeout: 5) { server_client.eof? } + # Use IO.select with a short poll interval to avoid blocking indefinitely on eof? + ret = retry_until_truthy(timeout: datastore['TIMEOUT']) do + IO.select([server_client], nil, nil, 0.1) && server_client.eof? + end server_client.close ret end @@ -135,7 +139,7 @@ def test_tcp_client_channel server_client.close # this behavior is wrong, it should just be an EOF, but when the channel is cleaned up, the socket is closed # this is how it's worked for years, so we'll test that it's still consistently wrong - retry_until_truthy(timeout: 5) { client.closed? } + retry_until_truthy(timeout: datastore['TIMEOUT']) { client.closed? } end end @@ -195,7 +199,7 @@ def test_tcp_server_channel server.close # Try to connect a new client - should fail since server is closed - ret = retry_until_truthy(timeout: 5) do + ret = retry_until_truthy(timeout: datastore['TIMEOUT']) do begin new_client = TCPSocket.new(server.params.localhost, server.params.localport) new_client.close @@ -214,7 +218,10 @@ def test_tcp_server_channel it '[TCP-Server] Propagates close events to the peer' do client, server_client = tcp_server_socket_pair server_client.close - ret = retry_until_truthy(timeout: 5) { client.eof? } + # Use IO.select with a short poll interval to avoid blocking indefinitely on eof? + ret = retry_until_truthy(timeout: datastore['TIMEOUT']) do + IO.select([client], nil, nil, 0.1) && client.eof? + end client.close ret end @@ -224,7 +231,7 @@ def test_tcp_server_channel client.close # this behavior is wrong, it should just be an EOF, but when the channel is cleaned up, the socket is closed # this is how it's worked for years, so we'll test that it's still consistently wrong - ret = retry_until_truthy(timeout: 5) { server_client.closed? } + ret = retry_until_truthy(timeout: datastore['TIMEOUT']) { server_client.closed? } ret end end @@ -252,11 +259,32 @@ def test_udp_channel data = Random.new.bytes(rand(10..100)) # Now server can send to the client's address server_client.send(data, 0, client.localhost, client.localport) - _, addrinfo = client.recvfrom(data.length) - ret = addrinfo.is_a?(Array) - ret &&= addrinfo[0] == 'AF_INET' - ret &&= addrinfo[1] == server_client.local_address.ip_port - ret &&= addrinfo[3] == server_client.local_address.ip_address + ret = IO.select([client], nil, nil, datastore['TIMEOUT']) + if ret + _, addrinfo = client.recvfrom(data.length) + expected_family = 'AF_INET' + expected_port = server_client.local_address.ip_port + expected_addr = server_client.local_address.ip_address + ret = addrinfo.is_a?(Array) + unless ret + print_error("Expected addrinfo to be an Array, got #{addrinfo.class}") + end + ret &&= addrinfo[0] == expected_family + unless addrinfo[0] == expected_family + print_error("Expected address family #{expected_family.inspect}, got #{addrinfo[0].inspect}") + end + ret &&= addrinfo[1] == expected_port + unless addrinfo[1] == expected_port + print_error("Expected peer port #{expected_port}, got #{addrinfo[1].inspect}") + end + ret &&= addrinfo[3] == expected_addr + unless addrinfo[3] == expected_addr + print_error("Expected peer address #{expected_addr.inspect}, got #{addrinfo[3].inspect}") + end + else + print_error("Timed out after #{datastore['TIMEOUT']}s waiting to receive data from peer") + ret = false + end client.close server_client.close ret @@ -266,8 +294,14 @@ def test_udp_channel client, server_client = udp_socket_pair data = Random.new.bytes(rand(10..100)) server_client.send(data, 0, client.localhost, client.localport) - received, _ = client.recvfrom(data.length) - ret = received == data + ret = IO.select([client], nil, nil, datastore['TIMEOUT']) + if ret + received, _ = client.recvfrom(data.length) + ret = received == data + else + print_error("Timed out after #{datastore['TIMEOUT']}s waiting to receive data from peer") + ret = false + end client.close server_client.close ret @@ -277,8 +311,14 @@ def test_udp_channel client, server_client = udp_socket_pair data = Random.new.bytes(rand(10..100)) client.send(data, 0, server_client.local_address.ip_address, server_client.local_address.ip_port) - received, _ = server_client.recvfrom(data.length) - ret = received == data + ret = IO.select([server_client], nil, nil, datastore['TIMEOUT']) + if ret + received, _ = server_client.recvfrom(data.length) + ret = received == data + else + print_error("Timed out after #{datastore['TIMEOUT']}s waiting to receive data from peer") + ret = false + end client.close server_client.close ret