Project

General

Profile

Actions

Bug #21104

open

Net::HTTP connections failing in Ruby >= 3.4.0 on macOS with Happy Eyeballs enabled

Added by mjt58 (Mike Thompson) about 1 month ago. Updated 13 days ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [arm64-darwin24]
[ruby-core:120855]

Description

A project I work on recently upgraded Ruby to 3.4.1 from 3.3.5. Following the upgrade, and when running locally on my Mac, all attempts to connect to an external service within the project over http(s) fail.

We use mise for managing development tool dependencies, including Ruby, and I am using macOS 15.3.

For example running something as simple as:

require 'net/http'
puts Net::HTTP.get(URI('https://bbc.co.uk'))

Will fail with the following stack trace:

/path/to/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/net/protocol.rb:46:in 'OpenSSL::SSL::SSLSocket#connect_nonblock': Connection reset by peer - SSL_connect (Errno::ECONNRESET)
	from /path/to/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/net/protocol.rb:46:in 'Net::Protocol#ssl_socket_connect'
	from /path/to/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/net/http.rb:1736:in 'Net::HTTP#connect'
	from /path/to/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/net/http.rb:1636:in 'Net::HTTP#do_start'
	from /path/to/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/net/http.rb:1625:in 'Net::HTTP#start'
	from /path/to/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/net/http.rb:1064:in 'Net::HTTP.start'
	from /path/to/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/net/http.rb:824:in 'Net::HTTP.get_response'
	from /path/to/.local/share/mise/installs/ruby/3.4.1/lib/ruby/3.4.0/net/http.rb:805:in 'Net::HTTP.get'
	from request.rb:2:in '<main>'

I tried different versions of Ruby and confirmed that the issue appears with 3.4.0. After reading the release notes for this version, I tried setting RUBY_TCP_NO_FAST_FALLBACK=1 and this worked, allowing me to work around the problem.

This issue has also been encountered by others, please see:
https://github.com/rubygems/rubygems/issues/8390

Updated by shioimm (Misaki Shioi) about 1 month ago

