Программное обеспечение Force связывается с интерфейсом, а не с 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 мог бы решить эту проблему, но, как обсуждалось в другой теме, у него все еще есть ошибка, не работающая таким образом через Wi-Fi.

Так как можно действительно заставить приложение использовать конкретный интерфейс без всей этой сложной маршрутизации, netns или правил iptables? LD_PRELOAD выглядит очень многообещающе, однако пока что этот код сосредоточен только на изменении привязанного IP, а не интерфейса.

Я думаю, вы ищете (только для Linux) SO_BINDTODEVICE. Из документации man 7 socket:

 SO_BINDTODEVICE
      Привязать этот сокет к определенному устройству, например, “eth0”, как указано в переданном
      имени интерфейса. Если имя является пустой строкой или длина опции равна нулю,
      привязка сокета к устройству удаляется. Переданная опция — это строка имени интерфейса переменной длины, заканчивающаяся нулем, с максимальным размером IFNAMSIZ.
      Если сокет привязан к интерфейсу, только пакеты, полученные с этого конкретного
      интерфейса, обрабатываются сокетом. Обратите внимание, что это работает только для некоторых типов сокетов, в частности для сокетов AF_INET. Это не поддерживается для пакетных сокетов (используйте обычный bind(2) в этом случае).

Вот пример программы, которая его использует:

#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, "Не удалось подключиться к серверу...\n");
        return EXIT_FAILURE;
    }

    // Выполнить HTTP-запрос к 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("Ответ: '%s'\n", buffer);

    close(sockfd);
    return EXIT_SUCCESS;
}

Программа использует SO_BINDTODEVICE, чтобы привязать один из моих сетевых интерфейсов (enp0s3). Затем она подключается к одному из серверов Google, выполняет простой HTTP-запрос и выводит первые несколько байт ответа.

Вот пример выполнения:

$ ./a.out
Ответ: 'HTTP/1.1 200 OK'

Спасибо @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>

//Кредиты идут на https://catonmat.net/simple-ld-preload-tutorial и https://catonmat.net/simple-ld-preload-tutorial-part-two
//И, конечно, на https://unix.stackexchange.com/a/648721/334883

