Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding IPv6 support to arduino-esp32 #6712

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cores/esp32/Client.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "Print.h"
#include "Stream.h"
#include "IPAddress.h"
#include "IPv6Address.h"

class Client: public Stream
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
WiFiTelnetToSerial - Example Transparent UART to Telnet Server for ESP32

Copyright (c) 2017 Hristo Gochkov. All rights reserved.
This file is part of the ESP32 WiFi library for Arduino environment.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <WiFi.h>
#include <WiFiMulti.h>

WiFiMulti wifiMulti;

//Even this example state IPv6, it is dual stack and compatible with IPv4 too

//how many clients should be able to telnet to this ESP32
#define MAX_SRV_CLIENTS 1
const char* ssid = "**********";
const char* password = "**********";

WiFiServer server(23);
WiFiClient serverClients[MAX_SRV_CLIENTS];

void setup() {
Serial.begin(115200);
Serial.println("\nConnecting");

wifiMulti.IPv6(true);
wifiMulti.addAP(ssid, password);
wifiMulti.addAP("ssid_from_AP_2", "your_password_for_AP_2");
wifiMulti.addAP("ssid_from_AP_3", "your_password_for_AP_3");

Serial.println("Connecting Wifi ");
for (int loops = 10; loops > 0; loops--) {
if (wifiMulti.run() == WL_CONNECTED) {
Serial.println("");
Serial.print("WiFi connected ");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
break;
}
else {
Serial.println(loops);
delay(1000);
}
}
if (wifiMulti.run() != WL_CONNECTED) {
Serial.println("WiFi connect failed");
delay(1000);
ESP.restart();
}

//start UART and the server
Serial1.begin(9600);
server.begin();
server.setNoDelay(true);

Serial.print("Ready! Use 'telnet ");
Serial.print(WiFi.localIP());
Serial.println(" 23' to connect");
}

void loop() {
uint8_t i;
if (wifiMulti.run() == WL_CONNECTED) {
//check if there are any new clients
if (server.hasClient()){
for(i = 0; i < MAX_SRV_CLIENTS; i++){
//find free/disconnected spot
if (!serverClients[i] || !serverClients[i].connected()){
if(serverClients[i]) serverClients[i].stop();
serverClients[i] = server.available();
if (!serverClients[i]) Serial.println("available broken");
Serial.print("New client: ");
Serial.print(i); Serial.print(' ');
Serial.println(serverClients[i].remoteIP6());
break;
}
}
if (i >= MAX_SRV_CLIENTS) {
//no free/disconnected spot so reject
server.available().stop();
}
}
//check clients for data
for(i = 0; i < MAX_SRV_CLIENTS; i++){
if (serverClients[i] && serverClients[i].connected()){
if(serverClients[i].available()){
//get data from the telnet client and push it to the UART
while(serverClients[i].available()) Serial1.write(serverClients[i].read());
}
}
else {
if (serverClients[i]) {
serverClients[i].stop();
}
}
}
//check UART for data
if(Serial1.available()){
size_t len = Serial1.available();
uint8_t sbuf[len];
Serial1.readBytes(sbuf, len);
//push UART data to all connected telnet clients
for(i = 0; i < MAX_SRV_CLIENTS; i++){
if (serverClients[i] && serverClients[i].connected()){
serverClients[i].write(sbuf, len);
delay(1);
}
}
}
}
else {
Serial.println("WiFi not connected!");
for(i = 0; i < MAX_SRV_CLIENTS; i++) {
if (serverClients[i]) serverClients[i].stop();
}
delay(1000);
}
}
95 changes: 86 additions & 9 deletions libraries/WiFi/src/WiFiClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,22 +210,43 @@ int WiFiClient::connect(IPAddress ip, uint16_t port)
{
return connect(ip,port,_timeout);
}