I suspect that this issue might be caused by an interaction between the specifications of Happy Eyeballs v2 and certain intermediate devices along the network path.
This is because, according to the report you linked, the problem occurs for some people but not for others, even though they are using the same OS, the same Ruby version, the same OpenSSL version, and connecting to the same destination (https://rubygems.org/). I also haven't been able to reproduce this issue in my own environment.

To help narrow down the cause, could you provide more information on the following?

  • You specified 'https://bbc.co.uk' as the host you're connecting to. Does the same issue occur when connecting to other hosts as well?
  • Does the issue still occur when you use a different network environment? For example, when connecting from your workplace, a cafe, or when using a VPN?

Updated by mjt58 (Mike Thompson) about 1 month ago

shioimm (Misaki Shioi) wrote in #note-1:

To help narrow down the cause, could you provide more information on the following?

  • You specified 'https://bbc.co.uk' as the host you're connecting to. Does the same issue occur when connecting to other hosts as well?

Yes, I saw it first running bundle install with errors like:

Bundler::HTTPError Could not fetch specs from https://rubygems.org/ due to underlying error <Errno::ECONNRESET: Connection reset by peer - SSL_connect (https://rubygems.org/specs.4.8.gz)>
  • Does the issue still occur when you use a different network environment? For example, when connecting from your workplace, a cafe, or when using a VPN?

Yes, I see it on, home wifi, office wifi and when tethered off my phone.

Updated by shioimm (Misaki Shioi) about 1 month ago

@mjt58
Thank you for your reply. Since you're seeing this issue across different environments, I'm starting to think it's more likely influenced by software running on the host rather than a problem with network intermediaries.
Could you also check the following?

  • Do you get any errors when running TCPSocket.new directly without using net/http? For example, try executing a script like this:
require "socket"
p TCPSocket.new("bbc.co.uk", 80, fast_fallback: true)
  • Could you verify via packet capture whether the source IP of the RST is the client host itself? You can see which IP address sends an RST by using tcpdump, for example, by running sudo tcpdump -i <interface name> host bbc.co.uk, and then executing Net::HTTP.get(URI("https://bbc.co.uk")).

I'd appreciate your help with further debugging.

Updated by radarek (Radosław Bułat) 20 days ago · Edited

I have the same issue with ruby 3.4.1 and 3.4.2. Because of it I can not install gems (ie. gem install solargraph fails). Only using mentioned workaround (RUBY_TCP_NO_FAST_FALLBACK=1 gem install solargraph) I am able to do this. I have mac os, arm64 (m1) architecture.

@shioimm (Misaki Shioi)

require "socket"
p TCPSocket.new("bbc.co.uk", 80, fast_fallback: true)

It works for me (it outputs TCPSocket instance data).

I also tried to get some tcpdump output. With RBENV_VERSION=3.4.1 ruby -rnet/http -e 'Net::HTTP.get(URI("https://bbc.co.uk"))' I didn't get any output in tcpdump session.

I only got output with ruby 3.3.4 RBENV_VERSION=3.3.4 ruby -rnet/http -e 'Net::HTTP.get(URI("https://bbc.co.uk"))':

$ sudo tcpdump -i en0 host bbc.co.uk
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on en0, link-type EN10MB (Ethernet), snapshot length 524288 bytes
15:25:07.847222 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [S], seq 1483240036, win 65535, options [mss 1460,nop,wscale 6,nop,nop,TS val 4232487628 ecr 0,sackOK,eol], length 0
15:25:07.889327 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [S.], seq 1267008426, ack 1483240037, win 65535, options [mss 1396,sackOK,TS val 3060773048 ecr 4232487628,nop,wscale 9], length 0
15:25:07.889447 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [.], ack 1, win 2054, options [nop,nop,TS val 4232487671 ecr 3060773048], length 0
15:25:07.920700 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [P.], seq 1:518, ack 1, win 2054, options [nop,nop,TS val 4232487701 ecr 3060773048], length 517
15:25:07.955066 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [.], ack 518, win 273, options [nop,nop,TS val 3060773113 ecr 4232487701], length 0
15:25:07.957494 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [.], seq 1:1381, ack 518, win 273, options [nop,nop,TS val 3060773115 ecr 4232487701], length 1380
15:25:07.957497 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [P.], seq 1381:2761, ack 518, win 273, options [nop,nop,TS val 3060773115 ecr 4232487701], length 1380
15:25:07.957499 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [.], seq 2761:4141, ack 518, win 273, options [nop,nop,TS val 3060773115 ecr 4232487701], length 1380
15:25:07.957501 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [P.], seq 4141:4970, ack 518, win 273, options [nop,nop,TS val 3060773115 ecr 4232487701], length 829
15:25:07.957580 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [.], ack 4970, win 1976, options [nop,nop,TS val 4232487739 ecr 3060773115], length 0
15:25:07.957687 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [.], ack 4970, win 2048, options [nop,nop,TS val 4232487739 ecr 3060773115], length 0
15:25:07.960236 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [P.], seq 518:576, ack 4970, win 2048, options [nop,nop,TS val 4232487741 ecr 3060773115], length 58
15:25:07.993411 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [.], ack 576, win 273, options [nop,nop,TS val 3060773152 ecr 4232487741], length 0
15:25:07.993548 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [P.], seq 576:722, ack 4970, win 2048, options [nop,nop,TS val 4232487775 ecr 3060773152], length 146
15:25:08.027521 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [.], ack 722, win 275, options [nop,nop,TS val 3060773186 ecr 4232487775], length 0
15:25:08.027523 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [P.], seq 4970:5815, ack 722, win 275, options [nop,nop,TS val 3060773187 ecr 4232487775], length 845
15:25:08.027525 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [P.], seq 5815:5839, ack 722, win 275, options [nop,nop,TS val 3060773187 ecr 4232487775], length 24
15:25:08.027526 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [F.], seq 5839, ack 722, win 275, options [nop,nop,TS val 3060773187 ecr 4232487775], length 0
15:25:08.027666 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [.], ack 5839, win 2034, options [nop,nop,TS val 4232487809 ecr 3060773187], length 0
15:25:08.027727 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [.], ack 5840, win 2034, options [nop,nop,TS val 4232487809 ecr 3060773187], length 0
15:25:08.028030 IP 192.168.100.38.53860 > 151.101.0.81.https: Flags [F.], seq 722, ack 5840, win 2048, options [nop,nop,TS val 4232487809 ecr 3060773187], length 0
15:25:08.061912 IP 151.101.0.81.https > 192.168.100.38.53860: Flags [.], ack 723, win 275, options [nop,nop,TS val 3060773220 ecr 4232487809], length 0

Updated by shioimm (Misaki Shioi) 18 days ago

@radarek
Thank you for your additional report.
I would like to determine how to address this issue, but unfortunately, I am completely unable to reproduce it in my environment.
Therefore, I would greatly appreciate your cooperation in debugging.

Here are the facts that we have confirmed so far:

  • When executing a TLS connection with Net::HTTP, the connection is terminated with ECONNRESET.
  • When executing TCPSocket.new (which is used internally by Net::HTTP) , the connection succeeds.
  • When Net::HTTP is executed with HEv2 disabled for TCPSocket.new, the connection succeeds.
  • Some environments experience this issue, while others do not. In the former case, the issue always occurs.
  • In environments where this issue occurs, it still happens with changing the network.
  • In environments where this issue occurs, tcpdump does not show any output, meaning that packets are not being sent out from the host (suggesting that an RST may be injected internally within the client host).

From these observations, I currently suspect that this issue is dependent on the configuration of the client machine or the functionality of security software utilizing network extensions.

To further investigate, I have prepared the following list of checks that I would like to ask you to try.
If possible, could you run these and let me know the results?

  • Check if you can connect to http://example.com/ (not https) using Net::HTTP.

    • This will help determine whether connections to port 80 behave the same way as connections to port 443. If the connection to port 80 succeeds while port 443 fails, it may indicate that a firewall or similar mechanism is injecting RST into port 443 connections.
  • Try running the following script and see if the connection succeeds:

    • This is a highly simplified version of what Net::HTTP executes internally. It will help narrow down where the issue is occurring
    require "openssl"
    require "socket"
    tcp_socket = TCPSocket.new(<any host>, 443)
    ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, OpenSSL::SSL::SSLContext.new)
    ssl_socket.connect
    ssl_socket.close
    tcp_socket.close
    
  • Check whether IPv6 is enabled or disabled, and test Net::HTTP in both states to see if the results are the same.

    • For example, if you are using Wi-Fi, you can check the IPv6 status with networksetup -getinfo Wi-Fi. This will help verify whether there is a hidden issue in the HEv2 implementation.
  • If you are using any network extension software on your Mac, disable it and check whether Net::HTTP behaves differently compared to when it is enabled.

    • You can check the list of network extension software by running systemextensionsctl list. This will help determine whether any particular software is injecting RST.
  • If you have another machine with Ruby 3.4 installed besides the Mac where the issue occurs, check whether the same problem happens on that machine.

