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

Add IPv6 support for Espressif #9436

Merged
merged 18 commits into from
Jul 23, 2024
Merged

Add IPv6 support for Espressif #9436

merged 18 commits into from
Jul 23, 2024

Conversation

jepler
Copy link
Member

@jepler jepler commented Jul 17, 2024

What works:

  • Getting a v6 address
  • Resolving v6 addresses
  • Connecting to v6 addresses
  • sending to v6 addresses (UDP)
  • Pinging a v6 address

presently you have to start_dhcp to get v6 addresses

Addresses & ping:

>>> import wifi, socketpool
>>> wifi.radio.ping("1.1.1.1")
0.021
>>> wifi.radio.start_dhcp()
>>> wifi.radio.addresses
('FE80::7EDF:A1FF:FE00:518C', 'FD5F:3F5C:FE50:0:7EDF:A1FF:FE00:518C', '10.0.3.96')
>>> wifi.radio.dns
'FD5F:3F5C:FE50::1'
>>> wifi.radio.ping(_)
0.014
>>> socket = socketpool.SocketPool(wifi.radio)
>>> socket.getaddrinfo("google.com", 80)
[(2, 0, 0, 'google.com', ('209.85.145.139', 80))]
>>> socket.getaddrinfo("ipv6.google.com", 80)
[(10, 0, 0, 'ipv6.google.com', ('2607:F8B0:4001:C06::64', 80, 0, 0))]

I added name resolution to wifi ping (espressif only):

>>> wifi.radio.ping("google.com")
0.043
>>> wifi.radio.ping("ipv6.google.com")
0.048

NTP manually:

>>> ntp_addr = ("fd5f:3f5c:fe50::20e", 123)
>>> PACKET_SIZE = 48
>>> 
>>> buf = bytearray(PACKET_SIZE)
>>> with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as s:
...     s.settimeout(1)
...     buf[0] = 0b0010_0011
...     s.sendto(buf, ntp_addr)
...     print(s.recvfrom_into(buf))
...     print(buf)
... 
48
(48, ('0.0.0.0', 123))
bytearray(b'$\x01\x03\xeb\x00\x00\x00\x00\x00\x00\x00GGPS\x00\xeaA0h\x07s;\xc0\x00\x00\x00\x00\x00\x00\x00\x00\xeaA0n\xeb4\x82-\xeaA0n\xebAU\xb1')

http manually:

>>> s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
>>> s.connect(("fd5f:3f5c:fe50::1", 80))
>>> s.send("GET / HTTP/1.0\r\n\r\n")
18
>>> b = bytearray(100)
>>> s.recv_into(b)
100
>>> b
bytearray(b'HTTP/1.0 200 OK\r\nConnection: close\r\nETag: "31e-346-65e90ba6"\r\nLast-Modified: Thu, 07 Mar 2024 00:34:')
>>> s.close()

Todo (some may end up as subsequent PRs):

  • enable generally on espressif (right now only on metro esp32s2)
  • determine when to start dhcp6. right now starts on wifi.radio.start_dhcp() but not on wifi workflow. not sure where dhcp4 is started
  • fix return values of recvfrom & accept (needs C API changes)
  • fix build errors on raspberrypi
  • enable v4 & v6 by default
    • {start,stop}_dhcp_client to gain 2 kwarg-only arguments, defaulting to true
    • v6 to be started automatically
  • support v6 addresses in bind, sendto

Future PRs:

  • add ping-by-name on raspberrypi
  • set & remove v6 addresses
  • access to v6 gateway & netmask-equivalent
  • add support for all v6 properties on AP
  • support v6-only networks (where dhcp4 never assigns an address)?
  • ipaddress.IPv6Address

jepler added 13 commits July 17, 2024 12:18
Otherwise, it was not possible to interact with a v6 address, as
`lwip_getaddrinfo` wouldn't resolve it.
 * metro esp32s2 only, because that's what I had handy
 * nothing is started at boot; I hung it on `start_dhcp()` which is dubious
 * I get a stateless address (which doesn't seem to work) and a dhcpv6 address (which does)

```
>>> wifi.radio.ipv6_addresses
('FE80::7EDF:A1FF:FE00:518C', 'FD5F:3F5C:FE50:0:7EDF:A1FF:FE00:518C')
```

 * depending whether a v4 or v6 dns server is configured, DNS resolution breaks

wrong ipv4_dns is first 4 bytes of the v6 dns server address:
```
>>> wifi.radio.ipv4_dns
253.95.63.92
```

 * I can connect to a v4 or v6 SSH server on the local network and read its banner

>>> s.close(); s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM); s.connect(("fd5f:3f5c:fe50:0:6d9:f5ff:fe1f:ce10", 22))
*** len[0]=28
*** len=28 family=10 port=5632
>>> s.recv_into(buf)
40
>>> bytes(buf)
b'SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u3\r\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
only tested recvfrom_into as can't bind() v6 addresses yet

wifi workflow may be broken by this or maybe it was broken before
.. & make web workflow bind to v6 if available. Binding v6 "any"
address also allows v4 connections
to accomodate multiple servers some day
 * v6 on by default
 * dhcp can start v4 & v6 separately
 * self documenting property for v4 & v6 support
   * v4 support is always on .. for now
