From 9105ba7afb263f1bc2942ff6c8ae6bed5d805788 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 15 May 2026 16:39:22 -0400 Subject: [PATCH 1/5] Change the return value of #recvfrom to match stdlib --- lib/rex/socket/udp.rb | 50 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/rex/socket/udp.rb b/lib/rex/socket/udp.rb index 58ba2c0..4ef9398 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -117,37 +117,37 @@ def sendto(gram, peerhost, peerport, flags = 0) end # - # Receives a datagram and returns the data and host:port of the requestor - # as [ data, host, port ]. - # - def recvfrom(length = 65535, timeout=def_read_timeout) - - begin - if ((rv = ::IO.select([ fd ], nil, nil, timeout)) and - (rv[0]) and (rv[0][0] == fd) - ) - data, saddr = recvfrom_nonblock(length) - af, host, port = Rex::Socket.from_sockaddr(saddr) - - return [ data, host, port ] - else - return [ '', nil, nil ] - end - rescue ::Timeout::Error - return [ '', nil, nil ] - rescue ::Interrupt - raise $! - rescue ::Exception - return [ '', nil, nil ] - end + # Receives a datagram and returns the data and sender address information + # as [ data, [address_family, port, host, host] ], matching the format of + # stdlib UDPSocket#recvfrom (host appears in both the hostname and numeric + # address positions; no reverse-DNS lookup is performed). + # + # @param maxlen [Integer] maximum number of bytes to receive + # @param timeout [Numeric] seconds to wait before raising Errno::EAGAIN + # (default: def_read_timeout = 10). NOTE: this parameter was previously + # named +flags+ and defaulted to 0; callers that passed flags=0 explicitly + # will now receive a 0-second (poll) receive instead of a 10-second wait. + # + def recvfrom(maxlen, timeout = def_read_timeout) + rv = ::IO.select([ fd ], nil, nil, timeout) + + raise Errno::EAGAIN, "Resource temporarily unavailable" if rv.nil? + + data, saddr = recvfrom_nonblock(maxlen) + af, host, port = Rex::Socket.from_sockaddr(saddr) + af_name = Socket.constants.grep(/^AF_/).find { |c| Socket.const_get(c) == af }.to_s + [data, [af_name, port, host, host]] + rescue ::Timeout::Error + raise Errno::EAGAIN, "Resource temporarily unavailable" + rescue ::Interrupt + raise end # # Calls recvfrom and only returns the data # def get(timeout=nil) - data, saddr, sport = recvfrom(65535, timeout) - return data + timed_read(65535, timeout) end # From 44ecc273ebc48bd92be5c7e51c3c372ebdcde6ce Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 15 May 2026 17:56:37 -0400 Subject: [PATCH 2/5] The second arg fto #recvfrom should be flags --- lib/rex/socket/udp.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/rex/socket/udp.rb b/lib/rex/socket/udp.rb index 4ef9398..f71c5f2 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -123,24 +123,19 @@ def sendto(gram, peerhost, peerport, flags = 0) # address positions; no reverse-DNS lookup is performed). # # @param maxlen [Integer] maximum number of bytes to receive - # @param timeout [Numeric] seconds to wait before raising Errno::EAGAIN - # (default: def_read_timeout = 10). NOTE: this parameter was previously - # named +flags+ and defaulted to 0; callers that passed flags=0 explicitly - # will now receive a 0-second (poll) receive instead of a 10-second wait. + # @param flags [Integer] flags passed to the underlying recvfrom(2) call (default: 0) # - def recvfrom(maxlen, timeout = def_read_timeout) - rv = ::IO.select([ fd ], nil, nil, timeout) + def recvfrom(maxlen, flags = 0) + rv = ::IO.select([ fd ], nil, nil, def_read_timeout) raise Errno::EAGAIN, "Resource temporarily unavailable" if rv.nil? - data, saddr = recvfrom_nonblock(maxlen) + data, saddr = recvfrom_nonblock(maxlen, flags) af, host, port = Rex::Socket.from_sockaddr(saddr) af_name = Socket.constants.grep(/^AF_/).find { |c| Socket.const_get(c) == af }.to_s [data, [af_name, port, host, host]] rescue ::Timeout::Error raise Errno::EAGAIN, "Resource temporarily unavailable" - rescue ::Interrupt - raise end # From 1c1e60bb77abd786ec0c3b35e735a6a8f58a057c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 10 Jun 2026 16:19:02 -0400 Subject: [PATCH 3/5] Align #recvfrom with stdlib and add #timed_recvfrom Make Rex::Socket::Udp#recvfrom a true drop-in for stdlib UDPSocket#recvfrom: it now blocks until a datagram arrives (no internal def_read_timeout / Errno::EAGAIN deadline) and returns [ data, [address_family, port, host, host] ]. Callers that need a deadline use the new #timed_recvfrom, which returns the same stdlib-shaped tuple and nil when the timeout elapses -- mirroring the read/timed_read pair. #timed_read is left as-is. Co-Authored-By: Claude Opus 4.8 --- lib/rex/socket/udp.rb | 50 +++++++++++++++++++++++------- spec/rex/socket/udp_spec.rb | 61 +++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 spec/rex/socket/udp_spec.rb diff --git a/lib/rex/socket/udp.rb b/lib/rex/socket/udp.rb index f71c5f2..5f3f717 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -117,26 +117,54 @@ def sendto(gram, peerhost, peerport, flags = 0) end # - # Receives a datagram and returns the data and sender address information - # as [ data, [address_family, port, host, host] ], matching the format of - # stdlib UDPSocket#recvfrom (host appears in both the hostname and numeric - # address positions; no reverse-DNS lookup is performed). + # Receives a datagram and returns the data and sender address information as + # [ data, [address_family, port, host, host] ], matching stdlib + # UDPSocket#recvfrom. Like the stdlib method, this blocks until a datagram is + # available and has no timeout of its own (see #timed_recvfrom for a variant + # that does). The host appears in both the hostname and numeric address + # positions; no reverse-DNS lookup is performed. # # @param maxlen [Integer] maximum number of bytes to receive # @param flags [Integer] flags passed to the underlying recvfrom(2) call (default: 0) + # @return [Array(String, Array)] the datagram and the sender address information # def recvfrom(maxlen, flags = 0) - rv = ::IO.select([ fd ], nil, nil, def_read_timeout) + # Block until the socket is readable to mirror the stdlib's blocking + # UDPSocket#recvfrom; a nil timeout waits indefinitely. + ::IO.select([ fd ], nil, nil, nil) + data, saddr = recvfrom_nonblock(maxlen, flags) + [ data, sender_addr_info(saddr) ] + end - raise Errno::EAGAIN, "Resource temporarily unavailable" if rv.nil? + # + # Receives a datagram like #recvfrom but waits at most +timeout+ seconds for + # one to arrive, returning nil if the timeout elapses first. The return value + # otherwise matches #recvfrom: [ data, [address_family, port, host, host] ]. + # + # @param maxlen [Integer] maximum number of bytes to receive + # @param timeout [Numeric] seconds to wait for a datagram before giving up + # @return [Array(String, Array), nil] the datagram and sender address + # information, or nil if no datagram arrived within +timeout+ seconds + # + def timed_recvfrom(maxlen = 65535, timeout = def_read_timeout) + return nil unless ::IO.select([ fd ], nil, nil, timeout) - data, saddr = recvfrom_nonblock(maxlen, flags) - af, host, port = Rex::Socket.from_sockaddr(saddr) - af_name = Socket.constants.grep(/^AF_/).find { |c| Socket.const_get(c) == af }.to_s - [data, [af_name, port, host, host]] + data, saddr = recvfrom_nonblock(maxlen) + [ data, sender_addr_info(saddr) ] rescue ::Timeout::Error - raise Errno::EAGAIN, "Resource temporarily unavailable" + nil + end + + # + # Converts a packed sockaddr into the stdlib UDPSocket#recvfrom-style sender + # address tuple [ address_family, port, host, host ]. + # + def sender_addr_info(saddr) + af, host, port = Rex::Socket.from_sockaddr(saddr) + af_name = ::Socket.constants.grep(/^AF_/).find { |c| ::Socket.const_get(c) == af }.to_s + [ af_name, port, host, host ] end + private :sender_addr_info # # Calls recvfrom and only returns the data diff --git a/spec/rex/socket/udp_spec.rb b/spec/rex/socket/udp_spec.rb new file mode 100644 index 0000000..d6c4ab4 --- /dev/null +++ b/spec/rex/socket/udp_spec.rb @@ -0,0 +1,61 @@ +# -*- coding:binary -*- +require 'spec_helper' + +RSpec.describe Rex::Socket::Udp do + let(:loopback) { '127.0.0.1' } + + def make_server + Rex::Socket::Udp.create('LocalHost' => loopback, 'LocalPort' => 0) + end + + def make_client(port) + Rex::Socket::Udp.create('PeerHost' => loopback, 'PeerPort' => port) + end + + describe '#recvfrom' do + it 'returns the data and a stdlib-style sender address tuple' do + server = make_server + client = make_client(server.local_address.ip_port) + client.write('hello') + + data, addr = server.recvfrom(65535) + expect(data).to eq('hello') + expect(addr).to be_an(Array) + expect(addr.length).to eq(4) + + af_name, port, host, numeric = addr + expect(af_name).to eq('AF_INET') + expect(port).to be_a(Integer) + expect(host).to eq(loopback) + expect(numeric).to eq(loopback) + ensure + client&.close + server&.close + end + end + + describe '#timed_recvfrom' do + it 'returns the datagram and sender address when one arrives in time' do + server = make_server + client = make_client(server.local_address.ip_port) + client.write('ping') + + result = server.timed_recvfrom(65535, 5) + expect(result).to_not be_nil + + data, addr = result + expect(data).to eq('ping') + expect(addr).to eq(['AF_INET', addr[1], loopback, loopback]) + ensure + client&.close + server&.close + end + + it 'returns nil when no datagram arrives before the timeout' do + server = make_server + expect(server.timed_recvfrom(65535, 0.1)).to be_nil + ensure + server&.close + end + end +end From 84bd11b3d8400b621e75f576cc61d5cce7a6da4c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 16 Jun 2026 15:00:05 -0400 Subject: [PATCH 4/5] Implement PR feedback --- lib/rex/socket/udp.rb | 2 +- spec/rex/socket/udp_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rex/socket/udp.rb b/lib/rex/socket/udp.rb index 5f3f717..31943b6 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -167,7 +167,7 @@ def sender_addr_info(saddr) private :sender_addr_info # - # Calls recvfrom and only returns the data + # Calls #timed_read and returns the data # def get(timeout=nil) timed_read(65535, timeout) diff --git a/spec/rex/socket/udp_spec.rb b/spec/rex/socket/udp_spec.rb index d6c4ab4..d56057c 100644 --- a/spec/rex/socket/udp_spec.rb +++ b/spec/rex/socket/udp_spec.rb @@ -5,11 +5,11 @@ let(:loopback) { '127.0.0.1' } def make_server - Rex::Socket::Udp.create('LocalHost' => loopback, 'LocalPort' => 0) + described_class.create('LocalHost' => loopback, 'LocalPort' => 0) end def make_client(port) - Rex::Socket::Udp.create('PeerHost' => loopback, 'PeerPort' => port) + described_class.create('PeerHost' => loopback, 'PeerPort' => port) end describe '#recvfrom' do From c70565fe0632b2bfb822f2bcb000242ff3d88cee Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 17 Jun 2026 16:18:24 -0400 Subject: [PATCH 5/5] Catch ::Errno::ECONNREFUSED in #timed_recvfrom --- lib/rex/socket/udp.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rex/socket/udp.rb b/lib/rex/socket/udp.rb index 31943b6..9aa81aa 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -151,7 +151,7 @@ def timed_recvfrom(maxlen = 65535, timeout = def_read_timeout) data, saddr = recvfrom_nonblock(maxlen) [ data, sender_addr_info(saddr) ] - rescue ::Timeout::Error + rescue ::Timeout::Error, ::Errno::ECONNREFUSED nil end