强制程序绑定到接口,而不是 IP 地址

强制程序绑定到接口,而不是 IP 地址

我有一台带有两个网络接口和两个不同互联网连接的机器。我知道有多个路由表之类的东西。不过我有一个非常简单的场景。传出 ssh 应用程序应始终通过 wlan0。那么为什么要做这么复杂的事情呢?

第一次使用curl 进行测试,它的工作非常完美:

curl --interface wlan0 ifconfig.me
185.107.XX.XX

curl --interface eth0 ifconfig.me
62.226.XX.XX

因此,无需为两个接口设置任何特殊的路由规则,它就完全按照我想要的方式工作。 eth0 是默认路由

ip route
default via 192.168.178.1 dev eth0 proto dhcp src 192.168.178.21 metric 202
default via 172.16.1.1 dev wlan0 proto dhcp src 172.16.1.88 metric 303
172.16.1.0/24 dev wlan0 proto dhcp scope link src 172.16.1.88 metric 303
192.168.178.0/24 dev eth0 proto dhcp scope link src 192.168.178.21 metric 202

现在尝试用 wget 做同样的事情。 Wget 非常适合调试,因为它具有--bind-address与 ssh 相同的选项-b

wget -O- --bind-address=192.168.178.21 ifconfig.me 2> /dev/null
62.226.XX.XX

省略时您会得到相同的输出--bind-address

wget -O- --bind-address=172.16.1.88 ifconfig.me 2> /dev/null

这个命令只会挂起大约 9 (!) 分钟,最后不会输出任何内容,就像 ssh 所做的那样。

我知道这个将unix程序绑定到特定的网络接口线。然而,即使标题是“将 unix 程序绑定到特定网络接口”,所有使用 LD_PRELOAD 的解决方案都会绑定到 IP 地址。 ssh 已经支持此功能,但在这里没有帮助。 Firejail 可以解决这个问题,但正如其他主题中所解释的那样,仍然存在无法通过 Wifi 工作的错误。

那么,如何才能真正强制应用程序使用特定接口,而不需要所有复杂的路由、netns 或 iptables 规则呢? LD_PRELOAD 看起来非常有前途,但是到目前为止,此代码仅关注更改绑定 IP 而不是绑定接口。

答案1

我认为您正在寻找(仅限 Linux)SO_BINDTODEVICE。从man 7 socket

 SO_BINDTODEVICE
      Bind this socket to a particular device like “eth0”, as specified in the passed
      interface name.  If the name is an empty string or the option length is zero,
      the socket device binding is removed. The passed option is a variable-length
      null-terminated interface name string with the maximum size of IFNAMSIZ.
      If a socket is bound to an interface, only packets received from that particular
      interface are processed by the socket.  Note that this works only for some socket
      types, particularly AF_INET sockets. It is not supported for packet sockets (use
      normal bind(2) there).

这是使用它的示例程序:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <net/if.h>

int main(void)
{
    const int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    if (sockfd < 0) {
        perror("socket");
        return EXIT_FAILURE;
    }

    const struct ifreq ifr = {
        .ifr_name = "enp0s3",
    };

    if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) {
        perror("setsockopt");
        return EXIT_FAILURE;
    }

    const struct sockaddr_in servaddr = {
        .sin_family      = AF_INET,
        .sin_addr.s_addr = inet_addr("142.250.73.196"),
        .sin_port        = htons(80),
    };

    if (connect(sockfd, (const struct sockaddr*) &servaddr, sizeof(servaddr)) < 0) {
        fprintf(stderr, "Connection to the server failed...\n");
        return EXIT_FAILURE;
    }

    // Make an HTTP request to Google
    dprintf(sockfd, "GET / HTTP/1.1\r\n");
    dprintf(sockfd, "HOST: www.google.com\r\n");
    dprintf(sockfd, "\r\n");

    char buffer[16] = {};
    read(sockfd, buffer, sizeof(buffer) - 1);

    printf("Response: '%s'\n", buffer);

    close(sockfd);
    return EXIT_SUCCESS;
}

该程序用于SO_BINDTODEVICE绑定我的网络接口之一 ( enp0s3)。然后它连接到 Google 的一台服务器并发出简单的 HTTP 请求并打印响应的前几个字节。

这是一个示例运行:

$ ./a.out
Response: 'HTTP/1.1 200 OK'

答案2

感谢@Andy Dalton 提供了这些有用的信息。基于此,我为 LD_PRELOAD 编写了一小段代码,以将 SO_BINDTODEVICE 实现到每个程序。

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <dlfcn.h>
#include <net/if.h>
#include <string.h>
#include <errno.h>

//Credits go to https://catonmat.net/simple-ld-preload-tutorial and https://catonmat.net/simple-ld-preload-tutorial-part-two
//And of course to https://unix.stackexchange.com/a/648721/334883

//compile with gcc -nostartfiles -fpic -shared bindInterface.c -o bindInterface.so -ldl -D_GNU_SOURCE
//Use with BIND_INTERFACE=<network interface> LD_PRELOAD=./bindInterface.so <your program> like curl ifconfig.me

int socket(int family, int type, int protocol)
{
    //printf("MySocket\n"); //"LD_PRELOAD=./bind.so wget -O- ifconfig.me 2> /dev/null" prints two times "MySocket". First is for DNS-Lookup. 
                            //If your first nameserver is not reachable via bound interface, 
                            //then it will try the next nameserver until it succeeds or stops with name resolution error. 
                            //This is why it could take significantly longer than curl --interface wlan0 ifconfig.me
    char *bind_addr_env;
    struct ifreq interface;
    int *(*original_socket)(int, int, int);
    original_socket = dlsym(RTLD_NEXT,"socket");
    int fd = (int)(*original_socket)(family,type,protocol);
    bind_addr_env = getenv("BIND_INTERFACE");
    int errorCode;
    if ( bind_addr_env!= NULL && strlen(bind_addr_env) > 0)
    {
        //printf(bind_addr_env);
        strcpy(interface.ifr_name,bind_addr_env);
        errorCode = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &interface, sizeof(interface));
        if ( errorCode < 0)
        {
            perror("setsockopt");
            errno = EINVAL;
            return -1;
        };
    }
    else
    {
        printf("Warning: Programm with LD_PRELOAD startet, but BIND_INTERFACE environment variable not set\n");
        fprintf(stderr,"Warning: Programm with LD_PRELOAD startet, but BIND_INTERFACE environment variable not set\n");
    }

    return fd;
}

编译它

gcc -nostartfiles -fpic -shared bindInterface.c -o bindInterface.so -ldl -D_GNU_SOURCE

与它一起使用

BIND_INTERFACE=wlan0 LD_PRELOAD=./bindInterface.so wget -O- ifconfig.me 2>/dev/null

注意:执行此操作可能比使用 This 花费更长的时间,因为它还尝试从绑定的接口curl --interface wlan0 ifconfig.me 到达您的第一个名称服务器。/etc/resolv.conf如果无法访问该名称服务器,则会使用第二个名称服务器,依此类推。例如,如果您编辑/etc/resolv.conf并将 Google 的公共 DNS 服务器 8.8.8.8 放在第一位,则它与 curl 版本一样快。使用--interface选项时,curl 仅在进行实际连接时绑定到此接口,而不是在解析 IP 地址时绑定到此接口。因此,当使用带有绑定接口和隐私VPN的curl时,如果配置不正确,它将通过正常连接泄漏DNS请求。使用此代码进行验证:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <dlfcn.h>
#include <net/if.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>

int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
    int *(*original_connect)(int, const struct sockaddr*, socklen_t);
    original_connect = dlsym(RTLD_NEXT,"connect");

    static struct sockaddr_in *socketAddress;
    socketAddress = (struct sockaddr_in *)addr;

    if (socketAddress -> sin_family == AF_INET)
    {
        // inet_ntoa(socketAddress->sin_addr.s_addr); when #include <arpa/inet.h> is not included
        char *dest = inet_ntoa(socketAddress->sin_addr); //with #include <arpa/inet.h>
        printf("connecting to: %s / ",dest);
    }

    struct ifreq boundInterface = 
        {
            .ifr_name = "none",
        };
    socklen_t optionlen = sizeof(boundInterface);
    int errorCode;
    errorCode = getsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &boundInterface, &optionlen);
    if ( errorCode < 0)
    {
        perror("getsockopt");
        return -1;
    };
    printf("Bound Interface: %s\n",boundInterface.ifr_name);

    return (int)original_connect(sockfd, addr, addrlen);    
}

使用与上面相同的选项进行编译。与使用

LD_PRELOAD=./bindInterface.so curl --interface wlan0 ifconfig.me
connecting to: 192.168.178.1 / Bound Interface: none
connecting to: 34.117.59.81 / Bound Interface: wlan0
185.107.XX.XX

注2:改变/etc/resolv.conf不是永久性的。这是另一个话题了。这样做只是为了证明为什么需要更长的时间。

相关内容