Skip to content

esp32/network: Add support for SO_BINDTODEVICE. #12062

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

Merged

Conversation

DvdGiessen
Copy link
Contributor

@DvdGiessen DvdGiessen commented Jul 21, 2023

This implements support for SO_BINDTODEVICE, which allows telling a socket to use a specific interface instead of lwIP automatically selecting one. This allows devices that have multiple connections (for example cellular over PPP in addition to WLAN) to explicitly choose which data is send over which connection, which may have different reliability and or (mobile data) costs associated with using them.

The used lwIP network stack already has support for this, so all that was needed was to expose this functionality in MicroPython. This commit exposes a new constant SO_BINDTODEVICE which can be set as an socket option. As a value it expects the name of the interface to bind to. These names can be retrieved using .config('ifname') implemented on each interface type (including adding a .config() method to PPP, which didn't have it before), which returns a string with the interface name:

>>> import machine
>>> import network
>>> network.WLAN(network.AP_IF).config('ifname')
'lo0'
>>> wlan = network.WLAN(network.AP_IF) ; wlan.active(True) and wlan.config('ifname')
'ap1'
>>> wlan = network.WLAN(network.STA_IF) ; wlan.active(True) and wlan.config('ifname')
'st1'
>>> ppp = network.PPP(machine.UART(0)) ; ppp.active(True) and ppp.config('ifname')
'pp1'
>>> ppp = network.PPP(machine.UART(0)) ; ppp.active(True) and ppp.config('ifname')
'pp2'
>>> ppp = network.PPP(machine.UART(0)) ; ppp.active(True) and ppp.config('ifname')
'pp3'

Note that lo0 seems to be returned by lwIP if the interface is not yet active. The method can also return None in the case of PPP where the entire lwIP interface doesn't yet exist before being activated. Currently no effort is made to unify those cases; it is expected that whatever we receive from lwIP is valid.

When the socket option is set, this forces using a specific device:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, 'st1')

setsockopt will throw (OSError: [Errno 19] ENODEV) if the specified interface does not exist.

I've tested it works correctly with LAN, WLAN, and PPP; I can specify which interface should be used and when testing with for example HTTP requests to ifconfig.co the returned IP address confirms a specific interface was used.

Like other SO_* constants this currently lacks further documentation. And, presumably we can also implement this in other ports that use lwIP; at that point we could also document the ifname config parameter in the general network docs.

Tagging #8173.

@DvdGiessen DvdGiessen force-pushed the esp32_network_sobindtodevice branch from b32ec4a to edefc22 Compare July 21, 2023 15:01
@dpgeorge
Copy link
Member

Thanks for the patch, it looks pretty good to me. It looks CPython compatible.

I'm guessing you can't use normal socket.bind(...) in this case, because the interface you want to bind to doesn't have a specific IP address?

Like other SO_* constants this currently lacks further documentation. And, presumably we can also implement this in other ports that use lwIP; at that point we could also document the .ifname() method in the general network docs.

This is the main point to consider, how/if this feature could be added to other ports if/when they need it. And also consider ifname() method, maybe it can instead be .config('ifname')?

@DvdGiessen DvdGiessen force-pushed the esp32_network_sobindtodevice branch 2 times, most recently from d15fe21 to 67fba03 Compare August 9, 2023 11:21
@DvdGiessen
Copy link
Contributor Author

And also consider ifname() method, maybe it can instead be .config('ifname')?

I've force-pushed a new version of the patch which uses .config() instead, that is indeed a nicer place to put it. It also adds the .config() method to PPP, which previously didn't have that.

It looks CPython compatible.

It is, if the platform you're running on supports SO_BINDTODEVICE. It does on CPython on Linux:

>>> import socket
>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, 'net1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: a bytes-like object is required, not 'str'
>>> s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, b'net1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 19] No such device
>>> s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, b'net0')
>>>

Main difference is that in CPython it requires the parameter to be bytes; for us it works with strings just as well.

I'm guessing you can't use normal socket.bind(...) in this case, because the interface you want to bind to doesn't have a specific IP address?

Correct. I could perhaps have polled the IP every time I wanted to make a connection, but this seemed like a better solution since this actually binds to a device, not an IP, and I wasn't sure whether lwIP uses a strong or weak model, thus whether binding to an IP was sufficient to guarantee it would use that device or it would still depend on the routing table.

presumably we can also implement this in other ports that use lwIP

This is the main point to consider, how/if this feature could be added to other ports if/when they need it.

I haven't looked into the network stack on other ports as I don't currently use networking on any of them; but given (some) seem to use lwIP as well I assume it will be similar as for the ESP32; since we don't really use much ESP-IDF specific functionality here, the only IDF function we use is simply a wrapper for netif_index_to_name(netif_get_index(netif) from the lwIP API.

This implements support for SO_BINDTODEVICE, which allows telling a socket
to use a specific interface instead of lwIP automatically selecting one.
This allows devices that have multiple connections (for example cellular
over PPP in addition to WLAN) to explicitly choose which data is send over
which connection, which may have different reliability and or (mobile data)
costs associated with using them.

The used lwIP network stack already has support for this, so all that was
needed was to expose this functionality in MicroPython.  This commit
exposes a new constant SO_BINDTODEVICE which can be set as an socket
option.  As a value it expects the name of the interface to bind to.  These
names can be retrieved using `.config('ifname')` implemented on each
interface type (including adding in this commit a `.config()` method to
PPP, which it didn't have before), which returns a string with the
interface name:

    >>> import machine
    >>> import network
    >>> network.WLAN(network.AP_IF).config('ifname')
    'lo0'
    >>> wlan = network.WLAN(network.AP_IF)
    >>> wlan.active(True) and wlan.config('ifname')
    'ap1'
    >>> wlan = network.WLAN(network.STA_IF)
    >>> wlan.active(True) and wlan.config('ifname')
    'st1'
    >>> ppp = network.PPP(machine.UART(0))
    >>> ppp.active(True) and ppp.config('ifname')
    'pp1'
    >>> ppp = network.PPP(machine.UART(0))
    >>> ppp.active(True) and ppp.config('ifname')
    'pp2'
    >>> ppp = network.PPP(machine.UART(0))
    >>> ppp.active(True) and ppp.config('ifname')
    'pp3'

Note that lo0 seems to be returned by lwIP if the interface is not yet
active.  The method can also return None in the case of PPP where the
entire lwIP interface doesn't yet exist before being activated.  Currently
no effort is made to unify those cases; it is expected that whatever we
receive from lwIP is valid.

When the socket option is set, this forces using a specific device:

    import socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, 'st1')

setsockopt will throw (OSError: [Errno 19] ENODEV) if the specified
interface does not exist.

Tested with LAN, WLAN, and PPP; can specify which interface should be used
and when testing with, for example, HTTP requests to ifconfig.co the
returned IP address confirms a specific interface was used.

Signed-off-by: Daniël van de Giessen <daniel@dvdgiessen.nl>
@dpgeorge dpgeorge force-pushed the esp32_network_sobindtodevice branch from 67fba03 to ba8aad3 Compare September 1, 2023 09:23
@dpgeorge dpgeorge merged commit ba8aad3 into micropython:master Sep 1, 2023
@dpgeorge
Copy link
Member

dpgeorge commented Sep 1, 2023

Thanks for updating to use .config('ifname'). That will be easier to add to other network interface implementations.

@ukrolelo
Copy link

ukrolelo commented Feb 2, 2024

Greetings, can it be done on rp2? RPI PICO W ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants