Repository: FreeRDP/FreeRDP (C)

Issue:
`+ipv6` does not work under Linux
**Describe the bug**
`+ipv6` doesn't seem to work under Arch Linux. It still just obeys `/etc/gai.conf`.

**To Reproduce**
Steps to reproduce the behavior:
1. Uncomment `precedence ::ffff:0:0/96  100` in `/etc/gai.conf` as per https://wiki.archlinux.org/title/IPv6#Prefer_IPv4_over_IPv6
2. `xfreerdp3 /v:ip.me +ipv6 /log-level:TRACE`
3. can see log: 'connecting to peer 212.102.35.236'
4. `xfreerdp3 /v:localhost +ipv6 /log-level:TRACE`
5. can see log: 'connecting to peer 127.0.0.1'

**Expected behavior**
It prefers IPv6 given `+ipv6`.

**Application details**
* FreeRDP version (`xfreerdp /version`): 3.5.1
* Command line used: see above
* Output of `xfreerdp /buildconfig`
```
This is FreeRDP version 3.5.1 (n/a)
Build configuration: BUILD_TESTING=OFF WINPR_HAVE_AIO_H=1 WINPR_HAVE_EXECINFO_BACKTRACE=1 WINPR_HAVE_EXECINFO_BACKTRACE_SYMBOLS=1 WINPR_HAVE_EXECINFO_BACKTRACE_SYMBOLS_FD=1 WINPR_HAVE_EXECINFO_HEADER=1 WINPR_HAVE_FCNTL_H=1 WINPR_HAVE_GETLOGIN_R=1 WINPR_HAVE_GETPWUID_R=1 WINPR_HAVE_INTTYPES_H=1 WINPR_HAVE_POLL_H=1 WINPR_HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIB=1 WINPR_HAVE_PTHREAD_MUTEX_TIMEDLOCK_LIBS= WINPR_HAVE_PTHREAD_MUTEX_TIMEDLOCK_SYMBOL=1 WINPR_HAVE_STDBOOL_H=1 WINPR_HAVE_STDINT_H=1 WINPR_HAVE_STRNDUP=1 WINPR_HAVE_SYSLOG_H=1 WINPR_HAVE_SYS_EVENTFD_H=1 WINPR_HAVE_SYS_FILIO_H= WINPR_HAVE_SYS_SELECT_H=1 WINPR_HAVE_SYS_SOCKIO_H= WINPR_HAVE_SYS_TIMERFD_H=1 WINPR_HAVE_TM_GMTOFF=1 WINPR_HAVE_UNISTD_H=1 WINPR_HAVE_UNWIND_H=1 WITH_AAD=ON WITH_ABSOLUTE_PLUGIN_LOAD_PATHS=ON WITH_ADD_PLUGIN_TO_RPATH=OFF WITH_ALSA=ON WITH_BINARY_VERSIONING=ON WITH_CAIRO=OFF WITH_CCACHE=ON WITH_CHANNELS=ON WITH_CLANG_FORMAT=ON WITH_CLIENT=ON WITH_CLIENT_AVAILABLE=1 WITH_CLIENT_CHANNELS=ON WITH_CLIENT_CHANNELS_AVAILABLE=1 WITH_CLIENT_COMMON=ON WITH_CLIENT_INTERFACE=OFF WITH_CLIENT_SDL=ON WITH_CLIENT_SDL_AVAILABLE=1 WITH_CUPS=ON WITH_DEBUG_ALL=OFF WITH_DEBUG_CAPABILITIES=OFF WITH_DEBUG_CERTIFICATE=OFF WITH_DEBUG_CHANNELS=OFF WITH_DEBUG_CLIPRDR=OFF WITH_DEBUG_CODECS=OFF WITH_DEBUG_DVC=OFF WITH_DEBUG_EVENTS=OFF WITH_DEBUG_KBD=OFF WITH_DEBUG_LICENSE=OFF WITH_DEBUG_MUTEX=OFF WITH_DEBUG_NEGO=OFF WITH_DEBUG_NLA=OFF WITH_DEBUG_NTLM=OFF WITH_DEBUG_RAIL=OFF WITH_DEBUG_RDP=OFF WITH_DEBUG_RDPDR=OFF WITH_DEBUG_RDPEI=OFF WITH_DEBUG_RDPGFX=OFF WITH_DEBUG_REDIR=OFF WITH_DEBUG_RFX=OFF WITH_DEBUG_RINGBUFFER=OFF WITH_DEBUG_SCARD=OFF WITH_DEBUG_SCHANNEL=OFF WITH_DEBUG_SDL_EVENTS=OFF WITH_DEBUG_SDL_KBD_EVENTS=OFF WITH_DEBUG_SND=OFF WITH_DEBUG_SVC=OFF WITH_DEBUG_SYMBOLS=OFF WITH_DEBUG_THREADS=OFF WITH_DEBUG_TIMEZONE=OFF WITH_DEBUG_TRANSPORT=OFF WITH_DEBUG_TSG=OFF WITH_DEBUG_TSMF=OFF WITH_DEBUG_TSMF_AVAILABLE=0 WITH_DEBUG_URBDRC=OFF WITH_DEBUG_WND=OFF WITH_DEBUG_X11=OFF WITH_DEBUG_X11_LOCAL_MOVESIZE=OFF WITH_DEBUG_XV=OFF WITH_DSP_EXPERIMENTAL=OFF WITH_DSP_FFMPEG=ON WITH_DSP_FFMPEG_AVAILABLE=1 WITH_EVENTFD_READ_WRITE=1 WITH_FAAC=OFF WITH_FAAD2=OFF WITH_FFMPEG=ON WITH_FREERDP_DEPRECATED=OFF WITH_FREERDP_DEPRECATED_COMMANDLINE=OFF WITH_FUSE=ON WITH_GFX_H264=ON WITH_GPROF=OFF WITH_GSM=OFF WITH_ICU=ON WITH_INTERNAL_MD4=OFF WITH_INTERNAL_MD5=OFF WITH_INTERNAL_RC4=OFF WITH_JPEG=ON WITH_KRB5=ON WITH_KRB5_NO_NTLM_FALLBACK=OFF WITH_LAME=OFF WITH_LIBRARY_VERSIONING=ON WITH_LIBRESSL=OFF WITH_LODEPNG=OFF WITH_MACAUDIO=OFF WITH_MACAUDIO_AVAILABLE=0 WITH_MANPAGES=ON WITH_MBEDTLS=OFF WITH_NATIVE_SSPI=OFF WITH_NEON=OFF WITH_OPENCL=OFF WITH_OPENH264=OFF WITH_OPENSC_PKCS11_LINKED=OFF WITH_OPENSSL=ON WITH_OPUS=OFF WITH_OSS=ON WITH_PCSC=ON WITH_PKCS11=ON WITH_PLATFORM_SERVER=ON WITH_POLL=ON WITH_PROFILER=OFF WITH_PROXY=ON WITH_PROXY_APP=ON WITH_PROXY_EMULATE_SMARTCARD=OFF WITH_PROXY_MODULES=ON WITH_PULSE=ON WITH_RDTK=ON WITH_SAMPLE=ON WITH_SANITIZE_ADDRESS=OFF WITH_SANITIZE_ADDRESS_AVAILABLE=1 WITH_SANITIZE_MEMORY=OFF WITH_SANITIZE_MEMORY_AVAILABLE=1 WITH_SANITIZE_THREAD=OFF WITH_SANITIZE_THREAD_AVAILABLE=1 WITH_SDL_IMAGE_DIALOGS=OFF WITH_SDL_LINK_SHARED=ON WITH_SERVER=ON WITH_SERVER_CHANNELS=ON WITH_SERVER_INTERFACE=ON WITH_SHADOW=ON WITH_SMARTCARD_EMULATE=ON WITH_SMARTCARD_INSPECT=OFF WITH_SMARTCARD_PCSC=ON WITH_SOXR=OFF WITH_SSE2=OFF WITH_SWSCALE=ON WITH_SYSTEMD=ON WITH_THIRD_PARTY=OFF WITH_UNICODE_BUILTIN=OFF WITH_URIPARSER=OFF WITH_VAAPI=OFF WITH_VAAPI_AVAILABLE=1 WITH_VALGRIND_MEMCHECK=OFF WITH_VALGRIND_MEMCHECK_AVAILABLE=1 WITH_VERBOSE_WINPR_ASSERT=ON WITH_VIDEO_FFMPEG=ON WITH_VIDEO_FFMPEG_AVAILABLE=1 WITH_WAYLAND=ON WITH_WEBVIEW=ON WITH_WEBVIEW_QT=OFF WITH_WINPR_DEPRECATED=OFF WITH_WINPR_TOOLS=ON WITH_WIN_CONSOLE=ON WITH_X11=ON WITH_XCURSOR=ON WITH_XEXT=ON WITH_XFIXES=ON WITH_XI=ON WITH_XINERAMA=ON WITH_XRANDR=ON WITH_XRENDER=ON WITH_XV=ON
Build type:          None
CFLAGS:              -march=x86-64 -mtune=generic -O2 -pipe -fno-plt -fexceptions         -Wp,-D_FORTIFY_SOURCE=3 -Wformat -Werror=format-security         -fstack-clash-protection -fcf-protection         -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -g -ffile-prefix-map=/build/freerdp/src=/usr/src/debug/freerdp -flto=auto -Wno-incompatible-pointer-types -Wall -Wpedantic -Wno-padded -Wno-cast-align -Wno-declaration-after-statement -fPIC -Wall -fvisibility=hidden -Wimplicit-function-declaration -Wredundant-decls -g -fno-omit-frame-pointer
Compiler:            GNU, 14.1.1
Target architecture: x64
Keyboard Shortcuts:
        <Right CTRL>
                releases keyboard and mouse grab
        <CTRL>+<ALT>+<Return>
                toggles fullscreen state of the application
        <CTRL>+<ALT>+c
                toggles remote control in a remote assistance session
        Action Script
                Executes a predefined script on key press.
                Should the script not exist it is ignored.
                Scripts can be provided at the default localtion ~/.config/freerdp/action.sh or as command line argument /action:script:<path>
                The script will receive the current key combination as argument.
                The output of the script is parsed for 'key-local' which tells that the script used the key combination, otherwise the combination is forwarded to the remote.
```

