aiohttp icon indicating copy to clipboard operation
aiohttp copied to clipboard

support setting client network interface by name

Open SobhanMP opened this issue 3 years ago • 7 comments

Is your feature request related to a problem?

While setting the target interface's ip should work in most cases, it's possible that multiple devices have the same ip.

Also i'm having trouble using a wireguard vpn when i set Table=off. Table=off disables setting up the routes for the vpn and it justs shows up as a regular device. The connection is fine as pycurl can access it when i set the device but aiohttp fails. probably related but not directly.

Describe the solution you'd like

Users should be able to set the inet device directly.

Describe alternatives you've considered

UnixConnector seems like a good starting place.

loop.create_connection with (socket.SOL_SOCKET, socket.SO_BINDTODEVICE, iface) inspired from here also seems like a good idea

Related component

Client

Additional context

No response

Code of Conduct

  • [X] I agree to follow the aio-libs Code of Conduct

SobhanMP avatar Dec 06 '21 04:12 SobhanMP

Interesting idea, thanks!

asvetlov avatar Dec 06 '21 09:12 asvetlov

Related component

Client

What you're describing sounds like a server-side thing, no?

webknjaz avatar Dec 06 '21 16:12 webknjaz

no?

i'm trying to do request with aiohttp.ClientSession. by definition that's a client side thing, right?

SobhanMP avatar Dec 06 '21 19:12 SobhanMP

Related component

Client

What you're describing sounds like a server-side thing, no?

Not exactly. You can have multiple interfaces connected to a linux machine all with the same subnet, and DHCP ranges. Adding the abillity to choose exactly what interface to be used for the communication adds a lot of flexbility.

I have created a workaround, but i believe this should be implemented natively. Please note that socket.SO_BINDTODEVICE is only a valid attribute if the OS supports it. Futhermore chosing the source ip and a interface should not be possible if this was to be implemented.

I have not investigated how the workaround works with proxies, but should not be a problem.

async def _wrap_create_connection(
        self,
        *args: Any,
        req: "ClientRequest",
        timeout: "ClientTimeout",
        client_error: Type[Exception] = ClientConnectorError,
        **kwargs: Any,
) -> Tuple[asyncio.Transport, ResponseHandler]:
    try:
        async with aiohttp.helpers.ceil_timeout(timeout.sock_connect):
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, 'en0'.encode())
            # nonblocking as we are async
            s.setblocking(False)
            # socket must be connected otherwise we are thrown an exception
            s.connect_ex((args[-2], args[-1]))
            # pass the socket to asyncio
            kwargs["sock"] = s
            # Remove host and ip from arguments
            args = args[:-2]
            return await self._loop.create_connection(*args, **kwargs)  # type: ignore[return-value]  # noqa
    except cert_errors as exc:
        raise ClientConnectorCertificateError(req.connection_key, exc) from exc
    except ssl_errors as exc:
        raise ClientConnectorSSLError(req.connection_key, exc) from exc
    except OSError as exc:
        if exc.errno is None and isinstance(exc, asyncio.TimeoutError):
            raise
        raise client_error(req.connection_key, exc) from exc

aiohttp.TCPConnector._wrap_create_connection = _wrap_create_connection

I will send a pull request a bit later.

Muno459 avatar Dec 17 '22 12:12 Muno459

Any chance of getting this implemented? Bug bounty?

foxx avatar Feb 02 '24 13:02 foxx

@foxx send a PR and we'll consider it.

webknjaz avatar Feb 02 '24 13:02 webknjaz

No can do, but here's a working solution for anyone who doesn't want to have to spend 9000 hours writing a PR:

import httpcore
import socket

class AsyncHttpConnectionPool(httpcore.AsyncConnectionPool):
    """."""

    def __init__(self, *args, **kwargs):
        """."""
        interface_name = kwargs.pop('interface_name', None)
        if interface_name:
            kwargs['socket_options'] = ((socket.SOL_SOCKET, 25, str(interface_name + '\0').encode('ascii')),)
        super().__init__(*args, **kwargs)

    async def get(self, *args, **kwargs):
        """."""
        return await self.request('GET', *args, **kwargs)

    async def post(self, *args, **kwargs):
        """."""
        return await self.request('POST', *args, **kwargs)
    

foxx avatar Feb 02 '24 14:02 foxx