int WiFiClient::connect(IPAddress ip, uint16_t port, int32_t timeout)
{
ip_addr_t dstip;
dstip = IPADDR_ANY_TYPE_INIT;
IP_ADDR4(&dstip, ip[0], ip[1], ip[2], ip[3]);
return connect(dstip, port, timeout);
}

int WiFiClient::connect(ip_addr_t ip, uint16_t port, int32_t timeout)
{
struct sockaddr_storage serveraddr;
_timeout = timeout;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int sockfd = -1;

if (IP_IS_V6(&ip)) {
sockfd = socket(AF_INET6, SOCK_STREAM, 0);
struct sockaddr_in6 *tmpaddr = (struct sockaddr_in6 *)&serveraddr;
memset((char *) tmpaddr, 0, sizeof(struct sockaddr_in6));
tmpaddr->sin6_family = AF_INET6;
inet6_addr_from_ip6addr(&tmpaddr->sin6_addr, ip_2_ip6(&ip));
tmpaddr->sin6_port = htons(port);
}
if (sockfd == -1 && IP_IS_V4(&ip)) {
sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in *tmpaddr = (struct sockaddr_in *)&serveraddr;
memset((char *) tmpaddr, 0, sizeof(struct sockaddr_in));
tmpaddr->sin_family = AF_INET;
tmpaddr->sin_addr.s_addr = ip4_addr_get_u32(&ip.u_addr.ip4);
tmpaddr->sin_port = htons(port);
}
if (sockfd < 0) {
log_e("socket: %d", errno);
return 0;
}
fcntl( sockfd, F_SETFL, fcntl( sockfd, F_GETFL, 0 ) | O_NONBLOCK );

uint32_t ip_addr = ip;
struct sockaddr_in serveraddr;
memset((char *) &serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
memcpy((void *)&serveraddr.sin_addr.s_addr, (const void *)(&ip_addr), 4);
serveraddr.sin_port = htons(port);
fd_set fdset;
struct timeval tv;
FD_ZERO(&fdset);
Expand Down Expand Up @@ -294,6 +315,13 @@ int WiFiClient::connect(const char *host, uint16_t port)

int WiFiClient::connect(const char *host, uint16_t port, int32_t timeout)
{
if (WiFiGenericClass::getStatusBits() & WIFI_WANT_IP6_BIT) {
ip_addr_t srv6;
if(!WiFiGenericClass::hostByName6(host, srv6)){
return 0;
}
return connect(srv6, port, timeout);
}
IPAddress srv((uint32_t)0);
if(!WiFiGenericClass::hostByName(host, srv)){
return 0;
Expand Down Expand Up @@ -561,8 +589,52 @@ IPAddress WiFiClient::remoteIP(int fd) const
struct sockaddr_storage addr;
socklen_t len = sizeof addr;
getpeername(fd, (struct sockaddr*)&addr, &len);
struct sockaddr_in *s = (struct sockaddr_in *)&addr;
return IPAddress((uint32_t)(s->sin_addr.s_addr));

// Old way, IPv4
if (((struct sockaddr*)&addr)->sa_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)&addr;
return IPAddress((uint32_t)(s->sin_addr.s_addr));
}
// IPv6, but it might be IPv4 mapped address
if (((struct sockaddr*)&addr)->sa_family == AF_INET6) {
struct sockaddr_in6 *saddr6 = (struct sockaddr_in6 *)&addr;
if (IN6_IS_ADDR_V4MAPPED(saddr6->sin6_addr.un.u32_addr)) {
struct sockaddr_in addr4;
memset(&addr4, 0, sizeof(addr4));
addr4.sin_family = AF_INET;
addr4.sin_port = saddr6->sin6_port;
memcpy(&addr4.sin_addr.s_addr, saddr6->sin6_addr.s6_addr+12, sizeof(addr4.sin_addr.s_addr));
return IPAddress((uint32_t)(addr4.sin_addr.s_addr));
}
log_e("WiFiClient::remoteIP IPv6 to IPv4 cannot convert");
return (IPAddress(0,0,0,0));
}
log_e("WiFiClient::remoteIP Not AF_INET or AF_INET6?");
return (IPAddress(0,0,0,0));
}

IPv6Address WiFiClient::remoteIP6(int fd) const
{
struct sockaddr_storage addr;
socklen_t len = sizeof addr;
getpeername(fd, (struct sockaddr*)&addr, &len);

// IPv4 socket we can print as IPv6 mapped
if (((struct sockaddr*)&addr)->sa_family == AF_INET) {
uint8_t buffer[16] = { 0 };
buffer[10] = 0xFF;
buffer[11] = 0xFF;
struct sockaddr_in *s = (struct sockaddr_in *)&addr;
memcpy(&buffer[12], (uint8_t*)&s->sin_addr.s_addr, 4);
return IPv6Address(buffer);
}
// IPv6
if (((struct sockaddr*)&addr)->sa_family == AF_INET6) {
struct sockaddr_in6 *saddr6 = (struct sockaddr_in6 *)&addr;
return (IPv6Address(saddr6->sin6_addr.s6_addr));
}
log_e("WiFiClient::remoteIP Not AF_INET or AF_INET6?");
return (IPv6Address());
}

uint16_t WiFiClient::remotePort(int fd) const
Expand All @@ -579,6 +651,11 @@ IPAddress WiFiClient::remoteIP() const
return remoteIP(fd());
}

IPv6Address WiFiClient::remoteIP6() const
{
return remoteIP6(fd());
}

uint16_t WiFiClient::remotePort() const
{
return remotePort(fd());
Expand Down
9 changes: 9 additions & 0 deletions libraries/WiFi/src/WiFiClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "Arduino.h"
#include "Client.h"
#include <memory>
#include "lwip/ip_addr.h"

class WiFiClientSocketHandle;
class WiFiClientRxBuffer;
Expand All @@ -49,6 +50,7 @@ class WiFiClient : public ESPLwIPClient
WiFiClient();
WiFiClient(int fd);
~WiFiClient();
int connect(ip_addr_t ip, uint16_t port, int32_t timeout);
int connect(IPAddress ip, uint16_t port);
int connect(IPAddress ip, uint16_t port, int32_t timeout);
int connect(const char *host, uint16_t port);
Expand Down Expand Up @@ -95,6 +97,8 @@ class WiFiClient : public ESPLwIPClient

IPAddress remoteIP() const;
IPAddress remoteIP(int fd) const;
IPv6Address remoteIP6() const;
IPv6Address remoteIP6(int fd) const;
uint16_t remotePort() const;
uint16_t remotePort(int fd) const;
IPAddress localIP() const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One complexity with IPv6 is that with IPv4 usually you will only have one local IP address (public address, private network, or if there is no network a link-local 169.254 address), however with IPv6 you will almost always have multiple addresses -- at least both an autoconfigure link local (fe80: address) as well as an address with the network global prefix, and possibly a ULA prefix as well, and outgoing addresses with privacy enhancements may have rotating outgoing addresses.

One way I was handling this was with the signature IPAddress localIP(uint8_t index = 0), returning one value for each index (or NULL / none once they run out), with another signature int8_t getLocalIPs(IPAddress addresses[], uint8_t max) to get multiple addresses, filling an array up to the max (and returning the number of addresses).

Usually you don't want to do a lot with the local address except display it, but with IPv6 you want to check which one to display.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can just add globalIP6 and localIP6? This will keep it simple for Arduino users. We can iterate and by prefix identify who is local and who is global.
Privacy enhancements as far as i know is not available in lwip, yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful with terminology not to mix up the concepts of "local" and "link-local", as they are different things.

Local vs remote: Local means this end of the connection, i.e. the IP address of the device, as opposed to remote, which is the other end.

Link-local vs global are scopes: Link-local is confined to the local link only, global is a routeable.

You can have both.

With IPv4 you usually only have one local address, and it usually has a global scope. With IPv6 you have multiple local addresses, both link-local and global scopes.

Generally the global scoped addresses are more useful, e.g. to use as a server address to connect to a device, or when looking in logs. (Although not always, e.g. if the destination machines is on the same link, then IPv6 will use the link-local address.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples, from a SIMCOM GSM NB-IoT library I am working on: https://github.com/sgryphon/AdvancedGsmClient/blob/main/src/SIM7020/SIM7020GsmModem.cpp#L470

When I connect the NB-IoT modem, it gets assigned 3 local addresses, plus it's address list includes a localhost address:

Local addresses:

  1. 2001:8004:4810:0:719d:1439:899a:42d7 - local IPv6 with global scope
  2. 10.88.134.167 - local IPv4 with global scope
  3. fe80:0:0:0:719d:1439:899a:42d7 - local IPv6 with link-local scope
  4. 127.0.0.1 - local IPv4 with link-local scope (loopback addresses count as link-local).

The actual order the addresses are reported by the device is fe80:, 2001:, 10.88, 127.0, however my getLocalIPs() method sorts them from largest scope (global) to smallest scope (link-local), and then within each scope according to the RFC 6724 precedence rules (which includes IPv6 before IPv4), to get the order listed above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my local desktop computer, sorting my addresses a similar way (but using the bytes; my simple GSM algorithm is using strings and so sorts ':' differently), to sort them in order of usefulness:

  1. 2407:8800:bc61:1300::8e6 - local IPv6 with global scope
  2. 2407:8800:bc61:1300:4a23:22d5:6c48:f0e8 - local IPv6 with global scope
  3. 2407:8800:bc61:1300:4df2:1392:9dd7:d2d7 - local IPv6 with global scope
  4. 2407:8800:bc61:1300:ee48:5c84:ad87:c65c - local IPv6 with global scope
  5. 192.168.1.195 - local IPv4 with global scope
  6. fd7c:e25e:67e8::8e6 - local IPv6 with global scope, in ULA range
  7. fd7c:e25e:67e8:0:2c73:36d3:d325:93df - local IPv6 with global scope, in ULA range
  8. fd7c:e25e:67e8:0:9ac7:63c9:5f26:b026 - local IPv6 with global scope, in ULA range
  9. fd7c:e25e:67e8:0:a9ed:c900:59fa:5449 - local IPv6 with global scope, in ULA range
  10. fe80::5f65:961a:14b4:6263 - local IPv6 with link-local scope

Global scope is sorted before link-local (because it is more useful to contact me as a server), with the precedence order IPv6, then IPv4, then IPv6 unique local address (ULA).

As a server the ::8e6 address is better, although outgoing logs will use the temporary addresses (probably not an issue for Arduino). If I was communicating with an IPv4 only server, then it would see the IPv4 address.

So, to really investigate I would need to list all of my addresses (e.g. in logs). Even the ULA addresses could be used, for local communications, and the IPv6 link-local address could even be used in some cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you really want to stick with separate properties for IPv6 and IPv4, for only one of each type, then you want to try and return a global scope address if possible:

  • localIP4() - Shows the local IPv4 address (this will usually be a global scope address); there is usually only one. To be consistent, in case there is more than one IPv4, you should also sort by scope (e.g. if a device has two IP addresses 169.254.1.2 and 10.1.2.3, then show 10.1.2.3 ... although that would be really weird as IPv4 doesn't work that way).
  • localIP6() - shows the first IPv6 address, sorting global scope before link-local scope.
  • localIP() - for a single address should show the biggest scope, highest precedence address. i.e. global IPv6 if you have one, otherwise global IPv4, and then finally (if you have no global addresses), then the link-local IPv6.

I do think you should have a getLocalIPs() that returns all of the addresses, which you could then use to select out the other values, e.g. for logging, although in the case of the GSM code I know it only ever gets max 4 addresses (i.e. no temporary, only one IPv6 prefix, etc), so I just log the first 2 address, which will be the one global IPv6 and the one global IPv4.

Expand All @@ -106,4 +110,9 @@ class WiFiClient : public ESPLwIPClient
using Print::write;
};

#define IN6_IS_ADDR_V4MAPPED(a) \
((((__const uint32_t *) (a))[0] == 0) \
&& (((__const uint32_t *) (a))[1] == 0) \
&& (((__const uint32_t *) (a))[2] == htonl (0xffff)))

#endif /* _WIFICLIENT_H_ */
53 changes: 51 additions & 2 deletions libraries/WiFi/src/WiFiGeneric.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -866,8 +866,8 @@ esp_err_t WiFiGenericClass::_eventCallback(arduino_event_t *event)
} else if(event->event_id == ARDUINO_EVENT_WIFI_STA_CONNECTED) {
WiFiSTAClass::_setStatus(WL_IDLE_STATUS);
setStatusBits(STA_CONNECTED_BIT);

//esp_netif_create_ip6_linklocal(esp_netifs[ESP_IF_WIFI_STA]);
if (getStatusBits() & WIFI_WANT_IP6_BIT)
esp_netif_create_ip6_linklocal(esp_netifs[ESP_IF_WIFI_STA]);
} else if(event->event_id == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
uint8_t reason = event->event_info.wifi_sta_disconnected.reason;
log_w("Reason: %u - %s", reason, reason2str(reason));
Expand Down Expand Up @@ -1335,6 +1335,25 @@ static void wifi_dns_found_callback(const char *name, const ip_addr_t *ipaddr, v
xEventGroupSetBits(_arduino_event_group, WIFI_DNS_DONE_BIT);
}

/**
* IPv6 compatible DNS callback
* @param name
* @param ipaddr
* @param callback_arg
*/
static void wifi_dns6_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg)
{
struct dns_api_msg *msg = (struct dns_api_msg *)callback_arg;

if(ipaddr && !msg->result) {
msg->ip_addr = *ipaddr;
msg->result = 1;
} else {
msg->result = -1;
}
xEventGroupSetBits(_arduino_event_group, WIFI_DNS_DONE_BIT);
}

/**
* Resolve the given hostname to an IP address.
* @param aHostname Name to be resolved
Expand Down Expand Up @@ -1362,6 +1381,36 @@ int WiFiGenericClass::hostByName(const char* aHostname, IPAddress& aResult)
return (uint32_t)aResult != 0;
}

/**
* Resolve the given hostname to an IP6 address.
* @param aHostname Name to be resolved
* @param aResult IPv6Address structure to store the returned IP address
* @return 1 if aHostname was successfully converted to an IP address,
* else error code
*/
int WiFiGenericClass::hostByName6(const char* aHostname, ip_addr_t& aResult)
{
ip_addr_t addr;
struct dns_api_msg arg;

memset(&arg, 0x0, sizeof(arg));
waitStatusBits(WIFI_DNS_IDLE_BIT, 16000);
clearStatusBits(WIFI_DNS_IDLE_BIT | WIFI_DNS_DONE_BIT);

err_t err = dns_gethostbyname_addrtype(aHostname, &addr, &wifi_dns6_found_callback,
&arg, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
if(err == ERR_OK) {
aResult = addr;
} else if(err == ERR_INPROGRESS) {
waitStatusBits(WIFI_DNS_DONE_BIT, 15000); //real internal timeout in lwip library is 14[s]
clearStatusBits(WIFI_DNS_DONE_BIT);
if (arg.result == 1)
aResult = arg.ip_addr;
}
setStatusBits(WIFI_DNS_IDLE_BIT);
return (uint32_t)err == ERR_OK || (err == ERR_INPROGRESS && arg.result == 1);
}

IPAddress WiFiGenericClass::calculateNetworkID(IPAddress ip, IPAddress subnet) {
IPAddress networkID;

Expand Down
Loading