**Environment (please complete the following information):**
 - OS: Arch Linux
 - Version/Distribution: rolling
 - Architecture: amd64

**Additional context**
With my `/etc/gai.conf`, here's a `curl` output:

```
$ nslookup ip.me
Server:         x.x.x.x
Address:        x.x.x.x#53

Non-authoritative answer:
Name:   ip.me
Address: 212.102.35.236
Name:   ip.me
Address: 2a02:6ea0:c035::11

$ curl -v ip.me
* Host ip.me:80 was resolved.
* IPv6: 2a02:6ea0:c035::11
* IPv4: 212.102.35.236
*   Trying 212.102.35.236:80...
*   Trying [2a02:6ea0:c035::11]:80...
* Connected to ip.me (212.102.35.236) port 80
> GET / HTTP/1.1
> Host: ip.me
> User-Agent: curl/8.8.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.18.0
< Date: Thu, 27 Jun 2024 13:01:00 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 15
< Connection: keep-alive
<
```



Discussion: same test works here on my debian and fedora machines.

1. connect with `xfreerdp /v:localhost +ipv6 /log-level:trace`
1. find in log: `[15:56:05:949] [10642:00002993] [DEBUG][com.freerdp.core] - [freerdp_tcp_default_connect]: connecting to peer ::1`
question: what does `getaddrinfo` return on your machine? (first entry, ...)
Sorry I'm AFK now so I can't test on the same machine. But I found a simple C program https://gist.github.com/jirihnidek/bf7a2363e480491da72301b228b35d5d, SSH'ed into an Arch box with the same config, and here's the output: 
```sh
$ printf 'localhost\nip.me\n' | ./getaddrinfo_example
Type domain name:
Host: localhost
IPv4 address: 127.0.0.1 (localhost)
IPv6 address: ::1 ((null))
Type domain name:
Host: ip.me
IPv4 address: 212.102.35.236 (ip.me)
IPv6 address: 2a02:6ea0:c035::11 ((null))
Type domain name:
```

