Below is a minimal working example of a C++ server which implements a dual stack network infrastructure. (Meaning that a single socket handles both ipv4 and ipv6 connections.)
#include <format>
#include <print>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <signal.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
const auto SERVER_PORT = 7778;
const auto server_fd = socket(AF_INET6, SOCK_STREAM, 0);
int opt = 0;
setsockopt(server_fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
sockaddr_in6 server_address;
std::memset(&server_address, 0, sizeof(server_address));
server_address.sin6_family = AF_INET6;
server_address.sin6_addr = in6addr_any;
server_address.sin6_port = htons(SERVER_PORT);
bind(server_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
listen(server_fd, 10);
sockaddr_storage peer_address;
socklen_t peer_address_length = sizeof(peer_address);
auto peer_fd = accept(server_fd, reinterpret_cast<sockaddr*>(&peer_address), &peer_address_length);
if(peer_address.ss_family == AF_INET)
{
const auto p_peer_address = &peer_address;
sockaddr_in* ipv4 = (sockaddr_in*)p_peer_address;
std::println("Client port (IPv4): {}", ntohs(ipv4->sin_port));
}
else if(peer_address.ss_family == AF_INET6)
{
const auto p_peer_address = &peer_address;
sockaddr_in6* ipv6 = (sockaddr_in6*)p_peer_address;
std::println("Client port (IPv6): {}", ntohs(ipv6->sin6_port));
}
else
{
throw std::runtime_error("unrecognized ss_family");
}
close(peer_fd);
close(server_fd);
return 0;
}
However, it is not working as expected. Regardless of whether a client connects via ipv4 or ipv6, the logic always follows the else if
branch of the if
statement. The first if
branch never runs, indicating that no connections are started with the AF_INET
family.
Both clients do actually appear to work. The server responds when a connection is started - it's just the ipv6 message is printed regardless of the client connection type. (ipv4 or ipv6)
If it makes a difference, the clients and server are both running on the same machine, connecting via localhost (127.0.0.1
)
Example C++ code for both types of client are provided below.
// Client: ipv6
#include <format>
#include <print>
#include <format>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
const auto PORT = 7778;
const auto socket_fd = socket(AF_INET6, SOCK_STREAM, 0);
in6_addr server_sin6_address;
std::memset(&server_sin6_address, 0, sizeof(server_sin6_address));
inet_pton(AF_INET6, "::1", &server_sin6_address);
sockaddr_storage server_address;
std::memset(&server_address, 0, sizeof(server_address));
server_address.ss_family = AF_INET6;
sockaddr_storage *p_server_address = &server_address;
sockaddr_in6 *p_server_address_in6 = reinterpret_cast<sockaddr_in6*>(p_server_address);
p_server_address_in6->sin6_family = AF_INET6; // why repeat?
p_server_address_in6->sin6_port = htons(PORT);
p_server_address_in6->sin6_flowinfo = 0; // not used?
p_server_address_in6->sin6_addr = server_sin6_address;
p_server_address_in6->sin6_scope_id = 0; // not used?
const auto connect_result = connect(socket_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
const char* const buffer = "hello world ipv6";
const auto buffer_length = strlen(buffer) + 1;
send(socket_fd, buffer, buffer_length, 0);
close(socket_fd);
return 0;
}
// Client: ipv4
#include <format>
#include <print>
#include <format>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
const auto PORT = 7778;
const auto socket_fd = socket(AF_INET, SOCK_STREAM, 0);
in_addr server_sin_address;
std::memset(&server_sin_address, 0, sizeof(server_sin_address));
inet_pton(AF_INET, "127.0.0.1", &server_sin_address);
sockaddr_storage server_address;
std::memset(&server_address, 0, sizeof(server_address));
server_address.ss_family = AF_INET;
sockaddr_storage *p_server_address = &server_address;
sockaddr_in *p_server_address_in = reinterpret_cast<sockaddr_in*>(p_server_address);
p_server_address_in->sin_family = AF_INET; // why repeat?
p_server_address_in->sin_port = htons(PORT);
p_server_address_in->sin_addr = server_sin_address;
//p_server_address_in->sin_zero = 0; // not used?
const auto connect_result = connect(socket_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
const char* const buffer = "hello world ipv4";
const auto buffer_length = strlen(buffer) + 1;
const auto send_result = send(socket_fd, buffer, buffer_length, 0);
close(socket_fd);
std::println("Server quit");
return 0;
}
Maybe it's a bug in the ipv4 client implementation, but so far I didn't find anything which looked like it might be obviously incorrect.
Do ipv4 connections to an ipv6 destination somehow get automatically upgraded by the OS or something? Just seems like really weird behavior.
A dual-stack socket is an IPv6 socket which can communicate with an IPv4 peer (by disabling the IPV6_V6ONLY
option). It is still an IPv6 socket nonetheless.
An AF_INET
socket can work only with AF_INET
addresses. And likewise, an AF_INET6
socket can work only with AF_INET6
addresses. So, when a client is accepted by an AF_INET6
server, whether the server is dual-stack or not, the accepted socket will also be AF_INET6
. Which is what you are seeing happen.
But, for a dual-stack server, if the client is using IPv4 instead of IPv6, then the client's IP address that is reported by accept()
or getpeername()
will be an IPv4-mapped IPv6 address, ie an AF_INET6
address that has a 96-bit prefix 0:0:0:0:0:FFFF
and the remaining 32-bits will be the IPv4 address.
You can see this if you log the actual IPs, not just the ports.
For example:
if(peer_address.ss_family == AF_INET) // <-- never true for an AF_INET6 server!
{
const sockaddr_in* ipv4 = reinterpret_cast<sockaddr_in*>(&peer_address);
char ipstr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &ipv4->sin_addr, ipstr, sizeof(ipstr));
std::println("Client IPv4: {}, port: {}", ipstr, ntohs(ipv4->sin_port));
}
else if(peer_address.ss_family == AF_INET6) // <-- always true for an AF_INET6 server!
{
const sockaddr_in6* ipv6 = reinterpret_cast<sockaddr_in6*>(&peer_address);
char ipstr[INET6_ADDRSTRLEN];
if (IN6_IS_ADDR_V4MAPPED(&sockaddr_in6->sin6_addr))
{
struct in_addr ipv4;
memcpy(&ipv4.s_addr, &sockaddr_in6->sin6_addr.s6_addr[12], 4);
inet_ntop(AF_INET, &ipv4, ipstr, sizeof(ipstr));
std::println("Client IPv4 (mapped): {}, port: {}", ipstr, ntohs(ipv6->sin6_port));
}
else
{
inet_ntop(AF_INET6, &ipv6->sin6_addr, ipstr, sizeof(ipstr));
std::println("Client IPv6: {}, port: {}", ipstr, ntohs(ipv6->sin6_port));
}
}
else // <-- never happens on an AF_INET6 server!
{
throw std::runtime_error("unrecognized ss_family");
}