-
Couldn't load subscription status.
- Fork 7.9k
Description
Answers checklist.
- I have read the documentation ESP-IDF Programming Guide and the issue is not addressed there.
- I have updated my IDF branch (master or release) to the latest version and checked that the issue is present there.
- I have searched the issue tracker for a similar issue and not found a similar issue.
IDF version.
master
Espressif SoC revision.
ESP32
Operating System used.
Linux
How did you build your project?
Command line with idf.py
If you are using Windows, please specify command line type.
None
Development Kit.
M5Stack Core 2
Power Supply used.
USB
What is the expected behavior?
With both IPv4 and IPv6 fully enabled, DNS lookup (via TLS) should work correctly in all network environments -- IPv4-only, IPv6-only, and dual-stack, for all reachable destinations, by taking into account available addresses.
For example when moving a device to an IPv6-only network only an IPv6 address is available (even though IPv4 is enabled), and so connections to a dual-stack host (that has both) should use the IPv6 address to make the TLS connection.
For this particular bug:
- IPv6 is enabled
- The network provides a public IPv6 address
- The destination has an IPv6 address, and there is a valid route
But the connection fails.
What is the actual behavior?
TLS connections fail on an IPv6-only network when connecting to a dual-stack destination, because the preference order is statically configured to IPv4 first. Even though a local IPv4 address is not currently available, for a dual-stack destination the IPv4 address is returned by getaddrinfo() (instead of the IPv6), so the connection fails.
Note that if you disable IPv4, then the connection works; if you enable IPv4 then then connection fails. Enabling IPv4 should not make IPv6 fail (and vice-versa).
Steps to reproduce.
- Use the updated common protocol example code that supports multiple network types, in PR branch Update protocol examples to support all network types #13249 (IDFGH-12196) #13250
- Make sure the https_request (TLS) example has a dual-stack destination; currently the address used in the example is IPv4 only, so either run in a network the DNS64 (so that it gets a NAT64 address), or change the code to use:
- WEB_SERVER "v4v6.ipv6-test.com"
- WEB_URL "https://v4v6.ipv6-test.com/api/myip.php"
- Build the https_request (TLS) example and connect to a dual-stack network, and see that it works (by using the IPv4 address, if you turn on logging on esp-tls)
- Change to an IPv6-only network; either reconfigure your network, or change the config value and rebuild. (A real world device may set the network dynamically rather than have it compiled in).
- Note the connection now fails, because the DNS returns and IPv4 address but the application does not have one.
A kind of work around is to reconfigure the application to entirely turn off IPv4, however this then stops the application from being able to roam to different network types and connect to IPv4 only servers.
Debug Logs.
I (816) example_connect: Connecting to Wildspace...
I (816) example_connect: Waiting for IP(s)
I (3226) wifi:new:<11,0>, old:<1,0>, ap:<255,255>, sta:<11,0>, prof:1
I (3486) wifi:state: init -> auth (b0)
I (3496) wifi:state: auth -> assoc (0)
I (3516) wifi:state: assoc -> run (10)
I (3556) wifi:connected with Wildspace, aid = 1, channel 11, BW20, bssid = ea:63:da:bd:5a:09
I (3556) wifi:security: WPA2-PSK, phy: bgn, rssi: -70
I (3566) wifi:pm start, type: 1
I (3566) wifi:dp: 1, bi: 102400, li: 3, scale listen interval from 307200 us to 307200 us
I (3626) wifi:AP's beacon interval = 102400 us, DTIM period = 1
I (4616) example_connect: Got IPv6 event: Interface "example_netif_sta" address: fe80:0000:0000:0000:0a3a:f2ff:fe65:db28, type: ESP_IP6_ADDR_IS_LINK_LOCAL
I (5626) wifi:<ba-add>idx:0 (ifx:0, ea:63:da:bd:5a:09), tid:0, ssn:0, winSize:64
I (7616) example_connect: Got IPv6 event: Interface "example_netif_sta" address: 2407:8800:bc61:1300:0a3a:f2ff:fe65:db28, type: ESP_IP6_ADDR_IS_GLOBAL
I (7616) example_connect: Got IPv6 event: Interface "example_netif_sta" address: fd7c:e25e:67e8:0000:0a3a:f2ff:fe65:db28, type: ESP_IP6_ADDR_IS_UNIQUE_LOCAL
I (7626) example_common: Connected to example_netif_sta
I (7636) example_common: - IPv4 address: 0.0.0.0,
I (7646) example_common: - IPv6 address: fe80:0000:0000:0000:0a3a:f2ff:fe65:db28, type: ESP_IP6_ADDR_IS_LINK_LOCAL
I (7656) example_common: - IPv6 address: 2407:8800:bc61:1300:0a3a:f2ff:fe65:db28, type: ESP_IP6_ADDR_IS_GLOBAL
I (7666) example_common: - IPv6 address: fd7c:e25e:67e8:0000:0a3a:f2ff:fe65:db28, type: ESP_IP6_ADDR_IS_UNIQUE_LOCAL
I (7676) example: Updating time from NVS
I (7676) example: Start https_request example
I (7686) example: https_request using crt bundle
I (7696) main_task: Returned from app_main()
E (7766) esp-tls: [sock=54] Resolved IPv4 address: 51.75.78.103
E (7776) esp-tls: [sock=54] connect() error: Host is unreachable
E (7776) esp-tls: Failed to open new connection
E (7776) example: Connection failed...
I (7786) example: 10...
I (8786) example: 9...
I (9786) example: 8...
I (10786) example: 7...
I (11786) example: 6...
More Information.
Issue
The DNS lookup function getaddrinfo() does not take into account available source address, and so does not work propertly across all network types.
In particular it preferences IPv4 over IPv6, and completely fails in an IPv6-only network for a dual-stack destination, as the IPv4 address is unreachable.
You can work around this in some cases, by checking available addresses yourself and then calling getaddrinfo() multiple times -- this approach has been used in the updated http_request example.
However HTTP is not secure and the same approach can't be used with HTTPS, as the host name is needed to TLS and resolved internally in the TLS code.
Technical details
For the https_request example the TLS code esp_tls.c eventually calls getaddrinfo() passing in AF_UNSPEC to get any address.
However the code in netdb.c then converts this into a fixed preference order (when both IPv4 and IPv6 are enabled) of NETCONN_DNS_IPV4_IPV6.
A client with both IPv4 and IPv6 enabled should work in any network IPv4-only, IPv6-only, or dual-stack, and to any reachable destination.
- The code works in an IPv4-only network.
- In a dual-stack it kind of works because it returns the IPv4 address by preference (even if dual stack), and only returns IPv6 if that fails (i.e. the destination is IPv6 only).
- However it fails in an IPv6-only network where the destination is dual-stack because it returns the unreachable IPv4 address.
Changing to use a static order of NETCONN_DNS_IPV6_IPV4 wouldn't fully work either.
This other order allows IPv6-only to work, and means that dual-stack preferences IPv6 and still falls back for IPv4-only destinations.
But it has the reverse problem that in an IPv4-only network a dual stack destination will fail, as it returns the unreachable IPv6 address.
Proposed solution
To be able to work across all networks, the address selection needs to be dynamic based on what is actually available. (Not static based on what is enabled in configuration)
For a dual-stack destination, if a global (including ULA) IPv6 address is available, then use IPv6, but if it a gobal IPv6 address is not available (even though IPv6 is enabled it may not be provided, e.g. if currently on an IPv4-only network) then the IPv4 address needs to be used.
A full implementation of this approach is detailed in RFC 6724, taking into account not only what addresses are available, but their scopes and with special allowances for deprecated address ranges.
Available addresses should be sorted according to RFC 6724, with the application using the first address returned.
The standard linux function getaddrinfo() takes this approach "The sorting function used within getaddrinfo() is defined in RFC 3484" (RFC 3484 was replaced by RFC 6724). See https://man7.org/linux/man-pages/man3/getaddrinfo.3.html
This new DNS resolution dynamically based on available addresses could be configuration flagged to allow the old behaviour (fixed prefrence of IPv4) to continue to be an option.