Add Interface socket option for binding to a network interface#80
Add Interface socket option for binding to a network interface#80karandesai2005 wants to merge 5 commits intorapid7:masterfrom
Interface socket option for binding to a network interface#80Conversation
Adds an 'Interface' option to Rex::Socket::Parameters so callers can
bind a socket to a specific network interface by name:
Rex::Socket::Udp.create('Interface' => 'eth0', ...)
Rex::Socket::Tcp.create('Interface' => 'eth0', ...)
Rex::Socket::TcpServer.create('Interface' => 'eth0', ...)
This is needed for broadcast-capable sockets (e.g. DHCP server) where
binding by IP address alone is insufficient to receive broadcast frames.
Changes:
- Add 'Interface' param to Rex::Socket::Parameters
- Call setsockopt(SO_BINDTODEVICE) in Comm::Local after bind, before
connect/listen (Linux only; raises BindFailed on other platforms)
- Raise Rex::BindFailed if Interface is combined with a proxy
- Raise Rex::BindFailed on invalid interface name (ENODEV) or
insufficient permissions (EPERM)
- Add RSpec tests for all new behaviors
See: rapid7/metasploit-framework#21114
- Adds reason: keyword to all BindFailed raises for clearer errors - Adds macOS support using IP_BOUND_IF (guarded by defined?) - Improves developer experience when debugging interface binding issues Addresses review feedback on PR rapid7#80.
|
I've pushed an update addressing both points:
Would love your thoughts on whether this covers the macOS case, and any suggestions for Windows support as well. |
- Adds clear BindFailed reason for Windows - Keeps fallback for other unsupported platforms Addresses maintainer feedback
|
pushed an update addressing your feedback:
Also re-ran the full test suite with elevated permissions:
|
|
@zeroSteiner following up on our Slack discussion — pushed the Platform matrix is now complete:
Also did a hardening pass:
Manually verified on Linux (all three cases pass), Windows approach 178 tests, 0 failures. Ready for final review whenever you get a chance. |
- Resolve interface name to IP via Socket.getifaddrs on Windows, overriding param.localhost so existing bind() handles the rest - Replace Socket.if_nametoindex on macOS with getifaddrs + ifindex since if_nametoindex is not available in all Ruby builds - Add rescue SystemCallError to Linux and macOS setsockopt blocks to prevent socket leaks on unexpected errors - Distinguish between interface not found vs interface has no IPv4 address in the Windows getifaddrs lookup - Add rescue for getifaddrs enumeration failures on Windows
549a3a5 to
125e231
Compare
lib/rex/socket/parameters.rb
Outdated
| self.timeout = hash['Timeout'].to_i | ||
| end | ||
|
|
||
| self.interface = hash['Interface'].to_s.strip if hash['Interface'] |
There was a problem hiding this comment.
| self.interface = hash['Interface'].to_s.strip if hash['Interface'] | |
| self.interface = hash['Interface'].to_s.strip if hash['Interface'] && !hash['Interface'].strip.empty? |
lib/rex/socket/comm/local.rb
Outdated
| ::Socket.getifaddrs.each do |ifaddr| | ||
| next unless ifaddr.name == param.interface | ||
| iface_found = true | ||
| next unless ifaddr.addr&.ipv4? |
There was a problem hiding this comment.
The parameters has a #v6 attribute
rex-socket/lib/rex/socket/parameters.rb
Lines 206 to 215 in 146bc6b
When that's set the address should be IPv6 and we'd want to select a v6 address not an v4 address. At this time, it's safe to assume that if it's not IPv6 that it's IPv4 because in practice we only support AF_INET and AF_INET6 nothing like AF_UNIX but maybe someday.
| # Notify handlers of the before socket create event. | ||
| self.instance.notify_before_socket_create(self, param) | ||
|
|
||
| if param.interface && !param.interface.empty? && param.proxies? |
There was a problem hiding this comment.
Would you mind please adding a comment here summarizing why we're doing this? We/I will surely forget in the future how we landed on this solution.
| raise Rex::BindFailed.new(param.localhost, param.localport, | ||
| reason: reason), caller | ||
| end | ||
| param.localhost = iface_ip |
There was a problem hiding this comment.
I think this approach may need to be modified ever so slightly. If we do this, then the param the caller passed us will be mutated and after this function has returned, the #localhost address the caller had originally set will be different. I think the easiest fix might be to update this to:
| param.localhost = iface_ip | |
| param = param.dup # avoid mutating the caller's instance | |
| param.localhost = iface_ip |
Alternatively you'd need to use a localhost local variable in this scope, defaulting to param.localhost and bind to that directly.
- Guard interface param against whitespace-only strings - Add comment explaining why Interface + proxy raises immediately - Use param.dup before mutating localhost to avoid side effects on the caller's instance - Select IPv6 address when param.v6 is true, IPv4 otherwise
|
@smcintyre-r7 addressed all four points:
178 tests, 0 failures. |
Summary
Adds an
Interfaceoption toRex::Socket::Parametersso callers canbind a socket to a specific network interface by name:
This is needed for broadcast-capable sockets (e.g. the DHCP server module)
where binding by IP address alone is insufficient — the socket must be bound
to a specific interface to send and receive broadcast frames correctly.
Changes
lib/rex/socket/parameters.rb— addsinterfaceattribute, parsed fromthe
'Interface'hash key, whitespace-stripped,nilby defaultlib/rex/socket/comm/local.rb— two additions insidecreate_by_type:Rex::BindFailedimmediately ifInterfaceis combinedwith a proxy (incompatible by design)
setsockopt(SOL_SOCKET, SO_BINDTODEVICE, interface)after thenormal bind block, before
SO_BROADCAST— Linux only. Non-Linuxplatforms raise
Rex::BindFailed(macOSIP_BOUND_IFis not availablein the current Ruby stdlib and can be added as a follow-up)
spec/rex/socket/parameters_spec.rb— new#interfacedescribe blockspec/rex/socket/comm/local_spec.rb— newInterface optiondescribeblock covering proxy guard, invalid interface name, loopback success
(root-guarded), and non-Linux platform
Testing
Notes
IP_BOUND_IFavailability across Ruby versions is confirmed