Skip to content

feat: Add IPv6 dual-stack support (#245)#383

Draft
andy5995 wants to merge 10 commits intodevelopfrom
feat/ipv6-245
Draft

feat: Add IPv6 dual-stack support (#245)#383
andy5995 wants to merge 10 commits intodevelopfrom
feat/ipv6-245

Conversation

@andy5995
Copy link
Copy Markdown
Collaborator

The server now binds to an AF_INET6 socket with IPV6_V6ONLY=0, which accepts both IPv4 and IPv6 connections simultaneously on a single port. On platforms that do not support dual-stack (e.g. OpenBSD), the server falls back to IPv4-only automatically.

Changes:

  • ServerSocket::bind(): try AF_INET6 + IPV6_V6ONLY=0, fall back to AF_INET
  • ServerSocket::accept(): use sockaddr_storage; strip ::ffff: prefix from IPv4-mapped addresses so downstream code sees plain IPv4 strings
  • ClientSocket::connect(): use getaddrinfo(AF_UNSPEC) for name resolution, recreating the socket as AF_INET6 when the server address requires it
  • Ip class: store address as string (addrStr) to support IPv6 literals; byte array kept for backwards-compatible getByte()/Inet_NtoA() paths
  • getLocalIPAddressList(): replace deprecated gethostbyname() with getaddrinfo(AF_UNSPEC) to also enumerate IPv6 host addresses
  • getLocalIPAddressListForPlatform(): add AF_INET6 branches (POSIX getifaddrs and Windows GetAdaptersAddresses) to include global-scope IPv6 addresses
  • Socket::getIp(): replace gethostbyname() with getaddrinfo()
  • Socket::socketFamily: new member tracking AF_INET / AF_INET6

Note: FTP file-transfer validation (isValidClientType) still uses uint32 and will not work for pure-IPv6 clients; IPv4-mapped connections are unaffected because accept() strips the ::ffff: prefix.

@andy5995
Copy link
Copy Markdown
Collaborator Author

image image
[andy@prometheus linux](feat/ipv6-245)$ telnet 127.0.0.1 61357
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
+�v3.13-dev-Rev: 6020.31e9f441e-GNUC: 150201 [64bit]prometheus��dc25298a-0c80-11e9-8d90-4d89b054579aLinux-X64
                                                                                                             Xx���J�0���^@<��@[ڵs"N<	��,Ikٚ�v�c�!���G�ѓ��	��
Zٮ��dc� B~�����p��)^��B2���Ȉe�!V"f
P��&,�9�4/d"Q�3��C�i�T(#���u͔H� ��c�R�Y�gV��Vw�R�.�^����ܶq�C�v�r�`=��a\�K
                                                                       Z��:vs����G&o�2␦g�ߕn֛�Ҭ5M[�.�#6�������٫��dr�2{�~�B�4���\�F��ݱf�A�V*����͟�s�;�CF,XĶ%%��qE@���z>e��.��� t���}^>ph�~�f$��<��rBX�V��^CConnection closed by foreign host.
[andy@prometheus linux](feat/ipv6-245)$ telnet ::1 61357
Trying ::1...
Connected to ::1.
Escape character is '^]'.
b�v3.13-dev-Rev: 6020.31e9f441e-GNUC: 150201 [64bit]prometheus��dc25298a-0c80-11e9-8d90-4d89b054579aLinux-X64
                                                                                                             Xx���J�0���^@<��@[ڵs"N<	��,Ikٚ�v�c�!���G�ѓ��	��
Zٮ��dc� B~�����p��)^��B2���Ȉe�!V"f
P��&,�9�4/d"Q�3��C�i�T(#���u͔H� ��c�R�Y�gV��Vw�R�.�^����ܶq�C�v�r�`=��a\�K
                                                                       Z��:vs����G&o�2␦g�ߕn֛�Ҭ5M[�.�#6�������٫��dr�2{�~�B�4���\�F��ݱf�A�V*����͟�s�;�CF,XĶ%%��qE@���z>e��.��� t���}^>ph�~�f$��<��rBX�V��^CConnection closed by foreign host.
image

@andy5995
Copy link
Copy Markdown
Collaborator Author

I can connect and start a LAN game using either 127.0.0.1 or ::1. I can connect to the lobby and host a game, and join with a client. So far I've done this on the same machine, but I believe this is ready for testing.

image

andy5995 and others added 10 commits March 25, 2026 09:26
The server now binds to an AF_INET6 socket with IPV6_V6ONLY=0, which
accepts both IPv4 and IPv6 connections simultaneously on a single port.
On platforms that do not support dual-stack (e.g. OpenBSD), the server
falls back to IPv4-only automatically.

Changes:
- ServerSocket::bind(): try AF_INET6 + IPV6_V6ONLY=0, fall back to AF_INET
- ServerSocket::accept(): use sockaddr_storage; strip ::ffff: prefix from
  IPv4-mapped addresses so downstream code sees plain IPv4 strings
- ClientSocket::connect(): use getaddrinfo(AF_UNSPEC) for name resolution,
  recreating the socket as AF_INET6 when the server address requires it
- Ip class: store address as string (addrStr) to support IPv6 literals;
  byte array kept for backwards-compatible getByte()/Inet_NtoA() paths
- getLocalIPAddressList(): replace deprecated gethostbyname() with
  getaddrinfo(AF_UNSPEC) to also enumerate IPv6 host addresses
- getLocalIPAddressListForPlatform(): add AF_INET6 branches (POSIX getifaddrs
  and Windows GetAdaptersAddresses) to include global-scope IPv6 addresses
- Socket::getIp(): replace gethostbyname() with getaddrinfo()
- Socket::socketFamily: new member tracking AF_INET / AF_INET6

Note: FTP file-transfer validation (isValidClientType) still uses uint32
and will not work for pure-IPv6 clients; IPv4-mapped connections are
unaffected because accept() strips the ::ffff: prefix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two regressions introduced by the IPv6 dual-stack commit:

1. 127.0.0.1 connect broken: the UI appends '_' as a cursor character
   to label text (e.g. "127.0.0.1_").  The old Ip(string) constructor
   used atoi() which silently ignored the trailing '_', but the new
   constructor stored the raw string verbatim so getaddrinfo() received
   "127.0.0.1_" and failed.  Fix: strip trailing '_' characters in
   Ip(const string&) before storing in addrStr.

2. Entering "::1" reverted to "0.0.0.0": the host:port parser in
   MenuStateJoinGame split the text on every ':' character, so "::1"
   became host="" → Ip("") → 0.0.0.0.  Fix: treat strings with more
   than one colon as bare IPv6 addresses (no port), and support the
   standard [addr]:port bracket notation for IPv6 with a port override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LAN discovery callback builds the server entry as "ip:port" and
then appends ":port" again, producing "ip:61357:61357" in the label.
The old tokenizer handled this by taking parts[0] and parts[1] whenever
size > 1.  The new IPv6-aware tokenizer treated size > 2 as an IPv6
address and left the string unchanged, so connectToServer() received
"ip:61357:61357" as the host.

Fix: when splitting on ':', treat the result as IPv4 host:port if the
first segment contains a dot (IPv4 addresses always do; IPv6 hex groups
never do).  This correctly handles the duplicate-port suffix while still
treating bare "::1" and "2001:db8::1" as IPv6 addresses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the cases fixed by the IPv6 dual-stack work:
- IPv4 string round-trip
- Trailing '_' cursor character is stripped (UI regression fix)
- Byte constructor still works
- IPv6 string stored and returned unchanged
- IPv6 cursor character stripped
- Empty string yields 0.0.0.0

Also adds shared_lib/platform to the test directory glob in
source/tests/CMakeLists.txt so new platform tests are picked up
automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass argv[1] to runner.run() so that specific suites or individual
tests can be selected from the command line, e.g.:

  ./megaglest_tests SocketTest
  ./megaglest_tests SocketTest::test_ip_ipv6_cursor_stripped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the POST_BUILD auto-run with add_test() + enable_testing() so
tests only run when explicitly requested. Build scripts gain a -t flag
(Linux/macOS) and -run-tests switch (Windows) for local use. CI runs
ctest as a dedicated step after each build on Linux (GCC), macOS, and
FreeBSD. macOS non-bundle builds now enable tests by default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The scripts cd into build/ before running make, so --test-dir build
resolved to build/build/. Drop --test-dir and let ctest run in the
current directory instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five tests in NetworkTest verify the dual-stack server implementation:
- server binds and reports isPortBound()
- IPv4 client connects and is accepted
- accepted socket reports a plain IPv4 address (not ::ffff:...)
- IPv6 client connects and is accepted (skipped if no IPv6 dual-stack)
- accepted socket reports the IPv6 client address (skipped likewise)

Also adds Socket::getSocketFamily() to query the address family in use,
used by the tests to detect whether dual-stack is available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@andy5995
Copy link
Copy Markdown
Collaborator Author

Test #1:

@Jammyjamjamman, on an ipv4 only network, connected to my dual-stack MG server. We played for about 40 minutes with no problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant