[0.18.32] DNS/hostname with both IPv6 and IPv4, uses only IPv4, when using --mp-connect or GUI

Post your bugs and problems so we can fix them.
Post Reply
Fast Inserter
Fast Inserter
Posts: 138
Joined: Fri Mar 08, 2019 7:07 pm

[0.18.32] DNS/hostname with both IPv6 and IPv4, uses only IPv4, when using --mp-connect or GUI

Post by movax20h »

It is a rather minor issue, but.

Code: Select all

   0.173 2020-06-25 20:34:55; Factorio 0.18.32 (build 52799, linux64, alpha)
   0.338 Operating system: Linux (Debian testing)
I have a headless server running on IPv6 only, using `--bind [::1]:8181--rcon-bind [::1]:8182`

When I try to connect to it using `factorio --mp-connect localhost:8181`, or use "localhost:8181" in the GUI to connect, factorio only tries for the network address:

Code: Select all

88357.782 Joining game IP ADDR:({})
Which does fail with the error eventually. It never tries IPv6 first (despite this being my configuration AFAIK), or fallbacks to other addresses after IPv4 failed.

My configuration is very standard and not modified from default:

Code: Select all

$ getent hosts localhost
::1             localhost ip6-localhost ip6-loopback

Code: Select all

$ getent ahosts localhost
::1             STREAM localhost
::1             DGRAM  
::1             RAW       STREAM       DGRAM       RAW    
$ getent ahostsv4 localhost       STREAM localhost       DGRAM       RAW       STREAM       DGRAM       RAW    
$ getent ahostsv6 localhost
::1             STREAM localhost
::1             DGRAM  
::1             RAW    

Code: Select all

$ cat /etc/hosts | grep localhost       localhost debian
::1             localhost ip6-localhost ip6-loopback

Code: Select all

$ grep hosts /etc/nsswitch.conf 
hosts:          files mdns4_minimal [NOTFOUND=return] dns myhostname mymachines
(files mean to check `/etc/hosts.conf` first, before trying DNS resolves are configured in `resolv.conf` - standard behavior).

Code: Select all

$ egrep -v '^#' /etc/gai.conf 

Code: Select all

$ cat /etc/host.conf 
multi on  #  If set to on, the resolver library will return all valid addresses for a host that appears in the `/etc/hosts` file, instead of only the first.

Code: Select all

$ host localhost
localhost has address
localhost has IPv6 address ::1
I am guessing when there are multiple results for the host, factorio uses just the first one. I would suggest to trying all in order returned by the resolver? Similar to how web browsers maybe do it? If the system doesn't have IPv6 available (this can be tested by opening the socket to destination - if there are no route to it or the protocol is disabled system wise, it will fail opening in `connect` immedietly, with `ENETUNREACH` error), the testing can be skipped. Otherwise factorio could be trying all address in the system default order, and only return back an error if all addresses failed?

But there must be more to it, because even if I change the gai.conf to:

Code: Select all

$ egrep -v '^#' /etc/gai.conf 
label ::1/128       0   # IPv6 localhost will be returned first
label ::/0          1
label 2002::/16     2
label ::/96         3
label ::ffff:0:0/96 4    # IPv4 in "tunneled IPv6 form", will be returned, but last
precedence  ::1/128       50
precedence  ::/0          40
precedence  2002::/16     30
precedence ::/96          20
precedence ::ffff:0:0/96  10
it still doesn't work (factorio is still trying IPv4 address by default).

Similarly, if I use, ask DNS resolver (which is not used for `localhost`, because it is handled by hosts resolver), to return tunneled form for IPv4 addresses, it still doesn't.

Code: Select all

$ cat /etc/resolv.conf
nameserver 2001:1620:2777:1::10
nameserver 2001:1620:2777:2::20
options inet6  # Ask `gethostbyname` to do AAAA query before A query, and wrap IPv4 addresses (A records) as "tunneled form" back to app if there are no AAAA records. AFAIK it is ignored by `getaddrinfo`, which requires explicit `AI_V4MAPPED` in the app for same functionality.

If I change the `/etc/hosts` to only list IPv6 for localhost it does work:

Code: Select all

# cat  /etc/hosts       ipv4-localhost debian
::1             localhost ip6-localhost ip6-loopback
Connect using "localhost":

Code: Select all

  13.228 Joining game IP ADDR:({[::1]:34197})
So I think still it has something to do with only trying the first address.

I know in reality if the factorio server has DNS address (and clients asked to use it), the factorio server process should listen on all IP addresses exposed via this DNS address, so any address would work (not doing so would be a server misconfiguration really). The problem is it is not totally possible with factorio headless server. It can only listen on ALL (current and future) interfaces (using `--port`), or on only one (via `--bind`). For non-public server, it doesn't is exactly a good option, because it forces one to listen on all interfaces or one.

However I do think the factorio is not respecting current standards:

RFC 6724: https://tools.ietf.org/rfc/rfc6724.txt - Default Address Selection for Internet Protocol Version 6 (IPv6)
(obsoletes RFC 3484)
RFC 3493: https://tools.ietf.org/rfc/rfc3493.txt - Basic Socket Interface Extensions for IPv6 (obsoletes RFC 2553)
RFC 4291: https://tools.ietf.org/rfc/rfc4291.txt - IP Version 6 Addressing Architecture (obsoletes RFC 3513)
RFC 4862: https://tools.ietf.org/rfc/rfc4862.txt - IPv6 Stateless Address Autoconfiguration (obsoletes RFC 2462)

Quote from RFC 6724 , section 2:

2. Context in Which the Algorithms Operate

Well-behaved applications SHOULD NOT simply use the first address
returned from an API such as getaddrinfo() and then give up if it

And more details about treatment of IPv4 and IPv6:
3.2. IPv4 Addresses and IPv4-Mapped Addresses

The destination address selection algorithm operates on both IPv6 and
IPv4 addresses. For this purpose, IPv4 addresses MUST be represented
as IPv4-mapped addresses [RFC4291]. For example, to look up the
precedence or other attributes of an IPv4 address in the policy
table, look up the corresponding IPv4-mapped IPv6 address.

IPv4 addresses are assigned scopes as follows. IPv4 auto-
configuration addresses [RFC3927], which have the prefix 169.254/16,
are assigned link-local scope. IPv4 loopback addresses (Section of [RFC1812]), which have the prefix 127/8, are assigned
link-local scope (analogously to the treatment of the IPv6 loopback
address (Section 4 of [RFC4007])). Other IPv4 addresses (including
IPv4 private addresses [RFC1918] and Shared Address Space addresses
[RFC6598]) are assigned global scope.

IPv4 addresses MUST be treated as having "preferred" (in the RFC 4862
sense) configuration status.

3.3. Other IPv6 Addresses with Embedded IPv4 Addresses

IPv4-compatible addresses [RFC4291], IPv4-mapped [RFC4291], IPv4-
converted [RFC6145], IPv4-translatable [RFC6145], and 6to4 addresses
[RFC3056] contain an embedded IPv4 address. For the purposes of this
document, these addresses MUST be treated as having global scope.

IPv4-compatible, IPv4-mapped, and IPv4-converted addresses MUST be
treated as having "preferred" (in the RFC 4862 sense) configuration

3.4. IPv6 Loopback Address and Other Format Prefixes

The loopback address MUST be treated as having link-local scope
(Section 4 of [RFC4007]) and "preferred" (in the RFC 4862 sense)
configuration status.

"preferred" (in the RFC 4862) only means that they can be used unrestricted by the applications (i.e. they are not stale, or link-local). It doesn't determinine priority of which address to use. All "preferred" addresses (which is some set IPv6 and IPv4 addresses) are considered equal in this sense:

From RFC 4862:
preferred address - an address assigned to an interface whose use by
upper-layer protocols is unrestricted. Preferred addresses may be
used as the source (or destination) address of packets sent from
(or to) the interface.
A non-preferred address types are tentative addresses and deprecated address. These will not be used. It is more for the purpose of selecting the source address, not the destination address, or the host resolution priorities.

As far as I know `getaddrinfo` in Linux (glibc 2.30 in my case), does respect this behaviors (as tested in other apps), but Factorio is not.

It somehow feels factorio is using own non-glibc resolver, and/or sets own address sorting, and only tries the first address.

It does appear factorio uses `getaddrinfo`, as I checked with gdb and ltrace:

Code: Select all