//Компилируйте с помощью gcc -nostartfiles -fpic -shared bindInterface.c -o bindInterface.so -ldl -D_GNU_SOURCE
//Используйте с BIND_INTERFACE=<сетевой интерфейс> LD_PRELOAD=./bindInterface.so <ваша программа>, например, 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" печатает дважды "MySocket". Первое это для DNS-запроса. 
                            //Если ваш первый DNS-сервер недоступен через привязанный интерфейс, 
                            //он попробует следующий DNS-сервер, пока не удастся или не возникнет ошибка разрешения имени. 
                            //Именно поэтому это может занять значительно больше времени, чем 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("Предупреждение: Программа с LD_PRELOAD запущена, но переменная окружения BIND_INTERFACE не установлена\n");
        fprintf(stderr,"Предупреждение: Программа с LD_PRELOAD запущена, но переменная окружения BIND_INTERFACE не установлена\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

Примечание: выполнение этого может занять гораздо больше времени, чем с curl --interface wlan0 ifconfig.me
Это потому, что он также пытается достичь вашего первого DNS-сервера из /etc/resolv.conf с привязанным интерфейсом. Если этот DNS-сервер недоступен, он использует второй и так далее. Если вы отредактируете /etc/resolv.conf и поставите, например, публичный DNS-сервер Google 8.8.8.8 на первое место, это будет так же быстро, как версия curl. С опцией --interface curl привязывается только к этому интерфейсу при установлении фактического соединения, а не при разрешении IP-адреса. Таким образом, при использовании curl с привязанным интерфейсом и приватной VPN он будет утекать 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); когда #include <arpa/inet.h> не включен
        char *dest = inet_ntoa(socketAddress->sin_addr); //с #include <arpa/inet.h>
        printf("подключаемся к: %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("Привязанный интерфейс: %s\n",boundInterface.ifr_name);

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

Используйте те же параметры, что и выше для компиляции. Используйте с

LD_PRELOAD=./bindInterface.so curl --interface wlan0 ifconfig.me
подключаемся к: 192.168.178.1 / Привязанный интерфейс: none
подключаемся к: 34.117.59.81 / Привязанный интерфейс: wlan0
185.107.XX.XX

Примечание 2: Изменение /etc/resolv.conf не является постоянным. Это другая тема. Это было сделано только для демонстрации, почему это занимает больше времени.

Я нашел это очень полезным и создал репозиторий и предварительно скомпилированное!

https://github.com/peppergrayxyz/bind2device

Может быть, Google приведет кого-то еще сюда (:

Ответ или решение

Для выполнения заданной задачи привязки программы к определённому сетевому интерфейсу в Linux, не прибегая к сложным маршрутизационным или iptables-правилам, можно использовать опцию SO_BINDTODEVICE. Эта функция позволяет привязывать сокет к конкретному сетевому интерфейсу, что делает её особенно полезной в вашем случае.

Что такое SO_BINDTODEVICE

SO_BINDTODEVICE — это опция для сокетов, которая позволяет привязать сокет к определённому интерфейсу, например, wlan0 или eth0. Это значит, что все пакеты, поступающие и исходящие через этот сокет, будут обрабатываться только через указанный интерфейс. Данная опция особенно актуальна для сокетов типа AF_INET (IPv4).

Реализация

Вы можете использовать следующий код, чтобы создать программу, которая будет использовать SO_BINDTODEVICE для привязки сокетов к заданному интерфейсу:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>

int main(void)
{
    const int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return EXIT_FAILURE;
    }

    const char* interface_name = "wlan0"; // Укажите нужный интерфейс
    struct ifreq ifr;

    strncpy(ifr.ifr_name, interface_name, IFNAMSIZ - 1);
    if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) {
        perror("setsockopt");
        return EXIT_FAILURE;
    }

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr("142.250.73.196"); // IP сервера
    servaddr.sin_port = htons(80); // Порт

    if (connect(sockfd, (const struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
        fprintf(stderr, "Не удалось подключиться к серверу...\n");
        return EXIT_FAILURE;
    }

    // Отправка HTTP-запроса
    dprintf(sockfd, "GET / HTTP/1.1\r\n");
    dprintf(sockfd, "HOST: www.google.com\r\n");
    dprintf(sockfd, "\r\n");

    char buffer[256] = {};
    read(sockfd, buffer, sizeof(buffer) - 1);
    printf("Ответ: '%s'\n", buffer);

    close(sockfd);
    return EXIT_SUCCESS;
}

Использование LD_PRELOAD

Если вам необходимо привязать любую существующую программу к определённому интерфейсу, вы можете использовать механизм LD_PRELOAD. Это позволит вам подменять функции, такие как socket(), чтобы они автоматически привязывались к нужному интерфейсу. Вот пример кода, который вы можете использовать:

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

int socket(int domain, int type, int protocol) {
    int (*original_socket)(int, int, int);
    original_socket = dlsym(RTLD_NEXT, "socket");
    int fd = original_socket(domain, type, protocol);

    if (fd < 0) {
        return fd; // Возвращаем ошибку, если создание сокета не удалось
    }

    const char* bind_interface = getenv("BIND_INTERFACE");
    if (bind_interface) {
        struct ifreq ifr;
        strncpy(ifr.ifr_name, bind_interface, IFNAMSIZ);
        if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) {
            perror("setsockopt");
            return -1;
        }
    }

    return fd;
}

Сборка и использование

Скомпилируйте код с помощью следующей команды:

gcc -fPIC -shared -o bindInterface.so bindInterface.c -ldl

Запустите ваше приложение с LD_PRELOAD, указывая интерфейс:

BIND_INTERFACE=wlan0 LD_PRELOAD=./bindInterface.so wget -O- ifconfig.me

Заключение

Используя SO_BINDTODEVICE и механизм LD_PRELOAD, вы сможете привязать любую программу к конкретному сетевому интерфейсу без необходимости в сложных маршрутах или дополнительных настройках. Это значительно упрощает задачу настройки сетевых приложений на многопрофильных серверах.

Оцените материал
Добавить комментарий

Капча загружается...