I would appreciate any information you can provide.

Updated by radarek (Radosław Bułat) 16 days ago

shioimm (Misaki Shioi) wrote in #note-5:

If possible, could you run these and let me know the results?

  • Check if you can connect to http://example.com/ (not https) using Net::HTTP.

It fails for ruby 3.4.2:

$ RBENV_VERSION=3.4.2 ruby -v -rnet/http -e 'puts Net::HTTP.get(URI("http://example.com/"))'
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24]
<internal:io>:63:in 'IO#read_nonblock': Connection reset by peer (Errno::ECONNRESET)
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/protocol.rb:218:in 'Net::BufferedIO#rbuf_fill'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/protocol.rb:199:in 'Net::BufferedIO#readuntil'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/protocol.rb:209:in 'Net::BufferedIO#readline'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http/response.rb:158:in 'Net::HTTPResponse.read_status_line'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http/response.rb:147:in 'Net::HTTPResponse.read_new'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:2414:in 'block in Net::HTTP#transport_request'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:2405:in 'Kernel#catch'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:2405:in 'Net::HTTP#transport_request'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:2378:in 'Net::HTTP#request'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:2249:in 'Net::HTTP#request_get'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:826:in 'block in Net::HTTP.get_response'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:1626:in 'Net::HTTP#start'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:1064:in 'Net::HTTP.start'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:824:in 'Net::HTTP.get_response'
	from /Users/radarek/.rbenv/versions/3.4.2/lib/ruby/3.4.0/net/http.rb:805:in 'Net::HTTP.get'
	from -e:1:in '<main>'

Doing it with pure socket also fails:

require "socket"
s = TCPSocket.open("example.com", 80)
s.write "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"
puts s.gets(nil)
s.close

Running this program I get:

$ RBENV_VERSION=3.4.2 ruby -v socket.rb
ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24]
socket.rb:4:in 'IO#gets': Connection reset by peer (Errno::ECONNRESET)
	from socket.rb:4:in '<main>'
  • This will help determine whether connections to port 80 behave the same way as connections to port 443. If the connection to port 80 succeeds while port 443 fails, it may indicate that a firewall or similar mechanism is injecting RST into port 443 connections.

From the previous experiment it looks like they behave the same so the problem is not specific to 443 port.

  • Check whether IPv6 is enabled or disabled, and test Net::HTTP in both states to see if the results are the same.
    • For example, if you are using Wi-Fi, you can check the IPv6 status with networksetup -getinfo Wi-Fi. This will help verify whether there is a hidden issue in the HEv2 implementation.

I turned off ipv6 by executing networksetup -setv6off Wi-Fi (and verified with networksetup -getinfo Wi-Fi) but it didn't change previous outcomes.

  • If you are using any network extension software on your Mac, disable it and check whether Net::HTTP behaves differently compared to when it is enabled.
    • You can check the list of network extension software by running systemextensionsctl list. This will help determine whether any particular software is injecting RST.

The only one extension which is listed is some OBS camera extension which is disabled:

