V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
cs010
V2EX  ›  macOS

macOS 如何让 socket 指定网卡发出数据(效果等同 Linux 的 SO_BINDTODEVICE)

  •  1
     
  •   cs010 · 2019-08-09 18:39:56 +08:00 · 5941 次点击
    这是一个创建于 1693 天前的主题,其中的信息可能已经有所发展或是发生改变。

    场景

    现有无线网卡和虚拟网卡 utun,想实现通过更改路由表,所有数据由 utun 接收并处理后,由无线网卡发出。

    如 data[1.1.1.1] --> utun --> en0 --> server[1.1.1.1]

    问题

    在 linux 下可通过socket.SO_BINDTODEVICE选项,将 socket 数据绑定到指定网卡,如 eth0,实现从 tun 设备读取数据,再由 eth0 发出,实验是成功的

    而在 mac 上,没有等同于socket.SO_BINDTODEVICE的选项,搜索一圈,试了各种方法后都不可用!

    试过但不可用的方法

    1. setsockopt(s, IPPROTO_IP, IP_BOUND_IF, &index, sizeof(index));
       int test_bind(int s, const char* dev){
           int index = if_nametoindex(dev);
           return setsockopt(s, IPPROTO_IP, IP_BOUND_IF, &index, sizeof(index));
       }
    
    1. bind(sock, pAdapterFound->ifa_addr, addrsize)
       int BindToDevice(int sock, int family, const char* devicename)
       {
           struct ifaddrs* pList = NULL;
           struct ifaddrs* pAdapter = NULL;
           struct ifaddrs* pAdapterFound = NULL;
           int bindresult = -1;
    
           int result = getifaddrs(&pList);
    
           if (result < 0)
               return -1;
    
           pAdapter = pList;
           while (pAdapter)
           {
               if ((pAdapter->ifa_addr != NULL) && (pAdapter->ifa_name != NULL) && (family      == pAdapter->ifa_addr->sa_family))
               {
                   if (strcmp(pAdapter->ifa_name, devicename) == 0)
                   {
                       pAdapterFound = pAdapter;
                       break;
                   }
               }
               pAdapter = pAdapter->ifa_next;
           }
    
           if (pAdapterFound != NULL)
           {
               int addrsize = (family == AF_INET6)?sizeof(struct sockaddr_in6):sizeof(struct    sockaddr_in);
               bindresult = bind(sock, pAdapterFound->ifa_addr, addrsize);
           }
    
           freeifaddrs(pList);
           return bindresult;
       }
    

    基本都是报错 Network is unreachable

    So mac, what's your problem?

    第 1 条附言  ·  2019-08-09 19:25:47 +08:00
    再描述下需求。

    设置默认路由到 utun,设置 2.2.2.2 路由到 en0,此时除 2.2.2.2 外,所有数据都经由 utun。

    utun 拿到后,根据规则一部分转发给 2.2.2.2,但另一部分,如发往 3.3.3.3 的数据包,我不想让它经由 2.2.2.2 转发,而是直接由 en0 发到 3.3.3.3,如何做到?

    另,将 3.3.3.3 再加路由表,路由到 en0 不现实,因为这样的 ip 会很多,而且规则会动态变化,动态改路由表效率太低,我希望直接在代码中指定由哪个网卡发出。

    谢谢各位大佬!
    35 条回复    2020-07-11 11:07:25 +08:00
    auser
        1
    auser  
       2019-08-09 18:53:19 +08:00 via iPhone   ❤️ 1
    楼主应该把想实现得需求描述出来。

    万一自己的理解是错的或者现在的技术方案行不通呢?
    cs010
        2
    cs010  
    OP
       2019-08-09 19:27:19 +08:00 via Android
    @auser 感谢提醒,已 append
    auser
        3
    auser  
       2019-08-09 20:44:49 +08:00 via iPhone   ❤️ 1
    需求说得好隐晦。

    建议楼主看下苹果的 Network Extension,不从路由层面入手。即便路由层面搞定了,复杂的规则,比如规律需要使用域名而非 IP,没了类似 Linux 下 dnsmasq 的 ipset,macOS 下想做这件事就困难很多了,及时性也是个问题。

    我在 mac 内核中干过这类事情,这是最佳解决方案,可以实现各种需求。不过苹果最新的系统加强了限制,驱动方向往应用层实现发展了,我估计连内核扩展的签名权限都很难申请了。

    tun 的方式建议在 Linux 下来做,转发层面可以快速出成果。
    auser
        4
    auser  
       2019-08-09 20:47:47 +08:00 via iPhone   ❤️ 1
    另外,macOS 的内核源码是开源的。这些选项建议配合源码来看,只看 socket 接口层即可。这样就内深入了解正文中的 problem 了。

    这是各非常冷门的方向,所以源码是重要的信息来源。
    cs010
        5
    cs010  
    OP
       2019-08-09 21:07:47 +08:00
    @auser 谢谢大佬!潜水这么久,第一次在 v2 提问,看到这么认真的回答,很感动。

    > tun 的方式建议在 Linux 下来做,转发层面可以快速出成果。

    为什么说在 mac,因为我想做的是跨平台,tun 在 linux,mac,win 都有 tun 的实现,linux 已验证、mac 正在验证、win 待验证。

    > 即便路由层面搞定了,复杂的规则,比如规律需要使用域名而非 IP

    是的,根据域名本来也是考虑范围,但是鉴于在 tun 层只能拿到 ip,只在 tun 层是很难搞定的,遂放低优先级。

    > 这些选项建议配合源码来看,只看 socket 接口层即可

    谢谢,我会去尝试深入

    但在此之前,我仍希望得到,像你这样热心的大佬们的指点,少走弯路 :)
    cs010
        6
    cs010  
    OP
       2019-08-09 21:32:18 +08:00
    @auser 补充一句,路由表目前在我的需求中,只是对数据包分流进行了粗分,我想在 tun 层进行细分
    gggxxxx
        7
    gggxxxx  
       2019-08-09 21:58:12 +08:00
    网络开放方面 mac 比 linux 舒服多了。

    如果是做 vpn 类似的东西,直接 pf (packet filter) 1 句话就搞定了。
    例子: "nat on utun2 from en5:network to any -> (utun2)"

    如果是多网卡情况下单纯指定某个物理网卡,直接 socket 的 bind 语句就行了。第二个参数填指定的物理网卡。
    cs010
        8
    cs010  
    OP
       2019-08-09 22:09:21 +08:00 via Android
    @gggxxx 感谢回复,无论直接 bind IP,还是在正文中,第二个不可用方法提到过的 bind,都是反复测试后,不可用的
    cs010
        9
    cs010  
    OP
       2019-08-09 22:14:46 +08:00 via Android
    @gggxxxx 而且,你可能指的是接收数据绑定,我这里是发送数据绑定哈。
    换句话说,虽然我默认把 2.2.2.2 路由到 utun,但是从 utun 拿到目的地 2.2.2.2 的数据后,我现在想指定 2.2.2.2 的数据由 en0 发送而不是 utun
    gggxxxx
        10
    gggxxxx  
       2019-08-09 22:20:42 +08:00
    1. pf 就是 linux 上的 iptables。难道还有什么需求是做不了?
    2. 你使用 socket 的 bind 的姿势不对,当然不会成功。我很多年前的代码在今天依然正常工作。
    cs010
        11
    cs010  
    OP
       2019-08-09 22:43:38 +08:00 via Android
    @gggxxxx pf 只做映射当然可以,但是要动态映射,动态指定网卡,不知道可否。这里主要问题不是流量能不能走到 utun,而是走到 utun,我拿到数据,怎么再决定它由 en0 而不是 utun 发到外网
    gggxxxx
        12
    gggxxxx  
       2019-08-09 23:04:02 +08:00
    你这不就是典型的 vpn 模型嘛。不知道你在纠结啥。
    pf 资料搜搜一大把,程序内动态应用新的 pf 规则不就行了。
    auser
        13
    auser  
       2019-08-09 23:57:25 +08:00 via iPhone
    使用 Network Extension 是苹果平台的最佳选择,手机与电脑都可以用。TUN 需要 root 权限,如果未来支持手机端这会是个问题。苹果那个开源的 tuntap 驱动跟 windows 在实现上是有的区别的。这方面建议了解下 open 开头那个产品文档,tun 方面有说明。

    至于在 tun 层细分,那么打算如何把截获的 IP 报文重新“插入”道协议栈然后送到二层继续处理?此外,IP 报文一定要处理分片的情况,那么这两处需要在预研阶段就确认,否则做出来的程序是有致命缺陷的。至于后续的 TCP 握手阶段 MSS,ICMP 层的一些协议,都是后续会发现的“坑”。

    等等等等细节有些多。题外话:为曾经的思路是在终端设备上搞,后来直接在网关上弄了,这样我在电视上都能看黑镜了,游戏主机也可以直接玩。然后还能给家里设备分到 ipv6 的地址只在在外网访问。如果楼主不是给公司做,建议在网关设备上摸索摸索。一劳永逸。

    祝玩得开心。
    cs010
        14
    cs010  
    OP
       2019-08-10 13:18:54 +08:00
    @gggxxxx 纠结之处在于,能动态修改但要高效,所以没有尝试去动态修改路由表(否则直接启进程调用 pf 就行了)。在 linux 中,简单的 setopt SO_BINDTODEVICE 参数就行,这也是最佳方案,简单高效,但在 mac,无法直接 bind。你提到说 bind 是可行的,我的姿势不对,不知道你是否方便分享一下你的代码片段,非常感谢!
    另外,我查了 pf 应该是可编程动态修改规则的,但又担心效率,我去试试。![man pf]( https://man.openbsd.org/pf) ![EXAMPLES]( https://man.openbsd.org/pf#EXAMPLES),如果 ok,我会 append
    cs010
        15
    cs010  
    OP
       2019-08-10 13:41:50 +08:00
    @auser 是的,分流的问题 linux 有 SO_BINDTODEVICE,Android 有 VPNService 的 protect,windows 似乎可以直接 bind,macos、ios 的无 root 通用方法似乎只有你提到的 Network Extension。

    现在,我的其它部分差不多都 ok 了,不分流的情况下已经可以正常稳定的跑起来了,主要是分流的问题。协议层的处理已经用 lwip 完成。总的来说就是,由 lwip 完成 tcp 协议的事情,拿到数据后,再交由代理模块处理,代理模块决定走代理,还是直接选择 en0 发出去。另外 mac 的 tun 的操作,是借鉴 openvpn 和 github 上的 water 项目。

    跨平台对我是必须要做的,不然不管 linux 的 tun/tap 还是 socket,甚至是 raw socket 都很方便,一路研究过来,坑比较少。
    gggxxxx
        16
    gggxxxx  
       2019-08-10 15:45:13 +08:00
    @cs010 实在无法理解你的纠结点......也无法理解你的思路。要不你从普通用户角度描述下你这个东西的具体作用和功能


    你顶楼说的情况,就一个物理网卡和 utun 而已。utun 唯一的作用是收集发出去的网络数据而已。真正对外收发数据的只有唯一的物理网卡。这不就是一个 nat 搞定的事么?
    再啰嗦一点,前面有人提到的 Network Extension,实际上就是把 pf 的操作包装了下。
    1. 创建一个 utun。2. pf 设置下 nat 规则
    如果你是做常规 vpn,你只需要做这 2 个动作就足够了。你只有一个物理网卡,你希望 bind 的意义在哪里?
    cs010
        17
    cs010  
    OP
       2019-08-10 19:48:43 +08:00 via Android
    @gggxxxx 你没理解到我的意思,我这里的关键是 utun 拿到数据后,要动态路由的问题

    考虑这个场景,假设我要全局劫持流量

    1,设置默认路由到 utun,所有流量到 utun
    2,从 utun 获取 IP 数据包,通过 lwip 获取 TCP 数据 data 后,然后不想通过 lwip 再次封装为 IP 包,而是直接通过 socket 将 data 发出去。这时问题是,默认路由是到 utun 的,如果默认路由发 data 出去,势必又会到 utun,造成死循环,为解决这个问题,可能的几种方法:

    1,上面提到的 socket 选项,但只有 Linux 支持,mac 没有

    2,是否可通过上面提到的 bind 等方案,我自己试验不行,如果你说可以,可否麻烦分享一下你的代码片段

    3,每次发送 data 时,动态更改路由(或者用 pf 等),发送完后,再改回来。这里是重点,决定了你的方案不可用,虽然我这次发送,决定走代理出去,但下次同一个目的 IP 数据包,我可能想让它通过 en0 出去,所以我不能把路由表或者 pf 的规则写死为 en0,而是动态的,这就需要效率,如果 man pf(4)的方法效率没问题,那可以考虑动态更改。

    如果不考虑需求,其实很简单可以描述,mac 上的 socket 是否有和 Linux 一样的选项,我可以指定这个 socket 发送数据时,由哪个网卡发出去,当然前提是我要动态和效率,不要写死路由表或者 pf 规则。如代码中,这样写 socket.sendto(data, 任意网卡名)

    我不知道这样说,你了解了吗?
    gggxxxx
        18
    gggxxxx  
       2019-08-11 07:24:10 +08:00 via iPhone
    @cs010 怎么又有代理的东西了?
    你方便的话直接明盘具体功能细节吧。

    1. utun 是 ip 数据包,是非常低的网络层了。你把 IP 数据包还原成 tcp 这步我看不懂思路和目的,这不是豆腐做成肉价钱了吗?要 tcp 数据,直接 socks proxy 不就行了吗?
    2. nat 规则写好了,是等同 bind 指定网卡的。你的情况只有一个真实有效的网络卡,怎么还会有所谓动态调整的需求?你先不要考虑你所谓的“自研路由代理”一类的东西,就单纯说一个 utun 和一个真实网卡这种模型,数据只能从物理网卡走如果想正常上网的话。
    Srar
        19
    Srar  
       2019-08-11 13:21:26 +08:00   ❤️ 1
    再来个 Pcap ?初始化 Pcap 时候指定网卡对外发包
    cs010
        20
    cs010  
    OP
       2019-08-11 14:14:53 +08:00 via Android
    @gggxxxx
    我觉得我解释得应该够清楚了:)

    1,我要全局代理可以用 tun 劫持全局流量对吧,我要在 Android 上全局代理,必须用 vpnservice,在 IP 层,为了平台通用性肯定选型在 IP 层,没问题吧。想改 TCP 数据,必须读取 TCP 数据,改完了,想发出去而不用再次封为 IP 报文,肯定用 socket 直接转发 TCP 数据啊

    2,往外发肯定走物理网卡毫无疑问,但是我要劫持流量啊,当然要用 tun 拿到全局流量,再往外发呀

    各种 socks 代理当然可以代理劫持,关键是,全局劫持,通用性,所以选 tun,在 IP 层做了呀。

    其实需求不用质疑,没问题,这里唯一的问题就是怎么用 socket 指定网卡发送的问题。

    你可以把问题简化为,mac 上,两个物理网卡,socket 怎么选任意网卡往外发的问题,只有这个问题
    gggxxxx
        21
    gggxxxx  
       2019-08-11 22:43:05 +08:00
    @cs010 我知道你的问题所在了。
    Mac 下的 socket 用 bind 指定 interface 的代码网上随便搜。我之前回帖说过,就是第二个参数填指定的 interface。
    你这问题 pf 轻松解决,常用做法就是编辑 pf 规则根据不同 ip source 做不同处理,还是不理解你为啥要舍近求远。还有就是为啥又要修改 tcp 数据.......修改 tcp 数据的什么内容呢?很像是国内搞酸酸乳圈子里那种思路,我个人觉得意义不是很大。
    cs010
        22
    cs010  
    OP
       2019-08-11 23:22:46 +08:00 via Android
    @gggxxxx 我已经 Google 过 n 次了,几乎能找到的方法都试过,我主题里面也提到过,都不可用,你说的随便搜,不礼貌的说法,有点想当然了,不管如何,谢谢你
    jedihy
        23
    jedihy  
       2019-08-12 00:03:56 +08:00 via iPhone
    你 bind 到指定网卡的 IP 不行吗?
    cs010
        24
    cs010  
    OP
       2019-08-12 09:20:52 +08:00 via Android
    @jedihy Of course not
    jedihy
        25
    jedihy  
       2019-08-12 09:58:19 +08:00
    @cs010 为什么
    cs010
        26
    cs010  
    OP
       2019-08-12 11:03:36 +08:00
    @jedihy 因为你没试过
    jedihy
        27
    jedihy  
       2019-08-12 13:01:06 +08:00 via iPhone
    @cs010 不明白你在说什么
    cs010
        28
    cs010  
    OP
       2019-08-12 16:59:44 +08:00
    @Srar 如果 socket 没法直接指定,libpcap 是个替代,但需要再重新把 tcp 数据封装为 ip 报文,太重了,会疯滴
    njzy
        29
    njzy  
       2019-08-12 19:16:14 +08:00 via Android
    利用 tun 实现透明代理 部分软件直连?
    FH0
        30
    FH0  
       2020-03-15 08:42:21 +08:00
    @cs010 楼主,绑定网卡是需要 root 权限的,跨平台的话不怎么行。还有我注意到你说用 lwip 接收来自 tun 的 tcp 数据,然后用 socket 重新写回 tun,这个怎么实现?
    cs010
        31
    cs010  
    OP
       2020-03-15 11:52:34 +08:00 via Android
    @FH0 不是 socket 直接写回 tun,与 tun 的所有数据交互,都是通过 lwip 完成的
    FunnyCat
        32
    FunnyCat  
       2020-05-21 15:28:24 +08:00
    @cs010 不知道楼主现在问题解决了没有,有机会的话可以沟通交流一下。我做了一个跨平台的 VPN 软件,其中很多遇到的坑应该都和你一样踩过。看了很多回复,很多人真的太过于想当然了。从路由层面上来说,修改路由表是跨平台方案绝对不可行的一条路,在 ios/android 平台基本上属于死路。首先 VPN 的话,如果想做到尽可能多的事情,Android 的 VPNService,macos/ios 的 Network Extension 或 Network system Extension(PacketTunnelProvider ),windows 的 Wintun 都是不错的选择。至于在 default route 走 tun 情况下,其他流量怎么出去;首先系统为了防止到 VPN server 不再命中 default ,都提供了一种添加明细的方式(当然,大部分都是开启时指定,不能动态修改),如 Android 里 VPNService 的 protect, macos/ios 里 NE 的 SeverIP/Exculde routes 等可以帮助你做到这点。如果不满足的话,就像你说的必须经过协议栈(lwip,实际上有一种巧妙的方法可以让系统协议栈帮我们做这件事而不用嵌入用户态协议栈)拿到报文,你自己作为一个 TCP/UDP 代理再绑定接口把包发出去。socket.SO_BINDTODEVICE 这个选项如果不行,你可以直接绑定接口的 src ip 尝试一下,我实测两种方式都是 work 的,后者怀疑是 macos 有相关的策略路由 :)
    cs010
        33
    cs010  
    OP
       2020-06-19 18:14:44 +08:00
    @FunnyCat
    感谢你的认真回复!
    前段时间一直有事暂时放下了,后面有空会继续

    > 修改路由表是跨平台方案绝对不可行的一条路

    是的,修改路由表只是为了,在桌面上劫持全局流量.移动端如 Android 本身就是默认全局劫持的,不用修改路由表


    > 首先系统为了防止到 VPN server 不再命中 default ,都提供了一种添加明细的方式

    我了解到 android 有 protect,在桌面端除了 socket.SO_BINDTODEVICE 来绑定指定网卡发数据,没找到其它方法,而且 mac 居然不支持,这也是发本帖的原因


    >实际上有一种巧妙的方法可以让系统协议栈帮我们做这件事而不用嵌入用户态协议栈

    方便放些资料吗?是不是个平台都可以实现呢?
    FunnyCat
        34
    FunnyCat  
       2020-06-29 19:44:51 +08:00
    @cs010
    利用系统协议栈收的方法是这样的,默认全平台是由三层的 tun 设备来引流的情况下,tun 上的 ip 我们可以利用。在 tun 上这个 ip 起 tcp 和 udp 的 server,外部命中 tun 路由的流量首先会被你从 tun 上 read 到,通过读到的 ip 报文修改目的 ip 和目的端口号(当然,包括首部检验和),将其修改为你 tun 上的端口和地址再写回 tun 。然后系统自然就会帮你组包,你就可拿到原始数据,再通过对应的 tcp 或 udp socket 绑定接口往外发(当然,你还得记录这些数据是送往哪个地址哪个端口的)。

    当然,这种能力还是需要绑定接口不命中默认路由的。这是两个问题,一个是 tcp 和 udp 数据处理重组的问题,因为你在移动端发出去一定会带额外的头部。 另一个是不命中路由的这个问题, 这个 socket.SO_BINDTODEVICE 是可行的,你可以再试试。
    cs010
        35
    cs010  
    OP
       2020-07-11 11:07:25 +08:00 via Android
    @FunnyCat 的确是个好思路,bind src ip 在 mac 上试过,能生效的前提是,dst ip 在路由表里本身能路由到 src ip 绑定的网卡,但是这显然不是和 protect 一样的效果,不知道你那边是 ok 的吗
    另外这是我的微信,大佬方便的话可以加一下,我们可以交流一下 base64: Y3NjczAxMDAxMAo=
    邮箱 base64:Y3MwMTBAaG90bWFpbC5jb20K
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3525 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 00:09 · PVG 08:09 · LAX 17:09 · JFK 20:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.