Thread 1 "factorio" hit Breakpoint 1, __GI_getaddrinfo (name=0x7ffd6deb5ca0 "localhost", service=0x7ffd6deb5cc0 "34197", hints=0x7ffd6deb5d10, pai=0x7ffd6deb5c88) at ../sysdeps/posix/getaddrinfo.c:2161
2161	in ../sysdeps/posix/getaddrinfo.c
(gdb) bt
#0  __GI_getaddrinfo (name=0x7ffd6deb5ca0 "localhost", service=0x7ffd6deb5cc0 "34197", hints=0x7ffd6deb5d10, pai=0x7ffd6deb5c88) at ../sysdeps/posix/getaddrinfo.c:2161
#1  0x0000000000ff69d8 in SocketAddress::SocketAddress () at /tmp/factorio-build-6v7ua5/src/Net/SocketAddress.cpp:117
#2  0x0000000000ff6b5b in NamedSocketAddress::resolve () at /tmp/factorio-build-6v7ua5/src/Net/NamedSocketAddress.cpp:12
#3  0x0000000001001164 in NamedSocketAddress::getAddress () at /tmp/factorio-build-6v7ua5/src/Net/NamedSocketAddress.cpp:5
#4  MultiplayerConnectSettings::getAddress () at /tmp/factorio-build-6v7ua5/src/Net/MultiplayerConnectSettings.cpp:27
#5  ClientRouter::joinGame () at /tmp/factorio-build-6v7ua5/src/Net/ClientRouter.cpp:40
#6  0x00000000011c3b22 in ClientMultiplayerManager::joinGame () at /tmp/factorio-build-6v7ua5/src/Net/ClientMultiplayerManager.cpp:271
(gdb) print *hints
$4 = {ai_flags = 1024, ai_family = 0, ai_socktype = 2, ai_protocol = 0, ai_addrlen = 0, ai_addr = 0x0, ai_canonname = 0x0, ai_next = 0x0}
`ai_flags = 1024`, is equivelent to `ai_flags = AI_NUMERICSERV`, which is OK (it asks getaddrinfo to only resolve the `service` from string to integer, without checking service resolution, i.e. in `/etc/services` or DNS SRV). That is OK.

`ai_family = 0` means `AF_UNSPEC`, which is also OK (return both AF_INET and AF_INET6).

`ai_socktype = 2` means `SOCK_DGRAM`, that is OK.

All is good, and if I trace it manually after the return:

Code: Select all

Thread 1 "factorio" hit Breakpoint 1, __GI_getaddrinfo (name=0x7ffd6deb5ca0 "localhost", service=0x7ffd6deb5cc0 "34197", hints=0x7ffd6deb5d10, pai=0x7ffd6deb5c88) at ../sysdeps/posix/getaddrinfo.c:2161
2161	../sysdeps/posix/getaddrinfo.c: No such file or directory.
(gdb) print pai
$8 = (struct addrinfo **) 0x7ffd6deb5c88

(gdb) advance getaddrinfo
0x0000000000ff69d8 in SocketAddress::SocketAddress () at /tmp/factorio-build-6v7ua5/src/Net/SocketAddress.cpp:117
117	/tmp/factorio-build-6v7ua5/src/Net/SocketAddress.cpp: No such file or directory.

(gdb) print **(struct addrinfo**)(0x7ffd6deb5c88)
$17 = {ai_flags = 1024, ai_family = 10, ai_socktype = 2, ai_protocol = 17, ai_addrlen = 28, ai_addr = 0x4a996f0, ai_canonname = 0x0, ai_next = 0xae6dc10}
(gdb) print *(**(struct addrinfo**)(0x7ffd6deb5c88))->ai_next
$20 = {ai_flags = 1024, ai_family = 2, ai_socktype = 2, ai_protocol = 17, ai_addrlen = 16, ai_addr = 0xae6dc40, ai_canonname = 0x0, ai_next = 0x0}

(gdb) print *(**(struct addrinfo**)(0x7ffd6deb5c88)).ai_addr
$22 = {sa_family = 10, sa_data = "\205\225", '\000' <repeats 11 times>}
(gdb) print *(*(**(struct addrinfo**)(0x7ffd6deb5c88))->ai_next).ai_addr
$26 = {sa_family = 2, sa_data = "\205\225\177\000\000\001\000\000\000\000\000\000\000"}
I can see glibc does return two addresses for "localhost", one IPv6 and one IPv4.

Code: Select all

"\205\225", '\000' <repeats 11 times> corresponds to (sockaddr_in6 value) of [::1]:34197
'\205\225\177\000\000\001...' corresponds to (sockaddr_in value) of  (0205 * 256 + 0225 == 34197)

(gdb) print ((**(struct addrinfo**)(0x7ffd6deb5c88)).ai_addr.sa_data)[/*port*/2+/*flow_info*/4+/*address_length*/16-1/*last_digit*/]
$73 = 1 '\001'   # 1 in `0000:0000:0000:0000:0000:0000:0000:0001`
So factorio knows about both addresses, and as you can see glibc returns IPv6 as the first one.

Post Reply

Return to “Bug Reports”

Who is online

Users browsing this forum: p4wana