macOS 26 not negotiating ECN for outgoing IPv4 connections (it does for IPv6 connections)

I have several macOS systems (Apple Silicon) running various flavours of macOS 26 (26.2 and 26.3 RC).

I also have a couple of Centos 10 Linux (ARM64) systems. All are connected to my 10 GbE switch, so not routers or anything else in the path that could mess with ECN flags. The network is dual stack.

The CentOS systems are configured to offer / accept ECN for both outgoing and incoming connections (net.ipv4.tcp_ecn = 1). The macOS systems have their default settings which also supposedly behave the same way:

$ sysctl -a | grep ecn net.inet.tcp.ecn_timeout: 5 net.inet.tcp.ecn_setup_percentage: 100 net.inet.tcp.accurate_ecn: 0 net.inet.tcp.ecn_initiate_out: 1 net.inet.tcp.ecn: 1 net.inet.ipsec.ecn: 0 net.inet.mptcp.probecnt: 5 net.inet6.ipsec6.ecn: 0 net.classq.fq_codel.enable_ecn: 0

I have a simple throughput test program (written in C and using the standard socket API) that runs as both a client and a server which I have ported to both OS.

When I run it between the two Linux systems using either IPv4 or IPv6 a tcpdump / Wireshark trace shows that ENC is active in both directions. Internet Protocol / Differentiated Services shows Explicit Congestion Notification: ECN-Capable Transport code point '10' for both flows.

When I run the same test between one of the macOS systems and one of the Linux systems what I observe is that when using IPv4 the Linux -> macOS flow has Not ECN_capable Transport (0) while the macOS -> Linux flow has ECN-Capable Transport code point '10'.

This seems wrong. I even tried enabling LS4 (defaults write -g network_enable_l4s -bool true) but unsurprisingly this made no difference.

If I run the same test over IPv6 then both flows have ECN-Capable Transport code point '10'.

How can I ensure that macOS tries to negotiate ECN for outgoing IPv4 connections? Or is this a macOS bug?

Answered by DTS Engineer in 875750022

Apple’s relationship to ECN is nuanced, largely due to compatibility concerns. If you want to be traumatised, check out the xnu/blob/main/bsd/netinet/tcp_cache.c file in Darwin and the various callsites for that subsystem.

So, my answer here depends on where you’re coming from. As a developer, you have APIs that allow you to opt out and opt in to ECN. For Network framework that is the ecnDisabled(_:) modifier [1]. For BSD Sockets there is the TCP_ENABLE_ECN socket option. The system will honour your request to specifically disable ECN, but there’s no guarantee that it will honour a request to enable it. It’s allowed to take other factors into account. That’s why ecnDisabled(_:) is named as it is.

Note The name of the socket option doesn’t convey that subtlety. Notably, if you rummage around in Darwin you’ll find a related non-public socket option that better captures this nuance.

Both APIs do have a way to determine whether ECN was actually used. For Network framework that’s the ecn property in the TCP metadata.

If you’re coming at this from the perspective of someone using their Mac and wondering why ECN is happening, that’s not something I give you definitive answers about. As I mentioned at the start of this, it’s complicated. Moreover, this level of on-the-wire behaviour is considered an implementation detail, something can has changed in the past and may well change in the future.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Network framework has an older Swift API and a C API, both of which have equivalents to this.

Apple’s relationship to ECN is nuanced, largely due to compatibility concerns. If you want to be traumatised, check out the xnu/blob/main/bsd/netinet/tcp_cache.c file in Darwin and the various callsites for that subsystem.

So, my answer here depends on where you’re coming from. As a developer, you have APIs that allow you to opt out and opt in to ECN. For Network framework that is the ecnDisabled(_:) modifier [1]. For BSD Sockets there is the TCP_ENABLE_ECN socket option. The system will honour your request to specifically disable ECN, but there’s no guarantee that it will honour a request to enable it. It’s allowed to take other factors into account. That’s why ecnDisabled(_:) is named as it is.

Note The name of the socket option doesn’t convey that subtlety. Notably, if you rummage around in Darwin you’ll find a related non-public socket option that better captures this nuance.

Both APIs do have a way to determine whether ECN was actually used. For Network framework that’s the ecn property in the TCP metadata.

If you’re coming at this from the perspective of someone using their Mac and wondering why ECN is happening, that’s not something I give you definitive answers about. As I mentioned at the start of this, it’s complicated. Moreover, this level of on-the-wire behaviour is considered an implementation detail, something can has changed in the past and may well change in the future.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Network framework has an older Swift API and a C API, both of which have equivalents to this.

macOS 26 not negotiating ECN for outgoing IPv4 connections (it does for IPv6 connections)
 
 
Q