From 96f2f21f31e460a92d5417880867aa55c5bfec52 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 77bcb97..60d0ea2 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -135,37 +135,37 @@ def send(mesg, flags, host = nil, port = nil) 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 cf26806ae884c2ec1d853f588366f9b3658a4e1b 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 60d0ea2..73446ad 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -141,24 +141,19 @@ def send(mesg, flags, host = nil, port = nil) # 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 75f509052708447fc846b3bec2df5149a82bed09 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 | 63 +++++++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/lib/rex/socket/udp.rb b/lib/rex/socket/udp.rb index 73446ad..fa1c73a 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -135,26 +135,54 @@ def send(mesg, flags, host = nil, port = 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). + # 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 index d4b8416..dfc8333 100644 --- a/spec/rex/socket/udp_spec.rb +++ b/spec/rex/socket/udp_spec.rb @@ -1,14 +1,40 @@ -# -*- coding: binary -*- +# -*- coding:binary -*- require 'spec_helper' RSpec.describe Rex::Socket::Udp do - let(:receiver) { UDPSocket.new.tap { |s| s.bind('127.0.0.1', 0) } } + let(:loopback) { '127.0.0.1' } + let(:receiver) { UDPSocket.new.tap { |s| s.bind(loopback, 0) } } let(:recv_port) { receiver.addr[1] } - let(:socket) { described_class.create('LocalHost' => '127.0.0.1') } + let(:socket) { described_class.create('LocalHost' => loopback) } - after do - socket.close rescue nil - receiver.close rescue nil + 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 '#send' do @@ -56,4 +82,29 @@ socket.sendto('data', '127.1.1.1', 1337) 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 3035851c5cb42adf0f2644b99544e4904e96d123 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 fa1c73a..ce50b42 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -185,7 +185,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 dfc8333..2aa6bf3 100644 --- a/spec/rex/socket/udp_spec.rb +++ b/spec/rex/socket/udp_spec.rb @@ -8,11 +8,11 @@ let(:socket) { described_class.create('LocalHost' => loopback) } 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 7de8716f9963cee62ded4a6deefb02381c4867b5 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 ce50b42..59b232c 100644 --- a/lib/rex/socket/udp.rb +++ b/lib/rex/socket/udp.rb @@ -169,7 +169,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