【问题标题】:Send http request through specific network interface通过特定的网络接口发送http请求
【发布时间】:2018-02-26 20:22:29
【问题描述】:

我有两个网络接口(wifi 和以太网)都可以访问互联网。假设我的接口是eth(以太网)和wlp2(wifi)。我需要通过eth 接口和其他通过wpl2 的特定请求。

类似:

// Through "eth"
request.post(url="http://myapi.com/store_ip", iface="eth")
// Through "wlp2" 
request.post(url="http://myapi.com/log", iface="wlp2")

我正在使用requests,但如果无法使用requests,我可以使用pycurlurllib

How to specify source interface in python requests module? 指的是Requests, bind to an ip,它不起作用。

【问题讨论】:

  • 您找到使用requests的解决方案了吗?
  • 还没有:( ...
  • 我问了一个类似的问题here,现在有两个答案。有一个简单的并不能满足我所有的需求,有我自己找到的答案,可以实现我想要的一切。

标签: python python-requests


【解决方案1】:

这是请求库的解决方案,无需猴子修补任何东西。

此函数将创建一个绑定到给定 IP 地址的会话。由您决定所需网络接口的 IP 地址。

经测试可与requests==2.23.0 一起使用。

import requests


def session_for_src_addr(addr: str) -> requests.Session:
    """
    Create `Session` which will bind to the specified local address
    rather than auto-selecting it.
    """
    session = requests.Session()
    for prefix in ('http://', 'https://'):
        session.get_adapter(prefix).init_poolmanager(
            # those are default values from HTTPAdapter's constructor
            connections=requests.adapters.DEFAULT_POOLSIZE,
            maxsize=requests.adapters.DEFAULT_POOLSIZE,
            # This should be a tuple of (address, port). Port 0 means auto-selection.
            source_address=(addr, 0),
        )

    return session


# usage example:
s = session_for_src_addr('192.168.1.12')
s.get('https://httpbin.org/ip')

请注意,尽管此方法与 curl--interface 选项相同,但在某些情况下无济于事。根据您的路由配置,即使您绑定到特定的 IP 地址,请求也可能会通过其他一些接口。因此,如果此答案对您不起作用,请首先检查 curl http://httpbin.org/ip --interface myinterface 是否会按预期工作。

【讨论】:

  • 这解决了我使用requests 的问题,其接口在Linux 中有两个IP 地址(如eth0eth0:1)。
【解决方案2】:

我找到了一种使用pycurl 的方法。这就像一个魅力。

import pycurl
from io import BytesIO
import json


def curl_post(url, data, iface=None):
    c = pycurl.Curl()
    buffer = BytesIO()
    c.setopt(pycurl.URL, url)
    c.setopt(pycurl.POST, True)
    c.setopt(pycurl.HTTPHEADER, ['Content-Type: application/json'])
    c.setopt(pycurl.TIMEOUT, 10)
    c.setopt(pycurl.WRITEFUNCTION, buffer.write)
    c.setopt(pycurl.POSTFIELDS, data)
    if iface:
        c.setopt(pycurl.INTERFACE, iface)
    c.perform()

    # Json response
    resp = buffer.getvalue().decode('UTF-8')

    #  Check response is a JSON if not there was an error
    try:
        resp = json.loads(resp)
    except json.decoder.JSONDecodeError:
        pass

    buffer.close()
    c.close()
    return resp


if __name__ == '__main__':
    dat = {"id": 52, "configuration": [{"eno1": {"address": "192.168.1.1"}}]}
    res = curl_post("http://127.0.0.1:5000/network_configuration/", json.dumps(dat), "wlp2")
    print(res)

我将问题打开,希望有人可以使用requests 给出答案。

【讨论】:

    【解决方案3】:

    如果您想在 Linux 上执行此操作,您可以将 SO_BINDTODEVICE 标志用于 setsockopt(请查看 man 7 socket,了解更多详细信息)。事实上,它就是使用by curl,如果你在linux上使用--interfaceoption。但请记住,SO_BINDTODEVICE 需要 root 权限(CAP_NET_RAW,尽管有 some attempts 来更改此设置)并且如果SO_BINDTODEVICE 失败,curl 会退回到常规的bind 技巧。

    这里是 curl strace 失败时的示例:

    strace -f -e setsockopt,bind curl --interface eth2 https://ifconfig.me/
    strace: Process 18208 attached
    [pid 18208] +++ exited with 0 +++
    setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
    setsockopt(3, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
    setsockopt(3, SOL_TCP, TCP_KEEPIDLE, [60], 4) = 0
    setsockopt(3, SOL_TCP, TCP_KEEPINTVL, [60], 4) = 0
    setsockopt(3, SOL_SOCKET, SO_BINDTODEVICE, "eth2\0", 5) = -1 EPERM (Operation not permitted)
    bind(3, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("192.168.8.1")}, 16) = 0 # curl fallbacks to regular bind
    127.0.0.1+++ exited with 0 +++
    

    另外,我想说的是,使用普通的bind 并不总是保证流量会通过指定的接口(@98​​7654325@ 使用普通的bind)。在 linux 上,只有SO_BINDTODEVICE 保证流量会通过指定的设备。

    这是一个如何将SO_BINDTODEVICErequestsrequests-toolbelt 一起使用的示例(正如我所说,它需要CAP_NET_RAW permissions)。

    import socket
    import requests
    from requests_toolbelt.adapters.socket_options import SocketOptionsAdapter
    
    
    session = requests.Session()
    # set interface here
    options = [(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, b"eth0")]
    for prefix in ('http://', 'https://'):
        session.mount(prefix, SocketOptionsAdapter(socket_options=options))
    
    
    print(session.get("https://ifconfig.me/").text)
    

    或者,如果您不想使用requests-toolbelt,您可以自己实现适配器类:

    import socket
    import requests
    from requests import adapters
    from urllib3.poolmanager import PoolManager
    
    
    class InterfaceAdapter(adapters.HTTPAdapter):
    
        def __init__(self, **kwargs):
            self.iface = kwargs.pop('iface', None)
            super(InterfaceAdapter, self).__init__(**kwargs)
    
        def _socket_options(self):
            if self.iface is None:
                return []
            else:
                return [(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, self.iface)]
    
        def init_poolmanager(self, connections, maxsize, block=False):
            self.poolmanager = PoolManager(
                num_pools=connections,
                maxsize=maxsize,
                block=block,
                socket_options=self._socket_options()
            )
    
    
    session = requests.Session()
    for prefix in ('http://', 'https://'):
        session.mount(prefix, InterfaceAdapter(iface=b'eth0'))
    
    
    print(session.get("https://ifconfig.me/").text)
    

    【讨论】:

      【解决方案4】:

      尝试将内部IP(192.168.0.200)更改为下面代码中对应的iface。

      import requests
      from requests_toolbelt.adapters import source
      
      def check_ip(inet_addr):
          s = requests.Session()
          iface = source.SourceAddressAdapter(inet_addr)
          s.mount('http://', iface)
          s.mount('https://', iface)
          url = 'https://emapp.cc/get_my_ip'
          resp = s.get(url)
          print(resp.text)
      
      if __name__ == '__main__':
          check_ip('192.168.0.200')
      

      【讨论】:

        最近更新 更多