systemextensionsctl list
1 extension(s)
--- com.apple.system_extension.cmio (Go to 'System Settings > General > Login Items & Extensions > Camera Extensions' to modify these system extension(s))
enabled	active	teamID	bundleID (version)	name	[state]
	*	2MMRE5MTB8	com.obsproject.obs-studio.mac-camera-extension (30.1.0/8254614054)	com.obsproject.obs-studio.mac-camera-extension	[activated disabled]
  • If you have another machine with Ruby 3.4 installed besides the Mac where the issue occurs, check whether the same problem happens on that machine.

I have second Mac from company I work for, connected to the same network (but it runs software related to the network - zscaler, which I believe can intercept network connections) and there is no issue.

I would appreciate any information you can provide.

I'll be happy to provide any information which could be useful to fix this bug.

Updated by shioimm (Misaki Shioi) 14 days ago · Edited

@radarek
Thank you very much for your cooperation.
Based on the information you provided, it seems almost certain that this issue occurs only on a specific host machine.
(An interesting point is that ECONNRESET does not occur at the moment when a connected Socket object is obtained via TCPSocket.open, but rather when attempting to call SSL_connect or write, at which point an exception is raised.)

Apart from the fact that Zscaler is being used on the Mac where the issue does not occur, is there anything else you can think of that might be different between the two machines in terms of network configuration?
Additionally, could you provide the OS versions of both machines?
And could you provide the output of sudo pfctl -s all?

One more request but if possible, could you try booting the Mac where the issue occurs in Safe Mode and check whether the script that triggers the issue runs successfully? Since Safe Mode disables third-party extensions and some system optimizations, it may help us to determine whether this issue is caused by an external factor such as a network extension.

Updated by LanikSJ (Ilan Erenstein) 13 days ago

I'm able to reproduce this error as well I think.

→ curl -Lks 'https://git.io/rg-ssl' | ruby

Here's your Ruby and OpenSSL environment:

Ruby:          ruby 3.4.2 (2025-02-15 revision d2930f8e7a) +PRISM [arm64-darwin24]
RubyGems:      3.6.5
Bundler:       2.6.5
OpenSSL:       3.3.0
Compiled with: OpenSSL 3.4.1 11 Feb 2025
Loaded with:   OpenSSL 3.4.1 11 Feb 2025

Trying connections to https://rubygems.org:

Bundler:       ❌ failed     (Connection reset by peer - SSL_connect)
RubyGems:      ❌ failed     (Errno::ECONNRESET: Connection reset by peer - SSL_connect (https://rubygems.org))
Ruby net/http: ❌ failed

Unfortunately, this Ruby can't connect to rubygems.org. 😡

Even worse, we're not sure why. 😕

Here's the full error information:
Errno::ECONNRESET: Connection reset by peer - SSL_connect
  /Users/LanikSJ/.rvm/rubies/ruby-3.4.2/lib/ruby/3.4.0/net/protocol.rb:46:in 'OpenSSL::SSL::SSLSocket#connect_nonblock'
  /Users/LanikSJ/.rvm/rubies/ruby-3.4.2/lib/ruby/3.4.0/net/protocol.rb:46:in 'Net::Protocol#ssl_socket_connect'
  /Users/LanikSJ/.rvm/rubies/ruby-3.4.2/lib/ruby/3.4.0/net/http.rb:1736:in 'Net::HTTP#connect'
  /Users/LanikSJ/.rvm/rubies/ruby-3.4.2/lib/ruby/3.4.0/net/http.rb:1636:in 'Net::HTTP#do_start'
  /Users/LanikSJ/.rvm/rubies/ruby-3.4.2/lib/ruby/3.4.0/net/http.rb:1631:in 'Net::HTTP#start'
  -:132:in '<main>'

You might have more luck using Mislav's SSL doctor.rb script. You can get it here:
https://github.com/mislav/ssl-tools/blob/8b3dec4/doctor.rb
Read more about the script and how to use it in this blog post:
https://mislav.net/2013/07/ruby-openssl/

For me the fix was to set

RUBY_TCP_NO_FAST_FALLBACK=1

This is all done from the CLI.

Updated by shioimm (Misaki Shioi) 13 days ago

I am continuing to investigate this issue.
For those experiencing this problem, can you successfully run the following script?

require "socket"
s = Socket.tcp("example.com", 80)
s.write "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"
puts s.gets(nil)
s.close

Additionally, if anyone has tried the following requests I made, please share your results:

Additionally, could you provide the OS versions of both machines?
And could you provide the output of sudo pfctl -s all ?

One more request, but if possible, could you try booting the Mac where the issue occurs in Safe Mode and check whether the script that triggers the issue runs successfully?

Actions

Also available in: Atom PDF

Like2
Like0Like0Like1Like0Like0Like0Like0Like0Like0