Major reason for that is BSD Sockets and their leaky abstraction that results in hardcoding protocol details in application code.
For a good decade a lot of software had to be slowly patched in every place that made a socket to add v6 support, and sometimes multiple times because getaddrinfo didn't reach everyone early enough.
> results in hardcoding protocol details in application code
Are you suggesting that this could have been implemented a different way? Example: IP could be negotiated to upgrade from v4 to v6? I am curious about your ideas.
I think in principle, an application didn't need to know the exact format of an IP address, even if connecting directly to an IP. A simple idea that could have made application code much more IP-agnostic would have been for SOCK_ADDR_IN to take the IP in string format, not as a four-byte value. That way, lots of application code would not need to even be recompiled to move from a 4 byte IPv4 address to a 16 byte IPv6 address, whereas today they not only need to be recompiled, they need to be changed at the source level to use a new type that allows for both.
Of course, code that operates on packets, in the TCP/IP stack of the OS would have still needed to be rewritten. But that is far less code than "every application that opens a socket".
Of course, this only applies to code that uses IPs only to open connections. There's lots of application code that does more things with IPs, such as parsing, displaying, validating etc. All of this code would still need to be rewritten to accept IPv6 addresses (and its much more complex string representations), that part is inevitable.
Yeah, the big issue is that any code that took addresses from user input had to do validation to make sure addresses were valid, in allowed ranges, etc.
While the sockaddr struct allowed to to abstractly handle v4/v6 socket connections, there wasn’t a clean way to do all of that additional stuff and IP address logic leaked into all kinds of software where you wouldn’t first expect it.
Something as simple as a web app that needs to inspect proxy headers would even have it.
It also didn’t help that it became practice to explicitly not trust the addr resolution offered by the sockets API because it would do unexpected things like resolving something that looked like an integer to a uint32 and then a 4 byte V4 addr.
This is vastly oversimplifying the problem, the difference between IPv4 and IPv6 is not just the format of the address. Different protocols have different features, which is why the sockaddr_in and sockaddr_in6 types don't just differ in the address field. Plus the vast majority of network programs are using higher level abstractions, for example even in C or C++ a lot of people would be using a network library like libevent or asio to handle a lot of these details (especially if you want to write code that easily works with TLS).
There isn't much need for many applications to know or care what IP protocol they are speaking, they are all just writing bytes to a TCP stream. I think the parent is saying that existing socket abstractions meant that these applications still had to be "upgraded" to support IPv6 whereas it could/should have been handled entirely by the OS with better socket APIs.
The simplest case would have been using a variant of Happy Eyeballs protocol.
Resolve the A and AAAA records, and try to connect to them at the same time. The first successful connection wins (maaaaybe with a slight bias for IPv6).
This would have required an API that uses the host name and folds the DNS resolution and connection into one call. Instead, the BSD socket API remained at the "network assembly" level with the `sockaddr_in/sockaddr_in6` structures used for address information.
For the examples I am going to use the typical "HTTP to example.com" case.
Some OSI-focused stacks provided high level abstraction that gave you a socket already set for listening or connected to another service, based on combination of "host name", "service name", and "service type".
You'd use something like
connect("example.com", "http", SVC_STREAM_GRACEFUL_CLOSE) // using OSI-like name for the kind of service TCP provides
and as far as application is concerned, it does not need to know if it's ipv4, ipv6, X.25, or a direct serial connection (OSI concept of separating "service" from "protocol" is really a great idea that got lost)
Similar approach was done in Plan 9 (and thus everyone who uses Go is going to see something similar) with the dial API:
dial("example.com!http",0,0,0)
As part of IPv6 effort an attempt at providing something similar with BSD Sockets was made, namely getaddrinfo which gives back information to be fed to socket/bind/connect calls - but for a long time people still learnt from old material which had them manually fill in socket parameters without GAI so adoption was slowed down.
No, for example on Android and iOS the APIs for when you connect to a server the hostname is a string. This hostname can be either an ipv4 address, an ipv6 adress, or a domain. The BSD sockets API on the other hand forces each application to implement this themselves and a lot of them took the shortcut of only supporting ipv4.
It isn't about upgrading one protocol to another but about having the operating system abstract away the different protocols from the application.
For a good decade a lot of software had to be slowly patched in every place that made a socket to add v6 support, and sometimes multiple times because getaddrinfo didn't reach everyone early enough.