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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open

Add IPv6 support for Espressif #9436

wants to merge 17 commits into from

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!

@@ -142,6 +142,16 @@ static mp_obj_t socketpool_socketpool_getaddrinfo(size_t n_args, const mp_obj_t
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

#if CIRCUITPY_SOCKETPOOL_IPV6
return common_hal_socketpool_getaddrinfo_raise(
Copy link
Member

Choose a reason for hiding this comment

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

Why not use this all of the time?

Copy link
Member Author

Choose a reason for hiding this comment

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

good point. will revise.

Comment on lines +70 to +72

{ MP_ROM_QSTR(MP_QSTR_supports_ipv6), MP_ROM_PTR(CIRCUITPY_SOCKETPOOL_IPV6 ? MP_ROM_TRUE : MP_ROM_FALSE) },
{ MP_ROM_QSTR(MP_QSTR_supports_ipv4), MP_ROM_TRUE },
Copy link
Member

Choose a reason for hiding this comment

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

How would you determine this in CPython? Should it be on Radio or SocketPool (if CPython's socket has a way to detect it.) I'm thinking ahead to having a thread network interface that is ipv6-only (I think.)

Copy link
Member Author

Choose a reason for hiding this comment

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

good question. I don't think there's a look-before-you-leap "is there working ipv6" check in CPython.. However, in CPython with no ipv6 global connectivity, a v6 connect() to an internet address will promptly fail:

>>> s.connect(("ipv6.google.com", 80))
OSError: [Errno 101] Network is unreachable

If the OS lacked even support compile-time for V6 sockets at all then the socket module can lack the AF_INET6 definition:

#ifdef AF_INET6
    ADD_INT_MACRO(m, AF_INET6); /* IP version 6 */
#endif

and creating a socket with an unsupported family will raise an exception:

>>> socket.socket(socket.AF_UNSPEC, socket.SOCK_STREAM)
OSError: [Errno 97] Address family not supported by protocol

Copy link
Member Author

Choose a reason for hiding this comment

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

I can drop these properties if they're not useful. I really only added them so that the documentation could show a "correct" default parameter for start_dhcp_server, by showing the constant supports_ipv6 instead of a literal True or False which would be incorrect

@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

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).

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.

None yet

3 participants