If I comment out that line in gai.conf:
```sh
$ printf 'localhost\nip.me\n' | ./getaddrinfo_example
Type domain name:
Host: localhost
IPv6 address: ::1 (localhost)
IPv4 address: 127.0.0.1 ((null))
Type domain name:
Host: ip.me
IPv6 address: 2a02:6ea0:c035::11 (ip.me)
IPv4 address: 212.102.35.236 ((null))
Type domain name:
```
@Frederick888 ok, indeed the ting I suspected.
your `DNS` query responds with`Ipv4` first and `IPv6` later.



Fix (patch):
diff --git a/client/common/cmdline.c b/client/common/cmdline.c
index dcb8b8d02df8..9cf95db6c5c6 100644
--- a/client/common/cmdline.c
+++ b/client/common/cmdline.c
@@ -4554,10 +4554,31 @@ static int freerdp_client_settings_parse_command_line_arguments_int(
 			if (!freerdp_settings_set_bool(settings, FreeRDP_RedirectHomeDrive, enable))
 				return COMMAND_LINE_ERROR;
 		}
+		CommandLineSwitchCase(arg, "ipv4")
+		{
+			if (strncmp(arg->Value, "force", 6) == 0)
+			{
+				if (!freerdp_settings_set_uint32(settings, FreeRDP_ForceIPvX, 4))
+					return COMMAND_LINE_ERROR;
+			}
+			else
+			{
+				if (!freerdp_settings_set_bool(settings, FreeRDP_PreferIPv6OverIPv4, FALSE))
+					return COMMAND_LINE_ERROR;
+			}
+		}
 		CommandLineSwitchCase(arg, "ipv6")
 		{
-			if (!freerdp_settings_set_bool(settings, FreeRDP_PreferIPv6OverIPv4, enable))
-				return COMMAND_LINE_ERROR;
+			if (strncmp(arg->Value, "force", 6) == 0)
+			{
+				if (!freerdp_settings_set_uint32(settings, FreeRDP_ForceIPvX, 6))
+					return COMMAND_LINE_ERROR;
+			}
+			else
+			{
+				if (!freerdp_settings_set_bool(settings, FreeRDP_PreferIPv6OverIPv4, TRUE))
+					return COMMAND_LINE_ERROR;
+			}
 		}
 		CommandLineSwitchCase(arg, "clipboard")
 		{
diff --git a/client/common/cmdline.h b/client/common/cmdline.h
index ad46ee7219f3..3d0de3313db0 100644
--- a/client/common/cmdline.h
+++ b/client/common/cmdline.h
@@ -247,7 +247,9 @@ static const COMMAND_LINE_ARGUMENT_A global_cmd_args[] = {
 	  "Print help" },
 	{ "home-drive", COMMAND_LINE_VALUE_BOOL, NULL, BoolValueFalse, NULL, -1, NULL,
 	  "Redirect user home as share" },
-	{ "ipv6", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, "6",
+	{ "ipv4", COMMAND_LINE_VALUE_OPTIONAL, "[:force]", NULL, NULL, -1, "4",
+	  "Prefer IPv4 AAA record over IPv6 A record" },
+	{ "ipv6", COMMAND_LINE_VALUE_OPTIONAL, "[:force]", NULL, NULL, -1, "6",
 	  "Prefer IPv6 AAA record over IPv4 A record" },
 #if defined(WITH_JPEG)
 	{ "jpeg", COMMAND_LINE_VALUE_FLAG, NULL, NULL, NULL, -1, NULL, "JPEG codec support" },
diff --git a/include/freerdp/settings_types_private.h b/include/freerdp/settings_types_private.h
index 2c95a663973c..099e689ccafa 100644
--- a/include/freerdp/settings_types_private.h
+++ b/include/freerdp/settings_types_private.h
@@ -746,7 +746,8 @@ struct rdp_settings
 	SETTINGS_DEPRECATED(ALIGN64 BOOL RedirectSerialPorts);   /* 4672 */
 	SETTINGS_DEPRECATED(ALIGN64 BOOL RedirectParallelPorts); /* 4673 */
 	SETTINGS_DEPRECATED(ALIGN64 BOOL PreferIPv6OverIPv4);    /* 4674 */
-	UINT64 padding4800[4800 - 4675];                         /* 4675 */
+	SETTINGS_DEPRECATED(ALIGN64 UINT32 ForceIPvX);           /* 4675 */
+	UINT64 padding4800[4800 - 4676];                         /* 4676 */
 
 	/**
 	 * Other Redirection
diff --git a/libfreerdp/common/settings_getters.c b/libfreerdp/common/settings_getters.c
index 352a285e763f..0ae9b0ff9d8a 100644
--- a/libfreerdp/common/settings_getters.c
+++ b/libfreerdp/common/settings_getters.c
@@ -1736,6 +1736,9 @@ UINT32 freerdp_settings_get_uint32(const rdpSettings* settings, FreeRDP_Settings
 		case FreeRDP_Floatbar:
 			return settings->Floatbar;
 
+		case FreeRDP_ForceIPvX:
+			return settings->ForceIPvX;
+
 		case FreeRDP_FrameAcknowledge:
 			return settings->FrameAcknowledge;
 
@@ -2169,6 +2172,10 @@ BOOL freerdp_settings_set_uint32(rdpSettings* settings, FreeRDP_Settings_Keys_UI
 			settings->Floatbar = cnv.c;
 			break;
 
+		case FreeRDP_ForceIPvX:
+			settings->ForceIPvX = cnv.c;
+			break;
+
 		case FreeRDP_FrameAcknowledge:
 			settings->FrameAcknowledge = cnv.c;
 			break;
diff --git a/libfreerdp/common/settings_str.h b/libfreerdp/common/settings_str.h
index 75e24718cd45..239185ab2c16 100644
--- a/libfreerdp/common/settings_str.h
+++ b/libfreerdp/common/settings_str.h
@@ -334,6 +334,7 @@ static const struct settings_str_entry settings_map[] = {
 	{ FreeRDP_FakeMouseMotionInterval, FREERDP_SETTINGS_TYPE_UINT32,
 	  "FreeRDP_FakeMouseMotionInterval" },
 	{ FreeRDP_Floatbar, FREERDP_SETTINGS_TYPE_UINT32, "FreeRDP_Floatbar" },
+	{ FreeRDP_ForceIPvX, FREERDP_SETTINGS_TYPE_UINT32, "FreeRDP_ForceIPvX" },
 	{ FreeRDP_FrameAcknowledge, FREERDP_SETTINGS_TYPE_UINT32, "FreeRDP_FrameAcknowledge" },
 	{ FreeRDP_GatewayAcceptedCertLength, FREERDP_SETTINGS_TYPE_UINT32,
 	  "FreeRDP_GatewayAcceptedCertLength" },
diff --git a/libfreerdp/core/tcp.c b/libfreerdp/core/tcp.c
index f424cc048186..7712ca915cbf 100644
--- a/libfreerdp/core/tcp.c
+++ b/libfreerdp/core/tcp.c
@@ -1176,21 +1176,46 @@ int freerdp_tcp_default_connect(rdpContext* context, rdpSettings* settings, cons
 			}
 			freerdp_set_last_error_log(context, 0);
 
+			/* By default we take the first returned entry.
+			 *
+			 * If PreferIPv6OverIPv4 = TRUE we force to IPv6 if there
+			 * is such an address available, but fall back to first if not found
+			 */
 			addr = result;
+			if (freerdp_settings_get_bool(settings, FreeRDP_PreferIPv6OverIPv4))
+			{
+				while (addr && (addr->ai_family != AF_INET6))
+					addr = addr->ai_next;
+				if (!addr)
+					addr = result;
+			}
 
-			if ((addr->ai_family == AF_INET6) && (addr->ai_next != 0) &&
-			    !settings->PreferIPv6OverIPv4)
+			/* We want to force IPvX, abort if not detected */
+			const UINT32 IPvX = freerdp_settings_get_uint32(settings, FreeRDP_ForceIPvX);
+			switch (IPvX)
 			{
-				while ((addr = addr->ai_next))
+				case 4:
+				case 6:
 				{
-					if (addr->ai_family == AF_INET)
-						break;
+					const int family = (IPvX == 4) ? AF_INET : AF_INET6;
+					while (addr && (addr->ai_family != family))
+						addr = addr->ai_next;
+					if (!addr)
+					{
+						freerdp_set_last_error_if_not(context, FREERDP_ERROR_DNS_NAME_NOT_FOUND);
+						return -1;
+					}
 				}
-
-				if (!addr)
-					addr = result;
+				break;
+				default:
+					break;
 			}
 
+			if (!addr)
+			{
+				freerdp_set_last_error_if_not(context, FREERDP_ERROR_DNS_NAME_NOT_FOUND);
+				return -1;
+			}
 			sockfd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
 
 			if (sockfd < 0)