Linux

強製程序綁定到介面,而不是 IP 地址

  • May 11, 2021

我有一台帶有兩個網路介面和兩個不同網際網路連接的機器。我知道有多個路由表和類似的東西。但是我有一個非常簡單的場景。傳出的 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-addressssh 相同的選項-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 而不是綁定介面。

感謝@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不是永久性的。這是另一個話題。這樣做只是為了說明為什麼需要更長的時間。

我認為您正在尋找 (Linux-only) 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'

引用自:https://unix.stackexchange.com/questions/648718