Skip to content

Commit dd519a7

Browse files
committed
Fix connection failure that happens on iOS when connecting to a Tailscale IPv4 address from an IPv6-only mobile network such as T-Mobile.
T-Mobile handles IPv4 addresses, including the special 100.x.x.x Tailscale addresses, using 464xlat and NAT64. This patch detects the 464XLAT situation and forces the address into AF_INET IPv4 mode, allowing the connection to reach Tailscale.
1 parent 58902e3 commit dd519a7

File tree

3 files changed

+150
-5
lines changed

3 files changed

+150
-5
lines changed

src/Connection.c

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ void LiStopConnection(void) {
136136
Limelog("done\n");
137137
}
138138
LC_ASSERT(stage == STAGE_NONE);
139-
139+
140140
if (RemoteAddrString != NULL) {
141141
free(RemoteAddrString);
142142
RemoteAddrString = NULL;
@@ -281,7 +281,7 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre
281281

282282
alreadyTerminated = false;
283283
ConnectionInterrupted = false;
284-
284+
285285
// Validate the audio configuration
286286
if (MAGIC_BYTE_FROM_AUDIO_CONFIG(StreamConfig.audioConfiguration) != 0xCA ||
287287
CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(StreamConfig.audioConfiguration) > AUDIO_CONFIGURATION_MAX_CHANNEL_COUNT) {
@@ -328,7 +328,7 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre
328328
Limelog("Disabling reference frame invalidation for 4K streaming with GFE\n");
329329
VideoCallbacks.capabilities &= ~CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
330330
}
331-
331+
332332
Limelog("Initializing platform...");
333333
ListenerCallbacks.stageStarting(STAGE_PLATFORM_INIT);
334334
err = initializePlatform();
@@ -380,7 +380,26 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre
380380
stage++;
381381
LC_ASSERT(stage == STAGE_NAME_RESOLUTION);
382382
ListenerCallbacks.stageComplete(STAGE_NAME_RESOLUTION);
383-
Limelog("done\n");
383+
384+
#ifdef AF_INET6
385+
// Handle the special case of wanting to connect to an IPv4 address or IPv4-only domain
386+
// while on an IPv6-only network. This is common with mobile providers such as T-Mobile.
387+
if (RemoteAddr.ss_family == AF_INET6 && isIPv4Address(serverInfo->address)) {
388+
if (is464XLATSynthesizedAddress(&RemoteAddr, serverInfo->address)) {
389+
// we must treat this as an IPv4 address so it gets routed correctly
390+
logWithSockaddrStorage(&RemoteAddr, "IPv4 address was resolved to synthesized IPv6 address %s, this network might be using 464XLAT\n");
391+
392+
err = resolveHostName(serverInfo->address, AF_INET, 47984, &RemoteAddr, &AddrLen);
393+
if (err != 0) {
394+
Limelog("resolveHostName for AF_INET failed: %d\n", err);
395+
ListenerCallbacks.stageFailed(STAGE_NAME_RESOLUTION, err);
396+
goto Cleanup;
397+
}
398+
399+
logWithSockaddrStorage(&RemoteAddr, "IPv4 address was restored via AF_INET: %s\n");
400+
}
401+
}
402+
#endif
384403

385404
// If STREAM_CFG_AUTO was requested, determine the streamingRemotely value
386405
// now that we have resolved the target address and impose the video packet
@@ -517,7 +536,7 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre
517536
LC_ASSERT(stage == STAGE_INPUT_STREAM_START);
518537
ListenerCallbacks.stageComplete(STAGE_INPUT_STREAM_START);
519538
Limelog("done\n");
520-
539+
521540
// Wiggle the mouse a bit to wake the display up
522541
LiSendMouseMoveEvent(1, 1);
523542
PltSleepMs(10);

src/PlatformSockets.c

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,18 @@ int enableNoDelay(SOCKET s) {
600600
return 0;
601601
}
602602

603+
static void logWithSockaddr(const char *format, const char *arg, const struct sockaddr *addr, socklen_t len) {
604+
char host[NI_MAXHOST], service[NI_MAXSERV];
605+
int res = getnameinfo(addr, len, host, sizeof(host), service, sizeof(service),
606+
NI_NUMERICHOST | NI_NUMERICSERV);
607+
if (res == 0) {
608+
Limelog(format, arg, host, service);
609+
}
610+
else {
611+
Limelog("getnameinfo() failed: %s\n", gai_strerror(res));
612+
}
613+
}
614+
603615
int resolveHostName(const char* host, int family, int tcpTestPort, struct sockaddr_storage* addr, SOCKADDR_LEN* addrLen)
604616
{
605617
struct addrinfo hints, *res, *currentAddr;
@@ -621,6 +633,8 @@ int resolveHostName(const char* host, int family, int tcpTestPort, struct sockad
621633
}
622634

623635
for (currentAddr = res; currentAddr != NULL; currentAddr = currentAddr->ai_next) {
636+
logWithSockaddr("Resolved %s to %s %s\n", host, currentAddr->ai_addr, sizeof(currentAddr->ai_addr));
637+
624638
// Use the test port to ensure this address is working if:
625639
// a) We have multiple addresses
626640
// b) The caller asked us to test even with a single address
@@ -634,6 +648,7 @@ int resolveHostName(const char* host, int family, int tcpTestPort, struct sockad
634648
continue;
635649
}
636650
else {
651+
Limelog("tcp test on port %d is ok\n", tcpTestPort);
637652
closeSocket(testSocket);
638653
}
639654
}
@@ -829,6 +844,79 @@ bool isNat64SynthesizedAddress(struct sockaddr_storage* address) {
829844
return false;
830845
}
831846

847+
static uint32_t ipv4ToHex(const char *ipv4Str) {
848+
struct in_addr addr;
849+
if (inet_pton(AF_INET, ipv4Str, &addr) != 1) {
850+
Limelog("ipv4ToHex: invalid IPv4 address: %s\n", ipv4ToHex);
851+
return 0;
852+
}
853+
return ntohl(addr.s_addr);
854+
}
855+
856+
// Return true if the given IPv6 sockaddr_storage is a synthesized address that
857+
// encapsulates an IPv4 address for use on an IPv6-only network.
858+
// See RFC 7050 and 8880 for more details.
859+
// input: IPv6 address as a struct sockaddr_storage *, and an IPv4 string
860+
bool is464XLATSynthesizedAddress(struct sockaddr_storage* ipv6Address, const char *ipv4Str) {
861+
bool ret = false;
862+
863+
#ifdef AF_INET6
864+
int err;
865+
struct addrinfo hints, *res, *currentAddr;
866+
memset(&hints, 0, sizeof(hints));
867+
868+
// perform an AAAA lookup on ipv4only.arpa
869+
// If this returns one or more responses, the network has a CLAT that is synthesizing IPv6 addresses
870+
hints.ai_family = AF_INET6; // will query AAAA only
871+
hints.ai_socktype = SOCK_STREAM;
872+
hints.ai_flags = AI_CANONNAME;
873+
err = getaddrinfo("ipv4only.arpa", NULL, &hints, &res);
874+
if (err != 0) {
875+
Limelog("is464XLATSynthesizedAddress getaddrinfo(ipv4only.arpa) failed: %d\n", err);
876+
return false;
877+
}
878+
else if (res == NULL) {
879+
return false;
880+
}
881+
882+
char ipv6AddressStr[INET6_ADDRSTRLEN];
883+
memset(&ipv6AddressStr, 0, sizeof(ipv6AddressStr));
884+
struct sockaddr_in6 *ipv6Addr = (struct sockaddr_in6 *)ipv6Address;
885+
inet_ntop(AF_INET6, &ipv6Addr->sin6_addr, ipv6AddressStr, sizeof(ipv6AddressStr));
886+
887+
// Look through all responses, we may see the Well Known Prefix 64:ff9b::/96 as well as
888+
// Network Specific Prefixes such as T-Mobile's 2607:7700:0:51::*
889+
char currentAddrStr[INET6_ADDRSTRLEN];
890+
for (currentAddr = res; currentAddr != NULL; currentAddr = currentAddr->ai_next) {
891+
memset(&currentAddrStr, 0, sizeof(currentAddrStr));
892+
struct sockaddr_in6 *addr = (struct sockaddr_in6 *)currentAddr->ai_addr;
893+
inet_ntop(AF_INET6, &addr->sin6_addr, currentAddrStr, sizeof(currentAddrStr));
894+
895+
// Check first 96 bits of IPv6 for matching prefix
896+
if (memcmp(&ipv6Addr->sin6_addr, &addr->sin6_addr, 12) == 0) {
897+
// The prefixes match; now compare the IPv4 portion
898+
uint32_t ipv6Hex;
899+
memcpy(&ipv6Hex, ((uint8_t*)&ipv6Addr->sin6_addr) + 12, sizeof(ipv6Hex));
900+
ipv6Hex = ntohl(ipv6Hex);
901+
902+
if (ipv6Hex == ipv4ToHex(ipv4Str)) {
903+
ret = true;
904+
break;
905+
}
906+
}
907+
}
908+
909+
freeaddrinfo(res);
910+
#endif
911+
912+
return ret;
913+
}
914+
915+
bool isIPv4Address(const char *str) {
916+
struct in_addr addr;
917+
return inet_pton(AF_INET, str, &addr) == 1;
918+
}
919+
832920
// Enable platform-specific low latency options (best-effort)
833921
void enterLowLatencyMode(void) {
834922
#if defined(LC_WINDOWS_DESKTOP)
@@ -959,3 +1047,36 @@ void cleanupPlatformSockets(void) {
9591047
#else
9601048
#endif
9611049
}
1050+
1051+
void logWithSockaddrStorage(struct sockaddr_storage* addr, const char *format) {
1052+
void *src = NULL;
1053+
1054+
#ifdef AF_INET6
1055+
char ip[INET6_ADDRSTRLEN];
1056+
#else
1057+
char ip[INET_ADDRSTRLEN];
1058+
#endif
1059+
1060+
#ifdef AF_INET6
1061+
if (addr->ss_family == AF_INET6) {
1062+
struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)addr;
1063+
src = &(addr_in6->sin6_addr);
1064+
}
1065+
else
1066+
#endif
1067+
if (addr->ss_family == AF_INET) {
1068+
struct sockaddr_in *addr_in = (struct sockaddr_in *)addr;
1069+
src = &(addr_in->sin_addr);
1070+
}
1071+
else {
1072+
Limelog("Unknown address family: %d\n", addr->ss_family);
1073+
return;
1074+
}
1075+
1076+
if (inet_ntop(addr->ss_family, src, ip, sizeof(ip)) == NULL) {
1077+
Limelog("invalid struct sockaddr_storage\n");
1078+
return;
1079+
}
1080+
1081+
Limelog(format, ip);
1082+
}

src/PlatformSockets.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ typedef int SOCKADDR_LEN;
6363
#include <errno.h>
6464
#include <signal.h>
6565
#include <poll.h>
66+
#include <stdarg.h>
6667

6768
#define ioctlsocket ioctl
6869
#define LastSocketError() errno
@@ -111,6 +112,8 @@ int setNonFatalRecvTimeoutMs(SOCKET s, int timeoutMs);
111112
void closeSocket(SOCKET s);
112113
bool isPrivateNetworkAddress(struct sockaddr_storage* address);
113114
bool isNat64SynthesizedAddress(struct sockaddr_storage* address);
115+
bool is464XLATSynthesizedAddress(struct sockaddr_storage* address, const char *ipv4Str);
116+
bool isIPv4Address(const char *str);
114117
int pollSockets(struct pollfd* pollFds, int pollFdsCount, int timeoutMs);
115118
bool isSocketReadable(SOCKET s);
116119

@@ -123,3 +126,5 @@ void exitLowLatencyMode(void);
123126

124127
int initializePlatformSockets(void);
125128
void cleanupPlatformSockets(void);
129+
130+
void logWithSockaddrStorage(struct sockaddr_storage* addr, const char *format);

0 commit comments

Comments
 (0)