@jepler
Copy link
Member Author

jepler commented Jul 22, 2024

  • I didn't find the esp-idf API for v6 netmask & route equivalent, so not adding it at this time
  • adding & removing addresses is unclear as well (there's esp_netif_add_ip6_address but it is defined as an event handler) and there's not currently a use case
    • though ipv6 AP would need .. something
  • v6-only networks also left for future
  • need to test raspberrypi & add ping-by-string there

@jepler jepler marked this pull request as ready for review July 22, 2024 15:58
@jepler
Copy link
Member Author

jepler commented Jul 22, 2024

Other modules & libraries may need to be updated in order to support v6. For instance, do adafruit_requests & adafruit_ntp work properly with v6 addresses & names? Since our focus is on v6 for matter support, I did not investigate this or file issues.

@jepler jepler requested review from dhalbert and tannewt July 22, 2024 16:01
@jepler
Copy link
Member Author

jepler commented Jul 22, 2024

good news! this does seem to set ipv6 mdns records:

$ avahi-resolve -6 -n cpy-00518c.local
cpy-00518c.local	fe80::7edf:a1ff:fe00:518c

@tannewt tannewt added this to the 9.2.0 milestone Jul 22, 2024
Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

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

A few questions. It all looks pretty good. Thanks for figuring this out!

shared-bindings/socketpool/SocketPool.c Show resolved Hide resolved
shared-bindings/wifi/__init__.c Outdated Show resolved Hide resolved
@jepler jepler requested a review from tannewt July 22, 2024 21:24
@anecdata
Copy link
Member

anecdata commented Jul 22, 2024

I don't have an in-depth understanding of IPv6, so I could be off-base on this. Do we know if the espressif (and eventually, the raspberrypi) implementation uses the RFC 4941 IPv6 privacy extensions? If not, I'd suggest not starting IPv6 by default. Some users may be concerned if their microcontrollers are globally identifiable.

It could be that the DHCP server will take care of this, but's that's dependent on the DHCP server implementation (https://media.defense.gov/2023/Jan/18/2003145994/-1/-1/1/CSI_IPv6_security_guidance_.PDF, p.2).

@jepler
Copy link
Member Author

jepler commented Jul 22, 2024

I'm happy to change this to default v6 off until we understand the ramifications better. @tannewt say the word

@anecdata
Copy link
Member

anecdata commented Jul 22, 2024

Found this:

DHCPv6 in lwIP is very simple and supports only stateless configuration

This is probably the original SLAAC mechanism, and may be a permanent address embedding the hardware MAC address (could be verified with testing).

Addendum:

Adafruit CircuitPython 9.1.0-6-g5837e53f39 on 2024-07-22; Adafruit QT Py ESP32-S3 4MB Flash 2MB PSRAM with ```
ESP32S3
>>> import wifi, os
>>> 
>>> wifi.radio.connect(os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD"))
>>> wifi.radio.addresses
('192.168.6.243',)
>>> wifi.radio.start_dhcp()
>>> ':'.join('%02X' % _ for _ in wifi.radio.mac_address)
'F4:12:FA:RE:DA:CT'
>>> wifi.radio.addresses
('FE80::F612:FAFF:FERE:DACT', 'FD41:2E18:D0BA:9740:F612:FAFF:FERE:DACT', '192.168.6.243')
#       ^^^^:^^.    ^^:^^^^.                       ^^^^:^^.    ^^:^^^^

Indeed, the IPv6 address contains the hardware MAC address (with a slight algorithmic manipulation common to SLAAC IPv6 creation). So I would encourage keeping IPv6 off by default.

A partial workaround...

...could be to initially, and then periodically, randomize the MAC address:

mac = bytearray()
for _ in range(0, 6):
    mac.append(random.randrange(0, 256))
mac[0] = mac[0] | 0b00000010  # local on, if desired
mac[0] = mac[0] & 0b11111110  # multicast off
wifi.radio.mac_address = mac

However, even with the partial workaround, the SLAAC IPv6 address still contains the local network prefix.

@tannewt
Copy link
Member

tannewt commented Jul 23, 2024

I'm happy to change this to default v6 off until we understand the ramifications better. @tannewt say the word

Default off is fine with me. That'll make its use explicit which will make more sense if we throw an exception to say it is unsupported.

 * don't enable ipv6 by default due to privacy concerns
 * move list of board support for ipv6 to socketpool documentation
 * removed wifi.supports_ipvx properties
 * throw an exception when start_dhcp_client(ipv6=True) but not supported
@jepler
Copy link
Member Author

jepler commented Jul 23, 2024

OK, default is now off, non-standard attributes in wifi module are removed, and attempting to enable v6 dhcp when not supported gets an exception.

Copy link
Member

@tannewt tannewt left a comment

Choose a reason for hiding this comment

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

Looks great! Thank you for digging into this.

@tannewt tannewt merged commit bffdf3b into adafruit:main Jul 23, 2024
214 checks passed
@jepler jepler mentioned this pull request Jul 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants